#!/usr/bin/env python3
"""exp14 — общий харнес: провайдер-вызов, usage→$ (3 reasoning-режима), per-call
predicted-cost кап (НЕ group-level — урок exp13 +10%), append-only персист usage/cost,
токенайзер-счёт выходных токенов (кириллица-фертильность), spec-фабрика.

Зона eval/. Цены — ЗАМОРОЖЕННАЯ зеркальная копия backend/configs/models.yaml
(prices_checked 2026-07-10) + фронтир gpt-5.4 live-фактчек 2026-07-11
(developers.openai.com/api/docs/pricing). Единственный источник истины — models.yaml;
при расхождении верить Go/models.yaml. Ключи из eval/.env через load_env_file (НЕ читаю .env).
"""
from __future__ import annotations
import json, os, sys, time, urllib.request, urllib.error
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent))
from refusal_bench import load_env_file  # noqa: E402

load_env_file()

DIAG = Path("/home/ubuntu/books/gu-zhenren/exp14")
DIAG.mkdir(parents=True, exist_ok=True)
RERUN = Path("/home/ubuntu/books/gu-zhenren/rerun")
TOKDIR = Path(__file__).resolve().parent / "tokenizers"

# ── Цены $/1M (in, out, cached_in) + reasoning-режим. Зеркало models.yaml. ──────────
PRICES = {
    "deepseek-v4-flash": (0.14, 0.28, 0.0028, "subset"),
    "deepseek-v4-pro": (0.435, 0.87, 0.003625, "subset"),
    "glm-5": (1.0, 3.2, 0.2, "subset"),
    "glm-5.1": (1.4, 4.4, 0.26, "subset"),
    "kimi-k2.6": (0.95, 4.0, 0.16, "subset"),
    "grok-4.3": (1.25, 2.50, 1.25, "additive"),
    "gemini-3.1-pro-preview": (2.0, 12.0, 0.20, "additive_total"),
    "gpt-5-mini": (0.25, 2.0, 0.025, "subset"),
    "gpt-5.4": (2.50, 15.0, 0.25, "subset"),  # live 2026-07-11
    "mistral-large-2512": (0.5, 1.5, 0.5, "subset"),
}
FALLBACK = "deepseek-v4-flash"  # никогда $0

# ── Спеки провайдеров (endpoint/keyenv/wire-квирки из 00-provider-quirks + models.yaml) ─
def spec(model: str, *, temp: float | None = None, max_tokens: int | None = None,
         reasoning_effort: str | None = None) -> dict:
    """Собирает per-call spec. temp/max_tokens/reasoning_effort перекрывают дефолты."""
    base_by = {
        "deepseek-v4-flash": ("https://api.deepseek.com/v1", "DEEPSEEK_API_KEY", {}),
        "deepseek-v4-pro": ("https://api.deepseek.com/v1", "DEEPSEEK_API_KEY", {}),
        "glm-5": ("https://api.z.ai/api/paas/v4", "ZAI_API_KEY", {"thinking": {"type": "disabled"}}),
        "glm-5.1": ("https://api.z.ai/api/paas/v4", "ZAI_API_KEY", {"thinking": {"type": "disabled"}}),
        "kimi-k2.6": ("https://api.moonshot.ai/v1", "KIMI_API_KEY", {}),
        "grok-4.3": ("https://api.x.ai/v1", "XAI_API_KEY", {}),
        "gemini-3.1-pro-preview": ("https://generativelanguage.googleapis.com/v1beta/openai", "GEMINI_API_KEY", {}),
        "gpt-5-mini": ("https://api.openai.com/v1", "OPENAI_API_KEY", {}),
        "gpt-5.4": ("https://api.openai.com/v1", "OPENAI_API_KEY", {}),
        "mistral-large-2512": ("https://api.mistral.ai/v1", "MISTRAL_API_KEY", {"safe_prompt": False}),
    }
    base, keyenv, extra = base_by[model]
    extra = dict(extra)
    s = {"model": model, "base": base, "keyenv": keyenv, "extra": extra}
    # reasoning wire per family
    if model in ("gpt-5-mini", "gpt-5.4"):
        s["openai_reasoner"] = True  # max_completion_tokens, no temperature
        extra["reasoning_effort"] = reasoning_effort or "medium"
    elif model == "kimi-k2.6":
        s["force_temp"] = 1.0
    elif model == "grok-4.3":
        if reasoning_effort:  # xAI plain reasoning_effort in body (none/low/medium/high)
            extra["reasoning_effort"] = reasoning_effort
    # temp
    if model in ("gpt-5-mini", "gpt-5.4"):
        s["temp"] = None
    elif model == "kimi-k2.6":
        s["temp"] = 1.0
    else:
        s["temp"] = 0.4 if temp is None else temp
    # max_tokens floors (D24.3)
    floor = 16000 if model == "kimi-k2.6" else (12000 if model == "gpt-5.4" else 8000)
    s["max_tokens"] = max_tokens or floor
    return s


def call(s: dict, messages: list[dict], timeout: int = 300) -> tuple[str, str | None, dict]:
    """Один chat/completions. Возврат (text, err|None, usage со стампом finish_reason).
    err — структурная строка: http|CODE|.. / transport|.. / content_filter|.. / empty|.."""
    key = os.environ.get(s["keyenv"], "")
    body = {"model": s["model"], "messages": messages, **s.get("extra", {})}
    if s.get("openai_reasoner"):
        body["max_completion_tokens"] = s["max_tokens"]  # gpt-5: max_tokens → 400
    else:
        body["max_tokens"] = s["max_tokens"]
    if s.get("temp") is not None:
        body["temperature"] = s["temp"]
    data = json.dumps(body).encode()
    req = urllib.request.Request(s["base"].rstrip("/") + "/chat/completions", data=data,
        headers={"Content-Type": "application/json", "Authorization": f"Bearer {key}"})
    try:
        with urllib.request.urlopen(req, timeout=timeout) as r:
            d = json.loads(r.read())
    except urllib.error.HTTPError as e:
        return "", f"http|{e.code}|{e.read()[:300].decode('utf-8','replace')}", {}
    except Exception as e:
        return "", f"transport|{type(e).__name__}|{str(e)[:200]}", {}
    ch = (d.get("choices") or [{}])[0]
    finish = ch.get("finish_reason", "")
    msg = ch.get("message", {}) or {}
    text = (msg.get("content") or "").strip()
    usage = d.get("usage", {}) or {}
    usage["finish_reason"] = finish
    if str(finish).lower() in ("content_filter", "prohibited_content", "safety") or "prohibited" in str(finish).lower():
        return "", f"content_filter|finish={finish}", usage
    if not text:
        has_reason = bool((msg.get("reasoning_content") or "").strip())
        return "", f"empty|finish={finish}|reasoning={has_reason}", usage
    return text, None, usage


def cost_of(model: str, usage: dict) -> float:
    """usage→$ с 3 reasoning-режимами (subset/additive/additive_total)."""
    inp = usage.get("prompt_tokens", 0) or 0
    comp = usage.get("completion_tokens", 0) or 0
    total = usage.get("total_tokens", 0) or 0
    pin, pout, _c, regime = PRICES.get(model, (*PRICES[FALLBACK][:3], "subset"))
    if regime == "additive":  # grok: reasoning_tokens отдельным полем, поверх completion
        reason = usage.get("reasoning_tokens", 0) or 0
        out = comp + reason
    elif regime == "additive_total":  # gemini: thinking только в total
        out = max(comp, total - inp) if total else comp
    else:  # subset: reasoning ⊆ completion
        out = comp
    return (inp * pin + out * pout) / 1_000_000


def predict_cost(model: str, in_tokens_est: int, max_out: int) -> float:
    """Верхняя оценка ДО вызова: in_est·pin + max_out·pout (потолок max_tokens как output)."""
    pin, pout, _c, _r = PRICES.get(model, (*PRICES[FALLBACK][:3], "subset"))
    return (in_tokens_est * pin + max_out * pout) / 1_000_000


# ── Токенайзеры (кириллица-фертильность; deepseek-v4/kimi отсутствуют → v3.2 прокси) ──
_TOK = {}
def _load_tok(name: str):
    from tokenizers import Tokenizer
    p = TOKDIR / name / "tokenizer.json"
    if name not in _TOK:
        _TOK[name] = Tokenizer.from_file(str(p))
    return _TOK[name]

def count_tokens(text: str, family: str = "glm") -> int:
    name = {"glm": "glm-4.6", "deepseek": "deepseek-v3.2"}.get(family, "glm-4.6")
    return len(_load_tok(name).encode(text, add_special_tokens=False).ids)


# ── Per-call кап + персист ────────────────────────────────────────────────────────
class Spender:
    """Per-key running spend + per-call predicted-cost кап (обязателен ДО вызова)."""
    def __init__(self, costs_path: Path, caps: dict[str, float]):
        self.path = costs_path
        self.caps = caps  # model → per-call cap $
        self.by_key: dict[str, float] = {}

    def guard(self, model: str, in_est: int, max_out: int) -> tuple[bool, float]:
        pc = predict_cost(model, in_est, max_out)
        cap = self.caps.get(model, 0.50)
        return (pc <= cap, pc)

    def record(self, rec: dict) -> float:
        c = cost_of(rec["model"], rec.get("usage", {}))
        rec["cost"] = round(c, 6)
        keyenv = rec.get("keyenv", rec["model"])
        self.by_key[keyenv] = self.by_key.get(keyenv, 0.0) + c
        with open(self.path, "a", encoding="utf-8") as f:
            f.write(json.dumps(rec, ensure_ascii=False) + "\n")
        return c


def messages_with_injection(system_prefix: str, injection: str, user: str) -> list[dict]:
    """Layout render.go MessagesWithInjection: system(prefix,cache) → system(inj) → user."""
    m = [{"role": "system", "content": system_prefix}]
    if injection.strip():
        m.append({"role": "system", "content": injection})
    m.append({"role": "user", "content": user})
    return m


if __name__ == "__main__":
    # смоук: цены/токенайзер/предикт
    ru = "Он шёл по тёмной улице, и снег ложился ему на плечи, укрывая следы."
    print("glm-4.6 tokens:", count_tokens(ru, "glm"))
    print("deepseek(v3.2) tokens:", count_tokens(ru, "deepseek"))
    u = {"prompt_tokens": 5000, "completion_tokens": 3000, "total_tokens": 8200}
    for m in ("glm-5", "gpt-5.4", "gemini-3.1-pro-preview", "grok-4.3"):
        print(f"cost_of {m}: ${cost_of(m, u):.5f}  predict(5k in, 8k out): ${predict_cost(m,5000,8000):.4f}")
