"""Operator-facing wait notice for long provider silences (#92550).

The 30s heartbeat is gateway liveness, not a warning. Once a request has been
silent past the notice threshold this module decides WHAT the status line says
(neutral: which phase of the wait we are in, which watchdog would reconnect and
when) and WHETHER to rewrite it: once when the silence starts, again only when
the wait phase changes or a watchdog deadline is near. Watchdog thresholds and
retry policy live with the watchdogs; this is presentation only.
"""

import math
from typing import Optional

NEAR_DEADLINE_SECS = 15.0


def _near_deadline(watchdog: Optional[tuple[str, float]]) -> bool:
    return watchdog is not None and watchdog[1] <= NEAR_DEADLINE_SECS


_PHASE_TEXT = {
    # Codex Responses (non-stream request path)
    "first_event": "{n}s waiting for the first provider event",
    "reconnect": "{n}s waiting for the first provider event after reconnect",
    "pre_progress": "provider stream open; {n}s without substantive model progress",
    "post_event": "provider stream active; {n}s without stream events",
    # Chat-completions streaming path
    "first_chunk": "{n}s waiting for the first stream chunk",
    "post_chunk": "stream open; {n}s without stream output",
}


def wait_notice_text(model: str, silence_secs: float, phase: str,
                     watchdog: Optional[tuple[str, float]] = None) -> str:
    """One neutral status line. ``watchdog`` is ``(label, seconds_until_it_fires)``."""
    lead = "still waiting on" if _near_deadline(watchdog) else "waiting on"
    text = f"⏳ {lead} {model} — " + _PHASE_TEXT[phase].format(n=int(silence_secs))
    if watchdog is not None:
        label, remaining = watchdog
        text += f" (auto-reconnect: {label} watchdog in {max(0, int(remaining))}s)"
    return text


def codex_watchdog_deadline(*, stale_timeout: float, ttfb_enabled: bool, ttfb_timeout: float,
    last_event_ts: Optional[float], last_progress_ts: Optional[float],
    retry_started_ts: Optional[float], call_start: float, idle_enabled: bool,
    idle_timeout: float, idle_requires_progress: bool, elapsed: float,
    progress_timeout: float = 0.0) -> Optional[tuple[str, float]]:
    """Earliest enabled Codex watchdog as ``(label, seconds_until_it_fires)``; None when
    none applies (disabled/infinite, or its deadline already passed)."""
    deadlines: list[tuple[str, float]] = []
    if math.isfinite(stale_timeout):
        deadlines.append(("wall-clock stale", stale_timeout))
    attempt_offset = max(0.0, retry_started_ts - call_start) if retry_started_ts is not None else 0.0
    if last_event_ts is None:
        if ttfb_enabled and math.isfinite(ttfb_timeout):
            deadlines.append(("TTFB", attempt_offset + ttfb_timeout))
    elif progress_timeout > 0 and last_progress_ts is None:
        deadlines.append(("first progress", attempt_offset + progress_timeout))
    elif (not idle_requires_progress or last_progress_ts is not None) and idle_enabled and math.isfinite(idle_timeout):
        deadlines.append(("stream idle", max(0.0, last_event_ts - call_start) + idle_timeout))
    if not deadlines:
        return None
    label, deadline = min(deadlines, key=lambda d: d[1])
    if deadline <= elapsed:
        return None
    return label, deadline - elapsed


class WaitNoticeState:
    """Per-request memory of what the status line currently shows.

    ``should_emit`` is True for the first notice of a silence, for a phase or
    watchdog change, and once when the applicable deadline comes within
    ``NEAR_DEADLINE_SECS``; every other heartbeat only touches liveness.
    ``reset`` when activity resumes so the next silence gets a fresh notice.
    """

    def __init__(self) -> None:
        self.reset()

    def reset(self) -> None:
        self.phase: Optional[str] = None
        self.watchdog_label: Optional[str] = None
        self.near_shown = False

    def should_emit(self, phase: str, watchdog: Optional[tuple[str, float]]) -> bool:
        label = watchdog[0] if watchdog is not None else None
        near = _near_deadline(watchdog)
        emit = self.phase != phase or self.watchdog_label != label or (near and not self.near_shown)
        self.phase, self.watchdog_label = phase, label
        if near:
            self.near_shown = True
        return emit
