"""模型单价表与成本换算(M2 设计 §6;参考仓零先例,从头设计)。

**零内置单价**: 实验室走中转网关,计费非官方牌价;库内硬编码单价表
必然过时并掩盖真实成本(P5 严禁默认值掩盖错误)。价格一律由使用方
提供——JSON 文件(`PGW_PRICING_PATH`)或 dict 注入;币种由使用方全表
统一口径,库不设币种字段(20 字段冻结)。查不到的 model → cost=None
且每 model 仅首次 warning(防日志风暴),不阻塞调用。
"""

from __future__ import annotations

import json
from dataclasses import dataclass
from pathlib import Path
from typing import TYPE_CHECKING

from loguru import logger

if TYPE_CHECKING:
    from collections.abc import Mapping


@dataclass(frozen=True)
class ModelPrice:
    """每百万 token 的输入/输出单价(币种由使用方口径统一)。

    `cached_input_per_1m` 是可选的**缓存读取单价**(issue #3): 供应商 prompt
    cache 命中的那部分输入按更低单价计费。不填即不启用——库绝不按经验折扣率
    猜一个数(P5 严禁默认值掩盖),未填时全额按 `input_per_1m` 计。
    """

    input_per_1m: float
    output_per_1m: float
    cached_input_per_1m: float | None = None

    def __post_init__(self) -> None:
        if self.input_per_1m < 0 or self.output_per_1m < 0:
            raise ValueError("单价不能为负")
        if self.cached_input_per_1m is not None and self.cached_input_per_1m < 0:
            raise ValueError("缓存读取单价不能为负")


class PricingTable:
    """model → 单价 的只读表;cost() 有两个调用点: `TelemetryEmitter`(chat 主路径)
    与 `embedding.py` 的批量换算。"""

    def __init__(self, prices: Mapping[str, ModelPrice]) -> None:
        self._prices = dict(prices)
        self._warned: set[str] = set()
        # 独立集合: 与"未知 model"的告警去重键分开,避免 model 名恰好撞上时互相抑制
        self._warned_clamp: set[str] = set()

    @classmethod
    def from_file(cls, path: Path | str) -> PricingTable:
        """装配 fail-loud: 文件缺失/JSON 坏/条目缺字段/负单价一律 ValueError。"""
        p = Path(path)
        try:
            data = json.loads(p.read_text(encoding="utf-8"))
        except (OSError, json.JSONDecodeError) as exc:
            raise ValueError(f"价格表 {p} 读取失败: {exc}") from exc
        if not isinstance(data, dict):
            raise ValueError(f"价格表 {p} 顶层必须是对象: model -> 单价")
        prices: dict[str, ModelPrice] = {}
        for model, entry in data.items():
            if not isinstance(entry, dict) or not {"input_per_1m", "output_per_1m"} <= set(entry):
                raise ValueError(f"价格表 {p} 条目 {model!r} 须含 input_per_1m 与 output_per_1m")
            cached_raw = entry.get("cached_input_per_1m")
            try:
                cached = None if cached_raw is None else float(cached_raw)
            except (TypeError, ValueError) as exc:
                raise ValueError(
                    f"价格表 {p} 条目 {model!r} 的 cached_input_per_1m 必须是数字: {cached_raw!r}"
                ) from exc
            if cached is not None and cached < 0:
                raise ValueError(f"价格表 {p} 条目 {model!r} 的 cached_input_per_1m 不能为负")
            prices[model] = ModelPrice(
                input_per_1m=float(entry["input_per_1m"]),
                output_per_1m=float(entry["output_per_1m"]),
                cached_input_per_1m=cached,
            )
        return cls(prices)

    def cost(
        self,
        model: str,
        prompt_tokens: int,
        completion_tokens: int,
        cached_prompt_tokens: int | None = None,
    ) -> float | None:
        """换算一次调用成本;未知 model 记 None 并仅首次 warning。

        `cached_prompt_tokens` 是供应商 prompt cache 命中的输入 token 数
        (issue #3);仅当该 model 配了 `cached_input_per_1m` 时才分段计价,
        否则全额按输入价——不猜折扣率。参数带默认值: embedding 侧的三参调用
        形态不受影响。
        """
        price = self._prices.get(model)
        if price is None:
            if model not in self._warned:
                self._warned.add(model)
                logger.warning("pricing 表无 model {!r} 的单价,cost 记 None", model)
            return None
        billed_input = prompt_tokens / 1_000_000 * price.input_per_1m
        # 负数按"无命中"处理: cost() 是公共方法,不能假定调用方已过 transport 的校验
        if price.cached_input_per_1m is not None and (cached_prompt_tokens or 0) > 0:
            cached = self._clamp_cached(model, prompt_tokens, cached_prompt_tokens)
            billed_input = (prompt_tokens - cached) / 1_000_000 * price.input_per_1m + (
                cached / 1_000_000 * price.cached_input_per_1m
            )
        return billed_input + completion_tokens / 1_000_000 * price.output_per_1m

    def _clamp_cached(self, model: str, prompt_tokens: int, cached: int) -> int:
        """命中数按输入总数夹取: 网关口径异常不得算出负成本(每 model 只警告一次)。"""
        if cached <= prompt_tokens:
            return cached
        if model not in self._warned_clamp:
            self._warned_clamp.add(model)
            logger.warning(
                "model {!r} 上报的缓存命中 {} 超过输入总数 {},按总数夹取计价",
                model,
                cached,
                prompt_tokens,
            )
        return prompt_tokens
