from __future__ import annotations

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

T = TypeVar("T")


def next_delay(delay: float, overhead_rate: float, max_delay: float, *, min_delay: float = 0.01) -> float:
    """Backoff helper shared by service clients."""
    return min(max_delay, max(delay * (1.0 + overhead_rate), min_delay))


async def poll_until(
    predicate: Callable[[], Awaitable[bool]],
    *,
    initial_delay: float = 0.01,
    overhead_rate: float = 0.1,
    max_delay: float = 0.5,
    timeout_s: float = 900,
    timeout_error: Callable[[float], Exception] | None = None,
) -> None:
    """Poll `predicate()` with backoff until it returns True (or timeout).

    `timeout_error` allows callers to raise a domain-specific exception type.
    """
    deadline = time.monotonic() + float(timeout_s)
    delay = max(0.0, float(initial_delay))

    while not await predicate():
        if time.monotonic() >= deadline:
            if timeout_error is None:
                raise TimeoutError(f"Timed out after {timeout_s}s waiting for completion")
            raise timeout_error(timeout_s)
        delay = next_delay(delay, overhead_rate, max_delay)
        await asyncio.sleep(delay)


def poll_until_sync(
    predicate: Callable[[], bool],
    *,
    initial_delay: float = 0.01,
    overhead_rate: float = 0.1,
    max_delay: float = 0.5,
    timeout_s: float = 900,
    timeout_error: Callable[[float], Exception] | None = None,
) -> None:
    """Sync polling helper (uses time.sleep for backoff).

    `timeout_error` allows callers to raise a domain-specific exception type.
    """
    deadline = time.monotonic() + float(timeout_s)
    delay = max(0.0, float(initial_delay))

    while not predicate():
        if time.monotonic() >= deadline:
            if timeout_error is None:
                raise TimeoutError(f"Timed out after {timeout_s}s waiting for completion")
            raise timeout_error(timeout_s)
        delay = next_delay(delay, overhead_rate, max_delay)
        time.sleep(delay)

