#!/usr/bin/env python3
"""Детерминированные рычаги канон-консистентности — кривая L1→L3 на ТРУДНОМ чанке.

Follow-up к adaptive_incontext/adaptive_reask (по запросу владельца: «раз не идём в гибрид —
долбить детерминированную сторону; замерить, сколько добивает каждый рычаг»). Лёгкий чанк
насыщал глоссарий до 1.0 → рычаги не было видно. Здесь — ТРУДНЫЙ чанк (1600 знаков, ~11
рекуррентных сущностей, плотно) + контролируемый recall-тест, где точный матч промахивается.

Три рычага (все ДЕТЕРМИНИРОВАННЫЕ, 0 ML, 0 VRAM):
  L1  post-check ЛЕММАТИЗИРУЮЩИЙ (pymorphy3), а не регэксп → убирает ложные флаги/undercount
      (мой промах 王胡→«Бородатого Вана» на регэкспе `Ван\b`). Меряем false-flag rate.
  L2  RECALL матчера: точный матч → +нормализация(trad→simp/NFKC) → +alias-граф → +sticky.
      Детерминированный (без LLM): сколько «должных» записей вообще инъектится.
  L3  post-check + точечный RE-ASK на РЕАЛЬНЫЕ промахи (verifier-gated correction) → прирост
      adherence на трудном чанке.

deepseek-v4-flash + grok-4.3. INDICATIVE (малый N). Usage: eval/.venv/bin/python eval/adaptive_levers.py
Output: stdout + eval/data/adaptive_levers.json
"""
from __future__ import annotations
import json, re, sys, unicodedata
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
import refusal_bench as rb
import pymorphy3

ROOT = Path(__file__).resolve().parent
SRC = ROOT / "data" / "samples" / "zh" / "luxun-ah-q-ch5-9.txt"
OUT = ROOT / "data" / "adaptive_levers.json"
MORPH = pymorphy3.MorphAnalyzer()

# Трудный чанк: 1600 знаков, ~11 рекуррентных сущностей (плотно → adherence проседает).
HARD = SRC.read_text(encoding="utf-8")[6450:6450 + 1600]

# entity → (canon dst для глоссария, СТРОГИЙ регэксп [наивный post-check], лемма-ключ[и] для L1)
ENT = {
    "阿Q":   ("А-кью",                   r"А-кью\b",                ["а-кью"]),   # дефис → hyph-режим
    "未庄":  ("Вэйчжуан",                r"Вэйчжуан\b",             ["вэйчжуан"]),
    "赵太爷": ("почтенный Чжао",          r"Чжао\b",                 ["чжао"]),
    "王胡":  ("Бородатый Ван",            r"Ван\b",                  ["ван"]),
    "小D":   ("Маленький Дэн",            r"Дэн\b",                  ["дэн"]),
    "假洋鬼子": ("Поддельный заморский чёрт", r"заморский чёрт\b",    ["заморский", "чёрт"]),
    "尼姑":  ("монашка",                  r"монашка\b",              ["монашка"]),
    "吴妈":  ("У-ма",                     r"У-ма\b",                 ["у-ма"]),
    "邹七嫂": ("тётушка Цзоу Седьмая",     r"Цзоу\b",                 ["цзоу"]),
    "秀才":  ("сюцай",                    r"сюцай\b",                ["сюцай"]),
    "钱":    ("Цянь",                     r"Цянь\b",                 ["цянь"]),
}
HYPH = {"а-кью": r"А-?кью", "у-ма": r"У-?ма"}  # дефисные translit — регэксп, не лемма

SYSTEM = ("Ты профессиональный литературный переводчик. Переведи фрагмент на русский язык. "
          "Сохрани все реплики и детали без пропусков; стиль — живой литературный русский. Выведи ТОЛЬКО перевод.")
GLOSS = "ГЛОССАРИЙ (используй утверждённые переводы последовательно):\n" + \
        "\n".join(f"{z} → {v[0]}" for z, v in ENT.items())
PRESENT = [z for z in ENT if z in HARD]


# ---------- матчеры post-check (L1) ----------
def match_strict(out, z):
    """Наивный регэксп-post-check (как в adaptive_incontext) — ломается на склонении."""
    return bool(re.search(ENT[z][1], out))

def lemmas_of(out):
    return {MORPH.parse(w)[0].normal_form for w in re.findall(r"[А-Яа-яЁё]+", out)}

def match_lemma(out, z, lem_cache):
    """L1: лемматизирующий post-check. Для дефисных translit — регэксп-фолбэк."""
    keys = ENT[z][2]
    if keys[0] in HYPH:
        return bool(re.search(HYPH[keys[0]], out))
    return all(k in lem_cache for k in keys)


# ---------- матчер РЕКОЛА (L2), детерминированный ----------
TRAD2SIMP = {"趙": "赵", "莊": "庄", "錢": "钱", "陳": "陈", "萬": "万", "舊": "旧", "臉": "脸"}
def normalize(s):
    s = unicodedata.normalize("NFKC", s)
    return "".join(TRAD2SIMP.get(c, c) for c in s)

ALIASES = {"王胡": ["王癞胡", "癞胡"], "赵太爷": ["赵老太爷"]}

def recall_config(chunks, keys_present, *, norm=False, alias=False, sticky=False):
    """Сколько записей из keys_present инъектится по цепочке чанков при данной конфигурации."""
    active_prev, injected_ever = set(), set()
    for ch in chunks:
        text = normalize(ch) if norm else ch
        hit = set()
        for z in keys_present:
            pats = [z] + (ALIASES.get(z, []) if alias else [])
            pats = [normalize(p) if norm else p for p in pats]
            if any(p in text for p in pats):
                hit.add(z)
        if sticky:
            hit |= (active_prev & set(keys_present))  # scene-inertia
        active_prev = hit
        injected_ever |= hit
    return injected_ever


def translate(model, base, key_env, user):
    t, _, _ = rb.call_provider({"name": model, "base_url": base, "model": model,
                                "api_key_env": key_env, "max_tokens": 8000,
                                **({"temperature": 0} if "grok" in model else {})}, SYSTEM, user, timeout=200)
    return t or ""


def main():
    res = {"present": PRESENT, "hard_chunk_len": len(HARD)}
    print(f"ТРУДНЫЙ чанк: {len(HARD)} знаков, present entities ({len(PRESENT)}): {PRESENT}\n")

    # ===== L2: детерминированный RECALL-curve (без LLM) =====
    print("=== L2 · RECALL матчера (детерминированный, без LLM) ===")
    # 3 чанка, где точный матч промахивается: trad-вариант, alias, местоименный (sticky)
    recall_chunks = [
        "趙太爺同錢老爺在未莊很有威。王癞胡也常來。",   # trad + alias 王癞胡(=王胡)
        "他慢慢站起来，看了看四周。",                      # местоименный — 王胡 из sticky
    ]
    keys = ["赵太爷", "钱", "未庄", "王胡"]
    configs = [("точный", {}), ("+норм", {"norm": True}),
               ("+alias", {"norm": True, "alias": True}),
               ("+sticky", {"norm": True, "alias": True, "sticky": True})]
    l2 = {}
    for name, kw in configs:
        got = recall_config(recall_chunks, keys, **kw)
        l2[name] = sorted(got)
        print(f"  {name:<9} recall {len(got)}/{len(keys)} = {len(got)/len(keys):.2f}  инъектировано: {sorted(got)}")
    res["L2_recall"] = l2

    # ===== L1 + L3: adherence + re-ask на трудном чанке (LLM) =====
    print("\n=== L1 (лемма-post-check) + L3 (re-ask) на трудном чанке ===")
    res["models"] = {}
    for model, base, kenv in [("deepseek-v4-flash", "https://api.deepseek.com/v1", "DEEPSEEK_API_KEY"),
                              ("grok-4.3", "https://api.x.ai/v1", "XAI_API_KEY")]:
        out = translate(model, base, kenv, GLOSS + "\n\nФРАГМЕНТ:\n" + HARD)
        if not out:
            print(f"\n[{model}] пустой ответ — пропуск"); continue
        lem = lemmas_of(out)
        strict = [z for z in PRESENT if match_strict(out, z)]
        lemma = [z for z in PRESENT if match_lemma(out, z, lem)]
        false_flags = [z for z in lemma if z not in strict]      # L1 чинит именно их
        real_miss = [z for z in PRESENT if z not in lemma]        # настоящие провалы adherence
        print(f"\n[{model}]  строгий-регэксп {len(strict)}/{len(PRESENT)}={len(strict)/len(PRESENT):.2f}"
              f"  |  L1 лемма-post-check {len(lemma)}/{len(PRESENT)}={len(lemma)/len(PRESENT):.2f}")
        print(f"  L1 убрал ЛОЖНЫХ флагов: {len(false_flags)}  {false_flags}")
        print(f"  РЕАЛЬНЫЕ промахи adherence (по лемме): {real_miss}")
        rec = {"strict": len(strict)/len(PRESENT), "lemma": len(lemma)/len(PRESENT),
               "false_flags": false_flags, "real_miss": real_miss}
        # L3: re-ask на реальные промахи
        if real_miss:
            terms = "; ".join(f"{z} = {ENT[z][0]}" for z in real_miss)
            out2 = translate(model, base, kenv, GLOSS + "\n\nФРАГМЕНТ:\n" + HARD +
                             f"\n\nВАЖНО: обязательно используй утверждённые формы: {terms}. Перепиши перевод с ними.")
            lem2 = lemmas_of(out2)
            lemma2 = [z for z in PRESENT if match_lemma(out2, z, lem2)]
            print(f"  L3 re-ask по {real_miss}: adherence {len(lemma)/len(PRESENT):.2f} → {len(lemma2)/len(PRESENT):.2f}")
            rec["after_reask"] = len(lemma2)/len(PRESENT)
            rec["still_missing"] = [z for z in PRESENT if z not in lemma2]
        else:
            print("  L3 re-ask не нужен (реальных промахов нет)")
            rec["after_reask"] = rec["lemma"]
        res["models"][model] = rec

    OUT.write_text(json.dumps(res, ensure_ascii=False, indent=2), encoding="utf-8")
    print(f"\nsaved → {OUT}")
    print("\nЧитается: L2 — рекол точного матча растёт с норм/alias/sticky; L1 — сколько ложных\n"
          "флагов убирает лемматизация (риск research/13 §2); L3 — сколько adherence добивает re-ask.")


if __name__ == "__main__":
    main()
