#!/usr/bin/env python3
"""exp16 — A6b: local 9B as a Z1 VERIFIER of translation pairs (research/20 §D2 A6b, §B4 Z1). $0 (stand).

§B4 routes SPOT to code (A6 showed code>=local 9B) and reserves the local 9B for VERIFICATION of the
stably-wrong class Z1 (蛊→«гусеница»: high termhood, zero spread, dst is a common word — invisible to the
spread signal). This mini-bench asks: can a local 9B judge whether a ru rendering of a zh term IN CONTEXT
is faithful? Gold is AUTOMATIC: CORRECT pairs use the owner-signed seed dst; WRONG pairs inject the Z1
failure class (plausible common-word mistranslations + cross-term swaps). Accuracy + wrong-catch rate.

30 pairs (15 correct + 15 wrong). Deterministic (temp 0, think off). Health-checked.
"""
from __future__ import annotations

import json
import re
import urllib.request
from pathlib import Path

import exp16_common as X

MODEL = "huihui_ai/qwen3.5-abliterated:9b"
OLLAMA = "http://localhost:11434/api/chat"
OUT = Path("/home/ubuntu/books/gu-zhenren/exp16")
_opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))

# Z1 verification bench: (src_term, correct_dst, wrong_dst[Z1 stably-wrong class]). Wrong = a plausible
# common Russian word or a cross-term swap that the spread signal would NOT catch.
BENCH = [
    ("蛊", "гу", "гусеница"),                 # Z1 archetype: 蛊 as a common bug-word
    ("蛊师", "гу-мастер", "заклинатель насекомых"),
    ("蛊虫", "гу-червь", "личинка"),
    ("转", "ранг", "оборот"),                  # 转 polysemy: rank vs turn/revolution
    ("元石", "первобытный камень", "метеорит"),
    ("真元", "истинная ци", "чистая энергия"),
    ("空窍", "апертура", "полость"),
    ("凡人", "смертный", "простолюдин"),
    ("修炼", "культивация", "тренировка"),
    ("长生", "бессмертие", "долголетие"),
    ("族长", "глава клана", "старейшина"),      # the D38 c3 confound as a Z1 pair
    ("四代族长", "Четвёртый глава рода", "четвёртый старейшина"),
    ("人上之人", "человек над людьми", "человек среди людей"),  # the c1 catastrophe as Z1
    ("方源", "Фан Юань", "Квадратный Источник"),  # name mistranslated literally
    ("花酒行者", "Монах Цветочного Вина", "Странник цветов и вина"),
]


def call_local(prompt, timeout=90):
    body = json.dumps({"model": MODEL, "think": False,
                       "messages": [{"role": "user", "content": prompt + "\n/no_think"}],
                       "stream": False, "options": {"temperature": 0.0, "num_predict": 200}}).encode()
    req = urllib.request.Request(OLLAMA, data=body, headers={"Content-Type": "application/json"})
    try:
        with _opener.open(req, timeout=timeout) as r:
            d = json.loads(r.read())
        return (d.get("message", {}) or {}).get("content", ""), None
    except Exception as e:
        return "", f"{type(e).__name__}: {str(e)[:120]}"


def find_sentence(term, chunks):
    tn = X.norm(term)
    for c in chunks:
        for sent in re.split(r"(?<=[。！？])", c.source):
            if term in sent and 6 <= len(sent) <= 80:
                return sent.strip()
    return term


def verdict_yes_no(text):
    text = re.sub(r"<think>.*?</think>", "", text, flags=re.S).lower()
    # first explicit yes/да or no/нет
    m = re.search(r"\b(да|верно|correct|yes|правильн)\b", text)
    n = re.search(r"\b(нет|неверно|incorrect|no|ошибочн|неправильн)\b", text)
    if n and (not m or n.start() < m.start()):
        return "no"
    if m:
        return "yes"
    return "?"


def main():
    chunks = X.load_chunks()
    pairs = []
    for term, ok_dst, bad_dst in BENCH:
        sent = find_sentence(term, chunks)
        pairs.append((term, sent, ok_dst, "correct"))
        pairs.append((term, sent, bad_dst, "wrong"))

    rows = []
    health = {"ok": 0, "failed": 0}
    for term, sent, dst, gold in pairs:
        prompt = (f"Китайский термин «{term}» в предложении:\n{sent}\n\n"
                  f"Предложенный русский перевод этого термина: «{dst}».\n\n"
                  f"Верен ли этот перевод термина по смыслу в данном контексте? "
                  f"Ответь одним словом: ДА или НЕТ, затем короткое обоснование.")
        out, err = call_local(prompt)
        if err:
            health["failed"] += 1
            rows.append(dict(term=term, dst=dst, gold=gold, pred="failed", err=err)); continue
        health["ok"] += 1
        pred = verdict_yes_no(out)
        model_says = "correct" if pred == "yes" else ("wrong" if pred == "no" else "?")
        rows.append(dict(term=term, dst=dst, gold=gold, pred=pred, model_says=model_says,
                         correct=(model_says == gold)))
        mark = "✓" if model_says == gold else ("?" if pred == "?" else "✗")
        print(f"  {mark} {term:<6} «{dst[:24]:<24}» gold={gold:<7} model={model_says:<7}")

    scored = [r for r in rows if r.get("pred") not in ("failed", "?")]
    acc = sum(r["correct"] for r in scored) / len(scored) if scored else 0
    # wrong-catch (recall on the Z1 wrong class) + correct-accept (specificity)
    wrong = [r for r in scored if r["gold"] == "wrong"]
    correct = [r for r in scored if r["gold"] == "correct"]
    wrong_catch = sum(r["model_says"] == "wrong" for r in wrong) / len(wrong) if wrong else 0
    correct_accept = sum(r["model_says"] == "correct" for r in correct) / len(correct) if correct else 0
    print(f"\n=== A6b Z1 verifier ({MODEL}) — {len(pairs)} pairs ===")
    print(f"health: {health}; scored: {len(scored)}")
    print(f"accuracy: {acc:.3f}")
    print(f"wrong-catch (recall on Z1 mistranslations): {sum(r['model_says']=='wrong' for r in wrong)}/{len(wrong)} = {wrong_catch:.3f}")
    print(f"correct-accept (specificity): {sum(r['model_says']=='correct' for r in correct)}/{len(correct)} = {correct_accept:.3f}")
    missed = [r["term"] for r in wrong if r["model_says"] != "wrong"]
    print(f"Z1 mistranslations the local 9B FAILED to catch: {missed}")
    json.dump({"model": MODEL, "health": health, "accuracy": acc, "wrong_catch": wrong_catch,
               "correct_accept": correct_accept, "rows": rows},
              open(OUT / "a6b_verify.json", "w"), ensure_ascii=False, indent=1)
    print(f"[written] {OUT/'a6b_verify.json'}")


if __name__ == "__main__":
    main()
