"""Retry logic with exponential backoff."""
from __future__ import annotations

import asyncio
import logging
import random
from typing import Any

import httpx

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

RETRYABLE_STATUS_CODES = frozenset({408, 429, 500, 502, 503, 504})
RETRYABLE_EXCEPTIONS = (httpx.ConnectError, httpx.ReadError, httpx.WriteError, httpx.PoolTimeout)


def should_retry_response(response: httpx.Response) -> bool:
    """Whether this response qualifies for automatic retry."""
    return response.status_code in RETRYABLE_STATUS_CODES


def should_retry_exception(exc: Exception) -> bool:
    """Whether this exception qualifies for automatic retry."""
    return isinstance(exc, RETRYABLE_EXCEPTIONS)


def calculate_delay(
    attempt: int,
    retry_after: str | None = None,
    *,
    base: float = 0.5,
    max_delay: float = 30.0,
) -> float:
    """Calculate retry delay with exponential backoff + jitter.

    If the server sent Retry-After, we respect it (capped at max_delay).
    Otherwise: delay = min(base * 2^attempt + jitter, max_delay)
    """
    if retry_after is not None:
        try:
            server_delay = float(retry_after)
            return min(server_delay, max_delay)
        except ValueError:
            pass

    exp_delay = base * (2 ** attempt)
    jitter = random.random() * base  # noqa: S311
    return min(exp_delay + jitter, max_delay)


async def execute_with_retry(
    client: httpx.AsyncClient,
    request_kwargs: dict[str, Any],
    *,
    max_retries: int,
) -> httpx.Response:
    """Execute an HTTP request with automatic retry on transient failures.

    Returns the final httpx.Response (caller is responsible for checking status).
    """
    last_exc: Exception | None = None

    for attempt in range(max_retries + 1):
        try:
            response = await client.request(**request_kwargs)

            if not should_retry_response(response) or attempt >= max_retries:
                return response

            # Retryable status — back off and retry
            retry_after = response.headers.get("Retry-After")
            delay = calculate_delay(attempt, retry_after)
            logger.debug(
                "Retry %d/%d for %s %s (status=%d, delay=%.2fs)",
                attempt + 1,
                max_retries,
                request_kwargs.get("method", "?"),
                request_kwargs.get("url", "?"),
                response.status_code,
                delay,
            )
            await asyncio.sleep(delay)

        except RETRYABLE_EXCEPTIONS as exc:
            last_exc = exc
            if attempt >= max_retries:
                raise
            delay = calculate_delay(attempt)
            logger.debug(
                "Retry %d/%d for %s %s (error=%s, delay=%.2fs)",
                attempt + 1,
                max_retries,
                request_kwargs.get("method", "?"),
                request_kwargs.get("url", "?"),
                type(exc).__name__,
                delay,
            )
            await asyncio.sleep(delay)

    # Should not reach here, but satisfy type checker
    if last_exc is not None:
        raise last_exc
    raise RuntimeError("Retry loop exited unexpectedly")  # pragma: no cover
