#!/usr/bin/env python3
"""exp15 — S2' synthetic scene-marked slice via guzhenren chapter concatenation (owner decision 2026-07-17).

S2' = concatenate the FIRST N consecutive 节 of 蛊真人 (opening arc), STRIP the 第X节 headers at the seams,
so each seam is an IMPLICIT scene boundary (no explicit marker). Every seam is recorded as a ground-truth
boundary (rune-offset in the concatenated text + the two adjacent 节 ids). The seam previews (tail of 节_i +
head of 节_{i+1}) are emitted so each seam can be MANUALLY classified scene-shift vs continuation-cliffhanger
(§1.14b): continuation seams are negative-boundaries ("must NOT cut here"), separate strata for Q1.

Deterministic, $0, pure function of the book bytes. Outputs OUTSIDE git.
Run: eval/.venv/bin/python eval/exp15/build_s2prime.py [N]     # default N=12 节
"""
from __future__ import annotations

import json
import re
import sys
from pathlib import Path

BOOK = Path("/home/ubuntu/books/gu-zhenren/guzhenren-utf8.txt")
OUT_DIR = Path("/home/ubuntu/books/gu-zhenren/exp15")
OUT_TXT = OUT_DIR / "S2prime.txt"
OUT_SEAMS = OUT_DIR / "S2prime_seams.json"
OUT_PREVIEW = OUT_DIR / "S2prime_seam_previews.md"

SEC_RE = re.compile(r"^第[0-9一二三四五六七八九十百千零两]{1,10}[节節][：: ]?.*$", re.M)
PREVIEW_TAIL, PREVIEW_HEAD = 220, 220


def load_text() -> str:
    raw = BOOK.read_bytes()
    try:
        return raw.decode("utf-8")
    except UnicodeDecodeError:
        return raw.decode("gb18030", errors="replace")


def sections(text: str):
    """Return list of (header_line, body_text) for each 节 in file order (body = up to next 节)."""
    marks = list(SEC_RE.finditer(text))
    out = []
    for k, m in enumerate(marks):
        header = m.group(0).strip()
        body_start = m.end()
        body_end = marks[k + 1].start() if k + 1 < len(marks) else len(text)
        body = text[body_start:body_end].strip("\n")
        out.append((k + 1, header, body))   # sec index n = k+1 (running, per gu_corpus_build convention)
    return out


def main():
    n = int(sys.argv[1]) if len(sys.argv) > 1 else 12
    OUT_DIR.mkdir(parents=True, exist_ok=True)
    text = load_text()
    secs = sections(text)
    if len(secs) < n:
        print(f"only {len(secs)} 节 found; using all", file=sys.stderr)
        n = len(secs)
    chosen = secs[:n]

    # concatenate bodies (headers STRIPPED at seams); single "\n" join = the implicit seam
    parts, seams, cursor = [], [], 0
    JOIN = "\n"
    for i, (idx, header, body) in enumerate(chosen):
        if i > 0:
            cursor += len(JOIN)                      # account the join separator (rune count)
        seam_offset = cursor                         # rune offset where this section's body begins
        if i > 0:
            seams.append({
                "seam_id": i,
                "rune_offset": seam_offset,           # boundary between prev body and this body
                "prev_sec": chosen[i - 1][0],
                "next_sec": idx,
                "prev_header": chosen[i - 1][1],
                "next_header": header,
                "class": "UNCLASSIFIED",              # -> scene-shift | continuation (manual, §1.14b)
                "rationale": "",
            })
        parts.append(body)
        cursor += len(body)
    joined = JOIN.join(parts)

    OUT_TXT.write_text(joined, encoding="utf-8")
    OUT_SEAMS.write_text(json.dumps({
        "book": "guzhenren", "arc": "opening", "n_sections": n,
        "section_ids": [c[0] for c in chosen],
        "total_runes": len(joined),
        "headers_stripped_at_seams": True,
        "seam_marker": "implicit (single \\n; no 第X节 header)",
        "seams": seams,
    }, ensure_ascii=False, indent=2), encoding="utf-8")

    # seam-context previews for MANUAL classification (read tail_i + head_{i+1})
    pv = [f"# S2' seam previews — classify each: scene-shift | continuation-cliffhanger (§1.14b)",
          f"# {n} 节 concatenated, headers stripped; {len(seams)} seams; total {len(joined):,} runes", ""]
    for s in seams:
        off = s["rune_offset"]
        tail = joined[max(0, off - len(JOIN) - PREVIEW_TAIL):off - len(JOIN)]
        head = joined[off:off + PREVIEW_HEAD]
        pv += [f"## seam {s['seam_id']}  (节{s['prev_sec']} → 节{s['next_sec']}, offset {off})",
               f"**{s['prev_header']}** → **{s['next_header']}**",
               f"…tail of 节{s['prev_sec']}: …{tail}",
               f"head of 节{s['next_sec']}: {head}…", ""]
    OUT_PREVIEW.write_text("\n".join(pv), encoding="utf-8")

    print(f"S2' built: {n} 节, {len(seams)} seams, {len(joined):,} runes -> {OUT_TXT}")
    print(f"section ids: {[c[0] for c in chosen]}")
    print(f"seams (offset, 节i->节j): " + ", ".join(f"{s['rune_offset']}({s['prev_sec']}->{s['next_sec']})" for s in seams))
    print(f"previews for manual classification -> {OUT_PREVIEW}")


if __name__ == "__main__":
    main()
