"""Retry logic with exponential backoff for transient API failures.

Handles RateLimitError (429) using the server-provided Retry-After value,
and CertiSigmaError with status_code >= 500 using exponential backoff
with jitter to prevent thundering herd.

Permanent failures (401, 403, 400/422) are raised immediately.
"""

from __future__ import annotations

import logging
import random
import time
from typing import Any, Callable, TypeVar

from certisigma import (
    AuthenticationError,
    CertiSigmaError,
    QuotaExceededError,
    RateLimitError,
)

logger = logging.getLogger(__name__)

T = TypeVar("T")

DEFAULT_MAX_RETRIES = 3
DEFAULT_BASE_DELAY = 1.0
MAX_BACKOFF = 120.0


def retry_api_call(
    fn: Callable[..., T],
    *args: Any,
    max_retries: int = DEFAULT_MAX_RETRIES,
    base_delay: float = DEFAULT_BASE_DELAY,
    **kwargs: Any,
) -> T:
    """Call *fn* with retry on transient failures.

    - RateLimitError (429): waits ``exc.retry_after`` seconds.
    - Server errors (5xx): exponential backoff ``base_delay * 2^attempt + jitter``.
    - AuthenticationError, QuotaExceededError: raised immediately (permanent).
    """
    last_exc: Exception | None = None

    for attempt in range(max_retries + 1):
        try:
            return fn(*args, **kwargs)
        except (AuthenticationError, QuotaExceededError):
            raise
        except RateLimitError as exc:
            last_exc = exc
            if attempt >= max_retries:
                break
            wait = getattr(exc, "retry_after", None)
            if not isinstance(wait, (int, float)) or wait <= 0:
                wait = min(base_delay * (2 ** attempt) + random.uniform(0, 1), MAX_BACKOFF)
            else:
                wait = min(wait, MAX_BACKOFF)
            logger.warning(
                "Rate limited (429), attempt %d/%d — waiting %.1fs",
                attempt + 1, max_retries, wait,
            )
            time.sleep(wait)
        except CertiSigmaError as exc:
            last_exc = exc
            if exc.status_code is not None and exc.status_code >= 500:
                if attempt >= max_retries:
                    break
                wait = min(base_delay * (2 ** attempt) + random.uniform(0, 1), MAX_BACKOFF)
                logger.warning(
                    "Server error %d, attempt %d/%d — retrying in %.1fs",
                    exc.status_code, attempt + 1, max_retries, wait,
                )
                time.sleep(wait)
            else:
                raise

    raise last_exc  # type: ignore[misc]
