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

import datetime
import hashlib
import json
import os
import subprocess
import sys
import tempfile
import time
import urllib.error
import urllib.request
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
MANIFEST = ROOT / "script/project-manifest.json"
BASE = "http://192.168.1.104:8022"


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(manifest: dict, key: str) -> Path:
    value = (manifest.get("active_paths") or {}).get(key)
    if not isinstance(value, str) or not value:
        raise RuntimeError(f"missing active_paths.{key}")
    path = Path(value)
    if not path.is_absolute():
        path = ROOT / path
    path.resolve().relative_to(ROOT.resolve())
    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(dir=path.parent, prefix=f".{path.name}.", suffix=".tmp")
    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 get_json(url: str) -> dict:
    with urllib.request.urlopen(url, timeout=30) as response:
        return json.loads(response.read().decode("utf-8"))


def post_json(url: str, payload: dict) -> dict:
    request = urllib.request.Request(
        url,
        data=json.dumps(payload, ensure_ascii=False).encode("utf-8"),
        headers={"Content-Type": "application/json"},
        method="POST",
    )
    with urllib.request.urlopen(request, timeout=60) as response:
        return json.loads(response.read().decode("utf-8"))


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


def main() -> int:
    project = json.loads(MANIFEST.read_text(encoding="utf-8"))
    paths = {key: resolve(project, key) for key in (
        "layout", "layout_receipt", "tts_audio", "tts_receipt", "footage", "footage_receipt"
    )}
    layout_receipt = json.loads(paths["layout_receipt"].read_text(encoding="utf-8"))
    tts_receipt = json.loads(paths["tts_receipt"].read_text(encoding="utf-8"))
    canon = project.get("canon_sha256")
    if layout_receipt.get("status") != "completed" or layout_receipt.get("verified") is not True or layout_receipt.get("source_canon_sha256") != canon:
        raise RuntimeError("layout receipt absent, failed, or stale")
    if tts_receipt.get("status") != "completed" or tts_receipt.get("verified") is not True or tts_receipt.get("source_canon_sha256") != canon:
        raise RuntimeError("TTS receipt absent, failed, or stale")
    if tts_receipt.get("artifact_sha256") != sha256(paths["tts_audio"]):
        raise RuntimeError("TTS audio drift")
    layout_artifact = layout_receipt.get("artifacts", {}).get(str(paths["layout"].relative_to(ROOT)), {})
    if layout_artifact.get("sha256") != sha256(paths["layout"]):
        raise RuntimeError("layout artifact drift")

    payload = {
        "layout": str(paths["layout"]),
        "footage_dir": "/data/video-pipeline/GacMaiAudio/footage",
        "pattern": "*.mp4",
        "recursive": True,
        "audio": str(paths["tts_audio"]),
        "output": str(paths["footage"]),
        "seed": 0,
        "overwrite": False,
        "width": 1920,
        "height": 1080,
        "fps": 30,
        "center_x": 656,
        "center_y": 0,
        "center_width": 608,
        "center_height": 1080,
        "video_encoder": "h264_nvenc",
        "preset": "p4",
        "cq": 21,
    }
    receipt_path = paths["footage_receipt"]
    job_id = None
    if receipt_path.exists():
        prior = json.loads(receipt_path.read_text(encoding="utf-8"))
        if prior.get("status") == "completed" and prior.get("verified") is True:
            raise RuntimeError("footage already completed and requires audit, not resubmission")
        if prior.get("status") in {"submitted", "queued", "running"}:
            job_id = prior.get("job_id")
            if not job_id:
                raise RuntimeError("running footage receipt lacks job_id")
            if prior.get("request") != payload:
                raise RuntimeError("existing job payload differs from current authority")
    elif paths["footage"].exists():
        raise RuntimeError("footage output exists without job receipt")

    if job_id is None:
        response = post_json(f"{BASE}/v1/footage-render-huyenan/jobs", payload)
        job_id = response.get("job_id") or response.get("id")
        if not isinstance(job_id, str) or not job_id:
            raise RuntimeError(f"submit response lacks job_id: {response}")
        atomic_json(receipt_path, {
            "status": response.get("status", "submitted"),
            "verified": False,
            "source_canon_sha256": canon,
            "job_id": job_id,
            "request": payload,
            "submit_response": response,
            "layout_sha256": sha256(paths["layout"]),
            "narration_sha256": sha256(paths["tts_audio"]),
            "submitted_at": datetime.datetime.now(datetime.timezone.utc).isoformat(),
        })

    while True:
        job = get_json(f"{BASE}/v1/footage-render-huyenan/jobs/{job_id}")
        status = job.get("status")
        if status in {"queued", "running", "submitted"}:
            current = json.loads(receipt_path.read_text(encoding="utf-8"))
            current.update({
                "status": status,
                "verified": False,
                "last_job_response": job,
                "updated_at": datetime.datetime.now(datetime.timezone.utc).isoformat(),
            })
            atomic_json(receipt_path, current)
            time.sleep(5)
            continue
        if status != "completed":
            failure = json.loads(receipt_path.read_text(encoding="utf-8"))
            failure.update({
                "status": status or "failed",
                "verified": False,
                "terminal_job_response": job,
                "failed_at": datetime.datetime.now(datetime.timezone.utc).isoformat(),
            })
            atomic_json(receipt_path, failure)
            raise RuntimeError(f"footage job terminal failure: {status}")
        break

    verification = job.get("verification") or {}
    output = Path(job.get("output", ""))
    output_probe = probe(paths["footage"]) if paths["footage"].is_file() else {}
    streams = output_probe.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"]
    output_duration = float((output_probe.get("format") or {}).get("duration", 0))
    narration_duration = float(tts_receipt.get("duration_seconds", 0))
    checks = {
        "endpoint_verified": verification.get("verified") is True,
        "output_exact": output == paths["footage"],
        "output_exists": paths["footage"].is_file() and paths["footage"].stat().st_size > 0,
        "resolution": verification.get("width") == 1920 and verification.get("height") == 1080,
        "mirror_applied": verification.get("mirror_applied") is True,
        "source_audio_discarded": verification.get("source_audio_discarded") is True,
        "audio_status": verification.get("audio_status") in {"narration_mixed", "narration_attached", "present", "ok", "verified"},
        "no_placeholder": verification.get("placeholder_remaining") in {False, None},
        "one_h264_video": len(videos) == 1 and videos[0].get("codec_name") == "h264",
        "one_aac_audio": len(audios) == 1 and audios[0].get("codec_name") == "aac",
        "probe_resolution": len(videos) == 1 and videos[0].get("width") == 1920 and videos[0].get("height") == 1080,
        "duration_matches_narration": narration_duration > 0 and abs(output_duration - narration_duration) <= 0.06,
    }
    verified = all(checks.values())
    result = {
        "status": "completed" if verified else "failed",
        "verified": verified,
        "source_canon_sha256": canon,
        "job_id": job_id,
        "request": payload,
        "terminal_job_response": job,
        "verification_checks": {key: {"verified": value, "value": value} for key, value in checks.items()},
        "layout_sha256": sha256(paths["layout"]),
        "narration_sha256": sha256(paths["tts_audio"]),
        "output_path": str(paths["footage"]),
        "artifact_sha256": sha256(paths["footage"]) if paths["footage"].is_file() else None,
        "bytes": paths["footage"].stat().st_size if paths["footage"].is_file() else None,
        "duration_seconds": output_duration,
        "independent_probe": output_probe,
        "completed_at": datetime.datetime.now(datetime.timezone.utc).isoformat(),
    }
    atomic_json(receipt_path, result)
    print(json.dumps({
        "status": result["status"],
        "verified": verified,
        "job_id": job_id,
        "artifact_sha256": result["artifact_sha256"],
        "bytes": result["bytes"],
        "receipt": str(receipt_path),
    }, 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)
