//! MonaVec — embedded vector search kernel for edge AI systems.
//!
//! Zero server dependency. Runs fully offline. Designed for constrained hardware.
//!
//! # Quick start
//!
//! ```rust,no_run
//! use monavec_core::{MonaVec, Config, Metric};
//!
//! let config = Config::new(384, Metric::Cosine, 4);
//! let mut index = MonaVec::new(config).unwrap();
//!
//! let ids  = vec![0u64, 1, 2];
//! let vecs = vec![
//!     vec![0.1f32; 384],
//!     vec![0.2f32; 384],
//!     vec![0.3f32; 384],
//! ];
//! index.add(&ids, &vecs).unwrap();
//!
//! let query   = vec![0.1f32; 384];
//! let results = index.search(&query, 3, None).unwrap();
//! ```

pub mod engine;
pub mod error;
pub mod io;
pub mod metric;
pub mod quantize;
pub mod search;
pub mod simd;

pub use engine::MonaVec;
pub use error::MonaVecError;
pub use metric::Metric;

/// Search index algorithm.
///
/// `BruteForce` is the default and only implemented variant. All others are API stubs
/// reserved for future releases — selecting them returns `MonaVecError::IndexTypeNotImplemented`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum IndexType {
    /// Exact brute-force scan over all quantized vectors. O(n) per query.
    /// Approximation comes only from quantization error, not graph traversal.
    /// No build cost. Best for n < 1M or edge/embedded deployments.
    #[default]
    BruteForce,
    /// Clustering-based approximate search (Inverted File Index + Scalar Quantization).
    /// Requires K-means training. O(n/k × nprobe) per query. Lower memory than HNSW.
    /// **Not yet implemented** — reserved for post-v1.0.
    IvfFlat,
    /// Graph-based approximate nearest-neighbour search. O(log n) per query.
    /// No training required. Higher memory (graph edges ~40 bytes/vector).
    /// **Not yet implemented** — reserved for post-v1.0.
    Hnsw,
}

impl IndexType {
    pub fn as_u8(self) -> u8 {
        match self {
            Self::BruteForce => 0,
            Self::IvfFlat    => 1,
            Self::Hnsw       => 2,
        }
    }

    pub fn from_u8(b: u8) -> error::Result<Self> {
        match b {
            0 => Ok(Self::BruteForce),
            1 => Ok(Self::IvfFlat),
            2 => Ok(Self::Hnsw),
            _ => Err(MonaVecError::UnsupportedIndexType(b)),
        }
    }

    pub fn name(self) -> &'static str {
        match self {
            Self::BruteForce => "BruteForce",
            Self::IvfFlat    => "IvfFlat",
            Self::Hnsw       => "Hnsw",
        }
    }
}

/// Index configuration: dimensions, distance metric, quantization bit-width, and rotation seed.
///
/// The `seed` controls the RHDH rotation matrix and is embedded in `.mvec` files so
/// that indexes can be reconstructed exactly after save/load.
#[derive(Debug, Clone)]
pub struct Config {
    /// Embedding dimension. Must match all vectors added to the index.
    pub dim:        usize,
    /// Distance metric used for scoring.
    pub metric:     Metric,
    /// Quantization precision: `4` (8× compression) or `2` (16× compression).
    /// For mixed-precision mode use `with_mixed_ratio` — `bit_width` describes the dominant mode.
    pub bit_width:  u8,
    /// Number of dimensions (in padded space) quantized at 4-bit.
    /// Remainder `padded_dim - n4_dims` dimensions are at 2-bit.
    /// Invariants: must be a multiple of 4 and ≤ padded_dim.
    /// `padded_dim` = `dim.next_power_of_two()`.
    /// - Pure 4-bit: `n4_dims == padded_dim` (default for bit_width=4)
    /// - Pure 2-bit: `n4_dims == 0`           (default for bit_width=2)
    /// - Mixed:      `0 < n4_dims < padded_dim`
    pub n4_dims:    usize,
    /// ChaCha20 seed for the RHDH rotation matrix. Embedded in `.mvec` files.
    pub seed:       u64,
    /// Search index algorithm. Default: `BruteForce`.
    pub index_type: IndexType,

    // --- IvfFlat params (used when index_type == IvfFlat, feature = "ivf") ---
    /// Number of Voronoi cells (clusters). Typical: sqrt(n). 0 = not set.
    pub ivf_nlist:  usize,
    /// Number of cells to probe at search time. Runtime-tunable. 0 = not set.
    pub ivf_nprobe: usize,

    // --- HNSW params (used when index_type == Hnsw, feature = "hnsw") ---
    /// Max neighbours per node per layer. Typical: 16–32. 0 = not set.
    pub hnsw_m:               usize,
    /// Candidate list size during graph construction. Typical: 100–200. 0 = not set.
    pub hnsw_ef_construction: usize,
    /// Candidate list size during search. Runtime-tunable. 0 = defaults to hnsw_m*2.
    pub hnsw_ef_search:       usize,
}

impl Config {
    /// Create a config with default seed and `BruteForce` index.
    /// `bit_width` must be `4` (8× compression) or `2` (16× compression).
    /// For mixed precision, call `.with_mixed_ratio(ratio)` afterwards.
    pub fn new(dim: usize, metric: Metric, bit_width: u8) -> Self {
        let padded_dim = dim.next_power_of_two();
        let n4_dims = if bit_width == 2 { 0 } else { padded_dim };
        Self {
            dim, metric, bit_width, n4_dims,
            seed:                 0xDEAD_BEEF_CAFE_1337,
            index_type:           IndexType::BruteForce,
            ivf_nlist:            0,
            ivf_nprobe:           0,
            hnsw_m:               0,
            hnsw_ef_construction: 0,
            hnsw_ef_search:       0,
        }
    }

    /// Override the RHDH rotation seed (builder pattern).
    pub fn with_seed(mut self, seed: u64) -> Self {
        self.seed = seed;
        self
    }

    /// Override the index algorithm (builder pattern).
    pub fn with_index_type(mut self, t: IndexType) -> Self {
        self.index_type = t;
        self
    }

    /// Configure IvfFlat index with the given cluster count and probe width.
    /// Requires the `ivf` Cargo feature.
    #[cfg(feature = "ivf")]
    pub fn with_ivf(mut self, nlist: usize, nprobe: usize) -> Self {
        self.index_type  = IndexType::IvfFlat;
        self.ivf_nlist   = nlist;
        self.ivf_nprobe  = nprobe;
        self
    }

    /// Configure HNSW index.
    /// `ef_search` defaults to `m * 2` at search time if left at 0.
    /// Requires the `hnsw` Cargo feature.
    #[cfg(feature = "hnsw")]
    pub fn with_hnsw(mut self, m: usize, ef_construction: usize) -> Self {
        self.index_type           = IndexType::Hnsw;
        self.hnsw_m               = m;
        self.hnsw_ef_construction = ef_construction;
        self.hnsw_ef_search       = 0; // resolved to m*2 at search time
        self
    }

    /// Directly set the number of 4-bit dimensions (in padded space).
    ///
    /// `n4` is clamped to `padded_dim` and rounded down to the nearest multiple of 4
    /// (required for byte alignment). Use `QuantPipeline::optimal_n4_dims` to compute
    /// a data-driven value from a sample of real embeddings.
    pub fn with_n4_dims(mut self, n4: usize) -> Self {
        let padded_dim = self.dim.next_power_of_two();
        self.n4_dims   = (n4.min(padded_dim) / 4) * 4;
        self
    }

    /// Set mixed-precision quantization: `ratio` fraction of dimensions at 4-bit,
    /// remainder at 2-bit. `ratio = 1.0` is pure 4-bit; `ratio = 0.0` is pure 2-bit.
    ///
    /// `n4_dims` is rounded down to a multiple of 4 so that both 4-bit and 2-bit
    /// packing stay byte-aligned (4-bit needs even, 2-bit needs mult-of-4).
    pub fn with_mixed_ratio(mut self, ratio: f32) -> Self {
        let padded_dim = self.dim.next_power_of_two();
        let ratio = ratio.clamp(0.0, 1.0);
        let n4_raw = (padded_dim as f32 * ratio).round() as usize;
        // Round down to multiple of 4 — required for byte alignment in both packings.
        self.n4_dims = (n4_raw / 4) * 4;
        self
    }

    /// Recommended HNSW M value for a given corpus size.
    ///
    /// M controls per-node degree (graph connectivity). At N ≥ 1M, graph
    /// diameter grows large enough that M=32 causes greedy search to terminate
    /// at suboptimal local minima — M=64 restores recall.
    ///
    /// Empirically validated on glove-100 (1.18M): M=32 → recall 0.800,
    /// M=64 → recall 0.850 at identical QPS.
    pub fn recommended_m(n: usize) -> usize {
        if n >= 1_000_000 { 64 } else { 32 }
    }

    /// True when some dims are 4-bit and some are 2-bit (neither pure mode).
    pub fn is_mixed(&self) -> bool {
        let padded_dim = self.dim.next_power_of_two();
        self.n4_dims > 0 && self.n4_dims < padded_dim
    }
}

/// A single search result returned by [`MonaVec::search`].
///
/// Scores are always ordered higher-is-better regardless of metric:
/// cosine and dot return values in `[-1, 1]`; L2 returns negated squared distance.
#[derive(Debug, Clone, PartialEq)]
pub struct SearchResult {
    /// The ID originally supplied via [`MonaVec::add`].
    pub id:    u64,
    /// Similarity score (higher is more similar).
    pub score: f32,
}
