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

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", "-select_streams", "v:0",
        "-show_entries", "stream=codec_name,width,height,pix_fmt",
        "-of", "json", str(path),
    ], text=True))["streams"][0]


def main() -> int:
    project = json.loads(PROJECT_MANIFEST.read_text(encoding="utf-8"))
    keys = (
        "left_panel_master", "right_panel_master", "intro_master",
        "left_panel_normalized", "right_panel_normalized", "intro_normalized",
        "layout", "artwork_visual_receipt", "orientation_receipt",
        "normalized_visual_receipt", "layout_normalization_receipt", "promotion_receipt", "image_prompts",
        "layout_manifest", "layout_receipt",
    )
    paths = {key: resolve_active(project, key) for key in keys}
    required = [path for key, path in paths.items() if key not in {"layout_manifest", "layout_receipt"}]
    missing = [str(path) for path in required if not path.exists()]
    if missing:
        raise RuntimeError(f"layout inputs missing: {missing}")
    existing = [str(paths[key]) for key in ("layout_manifest", "layout_receipt") if paths[key].exists()]
    if existing:
        raise RuntimeError(f"layout verification outputs already exist and require audit/invalidation: {existing}")

    promotion = json.loads(paths["promotion_receipt"].read_text(encoding="utf-8"))
    prompts = json.loads(paths["image_prompts"].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"))
    target_visual = json.loads(paths["normalized_visual_receipt"].read_text(encoding="utf-8"))
    normalization = json.loads(paths["layout_normalization_receipt"].read_text(encoding="utf-8"))
    canon_hash = promotion.get("canon_sha256")
    master_hashes = {
        "left_panel": sha256(paths["left_panel_master"]),
        "right_panel": sha256(paths["right_panel_master"]),
        "intro": sha256(paths["intro_master"]),
    }
    artifact_paths = [
        paths["left_panel_master"], paths["right_panel_master"], paths["left_panel_normalized"],
        paths["right_panel_normalized"], paths["layout"], paths["intro_master"], paths["intro_normalized"],
    ]
    normalized_expected = {
        str(path.relative_to(ROOT)): sha256(path)
        for path in (paths["left_panel_normalized"], paths["right_panel_normalized"], paths["layout"], paths["intro_normalized"])
    }
    normalized_recorded = {
        name: value.get("sha256") for name, value in normalization.get("outputs", {}).items()
    }
    target_visual_hashes = {
        key: value.get("sha256") for key, value in target_visual.get("artifacts", {}).items()
    }
    checks = {
        "promotion_verified": promotion.get("status") == "completed" and promotion.get("verified") is True,
        "prompt_canon_current": prompts.get("source_canon_sha256") == canon_hash,
        "image_api_called": prompts.get("image_api_called") is True,
        "visual_gate": visual.get("status") == "completed" and visual.get("verified") is True and visual.get("source_canon_sha256") == canon_hash and visual.get("artifact_sha256") == master_hashes,
        "orientation_gate": orientation.get("status") == "completed" and orientation.get("verified") is True and orientation.get("source_canon_sha256") == canon_hash and orientation.get("artifact_sha256") == master_hashes,
        "target_visual_gate": target_visual.get("status") == "completed" and target_visual.get("verified") is True and target_visual.get("source_canon_sha256") == canon_hash and target_visual_hashes == {
            "left_panel_normalized": sha256(paths["left_panel_normalized"]),
            "right_panel_normalized": sha256(paths["right_panel_normalized"]),
            "intro_normalized": sha256(paths["intro_normalized"]),
            "layout": sha256(paths["layout"]),
        },
        "normalization_receipt": normalization.get("status") == "completed_pending_target_visual_review" and normalization.get("source_canon_sha256") == canon_hash and normalized_recorded == normalized_expected,
        "left_dimensions": (probe(paths["left_panel_normalized"])["width"], probe(paths["left_panel_normalized"])["height"]) == (656, 1080),
        "right_dimensions": (probe(paths["right_panel_normalized"])["width"], probe(paths["right_panel_normalized"])["height"]) == (656, 1080),
        "layout_dimensions": (probe(paths["layout"])["width"], probe(paths["layout"])["height"]) == (1920, 1080),
        "intro_dimensions": (probe(paths["intro_normalized"])["width"], probe(paths["intro_normalized"])["height"]) == (1920, 1080),
        "png_codecs": all(probe(path).get("codec_name") == "png" for path in artifact_paths),
    }
    verified = all(checks.values())
    result = {
        "status": "completed" if verified else "failed", "verified": verified,
        "source_canon_sha256": canon_hash,
        "input_paths": [str(paths["left_panel_master"]), str(paths["right_panel_master"]), str(paths["intro_master"])],
        "output_path": str(paths["layout"]),
        "artifacts": {
            str(path.relative_to(ROOT)): {"sha256": sha256(path), "bytes": path.stat().st_size, "probe": probe(path)}
            for path in artifact_paths
        },
        "checks": {name: {"verified": value, "value": value} for name, value in checks.items()},
        "visual_receipt": {"path": str(paths["artwork_visual_receipt"]), "sha256": sha256(paths["artwork_visual_receipt"])},
        "orientation_receipt": {"path": str(paths["orientation_receipt"]), "sha256": sha256(paths["orientation_receipt"])},
        "target_visual_receipt": {"path": str(paths["normalized_visual_receipt"]), "sha256": sha256(paths["normalized_visual_receipt"])},
        "normalization_receipt": {"path": str(paths["layout_normalization_receipt"]), "sha256": sha256(paths["layout_normalization_receipt"])},
        "hash_method": "sha256 streaming 8 MiB",
        "blockers": [name for name, value in checks.items() if not value],
    }
    atomic_json(paths["layout_manifest"], result)
    atomic_json(paths["layout_receipt"], result)
    print(json.dumps({"status": result["status"], "verified": verified, "blockers": result["blockers"], "manifest": str(paths["layout_manifest"])}, 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)
