//! LLM Gateway — Provider & Model CRUD Storage (Phase-78: T78.2 + T78.3)
//!
//! Provides persistent storage for Provider and Model registrations using
//! DashMap (concurrent in-memory) + JSON file persistence.
//!
//! Features:
//! - Thread-safe concurrent CRUD (DashMap)
//! - API Key obfuscation (XOR-based, not production-grade crypto)
//! - JSON file persistence (auto-save on write, lazy-load on first read)
//! - Filter by brand/status

use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};

use dashmap::DashMap;
use serde::{Deserialize, Serialize};
use tracing::{debug, warn};

use super::error::{LlmError, LlmResult};
use super::types::{
    LlmModel, LlmProvider, ModelStatus, ProviderBrand, ProviderStatus,
};

// ============================================================================
// API Key Obfuscation
// ============================================================================

/// Simple XOR-based obfuscation key (32 bytes).
///
/// **Note**: This is NOT cryptographic encryption. For production deployments
/// with sensitive API keys, integrate with a proper secrets manager (e.g.,
/// HashiCorp Vault, AWS Secrets Manager) or use AES-256-GCM.
///
/// This obfuscation prevents plaintext API keys from appearing in JSON files
/// on disk, which is sufficient for development and single-user scenarios.
const OBFUSCATION_KEY: &[u8; 32] = b"aifastdb-llm-gateway-key-v1!@#%$";

/// Obfuscate a string using XOR with the key.
fn obfuscate(input: &str) -> String {
    let bytes: Vec<u8> = input
        .as_bytes()
        .iter()
        .enumerate()
        .map(|(i, b)| b ^ OBFUSCATION_KEY[i % OBFUSCATION_KEY.len()])
        .collect();
    // Encode as hex for safe JSON storage
    bytes.iter().map(|b| format!("{b:02x}")).collect()
}

/// Deobfuscate a hex string using XOR with the key.
fn deobfuscate(hex: &str) -> LlmResult<String> {
    if hex.len() % 2 != 0 {
        return Err(LlmError::EncryptionError("Invalid hex length".into()));
    }
    let bytes: Vec<u8> = (0..hex.len())
        .step_by(2)
        .map(|i| {
            u8::from_str_radix(&hex[i..i + 2], 16)
                .map_err(|e| LlmError::EncryptionError(format!("Invalid hex: {e}")))
        })
        .collect::<LlmResult<Vec<u8>>>()?;

    let plain: Vec<u8> = bytes
        .iter()
        .enumerate()
        .map(|(i, b)| b ^ OBFUSCATION_KEY[i % OBFUSCATION_KEY.len()])
        .collect();

    String::from_utf8(plain)
        .map_err(|e| LlmError::EncryptionError(format!("Invalid UTF-8: {e}")))
}

// ============================================================================
// Persisted Provider (with obfuscated API key)
// ============================================================================

/// Provider as stored on disk (api_key obfuscated).
#[derive(Debug, Clone, Serialize, Deserialize)]
struct PersistedProvider {
    #[serde(flatten)]
    provider: LlmProvider,
    /// Obfuscated API key (replaces api_key field during persistence).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    encrypted_api_key: Option<String>,
}

impl PersistedProvider {
    fn from_provider(mut provider: LlmProvider) -> Self {
        let encrypted = provider.api_key.as_ref().map(|k| obfuscate(k));
        // Clear plaintext key for persisted form
        provider.api_key = None;
        Self {
            provider,
            encrypted_api_key: encrypted,
        }
    }

    fn into_provider(mut self) -> LlmResult<LlmProvider> {
        if let Some(enc) = self.encrypted_api_key {
            self.provider.api_key = Some(deobfuscate(&enc)?);
        }
        Ok(self.provider)
    }
}

// ============================================================================
// Store File Format
// ============================================================================

#[derive(Debug, Default, Serialize, Deserialize)]
struct StoreFile {
    providers: Vec<PersistedProvider>,
    models: Vec<LlmModel>,
}

// ============================================================================
// LlmStore — Unified Provider + Model Store
// ============================================================================

/// Thread-safe concurrent store for LLM Providers and Models.
///
/// Uses `DashMap` for lock-free concurrent access and persists to a JSON file.
///
/// # Example
///
/// ```no_run
/// use aifastdb_core::llm::store::LlmStore;
/// use aifastdb_core::llm::{LlmProvider, ProviderBrand};
///
/// let store = LlmStore::open("./data/llm").unwrap();
/// let provider = LlmProvider::new("ollama", "Ollama", ProviderBrand::Ollama, "http://localhost:11434");
/// store.upsert_provider(provider).unwrap();
/// ```
pub struct LlmStore {
    providers: DashMap<String, LlmProvider>,
    models: DashMap<String, LlmModel>,
    file_path: PathBuf,
    dirty: AtomicBool,
}

impl LlmStore {
    /// Open or create an LLM store at the given directory.
    ///
    /// The store file is `<dir>/llm_registry.json`.
    pub fn open(dir: impl AsRef<Path>) -> LlmResult<Self> {
        let dir = dir.as_ref();
        std::fs::create_dir_all(dir).map_err(|e| {
            LlmError::StorageError(format!("Failed to create LLM store dir {}: {e}", dir.display()))
        })?;

        let file_path = dir.join("llm_registry.json");
        let store = Self {
            providers: DashMap::new(),
            models: DashMap::new(),
            file_path,
            dirty: AtomicBool::new(false),
        };

        store.load()?;
        Ok(store)
    }

    // ── Provider CRUD ──────────────────────────────────────────

    /// Insert or update a provider. Overwrites existing provider with same ID.
    pub fn upsert_provider(&self, provider: LlmProvider) -> LlmResult<()> {
        debug!(provider_id = %provider.id, brand = %provider.brand, "Upserting LLM provider");
        self.providers.insert(provider.id.clone(), provider);
        self.mark_dirty_and_save()
    }

    /// Get a provider by ID (with plaintext API key).
    pub fn get_provider(&self, id: &str) -> Option<LlmProvider> {
        self.providers.get(id).map(|r| r.value().clone())
    }

    /// List all providers, optionally filtered by brand and/or status.
    pub fn list_providers(
        &self,
        brand: Option<&ProviderBrand>,
        status: Option<ProviderStatus>,
    ) -> Vec<LlmProvider> {
        self.providers
            .iter()
            .filter(|r| {
                let p = r.value();
                brand.map_or(true, |b| &p.brand == b) && status.map_or(true, |s| p.status == s)
            })
            .map(|r| r.value().clone())
            .collect()
    }

    /// Delete a provider by ID. Also removes all models associated with this provider.
    pub fn delete_provider(&self, id: &str) -> LlmResult<bool> {
        let existed = self.providers.remove(id).is_some();
        if existed {
            // Cascade: remove models belonging to this provider
            let model_ids: Vec<String> = self
                .models
                .iter()
                .filter(|r| r.value().provider_id == id)
                .map(|r| r.key().clone())
                .collect();
            for mid in &model_ids {
                self.models.remove(mid);
            }
            if !model_ids.is_empty() {
                debug!(
                    provider_id = id,
                    removed_models = model_ids.len(),
                    "Cascade-deleted models for removed provider"
                );
            }
            self.mark_dirty_and_save()?;
        }
        Ok(existed)
    }

    /// Update a provider's status.
    pub fn set_provider_status(&self, id: &str, status: ProviderStatus) -> LlmResult<bool> {
        if let Some(mut entry) = self.providers.get_mut(id) {
            entry.status = status;
            entry.updated_at = now_ms();
            drop(entry);
            self.mark_dirty_and_save()?;
            Ok(true)
        } else {
            Ok(false)
        }
    }

    /// Update a provider's health status (called by HealthMonitor).
    pub fn set_provider_health(&self, id: &str, is_healthy: bool) -> bool {
        if let Some(mut entry) = self.providers.get_mut(id) {
            entry.is_healthy = is_healthy;
            entry.updated_at = now_ms();
            // Health status changes are frequent; don't persist immediately
            self.dirty.store(true, Ordering::Relaxed);
            true
        } else {
            false
        }
    }

    /// Count providers.
    pub fn provider_count(&self) -> usize {
        self.providers.len()
    }

    // ── Model CRUD ─────────────────────────────────────────────

    /// Insert or update a model.
    pub fn upsert_model(&self, model: LlmModel) -> LlmResult<()> {
        // Validate provider exists
        if !self.providers.contains_key(&model.provider_id) {
            return Err(LlmError::ProviderNotFound(format!(
                "Cannot register model '{}': provider '{}' not found",
                model.id, model.provider_id
            )));
        }
        debug!(model_id = %model.id, provider = %model.provider_id, "Upserting LLM model");
        self.models.insert(model.id.clone(), model);
        self.mark_dirty_and_save()
    }

    /// Get a model by ID.
    pub fn get_model(&self, id: &str) -> Option<LlmModel> {
        self.models.get(id).map(|r| r.value().clone())
    }

    /// List models, optionally filtered by provider_id and/or status.
    pub fn list_models(
        &self,
        provider_id: Option<&str>,
        status: Option<ModelStatus>,
    ) -> Vec<LlmModel> {
        self.models
            .iter()
            .filter(|r| {
                let m = r.value();
                provider_id.map_or(true, |pid| m.provider_id == pid)
                    && status.map_or(true, |s| m.status == s)
            })
            .map(|r| r.value().clone())
            .collect()
    }

    /// Delete a model by ID.
    pub fn delete_model(&self, id: &str) -> LlmResult<bool> {
        let existed = self.models.remove(id).is_some();
        if existed {
            self.mark_dirty_and_save()?;
        }
        Ok(existed)
    }

    /// Set a model as the default (unsets previous default).
    pub fn set_default_model(&self, id: &str) -> LlmResult<bool> {
        if !self.models.contains_key(id) {
            return Ok(false);
        }
        // Unset all defaults
        for mut entry in self.models.iter_mut() {
            entry.is_default = false;
        }
        // Set new default
        if let Some(mut entry) = self.models.get_mut(id) {
            entry.is_default = true;
            entry.updated_at = now_ms();
        }
        self.mark_dirty_and_save()?;
        Ok(true)
    }

    /// Get the current default model.
    pub fn get_default_model(&self) -> Option<LlmModel> {
        self.models
            .iter()
            .find(|r| r.value().is_default)
            .map(|r| r.value().clone())
    }

    /// Find models that can serve a given model name across all providers.
    ///
    /// Returns `(provider, model)` pairs where model.name matches.
    pub fn find_models_by_name(&self, model_name: &str) -> Vec<(LlmProvider, LlmModel)> {
        self.models
            .iter()
            .filter(|r| {
                let m = r.value();
                m.name == model_name && m.status == ModelStatus::Active
            })
            .filter_map(|r| {
                let m = r.value().clone();
                self.providers.get(&m.provider_id).map(|p| (p.value().clone(), m))
            })
            .collect()
    }

    /// Count models.
    pub fn model_count(&self) -> usize {
        self.models.len()
    }

    // ── Persistence ────────────────────────────────────────────

    /// Force save to disk.
    pub fn save(&self) -> LlmResult<()> {
        let data = StoreFile {
            providers: self
                .providers
                .iter()
                .map(|r| PersistedProvider::from_provider(r.value().clone()))
                .collect(),
            models: self.models.iter().map(|r| r.value().clone()).collect(),
        };

        let json = serde_json::to_string_pretty(&data)
            .map_err(|e| LlmError::SerializationError(e.to_string()))?;

        std::fs::write(&self.file_path, json).map_err(|e| {
            LlmError::StorageError(format!(
                "Failed to write LLM store to {}: {e}",
                self.file_path.display()
            ))
        })?;

        self.dirty.store(false, Ordering::Relaxed);
        debug!(path = %self.file_path.display(), "LLM store saved");
        Ok(())
    }

    /// Load from disk (called once during open).
    fn load(&self) -> LlmResult<()> {
        if !self.file_path.exists() {
            debug!(path = %self.file_path.display(), "LLM store file not found, starting fresh");
            return Ok(());
        }

        let json = std::fs::read_to_string(&self.file_path).map_err(|e| {
            LlmError::StorageError(format!(
                "Failed to read LLM store from {}: {e}",
                self.file_path.display()
            ))
        })?;

        let data: StoreFile = serde_json::from_str(&json).map_err(|e| {
            LlmError::SerializationError(format!("Failed to parse LLM store: {e}"))
        })?;

        for pp in data.providers {
            match pp.into_provider() {
                Ok(provider) => {
                    self.providers.insert(provider.id.clone(), provider);
                }
                Err(e) => {
                    warn!("Failed to load provider: {e}");
                }
            }
        }

        for model in data.models {
            self.models.insert(model.id.clone(), model);
        }

        debug!(
            path = %self.file_path.display(),
            providers = self.providers.len(),
            models = self.models.len(),
            "LLM store loaded"
        );
        Ok(())
    }

    /// Mark dirty and auto-save.
    fn mark_dirty_and_save(&self) -> LlmResult<()> {
        self.dirty.store(true, Ordering::Relaxed);
        self.save()
    }

    /// Flush pending changes to disk (if dirty).
    pub fn flush(&self) -> LlmResult<()> {
        if self.dirty.load(Ordering::Relaxed) {
            self.save()
        } else {
            Ok(())
        }
    }
}

impl std::fmt::Debug for LlmStore {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("LlmStore")
            .field("providers", &self.providers.len())
            .field("models", &self.models.len())
            .field("file_path", &self.file_path)
            .finish()
    }
}

fn now_ms() -> i64 {
    use std::time::{SystemTime, UNIX_EPOCH};
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default()
        .as_millis() as i64
}

// ============================================================================
// Tests
// ============================================================================

#[cfg(test)]
mod tests {
    use super::*;
    use crate::llm::types::{ModelCapabilities, ModelPricing};

    fn temp_store() -> (tempfile::TempDir, LlmStore) {
        let dir = tempfile::tempdir().unwrap();
        let store = LlmStore::open(dir.path().join("llm")).unwrap();
        (dir, store)
    }

    #[test]
    fn test_obfuscate_deobfuscate() {
        let key = "sk-abc123456789xyz";
        let enc = obfuscate(key);
        assert_ne!(enc, key);
        let dec = deobfuscate(&enc).unwrap();
        assert_eq!(dec, key);
    }

    #[test]
    fn test_obfuscate_empty() {
        let enc = obfuscate("");
        let dec = deobfuscate(&enc).unwrap();
        assert_eq!(dec, "");
    }

    #[test]
    fn test_provider_crud() {
        let (_dir, store) = temp_store();

        // Create
        let p = LlmProvider::new("ollama-local", "Ollama", ProviderBrand::Ollama, "http://localhost:11434");
        store.upsert_provider(p).unwrap();
        assert_eq!(store.provider_count(), 1);

        // Read
        let p = store.get_provider("ollama-local").unwrap();
        assert_eq!(p.brand, ProviderBrand::Ollama);

        // Update
        let mut p2 = p.clone();
        p2.base_url = "http://localhost:11435".into();
        store.upsert_provider(p2).unwrap();
        let updated = store.get_provider("ollama-local").unwrap();
        assert_eq!(updated.base_url, "http://localhost:11435");

        // List
        let all = store.list_providers(None, None);
        assert_eq!(all.len(), 1);

        // Delete
        assert!(store.delete_provider("ollama-local").unwrap());
        assert_eq!(store.provider_count(), 0);
        assert!(!store.delete_provider("ollama-local").unwrap());
    }

    #[test]
    fn test_provider_with_api_key_persistence() {
        let (dir, store) = temp_store();

        let p = LlmProvider::new("ds", "DeepSeek", ProviderBrand::DeepSeek, "https://api.deepseek.com")
            .with_api_key("sk-secret-key-12345");
        store.upsert_provider(p).unwrap();

        // Re-open store
        let store_path = dir.path().join("llm");
        let store2 = LlmStore::open(&store_path).unwrap();
        let p2 = store2.get_provider("ds").unwrap();
        assert_eq!(p2.api_key.as_deref(), Some("sk-secret-key-12345"));

        // Verify the file does NOT contain plaintext key
        let contents = std::fs::read_to_string(store_path.join("llm_registry.json")).unwrap();
        assert!(!contents.contains("sk-secret-key-12345"));
    }

    #[test]
    fn test_provider_filter() {
        let (_dir, store) = temp_store();

        store
            .upsert_provider(LlmProvider::new("o1", "Ollama1", ProviderBrand::Ollama, "http://a"))
            .unwrap();
        store
            .upsert_provider(LlmProvider::new("ds1", "DS", ProviderBrand::DeepSeek, "http://b"))
            .unwrap();

        let ollama_only = store.list_providers(Some(&ProviderBrand::Ollama), None);
        assert_eq!(ollama_only.len(), 1);
        assert_eq!(ollama_only[0].id, "o1");
    }

    #[test]
    fn test_model_crud() {
        let (_dir, store) = temp_store();

        // Provider must exist first
        store
            .upsert_provider(LlmProvider::new("ollama", "Ollama", ProviderBrand::Ollama, "http://localhost:11434"))
            .unwrap();

        // Create model
        let m = LlmModel::new("qwen2.5:14b", "ollama", "qwen2.5:14b", "Qwen 2.5 14B")
            .with_family("qwen2.5");
        store.upsert_model(m).unwrap();
        assert_eq!(store.model_count(), 1);

        // Read
        let m = store.get_model("qwen2.5:14b").unwrap();
        assert_eq!(m.display_name, "Qwen 2.5 14B");

        // Delete
        assert!(store.delete_model("qwen2.5:14b").unwrap());
        assert_eq!(store.model_count(), 0);
    }

    #[test]
    fn test_model_requires_provider() {
        let (_dir, store) = temp_store();

        let m = LlmModel::new("test", "nonexistent", "test", "Test");
        let err = store.upsert_model(m).unwrap_err();
        assert!(matches!(err, LlmError::ProviderNotFound(_)));
    }

    #[test]
    fn test_cascade_delete_provider() {
        let (_dir, store) = temp_store();

        store
            .upsert_provider(LlmProvider::new("p1", "P1", ProviderBrand::Ollama, "http://a"))
            .unwrap();
        store
            .upsert_model(LlmModel::new("m1", "p1", "m1", "M1"))
            .unwrap();
        store
            .upsert_model(LlmModel::new("m2", "p1", "m2", "M2"))
            .unwrap();

        assert_eq!(store.model_count(), 2);
        store.delete_provider("p1").unwrap();
        assert_eq!(store.model_count(), 0);
    }

    #[test]
    fn test_default_model() {
        let (_dir, store) = temp_store();

        store
            .upsert_provider(LlmProvider::new("p", "P", ProviderBrand::Ollama, "http://a"))
            .unwrap();
        store
            .upsert_model(LlmModel::new("m1", "p", "m1", "M1"))
            .unwrap();
        store
            .upsert_model(LlmModel::new("m2", "p", "m2", "M2"))
            .unwrap();

        assert!(store.get_default_model().is_none());

        store.set_default_model("m1").unwrap();
        let def = store.get_default_model().unwrap();
        assert_eq!(def.id, "m1");

        // Change default
        store.set_default_model("m2").unwrap();
        let def = store.get_default_model().unwrap();
        assert_eq!(def.id, "m2");
        // Old default unset
        let m1 = store.get_model("m1").unwrap();
        assert!(!m1.is_default);
    }

    #[test]
    fn test_find_models_by_name() {
        let (_dir, store) = temp_store();

        store
            .upsert_provider(LlmProvider::new("p1", "P1", ProviderBrand::Ollama, "http://a"))
            .unwrap();
        store
            .upsert_provider(LlmProvider::new("p2", "P2", ProviderBrand::DeepSeek, "http://b"))
            .unwrap();
        store
            .upsert_model(LlmModel::new("m1", "p1", "qwen2.5:14b", "Qwen"))
            .unwrap();
        store
            .upsert_model(LlmModel::new("m2", "p2", "qwen2.5:14b", "Qwen DS"))
            .unwrap();

        let results = store.find_models_by_name("qwen2.5:14b");
        assert_eq!(results.len(), 2);
    }

    #[test]
    fn test_provider_status() {
        let (_dir, store) = temp_store();

        store
            .upsert_provider(LlmProvider::new("p", "P", ProviderBrand::Ollama, "http://a"))
            .unwrap();

        assert!(store.set_provider_status("p", ProviderStatus::Disabled).unwrap());
        let p = store.get_provider("p").unwrap();
        assert_eq!(p.status, ProviderStatus::Disabled);
        assert!(!p.is_available());
    }

    #[test]
    fn test_provider_health() {
        let (_dir, store) = temp_store();

        store
            .upsert_provider(LlmProvider::new("p", "P", ProviderBrand::Ollama, "http://a"))
            .unwrap();

        assert!(store.set_provider_health("p", false));
        let p = store.get_provider("p").unwrap();
        assert!(!p.is_healthy);
        assert!(!p.is_available());
    }
}
