"""进程内熔断器：按 source_name 记连续失败数与冷却截止时刻。

内存级单实例，now 由调用方注入以保证纯确定性可测试。达阈值后开路冷却，
冷却到期放行试探（half-open）。force_open 支持 401/403 一次即熔断。
"""

from __future__ import annotations


class CircuitBreaker:
    """按 source_name 记失败数；达阈值后开路冷却，冷却到期放行试探（half-open）。

    Args:
        fail_threshold: 连续失败次数阈值，达到后触发开路。
        cooldown_s: 开路冷却时长（秒），冷却到期自动进入半开状态。
    """

    def __init__(self, fail_threshold: int, cooldown_s: float) -> None:
        self._fail_threshold = fail_threshold
        self._cooldown_s = cooldown_s
        self._fails: dict[str, int] = {}
        self._open_until: dict[str, float] = {}

    def is_open(self, source_name: str, now: float) -> bool:
        """判断指定源是否处于开路状态。

        冷却截止时刻之前为开路；到期返回 False（放行一个试探，即半开）。

        Args:
            source_name: 被熔断的源标识。
            now: 当前时刻（秒级时间戳），由调用方注入。

        Returns:
            True 表示开路（拒绝请求），False 表示关闭或半开（放行）。
        """
        until = self._open_until.get(source_name)
        return until is not None and now < until

    def record_failure(self, source_name: str, now: float) -> None:
        """记录一次失败；累计达阈值则开路至 now + cooldown。

        Args:
            source_name: 失败的源标识。
            now: 当前时刻（秒级时间戳）。
        """
        count = self._fails.get(source_name, 0) + 1
        self._fails[source_name] = count
        if count >= self._fail_threshold:
            self._open_until[source_name] = now + self._cooldown_s

    def force_open(self, source_name: str, now: float) -> None:
        """强制开路（用于 401/403 等不可恢复错误），一次即熔断。

        将失败计数预设为阈值，使半开探针仅需一次失败即可重新熔断。

        Args:
            source_name: 需要强制熔断的源标识。
            now: 当前时刻（秒级时间戳）。
        """
        self._fails[source_name] = self._fail_threshold
        self._open_until[source_name] = now + self._cooldown_s

    def record_success(self, source_name: str) -> None:
        """记录一次成功；清零失败计数与开路状态（关闭熔断器）。

        Args:
            source_name: 成功的源标识。
        """
        self._fails.pop(source_name, None)
        self._open_until.pop(source_name, None)
