"""Retry decorators with exponential backoff for sync and async functions."""

import asyncio
import functools
import random
import time
from collections.abc import Callable
from typing import Any


def retry_with_backoff(
    max_retries: int = 3,
    initial_delay: float = 1.0,
    max_delay: float = 30.0,
    backoff_factor: float = 2.0,
    jitter: bool = True,
    exceptions: tuple[type[Exception], ...] = (Exception,),
    on_retry: Callable[[Exception, int], None] | None = None,
) -> Callable:
    """Decorator that retries a synchronous function with exponential backoff.

    Args:
        max_retries: Maximum number of retry attempts after the first failure.
        initial_delay: Initial delay in seconds before the first retry.
        max_delay: Maximum delay cap in seconds.
        backoff_factor: Multiplier applied to delay after each retry.
        jitter: If True, multiplies delay by a random factor in [0.5, 1.0].
        exceptions: Tuple of exception types that trigger a retry.
        on_retry: Optional callback invoked with (exception, attempt) before sleeping.

    Returns:
        Decorator that wraps the target function with retry logic.

    Raises:
        Exception: The last exception raised after all retries are exhausted.
    """
    def decorator(func: Callable) -> Callable:
        @functools.wraps(func)
        def wrapper(*args: Any, **kwargs: Any) -> Any:
            delay = initial_delay
            for attempt in range(max_retries + 1):
                try:
                    return func(*args, **kwargs)
                except exceptions as exc:
                    if attempt >= max_retries:
                        raise
                    capped = min(delay, max_delay)
                    actual_delay = capped * (0.5 + random.random()) if jitter else capped
                    if on_retry is not None:
                        on_retry(exc, attempt)
                    time.sleep(actual_delay)
                    delay *= backoff_factor
        return wrapper
    return decorator


def retry_with_backoff_async(
    max_retries: int = 3,
    initial_delay: float = 1.0,
    max_delay: float = 30.0,
    backoff_factor: float = 2.0,
    jitter: bool = True,
    exceptions: tuple[type[Exception], ...] = (Exception,),
    on_retry: Callable[[Exception, int], None] | None = None,
) -> Callable:
    """Decorator that retries an async function with exponential backoff.

    Args:
        max_retries: Maximum number of retry attempts after the first failure.
        initial_delay: Initial delay in seconds before the first retry.
        max_delay: Maximum delay cap in seconds.
        backoff_factor: Multiplier applied to delay after each retry.
        jitter: If True, multiplies delay by a random factor in [0.5, 1.0].
        exceptions: Tuple of exception types that trigger a retry.
        on_retry: Optional callback invoked with (exception, attempt) before sleeping.

    Returns:
        Decorator that wraps the target async function with retry logic.

    Raises:
        Exception: The last exception raised after all retries are exhausted.
    """
    def decorator(func: Callable) -> Callable:
        @functools.wraps(func)
        async def wrapper(*args: Any, **kwargs: Any) -> Any:
            delay = initial_delay
            for attempt in range(max_retries + 1):
                try:
                    return await func(*args, **kwargs)
                except exceptions as exc:
                    if attempt >= max_retries:
                        raise
                    capped = min(delay, max_delay)
                    actual_delay = capped * (0.5 + random.random()) if jitter else capped
                    if on_retry is not None:
                        on_retry(exc, attempt)
                    await asyncio.sleep(actual_delay)
                    delay *= backoff_factor
        return wrapper
    return decorator
