//! Cross-backend perf GATE — the guardrail for the `Vmm<H: Hypervisor>`
//! unification (cross-platform port Prereq 2).
//!
//! WHY THIS EXISTS
//! ---------------
//! Unifying the HVF (`vmm/*`) and KVM (`kvm/run.rs`) runtimes behind one
//! generic `Vmm<H>` must NOT cost either backend a cycle. This binary pins the
//! three axes a run-loop / lifecycle refactor can regress and gates them
//! baseline-relative, on BOTH backends (HVF on macOS, KVM on Linux):
//!
//!   * `restore.first_acquire`  — cold restore + first acquire (lifecycle).
//!   * `exec.roundtrip`         — trivial `true` exec round-trip: one full
//!                                guest↔host hop = vCPU exits + vsock dataplane.
//!   * `vsock.throughput`       — sustained host←guest bytes (the muxer / drain
//!                                dataplane the seam must keep native).
//!
//! It mirrors the npm `_perf_harness.ts` contract: a committed per-platform
//! baseline of `{budget (hard ceiling), reference (observed), tolerance (soft
//! band multiplier), unit, lowerIsBetter}`, gating modes via `SM_PERF_GATE`
//! (fail|strict|warn|off), reseed via `SM_PERF_UPDATE=1`, and a machine-readable
//! `perf-results.json` every run. Run it before/after each unification stage:
//!
//!   cargo run --release --example _perf_gate            # gate (fail mode)
//!   SM_PERF_GATE=strict cargo run --release --example _perf_gate
//!   SM_PERF_UPDATE=1     cargo run --release --example _perf_gate  # reseed
//!
//! It's an example (not a `#[test]`) on purpose: perf wants a quiet host and
//! must stay OUT of the correctness suite (absolute-threshold perf tests are
//! exactly what flaked `snapshot_warm_baked_serves_first_cycle_fast`). CI runs
//! it in a dedicated job. If the rust_1_slim snapshot is absent it skips (exit
//! 0) — bring-up hosts without a baked snapshot don't fail the gate.

use std::time::{Duration, Instant};

use serde_json::{json, Map, Value};
use supermachine::Image;

const PLATFORM: &str = if cfg!(all(target_os = "macos", target_arch = "aarch64")) {
    "hvf"
} else if cfg!(all(target_os = "linux", target_arch = "x86_64")) {
    "kvm"
} else {
    "other"
};

fn home() -> String {
    std::env::var("HOME").unwrap_or_else(|_| "/root".into())
}
fn snap_path() -> String {
    format!("{}/.local/supermachine-snapshots/rust_1_slim", home())
}

fn median(mut v: Vec<f64>) -> f64 {
    v.sort_by(|a, b| a.partial_cmp(b).unwrap());
    if v.is_empty() {
        return 0.0;
    }
    v[v.len() / 2]
}

struct Metric {
    name: &'static str,
    value: f64,
    unit: &'static str,
    lower_is_better: bool,
}

// ── Measurement ────────────────────────────────────────────────────────────

/// Cold restore + first acquire latency (ms), median of `n`. Each iteration is
/// a fresh pool so the restore is genuinely cold (no warm idle VM reused).
fn measure_restore(snap: &str, n: usize) -> Option<f64> {
    let mut times = Vec::with_capacity(n);
    for _ in 0..n {
        let image = Image::from_snapshot(snap).ok()?;
        let t0 = Instant::now();
        let pool = image
            .pool()
            .min(1)
            .max(1)
            .idle_timeout(Duration::MAX)
            .restore_on_release(false)
            .build()
            .ok()?;
        let _vm = pool.acquire().ok()?;
        times.push(t0.elapsed().as_secs_f64() * 1000.0);
    }
    Some(median(times))
}

/// Trivial `true` exec round-trip latency (ms), median of `n`, on a warm VM
/// (so we isolate the host↔guest hop, not the boot). 3 warmup execs first.
fn measure_exec_roundtrip(snap: &str, n: usize) -> Option<f64> {
    let image = Image::from_snapshot(snap).ok()?;
    let pool = image
        .pool()
        .min(1)
        .max(1)
        .idle_timeout(Duration::MAX)
        .restore_on_release(false)
        .build()
        .ok()?;
    let vm = pool.acquire().ok()?;
    let run = || {
        vm.exec_builder()
            .argv(["true"].iter().copied())
            .timeout(Duration::from_secs(10))
            .output()
    };
    for _ in 0..3 {
        run().ok()?;
    }
    let mut times = Vec::with_capacity(n);
    for _ in 0..n {
        let t0 = Instant::now();
        let out = run().ok()?;
        if !out.success() {
            return None;
        }
        times.push(t0.elapsed().as_secs_f64() * 1000.0);
    }
    Some(median(times))
}

/// Sustained host←guest throughput (MiB/s), median of 3 runs of `mib` MiB each.
/// The guest streams `mib` MiB of zeros to stdout; the host drains it through
/// the vsock muxer (the dataplane `try_drain_rx`/muxer path the seam owns).
fn measure_throughput(snap: &str, mib: usize) -> Option<f64> {
    let image = Image::from_snapshot(snap).ok()?;
    let pool = image
        .pool()
        .min(1)
        .max(1)
        .idle_timeout(Duration::MAX)
        .restore_on_release(false)
        .build()
        .ok()?;
    let vm = pool.acquire().ok()?;
    let bytes = mib * 1024 * 1024;
    let cmd = format!("head -c {bytes} /dev/zero");
    // warmup
    let _ = vm
        .exec_builder()
        .argv(["sh", "-c", &cmd].iter().copied())
        .timeout(Duration::from_secs(60))
        .output()
        .ok()?;
    let mut rates = Vec::with_capacity(3);
    for _ in 0..3 {
        let t0 = Instant::now();
        let out = vm
            .exec_builder()
            .argv(["sh", "-c", &cmd].iter().copied())
            .timeout(Duration::from_secs(60))
            .output()
            .ok()?;
        let secs = t0.elapsed().as_secs_f64();
        if out.stdout.len() != bytes || secs <= 0.0 {
            return None;
        }
        rates.push((mib as f64) / secs);
    }
    Some(median(rates))
}

// ── Baseline-relative gating (mirrors _perf_harness.ts) ──────────────────────

fn main() {
    let gate = std::env::var("SM_PERF_GATE").unwrap_or_else(|_| "fail".into());
    let update = std::env::var("SM_PERF_UPDATE").ok().as_deref() == Some("1");
    let snap = snap_path();

    if PLATFORM == "other" {
        println!("[perf-gate] unsupported platform — skipping");
        return;
    }
    if !std::path::Path::new(&snap).exists() {
        println!("[perf-gate] no rust_1_slim snapshot at {snap} — skipping (exit 0)");
        return;
    }
    println!("[perf-gate] platform={PLATFORM} gate={gate} update={update}");

    let mut metrics: Vec<Metric> = Vec::new();
    if let Some(v) = measure_restore(&snap, 5) {
        metrics.push(Metric {
            name: "restore.first_acquire",
            value: v,
            unit: "ms",
            lower_is_better: true,
        });
    }
    if let Some(v) = measure_exec_roundtrip(&snap, 21) {
        metrics.push(Metric {
            name: "exec.roundtrip",
            value: v,
            unit: "ms",
            lower_is_better: true,
        });
    }
    if let Some(v) = measure_throughput(&snap, 64) {
        metrics.push(Metric {
            name: "vsock.throughput",
            value: v,
            unit: "MiB/s",
            lower_is_better: false,
        });
    }
    if metrics.is_empty() {
        eprintln!("[perf-gate] no metrics measured (workload failed) — FAIL");
        std::process::exit(1);
    }

    let manifest = env!("CARGO_MANIFEST_DIR");
    let baseline_path = format!("{manifest}/perf/perf-baseline.json");
    let mut baseline: Value = std::fs::read_to_string(&baseline_path)
        .ok()
        .and_then(|s| serde_json::from_str(&s).ok())
        .unwrap_or_else(|| json!({}));

    let plat_base = baseline.get(PLATFORM).cloned().unwrap_or_else(|| json!({}));

    let mut hard_fail = false;
    let mut soft_fail = false;
    let mut results = Map::new();

    for m in &metrics {
        let entry = plat_base.get(m.name);
        let reference = entry.and_then(|e| e["reference"].as_f64());
        let budget = entry.and_then(|e| e["budget"].as_f64());
        let tolerance = entry.and_then(|e| e["tolerance"].as_f64()).unwrap_or(2.0);

        // ratio: >1 always means WORSE, regardless of direction.
        let ratio = reference.map(|r| {
            if m.lower_is_better {
                m.value / r
            } else {
                r / m.value
            }
        });
        let over_budget = budget.is_some_and(|b| {
            if m.lower_is_better {
                m.value > b
            } else {
                m.value < b
            }
        });
        let over_soft = ratio.is_some_and(|r| r > tolerance);

        let status = if over_budget {
            hard_fail = true;
            "hard-regression"
        } else if over_soft {
            soft_fail = true;
            "soft-regression"
        } else if reference.is_none() {
            "untracked"
        } else {
            "ok"
        };

        let ratio_s = ratio.map(|r| format!("{r:.2}x")).unwrap_or("-".into());
        println!(
            "  {:<24} {:>10.2} {:<6} ref={:<8} budget={:<8} ratio={:<6} {status}",
            m.name,
            m.value,
            m.unit,
            reference.map(|r| format!("{r:.2}")).unwrap_or("-".into()),
            budget.map(|b| format!("{b:.0}")).unwrap_or("-".into()),
            ratio_s,
        );

        results.insert(
            m.name.to_string(),
            json!({
                "value": m.value, "unit": m.unit, "reference": reference,
                "budget": budget, "ratio": ratio, "status": status,
            }),
        );
    }

    // Emit machine-readable results for trend tracking.
    let _ = std::fs::write(
        format!("{manifest}/perf/perf-results-{PLATFORM}.json"),
        serde_json::to_string_pretty(&json!({
            "platform": PLATFORM,
            "metrics": Value::Object(results),
        }))
        .unwrap(),
    );

    if update {
        // Reseed `reference` (NOT budget — hard ceilings are hand-set) from this
        // run. Keep existing tolerance/budget; create generous defaults if new.
        let mut plat = plat_base.as_object().cloned().unwrap_or_default();
        for m in &metrics {
            let prev = plat.get(m.name).cloned().unwrap_or_else(|| json!({}));
            let budget = prev["budget"].as_f64().unwrap_or_else(|| {
                if m.lower_is_better {
                    m.value * 6.0
                } else {
                    m.value / 4.0
                }
            });
            let tolerance = prev["tolerance"].as_f64().unwrap_or(2.0);
            plat.insert(
                m.name.to_string(),
                json!({
                    "budget": budget, "reference": m.value,
                    "tolerance": tolerance, "unit": m.unit,
                    "lowerIsBetter": m.lower_is_better,
                }),
            );
        }
        if !baseline.is_object() {
            baseline = json!({});
        }
        baseline[PLATFORM] = Value::Object(plat);
        std::fs::create_dir_all(format!("{manifest}/perf")).ok();
        std::fs::write(
            &baseline_path,
            serde_json::to_string_pretty(&baseline).unwrap(),
        )
        .unwrap();
        println!("[perf-gate] reseeded {PLATFORM} baseline in {baseline_path}");
        return;
    }

    let fail = match gate.as_str() {
        "off" | "warn" => false,
        "strict" => hard_fail || soft_fail,
        _ => hard_fail, // "fail" (default)
    };
    if soft_fail && !fail {
        println!("[perf-gate] soft regression(s) — WARN (set SM_PERF_GATE=strict to fail)");
    }
    if fail {
        eprintln!("[perf-gate] regression gate FAILED (mode={gate})");
        std::process::exit(1);
    }
    println!("[perf-gate] OK");
}
