from __future__ import annotations

import threading
import time
from collections import deque
from dataclasses import dataclass


@dataclass
class CircuitState:
    state: str = "closed"  # closed | open | half_open
    consec_failures: int = 0
    opened_at: float | None = None
    cooldown_until: float = 0.0
    last_error: str = ""
    last_ok_at: float | None = None


class CircuitBreaker:
    """Sliding-window breaker: 5 consecutive failures or >50% of 60s window → open."""

    def __init__(
        self,
        failure_threshold: int = 5,
        window_seconds: float = 60.0,
        open_seconds: float = 120.0,
        max_open_seconds: float = 600.0,
        clock=time.monotonic,
    ) -> None:
        self.failure_threshold = failure_threshold
        self.window_seconds = window_seconds
        self.open_seconds = open_seconds
        self.max_open_seconds = max_open_seconds
        self._clock = clock
        self._lock = threading.Lock()
        self._events: deque[tuple[float, bool]] = deque()
        self.status = CircuitState()
        self._open_stretch = open_seconds

    def allow(self) -> bool:
        with self._lock:
            self._refresh_locked()
            if self.status.state == "open":
                return False
            if self.status.state == "half_open":
                # single probe in flight: caller must record success/failure
                return True
            return True

    def record_success(self) -> CircuitState:
        with self._lock:
            now = self._clock()
            self._events.append((now, True))
            self.status.last_ok_at = now
            self.status.consec_failures = 0
            self.status.last_error = ""
            self._open_stretch = self.open_seconds
            self.status.state = "closed"
            self.status.opened_at = None
            self.status.cooldown_until = 0.0
            return self._copy()

    def record_failure(self, error: str = "") -> CircuitState:
        with self._lock:
            now = self._clock()
            self._events.append((now, False))
            self.status.consec_failures += 1
            self.status.last_error = error
            self._prune_locked(now)
            failures = sum(1 for _, ok in self._events if not ok)
            total = len(self._events)
            rate = (failures / total) if total else 0.0
            trip = self.status.consec_failures >= self.failure_threshold or (
                total >= self.failure_threshold and rate > 0.5
            )
            if trip:
                self.status.state = "open"
                self.status.opened_at = now
                self.status.cooldown_until = now + self._open_stretch
                self._open_stretch = min(self.max_open_seconds, self._open_stretch * 2)
            return self._copy()

    def snapshot(self) -> CircuitState:
        with self._lock:
            self._refresh_locked()
            return self._copy()

    def _refresh_locked(self) -> None:
        now = self._clock()
        self._prune_locked(now)
        if self.status.state == "open" and now >= self.status.cooldown_until:
            self.status.state = "half_open"

    def _prune_locked(self, now: float) -> None:
        cutoff = now - self.window_seconds
        while self._events and self._events[0][0] < cutoff:
            self._events.popleft()

    def _copy(self) -> CircuitState:
        return CircuitState(
            state=self.status.state,
            consec_failures=self.status.consec_failures,
            opened_at=self.status.opened_at,
            cooldown_until=self.status.cooldown_until,
            last_error=self.status.last_error,
            last_ok_at=self.status.last_ok_at,
        )
