//! Node.js bindings for Ruvector via NAPI-RS
//!
//! High-performance Rust vector database with zero-copy buffer sharing,
//! async/await support, and complete TypeScript type definitions.

#![deny(clippy::all)]
#![warn(clippy::pedantic)]

use napi::bindgen_prelude::*;
use napi_derive::napi;
use ruvector_core::{
    types::{DbOptions, HnswConfig, QuantizationConfig},
    DistanceMetric, SearchQuery, SearchResult,
    VectorDB as CoreVectorDB, VectorEntry,
};
use std::sync::Arc;
use std::sync::RwLock;

/// Distance metric for similarity calculation
#[napi(string_enum)]
#[derive(Debug)]
pub enum JsDistanceMetric {
    /// Euclidean (L2) distance
    Euclidean,
    /// Cosine similarity (converted to distance)
    Cosine,
    /// Dot product (converted to distance for maximization)
    DotProduct,
    /// Manhattan (L1) distance
    Manhattan,
}

impl From<JsDistanceMetric> for DistanceMetric {
    fn from(metric: JsDistanceMetric) -> Self {
        match metric {
            JsDistanceMetric::Euclidean => DistanceMetric::Euclidean,
            JsDistanceMetric::Cosine => DistanceMetric::Cosine,
            JsDistanceMetric::DotProduct => DistanceMetric::DotProduct,
            JsDistanceMetric::Manhattan => DistanceMetric::Manhattan,
        }
    }
}

/// Quantization configuration
#[napi(object)]
#[derive(Debug)]
pub struct JsQuantizationConfig {
    /// Quantization type: "none", "scalar", "product", "binary"
    pub r#type: String,
    /// Number of subspaces (for product quantization)
    pub subspaces: Option<u32>,
    /// Codebook size (for product quantization)
    pub k: Option<u32>,
}

impl From<JsQuantizationConfig> for QuantizationConfig {
    fn from(config: JsQuantizationConfig) -> Self {
        match config.r#type.as_str() {
            "none" => QuantizationConfig::None,
            "scalar" => QuantizationConfig::Scalar,
            "product" => QuantizationConfig::Product {
                subspaces: config.subspaces.unwrap_or(16) as usize,
                k: config.k.unwrap_or(256) as usize,
            },
            "binary" => QuantizationConfig::Binary,
            _ => QuantizationConfig::Scalar,
        }
    }
}

/// HNSW index configuration
#[napi(object)]
#[derive(Debug)]
pub struct JsHnswConfig {
    /// Number of connections per layer (M)
    pub m: Option<u32>,
    /// Size of dynamic candidate list during construction
    pub ef_construction: Option<u32>,
    /// Size of dynamic candidate list during search
    pub ef_search: Option<u32>,
    /// Maximum number of elements
    pub max_elements: Option<u32>,
}

impl From<JsHnswConfig> for HnswConfig {
    fn from(config: JsHnswConfig) -> Self {
        HnswConfig {
            m: config.m.unwrap_or(32) as usize,
            ef_construction: config.ef_construction.unwrap_or(200) as usize,
            ef_search: config.ef_search.unwrap_or(100) as usize,
            max_elements: config.max_elements.unwrap_or(10_000_000) as usize,
        }
    }
}

/// Database configuration options
#[napi(object)]
#[derive(Debug)]
pub struct JsDbOptions {
    /// Vector dimensions
    pub dimensions: u32,
    /// Distance metric
    pub distance_metric: Option<JsDistanceMetric>,
    /// Storage path
    pub storage_path: Option<String>,
    /// HNSW configuration
    pub hnsw_config: Option<JsHnswConfig>,
    /// Quantization configuration
    pub quantization: Option<JsQuantizationConfig>,
}

impl From<JsDbOptions> for DbOptions {
    fn from(options: JsDbOptions) -> Self {
        DbOptions {
            dimensions: options.dimensions as usize,
            distance_metric: options
                .distance_metric
                .map(Into::into)
                .unwrap_or(DistanceMetric::Cosine),
            storage_path: options
                .storage_path
                .unwrap_or_else(|| "./ruvector.db".to_string()),
            hnsw_config: options.hnsw_config.map(Into::into),
            quantization: options.quantization.map(Into::into),
        }
    }
}

/// Vector entry
#[napi(object)]
pub struct JsVectorEntry {
    /// Optional ID (auto-generated if not provided)
    pub id: Option<String>,
    /// Vector data as Float32Array or array of numbers
    pub vector: Float32Array,
}

impl JsVectorEntry {
    fn to_core(&self) -> Result<VectorEntry> {
        Ok(VectorEntry {
            id: self.id.clone(),
            vector: self.vector.to_vec(),
            metadata: None,
        })
    }
}

/// Search query parameters
#[napi(object)]
pub struct JsSearchQuery {
    /// Query vector as Float32Array or array of numbers
    pub vector: Float32Array,
    /// Number of results to return (top-k)
    pub k: u32,
    /// Optional ef_search parameter for HNSW
    pub ef_search: Option<u32>,
}

impl JsSearchQuery {
    fn to_core(&self) -> Result<SearchQuery> {
        Ok(SearchQuery {
            vector: self.vector.to_vec(),
            k: self.k as usize,
            filter: None,
            ef_search: self.ef_search.map(|v| v as usize),
        })
    }
}

/// Search result with similarity score
#[napi(object)]
#[derive(Debug, Clone)]
pub struct JsSearchResult {
    /// Vector ID
    pub id: String,
    /// Distance/similarity score (lower is better for distance metrics)
    pub score: f64,
}

impl From<SearchResult> for JsSearchResult {
    fn from(result: SearchResult) -> Self {
        JsSearchResult {
            id: result.id,
            score: f64::from(result.score),
        }
    }
}

/// High-performance vector database with HNSW indexing
#[napi]
pub struct VectorDB {
    inner: Arc<RwLock<CoreVectorDB>>,
}

#[napi]
impl VectorDB {
    /// Create a new vector database with the given options
    ///
    /// # Example
    /// ```javascript
    /// const db = new VectorDB({
    ///   dimensions: 384,
    ///   distanceMetric: 'Cosine',
    ///   storagePath: './vectors.db',
    ///   hnswConfig: {
    ///     m: 32,
    ///     efConstruction: 200,
    ///     efSearch: 100
    ///   }
    /// });
    /// ```
    #[napi(constructor)]
    pub fn new(options: JsDbOptions) -> Result<Self> {
        let core_options: DbOptions = options.into();
        let db = CoreVectorDB::new(core_options)
            .map_err(|e| Error::from_reason(format!("Failed to create database: {}", e)))?;

        Ok(Self {
            inner: Arc::new(RwLock::new(db)),
        })
    }

    /// Create a vector database with default options
    ///
    /// # Example
    /// ```javascript
    /// const db = VectorDB.withDimensions(384);
    /// ```
    #[napi(factory)]
    pub fn with_dimensions(dimensions: u32) -> Result<Self> {
        let db = CoreVectorDB::with_dimensions(dimensions as usize)
            .map_err(|e| Error::from_reason(format!("Failed to create database: {}", e)))?;

        Ok(Self {
            inner: Arc::new(RwLock::new(db)),
        })
    }

    /// Insert a vector entry into the database
    ///
    /// Returns the ID of the inserted vector (auto-generated if not provided)
    ///
    /// # Example
    /// ```javascript
    /// const id = await db.insert({
    ///   vector: new Float32Array([1.0, 2.0, 3.0]),
    ///   metadata: { text: 'example' }
    /// });
    /// ```
    #[napi]
    pub async fn insert(&self, entry: JsVectorEntry) -> Result<String> {
        let core_entry = entry.to_core()?;
        let db = self.inner.clone();

        tokio::task::spawn_blocking(move || {
            let db = db.read().expect("RwLock poisoned");
            db.insert(core_entry)
        })
        .await
        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
        .map_err(|e| Error::from_reason(format!("Insert failed: {}", e)))
    }

    /// Insert multiple vectors in a batch
    ///
    /// Returns an array of IDs for the inserted vectors
    ///
    /// # Example
    /// ```javascript
    /// const ids = await db.insertBatch([
    ///   { vector: new Float32Array([1, 2, 3]) },
    ///   { vector: new Float32Array([4, 5, 6]) }
    /// ]);
    /// ```
    #[napi]
    pub async fn insert_batch(&self, entries: Vec<JsVectorEntry>) -> Result<Vec<String>> {
        let core_entries: Result<Vec<VectorEntry>> = entries
            .iter()
            .map(|e| e.to_core())
            .collect();
        let core_entries = core_entries?;
        let db = self.inner.clone();

        tokio::task::spawn_blocking(move || {
            let db = db.read().expect("RwLock poisoned");
            db.insert_batch(core_entries)
        })
        .await
        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
        .map_err(|e| Error::from_reason(format!("Batch insert failed: {}", e)))
    }

    /// Search for similar vectors
    ///
    /// Returns an array of search results sorted by similarity
    ///
    /// # Example
    /// ```javascript
    /// const results = await db.search({
    ///   vector: new Float32Array([1, 2, 3]),
    ///   k: 10,
    ///   filter: { category: 'example' }
    /// });
    /// ```
    #[napi]
    pub async fn search(&self, query: JsSearchQuery) -> Result<Vec<JsSearchResult>> {
        let core_query = query.to_core()?;
        let db = self.inner.clone();

        tokio::task::spawn_blocking(move || {
            let db = db.read().expect("RwLock poisoned");
            db.search(core_query)
        })
        .await
        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
        .map_err(|e| Error::from_reason(format!("Search failed: {}", e)))
        .map(|results| results.into_iter().map(Into::into).collect())
    }

    /// Delete a vector by ID
    ///
    /// Returns true if the vector was deleted, false if not found
    ///
    /// # Example
    /// ```javascript
    /// const deleted = await db.delete('vector-id');
    /// ```
    #[napi]
    pub async fn delete(&self, id: String) -> Result<bool> {
        let db = self.inner.clone();

        tokio::task::spawn_blocking(move || {
            let db = db.read().expect("RwLock poisoned");
            db.delete(&id)
        })
        .await
        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
        .map_err(|e| Error::from_reason(format!("Delete failed: {}", e)))
    }

    /// Get a vector by ID
    ///
    /// Returns the vector entry if found, null otherwise
    ///
    /// # Example
    /// ```javascript
    /// const entry = await db.get('vector-id');
    /// if (entry) {
    ///   console.log('Found:', entry.metadata);
    /// }
    /// ```
    #[napi]
    pub async fn get(&self, id: String) -> Result<Option<JsVectorEntry>> {
        let db = self.inner.clone();

        let result = tokio::task::spawn_blocking(move || {
            let db = db.read().expect("RwLock poisoned");
            db.get(&id)
        })
        .await
        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
        .map_err(|e| Error::from_reason(format!("Get failed: {}", e)))?;

        Ok(result.map(|entry| {
            JsVectorEntry {
                id: entry.id,
                vector: Float32Array::new(entry.vector),
            }
        }))
    }

    /// Get the number of vectors in the database
    ///
    /// # Example
    /// ```javascript
    /// const count = await db.len();
    /// console.log(`Database contains ${count} vectors`);
    /// ```
    #[napi]
    pub async fn len(&self) -> Result<u32> {
        let db = self.inner.clone();

        tokio::task::spawn_blocking(move || {
            let db = db.read().expect("RwLock poisoned");
            db.len()
        })
        .await
        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
        .map_err(|e| Error::from_reason(format!("Len failed: {}", e)))
        .map(|len| len as u32)
    }

    /// Check if the database is empty
    ///
    /// # Example
    /// ```javascript
    /// if (await db.isEmpty()) {
    ///   console.log('Database is empty');
    /// }
    /// ```
    #[napi]
    pub async fn is_empty(&self) -> Result<bool> {
        let db = self.inner.clone();

        tokio::task::spawn_blocking(move || {
            let db = db.read().expect("RwLock poisoned");
            db.is_empty()
        })
        .await
        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
        .map_err(|e| Error::from_reason(format!("IsEmpty failed: {}", e)))
    }
}

/// Get the version of the Ruvector library
#[napi]
pub fn version() -> String {
    env!("CARGO_PKG_VERSION").to_string()
}

/// Test function to verify the bindings are working
#[napi]
pub fn hello() -> String {
    "Hello from Ruvector Node.js bindings!".to_string()
}
