"""Retry manager with exponential backoff for failed requests.

RetryManager handles exponential backoff retry logic for failed requests.

Retry behavior:
- Retries on network errors and 5xx server errors
- Does NOT retry on 4xx client errors (invalid data, auth failures, etc.)
- Uses exponential backoff with max delay of 30 seconds
- Maximum 5 attempts by default
"""

import asyncio
from typing import Callable, TypeVar

import httpx

T = TypeVar("T")


class RetryManager:
    """Retry manager with exponential backoff."""

    def __init__(self, max_retries: int = 5) -> None:
        """Initialize retry manager.

        Args:
            max_retries: Maximum number of retry attempts (default: 5)
        """
        self._max_retries = max_retries

    async def execute(self, fn: Callable[[], T], attempt: int = 0) -> T:
        """Execute a function with retry logic.

        Args:
            fn: Async function to execute
            attempt: Current attempt number (internal use)

        Returns:
            Result of the function

        Raises:
            Exception: If all retries are exhausted or on non-retryable error
        """
        try:
            return await fn()
        except Exception as error:
            # Don't retry if max attempts reached
            if attempt >= self._max_retries:
                raise

            # Don't retry on 4xx errors (client errors)
            if self._is_client_error(error):
                raise

            # Calculate exponential backoff
            # Formula: min(1.0 * 2^attempt, 30.0) seconds
            backoff = min(1.0 * (2**attempt), 30.0)

            # Wait before retrying
            await asyncio.sleep(backoff)

            # Retry with incremented attempt counter
            return await self.execute(fn, attempt + 1)

    @staticmethod
    def _is_client_error(error: Exception) -> bool:
        """Check if error is a client error (4xx) that should not be retried.

        Args:
            error: Exception to check

        Returns:
            True if error is a 4xx client error
        """
        if isinstance(error, httpx.HTTPStatusError):
            return 400 <= error.response.status_code < 500
        return False

    @staticmethod
    def calculate_backoff(attempt: int) -> float:
        """Calculate backoff delay for a given attempt number.

        Useful for testing or displaying retry delays to users.

        Args:
            attempt: Attempt number (0-indexed)

        Returns:
            Backoff delay in seconds
        """
        return min(1.0 * (2**attempt), 30.0)
