# Auto-generated by DjangoCFG - see CLAUDE.md

"""
Retry Configuration and Utilities

Provides automatic retry logic for failed HTTP requests using tenacity.
Retries only on network errors and server errors (5xx), not client errors (4xx).
"""

from __future__ import annotations

import logging
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any

import httpx
from tenacity import (
    RetryCallState,
    before_sleep_log,
    retry,
    retry_if_exception,
    stop_after_attempt,
    wait_exponential,
)


@dataclass
class RetryConfig:
    """
    Retry configuration options.

    Uses exponential backoff with jitter by default to avoid thundering herd.
    """

    max_attempts: int = 3
    """Maximum number of retry attempts (default: 3)"""

    min_wait: float = 1.0
    """Minimum wait time between retries in seconds (default: 1.0)"""

    max_wait: float = 60.0
    """Maximum wait time between retries in seconds (default: 60.0)"""

    multiplier: float = 2.0
    """Exponential backoff multiplier (default: 2.0)"""

    on_retry: Callable[[RetryCallState], None] | None = None
    """Callback called on each retry attempt"""

    logger: logging.Logger | None = None
    """Logger for retry attempts (default: None)"""


DEFAULT_RETRY_CONFIG = RetryConfig()
"""Default retry configuration"""


def should_retry(exception: BaseException) -> bool:
    """
    Determine if an error should trigger a retry.

    Retries on:
    - Network errors (connection refused, timeout, etc.)
    - Server errors (5xx status codes)
    - Rate limiting (429 status code)

    Does NOT retry on:
    - Client errors (4xx except 429)
    - Authentication errors (401, 403)
    - Not found (404)

    Args:
        exception: The exception to check

    Returns:
        True if should retry, False otherwise
    """
    # Always retry network errors
    if isinstance(
        exception,
        httpx.NetworkError
        | httpx.TimeoutException
        | httpx.ConnectError
        | httpx.ReadError
        | httpx.WriteError
        | httpx.PoolTimeout,
    ):
        return True

    # For HTTP errors, check status code
    if isinstance(exception, httpx.HTTPStatusError):
        status = exception.response.status_code

        # Retry on 5xx server errors
        if 500 <= status < 600:
            return True

        # Retry on 429 (rate limit)
        if status == 429:
            return True

        # Do NOT retry on 4xx client errors
        return False

    # Don't retry on unknown errors
    return False


def create_retry_decorator(config: RetryConfig | None = None):
    """
    Create a retry decorator with the given configuration.

    Args:
        config: Retry configuration (uses defaults if None)

    Returns:
        Tenacity retry decorator

    Example:
        >>> retry_decorator = create_retry_decorator(RetryConfig(max_attempts=5))
        >>> @retry_decorator
        ... async def fetch_data():
        ...     async with httpx.AsyncClient() as client:
        ...         response = await client.get('https://api.example.com/users')
        ...         response.raise_for_status()
        ...         return response.json()
    """
    cfg = config or DEFAULT_RETRY_CONFIG

    # Build retry decorator
    retry_args = {
        'stop': stop_after_attempt(cfg.max_attempts),
        'wait': wait_exponential(
            multiplier=cfg.multiplier,
            min=cfg.min_wait,
            max=cfg.max_wait,
        ),
        'retry': retry_if_exception(should_retry),
        'reraise': True,
    }

    # Add logger if provided
    if cfg.logger:
        retry_args['before_sleep'] = before_sleep_log(cfg.logger, logging.WARNING)

    # Add custom callback if provided
    if cfg.on_retry:
        original_before_sleep = retry_args.get('before_sleep')

        def combined_before_sleep(retry_state: RetryCallState):
            if original_before_sleep:
                original_before_sleep(retry_state)
            if cfg.on_retry:
                cfg.on_retry(retry_state)

        retry_args['before_sleep'] = combined_before_sleep

    return retry(**retry_args)


async def with_retry(
    fn: Callable[..., Any],
    config: RetryConfig | None = None,
    *args,
    **kwargs
) -> Any:
    """
    Execute an async function with retry logic.

    Args:
        fn: Async function to retry
        config: Retry configuration (uses defaults if None)
        *args: Positional arguments for fn
        **kwargs: Keyword arguments for fn

    Returns:
        Result of the function

    Example:
        >>> async def fetch_users():
        ...     async with httpx.AsyncClient() as client:
        ...         response = await client.get('https://api.example.com/users')
        ...         response.raise_for_status()
        ...         return response.json()
        >>>
        >>> result = await with_retry(fetch_users, RetryConfig(max_attempts=5))
    """
    retry_decorator = create_retry_decorator(config)
    retryable_fn = retry_decorator(fn)
    return await retryable_fn(*args, **kwargs)


class RetryAsyncClient:
    """
    HTTP client wrapper that adds automatic retry logic.

    Wraps httpx.AsyncClient and applies retry logic to all HTTP methods.
    Transparently retries on network errors, 5xx status codes, and 429 rate limits.

    Example:
        >>> cfg = RetryConfig(max_attempts=5)
        >>> async with RetryAsyncClient('https://api.example.com', retry_config=cfg) as c:
        ...     response = await c.get('/users')
        ...     response.raise_for_status()
    """

    def __init__(
        self,
        base_url: str | None = None,
        retry_config: RetryConfig | None = None,
        **kwargs: Any
    ):
        """
        Initialize retry-enabled HTTP client.

        Args:
            base_url: Base URL for all requests
            retry_config: Retry configuration (None to disable retry)
            **kwargs: Additional httpx.AsyncClient kwargs
        """
        self._client = httpx.AsyncClient(base_url=base_url, **kwargs)
        self.retry_config = retry_config
        self._retry_decorator = create_retry_decorator(retry_config) if retry_config else None

    async def __aenter__(self) -> RetryAsyncClient:
        await self._client.__aenter__()
        return self

    async def __aexit__(self, *args: Any) -> None:
        await self._client.__aexit__(*args)

    async def aclose(self) -> None:
        """Close the HTTP client."""
        await self._client.aclose()

    def _wrap_with_retry(self, method: str):
        """Wrap HTTP method with retry logic."""
        original_method = getattr(self._client, method)

        if self._retry_decorator:
            async def wrapped(*args, **kwargs):
                @self._retry_decorator
                async def _do_request():
                    return await original_method(*args, **kwargs)
                return await _do_request()
            return wrapped
        else:
            return original_method

    async def get(self, *args, **kwargs) -> httpx.Response:
        """GET request with retry."""
        return await self._wrap_with_retry('get')(*args, **kwargs)

    async def post(self, *args, **kwargs) -> httpx.Response:
        """POST request with retry."""
        return await self._wrap_with_retry('post')(*args, **kwargs)

    async def put(self, *args, **kwargs) -> httpx.Response:
        """PUT request with retry."""
        return await self._wrap_with_retry('put')(*args, **kwargs)

    async def patch(self, *args, **kwargs) -> httpx.Response:
        """PATCH request with retry."""
        return await self._wrap_with_retry('patch')(*args, **kwargs)

    async def delete(self, *args, **kwargs) -> httpx.Response:
        """DELETE request with retry."""
        return await self._wrap_with_retry('delete')(*args, **kwargs)

    async def head(self, *args, **kwargs) -> httpx.Response:
        """HEAD request with retry."""
        return await self._wrap_with_retry('head')(*args, **kwargs)

    async def options(self, *args, **kwargs) -> httpx.Response:
        """OPTIONS request with retry."""
        return await self._wrap_with_retry('options')(*args, **kwargs)