"""进程内熔断门: CHS gate 契约的内存实现(D3 双后端)。

状态机蓝本 VT `adapters/breaker.py`(闭路→阈值开路→冷却半开→单探针),
契约形态承 CHS `provider_gate.py`: 半开探针是**带 TTL 的租约**(持有者
死亡后可被接管,防"探针永远在路上"死锁),写回经 epoch fencing 拒绝
旧世代污染。epoch 在每次进入 OPEN 时递增。时钟构造注入,纯确定性可测。

M2.5 双通道(设计 2026-07-21-m25 §3.1): 在连续失败通道之外加失败率
通道(双 30s 桶窗口,样本 ≥ min_calls 且失败率 ≥ fail_rate 即开路);
429(rate_limited)是背压不是故障,两通道均不计;开路时长按 reopen_streak
指数递增封顶 max_cooldown_s——连续通道触发的开路不递增 streak(误熔
健康源的代价封顶为单次 cooldown_s)。
"""

from __future__ import annotations

import time
from dataclasses import dataclass, field
from typing import TYPE_CHECKING

from polygateway.ports import GateDecision, GateState, GateUpdate

if TYPE_CHECKING:
    from collections.abc import Callable

    from polygateway.types import BreakerConfig


@dataclass
class _SourceGate:
    """单源的门控可变状态(窗口/退避域见设计 §3.1 状态机域清单)。"""

    state: GateState = GateState.CLOSED
    fails: int = 0
    epoch: int = 0
    open_until: float = 0.0
    probe_owner: str | None = None
    probe_expires: float = 0.0
    reasons: dict[str, str] = field(default_factory=dict)
    # 失败率窗口: 双半窗桶轮换(win_id = now // (window_s/2))
    win_id: int = -1
    a0: int = 0  # 当前桶 attempts / failures
    f0: int = 0
    a1: int = 0  # 上一桶
    f1: int = 0
    reopen_streak: int = 0
    closed_since: float | None = None


class InMemoryGate:
    """按 source_name 分别计数的进程内熔断门。"""

    def __init__(self, *, config: BreakerConfig, now: Callable[[], float] = time.monotonic) -> None:
        self._cfg = config
        self._now = now
        self._gates: dict[str, _SourceGate] = {}

    def _gate(self, source_name: str) -> _SourceGate:
        if not source_name.strip():
            raise ValueError("source_name 不能为空")
        return self._gates.setdefault(source_name, _SourceGate())

    # —— M2.5 失败率窗口 ——

    def _rotate_window(self, g: _SourceGate) -> None:
        """按半窗粒度轮换双桶;跨两桶以上的空窗直接清零。"""
        half = self._cfg.window_s / 2.0
        wid = int(self._now() // half)
        if wid == g.win_id:
            return
        if wid == g.win_id + 1:
            g.a1, g.f1 = g.a0, g.f0
        else:
            g.a1, g.f1 = 0, 0
        g.a0, g.f0 = 0, 0
        g.win_id = wid

    def _window_add(self, g: _SourceGate, *, failed: bool) -> None:
        self._rotate_window(g)
        g.a0 += 1
        if failed:
            g.f0 += 1

    def _window_evidently_healthy(self, g: _SourceGate) -> bool:
        """窗口样本充足且失败率低于阈值 = 有充分健康证据(迭代 6 抑制判据)。"""
        self._rotate_window(g)
        attempts = g.a0 + g.a1
        if attempts < self._cfg.min_calls:
            return False
        return (g.f0 + g.f1) / attempts < self._cfg.fail_rate

    def _rate_channel_open(self, g: _SourceGate) -> bool:
        self._rotate_window(g)
        attempts = g.a0 + g.a1
        if attempts < self._cfg.min_calls:
            return False
        return (g.f0 + g.f1) / attempts >= self._cfg.fail_rate

    def _cooldown_eff(self, g: _SourceGate) -> float:
        streak = max(1, g.reopen_streak)
        return min(self._cfg.cooldown_s * (2 ** (streak - 1)), self._cfg.max_cooldown_s)

    def _remaining(self, g: _SourceGate) -> float:
        """距离**确定**可再试的时刻还有多久(issue #14 的契约定义)。

        OPEN 的冷却截止是确定时刻;HALF_OPEN 下探针随时可能出结果,**不存在**
        确定时刻,故 `0.0`——`0 = 可立即重试` 是库既有约定。此前这里返回探针
        租约剩余,而租约长度是死锁保护参数(派生自 `2 × 最慢源 timeout`),与
        "源多久能恢复"无因果关系;它还被喂进源冷却备忘,而备忘 `set_until`
        取更晚者不可回退,于是门恢复 CLOSED 后本进程仍跳过该源整整一个租约。

        三个出口(`try_enter` 拒绝、`_snapshot`、`retry_after_s`)共用本方法,
        避免同一语义在三处各算一遍而漂移。
        """
        if g.state is GateState.OPEN:
            return max(0.0, g.open_until - self._now())
        return 0.0

    def _grant_probe(self, g: _SourceGate, source_name: str, owner: str) -> GateDecision:
        g.state = GateState.HALF_OPEN
        g.probe_owner = owner
        g.probe_expires = self._now() + self._cfg.probe_ttl_s
        return GateDecision(
            source_name=source_name,
            allowed=True,
            state=GateState.HALF_OPEN,
            epoch=g.epoch,
            is_probe=True,
            probe_owner=owner,
            retry_after_s=0.0,
        )

    async def try_enter(self, source_name: str, owner: str) -> GateDecision:
        """健康普通准入;冷却到期/探针租约过期时原子授予唯一探针。"""
        if not owner.strip():
            raise ValueError("owner 不能为空")
        g = self._gate(source_name)
        now = self._now()
        if g.state is GateState.CLOSED:
            return GateDecision(
                source_name=source_name,
                allowed=True,
                state=GateState.CLOSED,
                epoch=g.epoch,
                is_probe=False,
                probe_owner=None,
                retry_after_s=0.0,
            )
        if g.state is GateState.OPEN:
            if now >= g.open_until:
                return self._grant_probe(g, source_name, owner)
            return GateDecision(
                source_name=source_name,
                allowed=False,
                state=GateState.OPEN,
                epoch=g.epoch,
                is_probe=False,
                probe_owner=None,
                retry_after_s=self._remaining(g),
            )
        # HALF_OPEN: 探针在途;租约过期则接管,否则拒绝(防惊群)
        if now >= g.probe_expires:
            return self._grant_probe(g, source_name, owner)
        return GateDecision(
            source_name=source_name,
            allowed=False,
            state=GateState.HALF_OPEN,
            epoch=g.epoch,
            is_probe=False,
            probe_owner=None,
            retry_after_s=self._remaining(g),
        )

    def _fenced(self, g: _SourceGate, entry: GateDecision) -> bool:
        """写回资格: 世代一致;探针写回还要求 owner 仍在位(CHS fencing 同款)。"""
        if not entry.allowed:
            raise ValueError("被拒决定不得写回")
        if entry.epoch != g.epoch:
            return False
        return not (
            entry.is_probe
            and (g.state is not GateState.HALF_OPEN or g.probe_owner != entry.probe_owner)
        )

    def _snapshot(self, g: _SourceGate, applied: bool) -> GateUpdate:
        return GateUpdate(
            applied=applied,
            state=g.state,
            epoch=g.epoch,
            failure_count=g.fails,
            retry_after_s=self._remaining(g),
        )

    def _open(self, g: _SourceGate, reason: str, *, bump_streak: bool) -> None:
        if bump_streak:
            g.reopen_streak += 1
        g.state = GateState.OPEN
        g.epoch += 1  # 世代推进: 旧 entry 的迟到写回自此被 fencing 拒绝
        g.open_until = self._now() + self._cooldown_eff(g)
        g.fails = max(g.fails, self._cfg.fail_threshold)
        g.probe_owner = None
        g.probe_expires = 0.0
        g.closed_since = None

    async def record_success(
        self, entry: GateDecision, *, count_attempt: bool = True
    ) -> GateUpdate:
        g = self._gate(entry.source_name)
        if not self._fenced(g, entry):
            return self._snapshot(g, applied=False)
        was_probe = entry.is_probe
        if count_attempt:
            self._window_add(g, failed=False)
        # streak 衰减: CLOSED 稳定满 2×cooldown_eff 后的首次成功归零(设计 §3.1)
        if (
            g.reopen_streak > 0
            and g.closed_since is not None
            and self._now() - g.closed_since >= 2 * self._cooldown_eff(g)
        ):
            g.reopen_streak = 0
        if was_probe:
            g.closed_since = self._now()  # 仅探针转 CLOSED 时写,普通成功不刷新
        g.state = GateState.CLOSED
        g.fails = 0
        g.probe_owner = None
        g.probe_expires = 0.0
        return self._snapshot(g, applied=True)

    async def record_failure(
        self, entry: GateDecision, reason: str, force_open: bool
    ) -> GateUpdate:
        g = self._gate(entry.source_name)
        if not self._fenced(g, entry):
            return self._snapshot(g, applied=False)
        if reason == "rate_limited" and not force_open:
            # 429 = 背压不是故障(设计 §3.1): 两通道均不计,选源层软处理;
            # 探针撞 429 按无果归还语义放下家,不挂租约
            if entry.is_probe:
                g.state = GateState.OPEN
                g.open_until = self._now()
                g.probe_owner = None
                g.probe_expires = 0.0
            return self._snapshot(g, applied=True)
        if entry.is_probe or force_open:
            # 探针失败重开递增 streak;SourceDead 一击即熔不递增
            self._open(g, reason, bump_streak=entry.is_probe)
            return self._snapshot(g, applied=True)
        self._window_add(g, failed=True)
        g.fails += 1
        if self._rate_channel_open(g):
            self._open(g, reason, bump_streak=True)
        elif g.fails >= self._cfg.fail_threshold and not self._window_evidently_healthy(g):
            # 连续通道不递增(C1 封顶);窗口证据充足且健康时连败是噪声,
            # 不误熔"当前最好的源"(迭代 6,第八轮实证: 源1 被 5 连空补全误关 60s)
            self._open(g, reason, bump_streak=False)
        return self._snapshot(g, applied=True)

    async def release_probe(self, entry: GateDecision) -> GateUpdate:
        """探针无果归还(如取消): 源保持可接管状态让下一 caller 接手;幂等。"""
        if not (entry.allowed and entry.is_probe and entry.probe_owner is not None):
            raise ValueError("release_probe 只接受在途探针决定")
        g = self._gate(entry.source_name)
        if not self._fenced(g, entry):
            return self._snapshot(g, applied=False)
        g.state = GateState.OPEN
        g.open_until = self._now()  # 立即可被下一 caller 以探针身份接管
        g.probe_owner = None
        g.probe_expires = 0.0
        return self._snapshot(g, applied=True)

    async def retry_after_s(self, sources: tuple[str, ...]) -> float:
        """集合中最早可尝试时间;健康/到期返回 0。"""
        if not sources:
            raise ValueError("sources 不能为空")
        return min(self._remaining(self._gate(name)) for name in sources)
