"""Hard wall-clock deadline wrapper for blocking LLM/VLM calls.

Why this exists
---------------
The pipeline's heavy ``gpt-5.5`` reasoning + vision providers (projection
card @16000 tok, shot-aware plan, geometry VLM readback) issue a single
``litellm.completion`` call. ``litellm``'s ``timeout`` maps to httpx's
*per-read* timeout: it resets on every byte received, so a server that
slow-streams a large response (dribbling bytes) never trips it. Observed in
production: a ``timeout=180`` call that hung on an SSL socket read for 21
minutes, blocking the whole background-render chain (and leaving a stale
``running`` step-lock when force-killed).

``call_with_deadline`` enforces a TOTAL wall-clock deadline that ``litellm``
cannot. The work runs in a **daemon** thread; if it does not finish within
``deadline_seconds`` the wrapper raises :class:`HardDeadlineExceeded` and
returns immediately. The underlying thread is abandoned (CPython cannot
interrupt a thread blocked in a C-level socket read) — being a daemon it
never blocks interpreter exit and is reclaimed when its socket eventually
errors or the process ends. The caller treats the raise as a per-unit
failure (that fp/bg goes ``partial``), so one hung call no longer stalls the
entire run.

Scope note: wrap only callees that do NOT depend on thread-local context
(e.g. the ``ImageCallBudget`` is consumed by image-generation steps, which
are intentionally NOT wrapped here — their budget thread-local would not
propagate into the worker thread). The gpt-5.5 *text/vision* providers carry
no such thread-local, so wrapping them is safe.
"""
from __future__ import annotations

import threading
from typing import Any, Callable, TypeVar

T = TypeVar("T")


class HardDeadlineExceeded(Exception):
    """Raised when a wrapped call exceeds its total wall-clock deadline."""


def call_with_deadline(
    fn: Callable[..., T],
    *args: Any,
    deadline_seconds: float,
    **kwargs: Any,
) -> T:
    """Run ``fn(*args, **kwargs)`` with a hard total wall-clock deadline.

    Returns ``fn``'s value if it completes within ``deadline_seconds``.
    Re-raises any exception ``fn`` raises (unchanged — a callee error is not
    masked as a deadline error). Raises :class:`HardDeadlineExceeded` if the
    call is still running when the deadline elapses.
    """
    if deadline_seconds <= 0:
        raise ValueError(f"deadline_seconds must be positive (got {deadline_seconds})")

    box: dict[str, Any] = {}

    def _run() -> None:
        try:
            box["value"] = fn(*args, **kwargs)
        except BaseException as exc:  # noqa: BLE001 — propagate to caller thread
            box["error"] = exc

    worker = threading.Thread(
        target=_run, name="llm-deadline", daemon=True
    )
    worker.start()
    worker.join(timeout=deadline_seconds)
    if worker.is_alive():
        raise HardDeadlineExceeded(
            f"call exceeded hard deadline {deadline_seconds}s "
            f"(litellm per-read timeout did not bound it; abandoning thread)"
        )
    if "error" in box:
        raise box["error"]
    return box.get("value")  # type: ignore[return-value]
