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

ROOT = Path(__file__).resolve().parents[1]
MANIFEST = ROOT / "script/project-manifest.json"
FREEZE_KEYS = [
    "candidate",
    "outline",
    "story_brief",
    "characters",
    "ledger",
    "identity_registry",
    "reveal_ledger",
    "candidate_validation",
    "originality_receipt",
]


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


def audit_set_digest(locked_files):
    payload = json.dumps(
        {row["path"]: row["sha256"] for row in locked_files},
        ensure_ascii=False,
        sort_keys=True,
        separators=(",", ":"),
    ).encode("utf-8")
    return hashlib.sha256(payload).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():
    manifest = json.loads(MANIFEST.read_text(encoding="utf-8"))
    if manifest.get("active_run") != "run-002" or manifest.get("status") != "run-002_producer_closed_ready_to_freeze":
        fail("run-002 producer close is not enabled for freeze")
    active = manifest["active_paths"]
    producer_path = ROOT / active["producer_close"]
    lock_path = ROOT / active["audit_set_lock"]
    if lock_path.exists():
        fail("audit-set lock must be virgin")
    if not producer_path.exists():
        fail("producer-close receipt missing")
    producer = json.loads(producer_path.read_text(encoding="utf-8"))
    if producer.get("status") != "PASS" or producer.get("verified") is not True:
        fail("producer-close is not terminal PASS")
    if producer.get("manual_full_read_verified") is not True:
        fail("manual full read not verified")
    candidate_path = ROOT / active["candidate"]
    if producer.get("candidate_sha256") != sha256(candidate_path):
        fail("producer-close candidate hash drift")
    validation = json.loads((ROOT / active["candidate_validation"]).read_text(encoding="utf-8"))
    originality = json.loads((ROOT / active["originality_receipt"]).read_text(encoding="utf-8"))
    if validation.get("status") != "PASS" or validation.get("verified") is not True:
        fail("candidate validation not terminal PASS")
    if originality.get("status") != "PASS" or originality.get("verified") is not True:
        fail("originality not terminal PASS")
    if validation.get("candidate_sha256") != sha256(candidate_path) or originality.get("candidate_sha256") != sha256(candidate_path):
        fail("candidate lineage drift in validation/originality")
    if originality.get("corpus_project_count") != 17 or originality.get("semantic_architecture_verdict") != "PASS":
        fail("originality corpus/semantic gate failed")

    locked_files = []
    for key in FREEZE_KEYS:
        path = ROOT / active[key]
        if not path.exists():
            fail(f"freeze file missing: {key}")
        locked_files.append({
            "key": key,
            "path": str(path.relative_to(ROOT)),
            "bytes": path.stat().st_size,
            "mtime_ns": path.stat().st_mtime_ns,
            "sha256": sha256(path),
        })
    expected_authority = producer.get("authority_hashes", {})
    for key in ["outline", "story_brief", "characters", "ledger", "identity_registry", "reveal_ledger"]:
        observed = next(row["sha256"] for row in locked_files if row["key"] == key)
        if expected_authority.get(key) != observed:
            fail(f"producer-close authority hash drift: {key}")

    now = datetime.now(timezone.utc).isoformat()
    lock = {
        "status": "FROZEN",
        "verified": True,
        "run_id": "run-002",
        "revision": "v4",
        "candidate_sha256": sha256(candidate_path),
        "locked_file_count": len(locked_files),
        "locked_files": locked_files,
        "audit_set_sha256": audit_set_digest(locked_files),
        "producer_close_path": active["producer_close"],
        "producer_close_sha256": sha256(producer_path),
        "freeze_contract": "exactly nine listed files are read-only until terminal verdict; descriptor hash is distinct from audit_set_sha256",
        "manifest_scope": {
            "included_in_nine_file_set": False,
            "routing_fields_must_remain": {
                "active_run": "run-002",
                "candidate": active["candidate"],
                "revision": "v4",
            },
        },
        "frozen_at": now,
    }
    atomic_json(lock_path, lock)
    readback = json.loads(lock_path.read_text(encoding="utf-8"))
    current = [
        {
            "key": row["key"],
            "path": row["path"],
            "bytes": (ROOT / row["path"]).stat().st_size,
            "mtime_ns": (ROOT / row["path"]).stat().st_mtime_ns,
            "sha256": sha256(ROOT / row["path"]),
        }
        for row in readback["locked_files"]
    ]
    if current != readback["locked_files"] or audit_set_digest(current) != readback["audit_set_sha256"]:
        fail("post-write freeze readback mismatch")
    print(json.dumps({
        "status": "FROZEN",
        "candidate_sha256": lock["candidate_sha256"],
        "audit_set_sha256": lock["audit_set_sha256"],
        "audit_lock_receipt_sha256": sha256(lock_path),
        "locked_file_count": len(locked_files),
    }, ensure_ascii=False))


if __name__ == "__main__":
    main()
