#![allow(clippy::result_large_err)]

//! Configuration management for typedialog-ai backend
//!
//! Handles loading and managing AI backend configuration from TOML files.

use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use typedialog_core::error::{Error, Result};

/// LLM Provider selection
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum LlmProvider {
    OpenAi,
    Anthropic,
    Ollama,
}

impl std::fmt::Display for LlmProvider {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            LlmProvider::OpenAi => write!(f, "openai"),
            LlmProvider::Anthropic => write!(f, "anthropic"),
            LlmProvider::Ollama => write!(f, "ollama"),
        }
    }
}

impl std::str::FromStr for LlmProvider {
    type Err = String;

    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "openai" => Ok(LlmProvider::OpenAi),
            "anthropic" => Ok(LlmProvider::Anthropic),
            "ollama" => Ok(LlmProvider::Ollama),
            _ => Err(format!("Unknown LLM provider: {}", s)),
        }
    }
}

/// Interaction mode for AI backend
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum InteractionMode {
    Interactive,
    Autocomplete,
    ValidateOnly,
}

impl std::fmt::Display for InteractionMode {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            InteractionMode::Interactive => write!(f, "interactive"),
            InteractionMode::Autocomplete => write!(f, "autocomplete"),
            InteractionMode::ValidateOnly => write!(f, "validate_only"),
        }
    }
}

/// LLM generation settings
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LlmGenerationConfig {
    /// Temperature: 0.0-2.0, higher = more creative
    pub temperature: f32,

    /// Maximum tokens in response
    pub max_tokens: Option<usize>,

    /// Top-p (nucleus) sampling: 0.0-1.0
    pub top_p: Option<f32>,
}

impl Default for LlmGenerationConfig {
    fn default() -> Self {
        Self {
            temperature: 0.7,
            max_tokens: Some(2048),
            top_p: Some(0.9),
        }
    }
}

/// LLM Provider configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LlmConfig {
    /// LLM Provider to use
    pub provider: LlmProvider,

    /// Model name for the provider
    pub model: String,

    /// API endpoint (optional, uses provider defaults if not set)
    pub api_endpoint: Option<String>,

    /// Generation settings
    #[serde(default)]
    pub generation: LlmGenerationConfig,
}

impl Default for LlmConfig {
    fn default() -> Self {
        Self {
            provider: LlmProvider::OpenAi,
            model: "gpt-3.5-turbo".to_string(),
            api_endpoint: None,
            generation: LlmGenerationConfig::default(),
        }
    }
}

/// RAG (Retrieval-Augmented Generation) configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RagConfig {
    /// Enable RAG system
    pub enabled: bool,

    /// Index directory for cached embeddings
    pub index_path: PathBuf,

    /// Embedding dimensions: 384, 768, 1024
    pub embedding_dims: usize,

    /// Cache size for vector store
    pub cache_size: usize,
}

impl Default for RagConfig {
    fn default() -> Self {
        Self {
            enabled: true,
            index_path: PathBuf::from("~/.config/typedialog/ai/rag-index"),
            embedding_dims: 384,
            cache_size: 1000,
        }
    }
}

/// Microservice configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MicroserviceConfig {
    /// HTTP server host
    pub host: String,

    /// HTTP server port
    pub port: u16,

    /// Enable CORS
    pub enable_cors: bool,

    /// Enable WebSocket support
    pub enable_websocket: bool,
}

impl Default for MicroserviceConfig {
    fn default() -> Self {
        Self {
            host: "127.0.0.1".to_string(),
            port: 3001,
            enable_cors: false,
            enable_websocket: true,
        }
    }
}

/// Appearance/UX configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppearanceConfig {
    /// Interaction mode for AI suggestions
    pub interaction_mode: InteractionMode,

    /// Show LLM suggestions
    pub show_suggestions: bool,

    /// Confidence threshold for suggestions
    pub suggestion_confidence_threshold: f32,
}

impl Default for AppearanceConfig {
    fn default() -> Self {
        Self {
            interaction_mode: InteractionMode::Interactive,
            show_suggestions: true,
            suggestion_confidence_threshold: 0.5,
        }
    }
}

/// Complete AI Backend configuration
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TypeDialogAiConfig {
    /// LLM provider settings
    #[serde(default)]
    pub llm: LlmConfig,

    /// RAG system settings
    #[serde(default)]
    pub rag: RagConfig,

    /// Microservice settings
    #[serde(default)]
    pub microservice: MicroserviceConfig,

    /// Appearance settings
    #[serde(default)]
    pub appearance: AppearanceConfig,
}

impl TypeDialogAiConfig {
    /// Load configuration from TOML file
    pub fn load_from_file(path: &std::path::Path) -> Result<Self> {
        let content = std::fs::read_to_string(path).map_err(|e| {
            Error::validation_failed(format!(
                "Failed to read config file '{}': {}",
                path.display(),
                e
            ))
        })?;

        toml::from_str(&content).map_err(|e| {
            Error::validation_failed(format!(
                "Failed to parse config file '{}': {}",
                path.display(),
                e
            ))
        })
    }

    /// Load configuration with CLI override
    /// If cli_config_path is provided, use that file exclusively.
    /// Otherwise, search in order:
    /// 1. ~/.config/typedialog/ai/{TYPEDIALOG_ENV}.toml (if TYPEDIALOG_ENV set)
    /// 2. ~/.config/typedialog/ai/config.toml
    /// 3. Default values
    pub fn load_with_cli(cli_config_path: Option<&std::path::Path>) -> Result<Self> {
        // If CLI path provided, use it exclusively
        if let Some(path) = cli_config_path {
            return Self::load_from_file(path);
        }

        // Otherwise use search order
        Self::load()
    }

    /// Load configuration from environment or defaults
    /// Search order:
    /// 1. ~/.config/typedialog/ai/production.toml (if TYPEDIALOG_ENV=production)
    /// 2. ~/.config/typedialog/ai/dev.toml (if TYPEDIALOG_ENV=dev)
    /// 3. ~/.config/typedialog/ai/config.toml
    /// 4. Default values
    pub fn load() -> Result<Self> {
        let config_dir = dirs::config_dir()
            .unwrap_or_else(|| {
                std::path::PathBuf::from(std::env::var("HOME").unwrap_or_else(|_| ".".to_string()))
            })
            .join("typedialog/ai");

        // Create directory if it doesn't exist
        std::fs::create_dir_all(&config_dir).ok();

        // Check environment
        let env = std::env::var("TYPEDIALOG_ENV").unwrap_or_else(|_| "default".to_string());

        // Try environment-specific config first
        let env_config_path = config_dir.join(format!("{}.toml", env));
        if env_config_path.exists() {
            return Self::load_from_file(&env_config_path);
        }

        // Try generic config.toml
        let generic_config_path = config_dir.join("config.toml");
        if generic_config_path.exists() {
            return Self::load_from_file(&generic_config_path);
        }

        // Return defaults
        Ok(Self::default())
    }

    /// Resolve paths (expand ~ to home directory)
    pub fn resolve_paths(&mut self) {
        if let Some(home) = dirs::home_dir() {
            let home_str = home.to_string_lossy();
            let index_str = self.rag.index_path.to_string_lossy();
            if index_str.starts_with("~") {
                let resolved = index_str.replace("~", &home_str);
                self.rag.index_path = PathBuf::from(resolved);
            }
        }
    }
}

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

    #[test]
    fn test_default_config() {
        let config = TypeDialogAiConfig::default();
        assert_eq!(config.llm.provider, LlmProvider::OpenAi);
        assert_eq!(config.llm.model, "gpt-3.5-turbo");
        assert_eq!(config.microservice.port, 3001);
        assert!(config.rag.enabled);
    }

    #[test]
    fn test_llm_provider_display() {
        assert_eq!(LlmProvider::OpenAi.to_string(), "openai");
        assert_eq!(LlmProvider::Anthropic.to_string(), "anthropic");
        assert_eq!(LlmProvider::Ollama.to_string(), "ollama");
    }

    #[test]
    fn test_interaction_mode_display() {
        assert_eq!(InteractionMode::Interactive.to_string(), "interactive");
        assert_eq!(InteractionMode::Autocomplete.to_string(), "autocomplete");
        assert_eq!(InteractionMode::ValidateOnly.to_string(), "validate_only");
    }

    #[test]
    fn test_llm_provider_parse() {
        assert_eq!(
            "openai".parse::<LlmProvider>().unwrap(),
            LlmProvider::OpenAi
        );
        assert_eq!(
            "anthropic".parse::<LlmProvider>().unwrap(),
            LlmProvider::Anthropic
        );
        assert_eq!(
            "ollama".parse::<LlmProvider>().unwrap(),
            LlmProvider::Ollama
        );
    }

    #[test]
    fn test_load_default_config_file() {
        let mut config_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
        config_path.pop();
        config_path.pop();
        config_path.push("config/ai/default.toml");

        if config_path.exists() {
            let config = TypeDialogAiConfig::load_from_file(&config_path)
                .expect("Failed to load default.toml");
            assert_eq!(config.llm.provider, LlmProvider::OpenAi);
            assert_eq!(config.llm.model, "gpt-3.5-turbo");
            assert!(config.rag.enabled);
        }
    }

    #[test]
    fn test_load_dev_config_file() {
        let mut config_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
        config_path.pop();
        config_path.pop();
        config_path.push("config/ai/dev.toml");

        if config_path.exists() {
            let config =
                TypeDialogAiConfig::load_from_file(&config_path).expect("Failed to load dev.toml");
            assert_eq!(config.llm.provider, LlmProvider::Ollama);
            assert!(config.rag.enabled);
        }
    }

    #[test]
    fn test_load_production_config_file() {
        let mut config_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
        config_path.pop();
        config_path.pop();
        config_path.push("config/ai/production.toml");

        if config_path.exists() {
            let config = TypeDialogAiConfig::load_from_file(&config_path)
                .expect("Failed to load production.toml");
            assert_eq!(config.llm.provider, LlmProvider::Anthropic);
            assert!(config.rag.enabled);
        }
    }

    #[test]
    fn test_load_with_cli_explicit_path() {
        let mut config_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
        config_path.pop();
        config_path.pop();
        config_path.push("config/ai/dev.toml");

        if config_path.exists() {
            let config = TypeDialogAiConfig::load_with_cli(Some(config_path.as_path()))
                .expect("Failed to load with CLI path");
            assert_eq!(config.llm.provider, LlmProvider::Ollama);
        }
    }

    #[test]
    fn test_load_with_cli_no_path() {
        // When no CLI path is provided, should use search order (defaults)
        let config =
            TypeDialogAiConfig::load_with_cli(None).expect("Failed to load with search order");
        // Should have at least the defaults
        assert_eq!(config.llm.provider, LlmProvider::OpenAi);
    }
}
