//! Perception module configuration
//!
//! Configuration types for perception and reasoning engines.

use serde::{Deserialize, Serialize};

// ============================================================================
// Perception Engine Configuration
// ============================================================================

/// Configuration for built-in perception engine
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PerceptionConfig {
    /// Engine type: "candle", "onnx", or "none"
    #[serde(default = "default_engine_type")]
    pub engine_type: String,

    /// Model identifier (HuggingFace model ID or local path)
    #[serde(default = "default_model_id")]
    pub model_id: String,

    /// Output embedding dimension (inferred from model if not specified)
    pub dimension: Option<usize>,

    /// Device to run inference on: "auto", "cpu", "cuda", "cuda:N", "metal"
    ///
    /// `"auto"` (默认): 自动检测 CUDA → Metal → CPU，GPU 不可用时安全降级
    #[serde(default = "default_device")]
    pub device: String,

    /// Whether to download model if not found locally
    #[serde(default = "default_auto_download")]
    pub auto_download: bool,

    /// Local cache directory for models
    pub cache_dir: Option<String>,

    /// Maximum input length (tokens)
    pub max_length: Option<usize>,

    /// Batch size for batch processing
    #[serde(default = "default_batch_size")]
    pub batch_size: usize,
}

fn default_engine_type() -> String {
    "none".to_string()
}

fn default_model_id() -> String {
    "sentence-transformers/all-MiniLM-L6-v2".to_string()
}

fn default_device() -> String {
    "auto".to_string()
}

fn default_auto_download() -> bool {
    true
}

fn default_batch_size() -> usize {
    32
}

impl Default for PerceptionConfig {
    fn default() -> Self {
        Self {
            engine_type: default_engine_type(),
            model_id: default_model_id(),
            dimension: None,
            device: default_device(),
            auto_download: default_auto_download(),
            cache_dir: None,
            max_length: None,
            batch_size: default_batch_size(),
        }
    }
}

impl PerceptionConfig {
    /// Create a new config with Candle engine
    pub fn candle(model_id: impl Into<String>) -> Self {
        Self {
            engine_type: "candle".to_string(),
            model_id: model_id.into(),
            ..Default::default()
        }
    }

    /// Create a config for the lightweight MiniLM model
    pub fn minilm() -> Self {
        Self::candle("sentence-transformers/all-MiniLM-L6-v2")
    }

    /// Create a config for BGE small model
    pub fn bge_small() -> Self {
        Self::candle("BAAI/bge-small-en-v1.5")
    }

    /// Create a config for multilingual E5 model
    pub fn multilingual_e5() -> Self {
        Self::candle("intfloat/multilingual-e5-small")
    }

    /// Create a config for EmbeddingGemma model (highest quality)
    ///
    /// EmbeddingGemma is a state-of-the-art multilingual embedding model
    /// with 768 dimensions, supporting 100+ languages and Matryoshka
    /// representation learning (can be truncated to 512/256/128 dims).
    pub fn embedding_gemma() -> Self {
        Self {
            engine_type: "embedding_gemma".to_string(),
            model_id: "google/embeddinggemma-300m".to_string(),
            dimension: Some(768),
            max_length: Some(2048),
            ..Default::default()
        }
    }

    /// Create a config for EmbeddingGemma with custom Matryoshka dimension
    ///
    /// Supported dimensions: 768 (full), 512, 256, 128
    pub fn embedding_gemma_matryoshka(dim: usize) -> Self {
        assert!(
            dim == 768 || dim == 512 || dim == 256 || dim == 128,
            "Matryoshka dimension must be 768, 512, 256, or 128"
        );
        Self {
            engine_type: "embedding_gemma".to_string(),
            model_id: "google/embeddinggemma-300m".to_string(),
            dimension: Some(dim),
            max_length: Some(2048),
            ..Default::default()
        }
    }

    /// Create a config for Whisper-small audio embedding (768d)
    ///
    /// Uses OpenAI Whisper-small encoder for audio → 768d embedding.
    /// Requires `audio` feature.
    pub fn whisper_small() -> Self {
        Self {
            engine_type: "whisper".to_string(),
            model_id: "openai/whisper-small".to_string(),
            dimension: Some(768),
            ..Default::default()
        }
    }

    /// Create a config for Whisper-tiny audio embedding (384d, lightest)
    pub fn whisper_tiny() -> Self {
        Self {
            engine_type: "whisper".to_string(),
            model_id: "openai/whisper-tiny".to_string(),
            dimension: Some(384),
            ..Default::default()
        }
    }

    /// Create a config for Whisper-medium audio embedding (1024d, high quality)
    pub fn whisper_medium() -> Self {
        Self {
            engine_type: "whisper".to_string(),
            model_id: "openai/whisper-medium".to_string(),
            dimension: Some(1024),
            ..Default::default()
        }
    }

    /// Set the device
    pub fn with_device(mut self, device: impl Into<String>) -> Self {
        self.device = device.into();
        self
    }

    /// Set the cache directory
    pub fn with_cache_dir(mut self, cache_dir: impl Into<String>) -> Self {
        self.cache_dir = Some(cache_dir.into());
        self
    }

    /// Disable auto-download
    pub fn no_download(mut self) -> Self {
        self.auto_download = false;
        self
    }
}

// ============================================================================
// Reasoning Engine Configuration
// ============================================================================

/// Configuration for external reasoning engine (LLM API)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReasoningConfig {
    /// Provider type: "ollama", "openai", "azure", "anthropic"
    pub provider: String,

    /// API endpoint URL
    pub endpoint: String,

    /// Model name/identifier
    pub model: String,

    /// API key (optional for local providers like Ollama)
    pub api_key: Option<String>,

    /// Request timeout in seconds
    #[serde(default = "default_timeout")]
    pub timeout_secs: u64,

    /// Maximum tokens for generation
    pub max_tokens: Option<usize>,

    /// Temperature for generation (0.0 - 2.0)
    pub temperature: Option<f32>,

    /// Additional headers for API requests
    #[serde(default)]
    pub extra_headers: std::collections::HashMap<String, String>,
}

fn default_timeout() -> u64 {
    60
}

impl Default for ReasoningConfig {
    fn default() -> Self {
        Self {
            provider: "ollama".to_string(),
            endpoint: "http://localhost:11434/v1".to_string(),
            model: "llama3.2".to_string(),
            api_key: None,
            timeout_secs: default_timeout(),
            max_tokens: None,
            temperature: None,
            extra_headers: std::collections::HashMap::new(),
        }
    }
}

impl ReasoningConfig {
    /// Create a config for Ollama
    pub fn ollama(model: impl Into<String>) -> Self {
        Self {
            provider: "ollama".to_string(),
            endpoint: "http://localhost:11434/v1".to_string(),
            model: model.into(),
            ..Default::default()
        }
    }

    /// Create a config for OpenAI
    pub fn openai(api_key: impl Into<String>, model: impl Into<String>) -> Self {
        Self {
            provider: "openai".to_string(),
            endpoint: "https://api.openai.com/v1".to_string(),
            model: model.into(),
            api_key: Some(api_key.into()),
            ..Default::default()
        }
    }

    /// Create a config for Azure OpenAI
    pub fn azure(
        endpoint: impl Into<String>,
        api_key: impl Into<String>,
        deployment: impl Into<String>,
    ) -> Self {
        Self {
            provider: "azure".to_string(),
            endpoint: endpoint.into(),
            model: deployment.into(),
            api_key: Some(api_key.into()),
            ..Default::default()
        }
    }

    /// Set the endpoint
    pub fn with_endpoint(mut self, endpoint: impl Into<String>) -> Self {
        self.endpoint = endpoint.into();
        self
    }

    /// Set the timeout
    pub fn with_timeout(mut self, timeout_secs: u64) -> Self {
        self.timeout_secs = timeout_secs;
        self
    }

    /// Set max tokens
    pub fn with_max_tokens(mut self, max_tokens: usize) -> Self {
        self.max_tokens = Some(max_tokens);
        self
    }

    /// Set temperature
    pub fn with_temperature(mut self, temperature: f32) -> Self {
        self.temperature = Some(temperature.clamp(0.0, 2.0));
        self
    }

    /// Add an extra header
    pub fn with_header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
        self.extra_headers.insert(key.into(), value.into());
        self
    }
}

// ============================================================================
// Combined Configuration
// ============================================================================

/// Combined configuration for perception module
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PerceptionModuleConfig {
    /// Built-in perception engine config
    pub perception: Option<PerceptionConfig>,

    /// External reasoning engine config
    pub reasoning: Option<ReasoningConfig>,

    /// Whether to enable multimodal processing
    #[serde(default)]
    pub enable_multimodal: bool,

    /// Supported audio formats (when multimodal is enabled)
    #[serde(default = "default_audio_formats")]
    pub audio_formats: Vec<String>,

    /// Supported image formats (when multimodal is enabled)
    #[serde(default = "default_image_formats")]
    pub image_formats: Vec<String>,
}

fn default_audio_formats() -> Vec<String> {
    vec!["mp3".to_string(), "wav".to_string(), "flac".to_string(), "ogg".to_string()]
}

fn default_image_formats() -> Vec<String> {
    vec!["jpg".to_string(), "jpeg".to_string(), "png".to_string(), "webp".to_string()]
}

impl PerceptionModuleConfig {
    /// Create a config with only perception engine
    pub fn perception_only(perception: PerceptionConfig) -> Self {
        Self {
            perception: Some(perception),
            reasoning: None,
            enable_multimodal: false,
            audio_formats: default_audio_formats(),
            image_formats: default_image_formats(),
        }
    }

    /// Create a config with both perception and reasoning
    pub fn full(perception: PerceptionConfig, reasoning: ReasoningConfig) -> Self {
        Self {
            perception: Some(perception),
            reasoning: Some(reasoning),
            enable_multimodal: false,
            audio_formats: default_audio_formats(),
            image_formats: default_image_formats(),
        }
    }

    /// Enable multimodal processing
    pub fn with_multimodal(mut self) -> Self {
        self.enable_multimodal = true;
        self
    }
}

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

    #[test]
    fn test_perception_config_presets() {
        let minilm = PerceptionConfig::minilm();
        assert_eq!(minilm.model_id, "sentence-transformers/all-MiniLM-L6-v2");
        assert_eq!(minilm.engine_type, "candle");

        let bge = PerceptionConfig::bge_small();
        assert_eq!(bge.model_id, "BAAI/bge-small-en-v1.5");
    }

    #[test]
    fn test_reasoning_config_presets() {
        let ollama = ReasoningConfig::ollama("gemma3:27b");
        assert_eq!(ollama.provider, "ollama");
        assert_eq!(ollama.model, "gemma3:27b");
        assert!(ollama.api_key.is_none());

        let openai = ReasoningConfig::openai("sk-xxx", "gpt-4");
        assert_eq!(openai.provider, "openai");
        assert_eq!(openai.api_key, Some("sk-xxx".to_string()));
    }

    #[test]
    fn test_config_serialization() {
        let config = PerceptionModuleConfig::full(
            PerceptionConfig::minilm(),
            ReasoningConfig::ollama("llama3.2"),
        );

        let json = serde_json::to_string(&config).unwrap();
        let deserialized: PerceptionModuleConfig = serde_json::from_str(&json).unwrap();

        assert!(deserialized.perception.is_some());
        assert!(deserialized.reasoning.is_some());
    }
}
