#![cfg(target_arch = "wasm32")]

use std::cell::Cell;

use serde::Serialize;
use wasm_bindgen::prelude::*;

use crate::file::{OpfsIoCounters, opfs_io_counters_reset, opfs_io_counters_snapshot};
use crate::{BTreeOptions, OpfsBTree, OpfsFile};

#[derive(Debug, Clone, Serialize)]
struct PhaseTiming {
    phase: String,
    elapsed_ms: f64,
}

#[derive(Debug, Clone, Serialize)]
struct BenchmarkResult {
    operation: String,
    value_size: u32,
    count: u32,
    seed: u64,
    wall_elapsed_ms: f64,
    elapsed_ms: f64,
    ops_per_sec: f64,
    p95_op_ms: f64,
    reads: u32,
    read_hits: u32,
    read_misses: u32,
    writes: u32,
    deletes: u32,
    checksum: u64,
    phase_times_ms: Vec<PhaseTiming>,
    opfs_io_counters: OpfsIoCounters,
}

#[derive(Debug, Clone, Copy)]
struct MixedScenario {
    name: &'static str,
    read_pct: u8,
    write_pct: u8,
    update_pct: u8,
}

const MIXED_SCENARIOS: [MixedScenario; 3] = [
    MixedScenario {
        name: "mixed_random_70r_30w",
        read_pct: 70,
        write_pct: 30,
        update_pct: 80,
    },
    MixedScenario {
        name: "mixed_random_50r_50w_with_updates",
        read_pct: 50,
        write_pct: 50,
        update_pct: 90,
    },
    MixedScenario {
        name: "mixed_random_60r_20w_20d",
        read_pct: 60,
        write_pct: 20,
        update_pct: 80,
    },
];

const DEFAULT_BASE_SEED: u64 = 0xA5A5_A5A5_0123_4567;
const DEFAULT_BENCH_CACHE_BYTES: usize = 32 * 1024 * 1024;
const DEFAULT_BENCH_OVERFLOW_THRESHOLD: usize = 4 * 1024;
const DEFAULT_PIN_INTERNAL_PAGES: bool = true;
const DEFAULT_READ_COALESCE_PAGES: usize = 4;
const RANGE_WINDOW_KEYS: usize = 128;
const RANGE_RESULT_LIMIT: usize = 64;

thread_local! {
    static BENCH_CACHE_BYTES: Cell<usize> = const { Cell::new(DEFAULT_BENCH_CACHE_BYTES) };
    static BENCH_OVERFLOW_THRESHOLD: Cell<usize> = const { Cell::new(DEFAULT_BENCH_OVERFLOW_THRESHOLD) };
    static BENCH_PIN_INTERNAL_PAGES: Cell<bool> = const { Cell::new(DEFAULT_PIN_INTERNAL_PAGES) };
    static BENCH_READ_COALESCE_PAGES: Cell<usize> = const { Cell::new(DEFAULT_READ_COALESCE_PAGES) };
}

#[derive(Debug, Clone, Copy)]
enum OpChoice {
    Read,
    Write,
    Delete,
}

#[derive(Debug, Clone)]
struct DeterministicRng {
    state: u64,
}

impl DeterministicRng {
    fn new(seed: u64) -> Self {
        Self { state: seed }
    }

    fn next_u64(&mut self) -> u64 {
        self.state = self.state.wrapping_mul(6364136223846793005).wrapping_add(1);
        self.state
    }

    fn next_u8(&mut self) -> u8 {
        (self.next_u64() >> 56) as u8
    }

    fn next_usize(&mut self, upper: usize) -> usize {
        if upper == 0 {
            return 0;
        }
        (self.next_u64() as usize) % upper
    }
}

fn benchmark_options() -> BTreeOptions {
    BTreeOptions {
        page_size: 16 * 1024,
        cache_bytes: BENCH_CACHE_BYTES.with(|bytes| bytes.get()),
        overflow_threshold: BENCH_OVERFLOW_THRESHOLD.with(|bytes| bytes.get()),
        pin_internal_pages: BENCH_PIN_INTERNAL_PAGES.with(|flag| flag.get()),
        read_coalesce_pages: BENCH_READ_COALESCE_PAGES.with(|pages| pages.get()),
    }
}

fn key(i: usize) -> Vec<u8> {
    format!("k{i:08}").into_bytes()
}

fn value(size: usize, seed: u8) -> Vec<u8> {
    let mut out = vec![0u8; size];
    for (i, byte) in out.iter_mut().enumerate() {
        *byte = seed.wrapping_add((i % 251) as u8);
    }
    out
}

fn shuffled_indices(n: usize, seed: u64) -> Vec<usize> {
    let mut out: Vec<usize> = (0..n).collect();
    let mut state: u64 = 0xD1B54A32D192ED03 ^ seed;
    for i in (1..n).rev() {
        state = state.wrapping_mul(6364136223846793005).wrapping_add(1);
        let j = (state as usize) % (i + 1);
        out.swap(i, j);
    }
    out
}

fn derive_seed(base_seed: u64, label: &str, value_size: u32) -> u64 {
    const MAX_JS_SAFE_INT: u64 = 9_007_199_254_740_991;
    let mut h = 0xcbf2_9ce4_8422_2325u64 ^ base_seed ^ (value_size as u64);
    for &b in label.as_bytes() {
        h ^= b as u64;
        h = h.wrapping_mul(0x0000_0100_0000_01b3);
    }
    let mut derived =
        (h ^ ((value_size as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15))) % MAX_JS_SAFE_INT;
    if derived == 0 {
        derived = 1;
    }
    derived
}

fn unique_namespace(label: &str) -> String {
    let ts = js_sys::Date::now() as u64;
    let rand = (js_sys::Math::random() * 1_000_000.0) as u64;
    format!("bench-{label}-{ts}-{rand}")
}

fn high_res_now_ms() -> f64 {
    let global = js_sys::global();
    let perf_key = JsValue::from_str("performance");
    if let Ok(perf) = js_sys::Reflect::get(&global, &perf_key)
        && !perf.is_undefined()
        && !perf.is_null()
    {
        let now_key = JsValue::from_str("now");
        if let Ok(now_fn) = js_sys::Reflect::get(&perf, &now_key)
            && let Some(now_fn) = now_fn.dyn_ref::<js_sys::Function>()
            && let Ok(v) = now_fn.call0(&perf)
            && let Some(ms) = v.as_f64()
        {
            return ms;
        }
    }

    js_sys::Date::now()
}

fn push_phase(phase_times: &mut Vec<PhaseTiming>, phase: &str, start_ms: f64) {
    phase_times.push(PhaseTiming {
        phase: phase.to_string(),
        elapsed_ms: high_res_now_ms() - start_ms,
    });
}

fn percentile_ms(latencies_ms: &mut [f64], percentile: f64) -> f64 {
    if latencies_ms.is_empty() {
        return 0.0;
    }

    latencies_ms.sort_by(|a, b| a.total_cmp(b));
    let pos = ((latencies_ms.len() as f64 * percentile).ceil() as usize)
        .saturating_sub(1)
        .min(latencies_ms.len() - 1);
    latencies_ms[pos]
}

fn choose_operation(scenario: MixedScenario, roll: u8) -> OpChoice {
    if roll < scenario.read_pct {
        return OpChoice::Read;
    }
    if roll < scenario.read_pct.saturating_add(scenario.write_pct) {
        return OpChoice::Write;
    }
    OpChoice::Delete
}

fn find_mixed_scenario(name: &str) -> Option<MixedScenario> {
    MIXED_SCENARIOS.iter().copied().find(|s| s.name == name)
}

async fn open_db(namespace: &str) -> Result<OpfsBTree<OpfsFile>, JsValue> {
    let file = OpfsFile::open(namespace)
        .await
        .map_err(|e| JsValue::from_str(&e.to_string()))?;
    OpfsBTree::open(file, benchmark_options()).map_err(|e| JsValue::from_str(&e.to_string()))
}

fn to_js_value(result: &BenchmarkResult) -> Result<JsValue, JsValue> {
    serde_wasm_bindgen::to_value(result)
        .map_err(|e| JsValue::from_str(&format!("serialize benchmark result: {e}")))
}

async fn run_seq_write(count: u32, value_size: u32) -> Result<BenchmarkResult, JsValue> {
    opfs_io_counters_reset();
    let mut phase_times_ms = Vec::new();
    let wall_start = high_res_now_ms();
    let namespace = unique_namespace("seq-write");
    let phase_start = high_res_now_ms();
    OpfsFile::destroy(&namespace)
        .await
        .map_err(|e| JsValue::from_str(&e.to_string()))?;
    push_phase(&mut phase_times_ms, "cleanup_destroy", phase_start);

    let phase_start = high_res_now_ms();
    let mut db = open_db(&namespace).await?;
    push_phase(&mut phase_times_ms, "open_db", phase_start);
    let size = value_size as usize;
    let seed = derive_seed(DEFAULT_BASE_SEED, "seq_write", value_size);

    let io_start = opfs_io_counters_snapshot();
    let op_start = high_res_now_ms();
    let mut checksum = 0u64;
    for i in 0..(count as usize) {
        let k = key(i);
        let v = value(size, (i % 251) as u8);
        checksum = checksum.wrapping_add(v[0] as u64);
        db.put(&k, &v)
            .map_err(|e| JsValue::from_str(&e.to_string()))?;
    }
    push_phase(&mut phase_times_ms, "op_put_loop", op_start);

    let flush_start = high_res_now_ms();
    db.checkpoint()
        .map_err(|e| JsValue::from_str(&e.to_string()))?;
    push_phase(&mut phase_times_ms, "final_checkpoint", flush_start);
    let elapsed_ms = high_res_now_ms() - op_start;
    let opfs_io_counters = opfs_io_counters_snapshot().delta_since(io_start);

    drop(db);
    let phase_start = high_res_now_ms();
    OpfsFile::destroy(&namespace)
        .await
        .map_err(|e| JsValue::from_str(&e.to_string()))?;
    push_phase(&mut phase_times_ms, "teardown_destroy", phase_start);
    let wall_elapsed_ms = high_res_now_ms() - wall_start;

    Ok(BenchmarkResult {
        operation: "seq_write".to_string(),
        value_size,
        count,
        seed,
        wall_elapsed_ms,
        elapsed_ms,
        ops_per_sec: (count as f64) / (elapsed_ms / 1000.0),
        p95_op_ms: 0.0,
        reads: 0,
        read_hits: 0,
        read_misses: 0,
        writes: count,
        deletes: 0,
        checksum,
        phase_times_ms,
        opfs_io_counters,
    })
}

async fn run_random_write(count: u32, value_size: u32) -> Result<BenchmarkResult, JsValue> {
    opfs_io_counters_reset();
    let mut phase_times_ms = Vec::new();
    let wall_start = high_res_now_ms();
    let namespace = unique_namespace("rand-write");
    let phase_start = high_res_now_ms();
    OpfsFile::destroy(&namespace)
        .await
        .map_err(|e| JsValue::from_str(&e.to_string()))?;
    push_phase(&mut phase_times_ms, "cleanup_destroy", phase_start);

    let phase_start = high_res_now_ms();
    let mut db = open_db(&namespace).await?;
    push_phase(&mut phase_times_ms, "open_db", phase_start);
    let size = value_size as usize;
    let seed = derive_seed(DEFAULT_BASE_SEED, "random_write", value_size);
    let order = shuffled_indices(count as usize, seed);

    let io_start = opfs_io_counters_snapshot();
    let op_start = high_res_now_ms();
    let mut checksum = 0u64;
    for &i in &order {
        let k = key(i);
        let v = value(size, (i % 251) as u8);
        checksum = checksum.wrapping_add(v[0] as u64);
        db.put(&k, &v)
            .map_err(|e| JsValue::from_str(&e.to_string()))?;
    }
    push_phase(&mut phase_times_ms, "op_put_loop", op_start);

    let flush_start = high_res_now_ms();
    db.checkpoint()
        .map_err(|e| JsValue::from_str(&e.to_string()))?;
    push_phase(&mut phase_times_ms, "final_checkpoint", flush_start);
    let elapsed_ms = high_res_now_ms() - op_start;
    let opfs_io_counters = opfs_io_counters_snapshot().delta_since(io_start);

    drop(db);
    let phase_start = high_res_now_ms();
    OpfsFile::destroy(&namespace)
        .await
        .map_err(|e| JsValue::from_str(&e.to_string()))?;
    push_phase(&mut phase_times_ms, "teardown_destroy", phase_start);
    let wall_elapsed_ms = high_res_now_ms() - wall_start;

    Ok(BenchmarkResult {
        operation: "random_write".to_string(),
        value_size,
        count,
        seed,
        wall_elapsed_ms,
        elapsed_ms,
        ops_per_sec: (count as f64) / (elapsed_ms / 1000.0),
        p95_op_ms: 0.0,
        reads: 0,
        read_hits: 0,
        read_misses: 0,
        writes: count,
        deletes: 0,
        checksum,
        phase_times_ms,
        opfs_io_counters,
    })
}

async fn run_seq_read(count: u32, value_size: u32) -> Result<BenchmarkResult, JsValue> {
    opfs_io_counters_reset();
    let mut phase_times_ms = Vec::new();
    let wall_start = high_res_now_ms();
    let namespace = unique_namespace("seq-read");
    let phase_start = high_res_now_ms();
    OpfsFile::destroy(&namespace)
        .await
        .map_err(|e| JsValue::from_str(&e.to_string()))?;
    push_phase(&mut phase_times_ms, "cleanup_destroy", phase_start);

    let phase_start = high_res_now_ms();
    let mut db = open_db(&namespace).await?;
    push_phase(&mut phase_times_ms, "open_db", phase_start);
    let size = value_size as usize;
    let seed = derive_seed(DEFAULT_BASE_SEED, "seq_read", value_size);

    let prefill_puts_start = high_res_now_ms();
    for i in 0..(count as usize) {
        let k = key(i);
        let v = value(size, (i % 251) as u8);
        db.put(&k, &v)
            .map_err(|e| JsValue::from_str(&e.to_string()))?;
    }
    push_phase(&mut phase_times_ms, "prefill_put_loop", prefill_puts_start);
    let prefill_checkpoint_start = high_res_now_ms();
    db.checkpoint()
        .map_err(|e| JsValue::from_str(&e.to_string()))?;
    push_phase(
        &mut phase_times_ms,
        "prefill_checkpoint",
        prefill_checkpoint_start,
    );

    let io_start = opfs_io_counters_snapshot();
    let op_start = high_res_now_ms();
    let mut checksum = 0u64;
    for i in 0..(count as usize) {
        let k = key(i);
        let v = db
            .get(&k)
            .map_err(|e| JsValue::from_str(&e.to_string()))?
            .ok_or_else(|| JsValue::from_str("missing key during seq_read benchmark"))?;
        checksum = checksum.wrapping_add(v[0] as u64);
    }
    push_phase(&mut phase_times_ms, "op_read_loop", op_start);
    let elapsed_ms = high_res_now_ms() - op_start;
    let opfs_io_counters = opfs_io_counters_snapshot().delta_since(io_start);

    drop(db);
    let phase_start = high_res_now_ms();
    OpfsFile::destroy(&namespace)
        .await
        .map_err(|e| JsValue::from_str(&e.to_string()))?;
    push_phase(&mut phase_times_ms, "teardown_destroy", phase_start);
    let wall_elapsed_ms = high_res_now_ms() - wall_start;

    Ok(BenchmarkResult {
        operation: "seq_read".to_string(),
        value_size,
        count,
        seed,
        wall_elapsed_ms,
        elapsed_ms,
        ops_per_sec: (count as f64) / (elapsed_ms / 1000.0),
        p95_op_ms: 0.0,
        reads: count,
        read_hits: count,
        read_misses: 0,
        writes: 0,
        deletes: 0,
        checksum,
        phase_times_ms,
        opfs_io_counters,
    })
}

async fn run_random_read(count: u32, value_size: u32) -> Result<BenchmarkResult, JsValue> {
    opfs_io_counters_reset();
    let mut phase_times_ms = Vec::new();
    let wall_start = high_res_now_ms();
    let namespace = unique_namespace("rand-read");
    let phase_start = high_res_now_ms();
    OpfsFile::destroy(&namespace)
        .await
        .map_err(|e| JsValue::from_str(&e.to_string()))?;
    push_phase(&mut phase_times_ms, "cleanup_destroy", phase_start);

    let phase_start = high_res_now_ms();
    let mut db = open_db(&namespace).await?;
    push_phase(&mut phase_times_ms, "open_db", phase_start);
    let size = value_size as usize;
    let seed = derive_seed(DEFAULT_BASE_SEED, "random_read", value_size);

    let prefill_puts_start = high_res_now_ms();
    for i in 0..(count as usize) {
        let k = key(i);
        let v = value(size, (i % 251) as u8);
        db.put(&k, &v)
            .map_err(|e| JsValue::from_str(&e.to_string()))?;
    }
    push_phase(&mut phase_times_ms, "prefill_put_loop", prefill_puts_start);
    let prefill_checkpoint_start = high_res_now_ms();
    db.checkpoint()
        .map_err(|e| JsValue::from_str(&e.to_string()))?;
    push_phase(
        &mut phase_times_ms,
        "prefill_checkpoint",
        prefill_checkpoint_start,
    );

    let order = shuffled_indices(count as usize, seed);

    let io_start = opfs_io_counters_snapshot();
    let op_start = high_res_now_ms();
    let mut checksum = 0u64;
    for &i in &order {
        let k = key(i);
        let v = db
            .get(&k)
            .map_err(|e| JsValue::from_str(&e.to_string()))?
            .ok_or_else(|| JsValue::from_str("missing key during random_read benchmark"))?;
        checksum = checksum.wrapping_add(v[0] as u64);
    }
    push_phase(&mut phase_times_ms, "op_read_loop", op_start);
    let elapsed_ms = high_res_now_ms() - op_start;
    let opfs_io_counters = opfs_io_counters_snapshot().delta_since(io_start);

    drop(db);
    let phase_start = high_res_now_ms();
    OpfsFile::destroy(&namespace)
        .await
        .map_err(|e| JsValue::from_str(&e.to_string()))?;
    push_phase(&mut phase_times_ms, "teardown_destroy", phase_start);
    let wall_elapsed_ms = high_res_now_ms() - wall_start;

    Ok(BenchmarkResult {
        operation: "random_read".to_string(),
        value_size,
        count,
        seed,
        wall_elapsed_ms,
        elapsed_ms,
        ops_per_sec: (count as f64) / (elapsed_ms / 1000.0),
        p95_op_ms: 0.0,
        reads: count,
        read_hits: count,
        read_misses: 0,
        writes: 0,
        deletes: 0,
        checksum,
        phase_times_ms,
        opfs_io_counters,
    })
}

async fn run_cold_seq_read(count: u32, value_size: u32) -> Result<BenchmarkResult, JsValue> {
    opfs_io_counters_reset();
    let mut phase_times_ms = Vec::new();
    let wall_start = high_res_now_ms();
    let namespace = unique_namespace("cold-seq-read");
    let phase_start = high_res_now_ms();
    OpfsFile::destroy(&namespace)
        .await
        .map_err(|e| JsValue::from_str(&e.to_string()))?;
    push_phase(&mut phase_times_ms, "cleanup_destroy", phase_start);

    let phase_start = high_res_now_ms();
    let mut db = open_db(&namespace).await?;
    push_phase(&mut phase_times_ms, "open_db_prefill", phase_start);
    let size = value_size as usize;
    let seed = derive_seed(DEFAULT_BASE_SEED, "cold_seq_read", value_size);

    let prefill_puts_start = high_res_now_ms();
    for i in 0..(count as usize) {
        let k = key(i);
        let v = value(size, (i % 251) as u8);
        db.put(&k, &v)
            .map_err(|e| JsValue::from_str(&e.to_string()))?;
    }
    push_phase(&mut phase_times_ms, "prefill_put_loop", prefill_puts_start);
    let prefill_checkpoint_start = high_res_now_ms();
    db.checkpoint()
        .map_err(|e| JsValue::from_str(&e.to_string()))?;
    push_phase(
        &mut phase_times_ms,
        "prefill_checkpoint",
        prefill_checkpoint_start,
    );
    drop(db);

    // Cold-read window includes reopen + read loop.
    let io_start = opfs_io_counters_snapshot();
    let op_start = high_res_now_ms();
    let reopen_start = high_res_now_ms();
    let mut db = open_db(&namespace).await?;
    push_phase(&mut phase_times_ms, "reopen_db_for_cold_read", reopen_start);

    let mut checksum = 0u64;
    for i in 0..(count as usize) {
        let k = key(i);
        let v = db
            .get(&k)
            .map_err(|e| JsValue::from_str(&e.to_string()))?
            .ok_or_else(|| JsValue::from_str("missing key during cold_seq_read benchmark"))?;
        checksum = checksum.wrapping_add(v[0] as u64);
    }
    push_phase(&mut phase_times_ms, "op_read_loop", op_start);
    let elapsed_ms = high_res_now_ms() - op_start;
    let opfs_io_counters = opfs_io_counters_snapshot().delta_since(io_start);

    drop(db);
    let phase_start = high_res_now_ms();
    OpfsFile::destroy(&namespace)
        .await
        .map_err(|e| JsValue::from_str(&e.to_string()))?;
    push_phase(&mut phase_times_ms, "teardown_destroy", phase_start);
    let wall_elapsed_ms = high_res_now_ms() - wall_start;

    Ok(BenchmarkResult {
        operation: "cold_seq_read".to_string(),
        value_size,
        count,
        seed,
        wall_elapsed_ms,
        elapsed_ms,
        ops_per_sec: (count as f64) / (elapsed_ms / 1000.0),
        p95_op_ms: 0.0,
        reads: count,
        read_hits: count,
        read_misses: 0,
        writes: 0,
        deletes: 0,
        checksum,
        phase_times_ms,
        opfs_io_counters,
    })
}

async fn run_cold_random_read(count: u32, value_size: u32) -> Result<BenchmarkResult, JsValue> {
    opfs_io_counters_reset();
    let mut phase_times_ms = Vec::new();
    let wall_start = high_res_now_ms();
    let namespace = unique_namespace("cold-rand-read");
    let phase_start = high_res_now_ms();
    OpfsFile::destroy(&namespace)
        .await
        .map_err(|e| JsValue::from_str(&e.to_string()))?;
    push_phase(&mut phase_times_ms, "cleanup_destroy", phase_start);

    let phase_start = high_res_now_ms();
    let mut db = open_db(&namespace).await?;
    push_phase(&mut phase_times_ms, "open_db_prefill", phase_start);
    let size = value_size as usize;
    let seed = derive_seed(DEFAULT_BASE_SEED, "cold_random_read", value_size);

    let prefill_puts_start = high_res_now_ms();
    for i in 0..(count as usize) {
        let k = key(i);
        let v = value(size, (i % 251) as u8);
        db.put(&k, &v)
            .map_err(|e| JsValue::from_str(&e.to_string()))?;
    }
    push_phase(&mut phase_times_ms, "prefill_put_loop", prefill_puts_start);
    let prefill_checkpoint_start = high_res_now_ms();
    db.checkpoint()
        .map_err(|e| JsValue::from_str(&e.to_string()))?;
    push_phase(
        &mut phase_times_ms,
        "prefill_checkpoint",
        prefill_checkpoint_start,
    );
    drop(db);

    // Cold-read window includes reopen + read loop.
    let io_start = opfs_io_counters_snapshot();
    let op_start = high_res_now_ms();
    let reopen_start = high_res_now_ms();
    let mut db = open_db(&namespace).await?;
    push_phase(&mut phase_times_ms, "reopen_db_for_cold_read", reopen_start);

    let order = shuffled_indices(count as usize, seed);
    let mut checksum = 0u64;
    for &i in &order {
        let k = key(i);
        let v = db
            .get(&k)
            .map_err(|e| JsValue::from_str(&e.to_string()))?
            .ok_or_else(|| JsValue::from_str("missing key during cold_random_read benchmark"))?;
        checksum = checksum.wrapping_add(v[0] as u64);
    }
    push_phase(&mut phase_times_ms, "op_read_loop", op_start);
    let elapsed_ms = high_res_now_ms() - op_start;
    let opfs_io_counters = opfs_io_counters_snapshot().delta_since(io_start);

    drop(db);
    let phase_start = high_res_now_ms();
    OpfsFile::destroy(&namespace)
        .await
        .map_err(|e| JsValue::from_str(&e.to_string()))?;
    push_phase(&mut phase_times_ms, "teardown_destroy", phase_start);
    let wall_elapsed_ms = high_res_now_ms() - wall_start;

    Ok(BenchmarkResult {
        operation: "cold_random_read".to_string(),
        value_size,
        count,
        seed,
        wall_elapsed_ms,
        elapsed_ms,
        ops_per_sec: (count as f64) / (elapsed_ms / 1000.0),
        p95_op_ms: 0.0,
        reads: count,
        read_hits: count,
        read_misses: 0,
        writes: 0,
        deletes: 0,
        checksum,
        phase_times_ms,
        opfs_io_counters,
    })
}

async fn run_mixed_scenario(
    scenario: MixedScenario,
    count: u32,
    value_size: u32,
    base_seed: u64,
) -> Result<BenchmarkResult, JsValue> {
    opfs_io_counters_reset();
    let mut phase_times_ms = Vec::new();
    let wall_start = high_res_now_ms();
    let namespace = unique_namespace(scenario.name);
    let phase_start = high_res_now_ms();
    OpfsFile::destroy(&namespace)
        .await
        .map_err(|e| JsValue::from_str(&e.to_string()))?;
    push_phase(&mut phase_times_ms, "cleanup_destroy", phase_start);

    let phase_start = high_res_now_ms();
    let mut db = open_db(&namespace).await?;
    push_phase(&mut phase_times_ms, "open_db", phase_start);
    let size = value_size as usize;
    let seed = derive_seed(base_seed, scenario.name, value_size);
    let initial_key_space = (count as usize).max(1);

    let prefill_puts_start = high_res_now_ms();
    for i in 0..initial_key_space {
        let k = key(i);
        let v = value(size, (i % 251) as u8);
        db.put(&k, &v)
            .map_err(|e| JsValue::from_str(&e.to_string()))?;
    }
    push_phase(&mut phase_times_ms, "prefill_put_loop", prefill_puts_start);
    let prefill_checkpoint_start = high_res_now_ms();
    db.checkpoint()
        .map_err(|e| JsValue::from_str(&e.to_string()))?;
    push_phase(
        &mut phase_times_ms,
        "prefill_checkpoint",
        prefill_checkpoint_start,
    );

    let mut rng = DeterministicRng::new(seed);
    let mut key_space = initial_key_space;

    let mut reads = 0u32;
    let mut read_hits = 0u32;
    let mut read_misses = 0u32;
    let mut writes = 0u32;
    let mut deletes = 0u32;
    let mut checksum = 0u64;
    let mut op_latencies_ms = Vec::with_capacity(count as usize);

    let io_start = opfs_io_counters_snapshot();
    let mixed_start = high_res_now_ms();
    for step in 0..(count as usize) {
        let op = choose_operation(scenario, rng.next_u8() % 100);
        let per_op_start = high_res_now_ms();

        match op {
            OpChoice::Read => {
                reads += 1;
                let idx = rng.next_usize(key_space.max(1));
                let k = key(idx);
                let maybe = db.get(&k).map_err(|e| JsValue::from_str(&e.to_string()))?;
                if let Some(v) = maybe {
                    read_hits += 1;
                    checksum = checksum.wrapping_add(v[0] as u64);
                } else {
                    read_misses += 1;
                    checksum = checksum.wrapping_add(1);
                }
            }
            OpChoice::Write => {
                writes += 1;
                let update = (rng.next_u8() % 100) < scenario.update_pct;
                let idx = if update || key_space == 0 {
                    rng.next_usize(key_space.max(1))
                } else {
                    let i = key_space;
                    key_space += 1;
                    i
                };
                let k = key(idx);
                let v = value(size, ((step + idx) % 251) as u8);
                checksum = checksum.wrapping_add(v[0] as u64);
                db.put(&k, &v)
                    .map_err(|e| JsValue::from_str(&e.to_string()))?;
            }
            OpChoice::Delete => {
                deletes += 1;
                let idx = rng.next_usize(key_space.max(1));
                let k = key(idx);
                db.delete(&k)
                    .map_err(|e| JsValue::from_str(&e.to_string()))?;
                checksum = checksum.wrapping_add(idx as u64);
            }
        }

        op_latencies_ms.push(high_res_now_ms() - per_op_start);
    }
    push_phase(&mut phase_times_ms, "op_mixed_loop", mixed_start);

    let checkpoint_start = high_res_now_ms();
    db.checkpoint()
        .map_err(|e| JsValue::from_str(&e.to_string()))?;
    push_phase(&mut phase_times_ms, "final_checkpoint", checkpoint_start);

    let elapsed_ms = high_res_now_ms() - mixed_start;
    let opfs_io_counters = opfs_io_counters_snapshot().delta_since(io_start);

    drop(db);
    let phase_start = high_res_now_ms();
    OpfsFile::destroy(&namespace)
        .await
        .map_err(|e| JsValue::from_str(&e.to_string()))?;
    push_phase(&mut phase_times_ms, "teardown_destroy", phase_start);
    let wall_elapsed_ms = high_res_now_ms() - wall_start;

    Ok(BenchmarkResult {
        operation: scenario.name.to_string(),
        value_size,
        count,
        seed,
        wall_elapsed_ms,
        elapsed_ms,
        ops_per_sec: (count as f64) / (elapsed_ms / 1000.0),
        p95_op_ms: percentile_ms(&mut op_latencies_ms, 0.95),
        reads,
        read_hits,
        read_misses,
        writes,
        deletes,
        checksum,
        phase_times_ms,
        opfs_io_counters,
    })
}

async fn run_range_seq_window(count: u32, value_size: u32) -> Result<BenchmarkResult, JsValue> {
    run_range_scan(count, value_size, false).await
}

async fn run_range_random_window(count: u32, value_size: u32) -> Result<BenchmarkResult, JsValue> {
    run_range_scan(count, value_size, true).await
}

async fn run_range_scan(
    count: u32,
    value_size: u32,
    random_start: bool,
) -> Result<BenchmarkResult, JsValue> {
    opfs_io_counters_reset();
    let mut phase_times_ms = Vec::new();
    let wall_start = high_res_now_ms();
    let label = if random_start {
        "range-random-window"
    } else {
        "range-seq-window"
    };
    let operation = if random_start {
        "range_random_window_64"
    } else {
        "range_seq_window_64"
    };
    let namespace = unique_namespace(label);
    let phase_start = high_res_now_ms();
    OpfsFile::destroy(&namespace)
        .await
        .map_err(|e| JsValue::from_str(&e.to_string()))?;
    push_phase(&mut phase_times_ms, "cleanup_destroy", phase_start);

    let phase_start = high_res_now_ms();
    let mut db = open_db(&namespace).await?;
    push_phase(&mut phase_times_ms, "open_db", phase_start);
    let size = value_size as usize;
    let seed = derive_seed(DEFAULT_BASE_SEED, operation, value_size);
    let key_space = ((count as usize) * 4)
        .max(RANGE_WINDOW_KEYS + 1)
        .max(RANGE_RESULT_LIMIT + 1);

    let prefill_puts_start = high_res_now_ms();
    for i in 0..key_space {
        let k = key(i);
        let v = value(size, (i % 251) as u8);
        db.put(&k, &v)
            .map_err(|e| JsValue::from_str(&e.to_string()))?;
    }
    push_phase(&mut phase_times_ms, "prefill_put_loop", prefill_puts_start);
    let prefill_checkpoint_start = high_res_now_ms();
    db.checkpoint()
        .map_err(|e| JsValue::from_str(&e.to_string()))?;
    push_phase(
        &mut phase_times_ms,
        "prefill_checkpoint",
        prefill_checkpoint_start,
    );

    let max_start = key_space.saturating_sub(RANGE_WINDOW_KEYS + 1);
    let mut rng = DeterministicRng::new(seed);
    let mut op_latencies_ms = Vec::with_capacity(count as usize);
    let mut checksum = 0u64;
    let mut read_hits = 0u32;
    let mut read_misses = 0u32;

    let io_start = opfs_io_counters_snapshot();
    let op_start = high_res_now_ms();
    for i in 0..(count as usize) {
        let per_op_start = high_res_now_ms();
        let start_idx = if random_start {
            rng.next_usize(max_start.max(1))
        } else {
            (i.saturating_mul(7)) % max_start.max(1)
        };
        let end_idx = start_idx + RANGE_WINDOW_KEYS;
        let start = key(start_idx);
        let end = key(end_idx);

        let rows = db
            .range(&start, &end, RANGE_RESULT_LIMIT)
            .map_err(|e| JsValue::from_str(&e.to_string()))?;
        if rows.is_empty() {
            read_misses = read_misses.saturating_add(1);
            checksum = checksum.wrapping_add(start_idx as u64);
        } else {
            read_hits = read_hits.saturating_add(1);
            checksum = checksum.wrapping_add(rows.len() as u64);
            for (_, value) in rows {
                checksum = checksum.wrapping_add(value[0] as u64);
            }
        }
        op_latencies_ms.push(high_res_now_ms() - per_op_start);
    }
    push_phase(&mut phase_times_ms, "op_range_loop", op_start);
    let elapsed_ms = high_res_now_ms() - op_start;
    let opfs_io_counters = opfs_io_counters_snapshot().delta_since(io_start);

    drop(db);
    let phase_start = high_res_now_ms();
    OpfsFile::destroy(&namespace)
        .await
        .map_err(|e| JsValue::from_str(&e.to_string()))?;
    push_phase(&mut phase_times_ms, "teardown_destroy", phase_start);
    let wall_elapsed_ms = high_res_now_ms() - wall_start;

    Ok(BenchmarkResult {
        operation: operation.to_string(),
        value_size,
        count,
        seed,
        wall_elapsed_ms,
        elapsed_ms,
        ops_per_sec: (count as f64) / (elapsed_ms / 1000.0),
        p95_op_ms: percentile_ms(&mut op_latencies_ms, 0.95),
        reads: count,
        read_hits,
        read_misses,
        writes: 0,
        deletes: 0,
        checksum,
        phase_times_ms,
        opfs_io_counters,
    })
}

#[wasm_bindgen]
pub async fn bench_opfs_sequential_write(count: u32, value_size: u32) -> Result<JsValue, JsValue> {
    to_js_value(&run_seq_write(count, value_size).await?)
}

#[wasm_bindgen]
pub fn bench_set_cache_bytes(cache_bytes: u32) -> Result<(), JsValue> {
    let cache_bytes = cache_bytes as usize;
    if cache_bytes < 16 * 1024 {
        return Err(JsValue::from_str(
            "cache_bytes must be at least one page (16384 bytes)",
        ));
    }

    BENCH_CACHE_BYTES.with(|bytes| bytes.set(cache_bytes));
    Ok(())
}

#[wasm_bindgen]
pub fn bench_get_cache_bytes() -> u32 {
    BENCH_CACHE_BYTES.with(|bytes| bytes.get() as u32)
}

#[wasm_bindgen]
pub fn bench_reset_cache_bytes() {
    BENCH_CACHE_BYTES.with(|bytes| bytes.set(DEFAULT_BENCH_CACHE_BYTES));
}

#[wasm_bindgen]
pub fn bench_set_pin_internal_pages(pin_internal_pages: bool) {
    BENCH_PIN_INTERNAL_PAGES.with(|flag| flag.set(pin_internal_pages));
}

#[wasm_bindgen]
pub fn bench_get_pin_internal_pages() -> bool {
    BENCH_PIN_INTERNAL_PAGES.with(|flag| flag.get())
}

#[wasm_bindgen]
pub fn bench_reset_pin_internal_pages() {
    BENCH_PIN_INTERNAL_PAGES.with(|flag| flag.set(DEFAULT_PIN_INTERNAL_PAGES));
}

#[wasm_bindgen]
pub fn bench_set_overflow_threshold_bytes(overflow_threshold_bytes: u32) -> Result<(), JsValue> {
    let overflow_threshold_bytes = overflow_threshold_bytes as usize;
    if overflow_threshold_bytes == 0 {
        return Err(JsValue::from_str("overflow_threshold_bytes must be > 0"));
    }
    BENCH_OVERFLOW_THRESHOLD.with(|bytes| bytes.set(overflow_threshold_bytes));
    Ok(())
}

#[wasm_bindgen]
pub fn bench_get_overflow_threshold_bytes() -> u32 {
    BENCH_OVERFLOW_THRESHOLD.with(|bytes| bytes.get() as u32)
}

#[wasm_bindgen]
pub fn bench_reset_overflow_threshold_bytes() {
    BENCH_OVERFLOW_THRESHOLD.with(|bytes| bytes.set(DEFAULT_BENCH_OVERFLOW_THRESHOLD));
}

#[wasm_bindgen]
pub fn bench_set_read_coalesce_pages(read_coalesce_pages: u32) -> Result<(), JsValue> {
    let read_coalesce_pages = read_coalesce_pages as usize;
    if read_coalesce_pages == 0 {
        return Err(JsValue::from_str("read_coalesce_pages must be > 0"));
    }
    BENCH_READ_COALESCE_PAGES.with(|pages| pages.set(read_coalesce_pages));
    Ok(())
}

#[wasm_bindgen]
pub fn bench_get_read_coalesce_pages() -> u32 {
    BENCH_READ_COALESCE_PAGES.with(|pages| pages.get() as u32)
}

#[wasm_bindgen]
pub fn bench_reset_read_coalesce_pages() {
    BENCH_READ_COALESCE_PAGES.with(|pages| pages.set(DEFAULT_READ_COALESCE_PAGES));
}

#[wasm_bindgen]
pub async fn bench_opfs_random_write(count: u32, value_size: u32) -> Result<JsValue, JsValue> {
    to_js_value(&run_random_write(count, value_size).await?)
}

#[wasm_bindgen]
pub async fn bench_opfs_sequential_read(count: u32, value_size: u32) -> Result<JsValue, JsValue> {
    to_js_value(&run_seq_read(count, value_size).await?)
}

#[wasm_bindgen]
pub async fn bench_opfs_random_read(count: u32, value_size: u32) -> Result<JsValue, JsValue> {
    to_js_value(&run_random_read(count, value_size).await?)
}

#[wasm_bindgen]
pub async fn bench_opfs_cold_sequential_read(
    count: u32,
    value_size: u32,
) -> Result<JsValue, JsValue> {
    to_js_value(&run_cold_seq_read(count, value_size).await?)
}

#[wasm_bindgen]
pub async fn bench_opfs_cold_random_read(count: u32, value_size: u32) -> Result<JsValue, JsValue> {
    to_js_value(&run_cold_random_read(count, value_size).await?)
}

#[wasm_bindgen]
pub async fn bench_opfs_mixed_scenario(
    scenario_name: String,
    count: u32,
    value_size: u32,
    base_seed: Option<u64>,
) -> Result<JsValue, JsValue> {
    let scenario = find_mixed_scenario(&scenario_name)
        .ok_or_else(|| JsValue::from_str(&format!("unknown mixed scenario: {scenario_name}")))?;
    to_js_value(
        &run_mixed_scenario(
            scenario,
            count,
            value_size,
            base_seed.unwrap_or(DEFAULT_BASE_SEED),
        )
        .await?,
    )
}

#[wasm_bindgen]
pub async fn bench_opfs_range_seq_window(count: u32, value_size: u32) -> Result<JsValue, JsValue> {
    to_js_value(&run_range_seq_window(count, value_size).await?)
}

#[wasm_bindgen]
pub async fn bench_opfs_range_random_window(
    count: u32,
    value_size: u32,
) -> Result<JsValue, JsValue> {
    to_js_value(&run_range_random_window(count, value_size).await?)
}

#[wasm_bindgen]
pub async fn bench_opfs_matrix(count: u32) -> Result<JsValue, JsValue> {
    let sizes = [32u32, 256u32, 4096u32];
    let mut out = Vec::new();

    for value_size in sizes {
        out.push(run_seq_write(count, value_size).await?);
        out.push(run_random_write(count, value_size).await?);
        out.push(run_seq_read(count, value_size).await?);
        out.push(run_random_read(count, value_size).await?);
    }

    serde_wasm_bindgen::to_value(&out)
        .map_err(|e| JsValue::from_str(&format!("serialize benchmark matrix: {e}")))
}

#[wasm_bindgen]
pub async fn bench_opfs_mixed_matrix(count: u32) -> Result<JsValue, JsValue> {
    let sizes = [32u32, 256u32, 4096u32];
    let mut out = Vec::new();

    for value_size in sizes {
        for scenario in MIXED_SCENARIOS {
            out.push(run_mixed_scenario(scenario, count, value_size, DEFAULT_BASE_SEED).await?);
        }
    }

    serde_wasm_bindgen::to_value(&out)
        .map_err(|e| JsValue::from_str(&format!("serialize mixed benchmark matrix: {e}")))
}
