"""全部端口协议与治理快照类型(M1 设计 §4;最内层,只依赖 types/errors/标准库)。

决策逻辑与状态存储分离(D3): 中间件只面向这里的 Protocol,
后端(memory/redis)各自实现同一契约并共用一套契约测试。
时间量纲一律**秒**(CHS Redis 实现内部的毫秒换算是后端私事,不进契约)。
"""

from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from enum import StrEnum
from typing import Any, Protocol, runtime_checkable

from .types import (
    ChatRequest,
    EmbeddingTransportResult,
    LLMResponse,
    OcrLayoutResult,
    OcrLayoutTransportResult,
    OcrTextResult,
    OcrTextTransportResult,
    SourceConfig,
    SourceStats,
    TelemetryStatus,
    TransportResult,
)

CallNext = Callable[[ChatRequest], Awaitable[LLMResponse]]


@runtime_checkable
class Middleware(Protocol):
    """洋葱层: 包裹 call_next,层与层正交(D1)。"""

    async def __call__(self, request: ChatRequest, call_next: CallNext) -> LLMResponse: ...


@runtime_checkable
class Transport(Protocol):
    """一次原始调用的协议细节(请求组装/流式解析/错误翻译);不含任何治理。"""

    async def complete(
        self,
        *,
        messages: list[dict[str, Any]],
        source: SourceConfig,
        stream: bool,
        overlay: dict[str, Any],
        call_id: str,
    ) -> TransportResult: ...


@runtime_checkable
class EmbeddingTransport(Protocol):
    """一次原始 embedding 调用的协议细节(M2 §7);不含任何治理。"""

    async def embed(
        self, *, texts: list[str], source: SourceConfig, call_id: str
    ) -> EmbeddingTransportResult: ...


@runtime_checkable
class OcrTextPort(Protocol):
    """OCR 文本转录端口(D9;对应 MonkeyOCR POST /ocr/text)。"""

    async def recognize_text(self, image: bytes) -> OcrTextResult: ...


@runtime_checkable
class OcrLayoutPort(Protocol):
    """OCR 版面解析端口(D9;对应 MonkeyOCR POST /parse → GET ZIP)。"""

    async def parse_layout(self, image: bytes) -> OcrLayoutResult: ...


@runtime_checkable
class OcrTransport(Protocol):
    """一次原始 OCR 调用的协议细节(M3 设计 §3.2);不含任何治理。

    check_health 是探测不是调用: 2xx → True,异常 → False,
    CancelledError 穿透;由消费方(启动门)决定成败语义。
    """

    async def recognize_text(
        self, *, image: bytes, source: SourceConfig, call_id: str
    ) -> OcrTextTransportResult: ...

    async def parse_layout(
        self, *, image: bytes, source: SourceConfig, call_id: str
    ) -> OcrLayoutTransportResult: ...

    async def check_health(self, *, source: SourceConfig) -> bool: ...


@runtime_checkable
class Permit(Protocol):
    """限流入场许可;settle/release 均幂等,finally 中必然执行。"""

    async def release(self) -> None: ...

    async def settle(self, actual_tokens: int) -> None: ...


@runtime_checkable
class RateLimiter(Protocol):
    """限流契约(CHS limiter.py 形态): 并发/RPM/TPM × 全局/单源 六道闸。"""

    async def try_acquire(self, source_key: str, est_tokens: int) -> Permit | None: ...

    async def acquire(self, source_key: str, est_tokens: int) -> Permit: ...

    async def source_stats(self, source_key: str) -> SourceStats: ...

    async def mark_progress(self) -> None: ...

    async def progress_age_s(self) -> float: ...


class GateState(StrEnum):
    """熔断状态机三态。"""

    CLOSED = "closed"
    OPEN = "open"
    HALF_OPEN = "half_open"


@dataclass(frozen=True)
class GateDecision:
    """一次源准入决定,同时是结果写回所需的 fencing token(CHS ports.py:405)。"""

    source_name: str
    allowed: bool
    state: GateState
    epoch: int
    is_probe: bool
    probe_owner: str | None
    retry_after_s: float

    def __post_init__(self) -> None:
        # 校验逐条移植 CHS ports.py:405-440,保证快照内部一致
        self._check_bounds()
        self._check_admission()
        self._check_probe()

    def _check_bounds(self) -> None:
        if not self.source_name.strip():
            raise ValueError("source_name 不能为空")
        if self.epoch < 0:
            raise ValueError("epoch 不能为负")
        if self.retry_after_s < 0:
            raise ValueError("retry_after_s 不能为负")

    def _check_admission(self) -> None:
        if self.allowed and self.state is GateState.OPEN:
            raise ValueError("OPEN 状态不得准入")
        if self.allowed and self.state is GateState.HALF_OPEN and not self.is_probe:
            raise ValueError("HALF_OPEN 准入必须是探针")

    def _check_probe(self) -> None:
        if self.is_probe:
            probe_valid = (
                self.allowed
                and self.state is GateState.HALF_OPEN
                and self.probe_owner is not None
                and bool(self.probe_owner.strip())
            )
            if not probe_valid:
                raise ValueError("探针决定必须同时包含准入、HALF_OPEN 与 owner")
        elif self.probe_owner is not None:
            raise ValueError("非探针决定不得携带 probe_owner")


@dataclass(frozen=True)
class GateUpdate:
    """门控写回快照;applied=False 表示 fencing 已拒绝迟到结果(CHS ports.py:442)。"""

    applied: bool
    state: GateState
    epoch: int
    failure_count: int
    retry_after_s: float

    def __post_init__(self) -> None:
        if self.epoch < 0 or self.failure_count < 0 or self.retry_after_s < 0:
            raise ValueError("门控更新快照不能包含负数")


@runtime_checkable
class ProviderGate(Protocol):
    """熔断契约(CHS provider_gate.py 形态): 半开单探针租约 + epoch fencing 在契约内。"""

    async def try_enter(self, source_name: str, owner: str) -> GateDecision: ...

    async def record_success(
        self, entry: GateDecision, *, count_attempt: bool = True
    ) -> GateUpdate: ...

    async def record_failure(
        self, entry: GateDecision, reason: str, force_open: bool
    ) -> GateUpdate: ...

    async def release_probe(self, entry: GateDecision) -> GateUpdate: ...

    async def retry_after_s(self, sources: tuple[str, ...]) -> float: ...


@runtime_checkable
class CacheBackend(Protocol):
    """响应缓存后端: 笨 KV;key 公式归 CacheMW(算法一份)。"""

    async def get(self, key: str) -> str | None: ...

    async def set(self, key: str, value: str, ttl_s: int) -> None: ...


@runtime_checkable
class SourceSelector(Protocol):
    """选源策略(CHS selector.py 形态): 返回本次尝试的候选顺序。"""

    def order(
        self, sources: list[SourceConfig], stats: dict[str, SourceStats]
    ) -> list[SourceConfig]: ...


@runtime_checkable
class OutcomeAwareSelector(Protocol):
    """可选选源器扩展(M2.5 设计 §3.2): 消费尝试结果以维护健康视图。

    RetryMW 构造时 isinstance 判定一次;非本 Protocol 的选源器不受影响。
    喂数口径: 真实成功 ok=True;Transient/SourceDead/429 ok=False;
    ResultInvalid 与"网关健康拒坏请求"不喂(坏结果 ≠ 坏服务)。
    """

    def record_outcome(self, source_name: str, ok: bool) -> None: ...

    def health(self, source_name: str) -> float: ...


@runtime_checkable
class StructuredOutputStrategy(Protocol):
    """结构化输出策略(D7/D14): 请求侧叠加 + 响应侧解析。"""

    def request_overlay(self, schema: dict[str, Any] | None) -> dict[str, Any]: ...

    def parse(self, text: str) -> Any: ...


@runtime_checkable
class TelemetryStatusProvider(Protocol):
    """可自述可写状态的遥测后端;`TelemetryRecorder` 的**可选**伴生端口(issue #15)。

    与 `TelemetryRecorder` 分开而不是给它加成员,是因为后者是 `@runtime_checkable`
    而运行时检查按属性存在性做: 加一个属性会让所有只实现 `record_llm_call` 的
    实现**当场不再是** `TelemetryRecorder`,下游若有同款 isinstance 断言,升级即断
    (设计 §3.3)。消费方一律先 isinstance 再取值,取不到就当没有状态可报。
    """

    @property
    def telemetry_status(self) -> TelemetryStatus: ...


@runtime_checkable
class TelemetryRecorder(Protocol):
    """遥测后端;24 字段冻结(M1 设计 §4.4 + issue #3/#4/#11),唯一调用点是 TelemetryEmitter。

    新增参数不设默认值: 库外无第三方实现者(三项目迁移时删除了各自的同名
    Protocol),完整签名的成本为零,而少写一列会被 emitter 的降级吞成 warning。

    `tenant_id` 与 `meta` 到达 recorder 时**已由 emitter 归一化**——`tenant_id`
    的 `None` 已转空串,`meta` 已序列化为 JSON 字符串(空 dict 为 `'{}'`)。
    recorder 只负责落库,不做任何语义判断,与 `sampling` 列由
    `canonical_sampling_json()` 在 emitter 侧定型是同一先例。
    """

    async def record_llm_call(
        self,
        *,
        call_id: str,
        parent_call_id: str | None,
        session_id: str | None,
        model: str,
        provider: str,
        source_name: str,
        messages: str,
        response: str,
        thinking: str,
        prompt_tokens: int,
        completion_tokens: int,
        usage_source: str,
        latency_ms: int,
        ttft_ms: float | None,
        max_inter_token_ms: float | None,
        cache_hit: bool,
        error: str | None,
        cost: float | None,
        cached_prompt_tokens: int | None,
        model_reported: str | None,
        sampling: str | None,
        reasoning_tokens: int | None,
        tenant_id: str,
        meta: str,
    ) -> None: ...
