#!/usr/bin/env python3
"""Run one deterministic VideoReDo smart-render export against an exact build."""

from __future__ import annotations

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

import pythoncom
import win32com.client


def write_result(path: Path | None, result: dict[str, Any]) -> None:
    if path is None:
        return
    path.parent.mkdir(parents=True, exist_ok=True)
    temporary = path.with_suffix(path.suffix + ".tmp")
    temporary.write_text(
        json.dumps(result, indent=2, ensure_ascii=False) + "\n", encoding="utf-8"
    )
    temporary.replace(path)


def wait_for_server(process: subprocess.Popen[Any], seconds: float = 15.0) -> None:
    deadline = time.monotonic() + seconds
    while time.monotonic() < deadline:
        if process.poll() is not None:
            raise RuntimeError(f"VideoReDo exited before COM activation ({process.returncode})")
        time.sleep(0.25)
        if time.monotonic() + 1.0 >= deadline:
            break


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--exe", required=True, type=Path)
    parser.add_argument("--source", required=True, type=Path)
    parser.add_argument("--output", required=True, type=Path)
    parser.add_argument("--profile", required=True)
    parser.add_argument("--start-ms", required=True, type=int)
    parser.add_argument("--end-ms", required=True, type=int)
    parser.add_argument("--end-exclusive-ms", type=int)
    parser.add_argument("--start-frame", type=int)
    parser.add_argument("--end-frame-inclusive", type=int)
    parser.add_argument("--timeout", type=int, default=1800)
    parser.add_argument("--result", type=Path)
    arguments = parser.parse_args()
    arguments.exe = arguments.exe.resolve()
    arguments.source = arguments.source.resolve()
    arguments.output = arguments.output.resolve()
    if arguments.result is not None:
        arguments.result = arguments.result.resolve()
    result: dict[str, Any] = {
        "schema": "videoredo-export-run-v1",
        "executable": str(arguments.exe),
        "source": str(arguments.source),
        "output": str(arguments.output),
        "profile": arguments.profile,
        "start_ms": arguments.start_ms,
        "end_ms": arguments.end_ms,
        "status": "FAILED",
    }
    arguments.output.parent.mkdir(parents=True, exist_ok=True)
    arguments.output.unlink(missing_ok=True)
    startup = subprocess.STARTUPINFO()
    startup.dwFlags |= subprocess.STARTF_USESHOWWINDOW
    startup.wShowWindow = subprocess.SW_HIDE
    process = subprocess.Popen(
        [str(arguments.exe), "-Embedding"], cwd=arguments.exe.parent,
        startupinfo=startup, creationflags=subprocess.CREATE_NO_WINDOW,
    )
    result["server_pid"] = process.pid
    result["phase"] = "launched"
    # Persist the exact server PID before making any COM call. Some legacy
    # inputs make VideoReDo display an invisible modal dialog while hosted as
    # a COM server; the matrix watchdog can then terminate only this instance.
    write_result(arguments.result, result)
    app = None
    try:
        wait_for_server(process, 2.0)
        pythoncom.CoInitialize()
        result["phase"] = "activating_com"
        write_result(arguments.result, result)
        app = win32com.client.Dispatch("VideoReDo6.Application")
        result["version"] = str(app.ProgramGetVersionNumber)
        app.ProgramSetAudioAlert(False)
        result["phase"] = "opening_source"
        write_result(arguments.result, result)
        opened = bool(app.FileOpen(str(arguments.source), False))
        result["opened"] = opened
        if not opened:
            raise RuntimeError("VideoReDo did not open the source")
        app.EditSetMode(1)
        selection_start = arguments.start_ms
        selection_end = arguments.end_ms
        if arguments.start_frame is not None and arguments.end_frame_inclusive is not None:
            def snapped_cursor(target_ms: int) -> int:
                app.NavigationSeekToTime(target_ms)
                cursor = int(app.NavigationGetCursorTime)
                # Seek lands on or before the target. Advance only the handful
                # of displayed frames needed to reach the requested timestamp;
                # walking from frame zero is both slow and unreliable on VFR.
                for _ in range(16):
                    if cursor >= target_ms:
                        break
                    if not app.NavigationFrameAdvance(True, False):
                        break
                    advanced = int(app.NavigationGetCursorTime)
                    if advanced <= cursor:
                        break
                    cursor = advanced
                return cursor

            selection_start = snapped_cursor(arguments.start_ms)
            selection_end = snapped_cursor(
                arguments.end_exclusive_ms if arguments.end_exclusive_ms is not None else arguments.end_ms
            )
        result["selection_start_cursor_ms"] = selection_start
        result["selection_end_cursor_ms"] = selection_end
        result["selection_end_policy"] = "exclusive"
        app.EditSetSelectionStart(selection_start)
        app.EditSetSelectionEnd(selection_end)
        result["selection_added"] = bool(app.EditAddSelection())
        result["edit_count"] = int(app.EditGetEditsListCount)
        # VideoReDo 6 commonly returns false from EditAddSelection even though it
        # has committed the scene; the edit-list count is the authoritative API.
        if result["edit_count"] < 1:
            raise RuntimeError("VideoReDo rejected the selection")
        result["phase"] = "starting_export"
        write_result(arguments.result, result)
        started = bool(app.FileSaveAs(str(arguments.output), arguments.profile))
        result["save_started"] = started
        if not started:
            raise RuntimeError("VideoReDo did not start the export")
        result["phase"] = "exporting"
        write_result(arguments.result, result)
        deadline = time.monotonic() + arguments.timeout
        progress: list[dict[str, Any]] = []
        while True:
            state = int(app.OutputGetState)
            percent = float(app.OutputGetPercentComplete)
            status = str(app.OutputGetStatusText)
            if not progress or percent != progress[-1]["percent"] or status != progress[-1]["text"]:
                progress.append({"percent": percent, "text": status})
                print(f"{percent:.1f}% {status}", flush=True)
            if state == 0:
                break
            if time.monotonic() > deadline:
                try:
                    app.OutputAbort()
                except Exception:
                    pass
                raise TimeoutError("VideoReDo export timed out")
            time.sleep(0.5)
        result["progress"] = progress
        result["completed_info"] = str(app.OutputGetCompletedInfo)
        if not arguments.output.is_file() or arguments.output.stat().st_size == 0:
            raise RuntimeError("VideoReDo completed without a non-empty output")
        result["output_bytes"] = arguments.output.stat().st_size
        result["status"] = "PASSED"
        result["phase"] = "complete"
    except Exception as error:
        result["error"] = f"{type(error).__name__}: {error}"
        print(result["error"], file=sys.stderr, flush=True)
    finally:
        if app is not None:
            try:
                app.ProgramExit()
            except Exception:
                pass
        try:
            process.wait(timeout=10)
        except subprocess.TimeoutExpired:
            process.terminate()
            try:
                process.wait(timeout=5)
            except subprocess.TimeoutExpired:
                process.kill()
        pythoncom.CoUninitialize()
    encoded = json.dumps(result, indent=2, ensure_ascii=False) + "\n"
    write_result(arguments.result, result)
    print(encoded, end="")
    return 0 if result["status"] == "PASSED" else 1


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