"""Circuit breaker pattern for Meta Graph API resilience.

Prevents cascading failures by tracking consecutive errors and temporarily
rejecting requests when a failure threshold is exceeded.

States:
- CLOSED: Normal operation, requests pass through.
- OPEN: Failures exceeded threshold, requests are immediately rejected.
- HALF_OPEN: After recovery timeout, one probe request is allowed through.
"""
from __future__ import annotations

import asyncio
import time
from enum import Enum
from typing import Awaitable, Callable, TypeVar

from .errors import CircuitBreakerError
from .logging import get_logger

logger = get_logger(__name__)
T = TypeVar("T")


class CircuitState(str, Enum):
    CLOSED = "closed"
    OPEN = "open"
    HALF_OPEN = "half_open"


class CircuitBreaker:
    """Async circuit breaker for protecting against cascading API failures."""

    def __init__(
        self,
        *,
        failure_threshold: int = 5,
        recovery_timeout: float = 60.0,
        half_open_max_calls: int = 1,
        excluded_exceptions: tuple[type[Exception], ...] = (),
    ) -> None:
        self.failure_threshold = failure_threshold
        self.recovery_timeout = recovery_timeout
        self.half_open_max_calls = half_open_max_calls
        self.excluded_exceptions = excluded_exceptions

        self._state = CircuitState.CLOSED
        self._failure_count = 0
        self._last_failure_time: float = 0.0
        self._half_open_calls = 0
        self._lock = asyncio.Lock()

    @property
    def state(self) -> CircuitState:
        if self._state == CircuitState.OPEN:
            if time.monotonic() - self._last_failure_time >= self.recovery_timeout:
                return CircuitState.HALF_OPEN
        return self._state

    async def call(
        self,
        operation: Callable[[], Awaitable[T]],
        *,
        fallback: Callable[[], Awaitable[T]] | None = None,
    ) -> T:
        """Execute *operation* through the circuit breaker.

        Raises ``CircuitBreakerError`` when the circuit is open and no
        *fallback* is provided.
        """
        current_state = self.state

        if current_state == CircuitState.OPEN:
            logger.warning(
                "circuit_breaker_open failures=%d last_failure=%.1fs_ago",
                self._failure_count,
                time.monotonic() - self._last_failure_time,
            )
            if fallback is not None:
                return await fallback()
            raise CircuitBreakerError(
                "Circuit breaker is open — Meta API appears unavailable",
                service="meta_graph_api",
                recovery_timeout=self.recovery_timeout,
                failure_count=self._failure_count,
            )

        if current_state == CircuitState.HALF_OPEN:
            async with self._lock:
                if self._half_open_calls >= self.half_open_max_calls:
                    if fallback is not None:
                        return await fallback()
                    raise CircuitBreakerError(
                        "Circuit breaker is half-open — probe limit reached",
                        service="meta_graph_api",
                    )
                self._half_open_calls += 1

        try:
            result = await operation()
            await self._on_success()
            return result
        except Exception as exc:
            if isinstance(exc, self.excluded_exceptions):
                raise
            await self._on_failure()
            raise

    async def _on_success(self) -> None:
        async with self._lock:
            if self._state in (CircuitState.HALF_OPEN, CircuitState.OPEN):
                logger.info(
                    "circuit_breaker_recovered previous_state=%s failures=%d",
                    self._state,
                    self._failure_count,
                )
            self._failure_count = 0
            self._half_open_calls = 0
            self._state = CircuitState.CLOSED

    async def _on_failure(self) -> None:
        async with self._lock:
            self._failure_count += 1
            self._last_failure_time = time.monotonic()
            if self._failure_count >= self.failure_threshold:
                previous = self._state
                self._state = CircuitState.OPEN
                self._half_open_calls = 0
                if previous != CircuitState.OPEN:
                    logger.error(
                        "circuit_breaker_tripped failures=%d threshold=%d",
                        self._failure_count,
                        self.failure_threshold,
                    )

    async def reset(self) -> None:
        """Manually reset the circuit breaker to closed state."""
        async with self._lock:
            self._state = CircuitState.CLOSED
            self._failure_count = 0
            self._half_open_calls = 0
            logger.info("circuit_breaker_manually_reset")


# ---------------------------------------------------------------------------
# Singleton accessor
# ---------------------------------------------------------------------------

_global_breaker: CircuitBreaker | None = None


def get_circuit_breaker() -> CircuitBreaker:
    global _global_breaker
    if _global_breaker is None:
        _global_breaker = CircuitBreaker()
    return _global_breaker


def configure_circuit_breaker(
    *,
    failure_threshold: int = 5,
    recovery_timeout: float = 60.0,
    half_open_max_calls: int = 1,
    excluded_exceptions: tuple[type[Exception], ...] = (),
) -> CircuitBreaker:
    global _global_breaker
    _global_breaker = CircuitBreaker(
        failure_threshold=failure_threshold,
        recovery_timeout=recovery_timeout,
        half_open_max_calls=half_open_max_calls,
        excluded_exceptions=excluded_exceptions,
    )
    return _global_breaker
