#!/usr/bin/env python3
import hashlib
import json
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
RUN = ROOT / "work/story/run-002"
MANIFEST = ROOT / "script/project-manifest.json"
AUTHORITY_KEYS = [
    "story_brief",
    "characters",
    "ledger",
    "outline",
    "identity_registry",
    "reveal_ledger",
]
FORBIDDEN_LEDGER_KEYS = {
    "draft_progress",
    "draft_aggregate",
    "frozen_candidate_sha256",
    "frozen_at",
    "full_read_lines",
    "candidate_sha256",
    "candidate_words",
    "candidate_bytes",
}


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


def fail(message):
    raise RuntimeError(message)


def walk_keys(value):
    if isinstance(value, dict):
        for key, item in value.items():
            yield key
            yield from walk_keys(item)
    elif isinstance(value, list):
        for item in value:
            yield from walk_keys(item)


def main():
    manifest = json.loads(MANIFEST.read_text(encoding="utf-8"))
    if manifest.get("active_run") != "run-002" or manifest.get("status") != "run-002_authority_ready_for_validation":
        fail("run-002 authority validation is not enabled")

    options_path = RUN / "creative-options.json"
    options_receipt_path = RUN / "creative-options-validation.json"
    if not options_path.exists() or not options_receipt_path.exists():
        fail("Creative Options or terminal validation receipt missing")
    options = json.loads(options_path.read_text(encoding="utf-8"))
    options_receipt = json.loads(options_receipt_path.read_text(encoding="utf-8"))
    if options_receipt.get("status") != "PASS" or options_receipt.get("verified") is not True:
        fail("Creative Options validation is not terminal PASS")
    if options_receipt.get("options_sha256") != sha256(options_path):
        fail("Creative Options receipt hash drift")
    selected_id = options.get("selected_option_id")
    if options_receipt.get("selected_option_id") != selected_id:
        fail("selected option receipt mismatch")
    selected = next((item for item in options.get("options", []) if item.get("option_id") == selected_id), None)
    if selected is None or selected.get("eligible") is not True:
        fail("selected option is absent or ineligible")

    active = manifest.get("active_paths", {})
    authority_revision = manifest.get("authority_revision")
    if authority_revision != "v2":
        fail("manifest authority revision is not v2")
    authority_paths = {}
    authority_docs = {}
    for key in AUTHORITY_KEYS:
        path = ROOT / active[key]
        if not path.exists():
            fail(f"authority file missing: {key}")
        authority_paths[key] = path
        authority_docs[key] = json.loads(path.read_text(encoding="utf-8"))

    brief = authority_docs["story_brief"]
    characters = authority_docs["characters"]
    ledger = authority_docs["ledger"]
    outline = authority_docs["outline"]
    identity = authority_docs["identity_registry"]
    reveals = authority_docs["reveal_ledger"]

    for key, doc in authority_docs.items():
        if doc.get("run_id") != "run-002" or doc.get("status") != "locked_pre_prose":
            fail(f"authority status/run mismatch: {key}")
        if doc.get("authority_revision") != authority_revision:
            fail(f"authority revision mismatch: {key}")
        if doc.get("selected_option_id") != selected_id:
            fail(f"selected option binding mismatch: {key}")
        if doc.get("creative_options_sha256") != sha256(options_path):
            fail(f"Creative Options hash binding mismatch: {key}")

    if brief.get("story_family_primary") != "Cưới trước yêu sau" or brief.get("story_family_secondary") != "không có":
        fail("Brief family lock drift")
    if brief.get("family_selected_before_premise") is not True:
        fail("Brief family-first flag missing")
    if brief.get("canonical_title") != selected.get("working_title"):
        fail("Brief title differs from selected option")
    if brief.get("central_names") != selected.get("central_names"):
        fail("Brief central names differ from selected option")
    if brief.get("pov") != selected.get("pov"):
        fail("Brief POV differs from selected option")
    target_words = brief.get("target_words", [])
    if len(target_words) != 2 or target_words[0] < 9240 or target_words[1] > 13860 or target_words[0] > target_words[1]:
        fail("Brief word target violates 40-60 minute contract at 231 wpm")
    if brief.get("tts_words_per_minute") != 231:
        fail("Brief TTS rate must be 231 wpm")

    names = [item.get("name") for item in characters.get("characters", [])]
    if len(names) != len(set(names)) or any(not name for name in names):
        fail("character names missing or duplicated")
    if names[:2] != selected.get("central_names"):
        fail("central character order/names drift")
    if characters.get("pov_owner") != selected.get("central_names", [None])[0]:
        fail("POV owner drift")
    if identity.get("pov") != characters.get("pov_owner"):
        fail("identity registry POV drift")

    ledger_keys = set(walk_keys(ledger))
    leaked = sorted(FORBIDDEN_LEDGER_KEYS & ledger_keys)
    if leaked:
        fail(f"mutable production state leaked into authority Ledger: {leaked}")
    if not ledger.get("timeline") or not ledger.get("knowledge") or not ledger.get("relationship"):
        fail("Ledger timeline/knowledge/relationship missing")
    if not ledger.get("continuity_invariants"):
        fail("Ledger continuity invariants missing")

    beats = outline.get("beats", [])
    beat_ids = [item.get("beat") for item in beats]
    if len(beats) < 20 or beat_ids != list(range(1, len(beats) + 1)):
        fail("Outline must contain at least 20 sequential beats")
    if not outline.get("teaser_contract", {}).get("same_event_catchup_required"):
        fail("teaser/catch-up contract missing")
    if len(outline.get("retention_map", {})) < 7:
        fail("retention map incomplete")
    if len(outline.get("climax_contribution", [])) < 2:
        fail("climax contribution map incomplete")

    reveal_rows = reveals.get("reveals", [])
    if not reveal_rows:
        fail("Reveal Ledger empty")
    for row in reveal_rows:
        required = {"id", "seed_beat", "partial_beat", "full_reveal_beat", "provenance", "cannot_support"}
        if not required <= set(row):
            fail(f"Reveal Ledger row incomplete: {row.get('id')}")

    hashes = {key: sha256(path) for key, path in authority_paths.items()}
    if len(set(hashes.values())) != len(hashes):
        fail("authority files unexpectedly byte-identical")
    print(json.dumps({
        "status": "PASS",
        "verified": True,
        "run_id": "run-002",
        "authority_revision": authority_revision,
        "selected_option_id": selected_id,
        "creative_options_sha256": sha256(options_path),
        "authority_hashes": hashes,
        "authority_files": {key: str(path.relative_to(ROOT)) for key, path in authority_paths.items()},
        "prose_write_allowed": True,
    }, ensure_ascii=False))


if __name__ == "__main__":
    main()
