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

use vecdb::{
    AnyStoredVec, BytesVec, Database, ImportableVec, LZ4Vec, PcoVec, ReadableVec, StoredVec,
    Version, WritableVec, ZeroCopyVec, ZstdVec,
};

const DEFAULT_VALUE_COUNT: usize = 10_000_000_000; // 10B u64s = 80 GB
const BATCH_SIZE: usize = 10_000_000;
const MAX_RANGE_BYTES: usize = 8 * 1024 * 1024 * 1024; // 8 GB

fn value_count() -> usize {
    std::env::var("BENCH_COUNT")
        .ok()
        .and_then(|s| s.replace('_', "").parse().ok())
        .unwrap_or(DEFAULT_VALUE_COUNT)
}

fn range_passes() -> usize {
    std::env::var("BENCH_PASSES")
        .ok()
        .and_then(|s| s.parse().ok())
        .unwrap_or(5)
}

fn range_sizes(count: usize, value_size: usize) -> Vec<usize> {
    let max_elements = (MAX_RANGE_BYTES / value_size).min(count);
    let mut sizes = Vec::new();
    let mut n = 1_000;
    while n <= max_elements {
        sizes.push(n);
        let next5 = n * 5;
        if next5 <= max_elements && next5 != n * 10 {
            sizes.push(next5);
        }
        n *= 10;
    }
    sizes
}

fn repetitions(range_size: usize) -> usize {
    match range_size {
        n if n < 10_000 => 5_000,
        n if n < 100_000 => 1_000,
        n if n < 1_000_000 => 100,
        n if n < 10_000_000 => 20,
        n if n < 100_000_000 => 5,
        _ => 1,
    }
}

fn xorshift(state: &mut u64) -> u64 {
    *state ^= *state << 13;
    *state ^= *state >> 7;
    *state ^= *state << 17;
    *state
}

fn random_starts(count: usize, max_start: usize) -> Vec<usize> {
    let mut rng = 42u64;
    (0..count)
        .map(|_| xorshift(&mut rng) as usize % max_start.max(1))
        .collect()
}

fn format_bytes(bytes: usize) -> String {
    if bytes >= 1_000_000_000 {
        format!("{:.1} GB", bytes as f64 / 1e9)
    } else if bytes >= 1_000_000 {
        format!("{:.1} MB", bytes as f64 / 1e6)
    } else if bytes >= 1_000 {
        format!("{:.1} KB", bytes as f64 / 1e3)
    } else {
        format!("{} B", bytes)
    }
}

fn format_duration(d: Duration) -> String {
    let ns = d.as_nanos();
    if ns >= 1_000_000_000 {
        format!("{:.2} s", d.as_secs_f64())
    } else if ns >= 1_000_000 {
        format!("{:.2} ms", ns as f64 / 1e6)
    } else if ns >= 1_000 {
        format!("{:.2} us", ns as f64 / 1e3)
    } else {
        format!("{} ns", ns)
    }
}

fn throughput_str(bytes: usize, d: Duration) -> String {
    format!("{:.1} GB/s", bytes as f64 / d.as_secs_f64() / 1e9)
}

// --- Page cache eviction ---

fn drop_caches() -> bool {
    #[cfg(target_os = "macos")]
    {
        std::process::Command::new("purge")
            .stdout(std::process::Stdio::null())
            .stderr(std::process::Stdio::null())
            .status()
            .is_ok_and(|s| s.success())
    }
    #[cfg(target_os = "linux")]
    {
        std::process::Command::new("sync").status().ok();
        std::fs::write("/proc/sys/vm/drop_caches", "3").is_ok()
    }
    #[cfg(not(any(target_os = "macos", target_os = "linux")))]
    {
        false
    }
}

fn check_cache_eviction() -> bool {
    let ok = drop_caches();
    if ok {
        println!("  Cache eviction: available (cold-cache benchmarks)");
    } else {
        println!("  Cache eviction: unavailable (warm-cache benchmarks)");
        println!("  Hint: run with sudo for cold-cache results");
    }
    ok
}

// --- Vec size ---

fn print_vec_size(vec: &dyn AnyStoredVec, label: &str) {
    let region_bytes = vec.region().meta().len();
    let logical_bytes = vec.len() * vec.value_type_to_size_of();
    let ratio = if logical_bytes > 0 {
        region_bytes as f64 / logical_bytes as f64
    } else {
        0.0
    };
    eprintln!(
        "  {label} on disk: {} (logical: {}, ratio: {:.2}x)",
        format_bytes(region_bytes),
        format_bytes(logical_bytes),
        ratio,
    );
}

// --- Populate ---

fn populate<V: WritableVec<usize, u64> + ImportableVec + AnyStoredVec>(
    db: &Database,
    label: &str,
    count: usize,
) -> V {
    eprint!("  Populating {label} with {count} values...");
    flush();
    let start = Instant::now();
    let mut vec: V = V::import(db, "bench", Version::ONE).unwrap();
    let mut written = 0;
    while written < count {
        let end = (written + BATCH_SIZE).min(count);
        for i in written..end {
            vec.push(i as u64);
        }
        vec.write().unwrap();
        written = end;
        eprint!(
            "\r  Populating {label}: {:.0}%  ",
            written as f64 / count as f64 * 100.0
        );
    }
    db.flush().unwrap();
    eprintln!("\r  Populated {label} ({:?})       ", start.elapsed());
    print_vec_size(&vec, label);
    vec
}

// --- Benchmark helpers ---

// --- Benchmark runners ---

struct BenchResult {
    name: &'static str,
    duration: Duration,
}

fn print_full_results(results: &[BenchResult], total_bytes: usize) {
    let best = results.iter().min_by_key(|r| r.duration).unwrap().duration;
    for r in results {
        let pct = if r.duration > best {
            let overhead = (r.duration.as_secs_f64() / best.as_secs_f64() - 1.0) * 100.0;
            format!("(+{overhead:.0}%)")
        } else {
            String::new()
        };
        println!(
            "  {:<20} {} ({}) {}",
            r.name,
            format_duration(r.duration),
            throughput_str(total_bytes, r.duration),
            pct,
        );
    }
}

fn print_range_header(columns: &[&str]) {
    print!("{:>12} {:>10}", "range", "bytes");
    for col in columns {
        print!(" {:>14}", col);
    }
    println!("  {:<8}", "winner");
    let width = 24 + columns.len() * 15 + 10;
    println!("{}", "-".repeat(width));
}

fn print_range_row(range_size: usize, results: &[BenchResult]) {
    let range_bytes = range_size * 8;
    print!("{:>12} {:>10}", range_size, format_bytes(range_bytes));
    let best = results.iter().min_by_key(|r| r.duration).unwrap();
    for r in results {
        print!(" {:>14}", format_duration(r.duration));
    }
    println!("  {:<8}", best.name);
}

// --- StoredFold trait for IO/mmap access ---

trait StoredFold {
    fn fold_stored_io_sum(&self, from: usize, to: usize, acc: u64) -> u64;
    fn fold_stored_mmap_sum(&self, from: usize, to: usize, acc: u64) -> u64;
}

impl StoredFold for BytesVec<usize, u64> {
    fn fold_stored_io_sum(&self, from: usize, to: usize, acc: u64) -> u64 {
        self.fold_stored_io(from, to, acc, |a, v: u64| a.wrapping_add(v))
    }
    fn fold_stored_mmap_sum(&self, from: usize, to: usize, acc: u64) -> u64 {
        self.fold_stored_mmap(from, to, acc, |a, v: u64| a.wrapping_add(v))
    }
}

impl StoredFold for ZeroCopyVec<usize, u64> {
    fn fold_stored_io_sum(&self, from: usize, to: usize, acc: u64) -> u64 {
        self.fold_stored_io(from, to, acc, |a, v: u64| a.wrapping_add(v))
    }
    fn fold_stored_mmap_sum(&self, from: usize, to: usize, acc: u64) -> u64 {
        self.fold_stored_mmap(from, to, acc, |a, v: u64| a.wrapping_add(v))
    }
}

impl StoredFold for LZ4Vec<usize, u64> {
    fn fold_stored_io_sum(&self, from: usize, to: usize, acc: u64) -> u64 {
        self.fold_stored_io(from, to, acc, |a, v: u64| a.wrapping_add(v))
    }
    fn fold_stored_mmap_sum(&self, from: usize, to: usize, acc: u64) -> u64 {
        self.fold_stored_mmap(from, to, acc, |a, v: u64| a.wrapping_add(v))
    }
}

impl StoredFold for PcoVec<usize, u64> {
    fn fold_stored_io_sum(&self, from: usize, to: usize, acc: u64) -> u64 {
        self.fold_stored_io(from, to, acc, |a, v: u64| a.wrapping_add(v))
    }
    fn fold_stored_mmap_sum(&self, from: usize, to: usize, acc: u64) -> u64 {
        self.fold_stored_mmap(from, to, acc, |a, v: u64| a.wrapping_add(v))
    }
}

impl StoredFold for ZstdVec<usize, u64> {
    fn fold_stored_io_sum(&self, from: usize, to: usize, acc: u64) -> u64 {
        self.fold_stored_io(from, to, acc, |a, v: u64| a.wrapping_add(v))
    }
    fn fold_stored_mmap_sum(&self, from: usize, to: usize, acc: u64) -> u64 {
        self.fold_stored_mmap(from, to, acc, |a, v: u64| a.wrapping_add(v))
    }
}

// --- Unified benchmark ---

fn bench_vec<
    V: ReadableVec<usize, u64> + StoredFold + AnyStoredVec + StoredVec<I = usize, T = u64>,
>(
    vec: &V,
    label: &str,
    count: usize,
    can_purge: bool,
) {
    let total_bytes = count * 8;
    let disk_bytes = vec.region().meta().len();
    let ratio = if total_bytes > 0 {
        disk_bytes as f64 / total_bytes as f64
    } else {
        0.0
    };
    let ranges = range_sizes(count, 8);

    println!(
        "\n=== {label} — {count} values ({}, disk: {}, {:.2}x) ===\n",
        format_bytes(total_bytes),
        format_bytes(disk_bytes),
        ratio,
    );

    // Full scan — multiple passes in random method order to eliminate cache bias.
    let full_passes = range_passes();
    println!("--- Full scan ({full_passes} passes, random order) ---");

    let method_names: [&str; 6] = [
        "fold_stored_io",
        "fold_stored_mmap",
        "fold_range",
        "try_fold_range",
        "for_each_dyn",
        "for_each",
    ];
    let mut times = [Duration::ZERO; 6];
    let mut sum = 0u64;
    let mut rng = 0xCAFEu64;

    for _pass in 0..full_passes {
        let mut order = [0usize, 1, 2, 3, 4, 5];
        for i in (1..6).rev() {
            let j = xorshift(&mut rng) as usize % (i + 1);
            order.swap(i, j);
        }

        for &method in &order {
            if can_purge {
                drop_caches();
            }
            let t = Instant::now();
            match method {
                0 => sum = vec.fold_stored_io_sum(0, count, sum),
                1 => sum = vec.fold_stored_mmap_sum(0, count, sum),
                2 => sum = vec.fold_range(0, count, sum, |a, v: u64| a.wrapping_add(v)),
                3 => {
                    sum = vec
                        .try_fold_range(0, count, sum, |a, v: u64| Ok::<_, ()>(a.wrapping_add(v)))
                        .unwrap()
                }
                4 => vec.for_each_range_dyn(0, count, &mut |v: u64| sum = sum.wrapping_add(v)),
                5 => vec.for_each_range(0, count, |v: u64| sum = sum.wrapping_add(v)),
                _ => unreachable!(),
            }
            times[method] += t.elapsed();
        }
    }

    std::hint::black_box(sum);

    let results: Vec<BenchResult> = (0..6)
        .map(|i| BenchResult {
            name: method_names[i],
            duration: times[i] / full_passes as u32,
        })
        .collect();
    print_full_results(&results, total_bytes);

    // Range scans — multiple passes in random method order to eliminate cache bias.
    let passes = range_passes();
    println!("\n--- Range scans ({passes} passes, random order) ---");
    let columns: Vec<&str> = vec!["IO", "Mmap", "fold", "try_fold", "dyn", "static"];
    print_range_header(&columns);

    for &range_size in &ranges {
        let reps = repetitions(range_size);
        let max_start = count.saturating_sub(range_size);
        let starts = random_starts(reps, max_start);

        let mut times = [Duration::ZERO; 6];
        let mut sum = 0u64;
        let mut rng = range_size as u64 ^ 0xDEAD;

        for _pass in 0..passes {
            // Shuffle method order using Fisher-Yates.
            let mut order = [0usize, 1, 2, 3, 4, 5];
            for i in (1..6).rev() {
                let j = xorshift(&mut rng) as usize % (i + 1);
                order.swap(i, j);
            }

            for &method in &order {
                if can_purge {
                    drop_caches();
                }
                let t = Instant::now();
                for &s in &starts {
                    let from = s;
                    let to = s + range_size;
                    match method {
                        0 => sum = vec.fold_stored_io_sum(from, to, sum),
                        1 => sum = vec.fold_stored_mmap_sum(from, to, sum),
                        2 => sum = vec.fold_range(from, to, sum, |a, v: u64| a.wrapping_add(v)),
                        3 => {
                            sum = vec
                                .try_fold_range(from, to, sum, |a, v: u64| {
                                    Ok::<_, ()>(a.wrapping_add(v))
                                })
                                .unwrap()
                        }
                        4 => vec
                            .for_each_range_dyn(from, to, &mut |v: u64| sum = sum.wrapping_add(v)),
                        5 => vec.for_each_range(from, to, |v: u64| sum = sum.wrapping_add(v)),
                        _ => unreachable!(),
                    }
                }
                times[method] += t.elapsed();
            }
        }

        std::hint::black_box(sum);

        let divisor = (reps * passes) as u32;
        let row = vec![
            BenchResult {
                name: "IO",
                duration: times[0] / divisor,
            },
            BenchResult {
                name: "Mmap",
                duration: times[1] / divisor,
            },
            BenchResult {
                name: "fold",
                duration: times[2] / divisor,
            },
            BenchResult {
                name: "try_fold",
                duration: times[3] / divisor,
            },
            BenchResult {
                name: "dyn",
                duration: times[4] / divisor,
            },
            BenchResult {
                name: "static",
                duration: times[5] / divisor,
            },
        ];
        print_range_row(range_size, &row);
    }

    // --- Read-only clone benchmarks ---

    let ro = vec.read_only_clone();

    let ro_passes = range_passes();
    println!("\n--- Read-only full scan ({ro_passes} passes, random order) ---");

    let ro_method_names: [&str; 4] = [
        "ro_fold_range",
        "ro_try_fold_range",
        "ro_for_each_dyn",
        "ro_for_each",
    ];
    let mut ro_times = [Duration::ZERO; 4];
    let mut sum = 0u64;
    let mut rng = 0xBEEFu64;

    for _pass in 0..ro_passes {
        let mut order = [0usize, 1, 2, 3];
        for i in (1..4).rev() {
            let j = xorshift(&mut rng) as usize % (i + 1);
            order.swap(i, j);
        }

        for &method in &order {
            if can_purge {
                drop_caches();
            }
            let t = Instant::now();
            match method {
                0 => sum = ro.fold_range(0, count, sum, |a, v: u64| a.wrapping_add(v)),
                1 => {
                    sum = ro
                        .try_fold_range(0, count, sum, |a, v: u64| Ok::<_, ()>(a.wrapping_add(v)))
                        .unwrap()
                }
                2 => ro.for_each_range_dyn(0, count, &mut |v: u64| sum = sum.wrapping_add(v)),
                3 => ro.for_each_range(0, count, |v: u64| sum = sum.wrapping_add(v)),
                _ => unreachable!(),
            }
            ro_times[method] += t.elapsed();
        }
    }

    std::hint::black_box(sum);

    let ro_results: Vec<BenchResult> = (0..4)
        .map(|i| BenchResult {
            name: ro_method_names[i],
            duration: ro_times[i] / ro_passes as u32,
        })
        .collect();
    print_full_results(&ro_results, total_bytes);

    let ro_passes = range_passes();
    println!("\n--- Read-only range scans ({ro_passes} passes, random order) ---");
    let ro_columns: Vec<&str> = vec!["fold", "try_fold", "dyn", "static"];
    print_range_header(&ro_columns);

    for &range_size in &ranges {
        let reps = repetitions(range_size);
        let max_start = count.saturating_sub(range_size);
        let starts = random_starts(reps, max_start);

        let mut times = [Duration::ZERO; 4];
        let mut sum = 0u64;
        let mut rng = range_size as u64 ^ 0xFACE;

        for _pass in 0..ro_passes {
            let mut order = [0usize, 1, 2, 3];
            for i in (1..4).rev() {
                let j = xorshift(&mut rng) as usize % (i + 1);
                order.swap(i, j);
            }

            for &method in &order {
                if can_purge {
                    drop_caches();
                }
                let t = Instant::now();
                for &s in &starts {
                    let from = s;
                    let to = s + range_size;
                    match method {
                        0 => sum = ro.fold_range(from, to, sum, |a, v: u64| a.wrapping_add(v)),
                        1 => {
                            sum = ro
                                .try_fold_range(from, to, sum, |a, v: u64| {
                                    Ok::<_, ()>(a.wrapping_add(v))
                                })
                                .unwrap()
                        }
                        2 => {
                            ro.for_each_range_dyn(from, to, &mut |v: u64| sum = sum.wrapping_add(v))
                        }
                        3 => ro.for_each_range(from, to, |v: u64| sum = sum.wrapping_add(v)),
                        _ => unreachable!(),
                    }
                }
                times[method] += t.elapsed();
            }
        }

        std::hint::black_box(sum);

        let divisor = (reps * ro_passes) as u32;
        let row = vec![
            BenchResult {
                name: "fold",
                duration: times[0] / divisor,
            },
            BenchResult {
                name: "try_fold",
                duration: times[1] / divisor,
            },
            BenchResult {
                name: "dyn",
                duration: times[2] / divisor,
            },
            BenchResult {
                name: "static",
                duration: times[3] / divisor,
            },
        ];
        print_range_row(range_size, &row);
    }
}

/// Fixed bench directory — cleaned up at start so Ctrl+C leftovers don't accumulate.
fn bench_dir() -> std::path::PathBuf {
    std::env::temp_dir().join("vecdb_bench")
}

fn cleanup_bench_dir() {
    let dir = bench_dir();
    if dir.exists() {
        eprint!("  Cleaning up previous bench data...");
        flush();
        std::fs::remove_dir_all(&dir).ok();
        eprintln!(" done");
    }
}

fn run_bench<
    V: ReadableVec<usize, u64> + StoredFold + AnyStoredVec + StoredVec<I = usize, T = u64>,
>(
    label: &str,
    count: usize,
    can_purge: bool,
) {
    cleanup_bench_dir();
    let dir = bench_dir();
    std::fs::create_dir_all(&dir).unwrap();
    let db = Database::open(&dir).unwrap();
    let vec = populate::<V>(&db, label, count);
    bench_vec(&vec, label, count, can_purge);
    drop(vec);
    drop(db);
    std::fs::remove_dir_all(&dir).ok();
}

fn main() {
    let args: Vec<String> = std::env::args().collect();
    let mode = args.get(1).map(|s| s.as_str()).unwrap_or("all");
    let count = value_count();

    println!("BENCH_COUNT={count} (set env to override, e.g. BENCH_COUNT=1_000_000)");
    let can_purge = check_cache_eviction();
    println!();

    let run_bytes = matches!(mode, "all" | "bytes" | "raw");
    let run_zerocopy = matches!(mode, "all" | "zerocopy" | "raw");
    let run_lz4 = matches!(mode, "all" | "lz4" | "compressed");
    let run_pco = matches!(mode, "all" | "pco" | "compressed");
    let run_zstd = matches!(mode, "all" | "zstd" | "compressed");

    if run_bytes {
        run_bench::<BytesVec<usize, u64>>("BytesVec<usize, u64>", count, can_purge);
    }
    if run_zerocopy {
        run_bench::<ZeroCopyVec<usize, u64>>("ZeroCopyVec<usize, u64>", count, can_purge);
    }
    if run_lz4 {
        run_bench::<LZ4Vec<usize, u64>>("LZ4Vec<usize, u64>", count, can_purge);
    }
    if run_pco {
        run_bench::<PcoVec<usize, u64>>("PcoVec<usize, u64>", count, can_purge);
    }
    if run_zstd {
        run_bench::<ZstdVec<usize, u64>>("ZstdVec<usize, u64>", count, can_purge);
    }

    cleanup_bench_dir();
}

fn flush() {
    std::io::Write::flush(&mut std::io::stderr()).ok();
    std::io::Write::flush(&mut std::io::stdout()).ok();
}
