#!/usr/bin/env python3
"""Run the GCC-4241N E7/HL baseline and sweep suites.

This runner keeps the interactive loop short: it executes the stable JSON
configs, writes matching .output.txt files, then calls the two validators.
"""
from __future__ import annotations

import argparse
import os
import subprocess
import sys
from pathlib import Path

BASELINE_CONFIGS = [
    "test_gcc4241n_e7_hl_dispatcher_subcode2.json",
    "test_gcc4241n_e7_hl_dispatcher_subcode2_len1_pattern.json",
    "test_gcc4241n_e7_hl_dispatcher_subcode2_len3_pattern.json",
    "test_gcc4241n_e7_hl_dispatcher_subcode2_len4.json",
    "test_gcc4241n_e7_hl_dispatcher_subcode2_len4_pattern.json",
    "test_gcc4241n_e7_hl_dispatcher_subcode2_len8_pattern.json",
    "test_gcc4241n_e7_hl_dispatcher_subcode2_len16_pattern.json",
    "test_gcc4241n_e7_hl_dispatcher_subcode2_len20_pattern.json",
    "test_gcc4241n_e7_hl_dispatcher_subcode2_len40_pattern.json",
    "test_gcc4241n_e7_hl_dispatcher_subcode2_len4_offset0fff_pattern.json",
    "test_gcc4241n_e7_hl_dispatcher_subcode2_len8_offset0ffe_pattern.json",
]

SWEEP_CONFIGS = [
    "test_gcc4241n_e7_hl_copy_len1.json",
    "test_gcc4241n_e7_hl_copy_len3.json",
    "test_gcc4241n_e7_hl_copy_len4.json",
    "test_gcc4241n_e7_hl_copy_len8.json",
    "test_gcc4241n_e7_hl_copy_len16.json",
    "test_gcc4241n_e7_hl_copy_len20.json",
    "test_gcc4241n_e7_hl_copy_len40.json",
    "test_gcc4241n_e7_hl_sub1_len1.json",
    "test_gcc4241n_e7_hl_sub1_len3.json",
    "test_gcc4241n_e7_hl_sub1_len4.json",
    "test_gcc4241n_e7_hl_sub1_len8.json",
    "test_gcc4241n_e7_hl_sub1_len16.json",
    "test_gcc4241n_e7_hl_sub1_len20.json",
    "test_gcc4241n_e7_hl_sub1_len40.json",
    "test_gcc4241n_e7_hl_sub3_len4.json",
    "test_gcc4241n_e7_hl_sub4_len1.json",
    "test_gcc4241n_e7_hl_sub4_len3.json",
    "test_gcc4241n_e7_hl_sub4_len4.json",
    "test_gcc4241n_e7_hl_sub4_len8.json",
    "test_gcc4241n_e7_hl_sub4_len16.json",
    "test_gcc4241n_e7_hl_sub4_len20.json",
    "test_gcc4241n_e7_hl_sub4_len40.json",
    "test_gcc4241n_e7_hl_subD_dma_len1.json",
    "test_gcc4241n_e7_hl_subD_dma_len3.json",
    "test_gcc4241n_e7_hl_subD_dma_len4.json",
    "test_gcc4241n_e7_hl_subD_dma_len8.json",
    "test_gcc4241n_e7_hl_subD_dma_len16.json",
    "test_gcc4241n_e7_hl_subD_dma_len20.json",
    "test_gcc4241n_e7_hl_subD_dma_len40.json",
    "test_gcc4241n_e7_hl_subE_len4.json",
    "test_gcc4241n_e7_hl_subF_dma_len1.json",
    "test_gcc4241n_e7_hl_subF_dma_len3.json",
    "test_gcc4241n_e7_hl_subF_dma_len4.json",
    "test_gcc4241n_e7_hl_subF_dma_len8.json",
    "test_gcc4241n_e7_hl_subF_dma_len16.json",
    "test_gcc4241n_e7_hl_subF_dma_len20.json",
    "test_gcc4241n_e7_hl_subF_dma_len40.json",
]


def output_name(config_name: str) -> str:
    stem = Path(config_name).stem
    if stem.startswith("test_"):
        stem = stem[5:]
    return f"{stem}.output.txt"


def run_one(script_dir: Path, py: str, config_name: str, timeout_sec: int, *, skip_existing: bool = False, workspace: Path | None = None) -> Path:
    cfg = script_dir / config_name
    out = script_dir / output_name(config_name)
    if not cfg.exists():
        raise FileNotFoundError(f"missing config: {cfg}")
    if skip_existing and out.exists() and out.stat().st_size > 0:
        print(f"[SKIP] {cfg.name} -> {out.name}", flush=True)
        return out
    print(f"[RUN] {cfg.name} -> {out.name}", flush=True)
    env = os.environ.copy()
    if workspace is not None:
        env["XDVD_WORKSPACE_OVERRIDE"] = str(workspace.resolve())
    with out.open("w", encoding="utf-8") as fh:
        subprocess.run([py, str(script_dir / "mn103s_emu.py"), str(cfg)], stdout=fh, stderr=subprocess.STDOUT, check=True, timeout=timeout_sec, env=env)
    return out


def validate(script_dir: Path, py: str, validator: str, outputs: list[Path]) -> None:
    if not outputs:
        return
    cmd = [py, str(script_dir / validator), *map(str, outputs)]
    print(f"[VALIDATE] {validator} ({len(outputs)} log(s))", flush=True)
    subprocess.run(cmd, check=True)


def apply_start_at(configs: list[str], start_at: str | None) -> list[str]:
    if not start_at:
        return configs
    wanted = Path(start_at).name
    if not wanted.endswith(".json"):
        wanted = wanted + ".json"
    if wanted.startswith("gcc"):
        wanted = "test_" + Path(wanted).with_suffix("").name + ".json"
    try:
        idx = configs.index(wanted)
    except ValueError:
        names = ", ".join(configs)
        raise ValueError(f"--start-at {start_at!r} did not match a known config. Known configs: {names}")
    return configs[idx:]


def main(argv: list[str]) -> int:
    ap = argparse.ArgumentParser(description="Run GCC-4241N E7/HL emulator suites")
    group = ap.add_mutually_exclusive_group()
    group.add_argument("--baseline", action="store_true", help="run only subcode-2 baseline configs")
    group.add_argument("--sweep", action="store_true", help="run only neighboring HL subcode sweep configs")
    group.add_argument("--all", action="store_true", help="run baseline and sweep configs (default)")
    ap.add_argument("--python", default=sys.executable, help="Python executable to use")
    ap.add_argument("--timeout-sec", type=int, default=120, help="timeout per emulator run")
    ap.add_argument("--skip-existing", action="store_true", help="reuse existing non-empty output files instead of re-running them")
    ap.add_argument("--start-at", help="resume at this config name and run the rest of the selected suite")
    ap.add_argument("--workspace", type=Path, help="repo/workspace root to resolve firmware paths without editing JSON")
    ns = ap.parse_args(argv[1:])

    script_dir = Path(__file__).resolve().parent
    run_baseline = ns.all or ns.baseline or not (ns.baseline or ns.sweep or ns.all)
    run_sweep = ns.all or ns.sweep or not (ns.baseline or ns.sweep or ns.all)

    baseline_outputs: list[Path] = []
    sweep_outputs: list[Path] = []
    baseline_configs = list(BASELINE_CONFIGS) if run_baseline else []
    sweep_configs = list(SWEEP_CONFIGS) if run_sweep else []

    if ns.start_at:
        wanted = Path(ns.start_at).name
        if not wanted.endswith(".json"):
            wanted = wanted + ".json"
        if wanted.startswith("gcc"):
            wanted = "test_" + Path(wanted).with_suffix("").name + ".json"
        if wanted in baseline_configs:
            baseline_configs = apply_start_at(baseline_configs, wanted)
        elif wanted in sweep_configs:
            baseline_configs = []
            sweep_configs = apply_start_at(sweep_configs, wanted)
        else:
            known = baseline_configs + sweep_configs
            raise ValueError(f"--start-at {ns.start_at!r} did not match a selected config. Known configs: {', '.join(known)}")

    if baseline_configs:
        baseline_outputs = [run_one(script_dir, ns.python, cfg, ns.timeout_sec, skip_existing=ns.skip_existing, workspace=ns.workspace) for cfg in baseline_configs]
        validate(script_dir, ns.python, "validate_gcc4241n_e7_hl_trace.py", baseline_outputs)
    if sweep_configs:
        sweep_outputs = [run_one(script_dir, ns.python, cfg, ns.timeout_sec, skip_existing=ns.skip_existing, workspace=ns.workspace) for cfg in sweep_configs]
        validate(script_dir, ns.python, "validate_gcc4241n_e7_hl_sweep_trace.py", sweep_outputs)
    print(f"[OK] completed {len(baseline_outputs) + len(sweep_outputs)} GCC E7/HL run(s)", flush=True)
    return 0


if __name__ == "__main__":
    raise SystemExit(main(sys.argv))
