"""Retry logic for transient failures.

Provides exponential backoff retry decorator for handling temporary errors
in network requests, file I/O, and other operations that may fail transiently.
"""

import asyncio
import inspect
import logging
from collections.abc import Callable
from functools import wraps
from typing import Any, TypeVar

logger = logging.getLogger(__name__)

T = TypeVar("T")


class RetryError(Exception):
    """Raised when all retry attempts have been exhausted."""

    def __init__(self, message: str, last_exception: Exception) -> None:
        super().__init__(message)
        self.last_exception = last_exception


def retry(
    max_attempts: int = 3,
    initial_delay: float = 1.0,
    max_delay: float = 30.0,
    exponential_base: float = 2.0,
    exceptions: tuple[type[Exception], ...] = (Exception,),
    log_errors: bool = True,
) -> Callable[[Callable[..., T]], Callable[..., T]]:
    """Retry decorator with exponential backoff.

    Args:
        max_attempts: Maximum number of retry attempts (including first try).
        initial_delay: Initial delay in seconds before first retry.
        max_delay: Maximum delay in seconds between retries.
        exponential_base: Base for exponential backoff calculation.
        exceptions: Tuple of exception types to catch and retry.
        log_errors: Whether to log retry attempts.

    Returns:
        Decorator function.

    Example:
        @retry(max_attempts=3, exceptions=(ConnectionError, TimeoutError))
        def fetch_data():
            # May fail transiently
            return requests.get("https://api.example.com/data")
    """

    def decorator(func: Callable[..., T]) -> Callable[..., T]:
        @wraps(func)
        def sync_wrapper(*args: Any, **kwargs: Any) -> T:
            """Synchronous retry wrapper."""
            last_exception: Exception | None = None
            delay = initial_delay

            for attempt in range(1, max_attempts + 1):
                try:
                    return func(*args, **kwargs)
                except exceptions as e:
                    last_exception = e

                    if attempt == max_attempts:
                        # Final attempt failed
                        if log_errors:
                            logger.error(
                                f"{func.__name__} failed after {max_attempts} attempts: {e}"
                            )
                        raise RetryError(
                            f"Failed after {max_attempts} attempts", last_exception
                        ) from e

                    # Log retry attempt
                    if log_errors:
                        logger.warning(
                            f"{func.__name__} failed (attempt {attempt}/{max_attempts}): {e}. "
                            f"Retrying in {delay:.1f}s..."
                        )

                    # Wait before retry
                    import time

                    time.sleep(delay)

                    # Exponential backoff
                    delay = min(delay * exponential_base, max_delay)

            # Should never reach here
            raise RetryError(
                f"Unexpected retry loop exit after {max_attempts} attempts",
                last_exception or Exception("Unknown error"),
            )

        @wraps(func)
        async def async_wrapper(*args: Any, **kwargs: Any) -> T:
            """Asynchronous retry wrapper."""
            last_exception: Exception | None = None
            delay = initial_delay

            for attempt in range(1, max_attempts + 1):
                try:
                    return await func(*args, **kwargs)
                except exceptions as e:
                    last_exception = e

                    if attempt == max_attempts:
                        # Final attempt failed
                        if log_errors:
                            logger.error(
                                f"{func.__name__} failed after {max_attempts} attempts: {e}"
                            )
                        raise RetryError(
                            f"Failed after {max_attempts} attempts", last_exception
                        ) from e

                    # Log retry attempt
                    if log_errors:
                        logger.warning(
                            f"{func.__name__} failed (attempt {attempt}/{max_attempts}): {e}. "
                            f"Retrying in {delay:.1f}s..."
                        )

                    # Wait before retry
                    await asyncio.sleep(delay)

                    # Exponential backoff
                    delay = min(delay * exponential_base, max_delay)

            # Should never reach here
            raise RetryError(
                f"Unexpected retry loop exit after {max_attempts} attempts",
                last_exception or Exception("Unknown error"),
            )

        # Return appropriate wrapper based on function type
        if inspect.iscoroutinefunction(func):
            return async_wrapper  # type: ignore
        return sync_wrapper  # type: ignore

    return decorator


def retry_on_network_errors(
    max_attempts: int = 3,
    initial_delay: float = 2.0,
) -> Callable[[Callable[..., T]], Callable[..., T]]:
    """Convenience decorator for network-related errors.

    Retries on common network errors like ConnectionError, TimeoutError.

    Args:
        max_attempts: Maximum number of retry attempts.
        initial_delay: Initial delay in seconds.

    Returns:
        Decorator function.

    Example:
        @retry_on_network_errors()
        async def fetch_from_api():
            async with httpx.AsyncClient() as client:
                return await client.get("https://api.example.com")
    """
    network_exceptions = (
        ConnectionError,
        TimeoutError,
        OSError,  # Covers network-related OS errors
    )

    # Try to include httpx/requests exceptions if available
    try:
        import httpx

        network_exceptions = network_exceptions + (
            httpx.ConnectError,
            httpx.TimeoutException,
            httpx.NetworkError,
        )
    except ImportError:
        pass

    try:
        import requests

        network_exceptions = network_exceptions + (
            requests.exceptions.ConnectionError,
            requests.exceptions.Timeout,
            requests.exceptions.ReadTimeout,
        )
    except ImportError:
        pass

    return retry(
        max_attempts=max_attempts,
        initial_delay=initial_delay,
        exceptions=network_exceptions,
    )


def retry_on_file_errors(
    max_attempts: int = 2,
    initial_delay: float = 0.5,
) -> Callable[[Callable[..., T]], Callable[..., T]]:
    """Convenience decorator for file I/O errors.

    Retries on temporary file system errors.

    Args:
        max_attempts: Maximum number of retry attempts.
        initial_delay: Initial delay in seconds.

    Returns:
        Decorator function.

    Example:
        @retry_on_file_errors()
        def write_config(path: Path, data: str):
            path.write_text(data)
    """
    file_exceptions = (
        OSError,  # Covers most file system errors
        PermissionError,
    )

    return retry(
        max_attempts=max_attempts,
        initial_delay=initial_delay,
        exceptions=file_exceptions,
        max_delay=5.0,  # Don't wait too long for file operations
    )
