"""Circuit breaker pattern for AI service resilience.

This module implements the circuit breaker pattern to prevent cascading failures
when an AI service is experiencing issues. It protects the system by:

1. Tracking consecutive failures
2. "Opening" the circuit after a threshold is reached
3. Blocking calls during the open state
4. Testing recovery with "half-open" state
5. Resuming normal operation after successful tests

Usage:
    from agent_factory.core.circuit_breaker import CircuitBreaker

    breaker = CircuitBreaker(
        name="bedrock",
        failure_threshold=5,
        recovery_timeout=60,
    )

    allowed, reason = breaker.allow_request()
    if allowed:
        try:
            result = call_bedrock()
            breaker.record_success()
        except Exception:
            breaker.record_failure()
"""

import logging
import threading
import time
from dataclasses import dataclass
from enum import Enum
from typing import Optional, Tuple

logger = logging.getLogger(__name__)


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

    CLOSED = "closed"  # Normal operation
    OPEN = "open"  # Blocking requests
    HALF_OPEN = "half_open"  # Testing recovery


@dataclass
class CircuitBreakerConfig:
    """Configuration for the circuit breaker.

    Attributes:
        failure_threshold: Number of failures to open circuit
        recovery_timeout: Seconds before testing recovery
        success_threshold: Successes needed to close circuit in half-open state
    """

    failure_threshold: int = 5
    recovery_timeout: int = 60
    success_threshold: int = 2


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

    This class implements the circuit breaker pattern with three states:
    - CLOSED: Normal operation, all requests pass through
    - OPEN: Service is failing, requests are blocked
    - HALF_OPEN: Testing if service has recovered

    Example:
        breaker = CircuitBreaker("bedrock")

        # Before calling service
        allowed, reason = breaker.allow_request()
        if not allowed:
            raise CircuitBreakerOpenError(reason, service="bedrock")

        try:
            result = call_service()
            breaker.record_success()
            return result
        except Exception as e:
            breaker.record_failure()
            raise
    """

    def __init__(
        self,
        name: str = "default",
        failure_threshold: int = 5,
        recovery_timeout: int = 60,
        success_threshold: int = 2,
    ) -> None:
        """Initialize the circuit breaker.

        Args:
            name: Identifier for this circuit breaker
            failure_threshold: Failures before opening circuit
            recovery_timeout: Seconds before testing recovery
            success_threshold: Successes to close circuit in half-open state
        """
        self.name = name
        self.config = CircuitBreakerConfig(
            failure_threshold=failure_threshold,
            recovery_timeout=recovery_timeout,
            success_threshold=success_threshold,
        )

        self._state = CircuitState.CLOSED
        self._failure_count = 0
        self._success_count = 0
        self._last_failure_time: Optional[float] = None
        self._lock = threading.Lock()

    @property
    def state(self) -> CircuitState:
        """Get current circuit state."""
        return self._state

    @property
    def is_closed(self) -> bool:
        """Check if circuit is closed (normal operation)."""
        return self._state == CircuitState.CLOSED

    @property
    def is_open(self) -> bool:
        """Check if circuit is open (blocking requests)."""
        return self._state == CircuitState.OPEN

    def allow_request(self) -> Tuple[bool, str]:
        """Check if a request should be allowed.

        Returns:
            Tuple of (allowed, reason)
        """
        with self._lock:
            if self._state == CircuitState.CLOSED:
                return True, "Circuit closed - requests allowed"

            if self._state == CircuitState.OPEN:
                # Check if recovery timeout has elapsed
                if self._last_failure_time:
                    time_since_failure = time.time() - self._last_failure_time
                    if time_since_failure >= self.config.recovery_timeout:
                        # Transition to half-open
                        self._state = CircuitState.HALF_OPEN
                        self._success_count = 0
                        logger.info(
                            f"Circuit breaker '{self.name}' transitioning to half-open",
                            extra={"timeout_elapsed": time_since_failure},
                        )
                        return True, "Circuit half-open - testing recovery"

                time_remaining = self.config.recovery_timeout - (
                    time.time() - self._last_failure_time
                )
                logger.warning(
                    f"Circuit breaker '{self.name}' is open",
                    extra={
                        "failures": self._failure_count,
                        "time_remaining": time_remaining,
                    },
                )
                return (
                    False,
                    f"Circuit open - blocking requests ({time_remaining:.0f}s remaining)",
                )

            # Half-open state - allow request to test recovery
            return True, "Circuit half-open - testing recovery"

    def record_success(self) -> None:
        """Record a successful call."""
        with self._lock:
            if self._state == CircuitState.HALF_OPEN:
                self._success_count += 1
                if self._success_count >= self.config.success_threshold:
                    self._state = CircuitState.CLOSED
                    self._failure_count = 0
                    self._success_count = 0
                    logger.info(
                        f"Circuit breaker '{self.name}' closed - service recovered",
                    )
            elif self._state == CircuitState.CLOSED:
                # Reset failure count on success
                self._failure_count = 0

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

            if self._state == CircuitState.HALF_OPEN:
                # Failed during recovery test, reopen
                self._state = CircuitState.OPEN
                self._success_count = 0
                logger.warning(
                    f"Circuit breaker '{self.name}' reopened - recovery failed",
                    extra={"failure_count": self._failure_count},
                )
            elif self._failure_count >= self.config.failure_threshold:
                self._state = CircuitState.OPEN
                logger.error(
                    f"Circuit breaker '{self.name}' opened - too many failures",
                    extra={"failure_count": self._failure_count},
                )
            else:
                logger.warning(
                    f"Circuit breaker '{self.name}' failure recorded",
                    extra={
                        "failure_count": self._failure_count,
                        "threshold": self.config.failure_threshold,
                    },
                )

    def reset(self) -> None:
        """Reset the circuit breaker to closed state.

        Useful for testing or manual recovery.
        """
        with self._lock:
            self._state = CircuitState.CLOSED
            self._failure_count = 0
            self._success_count = 0
            self._last_failure_time = None
            logger.info(f"Circuit breaker '{self.name}' reset to closed state")

    def get_state(self) -> dict:
        """Get current state for monitoring/debugging.

        Returns:
            Dictionary with current state information
        """
        with self._lock:
            return {
                "name": self.name,
                "state": self._state.value,
                "failure_count": self._failure_count,
                "success_count": self._success_count,
                "last_failure_time": self._last_failure_time,
                "config": {
                    "failure_threshold": self.config.failure_threshold,
                    "recovery_timeout": self.config.recovery_timeout,
                    "success_threshold": self.config.success_threshold,
                },
            }


# Global circuit breakers for common services
_circuit_breakers: dict[str, CircuitBreaker] = {}
_breakers_lock = threading.Lock()


def get_circuit_breaker(
    name: str,
    failure_threshold: int = 5,
    recovery_timeout: int = 60,
    success_threshold: int = 2,
) -> CircuitBreaker:
    """Get or create a named circuit breaker.

    Args:
        name: Circuit breaker name
        failure_threshold: Failures before opening (only used on creation)
        recovery_timeout: Seconds before testing recovery (only used on creation)
        success_threshold: Successes to close (only used on creation)

    Returns:
        CircuitBreaker instance

    Example:
        bedrock_breaker = get_circuit_breaker("bedrock")
        anthropic_breaker = get_circuit_breaker("anthropic")
    """
    with _breakers_lock:
        if name not in _circuit_breakers:
            _circuit_breakers[name] = CircuitBreaker(
                name=name,
                failure_threshold=failure_threshold,
                recovery_timeout=recovery_timeout,
                success_threshold=success_threshold,
            )
        return _circuit_breakers[name]
