#!/usr/bin/env python3
"""exp16 — owner-facing deliverables (research/20 §C2-8 карта подписи, §E-1 мини-голд, §D1 precision@30). $0.

Emits three markdown sheets for the owner (sidecars, NOT the seed schema — miner never writes approved):
  1. signature_map.md   — per proposed entity: canon dst-proposal (by ALL occurrences), dst-variants,
                          top-KWIC contexts (H15 lever), gender counters (他/她), zone flags Z1-Z5, weak edges.
  2. alias_minigold.md  — candidate entity-edges for the ~20-30 min owner touchpoint (strike/add).
  3. precision_at30.md  — top-30 NON-seed A3 candidates for owner adjudication (§D1 precision@30 primary path).

Uses cold-start drafts for canon/spread if present (ecological), else injected records.json (confounded).
"""
from __future__ import annotations

import json
import re
from pathlib import Path

import exp16_common as X
import spread as SP
import canon as CANON
import palladius as PAL
import arms as A
import alias as ALIAS

BOOK = Path("/home/ubuntu/books/gu-zhenren")
OUT = BOOK / "exp16"
COLD_PLAIN = OUT / "coldstart" / "plain"

_RE_HE, _RE_SHE = re.compile("他"), re.compile("她")


def kwic(term, chunks, window=18, maxn=3):
    tn = X.norm(term)
    out = []
    for c in chunks:
        s = c.source
        for m in re.finditer(re.escape(term), s):
            i = m.start()
            ctx = s[max(0, i - window):i + len(term) + window].replace("\n", " ")
            out.append(f"…{ctx}…")
            if len(out) >= maxn:
                return out
    return out


def gender_counts(term, chunks, window=40):
    """他/她 counts within `window` chars of each occurrence — EVIDENCE ONLY (verdict = owner, D19.3)."""
    he = she = 0
    for c in chunks:
        s = c.source
        for m in re.finditer(re.escape(term), s):
            seg = s[max(0, m.start() - window):m.end() + window]
            he += len(_RE_HE.findall(seg))
            she += len(_RE_SHE.findall(seg))
    return he, she


def zone_flags(ent, cand, sm, chunks):
    """§B4 zone tags for the candidate (evidence, not verdict)."""
    flags = []
    sn = X.norm(ent.src if hasattr(ent, "src") else cand.src)
    sp = sm.spread(sn)
    variants, _ = sm.dst_variants(sn)
    top_dst = variants[0][0] if variants else ""
    # Z1 suspicious_stable: high score + spread~0 + dst not a Palladius name and not capitalized-name
    is_name_dst = PAL.is_palladius_token(top_dst, 2) and sm.is_name_lemma(top_dst)
    if cand and cand.score > 500 and sp < 0.05 and top_dst and not is_name_dst and \
       (hasattr(ent, "typ") and ent.typ in ("term", "title")):
        flags.append("Z1?suspicious_stable")
    if sp >= 0.3:
        flags.append("Z2?sense_split/spread")
    f = X.gt_occurrences(ent, chunks) if hasattr(ent, "norm_surfaces") else cand.freq
    if f < 3:
        flags.append("Z3:rare")
    return flags


def load_banknote_dst():
    """Aggregate translator-emitted src->dst from the cold-start banknote drafts (reliable dst; §3.3)."""
    bdir = OUT / "coldstart" / "banknote"
    dst = {}
    for f in sorted(bdir.glob("*.banknote.json")):
        for e in json.load(open(f))["entries"]:
            sn = X.norm(e["src"])
            dst.setdefault(sn, []).append(e["dst"])
    # majority dst per src
    from collections import Counter
    return {sn: Counter(v).most_common(1)[0][0] for sn, v in dst.items()}


def build_signature_map(gt, chunks, sm, a3, tag, bank_dst=None):
    bank_dst = bank_dst or {}
    lines = [f"# Карта подписи exp16 (miner-v1, {tag}) — сайдкар владельцу",
             "",
             "> Майнер НИКОГДА не пишет `approved`. Ниже — предложения `draft` по кластерам: канон-предложение "
             "(по ВСЕМ вхождениям × Палладий-конформность), dst-варианты, KWIC-контексты (детерминированный "
             "H15-рычаг), счётчики пола (他/她 — evidence, вердикт ваш, D19.3), флаги зон §B4. "
             "Владелец: approve / правка / оставить draft / расклеить.",
             ""]
    a3_by = {c.src: c for c in a3}
    # cover GT entities (the measurable set) + top non-GT name/place candidates
    gt_by_norm = {X.norm(s): e for e in gt for s in e.surfaces}
    seen = set()
    for e in sorted(gt, key=lambda e: -X.gt_occurrences(e, chunks)):
        sn = X.norm(e.src)
        if sn in seen:
            continue
        seen.add(sn)
        cand = a3_by.get(sn)
        proposed, variants = CANON.canon_propose(sm, e)
        vstr = ", ".join(f"{lm}({d})" for lm, d, _ in variants[:4])
        he, she = gender_counts(e.src, chunks)
        zf = zone_flags(e, cand, sm, chunks)
        occ = X.gt_occurrences(e, chunks)
        # dst proposal priority: banknote (translator-emitted, reliable) > Palladius-name canon > co-occ note
        bank = bank_dst.get(sn)
        if bank:
            canon_str = f"«{bank}» (banknote-канал)"
        elif proposed:
            canon_str = f"«{' '.join(proposed)}» (Палладий-канон)"
        else:
            canon_str = "(dst — банкнота/облако: co-occ извлечение вырождено, §3.3)"
        lines.append(f"### {e.src} — предл. dst: {canon_str}  "
                     f"[{e.typ}, occ={occ}, статус сида={e.status}, подписанный dst=«{e.dst}»]")
        lines.append(f"- co-occ dst-варианты (справочно): {vstr}")
        if he or she:
            lines.append(f"- пол-evidence: 他={he} 她={she} (вердикт — владелец)")
        if zf:
            lines.append(f"- зоны §B4: {', '.join(zf)}")
        for k in kwic(e.src, chunks):
            lines.append(f"- KWIC: `{k}`")
        lines.append("")
    return "\n".join(lines)


def build_minigold(gt, chunks, sm):
    gt_by_norm = {X.norm(s): e for e in gt for s in e.surfaces}
    ar = A.Arms(chunks, X.Contrast())
    a3 = ar.arm_A3()
    subsumed = ar.va.subsumed
    gt_surf = {X.norm(s) for e in gt for s in e.surfaces}
    PARTICLE = ALIAS.__dict__.get("PARTICLE", set())
    name_cands = [c.src for c in a3[:200] if any(t in c.types for t in ("name", "place", "title"))
                  and c.freq >= 5 and c.src not in subsumed and len(c.src) >= 2]
    surface_srcs = list(gt_surf | set(name_cands))
    surfaces = ALIAS.build_surfaces(surface_srcs, sm, gt_by_norm)
    ident, family, weak = ALIAS.propose_edges(surfaces, chunks, sm, frozenset(gt_surf))
    clusters = ALIAS.cluster(ident, list(surfaces))
    lines = ["# Мини-голд алиасов exp16 — тачпойнт владельца (~20–30 мин)",
             "",
             "> Код ПРЕДЛОЖИЛ рёбра сущностей слайса-25. Вычеркните ложные, допишите пропущенные. Только после "
             "этого меряется recall кластеризации (сейчас — только precision предложенных). Тир-1 имеет "
             "известную ошибку (§B2): спорное держим раздельным draft.", "",
             "## Предложенные IDENTITY-кластеры (одна сущность):"]
    for cl in clusters:
        lines.append(f"- [ ] {{ {', '.join(cl)} }}")
    lines += ["", "## Предложенные FAMILY-суперкластеры (общая фамилия, НЕ тождество):"]
    fam_pairs = sorted({tuple(sorted((e['a'], e['b']))) for e in family})
    for a, b in fam_pairs[:40]:
        lines.append(f"- [ ] {a} ~ {b}")
    lines += ["", "## Слабые/tier-2 рёбра (со-оккуренция, решения нет — предложения к связи):"]
    for e in weak[:30]:
        lines.append(f"- [ ] {e['a']} ? {e['b']}  [{e.get('rule','')}]")
    return "\n".join(lines)


def build_precision30(gt, chunks, sm, a3):
    gt_surf = {X.norm(s) for e in gt for s in e.surfaces}
    non_seed = [c for c in a3 if c.src not in gt_surf][:30]
    lines = ["# precision@30 — топ-30 НЕ-сидовых кандидатов A3 (V-C) для адъюдикации владельца",
             "",
             "> Отметьте: T = валидная сущность банка (имя/титул/место/термин), F = мусор/фрагмент, "
             "? = спорно. Это первичный путь precision@30 (§D1; precision@сид НЕ метрика — сид не исчерпывающий).",
             "", "| # | кандидат | freq | типы | evidence | dst-намёк | T/F/? |",
             "|---|---|---|---|---|---|---|"]
    for i, c in enumerate(non_seed):
        variants, _ = sm.dst_variants(c.src, top=2)
        dsth = variants[0][0] if variants else ""
        ev = ",".join(c.evidence[:2])
        lines.append(f"| {i+1} | {c.src} | {c.freq} | {'/'.join(c.types) or '-'} | {ev} | {dsth} |  |")
    return "\n".join(lines)


def main():
    gt = X.load_gt()
    # prefer cold-start drafts (ecological) for canon/spread; fall back to injected
    from coldstart_analyze import load_coldstart_chunks, COLD_CHAPTERS
    cold = load_coldstart_chunks("plain")
    if cold:
        chunks_for_canon = cold
        tag = f"cold-start ch{COLD_CHAPTERS}"
    else:
        chunks_for_canon = X.load_chunks()
        tag = "injected records.json (confounded — cold-start refines)"
    full = X.load_chunks()
    sm = SP.SpreadModel(chunks_for_canon)
    ar = A.Arms(full, X.Contrast())
    a3 = ar.arm_A3()

    # signature map over the canon-source chunks
    gt_canon = [e for e in gt if X.gt_occurrences(e, chunks_for_canon) >= 1]
    bank_dst = load_banknote_dst()
    sig = build_signature_map(gt_canon, chunks_for_canon, sm, a3, tag, bank_dst=bank_dst)
    (OUT / "signature_map.md").write_text(sig, encoding="utf-8")

    sm_full = SP.SpreadModel(full)
    mg = build_minigold(gt, full, sm_full)
    (OUT / "alias_minigold.md").write_text(mg, encoding="utf-8")

    p30 = build_precision30(gt, full, sm_full, a3)
    (OUT / "precision_at30.md").write_text(p30, encoding="utf-8")

    print(f"[written] {OUT}/signature_map.md ({len(gt_canon)} entities, canon source: {tag})")
    print(f"[written] {OUT}/alias_minigold.md")
    print(f"[written] {OUT}/precision_at30.md (top-30 non-seed A3)")


if __name__ == "__main__":
    main()
