//! Criterion benchmarks for the latency-budgeted hot paths (AGENTS.md
//! invariant 6). Default scale is CI-sized; set `GRIT_BENCH_NODES` /
//! `GRIT_BENCH_EDGES` to `100000` / `1000000` for the full envelope run
//! (see `examples/quickbench.rs` for a one-shot wall-clock report).

use std::sync::Arc;

use criterion::{Criterion, criterion_group, criterion_main};
use grit_core::{Budget, GraphOp, Grit, ManualClock, Options, Query, Traversal};
use uuid::Uuid;

struct Rng(u64);

impl Rng {
    fn next(&mut self) -> u64 {
        self.0 ^= self.0 << 13;
        self.0 ^= self.0 >> 7;
        self.0 ^= self.0 << 17;
        self.0
    }

    fn below(&mut self, n: usize) -> usize {
        (self.next() % n as u64) as usize
    }
}

const WORDS: &[&str] = &[
    "yoneda",
    "functor",
    "kernel",
    "image",
    "exact",
    "sequence",
    "lemma",
    "sheaf",
    "topos",
    "adjoint",
    "limit",
    "colimit",
    "monad",
    "algebra",
    "module",
    "tensor",
    "homology",
    "cohomology",
    "spectral",
    "fibration",
    "bundle",
    "manifold",
    "scheme",
    "variety",
    "ideal",
    "ring",
    "field",
    "group",
    "torsion",
    "lattice",
];

struct Fixture {
    _dir: tempfile::TempDir,
    g: Grit,
    node_ids: Vec<Uuid>,
}

fn env_scale(var: &str, default: usize) -> usize {
    std::env::var(var)
        .ok()
        .and_then(|s| s.parse().ok())
        .unwrap_or(default)
}

fn build_fixture() -> Fixture {
    let n_nodes = env_scale("GRIT_BENCH_NODES", 20_000);
    let n_edges = env_scale("GRIT_BENCH_EDGES", 60_000);
    let dir = tempfile::tempdir().unwrap();
    let clock = Arc::new(ManualClock::new(1_000_000_000));
    let g = Grit::open(
        dir.path().join("bench.db"),
        Options::new("bench").clock(clock),
    )
    .unwrap();
    let mut rng = Rng(0x9E37_79B9_7F4A_7C15);

    let mut node_ids = Vec::with_capacity(n_nodes);
    for i in 0..n_nodes {
        let id = g.new_id();
        let (w1, w2, w3) = (
            WORDS[rng.below(WORDS.len())],
            WORDS[rng.below(WORDS.len())],
            WORDS[rng.below(WORDS.len())],
        );
        g.apply(GraphOp::AddNode {
            id,
            kind: "concept".into(),
            name: format!("{w1} {w2} {i}"),
            summary: format!("the {w3} in context {i}"),
            attrs: serde_json::json!({}),
            group_id: format!("g{}", i % 8),
        })
        .unwrap();
        node_ids.push(id);
    }
    let mut edge_ids = Vec::with_capacity(n_edges);
    for i in 0..n_edges {
        let id = g.new_id();
        let (w1, w2) = (WORDS[rng.below(WORDS.len())], WORDS[rng.below(WORDS.len())]);
        g.apply(GraphOp::AddEdge {
            id,
            src: node_ids[rng.below(n_nodes)],
            dst: node_ids[rng.below(n_nodes)],
            rel: "RELATES".into(),
            fact: format!("the {w1} constrains the {w2} ({i})"),
            attrs: serde_json::json!({}),
            group_id: format!("g{}", i % 8),
            valid_at: Some(1_000),
        })
        .unwrap();
        edge_ids.push(id);
    }
    for _ in 0..n_edges / 20 {
        g.apply(GraphOp::InvalidateEdge {
            edge_id: edge_ids[rng.below(n_edges)],
            invalid_at: 500_000_000,
        })
        .unwrap();
    }
    Fixture {
        _dir: dir,
        g,
        node_ids,
    }
}

fn hot_paths(c: &mut Criterion) {
    let f = build_fixture();
    let mut rng = Rng(0xDEAD_BEEF_CAFE_F00D);

    c.bench_function("traverse_depth3_budget256", |b| {
        b.iter(|| {
            let seed = f.node_ids[rng.below(f.node_ids.len())];
            f.g.traverse(&[seed], &Traversal::default().depth(3))
                .unwrap()
        })
    });

    c.bench_function("hybrid_search_items20", |b| {
        b.iter(|| {
            let (w1, w2) = (WORDS[rng.below(WORDS.len())], WORDS[rng.below(WORDS.len())]);
            f.g.search(Query::text(format!("{w1} {w2}")).budget(Budget::items(20)))
                .unwrap()
        })
    });

    c.bench_function("node_history", |b| {
        b.iter(|| {
            let id = f.node_ids[rng.below(f.node_ids.len())];
            f.g.node_history(id).unwrap()
        })
    });

    c.bench_function("apply_add_node", |b| {
        let mut i = 0u64;
        b.iter(|| {
            i += 1;
            f.g.apply(GraphOp::AddNode {
                id: f.g.new_id(),
                kind: "bench".into(),
                name: format!("appended {i}"),
                summary: String::new(),
                attrs: serde_json::json!({}),
                group_id: "bench".into(),
            })
            .unwrap()
        })
    });
}

criterion_group!(benches, hot_paths);
criterion_main!(benches);
