//! MimirsWell Encoding - Semantic vector generation and memory sanitization.
//!
//! ## Embedding Backend
//!
//! The `EmbeddingBackend` enum provides a unified interface for text-to-embedding
//! conversion with automatic fallback: Sleipnir (LFM2.5) → SemanticEncoder (BERT) → Dummy (hash).
//! All backends are always compiled; fallback happens at runtime if model files are missing.

pub mod sanitizer;
pub use sanitizer::SynNo;

#[cfg(feature = "inference")]
use hf_hub::{Repo, RepoType, api::sync::Api};
#[cfg(feature = "inference")]
use mimirs_core::MimirError;
#[cfg(feature = "inference")]
use mimirs_wyrd::{BertConfig, BertModel, Device, Tensor, VarBuilder};
#[cfg(feature = "inference")]
use tokenizers::Tokenizer;

#[cfg(feature = "inference")]
pub mod alignment;
#[cfg(feature = "inference")]
pub mod sleipnir;

#[cfg(feature = "inference")]
pub use alignment::ProcrustesAligner;
#[cfg(feature = "inference")]
pub use sleipnir::SleipnirEngine;

/// A lightweight semantic encoder using a BERT-style model.
#[allow(dead_code)]
#[cfg(feature = "inference")]
pub struct SemanticEncoder {
    model: BertModel,
    tokenizer: Tokenizer,
    device: Device,
    /// Optional alignment for cognitive fidelity (Layer 1)
    alignment: Option<ProcrustesAligner>,
}

#[cfg(feature = "inference")]
impl SemanticEncoder {
    /// Loads the default encoder (all-MiniLM-L6-v2) from HuggingFace.
    pub fn load_default() -> Result<Self, MimirError> {
        let api = Api::new().map_err(|e| MimirError::Internal(e.to_string()))?;
        let repo = api.repo(Repo::new(
            "sentence-transformers/all-MiniLM-L6-v2".to_string(),
            RepoType::Model,
        ));

        let config_filename = repo
            .get("config.json")
            .map_err(|e| MimirError::Internal(e.to_string()))?;
        let tokenizer_filename = repo
            .get("tokenizer.json")
            .map_err(|e| MimirError::Internal(e.to_string()))?;
        let weights_filename = repo
            .get("model.safetensors")
            .map_err(|e| MimirError::Internal(e.to_string()))?;

        let device = Device::Cpu;
        let config_str = std::fs::read_to_string(config_filename)
            .map_err(|e| MimirError::Internal(e.to_string()))?;
        let config: BertConfig =
            serde_json::from_str(&config_str).map_err(|e| MimirError::Internal(e.to_string()))?;

        let tokenizer = Tokenizer::from_file(tokenizer_filename)
            .map_err(|e| MimirError::Internal(format!("Tokenizer error: {}", e)))?;

        // SAFETY: safetensors files are memory-mapped read-only.
        let vb = unsafe {
            VarBuilder::from_mmaped_safetensors(
                &[weights_filename],
                mimirs_wyrd::DType::F32,
                &device,
            )
            .map_err(|e| MimirError::Internal(format!("VarBuilder error: {}", e)))?
        };

        let model = BertModel::load(vb, &config)
            .map_err(|e| MimirError::Internal(format!("Model load error: {}", e)))?;

        Ok(Self {
            model,
            tokenizer,
            device,
            alignment: None,
        })
    }

    /// Encodes text into a semantic vector.
    pub fn encode(&self, text: &str) -> Result<Vec<f32>, MimirError> {
        // Module trait provides .forward() via trait-dispatch
        #[allow(unused_imports)]
        use mimirs_wyrd::Module;
        let tokens = self
            .tokenizer
            .encode(text, true)
            .map_err(|e| MimirError::Validation(format!("Tokenization failed: {}", e)))?;

        let token_ids = tokens.get_ids();
        let token_ids_tensor = Tensor::new(token_ids, &self.device)
            .map_err(|e| MimirError::Internal(format!("Tensor creation failed: {}", e)))?
            .unsqueeze(0)
            .map_err(|e| MimirError::Internal(format!("Tensor unsqueeze failed: {}", e)))?;

        let token_type_ids = vec![0u32; token_ids.len()];
        let token_type_ids_tensor = Tensor::new(token_type_ids.as_slice(), &self.device)
            .map_err(|e| MimirError::Internal(format!("Token type IDs failed: {}", e)))?
            .unsqueeze(0)
            .map_err(|e| MimirError::Internal(format!("Token type unsqueeze failed: {}", e)))?;

        let ys = self
            .model
            .forward(&token_ids_tensor, &token_type_ids_tensor, None)
            .map_err(|e| MimirError::Internal(format!("Model forward pass failed: {}", e)))?;

        let (_batch, seq_len, hidden_size) = ys
            .dims3()
            .map_err(|e| MimirError::Internal(format!("Unexpected model output dims: {}", e)))?;

        let mean_pooled = ys
            .sum(1)
            .map_err(|e| MimirError::Internal(format!("Sum pooling failed: {}", e)))?
            .reshape(&[hidden_size])
            .map_err(|e| MimirError::Internal(format!("Reshape failed: {}", e)))?
            .affine(1.0 / (seq_len as f64), 0.0)
            .map_err(|e| MimirError::Internal(format!("Scale failed: {}", e)))?;

        let l2_norm = mean_pooled
            .sqr()
            .map_err(|e| MimirError::Internal(format!("Sqr failed: {}", e)))?
            .sum_all()
            .map_err(|e| MimirError::Internal(format!("Sum all failed: {}", e)))?
            .to_scalar::<f32>()
            .map_err(|e| MimirError::Internal(format!("To scalar failed: {}", e)))?
            .sqrt();

        let mean_pooled = if l2_norm > 1e-12 {
            mean_pooled
                .affine(1.0 / (l2_norm as f64), 0.0)
                .map_err(|e| MimirError::Internal(format!("Normalize failed: {}", e)))?
        } else {
            mean_pooled
        };

        let vector: Vec<f32> = mean_pooled
            .to_vec1()
            .map_err(|e| MimirError::Internal(format!("Tensor to vec failed: {}", e)))?;

        Ok(vector)
    }
}

/// Thread-safe shared encoder.
#[cfg(feature = "inference")]
pub type SharedEncoder = std::sync::Arc<SemanticEncoder>;

#[cfg(feature = "inference")]
#[allow(dead_code)]
fn _assert_shared_encoder_send_sync() {
    fn check<T: Send + Sync>() {}
    check::<SemanticEncoder>();
}

// ═══════════════════════════════════════════════════════════════════════════
// EmbeddingBackend — Unified interface with automatic fallback
// ═══════════════════════════════════════════════════════════════════════════

/// Errors from the embedding backend.
#[derive(Debug, thiserror::Error)]
pub enum EmbedError {
    /// All backends failed.
    #[error("All embedding backends failed: {0}")]
    AllFailed(String),
    /// Model file not found.
    #[error("Model file not found: {0}")]
    ModelNotFound(String),
    /// Encoding failed.
    #[error("Encoding failed: {0}")]
    Encoding(String),
}

/// Unified embedding backend that tries real inference first, falls back to dummy.
///
/// ## Fallback Order
/// 1. **Sleipnir** (LFM2.5-VL-450M) — highest quality, requires GGUF model file
/// 2. **SemanticEncoder** (BERT) — good quality, requires safetensors model file
/// 3. **Dummy** (hash-based) — always available, deterministic, no model required
///
/// The backend is selected at construction time based on which model files exist.
/// This is a runtime decision, not a compile-time feature flag.
pub enum EmbeddingBackend {
    /// Full LFM2.5 inference via Sleipnir engine.
    #[cfg(feature = "inference")]
    Sleipnir {
        /// The Sleipnir engine.
        engine: std::sync::Arc<SleipnirEngine>,
    },
    /// BERT-style inference via SemanticEncoder.
    #[cfg(feature = "inference")]
    Semantic {
        /// The semantic encoder.
        encoder: std::sync::Arc<SemanticEncoder>,
    },
    /// Dummy hash-based embedding (always available).
    Dummy {
        /// Embedding dimension for the dummy backend.
        dim: usize,
    },
}

impl EmbeddingBackend {
    /// Creates the best available embedding backend.
    ///
    /// Tries Sleipnir first (if model exists), then SemanticEncoder (if model exists),
    /// then falls back to Dummy. Logs which backend was selected.
    ///
    /// # Parameters
    /// - `sleipnir_model_path`: Path to the Sleipnir LFM2.5 GGUF file
    /// - `dim`: Embedding dimension for the dummy fallback
    #[cfg(feature = "inference")]
    pub fn auto(sleipnir_model_path: &std::path::Path, dim: usize) -> Self {
        // Try Sleipnir first
        if sleipnir_model_path.exists() {
            match SleipnirEngine::load(sleipnir_model_path, Device::Cpu) {
                Ok(engine) => {
                    tracing::info!(
                        path = %sleipnir_model_path.display(),
                        "EmbeddingBackend: using Sleipnir (LFM2.5)"
                    );
                    return Self::Sleipnir {
                        engine: std::sync::Arc::new(engine),
                    };
                }
                Err(e) => {
                    tracing::warn!(
                        path = %sleipnir_model_path.display(),
                        error = %e,
                        "Sleipnir model found but failed to load, trying SemanticEncoder"
                    );
                }
            }
        }

        // Try SemanticEncoder second
        match SemanticEncoder::load_default() {
            Ok(encoder) => {
                tracing::info!("EmbeddingBackend: using SemanticEncoder (BERT)");
                return Self::Semantic {
                    encoder: std::sync::Arc::new(encoder),
                };
            }
            Err(e) => {
                tracing::warn!(
                    error = %e,
                    "SemanticEncoder failed to load, falling back to Dummy"
                );
            }
        }

        // Fall back to dummy
        tracing::info!(
            dim,
            "EmbeddingBackend: using Dummy (hash-based) — no model files available"
        );
        Self::Dummy { dim }
    }

    /// Creates the best available embedding backend (without inference — always Dummy).
    #[cfg(not(feature = "inference"))]
    pub fn auto(_sleipnir_model_path: &std::path::Path, dim: usize) -> Self {
        tracing::info!(
            dim,
            "EmbeddingBackend: using Dummy (inference feature disabled)"
        );
        Self::Dummy { dim }
    }

    /// Embeds text into a semantic vector.
    pub fn embed(&self, text: &str) -> Result<Vec<f32>, EmbedError> {
        match self {
            #[cfg(feature = "inference")]
            Self::Sleipnir { engine } => engine
                .encode(text)
                .map_err(|e| EmbedError::Encoding(e.to_string())),
            #[cfg(feature = "inference")]
            Self::Semantic { encoder } => encoder
                .encode(text)
                .map_err(|e| EmbedError::Encoding(e.to_string())),
            Self::Dummy { dim } => Ok(dummy_embedding(text, *dim)),
        }
    }

    /// Embeds text into a DensityMemory state via the quantum substrate.
    pub fn embed_to_density(&self, text: &str) -> Result<mimirs_core::DensityMemory, EmbedError> {
        let vec = self.embed(text)?;
        mimirs_core::DensityMemory::from_pure(&vec).map_err(|e| EmbedError::Encoding(e.to_string()))
    }

    /// Returns true if this backend uses real inference (not dummy).
    pub fn is_real(&self) -> bool {
        match self {
            #[cfg(feature = "inference")]
            Self::Sleipnir { .. } => true,
            #[cfg(feature = "inference")]
            Self::Semantic { .. } => true,
            Self::Dummy { .. } => false,
        }
    }

    /// Returns the name of the active backend.
    pub fn backend_name(&self) -> &'static str {
        match self {
            #[cfg(feature = "inference")]
            Self::Sleipnir { .. } => "sleipnir",
            #[cfg(feature = "inference")]
            Self::Semantic { .. } => "semantic",
            Self::Dummy { .. } => "dummy",
        }
    }

    /// Extracts unstructured PHI/PII entities from text if the Sleipnir backend is active.
    ///
    /// WHY: Delegates to SleipnirEngine's LFM2.5 model generation capabilities.
    ///
    /// CONTRACT: Returns a list of (entity, placeholder) pairs.
    pub fn extract_phi_entities(&self, text: &str) -> Vec<(String, String)> {
        #[cfg(feature = "inference")]
        {
            if let Self::Sleipnir { engine } = self
                && let Ok(entities) = engine.extract_phi_entities(text)
            {
                return entities;
            }
        }
        Vec::new()
    }
}

/// Produces a deterministic hash-based embedding.
///
/// This is the fallback when no model files are available.
/// Uses FxHasher for speed. The embedding is deterministic but not semantic.
fn dummy_embedding(content: &str, dim: usize) -> Vec<f32> {
    use rustc_hash::FxHasher;
    use std::hash::{Hash, Hasher};
    let mut hasher = FxHasher::default();
    content.hash(&mut hasher);
    let seed = hasher.finish();

    let mut embedding = vec![0.0f32; dim];
    for (i, v) in embedding.iter_mut().enumerate().take(dim) {
        *v = ((seed.wrapping_add(i as u64)).wrapping_mul(0x632BE59BD9B4E019)) as f32
            / u64::MAX as f32;
    }
    embedding
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_dummy_embedding_deterministic() {
        let a = dummy_embedding("hello world", 128);
        let b = dummy_embedding("hello world", 128);
        assert_eq!(a, b);
    }

    #[test]
    fn test_dummy_embedding_different_inputs() {
        let a = dummy_embedding("hello", 128);
        let b = dummy_embedding("world", 128);
        assert_ne!(a, b);
    }

    #[test]
    fn test_dummy_embedding_dimension() {
        let a = dummy_embedding("test", 384);
        assert_eq!(a.len(), 384);
    }

    #[test]
    fn test_embedding_backend_availability() {
        let backend = EmbeddingBackend::auto("/nonexistent/path.gguf".as_ref(), 128);
        let vec = backend.embed("test").unwrap();
        assert!(!vec.is_empty(), "Should produce an embedding");
        println!("Backend: {}, dim: {}", backend.backend_name(), vec.len());
    }

    #[test]
    fn test_embedding_backend_to_density() {
        let backend = EmbeddingBackend::auto("/nonexistent/path.gguf".as_ref(), 4);
        let rho = backend.embed_to_density("test").unwrap();
        assert!(rho.dimension() > 0, "Should have valid dimension");
        let m = rho.to_matrix();
        assert!((m.trace() - 1.0).abs() < 1e-4);
        println!("Density: dim={}, trace={:.4}", rho.dimension(), m.trace());
    }
}
