from __future__ import annotations

import time
from collections.abc import Callable
from typing import TypeVar

T = TypeVar("T")


class RetryError(RuntimeError):
    def __init__(self, message: str, attempts: int, last_error: BaseException | None = None) -> None:
        super().__init__(message)
        self.attempts = attempts
        self.last_error = last_error


def retry_call(
    fn: Callable[[], T],
    attempts: int = 5,
    base_delay: float = 0.2,
    max_delay: float = 8.0,
    sleeper: Callable[[float], None] = time.sleep,
    retry_on: tuple[type[BaseException], ...] = (Exception,),
) -> T:
    last: BaseException | None = None
    for attempt in range(1, max(1, attempts) + 1):
        try:
            return fn()
        except retry_on as exc:
            last = exc
            if attempt >= attempts:
                break
            delay = min(max_delay, base_delay * (2 ** (attempt - 1)))
            sleeper(delay)
    raise RetryError(f"retry exhausted after {attempts} attempts: {last}", attempts, last)
