"""
Circuit Breaker Pattern for Elasticsearch.

Prevents cascading failures by temporarily blocking requests to failing services.
"""

from __future__ import annotations

import threading
import time
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from enum import Enum
from functools import wraps
from typing import Any, TypeVar

from smf.exceptions import CircuitBreakerOpenError
from smf.smf_logging import get_logger

logger = get_logger("elasticsearch.circuit_breaker")

T = TypeVar("T")


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

    CLOSED = "closed"  # Normal operation, requests flow through
    OPEN = "open"  # Circuit is open, requests are blocked
    HALF_OPEN = "half_open"  # Testing if service has recovered


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

    Attributes:
        failure_threshold: Number of failures before opening circuit (default: 5).
        success_threshold: Successes needed to close circuit from half-open (default: 2).
        timeout: Seconds before transitioning from open to half-open (default: 30.0).
        half_open_max_calls: Max concurrent calls allowed in half-open state (default: 1).
    """

    failure_threshold: int = 5
    success_threshold: int = 2
    timeout: float = 30.0
    half_open_max_calls: int = 1


class CircuitBreaker:
    """Thread-safe circuit breaker implementation.

    States:
        - CLOSED: Normal operation. Failures are counted.
        - OPEN: Circuit is tripped. All requests fail fast with CircuitBreakerOpenError.
        - HALF_OPEN: Testing recovery. Limited requests allowed.

    State Transitions:
        CLOSED -> OPEN: When failure_threshold is reached.
        OPEN -> HALF_OPEN: After timeout expires.
        HALF_OPEN -> CLOSED: When success_threshold is reached.
        HALF_OPEN -> OPEN: On any failure.

    Args:
        name: Unique identifier for this circuit breaker.
        config: Circuit breaker configuration.

    Example:
        >>> breaker = CircuitBreaker("elasticsearch", CircuitBreakerConfig(failure_threshold=3))
        >>> if breaker.can_execute():
        ...     try:
        ...         result = perform_operation()
        ...         breaker.record_success()
        ...     except Exception as e:
        ...         breaker.record_failure()
        ...         raise
    """

    def __init__(
        self,
        name: str,
        config: CircuitBreakerConfig | None = None,
    ) -> None:
        self.name = name
        self.config = config or CircuitBreakerConfig()

        self._state = CircuitState.CLOSED
        self._failure_count = 0
        self._success_count = 0
        self._last_failure_time: float | None = None
        self._half_open_calls = 0
        self._lock = threading.Lock()

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

    @property
    def failure_count(self) -> int:
        """Get current failure count."""
        with self._lock:
            return self._failure_count

    def _check_state_transition(self) -> None:
        """Check and perform automatic state transitions (must hold lock)."""
        if self._state == CircuitState.OPEN and self._last_failure_time is not None:
            elapsed = time.monotonic() - self._last_failure_time
            if elapsed >= self.config.timeout:
                logger.info(
                    f"Circuit '{self.name}' transitioning from OPEN to HALF_OPEN",
                    circuit=self.name,
                    elapsed_seconds=round(elapsed, 2),
                )
                self._state = CircuitState.HALF_OPEN
                self._half_open_calls = 0
                self._success_count = 0

    def can_execute(self) -> bool:
        """Check if a request can be executed.

        Returns:
            True if the circuit allows the request to proceed.
        """
        with self._lock:
            self._check_state_transition()

            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

            # OPEN state
            return False

    def record_success(self) -> None:
        """Record a successful operation."""
        with self._lock:
            if self._state == CircuitState.HALF_OPEN:
                self._success_count += 1
                if self._success_count >= self.config.success_threshold:
                    logger.info(
                        f"Circuit '{self.name}' closing after recovery",
                        circuit=self.name,
                        success_count=self._success_count,
                    )
                    self._state = CircuitState.CLOSED
                    self._failure_count = 0
                    self._success_count = 0
            elif self._state == CircuitState.CLOSED:
                # Reset failure count on success in closed state
                self._failure_count = 0

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

            if self._state == CircuitState.HALF_OPEN:
                logger.warning(
                    f"Circuit '{self.name}' re-opening after failure in HALF_OPEN state",
                    circuit=self.name,
                )
                self._state = CircuitState.OPEN
                self._half_open_calls = 0
            elif self._state == CircuitState.CLOSED:
                if self._failure_count >= self.config.failure_threshold:
                    logger.warning(
                        f"Circuit '{self.name}' opening after {self._failure_count} failures",
                        circuit=self.name,
                        failure_count=self._failure_count,
                        threshold=self.config.failure_threshold,
                    )
                    self._state = CircuitState.OPEN

    def get_retry_after(self) -> float | None:
        """Get seconds until circuit might transition to half-open.

        Returns:
            Seconds until retry is possible, or None if circuit is not open.
        """
        with self._lock:
            if self._state != CircuitState.OPEN:
                return None
            if self._last_failure_time is None:
                return self.config.timeout
            elapsed = time.monotonic() - self._last_failure_time
            return max(0, self.config.timeout - elapsed)

    def reset(self) -> None:
        """Reset circuit breaker to initial closed state."""
        with self._lock:
            logger.info(f"Circuit '{self.name}' manually reset", circuit=self.name)
            self._state = CircuitState.CLOSED
            self._failure_count = 0
            self._success_count = 0
            self._last_failure_time = None
            self._half_open_calls = 0


# Global registry of circuit breakers
_circuit_breakers: dict[str, CircuitBreaker] = {}
_registry_lock = threading.Lock()


def get_circuit_breaker(
    name: str,
    config: CircuitBreakerConfig | None = None,
) -> CircuitBreaker:
    """Get or create a circuit breaker by name.

    Args:
        name: Unique name for the circuit breaker.
        config: Configuration (only used when creating new breaker).

    Returns:
        CircuitBreaker instance.

    Example:
        >>> breaker = get_circuit_breaker("elasticsearch-main")
        >>> if breaker.can_execute():
        ...     # proceed with operation
    """
    with _registry_lock:
        if name not in _circuit_breakers:
            _circuit_breakers[name] = CircuitBreaker(name, config)
        return _circuit_breakers[name]


def reset_circuit_breaker(name: str) -> bool:
    """Reset a circuit breaker by name.

    Args:
        name: Name of the circuit breaker to reset.

    Returns:
        True if breaker was found and reset, False otherwise.
    """
    with _registry_lock:
        if name in _circuit_breakers:
            _circuit_breakers[name].reset()
            return True
        return False


def reset_all_circuit_breakers() -> int:
    """Reset all circuit breakers.

    Returns:
        Number of breakers reset.
    """
    with _registry_lock:
        count = len(_circuit_breakers)
        for breaker in _circuit_breakers.values():
            breaker.reset()
        return count


def with_circuit_breaker(
    name: str,
    config: CircuitBreakerConfig | None = None,
) -> Callable[[Callable[..., T]], Callable[..., T]]:
    """Decorator to add circuit breaker protection to a sync function.

    Args:
        name: Circuit breaker name (used for shared state).
        config: Circuit breaker configuration.

    Returns:
        Decorated function with circuit breaker protection.

    Raises:
        CircuitBreakerOpenError: When circuit is open.

    Example:
        >>> @with_circuit_breaker("elasticsearch")
        ... def search(query: str) -> dict:
        ...     return client.search(query=query)
    """

    def decorator(func: Callable[..., T]) -> Callable[..., T]:
        @wraps(func)
        def wrapper(*args: Any, **kwargs: Any) -> T:
            breaker = get_circuit_breaker(name, config)

            if not breaker.can_execute():
                retry_after = breaker.get_retry_after()
                raise CircuitBreakerOpenError(
                    f"Circuit breaker '{name}' is open",
                    retry_after=retry_after,
                    details={"circuit_name": name, "state": breaker.state.value},
                )

            try:
                result = func(*args, **kwargs)
                breaker.record_success()
                return result
            except Exception:
                breaker.record_failure()
                raise

        return wrapper

    return decorator


def with_circuit_breaker_async(
    name: str,
    config: CircuitBreakerConfig | None = None,
) -> Callable[[Callable[..., Awaitable[T]]], Callable[..., Awaitable[T]]]:
    """Decorator to add circuit breaker protection to an async function.

    Args:
        name: Circuit breaker name (used for shared state).
        config: Circuit breaker configuration.

    Returns:
        Decorated async function with circuit breaker protection.

    Raises:
        CircuitBreakerOpenError: When circuit is open.

    Example:
        >>> @with_circuit_breaker_async("elasticsearch")
        ... async def search(query: str) -> dict:
        ...     return await client.search(query=query)
    """

    def decorator(func: Callable[..., Awaitable[T]]) -> Callable[..., Awaitable[T]]:
        @wraps(func)
        async def wrapper(*args: Any, **kwargs: Any) -> T:
            breaker = get_circuit_breaker(name, config)

            if not breaker.can_execute():
                retry_after = breaker.get_retry_after()
                raise CircuitBreakerOpenError(
                    f"Circuit breaker '{name}' is open",
                    retry_after=retry_after,
                    details={"circuit_name": name, "state": breaker.state.value},
                )

            try:
                result = await func(*args, **kwargs)
                breaker.record_success()
                return result
            except Exception:
                breaker.record_failure()
                raise

        return wrapper

    return decorator


__all__ = [
    "CircuitBreaker",
    "CircuitBreakerConfig",
    "CircuitState",
    "get_circuit_breaker",
    "reset_all_circuit_breakers",
    "reset_circuit_breaker",
    "with_circuit_breaker",
    "with_circuit_breaker_async",
]
