"""Circuit Breaker module for OCLAWMA.

This module provides a circuit breaker pattern implementation to prevent
cascade failures when calling external services.

Example:
    Basic usage:

    >>> from oclawma.circuit_breaker import CircuitBreaker, CircuitState
    >>> cb = CircuitBreaker(failure_threshold=5, recovery_timeout=30)
    >>> result = cb.call("api.example.com", lambda: requests.get("..."))

    Using the decorator:

    >>> @circuit_breaker("api.example.com")
    ... def call_api():
    ...     return requests.get("...")
"""

from __future__ import annotations

import functools
import threading
import time
from enum import Enum
from typing import Any, Callable, TypeVar

T = TypeVar("T")


class CircuitState(Enum):
    """Circuit breaker states.

    - CLOSED: Normal operation, calls pass through
    - OPEN: Circuit is open, calls fail fast
    - HALF_OPEN: Testing if service has recovered
    """

    CLOSED = "closed"
    OPEN = "open"
    HALF_OPEN = "half_open"


class CircuitBreakerError(Exception):
    """Exception raised when circuit breaker is OPEN.

    Attributes:
        destination: The service destination that is unavailable
        state: The current circuit state
    """

    def __init__(self, destination: str, state: CircuitState) -> None:
        self.destination = destination
        self.state = state
        super().__init__(
            f"Circuit breaker for '{destination}' is {state.value.upper()}: "
            "too many failures, service temporarily unavailable"
        )


class _CircuitState:
    """Internal state for a single circuit.

    Tracks the current state, failure count, last failure time,
    and metrics for a specific destination.
    """

    def __init__(self, destination: str) -> None:
        self.destination = destination
        self.state = CircuitState.CLOSED
        self.failure_count = 0
        self.success_count = 0
        self.last_failure_time: float | None = None
        self.half_open_calls = 0

        # Metrics
        self.state_changes_to_open = 0
        self.state_changes_to_closed = 0
        self.state_changes_to_half_open = 0

        # Lock for thread safety
        self._lock = threading.RLock()

    def record_success(self) -> None:
        """Record a successful call."""
        with self._lock:
            self.success_count += 1

            if self.state == CircuitState.HALF_OPEN:
                self.half_open_calls += 1
                # If we've allowed enough calls in HALF_OPEN, close the circuit
                self.state = CircuitState.CLOSED
                self.state_changes_to_closed += 1
                self.failure_count = 0
                self.half_open_calls = 0

    def record_failure(self) -> None:
        """Record a failed call."""
        with self._lock:
            self.failure_count += 1
            self.last_failure_time = time.monotonic()

            if self.state == CircuitState.HALF_OPEN:
                # Failed in HALF_OPEN, go back to OPEN
                self.state = CircuitState.OPEN
                self.state_changes_to_open += 1
                self.half_open_calls = 0

    def get_metrics(self) -> dict[str, Any]:
        """Get metrics for this circuit."""
        with self._lock:
            return {
                "state": self.state.value,
                "failure_count": self.failure_count,
                "success_count": self.success_count,
                "state_changes_to_open": self.state_changes_to_open,
                "state_changes_to_closed": self.state_changes_to_closed,
                "state_changes_to_half_open": self.state_changes_to_half_open,
                "last_failure_time": self.last_failure_time,
            }


class CircuitBreaker:
    """Circuit breaker for external service calls.

    Prevents cascade failures by tracking failures and opening the circuit
    when a threshold is reached, temporarily blocking calls to the failing service.

    Attributes:
        failure_threshold: Number of failures before opening circuit
        recovery_timeout: Seconds to wait before trying recovery
        half_open_max_calls: Max calls allowed in HALF_OPEN state
    """

    def __init__(
        self,
        failure_threshold: int = 5,
        recovery_timeout: float = 30.0,
        half_open_max_calls: int = 3,
    ) -> None:
        """Initialize circuit breaker.

        Args:
            failure_threshold: Number of consecutive failures before opening circuit
            recovery_timeout: Seconds to wait before transitioning to HALF_OPEN
            half_open_max_calls: Maximum calls allowed in HALF_OPEN state before closing
        """
        self.failure_threshold = failure_threshold
        self.recovery_timeout = recovery_timeout
        self.half_open_max_calls = half_open_max_calls

        # Per-destination circuit states
        self._circuits: dict[str, _CircuitState] = {}
        self._lock = threading.RLock()

    def _get_or_create_circuit(self, destination: str) -> _CircuitState:
        """Get or create circuit state for destination."""
        with self._lock:
            if destination not in self._circuits:
                self._circuits[destination] = _CircuitState(destination)
            return self._circuits[destination]

    def get_state(self, destination: str) -> CircuitState:
        """Get current state for a destination.

        Args:
            destination: The service destination to check

        Returns:
            Current circuit state (CLOSED, OPEN, or HALF_OPEN)
        """
        with self._lock:
            if destination not in self._circuits:
                return CircuitState.CLOSED
            return self._circuits[destination].state

    def reset(self, destination: str) -> None:
        """Reset circuit for a destination to CLOSED state.

        Args:
            destination: The service destination to reset
        """
        with self._lock:
            if destination in self._circuits:
                circuit = self._circuits[destination]
                circuit.state = CircuitState.CLOSED
                circuit.failure_count = 0
                circuit.half_open_calls = 0
            else:
                # Create a fresh circuit
                self._circuits[destination] = _CircuitState(destination)

    def _should_attempt_reset(self, circuit: _CircuitState) -> bool:
        """Check if enough time has passed to try recovery."""
        if circuit.last_failure_time is None:
            return False
        elapsed = time.monotonic() - circuit.last_failure_time
        return elapsed >= self.recovery_timeout

    def _check_and_transition_state(self, circuit: _CircuitState) -> None:
        """Check if circuit should transition from OPEN to HALF_OPEN."""
        if circuit.state == CircuitState.OPEN and self._should_attempt_reset(circuit):
            circuit.state = CircuitState.HALF_OPEN
            circuit.state_changes_to_half_open += 1
            circuit.half_open_calls = 0

    def call(self, destination: str, callable_fn: Callable[[], T]) -> T:
        """Execute a call with circuit breaker protection.

        Args:
            destination: The service destination being called
            callable_fn: The function to call

        Returns:
            The result of callable_fn()

        Raises:
            CircuitBreakerError: If circuit is OPEN and recovery timeout hasn't passed
            Any exception raised by callable_fn
        """
        circuit = self._get_or_create_circuit(destination)

        with circuit._lock:
            # Check if we should transition to HALF_OPEN
            self._check_and_transition_state(circuit)

            # Check if circuit is OPEN
            if circuit.state == CircuitState.OPEN:
                raise CircuitBreakerError(destination, CircuitState.OPEN)

            # Check if we've exceeded half_open_max_calls in HALF_OPEN
            if circuit.state == CircuitState.HALF_OPEN:
                if circuit.half_open_calls >= self.half_open_max_calls:
                    # We've tested enough, close the circuit
                    circuit.state = CircuitState.CLOSED
                    circuit.state_changes_to_closed += 1
                    circuit.failure_count = 0
                    circuit.half_open_calls = 0
                else:
                    circuit.half_open_calls += 1

        # Execute the call (outside the lock to avoid blocking)
        try:
            result = callable_fn()
            circuit.record_success()

            # Check if we need to open the circuit due to accumulated failures
            # (this handles the case where we transition to HALF_OPEN but fail again)
            if circuit.state == CircuitState.HALF_OPEN:
                # Success in HALF_OPEN closes the circuit (handled in record_success)
                pass

            return result
        except Exception:
            circuit.record_failure()

            # Check if we should open the circuit
            with circuit._lock:
                if (
                    circuit.failure_count >= self.failure_threshold
                    and circuit.state == CircuitState.CLOSED
                ):
                    circuit.state = CircuitState.OPEN
                    circuit.state_changes_to_open += 1

            raise

    def get_metrics(self) -> dict[str, dict[str, Any]]:
        """Get metrics for all circuits.

        Returns:
            Dictionary mapping destination to metrics dict
        """
        with self._lock:
            return {dest: circuit.get_metrics() for dest, circuit in self._circuits.items()}

    def get_metrics_snapshot(self) -> dict[str, Any]:
        """Get a complete metrics snapshot.

        Returns:
            Dictionary with circuits metrics and timestamp
        """
        return {
            "circuits": {
                dest: {
                    "state": metrics["state"],
                    "failure_count": metrics["failure_count"],
                    "success_count": metrics["success_count"],
                    "state_changes_to_open": metrics["state_changes_to_open"],
                    "state_changes_to_closed": metrics["state_changes_to_closed"],
                }
                for dest, metrics in self.get_metrics().items()
            },
        }


# Global circuit breaker instance for decorator convenience
_global_circuit_breaker: CircuitBreaker | None = None
_global_lock = threading.Lock()


def _get_global_circuit_breaker() -> CircuitBreaker:
    """Get or create global circuit breaker instance."""
    global _global_circuit_breaker
    with _global_lock:
        if _global_circuit_breaker is None:
            _global_circuit_breaker = CircuitBreaker()
        return _global_circuit_breaker


def circuit_breaker(
    destination: str,
    breaker: CircuitBreaker | None = None,
) -> Callable[[Callable[[], T]], Callable[[], T]]:
    """Decorator to wrap a function with circuit breaker protection.

    Args:
        destination: The service destination being called
        breaker: CircuitBreaker instance to use (uses global if None)

    Returns:
        Decorator function

    Example:
        >>> @circuit_breaker("api.example.com")
        ... def fetch_data():
        ...     return requests.get("https://api.example.com/data")
        ...
        >>> data = fetch_data()  # Protected by circuit breaker
    """

    def decorator(func: Callable[[], T]) -> Callable[[], T]:
        @functools.wraps(func)
        def wrapper(*args: Any, **kwargs: Any) -> T:
            cb = breaker if breaker is not None else _get_global_circuit_breaker()
            return cb.call(destination, lambda: func(*args, **kwargs))

        return wrapper

    return decorator


__all__ = [
    "CircuitBreaker",
    "CircuitBreakerError",
    "CircuitState",
    "circuit_breaker",
]
