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

ROOT = Path(__file__).resolve().parents[1]
CANVAS = (1920, 1080)
PANEL = (656, 1080)
BACKGROUND_HEX = "10191f"
BACKGROUND_RGB = bytes((16, 25, 31))


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


def dimensions(path):
    stream = probe(path)["streams"][0]
    return int(stream["width"]), int(stream["height"])


def png_part(path):
    return path.with_name(path.stem + ".part.png")


def contain(source, destination, size):
    source_size = dimensions(source)
    scale = min(1.0, size[0] / source_size[0], size[1] / source_size[1])
    width = max(1, round(source_size[0] * scale))
    height = max(1, round(source_size[1] * scale))
    offset = ((size[0] - width) // 2, (size[1] - height) // 2)
    destination.parent.mkdir(parents=True, exist_ok=True)
    part = png_part(destination)
    subprocess.run(
        [
            "ffmpeg", "-v", "error", "-y", "-i", str(source),
            "-vf", f"scale={width}:{height}:flags=lanczos,pad={size[0]}:{size[1]}:{offset[0]}:{offset[1]}:color=0x{BACKGROUND_HEX}",
            "-frames:v", "1", "-c:v", "png", str(part),
        ],
        check=True,
    )
    if dimensions(part) != size:
        part.unlink(missing_ok=True)
        raise RuntimeError("normalized still dimensions failed")
    os.replace(part, destination)
    return {
        "operation": "scale_down_and_pad_only",
        "source_dimensions": list(source_size),
        "target_dimensions": list(size),
        "scale": scale,
        "offset": list(offset),
        "cropped": False,
        "text_added_or_modified": False,
        "tool": "ffmpeg-single-frame-png",
    }


def compose_layout(left, right, output):
    output.parent.mkdir(parents=True, exist_ok=True)
    part = png_part(output)
    subprocess.run(
        [
            "ffmpeg", "-v", "error", "-y",
            "-f", "lavfi", "-i", f"color=c=0x{BACKGROUND_HEX}:s=1920x1080:r=1",
            "-i", str(left), "-i", str(right),
            "-filter_complex", "[0:v][1:v]overlay=0:0[tmp];[tmp][2:v]overlay=1264:0[out]",
            "-map", "[out]", "-frames:v", "1", "-c:v", "png", str(part),
        ],
        check=True,
    )
    if dimensions(part) != CANVAS:
        part.unlink(missing_ok=True)
        raise RuntimeError("layout dimensions failed")
    os.replace(part, output)


def center_region_clean(layout):
    result = subprocess.run(
        [
            "ffmpeg", "-v", "error", "-i", str(layout),
            "-vf", "crop=608:1080:656:0,format=rgb24", "-frames:v", "1",
            "-f", "rawvideo", "pipe:1",
        ],
        check=True,
        capture_output=True,
    )
    raw = result.stdout
    expected_pixels = 608 * 1080
    if len(raw) != expected_pixels * 3:
        raise RuntimeError("center raw-byte length mismatch")
    first_pixel = raw[:3]
    uniform = raw == first_pixel * expected_pixels
    color_close = all(abs(actual - expected) <= 2 for actual, expected in zip(first_pixel, BACKGROUND_RGB))
    return uniform and color_close


def main():
    manifest_path = ROOT / "script/project-manifest.json"
    manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
    active = manifest["active_paths"]
    generation_path = ROOT / active["artwork_generation_receipt"]
    visual_path = ROOT / active["artwork_visual_receipt"]
    receipt_path = ROOT / active["layout_receipt"]
    layout_manifest_path = ROOT / active["layout_manifest"]
    masters = {
        "intro": ROOT / active["intro_poster_master"],
        "left_panel": ROOT / active["left_panel_master"],
        "right_panel": ROOT / active["right_panel_master"],
    }
    normalized = {
        "intro": ROOT / active["intro_normalized"],
        "left_panel": ROOT / active["left_panel_normalized"],
        "right_panel": ROOT / active["right_panel_normalized"],
    }
    layout = ROOT / active["layout"]

    outputs = list(normalized.values()) + [layout, receipt_path, layout_manifest_path]
    if any(path.exists() or path.with_suffix(path.suffix + ".part").exists() or png_part(path).exists() for path in outputs):
        raise RuntimeError("layout outputs/receipts must be virgin")
    generation = json.loads(generation_path.read_text(encoding="utf-8"))
    visual = json.loads(visual_path.read_text(encoding="utf-8"))
    if generation.get("status") != "completed" or generation.get("verified") is not True:
        raise RuntimeError("artwork generation receipt not terminal")
    if visual.get("status") != "completed" or visual.get("verified") is not True:
        raise RuntimeError("provider-master visual gate not terminal")
    if visual.get("evidence_scope") != "provider-rendered still images only; no video frame used":
        raise RuntimeError("visual evidence scope drift")

    for key, master in masters.items():
        row = generation.get("results", {}).get(key)
        gate = visual.get("gates", {}).get(key)
        if not row or row.get("path") != str(master.relative_to(ROOT)) or sha256(master) != row.get("sha256"):
            raise RuntimeError(f"artwork generation lineage drift: {key}")
        if not gate or gate.get("verified") is not True or gate.get("master_sha256") != sha256(master):
            raise RuntimeError(f"provider-master visual gate drift: {key}")

    operations = {
        "intro": contain(masters["intro"], normalized["intro"], CANVAS),
        "left_panel": contain(masters["left_panel"], normalized["left_panel"], PANEL),
        "right_panel": contain(masters["right_panel"], normalized["right_panel"], PANEL),
    }
    compose_layout(normalized["left_panel"], normalized["right_panel"], layout)
    if not center_region_clean(layout):
        raise RuntimeError("layout center region is not clean")

    artifacts = {
        "intro_master": {"path": active["intro_poster_master"], "sha256": sha256(masters["intro"]), "bytes": masters["intro"].stat().st_size, "probe": probe(masters["intro"])},
        "intro_normalized": {"path": active["intro_normalized"], "sha256": sha256(normalized["intro"]), "bytes": normalized["intro"].stat().st_size, "probe": probe(normalized["intro"])},
        "left_master": {"path": active["left_panel_master"], "sha256": sha256(masters["left_panel"]), "bytes": masters["left_panel"].stat().st_size, "probe": probe(masters["left_panel"])},
        "left_normalized": {"path": active["left_panel_normalized"], "sha256": sha256(normalized["left_panel"]), "bytes": normalized["left_panel"].stat().st_size, "probe": probe(normalized["left_panel"])},
        "right_master": {"path": active["right_panel_master"], "sha256": sha256(masters["right_panel"]), "bytes": masters["right_panel"].stat().st_size, "probe": probe(masters["right_panel"])},
        "right_normalized": {"path": active["right_panel_normalized"], "sha256": sha256(normalized["right_panel"]), "bytes": normalized["right_panel"].stat().st_size, "probe": probe(normalized["right_panel"])},
        "layout": {"path": active["layout"], "sha256": sha256(layout), "bytes": layout.stat().st_size, "probe": probe(layout)},
    }
    now = datetime.datetime.now(datetime.timezone.utc).isoformat()
    receipt = {
        "status": "completed",
        "verified": True,
        "static_image_only": True,
        "static_image_processing_location": "local",
        "static_image_processing_policy": "local scale/pad/composite explicitly allowed; provider-rendered text remains unchanged",
        "video_encode_performed": False,
        "video_processing_policy": "all later MP4 render/encode/transcode stages use LXC workers 192.168.1.104 ports 8021-8024",
        "postproduction_text_added": False,
        "cropping_performed": False,
        "mechanical_operations": ["scale-down and pad provider stills", "composite normalized still panels onto one PNG canvas"],
        "source_canon_sha256": manifest["canon_sha256"],
        "generation_receipt_path": active["artwork_generation_receipt"],
        "generation_receipt_sha256": sha256(generation_path),
        "provider_visual_receipt_path": active["artwork_visual_receipt"],
        "provider_visual_receipt_sha256": sha256(visual_path),
        "geometry": {
            "canvas": [1920, 1080],
            "left": {"x": 0, "y": 0, "width": 656, "height": 1080},
            "center": {"x": 656, "y": 0, "width": 608, "height": 1080, "content": "flat placeholder for footage overlay"},
            "right": {"x": 1264, "y": 0, "width": 656, "height": 1080},
        },
        "operations": operations,
        "artifacts": artifacts,
        "normalized_visual_gate_required": True,
        "center_region_clean": True,
        "no_video_frame_used": True,
        "created_at": now,
    }
    atomic_json(layout_manifest_path, receipt)
    atomic_json(receipt_path, receipt)
    manifest["layout"] = {
        "status": "awaiting_normalized_visual_qa",
        "verified": False,
        "receipt": active["layout_receipt"],
        "artifact_sha256": artifacts["layout"]["sha256"],
        "updated_at": now,
    }
    manifest["updated_at"] = now
    atomic_json(manifest_path, manifest)
    print(json.dumps({"status": "completed", "layout_sha256": artifacts["layout"]["sha256"], "normalized_visual_gate_required": True}, ensure_ascii=False))


if __name__ == "__main__":
    main()
