"""General-purpose circuit breaker registry for service resilience.

Provides a standalone, provider-agnostic circuit breaker with a registry
for managing multiple breakers. Complements providers.resilience (which
wraps LLM providers) with a reusable, general-purpose interface.

Stdlib-only (ADR-007 Tier-0 compliant).
"""

from __future__ import annotations

import time
from collections.abc import Callable
from dataclasses import dataclass, field
from enum import Enum, auto
from threading import Lock
from typing import Any


class CircuitState(Enum):
    CLOSED = auto()
    OPEN = auto()
    HALF_OPEN = auto()


@dataclass
class CircuitBreakerConfig:
    """Configuration for a CircuitBreaker instance."""

    failure_threshold: int = 5
    """Consecutive failures before opening."""

    reset_timeout: float = 60.0
    """Seconds the circuit stays OPEN before probing."""

    success_threshold: int = 1
    """Successes in HALF_OPEN needed to close."""

    time_window: float = 60.0
    """Sliding window for rate-mode failure tracking."""

    failure_rate_threshold: float = 0.5
    """Fraction of calls that must fail to open (rate mode)."""

    use_rate_mode: bool = False
    """If True use failure-rate threshold instead of consecutive count."""


@dataclass
class CircuitBreakerStats:
    """Snapshot of circuit breaker metrics."""

    name: str
    state: str
    failure_count: int
    success_count: int
    total_calls: int
    last_failure_time: float | None
    opened_at: float | None
    consecutive_failures: int
    consecutive_successes: int


class CircuitOpenError(RuntimeError):
    """Raised when a request is blocked by an open circuit breaker."""

    def __init__(self, name: str, retry_after: float) -> None:
        self.name = name
        self.retry_after = retry_after
        super().__init__(f"Circuit OPEN for {name!r}; retry in {retry_after:.1f}s")


class CircuitBreaker:
    """Thread-safe circuit breaker for any callable service.

    Example::

        cb = CircuitBreaker("db-write")

        def write_record(data):
            return db.write(data)

        try:
            result = cb.call(lambda: write_record(payload))
        except CircuitOpenError as exc:
            # fast-fail; circuit is open
            use_fallback()
    """

    def __init__(self, name: str, config: CircuitBreakerConfig | None = None) -> None:
        self.name = name
        self.config = config or CircuitBreakerConfig()
        self._lock = Lock()
        self._state = CircuitState.CLOSED
        self._consecutive_failures = 0
        self._consecutive_successes = 0
        self._total_calls = 0
        self._failure_count = 0
        self._success_count = 0
        self._opened_at: float | None = None
        self._last_failure_time: float | None = None
        self._call_log: list[tuple[float, bool]] = []

    @property
    def state(self) -> CircuitState:
        with self._lock:
            return self._evaluate_state()

    def call(self, fn: Callable[[], Any]) -> Any:
        """Execute *fn* through the breaker; raises CircuitOpenError if OPEN."""
        with self._lock:
            if self._evaluate_state() == CircuitState.OPEN:
                raise CircuitOpenError(self.name, self._seconds_until_reset())
        try:
            result = fn()
            self._on_success()
            return result
        except Exception:
            self._on_failure()
            raise

    def record_success(self) -> None:
        self._on_success()

    def record_failure(self) -> None:
        self._on_failure()

    def reset(self) -> None:
        with self._lock:
            self._state = CircuitState.CLOSED
            self._consecutive_failures = 0
            self._consecutive_successes = 0
            self._opened_at = None

    def stats(self) -> CircuitBreakerStats:
        with self._lock:
            return CircuitBreakerStats(
                name=self.name,
                state=self._evaluate_state().name,
                failure_count=self._failure_count,
                success_count=self._success_count,
                total_calls=self._total_calls,
                last_failure_time=self._last_failure_time,
                opened_at=self._opened_at,
                consecutive_failures=self._consecutive_failures,
                consecutive_successes=self._consecutive_successes,
            )

    # ── internals ──────────────────────────────────────────────

    def _evaluate_state(self) -> CircuitState:
        if self._state == CircuitState.OPEN and self._opened_at is not None:
            if time.monotonic() - self._opened_at >= self.config.reset_timeout:
                self._state = CircuitState.HALF_OPEN
                self._consecutive_successes = 0
        return self._state

    def _on_success(self) -> None:
        with self._lock:
            self._total_calls += 1
            self._success_count += 1
            self._consecutive_failures = 0
            self._consecutive_successes += 1
            if self.config.use_rate_mode:
                self._log_call(True)
            if self._state == CircuitState.HALF_OPEN:
                if self._consecutive_successes >= self.config.success_threshold:
                    self._state = CircuitState.CLOSED
                    self._opened_at = None

    def _on_failure(self) -> None:
        with self._lock:
            now = time.monotonic()
            self._total_calls += 1
            self._failure_count += 1
            self._consecutive_successes = 0
            self._consecutive_failures += 1
            self._last_failure_time = now
            if self.config.use_rate_mode:
                self._log_call(False)
                if self._rate_exceeds_threshold():
                    self._trip(now)
            elif self._state == CircuitState.HALF_OPEN:
                self._trip(now)
            elif self._state == CircuitState.CLOSED:
                if self._consecutive_failures >= self.config.failure_threshold:
                    self._trip(now)

    def _trip(self, now: float) -> None:
        self._state = CircuitState.OPEN
        self._opened_at = now
        self._consecutive_successes = 0

    def _seconds_until_reset(self) -> float:
        if self._opened_at is None:
            return 0.0
        return max(0.0, self.config.reset_timeout - (time.monotonic() - self._opened_at))

    def _log_call(self, ok: bool) -> None:
        now = time.monotonic()
        cutoff = now - self.config.time_window
        self._call_log = [(t, s) for t, s in self._call_log if t >= cutoff]
        self._call_log.append((now, ok))

    def _rate_exceeds_threshold(self) -> bool:
        if not self._call_log:
            return False
        failures = sum(1 for _, s in self._call_log if not s)
        return (failures / len(self._call_log)) >= self.config.failure_rate_threshold


@dataclass
class CircuitBreakerRegistry:
    """Central registry managing circuit breakers for multiple services.

    Example::

        registry = CircuitBreakerRegistry()
        registry.register("vllm")
        registry.register("openai", config=CircuitBreakerConfig(failure_threshold=3))

        cb = registry.get_or_create("vllm")
        result = cb.call(lambda: provider.generate(prompt))
    """

    _config: CircuitBreakerConfig = field(default_factory=CircuitBreakerConfig)
    _breakers: dict[str, CircuitBreaker] = field(default_factory=dict)

    def register(
        self, name: str, config: CircuitBreakerConfig | None = None
    ) -> CircuitBreaker:
        cb = CircuitBreaker(name, config or self._config)
        self._breakers[name] = cb
        return cb

    def get(self, name: str) -> CircuitBreaker | None:
        return self._breakers.get(name)

    def get_or_create(
        self, name: str, config: CircuitBreakerConfig | None = None
    ) -> CircuitBreaker:
        if name not in self._breakers:
            self.register(name, config)
        return self._breakers[name]

    def all_stats(self) -> list[CircuitBreakerStats]:
        return [cb.stats() for cb in self._breakers.values()]

    def open_services(self) -> list[str]:
        return [n for n, cb in self._breakers.items() if cb.state == CircuitState.OPEN]

    def healthy_services(self) -> list[str]:
        return [n for n, cb in self._breakers.items() if cb.state == CircuitState.CLOSED]

    def reset_all(self) -> None:
        for cb in self._breakers.values():
            cb.reset()


_DEFAULT_REGISTRY = CircuitBreakerRegistry()


def get_default_registry() -> CircuitBreakerRegistry:
    """Return the module-level default registry."""
    return _DEFAULT_REGISTRY


__all__ = [
    "CircuitBreaker",
    "CircuitBreakerConfig",
    "CircuitBreakerRegistry",
    "CircuitBreakerStats",
    "CircuitOpenError",
    "CircuitState",
    "get_default_registry",
]
