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

ROOT = Path(__file__).resolve().parents[1]
MANIFEST = ROOT / "script/project-manifest.json"
MASTER_INPUT = ROOT / "log/artwork-master-vision-input.json"
NORMALIZED_INPUT = ROOT / "log/artwork-normalized-vision-input.json"
EXPECTED_SCOPE = "provider-rendered still images only; no video frame used"


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


def dimensions(path):
    result = subprocess.run(["ffprobe", "-v", "error", "-show_entries", "stream=codec_name,width,height", "-of", "json", str(path)], capture_output=True, text=True, check=True)
    streams = json.loads(result.stdout).get("streams", [])
    if len(streams) != 1 or streams[0].get("codec_name") != "png":
        raise RuntimeError(f"not a single PNG stream: {path}")
    return int(streams[0]["width"]), int(streams[0]["height"])


def atomic_json(path, value):
    path.parent.mkdir(parents=True, exist_ok=True)
    part = path.with_suffix(path.suffix + ".part")
    part.write_text(json.dumps(value, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    os.replace(part, path)


def validate_vision(row, path, expected_contract, expected_orientation):
    width, height = dimensions(path)
    orientation = "landscape" if width > height else "portrait" if height > width else "square"
    checks = {
        "verified": row.get("verified") is True,
        "path_exact": row.get("path") == str(path.relative_to(ROOT)),
        "sha256_exact": row.get("sha256") == sha256(path),
        "full_resolution_provider_still": row.get("evidence") == "full-resolution current PNG still",
        "no_video_frame": row.get("video_frame_used") is False,
        "text_contract_exact": row.get("transcribed_text_blocks") == expected_contract,
        "visual_brief_pass": row.get("visual_brief_pass") is True,
        "identity_pass": row.get("identity_pass") is True,
        "anatomy_pass": row.get("anatomy_pass") is True,
        "reveal_limit_pass": row.get("reveal_limit_pass") is True,
        "orientation_pass": orientation == expected_orientation and row.get("observed_orientation") == orientation,
        "dimensions_exact": row.get("width") == width and row.get("height") == height,
    }
    if not all(checks.values()):
        raise RuntimeError(f"vision gate failed for {path}: {checks}")
    return {
        "verified": True, "path": str(path.relative_to(ROOT)), "master_sha256": sha256(path),
        "sha256": sha256(path), "width": width, "height": height,
        "expected_orientation": expected_orientation, "observed_orientation": orientation,
        "transcribed_text_blocks": row["transcribed_text_blocks"], "checks": checks,
        "vision_notes": row.get("vision_notes", ""),
    }


def main():
    manifest = json.loads(MANIFEST.read_text(encoding="utf-8"))
    active = manifest["active_paths"]
    bundle = json.loads((ROOT / active["image_prompts"]).read_text(encoding="utf-8"))
    generation_path = ROOT / active["artwork_generation_receipt"]
    generation = json.loads(generation_path.read_text(encoding="utf-8"))
    output = ROOT / active["artwork_visual_receipt"]
    if generation.get("verified") is not True or generation.get("source_canon_sha256") != manifest.get("canon_sha256"):
        raise RuntimeError("artwork generation missing, failed or stale")
    contracts = {
        "intro": bundle["text_contracts"]["intro_and_right"],
        "left_panel": bundle["text_contracts"]["left"],
        "right_panel": bundle["text_contracts"]["intro_and_right"],
    }
    masters = {
        "intro": ROOT / active["intro_poster_master"],
        "left_panel": ROOT / active["left_panel_master"],
        "right_panel": ROOT / active["right_panel_master"],
    }
    if not output.exists():
        if not MASTER_INPUT.exists():
            raise RuntimeError("master Vision Gate input missing")
        supplied = json.loads(MASTER_INPUT.read_text(encoding="utf-8"))
        if supplied.get("evidence_scope") != EXPECTED_SCOPE:
            raise RuntimeError("master Vision evidence scope drift")
        gates = {}
        for key, path in masters.items():
            expected_orientation = "landscape" if key == "intro" else "portrait"
            gates[key] = validate_vision(supplied["gates"][key], path, contracts[key], expected_orientation)
            if gates[key]["master_sha256"] != generation["results"][key]["sha256"]:
                raise RuntimeError(f"generation lineage drift: {key}")
        receipt = {
            "status": "completed", "verified": True, "source_canon_sha256": manifest["canon_sha256"],
            "generation_receipt_path": active["artwork_generation_receipt"], "generation_receipt_sha256": sha256(generation_path),
            "evidence_scope": EXPECTED_SCOPE, "gates": gates, "normalized_gates": {},
            "video_frame_extraction_performed": False, "completed_at": datetime.datetime.now(datetime.timezone.utc).isoformat(),
        }
        atomic_json(output, receipt)
        print(json.dumps({"status": "master_visual_completed", "verified": True}, ensure_ascii=False))
        return

    receipt = json.loads(output.read_text(encoding="utf-8"))
    if receipt.get("verified") is not True or receipt.get("source_canon_sha256") != manifest.get("canon_sha256"):
        raise RuntimeError("master visual receipt failed or stale")
    if not NORMALIZED_INPUT.exists():
        raise RuntimeError("normalized Vision Gate input missing")
    supplied = json.loads(NORMALIZED_INPUT.read_text(encoding="utf-8"))
    if supplied.get("evidence_scope") != EXPECTED_SCOPE:
        raise RuntimeError("normalized Vision evidence scope drift")
    normalized_paths = {
        "intro": ROOT / active["intro_normalized"],
        "left_panel": ROOT / active["left_panel_normalized"],
        "right_panel": ROOT / active["right_panel_normalized"],
        "layout": ROOT / active["layout"],
    }
    normalized = {}
    normalized_contracts = {
        "intro": contracts["intro"],
        "left_panel": contracts["left_panel"],
        "right_panel": contracts["right_panel"],
        "layout": contracts["left_panel"] + contracts["right_panel"],
    }
    for key, path in normalized_paths.items():
        expected_orientation = "landscape" if key in {"intro", "layout"} else "portrait"
        normalized[key] = validate_vision(
            supplied["gates"][key], path, normalized_contracts[key], expected_orientation
        )
    receipt["normalized_gates"] = normalized
    receipt["normalized_verified"] = True
    receipt["completed_at"] = datetime.datetime.now(datetime.timezone.utc).isoformat()
    atomic_json(output, receipt)
    layout_receipt_path = ROOT / active["layout_receipt"]
    layout_receipt = json.loads(layout_receipt_path.read_text(encoding="utf-8"))
    if layout_receipt.get("normalized_visual_gate_required") is not True or layout_receipt.get("artifacts", {}).get("layout", {}).get("sha256") != sha256(normalized_paths["layout"]):
        raise RuntimeError("layout receipt lineage failed")
    layout_receipt["verified"] = True
    layout_receipt["normalized_visual_receipt_path"] = active["artwork_visual_receipt"]
    layout_receipt["normalized_visual_receipt_sha256"] = sha256(output)
    layout_receipt["normalized_visual_verified"] = True
    atomic_json(layout_receipt_path, layout_receipt)
    manifest["steps"]["artwork"] = "completed"
    manifest["steps"]["layout"] = "completed"
    manifest["layout"] = {"status": "completed", "verified": True, "receipt": active["layout_receipt"], "artifact_sha256": sha256(normalized_paths["layout"]), "completed_at": receipt["completed_at"]}
    atomic_json(MANIFEST, manifest)
    print(json.dumps({"status": "normalized_visual_completed", "verified": True, "layout_sha256": sha256(normalized_paths["layout"])}, ensure_ascii=False))


if __name__ == "__main__":
    main()
