"""Exponential backoff retry for transient AI provider errors.

Retries on:
  - HTTP 429 (rate limit)
  - HTTP 5xx (server errors)
  - ConnectionError, TimeoutError, OSError
"""

from __future__ import annotations

import logging
import random
import time
from collections.abc import Callable
from typing import Any, TypeVar

log = logging.getLogger(__name__)

T = TypeVar("T")

MAX_RETRIES = 3
BASE_DELAY = 1.0  # seconds
MAX_DELAY = 20.0  # seconds
BACKOFF_FACTOR = 2.0

_RATE_LIMIT_INDICATORS = ("429", "rate limit", "rate_limit", "too many requests", "quota")
_SERVER_ERROR_CODES = ("500", "502", "503", "504")


def is_rate_limit_error(exc: BaseException) -> bool:
    """Return True if exc looks like an HTTP 429 / rate limit error."""
    msg = str(exc).lower()
    return any(indicator in msg for indicator in _RATE_LIMIT_INDICATORS)


def is_transient_error(exc: BaseException) -> bool:
    """Return True if the error is likely temporary and worth retrying."""
    if is_rate_limit_error(exc):
        return True
    if isinstance(exc, (ConnectionError, TimeoutError, OSError)):
        return True
    msg = str(exc).lower()
    return any(code in msg for code in _SERVER_ERROR_CODES)


def retry_with_backoff(
    fn: Callable[..., T],
    *args: Any,
    max_retries: int = MAX_RETRIES,
    base_delay: float = BASE_DELAY,
    max_delay: float = MAX_DELAY,
    backoff_factor: float = BACKOFF_FACTOR,
    **kwargs: Any,
) -> T:
    """Call *fn* with exponential backoff on transient errors.

    Raises the last exception if all retries are exhausted.
    Non-transient exceptions are re-raised immediately.
    """
    last_exc: BaseException | None = None

    for attempt in range(max_retries + 1):
        try:
            return fn(*args, **kwargs)
        except BaseException as exc:
            if not is_transient_error(exc) or attempt == max_retries:
                raise

            base = min(base_delay * (backoff_factor**attempt), max_delay)
            # Random jitter (50-150%) to avoid CSCA detecting retry patterns
            delay = base * random.uniform(0.5, 1.5)
            log.warning(
                "Transient error on attempt %d/%d: %s — retrying in %.1fs",
                attempt + 1,
                max_retries,
                exc,
                delay,
            )
            last_exc = exc
            time.sleep(delay)

    # Should never reach here, but satisfy the type checker
    assert last_exc is not None  # pragma: no cover
    raise last_exc  # pragma: no cover
