#!/usr/bin/env python3
"""exp12 — извлечение per-chunk (source/draft/final) из приёмочного store + выбор 3 чистых глав.

$0. Никаких вызовов. Источники (ВНЕ git):
  - store копия:  /home/ubuntu/books/gu-zhenren/diag/store-copy.db (draft=deepseek, edit=glm-5)
  - параллель:    /home/ubuntu/books/gu-zhenren/acceptance/guzhenren-25ch-parallel.txt ([ОРИГ]/[ПЕРЕВОД])

Выход:
  - diag/chunks.json — per (chapter,chunk): source, draft(A0), final(A1), dispositions
  - diag/selection.json — метрики выбора глав + 3 выбранные главы (пре-регистрированное правило)

Пре-регистрированное правило выбора (frozen, см. 12-quality-diagnosis.md §Пре-регистрация):
  frame = ЧИСТЫЕ главы (все чанки disposition=ok на draft И edit, flag_reason пуст).
  метрика на ИСХОДНИКЕ zh: D = доля символов внутри кавычек “…” (U+201C…U+201D) от не-пробельных.
  выбор: high=argmax D, low=argmin D, mid=ближайшая к медиане D. Тай-брейк — меньший номер главы.
"""
from __future__ import annotations
import json, re, sqlite3, sys
from pathlib import Path

DIAG = Path("/home/ubuntu/books/gu-zhenren/diag")
DB = DIAG / "store-copy.db"
PARALLEL = Path("/home/ubuntu/books/gu-zhenren/acceptance/guzhenren-25ch-parallel.txt")

CH_HDR = re.compile(r"【\s*Глава\s+(\d+)\s*】")
CHUNK_HDR = re.compile(r"^---\s*чанк\s+(\d+)\s*\[(.+?)\]\s*---")
LQUOTE, RQUOTE = "“", "”"  # “ ”


def parse_parallel(path: Path) -> dict:
    """→ {(ch,chunk): {'source': str, 'final_parallel': str, 'flag': str}}"""
    out: dict = {}
    ch = None
    chunk = None
    mode = None  # 'orig' | 'per'
    buf_src: list[str] = []
    buf_per: list[str] = []

    def flush():
        if ch is not None and chunk is not None:
            out[(ch, chunk)] = {
                "source": "\n".join(buf_src).strip(),
                "final_parallel": "\n".join(buf_per).strip(),
                "flag": flag,
            }

    flag = ""
    for line in path.read_text(encoding="utf-8").splitlines():
        m = CH_HDR.search(line)
        if m:
            flush(); ch = int(m.group(1)); chunk = None; mode = None
            buf_src, buf_per = [], []
            continue
        m = CHUNK_HDR.match(line.strip())
        if m:
            flush()
            chunk = int(m.group(1)); flag = m.group(2).strip()
            mode = None; buf_src, buf_per = [], []
            continue
        s = line.strip()
        if s == "[ОРИГ]":
            mode = "orig"; continue
        if s == "[ПЕРЕВОД]":
            mode = "per"; continue
        if s.startswith("[⚠") or s.startswith("=====") or not s and mode is None:
            continue
        if mode == "orig":
            buf_src.append(line)
        elif mode == "per":
            buf_per.append(line)
    flush()
    return out


def load_store(db: Path) -> dict:
    """→ {(ch,chunk): {'draft':..,'edit':..,'draft_disp':..,'edit_disp':..,'draft_flag':..,'edit_flag':..}}"""
    con = sqlite3.connect(db)
    con.row_factory = sqlite3.Row
    rows = con.execute("""
      SELECT cs.chapter, cs.chunk_idx, cs.stage, cs.disposition, cs.flag_reason,
             cp.model_actual, cp.response_text
      FROM chunk_status cs
      LEFT JOIN checkpoints cp ON cp.request_hash = cs.final_hash
      ORDER BY cs.chapter, cs.chunk_idx, cs.stage
    """).fetchall()
    con.close()
    out: dict = {}
    for r in rows:
        key = (r["chapter"], r["chunk_idx"])
        d = out.setdefault(key, {})
        st = r["stage"]
        d[st] = r["response_text"] or ""
        d[f"{st}_disp"] = r["disposition"]
        d[f"{st}_flag"] = r["flag_reason"]
        d[f"{st}_model"] = r["model_actual"]
    return out


def dialogue_density(src: str) -> float:
    nonspace = re.sub(r"\s", "", src)
    if not nonspace:
        return 0.0
    inside = 0
    depth = 0
    for c in src:
        if c == LQUOTE:
            depth += 1; continue
        if c == RQUOTE:
            if depth > 0:
                depth -= 1
            continue
        if depth > 0 and not c.isspace():
            inside += 1
    return inside / len(nonspace)


def main() -> None:
    par = parse_parallel(PARALLEL)
    store = load_store(DB)

    # merge per chunk
    chunks = {}
    keys = sorted(set(par) | set(store))
    for k in keys:
        ch, ci = k
        p = par.get(k, {})
        s = store.get(k, {})
        chunks[f"{ch}/{ci}"] = {
            "chapter": ch, "chunk": ci,
            "source": p.get("source", ""),
            "draft": s.get("draft", ""),       # A0
            "final": s.get("edit", ""),        # A1 (glm-5 mono)
            "final_parallel": p.get("final_parallel", ""),
            "draft_disp": s.get("draft_disp", "?"),
            "edit_disp": s.get("edit_disp", "?"),
            "draft_flag": s.get("draft_flag", ""),
            "edit_flag": s.get("edit_flag", ""),
        }

    # sanity: store edit vs parallel final match
    mism = [k for k, v in chunks.items()
            if v["final"] and v["final_parallel"]
            and v["final"].strip()[:120] != v["final_parallel"].strip()[:120]]

    # per-chapter clean classification + dialogue density
    chap = {}
    for k, v in chunks.items():
        c = v["chapter"]
        d = chap.setdefault(c, {"chunks": 0, "clean": True, "src": [], "flags": []})
        d["chunks"] += 1
        d["src"].append(v["source"])
        if v["draft_disp"] != "ok" or v["edit_disp"] != "ok":
            d["clean"] = False
            d["flags"].append(f"{v['draft_flag']}|{v['edit_flag']}")

    metrics = {}
    for c, d in chap.items():
        full_src = "\n".join(d["src"])
        metrics[c] = {
            "clean": d["clean"],
            "chunks": d["chunks"],
            "src_chars": len(re.sub(r"\s", "", full_src)),
            "dialogue_density": round(dialogue_density(full_src), 4),
        }

    clean = {c: m for c, m in metrics.items() if m["clean"]}
    ds = sorted((m["dialogue_density"], c) for c, m in clean.items())
    dens = [x[0] for x in ds]
    median = dens[len(dens) // 2] if len(dens) % 2 else (dens[len(dens)//2 - 1] + dens[len(dens)//2]) / 2

    high = max(clean, key=lambda c: (clean[c]["dialogue_density"], -c))
    low = min(clean, key=lambda c: (clean[c]["dialogue_density"], c))
    mid = min((c for c in clean if c not in (high, low)),
              key=lambda c: (abs(clean[c]["dialogue_density"] - median), c))

    selection = {
        "rule": "clean-frame; D=share of chars in “…”; high=argmax, low=argmin, mid=nearest-median; tie=min chapter#",
        "clean_chapters": sorted(clean),
        "median_D": round(median, 4),
        "selected": {
            "high_dialogue": {"chapter": high, **clean[high]},
            "mid": {"chapter": mid, **clean[mid]},
            "low_exposition": {"chapter": low, **clean[low]},
        },
        "all_clean_metrics": {str(c): clean[c] for c in sorted(clean)},
    }

    DIAG.mkdir(exist_ok=True)
    (DIAG / "chunks.json").write_text(json.dumps(chunks, ensure_ascii=False, indent=1), encoding="utf-8")
    (DIAG / "selection.json").write_text(json.dumps(selection, ensure_ascii=False, indent=1), encoding="utf-8")

    print(f"chunks extracted: {len(chunks)}  | store-parallel final mismatches (first120): {len(mism)}")
    if mism:
        print("  mism keys:", mism[:10])
    print(f"clean chapters ({len(clean)}): {sorted(clean)}")
    print(f"median D = {median:.4f}")
    print("\nDialogue density (clean, sorted):")
    for dval, c in ds:
        star = " <-HIGH" if c == high else " <-LOW" if c == low else " <-MID" if c == mid else ""
        print(f"  ch{c:>2}: D={dval:.4f}  chunks={clean[c]['chunks']}  src_chars={clean[c]['src_chars']}{star}")
    print(f"\nSELECTED: high=ch{high} (D={clean[high]['dialogue_density']}), "
          f"mid=ch{mid} (D={clean[mid]['dialogue_density']}), low=ch{low} (D={clean[low]['dialogue_density']})")


if __name__ == "__main__":
    main()
