"""Circuit breaker module for Fedibooster.

This module provides configurable circuit breakers for external API calls
that work alongside stamina retries to provide robust failure handling.
"""

from dataclasses import dataclass
from dataclasses import field
from typing import Any
from typing import Callable
from typing import Optional

from loguru import logger as log
from pybreaker import CircuitBreaker
from pybreaker import CircuitBreakerError
from pybreaker import CircuitBreakerListener
from pybreaker import CircuitBreakerState


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

    # Maximum number of failures before opening the circuit
    fail_max: int = 5

    # Time in seconds to keep circuit open before attempting half-open
    reset_timeout: int = 60

    # Name for logging and identification
    name: str = "circuit_breaker"

    # Expected exception types that should be counted as failures
    # If None, all exceptions are counted
    expected_exception: Optional[type[Exception] | tuple[type[Exception], ...]] = None

    # Callback for state changes (for monitoring)
    state_change_callback: Optional[Callable[[str, str], None]] = None


@dataclass
class CircuitBreakerManager:
    """Manages multiple circuit breakers for different services."""

    breakers: dict[str, CircuitBreaker] = field(default_factory=dict)

    def create_breaker(self, config: CircuitBreakerConfig) -> CircuitBreaker:
        """Create and register a circuit breaker with the given configuration."""

        class StateChangeListener(CircuitBreakerListener):
            """Listener for circuit breaker state changes."""

            def __init__(self, config: CircuitBreakerConfig):
                self.config = config

            def before_call(self, cb: CircuitBreaker, func: Callable, *args, **kwargs) -> None:
                """Call before the circuit breaker `cb` calls `func`."""
                pass

            def failure(self, cb: CircuitBreaker, exc: BaseException) -> None:
                """Call when a circuit breaker `cb` call fails with `exc`."""
                pass

            def success(self, cb: CircuitBreaker) -> None:
                """Call when a circuit breaker `cb` call succeeds."""
                pass

            def state_change(
                self,
                cb: CircuitBreaker,
                old_state: CircuitBreakerState | None,
                new_state: CircuitBreakerState,
            ) -> None:
                """Handle circuit breaker state changes."""
                old_str = str(old_state) if old_state else "None"
                new_str = str(new_state)
                log.info(f"Circuit breaker '{cb.name}' changed from {old_str} to {new_str}")

                # Call custom callback if provided
                if self.config.state_change_callback:
                    self.config.state_change_callback({"name": cb.name, "old_state": old_state, "new_state": new_state})

        # Create the circuit breaker
        breaker = CircuitBreaker(
            fail_max=config.fail_max,
            reset_timeout=config.reset_timeout,
            name=config.name,
        )

        # Add state change listener
        listener = StateChangeListener(config)
        breaker.add_listener(listener)

        # Register expected exception if specified
        if config.expected_exception:
            if isinstance(config.expected_exception, tuple):
                for exc_type in config.expected_exception:
                    breaker.add_excluded_exception(exc_type)
            else:
                breaker.add_excluded_exception(config.expected_exception)

        # Store the breaker
        self.breakers[config.name] = breaker

        return breaker

    def get_breaker(self, name: str) -> Optional[CircuitBreaker]:
        """Get a circuit breaker by name."""
        return self.breakers.get(name)

    def get_breaker_stats(self, name: str) -> dict[str, Any]:
        """Get statistics for a circuit breaker."""
        breaker = self.get_breaker(name)
        if not breaker:
            return {}

        return {
            "name": breaker.name,
            "state": breaker.current_state,
            "fail_counter": breaker.fail_counter,
            "fail_max": breaker.fail_max,
            "reset_timeout": breaker.reset_timeout,
        }

    def get_all_stats(self) -> dict[str, dict[str, Any]]:
        """Get statistics for all circuit breakers."""
        return {name: self.get_breaker_stats(name) for name in self.breakers}


# Global circuit breaker manager instance
circuit_breaker_manager = CircuitBreakerManager()


def create_default_breakers() -> None:
    """Create default circuit breakers for Fedibooster."""
    # Circuit breaker for searching on external instances
    search_config = CircuitBreakerConfig(
        name="search_instance",
        fail_max=3,  # Fewer failures needed for search since it's critical
        reset_timeout=300,  # 5 minutes for search instances
        expected_exception=None,  # All exceptions count as failures
    )
    circuit_breaker_manager.create_breaker(search_config)

    # Circuit breaker for reblog operations
    reblog_config = CircuitBreakerConfig(
        name="reblog_operation",
        fail_max=5,
        reset_timeout=180,  # 3 minutes for reblog operations
        expected_exception=None,
    )
    circuit_breaker_manager.create_breaker(reblog_config)

    # Circuit breaker for external configuration loading
    config_config = CircuitBreakerConfig(
        name="config_loading",
        fail_max=2,  # Very sensitive - if config fails, we have bigger problems
        reset_timeout=600,  # 10 minutes for config sources
        expected_exception=None,
    )
    circuit_breaker_manager.create_breaker(config_config)

    # Circuit breaker for authentication/connection
    auth_config = CircuitBreakerConfig(
        name="authentication",
        fail_max=2,  # Authentication failures are critical
        reset_timeout=900,  # 15 minutes for auth failures
        expected_exception=None,
    )
    circuit_breaker_manager.create_breaker(auth_config)


def circuit_protected_call(
    breaker_name: str,
    operation_description: str = "",
) -> Callable:
    """Create a decorator factory for circuit-protected operations.

    This creates a decorator that works alongside stamina retries - retries handle
    transient failures within the protected call, while the circuit breaker handles
    persistent failures across multiple calls.

    Args:
        breaker_name: Name of the circuit breaker to use
        operation_description: Description for logging

    Returns:
        A decorator that can be applied to functions

    """

    def decorator(func):
        def wrapper(*args, **kwargs):
            breaker = circuit_breaker_manager.get_breaker(breaker_name)

            if not breaker:
                log.warning(f"No circuit breaker found with name '{breaker_name}'")
                return func(*args, **kwargs)

            # Check if circuit is open
            if breaker.current_state == "open":
                log.warning(f"Circuit breaker '{breaker_name}' is OPEN for operation: {operation_description}")
                raise CircuitBreakerError(f"Circuit breaker '{breaker_name}' is OPEN")

            try:
                # Call through the circuit breaker
                return breaker.call(func, *args, **kwargs)
            except CircuitBreakerError:
                log.error(f"Circuit breaker '{breaker_name}' opened during: {operation_description}")
                raise

        return wrapper

    return decorator


def circuit_breaker_decorator(
    breaker_name: str,
    fallback_value: Any = None,
    fallback_func: Optional[Callable[[], Any]] = None,
):
    """Create a decorator for circuit-protected functions.

    This decorator can be used alongside @stamina.retry decorators.
    The circuit breaker wraps the entire retry logic.

    Example:
        @circuit_breaker_decorator("search_instance", fallback_value=[])
        @stamina.retry(on=NetworkError, attempts=3)
        async def search_function():
            # ...

    """

    def decorator(func):
        def wrapper(*args, **kwargs):
            breaker = circuit_breaker_manager.get_breaker(breaker_name)

            if not breaker:
                log.warning(f"No circuit breaker found with name '{breaker_name}'")
                return func(*args, **kwargs)

            # Check if circuit is open
            if breaker.current_state == "open":
                log.warning(f"Circuit breaker '{breaker_name}' is OPEN for {func.__name__}")

                if fallback_func:
                    return fallback_func()
                elif fallback_value is not None:
                    return fallback_value
                else:
                    raise CircuitBreakerError(f"Circuit breaker '{breaker_name}' is OPEN")

            try:
                # Call through the circuit breaker
                return breaker.call(func, *args, **kwargs)
            except CircuitBreakerError:
                log.error(f"Circuit breaker '{breaker_name}' opened during {func.__name__}")

                if fallback_func:
                    return fallback_func()
                elif fallback_value is not None:
                    return fallback_value
                else:
                    raise

        return wrapper

    return decorator


# Initialize default circuit breakers when module is imported
create_default_breakers()
