//! Vector search profiling: HNSW build time, kNN latency, recall vs ef.

use super::harness::*;
use luci_index::vector::query::KnnQuery;
use luci_index::search::searcher::Searcher;
use luci_index::reader::IndexReader;
use luci_analysis::AnalyzerRegistry;
use luci_storage::SingleFileDirectory;
use serde_json::json;

use std::collections::HashSet;
use std::time::Instant;

#[test]
fn hnsw_build_time() {
    println!("\n=== Vector: HNSW Build Time ===\n");

    for &(count, dims) in &[(1_000, 64), (5_000, 64), (10_000, 64), (1_000, 128), (1_000, 256)] {
        let start = Instant::now();
        let (path, _index) = build_vector_corpus(&format!("hnsw_build_{count}_{dims}"), count, dims);
        let elapsed = start.elapsed();

        println!("{count:>6} vectors x {dims}d: {:.0}ms ({:.0} vecs/s)",
            elapsed.as_millis(),
            count as f64 / elapsed.as_secs_f64());

        cleanup(&path);
    }
}

#[test]
fn knn_latency() {
    println!("\n=== Vector: kNN Query Latency (5K vectors, 64d) ===\n");

    let dims = 64;
    let (path, _index) = build_vector_corpus("knn_latency", 5_000, dims);

    let storage = SingleFileDirectory::open(&path).unwrap();
    let reader = IndexReader::open(&storage).unwrap();
    let searcher = Searcher::with_analyzers(reader.into_segments(), AnalyzerRegistry::new());

    // Query vector
    let query_vec: Vec<f32> = (0..dims).map(|i| (i as f32 / dims as f32) * 2.0 - 1.0).collect();

    println!("{:<20} {:>12}  {:>12}", "config", "p50", "p99");
    println!("{}", "-".repeat(50));

    for &(k, ef) in &[(10, 50), (10, 100), (10, 200), (50, 100), (100, 200)] {
        let knn = KnnQuery {
            field: "embedding".into(),
            query_vector: query_vec.clone(),
            k, num_candidates: ef,
        };

        let mut times = Vec::new();
        for _ in 0..5 { let _ = searcher.search_query(&knn, k, 0, &luci_index::search::SourceFilter::Enabled, &None, false, &[]); } // warmup
        for _ in 0..20 {
            let start = Instant::now();
            let _ = searcher.search_query(&knn, k, 0, &luci_index::search::SourceFilter::Enabled, &None, false, &[]);
            times.push(start.elapsed());
        }
        times.sort();
        let p50 = times[times.len() / 2];
        let p99 = times[times.len() * 99 / 100];
        println!("k={k:<3} ef={ef:<4}       p50={:>6.1}us  p99={:>6.1}us",
            p50.as_micros() as f64, p99.as_micros() as f64);

        // kNN on 5K vectors should be under 25ms p99
        assert!(p99.as_millis() < 25,
            "REGRESSION: kNN k={k} ef={ef} p99={}ms exceeds 25ms", p99.as_millis());
    }

    cleanup(&path);
}

#[test]
fn knn_recall_vs_ef() {
    println!("\n=== Vector: Recall@10 vs ef_search (5K vectors, 64d) ===\n");

    let dims = 64;
    let n = 5_000;
    let (path, _index) = build_vector_corpus("knn_recall", n, dims);

    let storage = SingleFileDirectory::open(&path).unwrap();
    let reader = IndexReader::open(&storage).unwrap();
    let searcher = Searcher::with_analyzers(reader.into_segments(), AnalyzerRegistry::new());

    // Generate multiple query vectors and average recall
    let mut rng: u64 = 777;
    let num_queries = 10;

    println!("{:<10} {:>10}", "ef", "recall@10");
    println!("{}", "-".repeat(25));

    for &ef in &[10, 20, 50, 100, 200] {
        let mut total_recall = 0.0;

        for _ in 0..num_queries {
            let mut query_vec = Vec::with_capacity(dims);
            for _ in 0..dims {
                rng ^= rng << 13; rng ^= rng >> 7; rng ^= rng << 17;
                query_vec.push((rng as f32 / u64::MAX as f32) * 2.0 - 1.0);
            }

            let knn = KnnQuery {
                field: "embedding".into(),
                query_vector: query_vec.clone(),
                k: 10, num_candidates: ef,
            };
            let results = searcher.search_query(&knn, 10, 0, &luci_index::search::SourceFilter::Enabled, &None, false, &[]);
            let hnsw_ids: HashSet<u32> = results.hits.iter().map(|h| h.doc_id.as_u32()).collect();

            // Brute force for ground truth
            let brute = brute_force_knn(&path, &query_vec, 10, dims);
            let brute_ids: HashSet<u32> = brute.into_iter().collect();

            let recall = hnsw_ids.intersection(&brute_ids).count() as f64 / 10.0;
            total_recall += recall;
        }

        let avg_recall = total_recall / num_queries as f64;
        println!("ef={ef:<6}   recall={avg_recall:.3}");
    }

    cleanup(&path);
}

fn brute_force_knn(path: &std::path::Path, query: &[f32], k: usize, dims: usize) -> Vec<u32> {
    // Re-read vectors from the index and brute-force search
    // This is a simplified approach — just use the search with very high ef
    let storage = SingleFileDirectory::open(path).unwrap();
    let reader = IndexReader::open(&storage).unwrap();
    let searcher = Searcher::with_analyzers(reader.into_segments(), AnalyzerRegistry::new());

    let knn = KnnQuery {
        field: "embedding".into(),
        query_vector: query.to_vec(),
        k,
        num_candidates: 5000, // very high ef = near-brute-force
    };
    let results = searcher.search_query(&knn, k, 0, &luci_index::search::SourceFilter::Enabled, &None, false, &[]);
    results.hits.iter().map(|h| h.doc_id.as_u32()).collect()
}
