#!/usr/bin/env python3
import argparse
import hashlib
import json
import os
import re
import unicodedata
from datetime import datetime, timezone
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
RUN = ROOT / "work/story/run-002"
AUTH_RECEIPT = RUN / "authority-validation.json"
PROGRESS = ROOT / "log/run-002/draft-progress.json"
PRODUCTION_TERMS = re.compile(
    r"(?i)(?<!\w)(teaser|cold open|catch-up|beat|scene|POV|outline|candidate|canon|narration|prompt|receipt|metadata)(?!\w)"
)
EXPECTED_BEATS = {
    1: [1, 2, 3, 4, 5, 6],
    2: [7, 8, 9, 10, 11, 12],
    3: [13, 14, 15, 16, 17, 18],
    4: [19, 20, 21, 22, 23, 24],
}


def sha256(path):
    return hashlib.sha256(path.read_bytes()).hexdigest()


def atomic_json(path, value):
    path.parent.mkdir(parents=True, exist_ok=True)
    part = path.with_suffix(path.suffix + ".part")
    data = (json.dumps(value, ensure_ascii=False, indent=2) + "\n").encode("utf-8")
    with open(part, "xb") as handle:
        handle.write(data)
        handle.flush()
        os.fsync(handle.fileno())
    os.replace(part, path)


def fail(message):
    raise RuntimeError(message)


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--part", type=int, choices=EXPECTED_BEATS, required=True)
    parser.add_argument("--source", type=Path, required=True, help="Reviewed temp draft to promote as official source part")
    parser.add_argument("--review-note", required=True)
    args = parser.parse_args()

    manifest = json.loads((ROOT / "script/project-manifest.json").read_text(encoding="utf-8"))
    if manifest.get("active_run") != "run-002" or manifest.get("authority_revision") != "v2":
        fail("active run/authority revision mismatch")
    if manifest.get("status") != "run-002_authority_locked_prose_allowed":
        fail("official source-part close is not enabled")
    receipt = json.loads(AUTH_RECEIPT.read_text(encoding="utf-8"))
    if receipt.get("status") != "PASS" or receipt.get("verified") is not True or receipt.get("authority_revision") != "v2":
        fail("authority validation v2 not terminal PASS")
    for key, expected in receipt["authority_hashes"].items():
        path = ROOT / manifest["active_paths"][key]
        if sha256(path) != expected:
            fail(f"authority drift before part close: {key}")

    official = RUN / f"part-{args.part:02d}.txt"
    if official.exists() or official.with_suffix(".txt.part").exists():
        fail(f"official part already exists: {official}")
    if not args.source.is_absolute():
        args.source = (ROOT / args.source).resolve()
    if not args.source.exists():
        fail("reviewed temp source missing")
    if RUN not in args.source.parents or args.source.parent == RUN:
        fail("temp source must be under a run-002 subdirectory, not official namespace")

    raw = args.source.read_bytes()
    if raw.startswith(b"\xef\xbb\xbf") or b"\r" in raw:
        fail("BOM/CR forbidden")
    if not raw.endswith(b"\n") or raw.endswith(b"\n\n"):
        fail("part must end with exactly one LF")
    try:
        text = raw.decode("utf-8")
    except UnicodeDecodeError as exc:
        fail(f"invalid UTF-8: {exc}")
    if not unicodedata.is_normalized("NFC", text):
        fail("part is not NFC")
    if any(line.endswith((" ", "\t")) for line in text.splitlines()):
        fail("trailing whitespace")
    if re.search(r"(?m)^\s*#{1,6}\s+", text) or re.search(r"(?im)^\s*(chương|phần|cảnh|hồi|chapter|part|scene)\s*(\d+|[IVXLC]+|[:\-])", text):
        fail("heading/section marker forbidden")
    production_hits = [i for i, line in enumerate(text.splitlines(), 1) if PRODUCTION_TERMS.search(line)]
    if production_hits:
        fail(f"production terminology in narration lines: {production_hits}")
    cta_hits = [i for i, line in enumerate(text.splitlines(), 1) if re.search(r"(?i)(đăng ký kênh|subscribe|nhấn chuông|hãy like|hãy chia sẻ)", line)]
    if cta_hits:
        fail(f"CTA in narration lines: {cta_hits}")

    prior = []
    if PROGRESS.exists():
        state = json.loads(PROGRESS.read_text(encoding="utf-8"))
        if state.get("authority_validation_sha256") != sha256(AUTH_RECEIPT):
            fail("draft progress belongs to another authority validation receipt")
        prior = state.get("parts", [])
    expected_prior = list(range(1, args.part))
    if [row["part"] for row in prior] != expected_prior:
        fail("parts must close sequentially without gaps")

    official.parent.mkdir(parents=True, exist_ok=True)
    temp = official.with_suffix(".txt.part")
    with open(temp, "xb") as handle:
        handle.write(raw)
        handle.flush()
        os.fsync(handle.fileno())
    os.replace(temp, official)
    if official.read_bytes() != raw:
        fail("official part readback mismatch")

    now = datetime.now(timezone.utc).isoformat()
    row = {
        "part": args.part,
        "status": "closed",
        "beats": EXPECTED_BEATS[args.part],
        "path": str(official.relative_to(ROOT)),
        "source_temp_path": str(args.source.relative_to(ROOT)),
        "source_temp_sha256": hashlib.sha256(raw).hexdigest(),
        "sha256": sha256(official),
        "bytes": len(raw),
        "words": len(text.split()),
        "review_note": args.review_note,
        "closed_at": now,
    }
    state = {
        "status": "in_progress" if args.part < 4 else "all_parts_closed",
        "verified": True,
        "run_id": "run-002",
        "authority_revision": "v2",
        "authority_validation_path": "work/story/run-002/authority-validation.json",
        "authority_validation_sha256": sha256(AUTH_RECEIPT),
        "authority_hashes": receipt["authority_hashes"],
        "parts": prior + [row],
        "total_words": sum(item["words"] for item in prior + [row]),
        "updated_at": now,
    }
    atomic_json(PROGRESS, state)
    print(json.dumps(row, ensure_ascii=False))


if __name__ == "__main__":
    main()
