"""三层流式活性超时看门狗：给任意异步迭代器施加首产出/产出间/总时长三层超时保护。

领域无关、不打日志、不触网（Humble Object）：仅以 asyncio.timeout 包裹取值，
便于注入假异步迭代器做单测。超时抛 StreamLivenessTimeout（携 kind/elapsed/是否已出首项），
由 adapter 边界翻译为领域错误并埋点。

参考: CHSAnalyzer2 app/providers/streaming.py
"""

from __future__ import annotations

import asyncio
import contextlib
import time
from typing import TYPE_CHECKING, TypeVar

if TYPE_CHECKING:
    from collections.abc import AsyncIterator

_T = TypeVar("_T")


class StreamLivenessTimeout(Exception):  # noqa: N818 — 规格要求名称，非 Error 后缀
    """流活性超时异常。

    属性:
        kind: 超时类型，取值 {"ttft", "inter_token", "total"}。
        elapsed_s: 自流开始到触发超时的秒数。
        first_token_seen: 触发时是否已产出过首项。
    """

    def __init__(self, kind: str, elapsed_s: float, first_token_seen: bool) -> None:
        self.kind = kind
        self.elapsed_s = elapsed_s
        self.first_token_seen = first_token_seen
        super().__init__(f"流活性超时({kind}, elapsed={elapsed_s:.1f}s)")


async def _anext_within(
    it: AsyncIterator[_T],
    timeout_s: float,
    *,
    kind: str,
    start: float,
    first: bool,
) -> _T:
    """限时取下一项；本层 deadline 触发抛 StreamLivenessTimeout(kind)，上游自抛的 TimeoutError 原样上抛。

    参数:
        it: 底层异步迭代器。
        timeout_s: 本次取项的超时预算（秒）。
        kind: 本层超时归类（ttft/inter_token/total），仅在本层 deadline 触发时使用。
        start: 流开始时刻（time.monotonic），用于计算 elapsed_s。
        first: 是否尚未产出过首项（用于 StreamLivenessTimeout.first_token_seen）。

    返回:
        source 的下一项。

    异常:
        StreamLivenessTimeout: 本层 asyncio.timeout deadline 触发（cm.expired()）。
        StopAsyncIteration: 底层迭代器耗尽（由调用方处理收尾）。
    """
    try:
        async with asyncio.timeout(timeout_s) as cm:
            return await it.__anext__()
    except TimeoutError:
        if not cm.expired():
            raise  # 上游自抛的 TimeoutError，非本层 deadline，原样上抛
        raise StreamLivenessTimeout(kind, time.monotonic() - start, not first) from None


async def stream_with_liveness_timeouts(
    source: AsyncIterator[_T],
    *,
    ttft_s: float,
    inter_token_s: float,
    total_s: float,
) -> AsyncIterator[_T]:
    """逐项产出 source，并施加三层活性超时。

    参数:
        source: 待包裹的异步迭代器（如 VLM 流式分片）。
        ttft_s: 首项产出超时（time-to-first-token，秒）。
        inter_token_s: 相邻两项之间的产出超时（秒）。
        total_s: 整条流的总时长超时（秒）。

    产出:
        与 source 逐项一致的元素。

    异常:
        StreamLivenessTimeout: 首项超 ttft_s、后续项间超 inter_token_s 或整体超 total_s。

    关键实现:
        超时**只包裹单次 __anext__**，绝不包裹 yield——否则总时长 deadline 会在生成器挂起
        （消费者处理已产出项）期间继续计时，可能取消消费者的 await 而非由本生成器稳定抛出
        StreamLivenessTimeout（取消泄漏）。总时长改由「每轮取 min(本层预算, 剩余总额) 钳制」
        实现：钳到剩余总额（clamped）时若超时即判 total，否则判 ttft/inter_token。用
        asyncio.timeout(...) 的 cm.expired() 区分本层 deadline 与上游自抛的 TimeoutError。
        finally 中调用底层迭代器的 aclose（若有）以释放上游资源。
    """
    it = source.__aiter__()
    start = time.monotonic()
    deadline = start + total_s
    first = True
    try:
        while True:
            remaining_total = deadline - time.monotonic()
            if remaining_total <= 0:
                raise StreamLivenessTimeout("total", time.monotonic() - start, not first)
            budget = ttft_s if first else inter_token_s
            # 剩余总额比本层预算更紧 → 先到的是 total
            clamped = remaining_total <= budget
            kind = "total" if clamped else ("ttft" if first else "inter_token")
            try:
                item = await _anext_within(
                    it,
                    remaining_total if clamped else budget,
                    kind=kind,
                    start=start,
                    first=first,
                )
            except StopAsyncIteration:
                return
            first = False
            yield item
    finally:
        # 收尾关闭上游迭代器；清理失败不得掩盖主异常，故显式抑制
        aclose = getattr(it, "aclose", None)
        if aclose is not None:
            with contextlib.suppress(Exception):
                await aclose()
