"""Circuit Breaker for multi-account AWS operations.

Prevents cascade failures when one AWS account is throttled or unavailable.
States: CLOSED (normal) → OPEN (failing) → HALF_OPEN (testing recovery)

Usage:
    breaker = CircuitBreaker(failure_threshold=3, recovery_timeout=60)

    for account_id in accounts:
        try:
            with breaker.protect(account_id):
                result = scan_account(account_id)
                results.append(result)
        except CircuitBreakerOpenError as e:
            logger.warning(f"Skipping {account_id}: circuit open after {e.failure_count} failures")
            skipped.append(account_id)
"""

import threading
import time
from dataclasses import dataclass, field
from enum import Enum
from typing import List


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


class CircuitBreakerOpenError(Exception):
    """Raised when circuit is open and operation is rejected."""

    def __init__(self, account_id: str, failure_count: int, failed_accounts: List[str]):
        self.account_id = account_id
        self.failure_count = failure_count
        self.failed_accounts = failed_accounts
        super().__init__(f"Circuit open for {account_id} after {failure_count} consecutive failures")


@dataclass
class CircuitBreaker:
    """Thread-safe Circuit Breaker with per-account tracking."""

    failure_threshold: int = 3
    recovery_timeout: float = 60.0

    _state: CircuitState = field(default=CircuitState.CLOSED, init=False)
    _consecutive_failures: int = field(default=0, init=False)
    _failed_accounts: List[str] = field(default_factory=list, init=False)
    _last_failure_time: float = field(default=0.0, init=False)
    _lock: threading.RLock = field(default_factory=threading.RLock, init=False)

    def protect(self, account_id: str) -> "_CircuitBreakerContext":
        """Context manager that protects an operation with circuit breaker logic."""
        return _CircuitBreakerContext(self, account_id)

    def record_success(self) -> None:
        """Record a successful operation; transition to CLOSED."""
        with self._lock:
            self._consecutive_failures = 0
            self._state = CircuitState.CLOSED

    def record_failure(self, account_id: str) -> None:
        """Record a failed operation; open circuit when threshold is reached."""
        with self._lock:
            self._consecutive_failures += 1
            self._last_failure_time = time.time()
            if account_id not in self._failed_accounts:
                self._failed_accounts.append(account_id)
            if self._consecutive_failures >= self.failure_threshold:
                self._state = CircuitState.OPEN

    def should_allow(self, account_id: str) -> bool:
        """Return True if the operation should be allowed to proceed."""
        with self._lock:
            if self._state == CircuitState.CLOSED:
                return True
            if self._state == CircuitState.OPEN:
                if time.time() - self._last_failure_time >= self.recovery_timeout:
                    self._state = CircuitState.HALF_OPEN
                    return True
                return False
            # HALF_OPEN: allow one probe
            return True

    @property
    def state(self) -> CircuitState:
        """Current circuit state."""
        with self._lock:
            return self._state

    @property
    def failed_accounts(self) -> List[str]:
        """Copy of accounts that have recorded failures."""
        with self._lock:
            return list(self._failed_accounts)

    def reset(self) -> None:
        """Reset all state back to CLOSED with zero failure counts."""
        with self._lock:
            self._state = CircuitState.CLOSED
            self._consecutive_failures = 0
            self._failed_accounts.clear()
            self._last_failure_time = 0.0


class _CircuitBreakerContext:
    """Internal context manager returned by CircuitBreaker.protect()."""

    def __init__(self, breaker: CircuitBreaker, account_id: str) -> None:
        self._breaker = breaker
        self._account_id = account_id

    def __enter__(self) -> "_CircuitBreakerContext":
        if not self._breaker.should_allow(self._account_id):
            raise CircuitBreakerOpenError(
                self._account_id,
                self._breaker._consecutive_failures,
                self._breaker.failed_accounts,
            )
        return self

    def __exit__(self, exc_type: type, exc_val: BaseException, exc_tb: object) -> bool:
        if exc_type is None:
            self._breaker.record_success()
        elif exc_type is not CircuitBreakerOpenError:
            self._breaker.record_failure(self._account_id)
        return False  # Never suppress exceptions
