"""Per-model stale-timeout FLOOR for known reasoning models.

Reasoning models routinely exceed the default chat-model stale detectors (stream 180s,
non-stream 90s): upstream proxies idle-kill the stream mid-think, surfacing as
``BrokenPipeError``/``RemoteProtocolError``. The stale-detector scaling applies
``max(default, floor)`` from :func:`get_reasoning_stale_timeout_floor`, so this never
overrides explicit per-model ``stale_timeout_seconds``/``request_timeout_seconds`` (that
branch never calls it), never lowers a threshold, and is ``None`` for non-allowlisted models.
"""

from __future__ import annotations

import re
from typing import Optional


# floor_seconds -> slugs. Order irrelevant — longest slug wins at match time.
_REASONING_STALE_TIMEOUT_FLOORS: dict[int, tuple[str, ...]] = {
    600: (
        # NVIDIA Nemotron behind hosted NIM: documented 60-180s upstream idle kill.
        "nemotron-3-ultra", "nemotron-3-super",
        # DeepSeek R1 / V4 (reasoning_content streamed before final content).
        # ``deepseek-flash`` is the version-less canonical Flash id (2026-09 Flash refresh);
        # ``deepseek-v4-flash`` still aliases onto it server-side.
        "deepseek-r1", "deepseek-reasoner", "deepseek-flash", "deepseek-v4-flash", "deepseek-v4.1-flash", "deepseek-v4-pro",
        # OpenAI o-series: each variant enumerated so bare ``o1`` cannot over-match ``olmo-1``.
        "o1", "o1-mini", "o1-pro", "o1-preview", "o3", "o3-pro",
        # OpenAI named reasoning lines (gpt-5.6-sol/-terra/-luna, gpt-6-astra, their -pro/-900k
        # variants): minutes-long thinking at xhigh/max/ultra; sub-10k-token requests sit below the
        # Codex context-size floor, so this is their only protection. Anchored so gpt-5.5 and the
        # gpt-4.x / gpt-5.1-chat lines keep the effort-tier defaults (#112909).
        "gpt-5.6", "gpt-6",
        # Mythos-class named models (claude-fable-5): 1M ctx + 128K output, a heavier thinking
        # phase than the numbered line — otherwise the stale detector trips the circuit breaker.
        "claude-fable",
    ),
    300: (
        "nemotron-3-nano", "nemotron-3.5-lightning", "qwq-32b", "o3-mini", "o4-mini",
        # xAI Grok: explicit reasoning pairs only, so bare ``grok-3``/``grok-4`` fast variants
        # don't inherit the floor.
        "grok-4-fast-reasoning", "grok-4.20-reasoning", "grok-4.5", "grok-4.6",
        # "Ox Alpha" stealth reasoning model (OpenRouter / OpenCode Zen slugs); Thinking
        # Machines Inkling (covers inkling-small and :free SKUs).
        "ox-alpha", "x-preview-f-free", "inkling",
        # MiniMax M2.x (m2.5/m2.7): reasoning_content before first content token; 240s
        # mid-think stalls observed (#62353).
        "minimax-m2",
    ),
    # Anthropic Claude 4.x+ thinking variants (anchored so 3.x never matches).
    240: ("claude-opus-4", "claude-opus-5"),
    # qwen3 family: instruct variants also match — a slightly longer wait on a hung provider
    # beats a pattern (``qwen3-.*-thinking``) that breaks on the next naming shape.
    180: ("qwen3", "claude-sonnet-5", "claude-sonnet-4.5", "claude-sonnet-4.6", "grok-4-fast-non-reasoning"),
}


# Pre-compiled once at import (immutable afterwards — safe under free-threaded Python).
# Right anchor: end-of-string or a slug separator; ``:`` because OpenRouter routing suffixes
# (``:free``, ``:nitro``) attach directly to the slug. Longest-first so ``o3-mini`` beats ``o3``.
_SORTED_REASONING_FLOORS: list[tuple[str, float, re.Pattern[str]]] = [
    (slug, floor, re.compile(r"^" + re.escape(slug) + r"(?:$|[\-._:])"))
    for slug, floor in sorted(
        ((slug, floor) for floor, slugs in _REASONING_STALE_TIMEOUT_FLOORS.items() for slug in slugs),
        key=lambda kv: -len(kv[0]),
    )
]


def get_reasoning_stale_timeout_floor(model: object) -> Optional[float]:
    """Stale-timeout floor (seconds) for a known reasoning model, else ``None``.

    The aggregator prefix (up to the last ``/``) is stripped and the slug matched
    start-anchored with an end-or-separator right anchor, so ``qwen3-235b`` matches ``qwen3``
    but ``some-other-qwen3`` and ``llama-4-70b-o1-preview`` do not.
    """
    if not model or not isinstance(model, str):
        return None
    name = model.strip().lower().rsplit("/", 1)[-1]
    for _slug, floor, pattern in _SORTED_REASONING_FLOORS:
        if pattern.search(name):
            return float(floor)
    return None
