"""Retry decorator with exponential backoff + jitter for network-bound fetcher functions.

Usage:
    @with_retry(max_retries=3, base_delay=2)
    def fetch_something():
        ...
"""
import time
import random
import logging
import functools

logger = logging.getLogger(__name__)


def with_retry(max_retries: int = 3, base_delay: float = 2.0, max_delay: float = 30.0):
    """Decorator: retries the wrapped function on any exception with exponential backoff + jitter.

    Args:
        max_retries:  Number of retry attempts after the initial failure.
        base_delay:   Base delay (seconds) for exponential backoff (2 → 4 → 8 …).
        max_delay:    Cap on the delay between retries.
    """
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            last_exc = None
            for attempt in range(1 + max_retries):
                try:
                    return func(*args, **kwargs)
                except Exception as exc:
                    last_exc = exc
                    if attempt < max_retries:
                        delay = min(base_delay * (2 ** attempt), max_delay)
                        jitter = random.uniform(0, delay * 0.25)
                        total = delay + jitter
                        logger.warning(
                            "%s failed (attempt %d/%d): %s — retrying in %.1fs",
                            func.__name__, attempt + 1, max_retries + 1, exc, total,
                        )
                        time.sleep(total)
                    else:
                        logger.error(
                            "%s failed after %d attempts: %s",
                            func.__name__, max_retries + 1, exc,
                        )
            raise last_exc  # type: ignore[misc]
        return wrapper
    return decorator
