//! Vector index abstraction layer.
//!
//! Defines the [`VectorIndex`] trait -- the single interface through which all
//! ANN operations flow in tidalDB. Concrete implementations include:
//!
//! - [`UsearchIndex`]: production HNSW index backed by `USearch` (approximate search)
//! - [`BruteForceIndex`]: exact linear-scan search for correctness baselines and small datasets
//! - [`MockVectorIndex`]: predetermined results for unit testing call sequences
//!
//! The rest of the codebase never imports `usearch` directly -- all access
//! goes through the [`VectorIndex`] trait.
//!
//! # Design decisions
//!
//! - **`VectorId = u64`**: matches `EntityId` width; the mapping between entity
//!   IDs and vector IDs is managed by a higher layer (embedding slot registry).
//! - **L2 squared distance**: avoids the `sqrt` in hot loops. For L2-normalized
//!   vectors, L2 squared in `[0.0, 4.0]` is monotonically equivalent to cosine
//!   distance, so ranking order is preserved.
//! - **`ef_search` parameter**: accepted on all implementations for trait
//!   uniformity. Brute-force and mock ignore it (exact search has no beam width).

mod brute;
pub mod lifecycle;
pub mod mock;
pub mod planner;
pub mod registry;
pub mod usearch_index;

pub use brute::BruteForceIndex;
pub use lifecycle::{
    delete_embedding, deserialize_embedding, embedding_store_key, insert_embedding, l2_normalize,
    serialize_embedding, update_embedding,
};
pub use mock::{MockVectorIndex, VectorIndexCall};
pub use planner::{
    AdaptiveQueryPlanner, AnnQueryStats, AnnStrategy, FixedSelectivityEstimator, PlannerConfig,
    SelectivityEstimator,
};
pub use registry::{EmbeddingSlotRegistry, EmbeddingSlotState, EmbeddingSource, HnswParams};
pub use usearch_index::UsearchIndex;

use std::path::Path;

/// Identifier for a vector in the index. Maps 1:1 to an entity + embedding slot.
pub type VectorId = u64;

/// A single result from a vector search operation.
#[derive(Debug, Clone)]
pub struct VectorSearchResult {
    /// The ID of the matched vector.
    pub id: VectorId,
    /// L2 squared distance from the query vector. Lower = more similar.
    /// For L2-normalized vectors, the range is `[0.0, 4.0]` where `0.0` = identical.
    pub distance: f32,
}

/// Configuration for a vector index instance.
///
/// These parameters are set at index creation time and cannot be changed
/// without rebuilding the index.
#[derive(Debug, Clone)]
pub struct VectorIndexConfig {
    /// Dimensionality of stored vectors. All vectors must match this exactly.
    pub dimensions: usize,
    /// Distance metric used for nearest-neighbor computation.
    pub metric: DistanceMetric,
    /// Quantization level for stored vectors.
    pub quantization: QuantizationLevel,
    /// HNSW connectivity parameter (M). Higher = better recall, more memory.
    /// Ignored by brute-force and mock implementations.
    pub connectivity: usize,
    /// HNSW construction-time beam width. Higher = better graph quality, slower builds.
    /// Ignored by brute-force and mock implementations.
    pub ef_construction: usize,
    /// Default search-time beam width. Can be overridden per-query.
    /// Ignored by brute-force and mock implementations.
    pub ef_search: usize,
}

impl VectorIndexConfig {
    /// Create a default `VectorIndexConfig` for the given dimensionality.
    ///
    /// Same quality parameters as `Default::default()` (M=16, `ef_construction=400`)
    /// but with a custom `dimensions` value. Useful in tests and benchmarks that
    /// use reduced dimensionality (e.g. 128D instead of the production 1536D).
    #[must_use]
    pub fn default_for_dim(dimensions: usize) -> Self {
        Self {
            dimensions,
            ..Self::default()
        }
    }
}

impl Default for VectorIndexConfig {
    fn default() -> Self {
        Self {
            dimensions: 1536,
            metric: DistanceMetric::L2,
            quantization: QuantizationLevel::F16,
            // M=16 is the sweet spot for quality vs. memory (Malkov & Yashunin, 2018).
            // ef_construction=400 raises recall@10 to >0.99 at 100K vectors (128D)
            // with ~2× build overhead vs. ef=200 — acceptable for a write-once index.
            // Grid search at 100K vectors / 128D confirmed: M=16/ef=400 achieves
            // recall@10 ≈ 0.993 vs. 0.978 for M=16/ef=200 (see docs/profiling/usearch-tuning.md).
            connectivity: 16,
            ef_construction: 400,
            ef_search: 200,
        }
    }
}

/// Distance metric for nearest-neighbor computation.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DistanceMetric {
    /// Euclidean (L2) distance. Default for normalized embeddings.
    L2,
    /// Inner product distance. Use when vectors are not normalized.
    InnerProduct,
}

/// Quantization level for stored vectors.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum QuantizationLevel {
    /// Full 32-bit floating point. Maximum precision, maximum memory.
    F32,
    /// Half-precision 16-bit float. Default. <1% recall loss, 50% memory savings.
    F16,
    /// 8-bit integer quantization. Aggressive compression for very large indexes.
    Int8,
}

/// Errors that can occur during vector index operations.
#[derive(Debug, thiserror::Error)]
pub enum VectorError {
    /// The provided vector has the wrong number of dimensions.
    #[error("dimension mismatch: expected {expected}, got {got}")]
    DimensionMismatch {
        /// Dimensionality the index was configured with.
        expected: usize,
        /// Dimensionality of the vector that was provided.
        got: usize,
    },
    /// The index has reached its maximum capacity.
    #[error("capacity exceeded: limit is {capacity}")]
    CapacityExceeded {
        /// The capacity limit that was exceeded.
        capacity: usize,
    },
    /// The requested vector ID was not found in the index.
    #[error("vector not found: id {id}")]
    NotFound {
        /// The ID that was looked up.
        id: VectorId,
    },
    /// An I/O error occurred during persistence operations.
    #[error("I/O error: {0}")]
    Io(#[from] std::io::Error),
    /// The index file is corrupted (bad magic bytes, version mismatch, truncation).
    #[error("corrupted index: {0}")]
    CorruptedIndex(String),
    /// An error from the underlying backend (e.g., `USearch` FFI failure).
    #[error("backend error: {0}")]
    Backend(String),
    /// A zero-norm vector was provided where a non-zero norm is required.
    #[error("zero-norm vector")]
    ZeroNormVector,
}

/// The vector index trait -- the single interface for all ANN operations.
///
/// Every vector index implementation (brute-force, `USearch` HNSW, mock) sits
/// behind this trait. The rest of tidalDB never imports a concrete index type
/// directly, enabling swappability and testability.
///
/// # Thread safety
///
/// Implementations must be `Send + Sync`. Concurrent inserts and searches
/// are expected in production (signal writes + ranking queries).
pub trait VectorIndex: Send + Sync {
    /// Insert a vector with the given ID. Replaces any existing vector with the same ID.
    ///
    /// # Errors
    ///
    /// - [`VectorError::DimensionMismatch`] if `embedding.len() != config.dimensions`
    /// - [`VectorError::CapacityExceeded`] if the index is full (HNSW only)
    fn insert(&self, id: VectorId, embedding: &[f32]) -> Result<(), VectorError>;

    /// Find the `k` nearest neighbors to `query`.
    ///
    /// Results are sorted by ascending distance (closest first).
    /// The `ef_search` parameter controls beam width for HNSW; brute-force
    /// implementations ignore it (exact search has no beam width).
    ///
    /// # Errors
    ///
    /// - [`VectorError::DimensionMismatch`] if `query.len() != config.dimensions`
    fn search(
        &self,
        query: &[f32],
        k: usize,
        ef_search: usize,
    ) -> Result<Vec<VectorSearchResult>, VectorError>;

    /// Find the `k` nearest neighbors to `query`, considering only vectors
    /// where `filter(id)` returns `true`.
    ///
    /// Vectors that do not pass the filter are skipped entirely -- their
    /// distance is never computed.
    ///
    /// # Errors
    ///
    /// - [`VectorError::DimensionMismatch`] if `query.len() != config.dimensions`
    fn filtered_search(
        &self,
        query: &[f32],
        k: usize,
        ef_search: usize,
        filter: &dyn Fn(VectorId) -> bool,
    ) -> Result<Vec<VectorSearchResult>, VectorError>;

    /// Delete a vector by ID. This is a true delete for brute-force; HNSW
    /// implementations may use tombstones internally.
    ///
    /// # Errors
    ///
    /// - [`VectorError::NotFound`] if no vector with this ID exists
    fn delete(&self, id: VectorId) -> Result<(), VectorError>;

    /// Pre-allocate capacity for `additional` more vectors.
    ///
    /// This is a hint -- implementations may ignore it if they resize
    /// automatically (e.g., `HashMap`).
    ///
    /// # Errors
    ///
    /// Returns an error only if the reservation itself fails (e.g., OOM).
    fn reserve(&self, additional: usize) -> Result<(), VectorError>;

    /// Persist the index to the given path.
    ///
    /// # Errors
    ///
    /// - [`VectorError::Io`] on filesystem failure
    fn save(&self, path: &Path) -> Result<(), VectorError>;

    /// Load an index from the given path, fully reading it into memory.
    ///
    /// # Errors
    ///
    /// - [`VectorError::Io`] on filesystem failure
    /// - [`VectorError::CorruptedIndex`] on invalid magic bytes or version
    fn load(path: &Path, config: &VectorIndexConfig) -> Result<Self, VectorError>
    where
        Self: Sized;

    /// Open an index from the given path using memory-mapped I/O where supported.
    ///
    /// For implementations that do not support mmap (e.g., brute-force),
    /// this delegates to [`VectorIndex::load`].
    ///
    /// # Errors
    ///
    /// - [`VectorError::Io`] on filesystem failure
    /// - [`VectorError::CorruptedIndex`] on invalid magic bytes or version
    fn view(path: &Path, config: &VectorIndexConfig) -> Result<Self, VectorError>
    where
        Self: Sized;

    /// Total number of vectors in the index, including tombstoned entries.
    fn len(&self) -> usize;

    /// Number of live (non-tombstoned) vectors in the index.
    fn len_live(&self) -> usize;

    /// Returns `true` if the index contains no live vectors.
    fn is_empty(&self) -> bool {
        self.len_live() == 0
    }

    /// Fraction of entries that are tombstoned, in `[0.0, 1.0]`.
    ///
    /// Useful for triggering compaction in HNSW implementations.
    #[allow(clippy::cast_precision_loss)]
    fn tombstone_ratio(&self) -> f64 {
        if self.len() == 0 {
            0.0
        } else {
            (self.len() - self.len_live()) as f64 / self.len() as f64
        }
    }
}

/// Compute the squared Euclidean (L2) distance between two vectors.
///
/// This avoids the `sqrt` call -- the squared distance preserves ranking order
/// and is sufficient for nearest-neighbor selection. Exposed at crate level
/// for use by the adaptive query planner and other internal components.
///
/// # Panics (debug only)
///
/// Debug-asserts that `a` and `b` have the same length.
#[allow(dead_code)]
pub(crate) fn l2_distance_sq(a: &[f32], b: &[f32]) -> f32 {
    brute::l2_distance_sq(a, b)
}
