from __future__ import annotations

import asyncio
import random
import time
from dataclasses import dataclass
from typing import Awaitable, Callable, TypeVar

T = TypeVar("T")

RETRYABLE_STATUS_CODES = frozenset({429, 500, 502, 503, 504})


@dataclass(frozen=True)
class RetryConfig:
    max_retries: int = 2
    base_delay_ms: float = 500


def is_retryable_status(status: int) -> bool:
    return status in RETRYABLE_STATUS_CODES


def calculate_delay(attempt: int, base_delay_ms: float = 500) -> float:
    """Return a jittered delay in seconds using full-jitter exponential backoff."""
    exponential_ms = base_delay_ms * (2**attempt)
    return random.random() * exponential_ms / 1000


def parse_retry_after(header: str | None) -> float | None:
    """Parse a Retry-After header value into seconds. Returns None if unparseable."""
    if header is None:
        return None
    try:
        seconds = float(header)
        return max(seconds, 0)
    except ValueError:
        pass
    # Try HTTP-date parsing
    try:
        from email.utils import parsedate_to_datetime

        dt = parsedate_to_datetime(header)
        delta = dt.timestamp() - time.time()
        return max(delta, 0)
    except Exception:
        return None


def with_retry_sync(
    fn: Callable[[], T],
    config: RetryConfig,
    should_retry: Callable[[Exception], tuple[bool, float | None]],
) -> T:
    """Execute fn with synchronous retry logic."""
    last_error: Exception | None = None

    for attempt in range(config.max_retries + 1):
        try:
            return fn()
        except Exception as exc:
            last_error = exc
            if attempt >= config.max_retries:
                break
            retry, retry_after_s = should_retry(exc)
            if not retry:
                break
            delay = retry_after_s if retry_after_s is not None else calculate_delay(attempt, config.base_delay_ms)
            time.sleep(delay)

    raise last_error  # type: ignore[misc]


async def with_retry_async(
    fn: Callable[[], Awaitable[T]],
    config: RetryConfig,
    should_retry: Callable[[Exception], tuple[bool, float | None]],
) -> T:
    """Execute fn with async retry logic."""
    last_error: Exception | None = None

    for attempt in range(config.max_retries + 1):
        try:
            return await fn()
        except Exception as exc:
            last_error = exc
            if attempt >= config.max_retries:
                break
            retry, retry_after_s = should_retry(exc)
            if not retry:
                break
            delay = retry_after_s if retry_after_s is not None else calculate_delay(attempt, config.base_delay_ms)
            await asyncio.sleep(delay)

    raise last_error  # type: ignore[misc]
