"""Resilience patterns for fair task queue.

This module includes:
- Circuit breaker for protecting against cascading failures
- Retry strategies for handling task failures
"""

from datetime import UTC, datetime, timedelta
from enum import Enum

from pydantic import BaseModel

from haolib.background.fair.abstract import FairTask

# ============================================================================
# Circuit Breaker
# ============================================================================


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

    CLOSED = "closed"  # Normal operation
    OPEN = "open"  # Failing, reject requests
    HALF_OPEN = "half_open"  # Testing if service recovered


class CircuitBreaker(BaseModel):
    """Circuit breaker for protecting against cascading failures.

    Implements a simple circuit breaker pattern:
    - CLOSED: Normal operation, requests pass through
    - OPEN: Too many failures, reject requests immediately
    - HALF_OPEN: Testing if service recovered, allow limited requests

    Example:
        ```python
        breaker = CircuitBreaker(
            failure_threshold=5,
            timeout=timedelta(seconds=60),
            half_open_max_requests=3,
        )

        if breaker.can_proceed():
            try:
                result = await storage.operation()
                breaker.record_success()
            except Exception as e:
                breaker.record_failure()
                raise
        else:
            raise CircuitBreakerOpenError("Circuit breaker is open")
        ```

    """

    failure_threshold: int = 5
    timeout: timedelta = timedelta(seconds=60)
    half_open_max_requests: int = 3

    _state: CircuitState = CircuitState.CLOSED
    _failure_count: int = 0
    _last_failure_time: datetime | None = None
    _half_open_requests: int = 0

    def can_proceed(self) -> bool:
        """Check if request can proceed.

        Returns:
            True if request can proceed, False otherwise

        """
        now = datetime.now(UTC)

        # If circuit is closed, allow requests
        if self._state == CircuitState.CLOSED:
            return True

        # If circuit is open, check if timeout has passed
        if self._state == CircuitState.OPEN:
            if self._last_failure_time and (now - self._last_failure_time) >= self.timeout:
                # Move to half-open state
                self._state = CircuitState.HALF_OPEN
                self._half_open_requests = 0
                return True
            return False

        # If circuit is half-open, allow limited requests
        if self._state == CircuitState.HALF_OPEN:
            return self._half_open_requests < self.half_open_max_requests

        return False

    def record_success(self) -> None:
        """Record a successful operation."""
        if self._state == CircuitState.HALF_OPEN:
            # Success in half-open means service recovered
            self._state = CircuitState.CLOSED
            self._failure_count = 0
            self._half_open_requests = 0
            self._last_failure_time = None
        elif self._state == CircuitState.CLOSED:
            # Reset failure count on success
            self._failure_count = 0

    def record_failure(self) -> None:
        """Record a failed operation."""
        now = datetime.now(UTC)
        self._failure_count += 1
        self._last_failure_time = now

        if self._state == CircuitState.HALF_OPEN:
            # Failure in half-open means service not recovered
            self._state = CircuitState.OPEN
            self._half_open_requests = 0
        elif self._state == CircuitState.CLOSED:
            # Check if we've exceeded threshold
            if self._failure_count >= self.failure_threshold:
                self._state = CircuitState.OPEN

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

        Returns:
            Current circuit breaker state

        """
        return self._state

    def reset(self) -> None:
        """Reset circuit breaker to closed state."""
        self._state = CircuitState.CLOSED
        self._failure_count = 0
        self._half_open_requests = 0
        self._last_failure_time = None


# ============================================================================
# Retry Strategies
# ============================================================================


class ExponentialBackoffRetryStrategy(BaseModel):
    """Exponential backoff retry strategy.

    This is the default retry strategy that uses exponential backoff.
    """

    base_delay: float = 1.0
    multiplier: float = 2.0
    max_delay: float = 300.0  # 5 minutes

    def should_retry(
        self,
        task: FairTask,  # noqa: ARG002
        error: Exception,  # noqa: ARG002
        retry_count: int,
        max_retries: int,
    ) -> bool:
        """Determine if a task should be retried.

        Args:
            task: The task that failed
            error: The exception that occurred
            retry_count: Current retry count
            max_retries: Maximum retry attempts

        Returns:
            True if task should be retried, False otherwise

        """
        return retry_count < max_retries

    def get_backoff_delay(
        self,
        task: FairTask,  # noqa: ARG002
        error: Exception,  # noqa: ARG002
        retry_count: int,
    ) -> float:
        """Calculate backoff delay before retry.

        Args:
            task: The task that failed
            error: The exception that occurred
            retry_count: Current retry count

        Returns:
            Backoff delay in seconds

        """
        delay = self.base_delay * (self.multiplier**retry_count)
        return min(delay, self.max_delay)


class LinearBackoffRetryStrategy(BaseModel):
    """Linear backoff retry strategy."""

    base_delay: float = 1.0
    increment: float = 1.0
    max_delay: float = 300.0  # 5 minutes

    def should_retry(
        self,
        task: FairTask,  # noqa: ARG002
        error: Exception,  # noqa: ARG002
        retry_count: int,
        max_retries: int,
    ) -> bool:
        """Determine if a task should be retried.

        Args:
            task: The task that failed
            error: The exception that occurred
            retry_count: Current retry count
            max_retries: Maximum retry attempts

        Returns:
            True if task should be retried, False otherwise

        """
        return retry_count < max_retries

    def get_backoff_delay(
        self,
        task: FairTask,  # noqa: ARG002
        error: Exception,  # noqa: ARG002
        retry_count: int,
    ) -> float:
        """Calculate backoff delay before retry.

        Args:
            task: The task that failed
            error: The exception that occurred
            retry_count: Current retry count

        Returns:
            Backoff delay in seconds

        """
        delay = self.base_delay + (self.increment * retry_count)
        return min(delay, self.max_delay)


class FixedDelayRetryStrategy(BaseModel):
    """Fixed delay retry strategy."""

    delay: float = 1.0

    def should_retry(
        self,
        task: FairTask,  # noqa: ARG002
        error: Exception,  # noqa: ARG002
        retry_count: int,
        max_retries: int,
    ) -> bool:
        """Determine if a task should be retried.

        Args:
            task: The task that failed
            error: The exception that occurred
            retry_count: Current retry count
            max_retries: Maximum retry attempts

        Returns:
            True if task should be retried, False otherwise

        """
        return retry_count < max_retries

    def get_backoff_delay(
        self,
        task: FairTask,  # noqa: ARG002
        error: Exception,  # noqa: ARG002
        retry_count: int,  # noqa: ARG002
    ) -> float:
        """Calculate backoff delay before retry.

        Args:
            task: The task that failed
            error: The exception that occurred
            retry_count: Current retry count

        Returns:
            Backoff delay in seconds

        """
        return self.delay
