#!/usr/bin/env python3
"""exp14 — детерминированный структурный KPI (Претензия 1, БЕЗ судьи) + гварды.

$0. Метрика Претензии 1: распределение предложений/нарратив-абзац (baseline P0: медиана 1.0,
среднее 1.70, доля 1-предл. 0.58). Гварды: CJK-утечка=0, length-ratio, glossary-присутствие,
диалог-тире-парность. Аварийный гейт: нельзя «побеждать» только МЕНЬШИМ числом абзацев →
KPI парно с atom-coverage-прокси (длина не должна коллапсировать).

Использование: exp14_kpi.py [--scope trap|stage2]
"""
from __future__ import annotations
import argparse, json, re, statistics
from pathlib import Path
import exp14_common as C

ARMS = C.DIAG / "arms"
MAT = json.load(open(C.DIAG / "material.json"))


def paras(t): return [p for p in re.split(r"\n\s*\n", t.strip()) if p.strip()]
def sents(p): return [s for s in re.split(r"(?<=[.!?…])\s+", p.strip()) if s.strip()]
def is_dlg(p): return p.strip().startswith("—") or p.strip().startswith("–")
def han(t): return sum(1 for c in t if "一" <= c <= "鿿" or "㐀" <= c <= "䶿")


def kpi_of(text: str) -> dict:
    ps = paras(text)
    narr = [p for p in ps if not is_dlg(p)]
    dlg = [p for p in ps if is_dlg(p)]
    sp = [len(sents(p)) for p in narr] or [0]
    return dict(
        n_para=len(ps), n_narr=len(narr), n_dlg=len(dlg),
        median_spp=statistics.median(sp), mean_spp=round(statistics.mean(sp), 2),
        share_1sent=round(sum(1 for x in sp if x == 1) / len(sp), 3),
        chars=len(text), han=han(text),
    )


def gloss_presence(text: str, editor_constraint: str) -> float:
    """Доля dst-форм из инъекции, чьё СТЕММ (первые 4 симв.) встречается в выходе (прокси)."""
    dsts = re.findall(r"«([^»]+)»", editor_constraint)
    if not dsts:
        return 1.0
    hit = 0
    for d in dsts:
        stem = d.split()[0][:4]
        if stem and stem.lower() in text.lower():
            hit += 1
    return round(hit / len(dsts), 3)


def unit_map(scope):
    if scope == "trap":
        return {f"{u['chapter']}.{u['chunk_idx']}": u for u in MAT["trap_chunks"]}
    m = {}
    for ch, chunks in MAT["stage2"].items():
        for u in chunks:
            m[f"{ch}.{u['chunk_idx']}"] = dict(chapter=int(ch), **u)
    return m


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--scope", default="trap")
    a = ap.parse_args()
    units = unit_map(a.scope)
    arms = sorted(d.name for d in ARMS.iterdir() if d.is_dir())
    print(f"=== exp14 KPI ({a.scope}, {len(units)} chunks) — Претензия 1 + гварды ===\n")
    rows = {}
    for arm in arms:
        agg = []
        han_total, chars_total, chars_p0 = 0, 0, 0
        gl = []
        for uid, u in units.items():
            fp = ARMS / arm / f"{uid}.txt"
            if not fp.exists():
                continue
            t = fp.read_text(encoding="utf-8")
            k = kpi_of(t)
            agg.append(k); han_total += k["han"]; chars_total += k["chars"]
            chars_p0 += len(u["final_p0"])
            gl.append(gloss_presence(t, u.get("editor_constraint", "")))
        if not agg:
            continue
        allsp_median = statistics.median([k["median_spp"] for k in agg])
        mean_spp = round(statistics.mean([k["mean_spp"] for k in agg]), 2)
        share1 = round(statistics.mean([k["share_1sent"] for k in agg]), 3)
        lr = round(chars_total / max(1, chars_p0), 3)
        rows[arm] = dict(n=len(agg), mean_spp=mean_spp, share_1sent=share1,
                         han=han_total, len_ratio_vs_p0=lr,
                         gloss=round(statistics.mean(gl), 3))
    hdr = f"{'arm':<12}{'n':>3}{'mean_spp':>10}{'share_1s':>10}{'CJK':>6}{'len/P0':>8}{'gloss':>7}"
    print(hdr); print("-" * len(hdr))
    # baseline first
    order = [a for a in ["P0", "P1a", "P1b", "P1c", "A-layout", "A-2pass",
                         "A-ref-prev", "A-ref-next", "A-ref-both", "F-base", "F-disc"] if a in rows]
    order += [a for a in rows if a not in order]
    for arm in order:
        r = rows[arm]
        print(f"{arm:<12}{r['n']:>3}{r['mean_spp']:>10}{r['share_1sent']:>10}"
              f"{r['han']:>6}{r['len_ratio_vs_p0']:>8}{r['gloss']:>7}")
    print("\nПретензия-1 сигнал: mean_spp↑ и share_1sent↓ vs P0 = меньше рублености (лучше);")
    print("гейт: len/P0 не должен коллапсировать (<~0.9 = подозрение на потерю атомов); CJK=0 обяз.")
    (C.DIAG / f"kpi_{a.scope}.json").write_text(json.dumps(rows, ensure_ascii=False, indent=1), encoding="utf-8")


if __name__ == "__main__":
    main()
