#!/usr/bin/env python3
from __future__ import annotations

import datetime
import hashlib
import json
import os
import subprocess
import sys
import tempfile
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
PROJECT_MANIFEST = ROOT / "script/project-manifest.json"


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


def resolve_active(manifest: dict, key: str) -> Path:
    value = (manifest.get("active_paths") or {}).get(key)
    if not isinstance(value, str) or not value:
        raise RuntimeError(f"active_paths.{key} is missing")
    path = Path(value)
    if not path.is_absolute():
        path = ROOT / path
    try:
        path.resolve().relative_to(ROOT.resolve())
    except ValueError as exc:
        raise RuntimeError(f"active path escapes project: {key}") from exc
    return path


def atomic_json(path: Path, value: dict) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    data = (json.dumps(value, ensure_ascii=False, indent=2) + "\n").encode("utf-8")
    fd, name = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
    try:
        with os.fdopen(fd, "wb") as handle:
            handle.write(data)
            handle.flush()
            os.fsync(handle.fileno())
        os.replace(name, path)
    finally:
        if os.path.exists(name):
            os.unlink(name)


def probe(path: Path) -> dict:
    return json.loads(subprocess.check_output([
        "ffprobe", "-v", "error", "-show_entries",
        "format=duration:stream=codec_name,codec_type,width,height,sample_rate,channels,r_frame_rate",
        "-of", "json", str(path),
    ], text=True))


def main() -> int:
    project = json.loads(PROJECT_MANIFEST.read_text(encoding="utf-8"))
    if project.get("active_version") != "v2":
        raise RuntimeError("active_version is not v2")
    keys = (
        "promotion_receipt", "artwork_visual_receipt", "orientation_receipt",
        "intro_normalized", "intro_voice", "intro_silence", "intro_audio",
        "intro", "intro_receipt",
    )
    paths = {key: resolve_active(project, key) for key in keys}
    promotion = json.loads(paths["promotion_receipt"].read_text(encoding="utf-8"))
    visual = json.loads(paths["artwork_visual_receipt"].read_text(encoding="utf-8"))
    orientation = json.loads(paths["orientation_receipt"].read_text(encoding="utf-8"))
    canon = promotion.get("canon_sha256")
    if promotion.get("verified") is not True or canon != project.get("canon_sha256"):
        raise RuntimeError("promotion gate is absent or stale")
    if visual.get("verified") is not True or visual.get("source_canon_sha256") != canon or visual.get("user_approved") is not True:
        raise RuntimeError("poster Visual Direction/Text Gate is absent or stale")
    if orientation.get("verified") is not True or orientation.get("source_canon_sha256") != canon or orientation.get("user_approved") is not True:
        raise RuntimeError("poster Orientation Gate is absent or stale")
    poster_hash = sha256(paths["intro_normalized"])
    if visual.get("normalized_sha256") != poster_hash or orientation.get("normalized_sha256") != poster_hash:
        raise RuntimeError("normalized poster differs from approved receipts")

    superseded = (project.get("poster_v2_promotion") or {}).get("superseded_paths") or {}
    old_receipt_value = superseded.get("intro_receipt")
    if not isinstance(old_receipt_value, str) or not old_receipt_value:
        raise RuntimeError("superseded intro receipt is missing")
    old_receipt_path = ROOT / old_receipt_value
    old = json.loads(old_receipt_path.read_text(encoding="utf-8"))
    if old.get("status") != "completed" or old.get("verified") is not True or old.get("source_canon_sha256") != canon:
        raise RuntimeError("prior intro audio authority receipt is not verified")
    expected = {
        "intro_voice": old.get("voice_sha256"),
        "intro_silence": old.get("silence_sha256"),
        "intro_audio": old.get("intro_full_sha256"),
    }
    for key, digest in expected.items():
        path = paths[key]
        if not path.exists() or sha256(path) != digest:
            raise RuntimeError(f"reused intro audio drift: {key}")
    if paths["intro"].exists() or paths["intro_receipt"].exists():
        raise RuntimeError("v2 intro output/receipt already exists and requires audit")

    voice_probe = probe(paths["intro_voice"])
    silence_probe = probe(paths["intro_silence"])
    audio_probe = probe(paths["intro_audio"])
    voice_duration = float(voice_probe["format"]["duration"])
    silence_duration = float(silence_probe["format"]["duration"])
    audio_duration = float(audio_probe["format"]["duration"])
    if abs(silence_duration - 2.0) > 0.001 or abs(audio_duration - voice_duration - 2.0) > 0.02:
        raise RuntimeError("reused intro audio duration verification failed")

    paths["intro"].parent.mkdir(parents=True, exist_ok=True)
    subprocess.run([
        "ffmpeg", "-v", "error", "-y", "-loop", "1", "-i", str(paths["intro_normalized"]),
        "-i", str(paths["intro_audio"]), "-t", f"{audio_duration:.6f}", "-r", "30",
        "-c:v", "libx264", "-preset", "medium", "-crf", "18", "-pix_fmt", "yuv420p",
        "-c:a", "aac", "-ar", "48000", "-ac", "2", "-movflags", "+faststart",
        str(paths["intro"]),
    ], check=True)
    info = probe(paths["intro"])
    duration = float(info["format"]["duration"])
    streams = info.get("streams", [])
    videos = [item for item in streams if item.get("codec_type") == "video"]
    audios = [item for item in streams if item.get("codec_type") == "audio"]
    verified = (
        abs(duration - audio_duration) <= 1 / 30 + 0.005
        and len(videos) == 1 and len(audios) == 1
        and videos[0].get("codec_name") == "h264"
        and (int(videos[0].get("width", 0)), int(videos[0].get("height", 0))) == (1920, 1080)
        and audios[0].get("codec_name") == "aac"
    )
    receipt = {
        "status": "completed" if verified else "failed", "verified": verified,
        "source_canon_sha256": canon, "active_version": "v2",
        "rebuild_reason": "user-approved cinematic poster replaces rejected v1 poster",
        "poster_visual_receipt": {"path": str(paths["artwork_visual_receipt"]), "sha256": sha256(paths["artwork_visual_receipt"])},
        "poster_orientation_receipt": {"path": str(paths["orientation_receipt"]), "sha256": sha256(paths["orientation_receipt"])},
        "poster_path": str(paths["intro_normalized"]), "poster_sha256": poster_hash,
        "audio_reused_without_new_tts_request": True,
        "prior_intro_receipt": {"path": str(old_receipt_path), "sha256": sha256(old_receipt_path)},
        "voice_path": str(paths["intro_voice"]), "voice_sha256": expected["intro_voice"],
        "silence_path": str(paths["intro_silence"]), "silence_sha256": expected["intro_silence"],
        "silence_duration_seconds": silence_duration,
        "intro_audio_path": str(paths["intro_audio"]), "intro_audio_sha256": expected["intro_audio"],
        "audio_duration_seconds": audio_duration, "encoder": "libx264",
        "output_path": str(paths["intro"]), "artifact_sha256": sha256(paths["intro"]),
        "video_duration_seconds": duration, "duration_delta_seconds": abs(duration - audio_duration),
        "independent_probe": info,
        "completed_at": datetime.datetime.now(datetime.timezone.utc).isoformat(),
        "hash_method": "sha256 streaming 8 MiB",
    }
    atomic_json(paths["intro_receipt"], receipt)
    print(json.dumps({"status": receipt["status"], "verified": verified, "output": str(paths["intro"]), "duration_seconds": duration, "receipt": str(paths["intro_receipt"])}, ensure_ascii=False))
    return 0 if verified else 1


if __name__ == "__main__":
    try:
        sys.exit(main())
    except Exception as exc:
        print(json.dumps({"status": "failed", "verified": False, "error": f"{type(exc).__name__}: {exc}"}, ensure_ascii=False), file=sys.stderr)
        sys.exit(1)
