#!/usr/bin/env python3
"""exp14b — судейский проход по SOFT-классам (e чэнъюй, f кореференция) × армы C/F/X.
≥2 КРОСС-семейных span-цитирующих судьи, self-family exclusion (урок D37: exp14 фронтир
судил ОДИН судья). leave-one-out. Катастроф-скрин к победителю. $ персист, честный учёт.

Судьи: gpt-5-mini(openai) + kimi(moonshot) + mistral(mistral). Для F(gpt) gpt-5-mini исключён.
"""
from __future__ import annotations
import argparse, json, re, sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "exp14"))  # exp14b reuses exp14's harness (exp14_common lives in eval/exp14/)
import exp14_common as C

ARMS = Path("/home/ubuntu/books/gu-zhenren/exp14b/arms")
OUT = Path("/home/ubuntu/books/gu-zhenren/exp14b")
ARM_FAMILY = {"C": "zai", "F": "openai", "X": "xai",
              "D": "deepseek", "K": "moonshot", "M": "mistral"}  # K→kimi-судья excl; M→mistral-судья excl
JUDGE_FAMILY = {"gpt-5-mini": "openai", "kimi-k2.6": "moonshot", "mistral-large-2512": "mistral"}
ALL_ARMS = ("C", "F", "X", "D", "K", "M")
CAPS = {"gpt-5-mini": 0.05, "kimi-k2.6": 0.08, "mistral-large-2512": 0.05}

SOFT = [
    ("e1", "8.1", "物是人非", "контраст «вещи/места ПРЕЖНИЕ — люди уже НЕ ТЕ»; плоское «всё изменилось» теряет половину (物是)."),
    ("e2", "2.1", "韬光养晦", "«скрывать блеск, выжидать» — таиться/копить силы, не выпячиваться; не «развиваться/трудиться»."),
    ("f1", "16.0", "这促使它不得不外出，寻找野生的酒囊花", "субъект «它» = ВИННЫЙ ЧЕРВЬ (酒虫); это ОН вынужден выйти искать дикие酒囊花. Не спутать субъект."),
    ("f2", "11.1", "他们的身形隐没在阴影中…分不清男女", "двое (пол НЕЯСЕН) в тени; НЕ навязывать род/пол, НЕ выдумывать кто это."),
]
SYS = ("Ты — билингвальный эксперт zh→ru. Оцени, верно ли русский перевод передаёт смысл ОДНОГО "
       "указанного китайского фрагмента. Ответь СТРОГО JSON без markdown: "
       '{"verdict":"correct|partial|wrong","catastrophe":true|false,'
       '"evidence":"дословная цитата из перевода ≤15 слов","note":"кратко"}. '
       "catastrophe=true при подмене субъекта/участника или перевёрнутом смысле.")


def run():
    ap = argparse.ArgumentParser(); ap.add_argument("--dry", action="store_true"); a = ap.parse_args()
    sp = C.Spender(OUT / "judge_costs.jsonl", CAPS)
    vpath = OUT / "soft_verdicts.jsonl"; out = []; done = set()
    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["trap"], v["arm"], v["judge"]))
    tasks = []
    for tid, cid, span, meaning in SOFT:
        for arm in ALL_ARMS:
            fp = ARMS / arm / f"{cid}.txt"
            if not fp.exists(): continue
            for jm in JUDGE_FAMILY:
                if JUDGE_FAMILY[jm] == ARM_FAMILY[arm]: continue  # self-family exclusion
                if (tid, arm, jm) in done: continue
                tasks.append((tid, cid, span, meaning, arm, jm, fp.read_text(encoding="utf-8")))
    print(f"soft-judge tasks: {len(tasks)} new ({len(done)} done)")
    if a.dry:
        from collections import Counter; print(Counter(t[5] for t in tasks)); return
    errs = {"empty": 0, "http": 0, "other": 0}
    for tid, cid, span, meaning, arm, jm, text in tasks:
        user = (f"КИТАЙСКИЙ ФРАГМЕНТ: {span}\nДОПУСТИМЫЙ СМЫСЛ: {meaning}\n\n"
                f"РУССКИЙ ПЕРЕВОД ОТРЫВКА:\n{text}\n\nОцени передачу ТОЛЬКО этого фрагмента. JSON.")
        s = C.spec(jm, max_tokens=(16000 if jm == "kimi-k2.6" else 6000))
        in_est = C.count_tokens(SYS + user, "glm")
        ok, pred = sp.guard(jm, in_est, s["max_tokens"])
        if not ok:
            print(f"  OVER {tid} {arm} {jm}"); continue
        t, 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"], "trap": tid, "arm": arm, "err": err, "usage": usage})
        if err:
            errs["empty" if "empty" in err else ("http" if "http" in err else "other")] += 1
        v = parse(t) if not err else {"verdict": "ERR"}
        v.update(trap=tid, arm=arm, judge=jm)
        out.append(v)
        with open(vpath, "a", encoding="utf-8") as f: f.write(json.dumps(v, ensure_ascii=False) + "\n")
        print(f"  {tid} {arm} {jm:<18} → {v.get('verdict'):<8} cat={v.get('catastrophe')} ${cst:.4f}")
    print(f"\n=== ошибки судей (честно): {errs} ===")
    summarize(out)


def parse(text):
    m = re.search(r"\{.*\}", text, re.S)
    if not m: return {"verdict": "PARSE"}
    try:
        d = json.loads(m.group(0))
        return {"verdict": str(d.get("verdict", "?")).lower(), "catastrophe": bool(d.get("catastrophe")),
                "evidence": (d.get("evidence") or "")[:100], "note": (d.get("note") or "")[:100]}
    except Exception:
        vv = re.search(r'"verdict"\s*:\s*"(\w+)"', m.group(0))
        return {"verdict": vv.group(1).lower() if vv else "PARSE", "catastrophe": "true" in m.group(0).lower()}


def summarize(out):
    from collections import defaultdict
    agg = defaultdict(lambda: defaultdict(list))
    for v in out: agg[v["trap"]][v["arm"]].append(v)
    print("\n=== SOFT-классы: вердикты (арм: судьи) ===")
    for tid, cid, span, meaning in SOFT:
        if tid not in agg: continue
        print(f"\n{tid} ch{cid} [{span}]:")
        for arm in ALL_ARMS:
            vs = agg[tid].get(arm)
            if not vs: continue
            zv = [(v["judge"][:4], v["verdict"], "CAT" if v.get("catastrophe") else "") for v in vs]
            print(f"   {arm}: {zv}")


if __name__ == "__main__":
    run()
