#!/usr/bin/env python3
"""exp16 — ADDITIVE canon extractor: base-rate-corrected (lift/PMI) dst-variants (research/20 §B1 mitigation
for ubiquitous terms). $0. NOT frozen — an analysis-layer improvement over the pre-registered chunk-Dice
(spread.dst_variants), which degenerates for terms present in ~every chunk (方源/蛊/古月 — no 'chunks
without X' contrast, so every frequent ru lemma looks associated).

lift(lemma, term) = |chunks(term) ∩ chunks(lemma)| / expected, expected = |chunks(term)|*|chunks(lemma)|/N.
A common ru word (год, everywhere) has lift≈1; the term's actual rendering (гу — only near 蛊) has high
lift. This surfaces the rendering even for ubiquitous terms. Reported ALONGSIDE the frozen method so the
pre-reg canon number and the true recoverability are both visible (§D4-а, no manufactured convergence).
"""
from __future__ import annotations

import exp16_common as X
import spread as SP
import palladius as PAL


def dst_variants_lift(sm: SP.SpreadModel, cand_norm: str, top=6, min_lift=1.5, min_cooccur=2):
    cidx = set(sm.cand_chunk_indices(cand_norm))
    N = len(sm.chunks)
    if len(cidx) < 1:
        return []
    from collections import Counter
    counts = Counter()
    for i in cidx:
        counts.update(sm.chunk_lemmas[i])
    scored = []
    for lm, co in counts.items():
        if co < min_cooccur:
            continue
        base = len(sm.lemma_chunks[lm])
        expected = len(cidx) * base / N
        lift = co / expected if expected else 0.0
        if lift >= min_lift:
            scored.append((lm, round(lift, 2), co))
    scored.sort(key=lambda t: (-t[1], -t[2], t[0]))
    return scored[:top]


def canon_propose_lift(sm, ent, top=6):
    lifted = dst_variants_lift(sm, X.norm(ent.src), top=top)
    lemmas = [lm for lm, _, _ in lifted]
    if ent.typ in ("name", "place", "nickname"):
        proposed = [lm for lm in lemmas if PAL.is_palladius_token(lm, 1) and sm.is_name_lemma(lm)]
        head = [lm for lm in lemmas if lm in ("гора", "деревня")]
        proposed = head[:1] + proposed if ent.typ == "place" else proposed
    else:
        proposed = lemmas[:4]
    return proposed, lifted


def canon_recovery_lift(sm, ent, cover_frac=0.5):
    import canon as CANON
    signed = set(CANON.signed_content_lemmas(ent.dst))
    if not signed:
        return None, [], []
    proposed, lifted = canon_propose_lift(sm, ent)
    assoc = {lm for lm, _, _ in lifted}
    covered = {s for s in signed if s in set(proposed) or s in assoc}
    return (len(covered) / len(signed) >= cover_frac), sorted(signed), proposed


def run(chunks, gt):
    sm = SP.SpreadModel(chunks)
    rec = tot = 0
    rows = []
    for e in gt:
        if not e.dst or X.gt_occurrences(e, chunks) == 0:
            continue
        r, signed, proposed = canon_recovery_lift(sm, e)
        if r is None:
            continue
        tot += 1
        rec += bool(r)
        rows.append(dict(src=e.src, typ=e.typ, recovered=bool(r), signed=signed, proposed=proposed))
    return rows, (rec / tot if tot else None), tot


if __name__ == "__main__":
    chunks = X.load_chunks(); gt = X.load_gt()
    sm = SP.SpreadModel(chunks)
    print("lift-based dst extraction (fixes ubiquitous-term degeneration) — full 57ch:\n")
    for s in ["蛊", "方源", "古月", "蛊师", "转", "元石", "族长"]:
        e = next(x for x in gt if x.src == s)
        prop, lifted = canon_propose_lift(sm, e)
        print(f"  {s:<4} signed=«{e.dst}» -> lift-proposed={prop}  top-lift={[(l,v) for l,v,_ in lifted[:4]]}")
    rows, rate, tot = run(chunks, gt)
    print(f"\ncanon-recovery (lift) on records.json full 57ch: {rate:.3f} (n={tot})")
