use std::sync::atomic::AtomicBool;

use common::counter::hardware_counter::HardwareCounterCell;
use common::types::PointOffsetType;
use criterion::{BatchSize, Criterion, criterion_group, criterion_main};
use dataset::Dataset;
use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle};
use itertools::Itertools as _;
use rand::SeedableRng;
use rand::rngs::StdRng;
use segment::fixtures::sparse_fixtures::fixture_sparse_index_from_iter;
use segment::index::sparse_index::sparse_index_config::{SparseIndexConfig, SparseIndexType};
use segment::index::sparse_index::sparse_vector_index::{
    SparseVectorIndex, SparseVectorIndexOpenArgs,
};
use segment::index::{PayloadIndex, VectorIndex};
use segment::payload_json;
use segment::types::PayloadSchemaType::Keyword;
use segment::types::{Condition, FieldCondition, Filter};
use sparse::common::sparse_vector::SparseVector;
use sparse::common::sparse_vector_fixture::{random_positive_sparse_vector, random_sparse_vector};
use sparse::index::inverted_index::inverted_index_compressed_mmap::InvertedIndexCompressedMmap;
use sparse::index::inverted_index::inverted_index_ram::InvertedIndexRam;
use sparse::index::loaders::Csr;
use tempfile::Builder;

#[cfg(not(target_os = "windows"))]
mod prof;

const NUM_VECTORS: usize = 50_000;
const MAX_SPARSE_DIM: usize = 30_000;
const NUM_QUERIES: usize = 2048;
const TOP: usize = 10;
const FULL_SCAN_THRESHOLD: usize = 1; // low value to trigger index usage by default

fn sparse_vector_index_search_benchmark(c: &mut Criterion) {
    let mut rnd = StdRng::seed_from_u64(0);
    let query_vectors = (0..NUM_QUERIES)
        // Positive values to test pruning.
        .map(|_| random_positive_sparse_vector(&mut rnd, MAX_SPARSE_DIM))
        .collect::<Vec<_>>();

    let mut rnd = StdRng::seed_from_u64(42);
    let random_vectors = (0..NUM_VECTORS).map(|_| random_sparse_vector(&mut rnd, MAX_SPARSE_DIM));
    sparse_vector_index_search_benchmark_impl(c, "random-50k", random_vectors, &query_vectors);

    let dataset_vectors = Csr::open(Dataset::NeurIps2023_1M.download().unwrap()).unwrap();
    let query_vectors = Csr::open(Dataset::NeurIps2023Queries.download().unwrap())
        .unwrap()
        .iter()
        .map(|v| v.unwrap())
        .collect_vec();
    sparse_vector_index_search_benchmark_impl(
        c,
        "neurips2023-1M",
        dataset_vectors.iter().map(|v| v.unwrap()),
        &query_vectors,
    );
}

fn sparse_vector_index_search_benchmark_impl(
    c: &mut Criterion,
    group: &str,
    vectors: impl ExactSizeIterator<Item = SparseVector>,
    query_vectors: &[SparseVector],
) {
    let mut group = c.benchmark_group(format!("sparse_vector_index_search/{group}"));
    group.sample_size(10);

    let vectors_len = vectors.len();

    let stopped = AtomicBool::new(false);

    let data_dir = Builder::new().prefix("data_dir").tempdir().unwrap();

    let sparse_vector_index = fixture_sparse_index_from_iter::<InvertedIndexRam>(
        data_dir.path(),
        progress("Indexing (1/2)", vectors_len).wrap_iter(vectors),
        FULL_SCAN_THRESHOLD,
        SparseIndexType::MutableRam,
    )
    .unwrap();

    // adding payload on field
    let field_name = "field";
    let field_value = "important value";
    let payload = payload_json! {field_name: field_value};

    let hw_counter = HardwareCounterCell::new();

    // all points have the same payload
    let mut payload_index = sparse_vector_index.payload_index().borrow_mut();
    for idx in 0..NUM_VECTORS {
        payload_index
            .set_payload(idx as PointOffsetType, &payload, &None, &hw_counter)
            .unwrap();
    }
    drop(payload_index);

    let mut query_vector_it = query_vectors.iter().cycle();

    // mmap inverted index
    let mmap_index_dir = Builder::new().prefix("mmap_index_dir").tempdir().unwrap();
    let sparse_index_config =
        SparseIndexConfig::new(Some(FULL_SCAN_THRESHOLD), SparseIndexType::Mmap, None);
    let pb = progress("Indexing (2/2)", vectors_len);
    let sparse_vector_index_mmap: SparseVectorIndex<InvertedIndexCompressedMmap<f32>> =
        SparseVectorIndex::open(SparseVectorIndexOpenArgs {
            config: sparse_index_config,
            id_tracker: sparse_vector_index.id_tracker().clone(),
            vector_storage: sparse_vector_index.vector_storage().clone(),
            payload_index: sparse_vector_index.payload_index().clone(),
            path: mmap_index_dir.path(),
            stopped: &stopped,
            tick_progress: || pb.inc(1),
        })
        .unwrap();
    pb.finish_and_clear();
    assert_eq!(sparse_vector_index_mmap.indexed_vector_count(), vectors_len);

    // intent: bench `search` without filter on mmap inverted index
    group.bench_function("mmap-inverted-index-search", |b| {
        b.iter_batched(
            || query_vector_it.next().unwrap().clone().into(),
            |vec| {
                let results = sparse_vector_index_mmap
                    .search(&[&vec], None, TOP, None, &Default::default())
                    .unwrap();

                assert_eq!(results[0].len(), TOP);
            },
            BatchSize::SmallInput,
        )
    });

    // intent: bench `search` without filter
    group.bench_function("inverted-index-search", |b| {
        b.iter_batched(
            || query_vector_it.next().unwrap().clone().into(),
            |vec| {
                let results = sparse_vector_index
                    .search(&[&vec], None, TOP, None, &Default::default())
                    .unwrap();

                assert_eq!(results[0].len(), TOP);
            },
            BatchSize::SmallInput,
        )
    });

    // filter by field
    let filter = Filter::new_must(Condition::Field(FieldCondition::new_match(
        field_name.parse().unwrap(),
        field_value.to_owned().into(),
    )));

    // intent: bench plain search when the filtered payload key is not indexed
    if vectors_len < 100_000 {
        group.bench_function("inverted-index-filtered-plain", |b| {
            b.iter_batched(
                || query_vector_it.next().unwrap(),
                |vec| {
                    let mut prefiltered_points = None;
                    let results = sparse_vector_index
                        .search_plain(
                            vec,
                            &filter,
                            TOP,
                            &mut prefiltered_points,
                            &Default::default(),
                        )
                        .unwrap();

                    assert_eq!(results.len(), TOP);
                },
                BatchSize::SmallInput,
            )
        });
    }

    let mut payload_index = sparse_vector_index.payload_index().borrow_mut();

    // create payload field index
    payload_index
        .set_indexed(&field_name.parse().unwrap(), Keyword, &hw_counter)
        .unwrap();

    drop(payload_index);

    // intent: bench `search` when the filtered payload key is indexed
    group.bench_function("inverted-index-filtered-payload-index", |b| {
        b.iter_batched(
            || query_vector_it.next().unwrap().clone().into(),
            |vec| {
                let results = sparse_vector_index
                    .search(&[&vec], Some(&filter), TOP, None, &Default::default())
                    .unwrap();

                assert_eq!(results[0].len(), TOP);
            },
            BatchSize::SmallInput,
        );
    });

    // intent: bench plain search when the filtered payload key is indexed
    if vectors_len < 100_000 {
        group.bench_function("plain-filtered-payload-index", |b| {
            b.iter_batched(
                || query_vector_it.next().unwrap(),
                |vec| {
                    let mut prefiltered_points = None;
                    let results = sparse_vector_index
                        .search_plain(
                            vec,
                            &filter,
                            TOP,
                            &mut prefiltered_points,
                            &Default::default(),
                        )
                        .unwrap();

                    assert_eq!(results.len(), TOP);
                },
                BatchSize::SmallInput,
            )
        });
    }

    group.finish();
}

fn progress(name: &str, length: usize) -> ProgressBar {
    let pb =
        ProgressBar::with_draw_target(Some(length as u64), ProgressDrawTarget::stderr_with_hz(12));
    pb.set_style(
        ProgressStyle::default_bar()
            .template("{msg} {wide_bar} {pos}/{len} (eta:{eta})")
            .unwrap(),
    );
    pb.set_message(name.to_owned());
    pb
}

#[cfg(not(target_os = "windows"))]
criterion_group! {
    name = benches;
    config = Criterion::default().with_profiler(prof::FlamegraphProfiler::new(100));
    targets = sparse_vector_index_search_benchmark
}

#[cfg(target_os = "windows")]
criterion_group! {
    name = benches;
    config = Criterion::default();
    targets = sparse_vector_index_search_benchmark,
}

criterion_main!(benches);
