import time
from secrets import SystemRandom
from enum import Enum
from typing import Callable, Any, TypeVar, Optional
from functools import wraps
import logging

logger = logging.getLogger(__name__)

T = TypeVar("T")


class CircuitState(Enum):
    CLOSED = "closed"
    OPEN = "open"
    HALF_OPEN = "half_open"


class CircuitBreakerOpenException(Exception):
    pass


class CircuitBreaker:
    """
    Circuit breaker implementation with exponential backoff and jitter.
    """
    def __init__(
        self,
        name: str = "default",
        failure_threshold: int = 5,
        recovery_timeout: float = 60.0,
        expected_exceptions: tuple[type[Exception], ...] = (Exception,),
        fallback_function: Optional[Callable[..., T]] = None,
    ):
        self.name = name
        self.failure_threshold = failure_threshold
        self.recovery_timeout = recovery_timeout
        self.expected_exceptions = expected_exceptions
        self.fallback_function = fallback_function

        self._state = CircuitState.CLOSED
        self._failure_count = 0
        self._last_failure_time = 0.0
        self._next_attempt_time = 0.0

    @property
    def state(self) -> CircuitState:
        return self._state

    def call(self, func: Callable[..., T], *args: Any, **kwargs: Any) -> T:
        if self._state == CircuitState.OPEN:
            if time.time() >= self._next_attempt_time:
                self._state = CircuitState.HALF_OPEN
                logger.info(f"Circuit {self.name} entering HALF_OPEN state")
            else:
                if self.fallback_function:
                    return self.fallback_function(*args, **kwargs)
                raise CircuitBreakerOpenException(f"Circuit {self.name} is OPEN")

        # Retry logic
        retries = 3
        backoff = 0.1
        last_exception = None
        rng = SystemRandom()

        for attempt in range(retries + 1):
            try:
                result = func(*args, **kwargs)
                self._handle_success()
                return result
            except self.expected_exceptions as e:
                last_exception = e
                if attempt < retries:
                    sleep_time = backoff * (2 ** attempt) + rng.uniform(0, 0.1)
                    logger.warning(f"Circuit {self.name} retry {attempt + 1}/{retries} after error: {e}")
                    time.sleep(sleep_time)
                else:
                    # Final attempt failed
                    self._handle_failure(e)

        # Should be unreachable if retries > 0 and exceptions are raised
        if self.fallback_function:
            return self.fallback_function(*args, **kwargs)
        if last_exception:
            raise last_exception
        raise Exception("Circuit breaker failed without exception")

    def _handle_failure(self, exception: Exception) -> None:
        self._failure_count += 1
        self._last_failure_time = time.time()

        logger.warning(f"Circuit {self.name} failure ({self._failure_count}/{self.failure_threshold}): {exception}")

        if self._state == CircuitState.HALF_OPEN or self._failure_count >= self.failure_threshold:
            self._state = CircuitState.OPEN
            # Exponential backoff could be added here, currently using fixed timeout
            self._next_attempt_time = time.time() + self.recovery_timeout
            logger.error(f"Circuit {self.name} opened! Next attempt in {self.recovery_timeout}s")

    def _handle_success(self) -> None:
        if self._state != CircuitState.CLOSED:
            logger.info(f"Circuit {self.name} closed (recovered)")
            self._state = CircuitState.CLOSED
            self._failure_count = 0

    def __call__(self, func: Callable[..., T]) -> Callable[..., T]:
        @wraps(func)
        def wrapper(*args: Any, **kwargs: Any) -> T:
            return self.call(func, *args, **kwargs)
        return wrapper
