"""Circuit breaker pattern implementation for telemetry export resilience.

This module provides a CircuitBreakerRetryPolicy wrapper that combines circuit breaker
logic with retry policies to prevent cascading failures when the collector is unavailable.

The circuit breaker has three states:
- CLOSED: Normal operation, requests pass through to retry policy
- OPEN: Circuit is "tripped", requests fail immediately without retry (fast-fail)
- HALF_OPEN: Testing recovery, single request attempts to test collector availability

State transitions:
- CLOSED → OPEN: After N consecutive failures (configurable, default: 5)
- OPEN → HALF_OPEN: After timeout period (configurable, default: 60 seconds)
- HALF_OPEN → CLOSED: After successful test request
- HALF_OPEN → OPEN: If test request fails
"""

import logging
import time
from typing import Callable, TypeVar, Any, Dict, Optional

from pybreaker import CircuitBreaker, CircuitBreakerError

logger = logging.getLogger(__name__)

T = TypeVar("T")

# State mapping for OTEL metrics
STATE_VALUES = {"closed": 0, "half_open": 1, "open": 2}


class CircuitBreakerRetryPolicy:
    """Circuit breaker wrapper for RetryPolicy to prevent cascading failures.

    Wraps a RetryPolicy with circuit breaker logic to provide fast-fail behavior
    when the collector is consistently unavailable. This prevents wasted retry
    attempts and protects system resources.

    Example:
        >>> from mca_sdk.buffering.retry import RetryPolicy
        >>> from mca_sdk.resilience.circuit_breaker import CircuitBreakerRetryPolicy
        >>>
        >>> retry_policy = RetryPolicy(max_attempts=3, base_delay=1.0)
        >>> circuit_breaker = CircuitBreakerRetryPolicy(
        ...     retry_policy=retry_policy,
        ...     fail_max=5,
        ...     reset_timeout=60
        ... )
        >>>
        >>> # Execute with circuit breaker protection
        >>> try:
        ...     result = circuit_breaker.execute(export_function, data)
        ... except CircuitBreakerError:
        ...     # Circuit is open, queue telemetry for later
        ...     queue.enqueue(data)

    Attributes:
        _retry_policy: Underlying RetryPolicy for actual export attempts
        _circuit: pybreaker CircuitBreaker instance
        _name: Circuit breaker name for logging/monitoring
        _last_failure_time: Timestamp of last failure (for stats)
    """

    def __init__(
        self,
        retry_policy: Any,
        fail_max: int = 5,
        reset_timeout: int = 60,
        name: str = "otlp_exporter",
    ):
        """Initialize circuit breaker with retry policy.

        Args:
            retry_policy: RetryPolicy instance to wrap
            fail_max: Number of consecutive failures before opening circuit (default: 5)
            reset_timeout: Cool-down period in seconds before entering HALF_OPEN (default: 60)
            name: Circuit breaker name for logging and monitoring

        Raises:
            ValueError: If fail_max <= 0 or reset_timeout <= 0
        """
        if fail_max <= 0:
            raise ValueError(f"fail_max must be positive, got: {fail_max}")

        if reset_timeout <= 0:
            raise ValueError(f"reset_timeout must be positive, got: {reset_timeout}")

        self._retry_policy = retry_policy
        self._name = name
        self._last_failure_time: Optional[float] = None

        # OTEL metrics (initialized later via set_meter)
        self._meter = None
        self._state_gauge = None
        self._transition_counter = None
        self._failure_gauge = None

        # Initialize pybreaker CircuitBreaker
        # Note: pybreaker uses reset_timeout parameter, not reset_timeout
        self._circuit = CircuitBreaker(
            fail_max=fail_max,
            reset_timeout=reset_timeout,
            name=name,
            listeners=[self],  # Pass self as listener
        )

        logger.info(
            f"Circuit breaker initialized: {name}",
            extra={"circuit_name": name, "fail_max": fail_max, "reset_timeout": reset_timeout},
        )

    def execute(self, func: Callable[..., T], *args: Any, **kwargs: Any) -> T:
        """Execute function through circuit breaker with retry policy.

        Behavior by circuit state:
        - CLOSED: Normal operation, call retry_policy.execute(func)
        - OPEN: Fast-fail, raise CircuitBreakerError immediately (<1ms)
        - HALF_OPEN: Test mode, single call without full retry policy

        Args:
            func: Function to execute (typically export function)
            *args: Positional arguments for func
            **kwargs: Keyword arguments for func

        Returns:
            Result of successful function execution

        Raises:
            CircuitBreakerError: If circuit is OPEN (fast-fail)
            Exception: Original exception from func if circuit is CLOSED/HALF_OPEN
        """
        try:
            # Circuit breaker call wraps retry policy execution
            # pybreaker will handle state checks and transitions
            result = self._circuit.call(self._retry_policy.execute, func, *args, **kwargs)
            return result

        except CircuitBreakerError:
            # Circuit is OPEN - fast-fail without retry
            logger.info(
                f"Circuit breaker open for {self._name}, failing fast",
                extra={
                    "circuit_name": self._name,
                    "circuit_state": self.get_state(),
                    "failure_count": self._circuit.fail_counter,
                },
            )
            raise

    def get_state(self) -> str:
        """Get current circuit breaker state.

        Returns:
            Current state as string: 'closed', 'open', or 'half_open'
        """
        return self._circuit.current_state

    def get_stats(self) -> Dict[str, Any]:
        """Get circuit breaker statistics for monitoring.

        Returns:
            Dictionary with circuit breaker metrics:
                - state: Current state (closed/open/half_open)
                - failure_count: Current failure counter
                - fail_max: Threshold for opening circuit
                - reset_timeout: Cool-down period in seconds
                - last_failure_time: Timestamp of last failure (Unix timestamp)
                - name: Circuit breaker name

        Example:
            >>> stats = circuit_breaker.get_stats()
            >>> print(f"Circuit state: {stats['state']}")
            >>> print(f"Failures: {stats['failure_count']}/{stats['fail_max']}")
        """
        return {
            "state": self.get_state(),
            "failure_count": self._circuit.fail_counter,
            "fail_max": self._circuit.fail_max,
            "reset_timeout": self._circuit.reset_timeout,
            "last_failure_time": self._last_failure_time,
            "name": self._name,
        }

    def reset(self) -> None:
        """Manually reset circuit breaker to CLOSED state.

        This method is primarily for operational use (manual intervention)
        and testing purposes. Use with caution in production.

        Example:
            >>> # Manual recovery override
            >>> circuit_breaker.reset()
            >>> print(circuit_breaker.get_state())  # 'closed'
        """
        old_state = self.get_state()
        self._circuit.close()
        self._last_failure_time = None

        logger.warning(
            f"Circuit breaker manually reset: {self._name}",
            extra={"circuit_name": self._name, "old_state": old_state, "new_state": "closed"},
        )

    def set_meter(self, meter: Any) -> None:
        """Initialize OTEL metrics for circuit breaker monitoring.

        Must be called after OpenTelemetry providers are set up.

        Args:
            meter: OpenTelemetry Meter instance for creating metrics
        """
        self._meter = meter

        # Create observable gauge for circuit state (0=closed, 1=half_open, 2=open)
        # Using observable gauge with callback is the correct pattern for non-additive values
        self._state_gauge = meter.create_observable_gauge(
            name="circuit_breaker.state",
            callbacks=[self._observe_state],
            description="Current circuit breaker state (0=closed, 1=half_open, 2=open)",
            unit="1",
        )

        # Create counter for state transitions
        self._transition_counter = meter.create_counter(
            name="circuit_breaker.transitions",
            description="Count of circuit breaker state transitions",
            unit="1",
        )

        # Create observable gauge for failure count
        self._failure_gauge = meter.create_observable_gauge(
            name="circuit_breaker.failures",
            callbacks=[self._observe_failures],
            description="Current consecutive failure count",
            unit="1",
        )

    def _observe_state(self, options: Any) -> Any:
        """Observable callback for circuit breaker state gauge.

        This callback is invoked by OTel to collect the current state value.
        """
        from opentelemetry.metrics import Observation

        state = self.get_state()
        state_value = STATE_VALUES.get(state, 0)

        yield Observation(
            value=state_value, attributes={"circuit_name": self._name, "state": state}
        )

    def _observe_failures(self, options: Any) -> Any:
        """Observable callback for circuit breaker failure count gauge.

        This callback is invoked by OTel to collect the current failure count.
        """
        from opentelemetry.metrics import Observation

        yield Observation(value=self._circuit.fail_counter, attributes={"circuit_name": self._name})

    def state_change(self, cb: CircuitBreaker, old_state: str, new_state: str) -> None:
        """Event listener for circuit state transitions (pybreaker callback).

        Logs all state transitions and records OTEL metrics.

        Args:
            cb: CircuitBreaker instance (provided by pybreaker)
            old_state: Previous state
            new_state: New state
        """
        self._last_failure_time = time.time()

        logger.info(
            f"Circuit breaker state transition: {old_state} → {new_state}",
            extra={
                "circuit_name": cb.name,
                "old_state": old_state,
                "new_state": new_state,
                "failure_count": cb.fail_counter,
                "timestamp": self._last_failure_time,
            },
        )

        # Record state transition in OTEL metrics
        if self._transition_counter:
            self._transition_counter.add(
                1, {"circuit_name": cb.name, "from_state": old_state, "to_state": new_state}
            )

        # Note: state and failure gauges use observable callbacks, no manual update needed

    def before_call(self, cb: CircuitBreaker, func, *args, **kwargs) -> None:
        """Called before function execution (pybreaker callback)."""
        pass

    def success(self, cb: CircuitBreaker) -> None:
        """Called after successful function execution (pybreaker callback)."""
        pass

    def failure(self, cb: CircuitBreaker, exception: Exception) -> None:
        """Called after failed function execution (pybreaker callback)."""
        self._last_failure_time = time.time()
