"""Circuit breaker pattern for protecting external API calls."""

import threading
import time
from enum import Enum


class CircuitBreakerState(Enum):
    CLOSED = "closed"
    OPEN = "open"
    HALF_OPEN = "half_open"


class CircuitBreakerOpen(Exception):
    """Raised when the circuit breaker is open and blocking requests."""

    def __init__(self, retry_after: float) -> None:
        self.retry_after = retry_after
        super().__init__(f"Circuit breaker is open. Retry after {retry_after:.1f}s")


def _is_permanent_error(error: Exception) -> bool:
    """Return True if the error is permanent (should open circuit immediately)."""
    try:
        from classify_api_error import classify_api_error

        return classify_api_error(error) == "permanent"
    except ImportError:
        # Fallback: inspect error text directly
        text = str(error)
        if error.__cause__ is not None:
            text += " " + str(error.__cause__)
        permanent_patterns = ["400", "401", "403", "Forbidden", "Unauthorized"]
        return any(p in text for p in permanent_patterns)


class CircuitBreaker:
    """Thread-safe circuit breaker for protecting external API calls.

    Implements three states:
    - CLOSED: requests pass through normally.
    - OPEN: requests are blocked with CircuitBreakerOpen.
    - HALF_OPEN: one probe request is allowed through.

    Args:
        failure_threshold: Consecutive failures before opening. Default 5.
        reset_timeout: Seconds to wait in OPEN before trying HALF_OPEN. Default 300.0.
    """

    def __init__(
        self,
        failure_threshold: int = 5,
        reset_timeout: float = 300.0,
    ) -> None:
        self._failure_threshold = failure_threshold
        self._reset_timeout = reset_timeout
        self._lock = threading.Lock()

        self._state = CircuitBreakerState.CLOSED
        self._failure_count = 0
        self._opened_at: float | None = None

    # ------------------------------------------------------------------
    # Public interface
    # ------------------------------------------------------------------

    def check(self) -> None:
        """Check whether a request is allowed through.

        Raises:
            CircuitBreakerOpen: If the circuit is open and reset_timeout
                has not elapsed yet.
        """
        with self._lock:
            if self._state is CircuitBreakerState.CLOSED:
                return

            if self._state is CircuitBreakerState.OPEN:
                elapsed = time.monotonic() - self._opened_at  # type: ignore[operator]
                if elapsed >= self._reset_timeout:
                    self._state = CircuitBreakerState.HALF_OPEN
                    return
                remaining = self._reset_timeout - elapsed
                raise CircuitBreakerOpen(min(remaining, 30.0))

            # HALF_OPEN: allow exactly one probe — caller holds the slot
            if self._state is CircuitBreakerState.HALF_OPEN:
                return

    def record_success(self) -> None:
        """Record a successful request. Resets the breaker to CLOSED."""
        with self._lock:
            self._state = CircuitBreakerState.CLOSED
            self._failure_count = 0
            self._opened_at = None

    def record_failure(self, error: Exception) -> None:
        """Record a failed request.

        If the error is permanent (e.g. 401/403), opens immediately.
        Otherwise increments the failure counter and opens once it
        reaches failure_threshold.

        Args:
            error: The exception that was raised.
        """
        with self._lock:
            if _is_permanent_error(error):
                self._trip()
                return

            if self._state is CircuitBreakerState.HALF_OPEN:
                self._trip()
                return

            self._failure_count += 1
            if self._failure_count >= self._failure_threshold:
                self._trip()

    @property
    def retry_after(self) -> float:
        """Seconds until the circuit transitions to HALF_OPEN.

        Returns 0.0 when not in OPEN state, capped at 30 seconds.
        """
        with self._lock:
            if self._state is not CircuitBreakerState.OPEN:
                return 0.0
            elapsed = time.monotonic() - self._opened_at  # type: ignore[operator]
            remaining = self._reset_timeout - elapsed
            return min(max(remaining, 0.0), 30.0)

    # ------------------------------------------------------------------
    # Internal helpers
    # ------------------------------------------------------------------

    def _trip(self) -> None:
        """Open the circuit (must be called with _lock held)."""
        self._state = CircuitBreakerState.OPEN
        self._failure_count = 0
        self._opened_at = time.monotonic()
