"""Circuit breaker with error classification for protecting broker operations."""

from __future__ import annotations

import logging
import threading
import time
from enum import Enum
from typing import Callable, Optional

from neonlink.errors import CircuitBreakerOpenError

logger = logging.getLogger(__name__)


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


class CircuitBreakerOpen(CircuitBreakerOpenError):
    """Raised when the circuit breaker is open and rejecting calls.

    Kept as a subclass of CircuitBreakerOpenError for backwards compatibility.
    """

    pass


class CircuitBreaker:
    """Thread-safe circuit breaker with Allow/Record split and error classification.

    - Closed (normal): all calls pass through.
    - Open (tripped): after max_failures consecutive infrastructure errors,
      all calls fail immediately for reset_timeout_sec.
    - Half-Open: after timeout, one call is allowed through.

    Permanent errors (validation, schema) pass through WITHOUT tripping
    the breaker.
    """

    def __init__(
        self,
        name: str,
        max_failures: int = 5,
        reset_timeout_sec: int = 30,
        on_state_change: Optional[Callable[[str, str, str], None]] = None,
    ) -> None:
        self._name = f"neonlink:{name}"
        self._max_failures = max_failures
        self._reset_timeout = reset_timeout_sec
        self._state = CircuitBreakerState.CLOSED
        self._failure_count = 0
        self._last_failure_time = 0.0
        self._lock = threading.Lock()
        self._on_state_change = on_state_change

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

    def allow(self) -> None:
        """Check whether a call is permitted.

        Raises:
            CircuitBreakerOpen: if the breaker is open.
        """
        with self._lock:
            state = self._evaluate_state()
            if state == CircuitBreakerState.OPEN:
                raise CircuitBreakerOpen(
                    f"neonlink: circuit breaker '{self._name}' is open"
                )

    def record_success(self) -> None:
        """Record a successful operation. Resets failure counter."""
        with self._lock:
            self._failure_count = 0
            old = self._state
            if old != CircuitBreakerState.CLOSED:
                self._state = CircuitBreakerState.CLOSED
                self._transition_log(old, CircuitBreakerState.CLOSED)

    def record_failure(self) -> None:
        """Record a failed operation. May transition to Open."""
        with self._lock:
            self._failure_count += 1
            self._last_failure_time = time.monotonic()
            if self._failure_count >= self._max_failures:
                old = self._state
                if old != CircuitBreakerState.OPEN:
                    self._state = CircuitBreakerState.OPEN
                    self._transition_log(old, CircuitBreakerState.OPEN)

    def execute(self, fn: Callable[[], None]) -> None:
        """Execute fn through the circuit breaker. Every error trips the breaker.

        Raises:
            CircuitBreakerOpen: if the breaker is open.
        """
        self.allow()
        try:
            fn()
        except Exception:
            self.record_failure()
            raise
        else:
            self.record_success()

    def execute_with_classifier(
        self,
        fn: Callable[[], None],
        should_trip: Callable[[Exception], bool],
    ) -> None:
        """Execute fn, but only record failure if should_trip(err) returns True.

        This allows permanent errors (validation, schema) to pass through
        without tripping the breaker.

        Raises:
            CircuitBreakerOpen: if the breaker is open.
        """
        self.allow()
        try:
            fn()
        except Exception as exc:
            if should_trip(exc):
                self.record_failure()
            raise
        else:
            self.record_success()

    def _evaluate_state(self) -> CircuitBreakerState:
        """Determine current state (must be called under lock)."""
        if self._state == CircuitBreakerState.OPEN:
            elapsed = time.monotonic() - self._last_failure_time
            if elapsed >= self._reset_timeout:
                old = self._state
                self._state = CircuitBreakerState.HALF_OPEN
                self._transition_log(old, CircuitBreakerState.HALF_OPEN)
                return CircuitBreakerState.HALF_OPEN
        return self._state

    def _transition_log(self, from_state: CircuitBreakerState, to_state: CircuitBreakerState) -> None:
        """Log and notify on state transition."""
        logger.info(
            "neonlink: circuit breaker state change breaker=%s from=%s to=%s",
            self._name,
            from_state.value,
            to_state.value,
        )
        if self._on_state_change is not None:
            self._on_state_change(self._name, from_state.value, to_state.value)
