from __future__ import annotations

import asyncio
import logging
import time
from typing import Awaitable, Callable, TypeVar

T = TypeVar("T")
logger = logging.getLogger(__name__)


def clamp_retries(max_retries: int | None) -> int:
    if max_retries is None:
        return 0
    if max_retries < 0:
        return 0
    return max_retries


def compute_delay(attempt: int, retry_backoff_s: float) -> float:
    return retry_backoff_s * (2**attempt)


def run_with_retries(
    *,
    func: Callable[[], T],
    max_retries: int,
    retry_backoff_s: float,
    request_id: str | None = None,
) -> T:
    attempt = 0
    while True:
        try:
            return func()
        except Exception as exc:
            if attempt >= max_retries:
                raise
            delay = compute_delay(attempt, retry_backoff_s)
            logger.warning(
                "Retrying LLM request: attempt=%d delay=%.2fs request_id=%s error=%s",
                attempt + 1,
                delay,
                request_id,
                exc,
            )
            time.sleep(delay)
            attempt += 1


async def run_with_retries_async(
    *,
    func: Callable[[], Awaitable[T]],
    max_retries: int,
    retry_backoff_s: float,
    request_id: str | None = None,
) -> T:
    attempt = 0
    while True:
        try:
            return await func()
        except Exception as exc:
            if attempt >= max_retries:
                raise
            delay = compute_delay(attempt, retry_backoff_s)
            logger.warning(
                "Retrying LLM request (async): attempt=%d delay=%.2fs request_id=%s error=%s",
                attempt + 1,
                delay,
                request_id,
                exc,
            )
            await asyncio.sleep(delay)
            attempt += 1


async def run_sync_in_thread(func: Callable[[], T]) -> T:
    loop = asyncio.get_running_loop()
    return await loop.run_in_executor(None, func)
