"""
Circuit Breaker

Implements circuit breaker pattern for module fault tolerance.
"""

from enum import Enum
import time
from typing import Dict, Optional, Callable
from dataclasses import dataclass
import threading


class CircuitState(Enum):
    """Circuit breaker states"""
    CLOSED = "CLOSED"        # Normal operation
    OPEN = "OPEN"            # Failing, reject requests
    HALF_OPEN = "HALF_OPEN"  # Testing recovery


@dataclass
class CircuitBreakerConfig:
    """Configuration for circuit breaker"""
    failure_threshold: int = 5  # Failures before opening
    success_threshold: int = 2  # Successes before closing from half-open
    timeout: float = 60.0       # Seconds before attempting recovery
    half_open_max_calls: int = 3  # Max calls to test in half-open state


class CircuitBreaker:
    """
    Circuit breaker for a single module.

    States:
    - CLOSED: Normal operation, requests allowed
    - OPEN: Too many failures, requests rejected
    - HALF_OPEN: Testing recovery, limited requests allowed
    """

    def __init__(
        self,
        module_id: str,
        config: Optional[CircuitBreakerConfig] = None
    ):
        self.module_id = module_id
        self._config = config or CircuitBreakerConfig()

        self._state = CircuitState.CLOSED
        self._failure_count = 0
        self._success_count = 0
        self._last_failure_time: Optional[float] = None
        self._half_open_calls = 0

        self._lock = threading.Lock()

        # Callbacks
        self._state_change_callbacks: list[Callable[[CircuitState, CircuitState], None]] = []

    def register_state_change_callback(
        self,
        callback: Callable[[CircuitState, CircuitState], None]
    ):
        """Register callback for state changes"""
        self._state_change_callbacks.append(callback)

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

    def can_execute(self) -> bool:
        """
        Check if execution is allowed.

        Returns:
            True if execution allowed, False if circuit is open
        """
        with self._lock:
            # Check if we should transition from OPEN to HALF_OPEN
            if self._state == CircuitState.OPEN:
                if self._should_attempt_reset():
                    self._transition_to(CircuitState.HALF_OPEN)

            # Allow execution in CLOSED and HALF_OPEN states
            if self._state == CircuitState.CLOSED:
                return True

            if self._state == CircuitState.HALF_OPEN:
                if self._half_open_calls < self._config.half_open_max_calls:
                    self._half_open_calls += 1
                    return True

            return False

    def record_success(self):
        """Record a successful execution"""
        with self._lock:
            if self._state == CircuitState.HALF_OPEN:
                self._success_count += 1
                if self._success_count >= self._config.success_threshold:
                    self._transition_to(CircuitState.CLOSED)
                    self._reset_counts()
            elif self._state == CircuitState.CLOSED:
                # Reset failure count on success
                self._failure_count = 0

    def record_failure(self):
        """Record a failed execution"""
        with self._lock:
            self._last_failure_time = time.time()

            if self._state == CircuitState.HALF_OPEN:
                # Any failure in half-open immediately opens circuit
                self._transition_to(CircuitState.OPEN)
                self._reset_counts()
            elif self._state == CircuitState.CLOSED:
                self._failure_count += 1
                if self._failure_count >= self._config.failure_threshold:
                    self._transition_to(CircuitState.OPEN)

    def _should_attempt_reset(self) -> bool:
        """Check if enough time has passed to attempt reset"""
        if self._last_failure_time is None:
            return True

        elapsed = time.time() - self._last_failure_time
        return elapsed >= self._config.timeout

    def _transition_to(self, new_state: CircuitState):
        """Transition to a new state"""
        old_state = self._state
        self._state = new_state

        # Reset half-open call counter when entering half-open
        if new_state == CircuitState.HALF_OPEN:
            self._half_open_calls = 0
            self._success_count = 0

        # Notify callbacks
        for callback in self._state_change_callbacks:
            try:
                callback(old_state, new_state)
            except Exception as e:
                print(f"Error in circuit breaker callback: {e}")

    def _reset_counts(self):
        """Reset failure and success counts"""
        self._failure_count = 0
        self._success_count = 0
        self._half_open_calls = 0

    def get_metrics(self) -> dict:
        """Get circuit breaker metrics"""
        with self._lock:
            return {
                "module_id": self.module_id,
                "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,
                    "success_threshold": self._config.success_threshold,
                    "timeout": self._config.timeout
                }
            }

    def force_open(self):
        """Manually open the circuit (for testing/emergency)"""
        with self._lock:
            self._transition_to(CircuitState.OPEN)

    def force_close(self):
        """Manually close the circuit (for testing/recovery)"""
        with self._lock:
            self._transition_to(CircuitState.CLOSED)
            self._reset_counts()


class CircuitBreakerRegistry:
    """Manages circuit breakers for all modules"""

    def __init__(self, default_config: Optional[CircuitBreakerConfig] = None):
        self._breakers: Dict[str, CircuitBreaker] = {}
        self._default_config = default_config or CircuitBreakerConfig()
        self._lock = threading.Lock()

    def get_breaker(self, module_id: str) -> CircuitBreaker:
        """Get or create circuit breaker for a module"""
        with self._lock:
            if module_id not in self._breakers:
                self._breakers[module_id] = CircuitBreaker(
                    module_id,
                    self._default_config
                )
            return self._breakers[module_id]

    def get_all_metrics(self) -> Dict[str, dict]:
        """Get metrics for all circuit breakers"""
        with self._lock:
            return {
                module_id: breaker.get_metrics()
                for module_id, breaker in self._breakers.items()
            }
