#!/usr/bin/env python3
"""exp14 §3 — Fidelity re-gate пере-прогона (D34.3, ОБЯЗАТЕЛЕН; независим от §1-§2).

Span-цитирующий, РАЗДЕЛЬНО стиль/верность, охота на КЛАСС ИНВЕРСИЙ РЕДАКТУРЫ (final vs draft
vs src: билингв-редактор мог купить гладкость ценой смысла), катастроф-скрин терминов.
author≠reviewer: судьи НЕ glm-5 (редактор) и НЕ deepseek (черновик) — кросс-семейно
gpt-5-mini + kimi (+ gemini точечно на флагах). leave-one-out. $ персист.

Вход: rerun/records.json (source/draft/final). ch5.0/ch20.0 — final пуст (экспорт-баг D35.4a),
скорятся отдельно (не в среднем). Выход: exp14/regate_verdicts.jsonl + свод.

Использование: exp14_regate.py [--dry] [--limit N]
"""
from __future__ import annotations
import argparse, json, re, sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
import exp14_common as C

RERUN = Path("/home/ubuntu/books/gu-zhenren/rerun")
RECS = json.load(open(RERUN / "records.json"))
JUDGES = ["gpt-5-mini", "kimi-k2.6"]  # кросс-семейно к glm-5/deepseek; gemini — точечно вне этого прохода
CAPS = {"gpt-5-mini": 0.06, "kimi-k2.6": 0.10, "gemini-3.1-pro-preview": 0.30}

SYS = ("Ты — билингвальный редактор-эксперт zh→ru, СУДЬЯ ВЕРНОСТИ. Тебе даны: ИСХОДНИК (zh), "
       "ЧЕРНОВИК (ru) и ФИНАЛ после редактуры (ru). Оцени ВЕРНОСТЬ ФИНАЛА исходнику РАЗДЕЛЬНО от стиля. "
       "Особо ищи КЛАСС ИНВЕРСИЙ РЕДАКТУРЫ: правки, где ФИНАЛ отошёл от смысла исходника ДАЛЬШЕ черновика "
       "(редактор внёс смысл, которого в исходнике нет; перевернул полярность/участника/число; выбросил атом). "
       "Ответь СТРОГО JSON без markdown: "
       '{"fidelity":0-100,"style":0-100,"n_mistranslation":int,"n_omission":int,'
       '"n_edit_inversion":int,"catastrophe":true|false,'
       '"flags":[{"type":"inversion|omission|term|addition","span_src":"≤12 слов zh",'
       '"quote_final":"≤15 слов ru","why":"кратко"}]}. '
       "catastrophe=true при инверсии полярности/участника/действия ИЛИ потере ключевого атома (заголовок/событие/термин).")


def run():
    ap = argparse.ArgumentParser(); ap.add_argument("--dry", action="store_true")
    ap.add_argument("--limit", type=int, default=0); a = ap.parse_args()
    sp = C.Spender(C.DIAG / "regate_costs.jsonl", CAPS)
    vpath = C.DIAG / "regate_verdicts.jsonl"
    done = set(); out = []
    if vpath.exists():
        for l in vpath.read_text(encoding="utf-8").splitlines():
            if l.strip():
                v = json.loads(l); out.append(v); done.add((v["chunk"], v["judge"]))
    recs = [r for r in RECS if (r["chapter"], r["chunk_idx"]) not in {(5, 0), (20, 0)}
            and (r.get("final") or "").strip()]
    empties = [(r["chapter"], r["chunk_idx"]) for r in RECS if not (r.get("final") or "").strip()]
    if a.limit:
        recs = recs[:a.limit]
    tasks = [(r, jm) for r in recs for jm in JUDGES
             if (f"{r['chapter']}.{r['chunk_idx']}", jm) not in done]
    print(f"re-gate: {len(recs)} chunks × {len(JUDGES)} judges = {len(tasks)} new tasks "
          f"({len(done)} done); empty-final (экспорт-баг, вне скора): {empties}")
    if a.dry:
        est = sum(C.predict_cost(jm, 4000, 4000) for _, jm in tasks)
        print(f"predicted ≤ ${est:.2f}"); return
    for r, jm in tasks:
        cid = f"{r['chapter']}.{r['chunk_idx']}"
        user = (f"ИСХОДНИК (zh):\n{r['source']}\n\nЧЕРНОВИК (ru):\n{r['draft']}\n\n"
                f"ФИНАЛ после редактуры (ru):\n{r['final']}\n\nОцени верность ФИНАЛА. Ответь JSON.")
        # gpt-5-mini: reasoning ⊆ completion → нужен запас (medium@4000 съедал бюджет → empty); low@10000
        s = C.spec(jm, max_tokens=(16000 if jm == "kimi-k2.6" else 10000),
                   reasoning_effort=("low" if jm == "gpt-5-mini" else None))
        in_est = C.count_tokens(SYS + user, "deepseek")
        ok, pred = sp.guard(jm, in_est, s["max_tokens"])
        if not ok:
            print(f"  OVER-CAP {cid} {jm} pred=${pred:.3f}"); continue
        text, err, usage = C.call(s, [{"role": "system", "content": SYS},
                                      {"role": "user", "content": user}], timeout=300)
        cst = sp.record({"model": jm, "judge": jm, "keyenv": s["keyenv"], "chunk": cid,
                         "err": err, "usage": usage})
        v = parse(text) if not err else {"fidelity": None, "err": err[:60]}
        v.update(chunk=cid, judge=jm)
        out.append(v)
        with open(vpath, "a", encoding="utf-8") as f:
            f.write(json.dumps(v, ensure_ascii=False) + "\n")
        fl = len(v.get("flags", []) or [])
        print(f"  {cid:<7} {jm:<12} fid={v.get('fidelity')} style={v.get('style')} "
              f"cat={v.get('catastrophe')} flags={fl} ${cst:.4f}")
    summarize(out, empties)


def parse(text):
    m = re.search(r"\{.*\}", text, re.S)
    if not m:
        return {"fidelity": None, "parse": text[:80]}
    try:
        d = json.loads(m.group(0))
        return {k: d.get(k) for k in ("fidelity", "style", "n_mistranslation", "n_omission",
                                      "n_edit_inversion", "catastrophe", "flags")}
    except Exception:
        f = re.search(r'"fidelity"\s*:\s*(\d+)', m.group(0))
        cat = '"catastrophe": true' in m.group(0).lower() or '"catastrophe":true' in m.group(0).lower()
        return {"fidelity": int(f.group(1)) if f else None, "catastrophe": cat, "parse_fallback": True}


def summarize(out, empties):
    from collections import defaultdict
    import statistics
    byj = defaultdict(list); cats = []; invs = []
    for v in out:
        if v.get("fidelity") is not None:
            byj[v["judge"]].append(v["fidelity"])
        if v.get("catastrophe"):
            cats.append((v["chunk"], v["judge"]))
        for fl in (v.get("flags") or []):
            if fl.get("type") == "inversion":
                invs.append((v["chunk"], v["judge"], fl.get("quote_final", "")[:60]))
    print("\n=== FIDELITY RE-GATE свод ===")
    for j, xs in byj.items():
        print(f"  {j}: mean fidelity {statistics.mean(xs):.1f} (n={len(xs)}, min={min(xs)})")
    if len(byj) == 2:  # leave-one-out
        js = list(byj)
        print(f"  leave-one-out: без {js[0]} → {statistics.mean(byj[js[1]]):.1f}; без {js[1]} → {statistics.mean(byj[js[0]]):.1f}")
    print(f"  КАТАСТРОФ-флаги: {len(cats)} → {cats[:20]}")
    print(f"  ИНВЕРСИИ РЕДАКТУРЫ: {len(invs)}")
    for c in invs[:15]:
        print(f"     {c[0]} [{c[1]}]: {c[2]}")
    print(f"  экспорт-баг пустые финалы (вне скора): {empties}")


if __name__ == "__main__":
    run()
