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

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

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


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 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",
        "-of", "json", str(path),
    ], text=True))


def resolve(manifest: dict, key: str) -> Path:
    value = manifest["active_paths"][key]
    path = Path(value)
    if not path.is_absolute():
        path = ROOT / path
    resolved = path.resolve()
    resolved.relative_to(ROOT.resolve())
    return resolved


def atomic_json(path: Path, value: dict) -> None:
    data = (json.dumps(value, ensure_ascii=False, indent=2) + "\n").encode("utf-8")
    path.parent.mkdir(parents=True, exist_ok=True)
    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 main() -> int:
    manifest = json.loads(MANIFEST.read_text(encoding="utf-8"))
    promotion = json.loads(resolve(manifest, "promotion_receipt").read_text(encoding="utf-8"))
    layout = json.loads(resolve(manifest, "layout_receipt").read_text(encoding="utf-8"))
    poster = resolve(manifest, "intro_normalized")
    voice = resolve(manifest, "intro_voice")
    silence = resolve(manifest, "intro_silence")
    audio = resolve(manifest, "intro_audio")
    output = resolve(manifest, "intro")
    receipt_path = resolve(manifest, "intro_receipt")
    if output.exists():
        raise RuntimeError("recovery output already exists")
    failed_receipt = resolve(manifest, FAILED_RECEIPT_KEY)
    failed = json.loads(failed_receipt.read_text(encoding="utf-8"))
    if failed.get("status") != "failed" or "h264_nvenc" not in str(failed.get("error", "")):
        raise RuntimeError("failed receipt does not prove local NVENC encode failure")
    if promotion.get("verified") is not True or layout.get("verified") is not True:
        raise RuntimeError("promotion/layout gate not verified")
    voice_duration = float(probe(voice)["format"]["duration"])
    silence_duration = float(probe(silence)["format"]["duration"])
    audio_duration = float(probe(audio)["format"]["duration"])
    if abs(silence_duration - 2.0) > 0.001 or abs(audio_duration - voice_duration - 2.0) > 0.02:
        raise RuntimeError("locked intro audio duration relation failed")
    output.parent.mkdir(parents=True, exist_ok=True)
    subprocess.run([
        "ffmpeg", "-v", "error", "-y", "-loop", "1", "-i", str(poster),
        "-i", str(audio), "-t", f"{audio_duration:.6f}", "-r", "30",
        "-c:v", "libx264", "-preset", "medium", "-crf", "20", "-pix_fmt", "yuv420p",
        "-c:a", "aac", "-ar", "48000", "-ac", "2", "-movflags", "+faststart", str(output),
    ], check=True)
    info = probe(output)
    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"]
    duration = float(info["format"]["duration"])
    checks = {
        "one_video": len(videos) == 1, "one_audio": len(audios) == 1,
        "h264": len(videos) == 1 and videos[0].get("codec_name") == "h264",
        "resolution": len(videos) == 1 and (int(videos[0].get("width", 0)), int(videos[0].get("height", 0))) == (1920, 1080),
        "aac": len(audios) == 1 and audios[0].get("codec_name") == "aac",
        "duration_one_frame": abs(duration - audio_duration) <= 1 / 30 + 0.005,
        "silence_exact": abs(silence_duration - 2.0) <= 0.001,
        "audio_relation": abs(audio_duration - voice_duration - 2.0) <= 0.02,
    }
    verified = all(checks.values())
    receipt = {
        "status": "completed" if verified else "failed", "verified": verified,
        "source_canon_sha256": promotion["canon_sha256"],
        "recovery_mode": "encode-only local libx264 after NVENC unavailable; no TTS retry",
        "failed_receipt": {"path": str(failed_receipt), "sha256": sha256(failed_receipt)},
        "layout_receipt": {"path": str(resolve(manifest, "layout_receipt")), "sha256": sha256(resolve(manifest, "layout_receipt"))},
        "input_path": str(poster), "input_sha256": sha256(poster),
        "voice_path": str(voice), "voice_sha256": sha256(voice), "voice_duration_seconds": voice_duration,
        "silence_path": str(silence), "silence_sha256": sha256(silence), "silence_duration_seconds": silence_duration,
        "intro_full_path": str(audio), "intro_full_sha256": sha256(audio), "audio_duration_seconds": audio_duration,
        "output_path": str(output), "artifact_sha256": sha256(output), "video_duration_seconds": duration,
        "independent_probe": info, "checks": checks,
        "completed_at": datetime.datetime.now(datetime.timezone.utc).isoformat(), "hash_method": "sha256 streaming 8 MiB",
    }
    atomic_json(receipt_path, receipt)
    print(json.dumps({"status": receipt["status"], "verified": verified, "output": str(output), "receipt": str(receipt_path)}, ensure_ascii=False))
    return 0 if verified else 1


if __name__ == "__main__":
    raise SystemExit(main())
