//! Embeddings service for text encoding
//!
//! Provides functionality to convert text into dense vector embeddings
//! for semantic similarity and retrieval tasks.
//!
//! Uses deterministic hashing for embedding generation. For production,
//! integrate with actual ML models like fastembed or OpenAI's API.

use crate::error::{ErrorWrapper, Result};
use serde::{Deserialize, Serialize};
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};

/// Embedding model types supported by the system
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum EmbeddingModel {
    /// BAAI/bge-small-en-v1.5 (384 dimensions, faster)
    BgeSmallEn,
    /// BAAI/bge-base-en-v1.5 (768 dimensions, better quality)
    BgeBaseEn,
    /// Multilingual BAAI/bge-m3 (1024 dimensions)
    BgeM3,
}

impl EmbeddingModel {
    /// Get the HuggingFace model identifier
    pub fn model_name(&self) -> &'static str {
        match self {
            EmbeddingModel::BgeSmallEn => "BAAI/bge-small-en-v1.5",
            EmbeddingModel::BgeBaseEn => "BAAI/bge-base-en-v1.5",
            EmbeddingModel::BgeM3 => "BAAI/bge-m3",
        }
    }

    /// Get embedding dimension size
    pub fn dimension(&self) -> usize {
        match self {
            EmbeddingModel::BgeSmallEn => 384,
            EmbeddingModel::BgeBaseEn => 768,
            EmbeddingModel::BgeM3 => 1024,
        }
    }

    /// Convert to string representation
    pub fn as_str(&self) -> &'static str {
        match self {
            EmbeddingModel::BgeSmallEn => "BgeSmallEn",
            EmbeddingModel::BgeBaseEn => "BgeBaseEn",
            EmbeddingModel::BgeM3 => "BgeM3",
        }
    }

    /// Parse from string representation
    pub fn parse(s: &str) -> Self {
        match s {
            "BgeBaseEn" => EmbeddingModel::BgeBaseEn,
            "BgeM3" => EmbeddingModel::BgeM3,
            _ => EmbeddingModel::BgeSmallEn, // Default fallback
        }
    }
}

/// Embedding vector with metadata
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Embedding {
    /// The embedding vector (dense float array)
    pub vector: Vec<f32>,
    /// Original text that was embedded
    pub text: String,
    /// Model used to generate this embedding
    pub model: EmbeddingModel,
}

impl Embedding {
    /// Calculate cosine similarity between this embedding and another
    pub fn cosine_similarity(&self, other: &Embedding) -> f32 {
        if self.vector.is_empty() || other.vector.is_empty() {
            return 0.0;
        }

        let dot: f32 = self
            .vector
            .iter()
            .zip(&other.vector)
            .map(|(a, b)| a * b)
            .sum();

        let norm_a: f32 = self.vector.iter().map(|x| x * x).sum::<f32>().sqrt();
        let norm_b: f32 = other.vector.iter().map(|x| x * x).sum::<f32>().sqrt();

        if norm_a == 0.0 || norm_b == 0.0 {
            0.0
        } else {
            dot / (norm_a * norm_b)
        }
    }
}

/// Embeddings service for encoding text using deterministic hashing
///
/// Uses a hash-based approach for generating embeddings. For production,
/// replace with actual ML model integration (fastembed, OpenAI, etc).
#[cfg(feature = "ai_backend")]
pub struct EmbeddingsService {
    model_type: EmbeddingModel,
}

#[cfg(feature = "ai_backend")]
impl EmbeddingsService {
    /// Create a new embeddings service with specified model
    pub fn new(model_type: EmbeddingModel) -> Result<Self> {
        Ok(EmbeddingsService { model_type })
    }

    /// Embed a single text using deterministic hashing
    pub fn embed(&self, text: &str) -> Result<Embedding> {
        if text.is_empty() {
            return Err(ErrorWrapper::validation_failed("Cannot embed empty text"));
        }

        let vector = Self::hash_to_embedding(text, self.model_type.dimension());

        Ok(Embedding {
            vector,
            text: text.to_string(),
            model: self.model_type,
        })
    }

    /// Embed multiple texts
    pub fn embed_batch(&self, texts: &[&str]) -> Result<Vec<Embedding>> {
        if texts.is_empty() {
            return Err(ErrorWrapper::validation_failed("Cannot embed empty batch"));
        }

        Ok(texts
            .iter()
            .map(|text| {
                let vector = Self::hash_to_embedding(text, self.model_type.dimension());
                Embedding {
                    vector,
                    text: text.to_string(),
                    model: self.model_type,
                }
            })
            .collect())
    }

    /// Get the model type
    pub fn model_type(&self) -> EmbeddingModel {
        self.model_type
    }

    /// Get embedding dimension
    pub fn dimension(&self) -> usize {
        self.model_type.dimension()
    }

    /// Generate embedding vector from text hash
    fn hash_to_embedding(text: &str, dim: usize) -> Vec<f32> {
        let mut vector = vec![0.0; dim];

        // Use each word in the text to generate different hash values
        for (i, word) in text.split_whitespace().enumerate() {
            let mut hasher = DefaultHasher::new();
            word.hash(&mut hasher);
            let hash = hasher.finish();

            // Distribute hash bits across the vector
            for (j, vec_elem) in vector.iter_mut().enumerate() {
                let bit_index = (i * 64 + j) % 64;
                let bit = (hash >> bit_index) & 1;
                *vec_elem += if bit == 1 { 1.0 } else { -1.0 };
            }
        }

        // Normalize vector
        let norm: f32 = vector.iter().map(|x| x * x).sum::<f32>().sqrt();
        if norm > 0.0 {
            vector.iter_mut().for_each(|x| *x /= norm);
        }

        vector
    }
}

#[cfg(all(test, feature = "ai_backend"))]
mod tests {
    use super::*;

    #[test]
    fn test_embedding_model_names() {
        assert_eq!(
            EmbeddingModel::BgeSmallEn.model_name(),
            "BAAI/bge-small-en-v1.5"
        );
        assert_eq!(
            EmbeddingModel::BgeBaseEn.model_name(),
            "BAAI/bge-base-en-v1.5"
        );
        assert_eq!(EmbeddingModel::BgeM3.model_name(), "BAAI/bge-m3");
    }

    #[test]
    fn test_embedding_dimensions() {
        assert_eq!(EmbeddingModel::BgeSmallEn.dimension(), 384);
        assert_eq!(EmbeddingModel::BgeBaseEn.dimension(), 768);
        assert_eq!(EmbeddingModel::BgeM3.dimension(), 1024);
    }

    #[test]
    fn test_cosine_similarity() {
        let emb1 = Embedding {
            vector: vec![1.0, 0.0, 0.0],
            text: "test1".to_string(),
            model: EmbeddingModel::BgeSmallEn,
        };

        let emb2 = Embedding {
            vector: vec![1.0, 0.0, 0.0],
            text: "test2".to_string(),
            model: EmbeddingModel::BgeSmallEn,
        };

        let similarity = emb1.cosine_similarity(&emb2);
        assert!((similarity - 1.0).abs() < 0.001);
    }

    #[test]
    fn test_cosine_similarity_orthogonal() {
        let emb1 = Embedding {
            vector: vec![1.0, 0.0, 0.0],
            text: "test1".to_string(),
            model: EmbeddingModel::BgeSmallEn,
        };

        let emb2 = Embedding {
            vector: vec![0.0, 1.0, 0.0],
            text: "test2".to_string(),
            model: EmbeddingModel::BgeSmallEn,
        };

        let similarity = emb1.cosine_similarity(&emb2);
        assert!(similarity.abs() < 0.001);
    }

    #[test]
    fn test_cosine_similarity_opposite() {
        let emb1 = Embedding {
            vector: vec![1.0, 0.0, 0.0],
            text: "test1".to_string(),
            model: EmbeddingModel::BgeSmallEn,
        };

        let emb2 = Embedding {
            vector: vec![-1.0, 0.0, 0.0],
            text: "test2".to_string(),
            model: EmbeddingModel::BgeSmallEn,
        };

        let similarity = emb1.cosine_similarity(&emb2);
        assert!((similarity + 1.0).abs() < 0.001);
    }
}
