import logging
import random
import time
from dataclasses import dataclass
from enum import Enum
from typing import Optional

from .exceptions import RetryExhaustedError

logger = logging.getLogger("nebulascrape.retry")


class RetryAction(str, Enum):
    PASS = "pass"
    WAIT_RETRY = "wait_retry"
    ROTATE_SESSION = "rotate_session"
    SWITCH_TRANSPORT = "switch_transport"
    REBUILD_CONNECTION = "rebuild_connection"
    ABORT = "abort"


@dataclass
class RetryDecision:
    action: RetryAction
    backoff_seconds: float = 0.0
    rotate_session: bool = False
    rebuild_connection: bool = False
    switch_transport: bool = False
    reason: str = ""


class SmartRetryEngine:
    def __init__(self, max_retries: int = 5, base_backoff: float = 1.5):
        self.max_retries = max_retries
        self.base_backoff = base_backoff
        self._attempt: int = 0

    def reset(self):
        self._attempt = 0

    @property
    def attempt(self) -> int:
        return self._attempt

    def _jitter(self, value: float, pct: float = 0.25) -> float:
        spread = value * pct
        return value + random.uniform(-spread, spread)

    def _exponential_backoff(self, attempt: int, cap: float = 120.0) -> float:
        raw = self.base_backoff * (2 ** attempt)
        return self._jitter(min(raw, cap))

    def _parse_retry_after(self, resp) -> Optional[float]:
        ra = resp.headers.get("Retry-After")
        if ra is None:
            return None
        try:
            return float(ra)
        except (ValueError, TypeError):
            return None

    def analyze_response(self, resp, intel_result=None) -> RetryDecision:
        if self._attempt >= self.max_retries:
            return RetryDecision(
                action=RetryAction.ABORT,
                reason=f"Max retries ({self.max_retries}) exhausted"
            )

        status = resp.status_code

        if status == 200:
            self._attempt = 0
            return RetryDecision(action=RetryAction.PASS)

        self._attempt += 1

        if status == 429:
            ra = self._parse_retry_after(resp)
            if ra is not None:
                backoff = self._jitter(ra)
            else:
                backoff = self._exponential_backoff(self._attempt, cap=120.0)
            logger.debug(f"[SmartRetry] 429 Rate Limited — waiting {backoff:.1f}s (attempt {self._attempt})")
            return RetryDecision(
                action=RetryAction.WAIT_RETRY,
                backoff_seconds=backoff,
                rotate_session=self._attempt >= 2,
                reason="Rate limited (429)"
            )

        if status == 403:
            backoff = self._jitter(random.uniform(2.0, 8.0))
            logger.debug(f"[SmartRetry] 403 Forbidden — rotating fingerprint+session, waiting {backoff:.1f}s")
            return RetryDecision(
                action=RetryAction.ROTATE_SESSION,
                backoff_seconds=backoff,
                rotate_session=True,
                rebuild_connection=self._attempt >= 3,
                switch_transport=self._attempt >= 4,
                reason="Access denied (403) — rotating fingerprint"
            )

        if status == 503:
            backoff = self._exponential_backoff(self._attempt, cap=60.0)
            logger.debug(f"[SmartRetry] 503 Service Unavailable — rebuilding connection, waiting {backoff:.1f}s")
            return RetryDecision(
                action=RetryAction.REBUILD_CONNECTION,
                backoff_seconds=backoff,
                rebuild_connection=True,
                switch_transport=self._attempt >= 2,
                reason="Service unavailable (503) — rebuilding connection"
            )

        if status in (407, 408, 502, 504, 520, 521, 522, 523, 524, 525, 526):
            backoff = self._exponential_backoff(self._attempt, cap=90.0)
            return RetryDecision(
                action=RetryAction.WAIT_RETRY,
                backoff_seconds=backoff,
                rotate_session=self._attempt >= 2,
                reason=f"Transient error ({status})"
            )

        if intel_result is not None and intel_result.retry_recommended:
            backoff = self._jitter(3.0 + self._attempt * 2)
            return RetryDecision(
                action=RetryAction.ROTATE_SESSION,
                backoff_seconds=backoff,
                rotate_session=intel_result.rotate_session,
                switch_transport=intel_result.switch_transport,
                reason=f"Intel-driven retry: {intel_result.challenge_type}"
            )

        return RetryDecision(action=RetryAction.PASS)

    def execute_decision(self, decision: RetryDecision):
        if decision.action == RetryAction.ABORT:
            raise RetryExhaustedError(decision.reason)

        if decision.backoff_seconds > 0:
            logger.debug(f"[SmartRetry] Sleeping {decision.backoff_seconds:.2f}s — {decision.reason}")
            time.sleep(decision.backoff_seconds)
