#!/usr/bin/env python3
"""Audit SmartCut64 and VideoReDo mid-GOP comparison renders."""

from __future__ import annotations

import argparse
import json
from pathlib import Path
import subprocess
from typing import Any


def run(command: list[str], timeout: int = 300) -> subprocess.CompletedProcess[str]:
    try:
        return subprocess.run(
            command, capture_output=True, text=True, encoding="utf-8",
            errors="replace", timeout=timeout,
        )
    except subprocess.TimeoutExpired as error:
        return subprocess.CompletedProcess(command, 124, "", f"timeout after {timeout}s: {error}")


def write_json(path: Path, document: Any) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    temporary = path.with_suffix(path.suffix + ".tmp")
    temporary.write_text(json.dumps(document, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
    temporary.replace(path)


def probe(ffprobe: Path, media: Path) -> dict[str, Any]:
    result = run([
        str(ffprobe), "-v", "error", "-count_frames", "-count_packets",
        "-show_streams", "-show_format", "-of", "json", str(media),
    ])
    if result.returncode != 0:
        return {"status": "FAILED", "error": result.stderr[-4000:]}
    try:
        document = json.loads(result.stdout)
    except json.JSONDecodeError as error:
        return {"status": "FAILED", "error": f"invalid ffprobe JSON: {error}"}
    streams = []
    for stream in document.get("streams", []):
        streams.append({
            "index": stream.get("index"),
            "codec_type": stream.get("codec_type"),
            "codec_name": stream.get("codec_name"),
            "profile": stream.get("profile"),
            "pixel_format": stream.get("pix_fmt"),
            "width": stream.get("width"),
            "height": stream.get("height"),
            "sample_rate": stream.get("sample_rate"),
            "channels": stream.get("channels"),
            "channel_layout": stream.get("channel_layout"),
            "frames": int(stream.get("nb_read_frames", 0) or 0),
            "packets": int(stream.get("nb_read_packets", 0) or 0),
        })
    return {
        "status": "PASSED",
        "format_name": document.get("format", {}).get("format_name"),
        "duration": document.get("format", {}).get("duration"),
        "bytes": media.stat().st_size,
        "streams": streams,
    }


def strict_decode(ffmpeg: Path, media: Path) -> dict[str, Any]:
    result = run([
        str(ffmpeg), "-hide_banner", "-v", "error", "-xerror", "-i", str(media),
        "-map", "0:v:0", "-map", "0:a?", "-fps_mode", "passthrough", "-f", "null", "-",
    ], 600)
    return {
        "status": "PASSED" if result.returncode == 0 else "FAILED",
        "exit_code": result.returncode,
        "errors": result.stderr[-8000:],
    }


def frame_hashes(ffmpeg: Path, media: Path, count: int, start: int | None = None) -> dict[str, Any]:
    command = [str(ffmpeg), "-hide_banner", "-v", "error", "-i", str(media), "-map", "0:v:0"]
    if start is not None:
        command += ["-vf", f"trim=start_frame={start}:end_frame={start + count},setpts=PTS-STARTPTS"]
    command += ["-frames:v", str(count), "-fps_mode", "passthrough", "-f", "framemd5", "-"]
    result = run(command, 600)
    hashes: list[str] = []
    if result.returncode == 0:
        for line in result.stdout.splitlines():
            if not line or line.startswith("#"):
                continue
            fields = [field.strip() for field in line.split(",")]
            if len(fields) >= 6:
                hashes.append(fields[-1])
    return {
        "status": "PASSED" if result.returncode == 0 else "FAILED",
        "frames": len(hashes), "hashes": hashes,
        "errors": result.stderr[-4000:],
    }


def smart_render_counts(run_document: dict[str, Any]) -> tuple[int, int]:
    report = (run_document.get("document") or {}).get("report") or {}
    segments = report.get("segment_reports") or []
    copied = sum(int(segment.get("copied_frames", 0) or 0) for segment in segments)
    encoded = sum(int(segment.get("encoded_frames", 0) or 0) for segment in segments)
    return copied, encoded


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--manifest", required=True, type=Path)
    parser.add_argument("--results", required=True, type=Path)
    parser.add_argument("--ffmpeg", required=True, type=Path)
    parser.add_argument("--ffprobe", required=True, type=Path)
    parser.add_argument("--output-json", required=True, type=Path)
    parser.add_argument("--output-markdown", required=True, type=Path)
    parser.add_argument("--skip-frame-hashes", action="store_true")
    arguments = parser.parse_args()

    manifest = json.loads(arguments.manifest.read_text(encoding="utf-8"))
    report: dict[str, Any] = {
        "schema": "smartcut-videoredo-comparison-report-v1",
        "manifest": str(arguments.manifest), "cases": [], "summary": {},
    }
    for index, case in enumerate(manifest["cases"]):
        print(f"[{index + 1}/{len(manifest['cases'])}] {case['name']}", flush=True)
        case_root = arguments.results / case["name"]
        start = int(case["start_frame"])
        expected_frames = int(case["expected_frames"])
        source_hash = {"status": "SKIPPED", "hashes": [], "frames": 0}
        if not arguments.skip_frame_hashes:
            source_hash = frame_hashes(arguments.ffmpeg, Path(case["source"]), expected_frames, start)
        case_report: dict[str, Any] = {
            "name": case["name"], "source": case["source"],
            "video": case["video"], "audio": case["audio"],
            "start_frame": start, "end_frame_exclusive": case["end_frame_exclusive"],
            "expected_frames": expected_frames,
            "start_inside_gop": start not in case["keyframes"],
            "end_inside_gop": int(case["end_frame_exclusive"]) not in case["keyframes"],
            "source_frame_hash": {key: value for key, value in source_hash.items() if key != "hashes"},
            "renders": {},
        }
        render_names = ["smartcut"] + [f"videoredo-{build}" for build in case["videoredo_builds"]]
        for name in render_names:
            run_path = case_root / f"{name}-run.json"
            run_document = json.loads(run_path.read_text(encoding="utf-8")) if run_path.exists() else {}
            output = Path(run_document.get("output", case_root / f"{name}{case['output_extension']}"))
            if not output.is_absolute():
                output = Path.cwd() / output
            exists = output.is_file() and output.stat().st_size > 0
            render: dict[str, Any] = {
                "run_status": ((run_document.get("document") or {}).get("status") or run_document.get("status")),
                "exit_code": run_document.get("exit_code", run_document.get("launcher_exit_code")),
                "output": str(output), "output_exists": exists,
            }
            if name == "smartcut":
                copied, encoded = smart_render_counts(run_document)
                render["copied_frames"] = copied
                render["encoded_frames"] = encoded
                render["boundary_reencode_exercised"] = encoded > 0
            if exists:
                render["probe"] = probe(arguments.ffprobe, output)
                render["strict_decode"] = strict_decode(arguments.ffmpeg, output)
                output_hash = {"status": "SKIPPED", "hashes": [], "frames": 0}
                if not arguments.skip_frame_hashes:
                    output_hash = frame_hashes(arguments.ffmpeg, output, expected_frames + 8)
                hashes = output_hash.get("hashes", [])
                source_hashes = source_hash.get("hashes", [])
                comparisons = min(len(hashes), len(source_hashes))
                render["frame_audit"] = {
                    "status": output_hash["status"],
                    "decoded_frames": output_hash.get("frames", 0),
                    "expected_frames": expected_frames,
                    "frame_count_match": output_hash.get("frames", 0) == expected_frames,
                    "source_exact_frame_matches": sum(
                        hashes[position] == source_hashes[position] for position in range(comparisons)
                    ),
                    "compared_frames": comparisons,
                    "first_frame_exact": bool(comparisons and hashes[0] == source_hashes[0]),
                    "last_frame_exact": bool(
                        comparisons == expected_frames and hashes[expected_frames - 1] == source_hashes[expected_frames - 1]
                    ),
                    "errors": output_hash.get("errors", ""),
                }
            case_report["renders"][name] = render
        report["cases"].append(case_report)

    smart_pass = sum(case["renders"]["smartcut"].get("run_status") == "PASSED" for case in report["cases"])
    smart_decode = sum(
        case["renders"]["smartcut"].get("strict_decode", {}).get("status") == "PASSED"
        for case in report["cases"]
    )
    reference_pass = sum(
        render.get("run_status") == "PASSED"
        for case in report["cases"] for name, render in case["renders"].items() if name.startswith("videoredo-")
    )
    report["summary"] = {
        "cases": len(report["cases"]), "smartcut_run_passed": smart_pass,
        "smartcut_strict_decode_passed": smart_decode,
        "reference_renders_passed": reference_pass,
    }
    write_json(arguments.output_json, report)

    lines = [
        "# SmartCut64 / VideoReDo mid-GOP comparison", "",
        f"Cases: {len(report['cases'])}; SmartCut renders passed: {smart_pass}; "
        f"SmartCut strict decodes passed: {smart_decode}; reference renders passed: {reference_pass}.", "",
        "| Case | Mid-GOP | SmartCut | Copy / encode | Original VRD | Updated VRD |",
        "|---|---:|---|---:|---|---|",
    ]
    for case in report["cases"]:
        renders = case["renders"]
        smart = renders["smartcut"]
        original = renders.get("videoredo-original", {})
        updated = renders.get("videoredo-updated", {})
        lines.append(
            f"| {case['name']} | {'yes' if case['start_inside_gop'] and case['end_inside_gop'] else 'partial'} "
            f"| {smart.get('run_status', 'missing')} | {smart.get('copied_frames', 0)} / {smart.get('encoded_frames', 0)} "
            f"| {original.get('run_status', 'n/a')} | {updated.get('run_status', 'n/a')} |"
        )
    lines += ["", "Generated from machine-readable evidence in the companion JSON report.", ""]
    arguments.output_markdown.write_text("\n".join(lines), encoding="utf-8")
    print(json.dumps(report["summary"], indent=2), flush=True)
    return 0


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