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

ROOT = Path(__file__).resolve().parents[1]
EXPECTED_CANDIDATE = "a73c556b40b85ef21b43121c1c80cf28a7bb241361f217d151a083f2674cb2ac"
EXPECTED_OUTLINE = "eb872d9dfde46bcbc5f9e5a046c6f6fffeda73c52814b05a47557bfb350ee59f"
EXPECTED_AUDIT_LOCK = "298d59b0ee749d8f36f0603c225c473ac12c0d46d9fe5d2ca18dde598fcb12f0"
EXPECTED_REVISION = "v3-post-semantic-fixes"
RUN_ID = "run-002"
AUTHORITY = [
    "story/story-brief.json",
    "story/characters.json",
    "story/ledger.json",
    "script/outline.json",
    "script/identity-registry.json",
    "script/reveal-ledger.json",
]


def sha256(path):
    digest = hashlib.sha256()
    with path.open("rb") as handle:
        for block in iter(lambda: handle.read(8 * 1024 * 1024), b""):
            digest.update(block)
    return digest.hexdigest()


def atomic_bytes(path, data):
    path.parent.mkdir(parents=True, exist_ok=True)
    part = path.with_suffix(path.suffix + ".part")
    part.write_bytes(data)
    os.replace(part, path)


def atomic_json(path, value):
    atomic_bytes(path, (json.dumps(value, ensure_ascii=False, indent=2) + "\n").encode("utf-8"))


def main():
    manifest_path = ROOT / "script/project-manifest.json"
    manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
    paths = manifest["active_paths"]
    candidate = ROOT / f"work/story/{RUN_ID}/candidate.txt"
    validation_path = ROOT / f"log/{RUN_ID}/candidate-validation.json"
    originality_path = ROOT / f"log/{RUN_ID}/originality.json"
    audit_lock_path = ROOT / f"log/{RUN_ID}/audit-set-lock.json"
    semantic_path = ROOT / f"log/{RUN_ID}/semantic-review.json"
    canon = ROOT / paths["story_canon"]
    source = ROOT / paths["spoken_narration_source"]
    promotion_path = ROOT / paths["promotion_receipt"]
    story_manifest_path = ROOT / "script/story-manifest.json"

    for output in (canon, source, promotion_path, story_manifest_path):
        if output.exists():
            raise RuntimeError(f"promotion output must be virgin: {output}")

    candidate_bytes = candidate.read_bytes()
    candidate_sha = hashlib.sha256(candidate_bytes).hexdigest()
    if candidate_sha != EXPECTED_CANDIDATE:
        raise RuntimeError("candidate hash drift")
    if sha256(ROOT / "script/outline.json") != EXPECTED_OUTLINE:
        raise RuntimeError("outline hash drift")
    if manifest.get("candidate", {}).get("sha256") != candidate_sha:
        raise RuntimeError("manifest candidate hash drift")

    validation = json.loads(validation_path.read_text(encoding="utf-8"))
    originality = json.loads(originality_path.read_text(encoding="utf-8"))
    audit_lock = json.loads(audit_lock_path.read_text(encoding="utf-8"))
    semantic = json.loads(semantic_path.read_text(encoding="utf-8"))
    if sha256(audit_lock_path) != EXPECTED_AUDIT_LOCK:
        raise RuntimeError("audit-set lock file drift")
    for name, receipt in (("validation", validation), ("originality", originality), ("semantic", semantic)):
        if receipt.get("verified") is not True or receipt.get("candidate_sha256") != candidate_sha:
            raise RuntimeError(f"{name} receipt not terminal on current candidate")
    if audit_lock.get("verified") is not True or audit_lock.get("candidate_sha256") != candidate_sha:
        raise RuntimeError("audit-set lock not terminal on current candidate")
    if audit_lock.get("outline_sha256") != EXPECTED_OUTLINE:
        raise RuntimeError("audit-set lock outline hash drift")
    if semantic.get("outline_sha256") != EXPECTED_OUTLINE:
        raise RuntimeError("semantic audit outline hash drift")
    if semantic.get("run_id") != RUN_ID or semantic.get("revision") != EXPECTED_REVISION:
        raise RuntimeError("semantic audit run/revision drift")
    if semantic.get("stale") is not False:
        raise RuntimeError("semantic audit currentness is not terminal")
    if semantic.get("audit_set_sha256") != EXPECTED_AUDIT_LOCK:
        raise RuntimeError("semantic audit did not bind the exact audit-set lock")
    if semantic.get("verdict") != "PASS" or semantic.get("blocker_count", 1) or semantic.get("high_count", 1):
        raise RuntimeError("semantic audit did not return terminal PASS")

    authority = {}
    for rel in AUTHORITY:
        path = ROOT / rel
        json.loads(path.read_text(encoding="utf-8"))
        authority[rel] = sha256(path)
    if authority["script/outline.json"] != EXPECTED_OUTLINE:
        raise RuntimeError("authority snapshot drift")
    locked_files = {row["path"]: row["sha256"] for row in audit_lock.get("files", [])}
    expected_files = set(AUTHORITY) | {
        f"work/story/{RUN_ID}/candidate.txt",
        f"log/{RUN_ID}/candidate-validation.json",
        f"log/{RUN_ID}/originality.json",
    }
    if set(locked_files) != expected_files:
        raise RuntimeError("audit-set file inventory drift")
    current_files = {rel: sha256(ROOT / rel) for rel in expected_files}
    if current_files != locked_files:
        raise RuntimeError("audit-set current bytes drift")
    if semantic.get("observed_hashes") != locked_files:
        raise RuntimeError("semantic audit observed hashes do not match current audit set")
    for rel, digest in authority.items():
        if locked_files.get(rel) != digest:
            raise RuntimeError(f"audit-set authority drift: {rel}")
    if locked_files.get(f"work/story/{RUN_ID}/candidate.txt") != candidate_sha:
        raise RuntimeError("audit-set candidate drift")

    text = candidate_bytes.decode("utf-8")
    word_count = len(text.split())
    if validation.get("word_count_algorithm") != "len(decoded_utf8.split())" or validation.get("word_count") != word_count:
        raise RuntimeError("candidate word-count receipt is not reproducible")
    if not 9800 <= word_count <= 11800:
        raise RuntimeError("candidate outside story word target")

    atomic_bytes(canon, candidate_bytes)
    atomic_bytes(source, candidate_bytes)
    if sha256(canon) != candidate_sha or sha256(source) != candidate_sha:
        raise RuntimeError("promotion byte equality failed")

    now = datetime.datetime.now(datetime.timezone.utc).isoformat()
    promotion = {
        "status": "completed",
        "verified": True,
        "project_id": manifest["project_id"],
        "run_id": manifest["active_run"],
        "source_candidate_path": str(candidate.relative_to(ROOT)),
        "source_candidate_sha256": candidate_sha,
        "source_canon_path": paths["story_canon"],
        "source_canon_sha256": candidate_sha,
        "spoken_narration_source_path": paths["spoken_narration_source"],
        "spoken_narration_source_sha256": candidate_sha,
        "spoken_narration_path": paths["spoken_narration"],
        "spoken_narration_sha256": None,
        "pronunciation_status": "pending",
        "word_count": word_count,
        "predicted_duration_minutes": round(word_count / 231, 4),
        "gates": {
            "candidate_validation": True,
            "semantic_review": True,
            "originality": True,
            "deterministic_story_gates": validation.get("verified") is True,
            "audit_set_lock": True,
            "authority_drift": False,
            "canon_candidate_byte_equality": True,
            "narration_source_candidate_byte_equality": True,
        },
        "receipt_sha256": {
            "candidate_validation": sha256(validation_path),
            "semantic_review": sha256(semantic_path),
            "originality": sha256(originality_path),
            "deterministic_story_gates": sha256(validation_path),
            "audit_set_lock": sha256(audit_lock_path),
        },
        "authority_snapshot": authority,
        "completed_at": now,
    }
    story_manifest = {
        "status": "promoted",
        "verified": True,
        "project_id": manifest["project_id"],
        "canonical_title": manifest["story_title"],
        "run_id": manifest["active_run"],
        "authority_snapshot": authority,
        "candidate_path": str(candidate.relative_to(ROOT)),
        "candidate_sha256": candidate_sha,
        "story_canon_path": paths["story_canon"],
        "story_canon_sha256": candidate_sha,
        "spoken_narration_source_path": paths["spoken_narration_source"],
        "spoken_narration_source_sha256": candidate_sha,
        "spoken_narration_path": paths["spoken_narration"],
        "spoken_narration_sha256": None,
        "word_count": word_count,
        "predicted_duration_minutes_at_231_wpm": round(word_count / 231, 4),
        "candidate_validation": str(validation_path.relative_to(ROOT)),
        "semantic_review": str(semantic_path.relative_to(ROOT)),
        "originality": str(originality_path.relative_to(ROOT)),
        "promoted_at": now,
    }
    atomic_json(promotion_path, promotion)
    atomic_json(story_manifest_path, story_manifest)

    manifest["status"] = "story_promoted_pronunciation_pending"
    manifest["canon_sha256"] = candidate_sha
    manifest["spoken_narration_source_sha256"] = candidate_sha
    manifest["spoken_narration_sha256"] = None
    manifest["steps"]["story"] = "completed"
    manifest["steps"]["pronunciation"] = "pending"
    manifest["blocking_tasks"]["story_promotion_audit"] = {
        "status": "completed",
        "blocking": True,
        "verified": True,
        "receipt": str(semantic_path.relative_to(ROOT)),
        "candidate_sha256": candidate_sha,
        "outline_sha256": EXPECTED_OUTLINE,
    }
    manifest["story"] = {
        "status": "completed",
        "verified": True,
        "promotion_receipt": str(promotion_path.relative_to(ROOT)),
        "canon_sha256": candidate_sha,
        "word_count": word_count,
        "predicted_duration_minutes": round(word_count / 231, 4),
        "completed_at": now,
    }
    manifest["updated_at"] = now
    atomic_json(manifest_path, manifest)
    print(json.dumps({"status": "promoted", "canon_sha256": candidate_sha, "word_count": word_count}, ensure_ascii=False))


if __name__ == "__main__":
    main()
