#!/usr/bin/env python3
"""Render a manifest with SmartCut64 and exact VideoReDo reference builds."""

from __future__ import annotations

import argparse
import json
import os
from pathlib import Path
import subprocess
import sys
import time
from typing import Any


def last_json(output: str) -> dict[str, Any] | None:
    for line in reversed(output.splitlines()):
        line = line.strip()
        if line.startswith("{") and line.endswith("}"):
            try:
                return json.loads(line)
            except json.JSONDecodeError:
                pass
    return None


def run(command: list[str], timeout: int, environment: dict[str, str] | None = None) -> dict[str, Any]:
    started = time.monotonic()
    try:
        completed = subprocess.run(
            command, capture_output=True, text=True, timeout=timeout,
            encoding="utf-8", errors="replace", env=environment,
        )
        output = completed.stdout + completed.stderr
        return {
            "exit_code": completed.returncode,
            "seconds": round(time.monotonic() - started, 3),
            "document": last_json(completed.stdout),
            "log": output[-20000:],
        }
    except subprocess.TimeoutExpired as error:
        return {
            "exit_code": 124,
            "seconds": round(time.monotonic() - started, 3),
            "document": None,
            "log": f"timeout: {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 reference_timeout(case: dict[str, Any], build: str) -> int:
    codec = case["video"]["codec"]
    bit_depth = case["video"].get("bit_depth", 8)
    if codec in {"mpeg1video", "mpeg4", "vc1", "wmv3"}:
        return 60
    # The unmodified build cannot encode 10-bit boundaries. The updated build
    # extends HEVC Main10 only; H.264 High10, AV1 Main10 and other 10-bit
    # encoders remain unsupported controls and should fail quickly.
    if bit_depth > 8 and (build == "original" or codec != "hevc"):
        return 60
    if bit_depth > 8 or codec in {"hevc", "av1", "vp9"}:
        return 360
    return 180


def terminate_server_from_result(result_path: Path) -> int | None:
    if not result_path.exists():
        return None
    try:
        server_pid = int(json.loads(result_path.read_text(encoding="utf-8"))["server_pid"])
    except (KeyError, TypeError, ValueError, json.JSONDecodeError, OSError):
        return None
    subprocess.run(
        ["taskkill", "/PID", str(server_pid), "/T", "/F"],
        capture_output=True, text=True, timeout=15,
    )
    return server_pid


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--manifest", required=True, type=Path)
    parser.add_argument("--output-root", required=True, type=Path)
    parser.add_argument("--worker", required=True, type=Path)
    parser.add_argument("--original", required=True, type=Path)
    parser.add_argument("--updated", required=True, type=Path)
    parser.add_argument("--start-index", type=int, default=0)
    parser.add_argument("--end-index", type=int, default=0)
    parser.add_argument("--smart-only", action="store_true")
    parser.add_argument("--vrd-only", action="store_true")
    parser.add_argument("--resume", action="store_true")
    arguments = parser.parse_args()
    manifest = json.loads(arguments.manifest.read_text(encoding="utf-8"))
    cases = manifest["cases"]
    end_index = arguments.end_index or len(cases)
    cases = cases[arguments.start_index:end_index]
    arguments.output_root.mkdir(parents=True, exist_ok=True)
    environment = os.environ.copy()
    runtime = str(arguments.worker.parent)
    environment["PATH"] = runtime + os.pathsep + environment.get("PATH", "")
    executable_map = {"original": arguments.original, "updated": arguments.updated}
    batch_results: list[dict[str, Any]] = []
    for absolute_index, case in enumerate(cases, arguments.start_index):
        print(f"[{absolute_index + 1}/{len(manifest['cases'])}] {case['name']}", flush=True)
        case_root = arguments.output_root / case["name"]
        case_root.mkdir(parents=True, exist_ok=True)
        case_result: dict[str, Any] = {
            "schema": "smartcut-videoredo-case-run-v1",
            "index": absolute_index,
            "case": case,
            "renders": {},
        }
        # The edit model may be bounded for a short deterministic fixture, but
        # it must include the complete GOP after the out-point. Giving the
        # renderer only one following frame starves B-frame/reference
        # dependencies and is not equivalent to editing the full source.
        next_random_access = max(case["keyframes"])
        frame_count = max(case["end_frame_exclusive"] + 16, next_random_access + 16)
        cuts = f"0:{case['start_frame']},{case['end_frame_exclusive']}:{frame_count}"
        if not arguments.vrd_only:
            output = case_root / f"smartcut{case['output_extension']}"
            result_path = case_root / "smartcut-run.json"
            if arguments.resume and result_path.exists():
                smart = json.loads(result_path.read_text(encoding="utf-8"))
            else:
                output.unlink(missing_ok=True)
                command = [
                    str(arguments.worker), "--export-edits", "--input", case["source"],
                    "--output", str(output), "--frame-count", str(frame_count),
                    "--cuts", cuts, "--mode", "smart", "--audio-offset-ms", "0",
                ]
                smart = run(command, 900, environment)
                smart["output"] = str(output)
                smart["context_frame_count"] = frame_count
                smart["output_exists"] = output.is_file()
                smart["output_bytes"] = output.stat().st_size if output.is_file() else 0
                write_json(result_path, smart)
            case_result["renders"]["smartcut"] = smart
            print(f"  SmartCut exit={smart['exit_code']} bytes={smart.get('output_bytes', 0)}", flush=True)
        if not arguments.smart_only:
            for build in case["videoredo_builds"]:
                output = case_root / f"videoredo-{build}{case['output_extension']}"
                result_path = case_root / f"videoredo-{build}-run.json"
                if arguments.resume and result_path.exists():
                    vrd = json.loads(result_path.read_text(encoding="utf-8"))
                else:
                    output.unlink(missing_ok=True)
                    result_path.unlink(missing_ok=True)
                    timeout = reference_timeout(case, build)
                    command = [
                        sys.executable, str(Path(__file__).with_name("videoredo-export.py")),
                        "--exe", str(executable_map[build]), "--source", case["source"],
                        "--output", str(output), "--profile", case["output_profile"],
                        "--start-ms", str(case["start_ms"]), "--end-ms", str(case["end_ms"]),
                        "--end-exclusive-ms", str(case.get("end_exclusive_ms", case["end_ms"])),
                        "--start-frame", str(case["start_frame"]),
                        "--end-frame-inclusive", str(case["end_frame_inclusive"]),
                        "--timeout", str(timeout), "--result", str(result_path),
                    ]
                    invoked = run(command, timeout + 30, environment)
                    terminated_pid = None
                    if invoked["exit_code"] == 124:
                        terminated_pid = terminate_server_from_result(result_path)
                    if result_path.exists():
                        vrd = json.loads(result_path.read_text(encoding="utf-8"))
                    else:
                        vrd = {
                            "schema": "videoredo-export-run-v1", "status": "FAILED",
                            "error": invoked["log"],
                        }
                    if invoked["exit_code"] == 124:
                        vrd["status"] = "FAILED"
                        vrd["error"] = (
                            f"Reference watchdog timeout after {timeout + 30}s; "
                            f"terminated exact VideoReDo server PID {terminated_pid}"
                        )
                    vrd["launcher_exit_code"] = invoked["exit_code"]
                    vrd["seconds"] = invoked["seconds"]
                    vrd["output"] = str(output)
                    vrd["output_exists"] = output.is_file()
                    vrd["output_bytes"] = output.stat().st_size if output.is_file() else 0
                    write_json(result_path, vrd)
                case_result["renders"][f"videoredo-{build}"] = vrd
                print(f"  VideoReDo {build} status={vrd.get('status')} bytes={vrd.get('output_bytes', 0)}", flush=True)
        write_json(case_root / "run.json", case_result)
        batch_results.append(case_result)
    write_json(arguments.output_root / f"run-{arguments.start_index:02d}-{end_index:02d}.json", {
        "schema": "smartcut-videoredo-matrix-run-v1", "results": batch_results,
    })
    return 0


if __name__ == "__main__":
    sys.exit(main())
