//! Configuration file scanner for MCP servers

use std::env;
use std::path::{Path, PathBuf};

use serde_json::Value;
use tracing::{debug, warn};

use crate::config::TransportConfig;
use crate::{Error, Result};

use super::{DiscoveredServer, DiscoverySource, ServerMetadata};

/// Scans config files for MCP server definitions
pub struct ConfigScanner;

impl ConfigScanner {
    /// Create new config scanner
    #[must_use]
    pub fn new() -> Self {
        Self
    }

    /// Scan all known config locations
    ///
    /// # Errors
    ///
    /// Returns an error if a critical scanning operation fails.
    pub async fn scan_all(&self) -> Result<Vec<DiscoveredServer>> {
        let mut servers = Vec::new();

        // Scan Claude Desktop config
        if let Ok(mut claude_servers) = self.scan_claude_desktop().await {
            servers.append(&mut claude_servers);
        }

        // Scan VS Code config
        if let Ok(mut vscode_servers) = self.scan_vscode().await {
            servers.append(&mut vscode_servers);
        }

        // Scan Windsurf config
        if let Ok(mut windsurf_servers) = self.scan_windsurf().await {
            servers.append(&mut windsurf_servers);
        }

        // Scan generic MCP config directory
        if let Ok(mut mcp_servers) = self.scan_mcp_config_dir().await {
            servers.append(&mut mcp_servers);
        }

        // Scan environment variables
        if let Ok(mut env_servers) = self.scan_environment() {
            servers.append(&mut env_servers);
        }

        Ok(servers)
    }

    /// Scan Claude Desktop configuration
    ///
    /// # Errors
    ///
    /// Returns an error if the config file exists but cannot be read or parsed.
    pub async fn scan_claude_desktop(&self) -> Result<Vec<DiscoveredServer>> {
        let config_path = Self::claude_desktop_config_path()?;
        if !config_path.exists() {
            debug!("Claude Desktop config not found at {}", config_path.display());
            return Ok(Vec::new());
        }

        debug!("Scanning Claude Desktop config at {}", config_path.display());
        self.parse_claude_config(&config_path, DiscoverySource::ClaudeDesktop)
            .await
    }

    /// Scan VS Code/Cursor MCP configuration
    ///
    /// # Errors
    ///
    /// Returns an error if a config file exists but cannot be read or parsed.
    pub async fn scan_vscode(&self) -> Result<Vec<DiscoveredServer>> {
        let mut servers = Vec::new();

        // VS Code settings
        if let Ok(vscode_path) = Self::vscode_config_path() {
            if vscode_path.exists() {
                debug!("Scanning VS Code config at {}", vscode_path.display());
                if let Ok(mut vs_servers) = self
                    .parse_vscode_config(&vscode_path, DiscoverySource::VsCode)
                    .await
                {
                    servers.append(&mut vs_servers);
                }
            }
        }

        // Cursor settings (similar format)
        if let Ok(cursor_path) = Self::cursor_config_path() {
            if cursor_path.exists() {
                debug!("Scanning Cursor config at {}", cursor_path.display());
                if let Ok(mut cursor_servers) = self
                    .parse_vscode_config(&cursor_path, DiscoverySource::VsCode)
                    .await
                {
                    servers.append(&mut cursor_servers);
                }
            }
        }

        Ok(servers)
    }

    /// Scan Windsurf MCP configuration
    ///
    /// # Errors
    ///
    /// Returns an error if the config file exists but cannot be read or parsed.
    pub async fn scan_windsurf(&self) -> Result<Vec<DiscoveredServer>> {
        let config_path = Self::windsurf_config_path()?;
        if !config_path.exists() {
            debug!("Windsurf config not found at {}", config_path.display());
            return Ok(Vec::new());
        }

        debug!("Scanning Windsurf config at {}", config_path.display());
        self.parse_claude_config(&config_path, DiscoverySource::Windsurf)
            .await
    }

    /// Scan ~/.config/mcp/*.json files
    ///
    /// # Errors
    ///
    /// Returns an error if the config directory cannot be read.
    pub async fn scan_mcp_config_dir(&self) -> Result<Vec<DiscoveredServer>> {
        let mcp_dir = Self::mcp_config_dir()?;
        if !mcp_dir.exists() {
            debug!("MCP config directory not found at {}", mcp_dir.display());
            return Ok(Vec::new());
        }

        let mut servers = Vec::new();
        let entries = tokio::fs::read_dir(&mcp_dir).await.map_err(|e| {
            Error::Config(format!("Failed to read MCP config dir: {e}"))
        })?;

        let mut entries = entries;
        while let Some(entry) = entries.next_entry().await.map_err(|e| {
            Error::Config(format!("Failed to read dir entry: {e}"))
        })? {
            let path = entry.path();
            if path.extension().and_then(|s| s.to_str()) == Some("json") {
                debug!("Scanning MCP config file: {}", path.display());
                if let Ok(mut config_servers) = self
                    .parse_claude_config(&path, DiscoverySource::McpConfig)
                    .await
                {
                    servers.append(&mut config_servers);
                }
            }
        }

        Ok(servers)
    }

    /// Scan environment variables for MCP_* patterns
    ///
    /// # Errors
    ///
    /// This function currently does not return errors but maintains the `Result`
    /// type for consistency with other scanning methods.
    pub fn scan_environment(&self) -> Result<Vec<DiscoveredServer>> {
        let mut servers = Vec::new();

        // Look for MCP_SERVER_* environment variables
        for (key, value) in env::vars() {
            if key.starts_with("MCP_SERVER_") && key.ends_with("_URL") {
                // Extract server name from MCP_SERVER_NAME_URL
                let name = key
                    .strip_prefix("MCP_SERVER_")
                    .and_then(|s| s.strip_suffix("_URL"))
                    .unwrap_or("unknown")
                    .to_lowercase()
                    .replace('_', "-");

                debug!("Found MCP server in environment: {name} = {value}");

                servers.push(DiscoveredServer {
                    name: name.clone(),
                    description: format!("MCP server from environment variable {key}"),
                    source: DiscoverySource::Environment,
                    transport: TransportConfig::Http {
                        http_url: value,
                        streamable_http: false,
                        protocol_version: None,
                    },
                    metadata: ServerMetadata {
                        config_path: None,
                        pid: None,
                        port: None,
                        command: None,
                        working_dir: None,
                    },
                });
            }
        }

        Ok(servers)
    }

    /// Parse Claude Desktop format config (also used by Windsurf)
    async fn parse_claude_config(
        &self,
        path: &Path,
        source: DiscoverySource,
    ) -> Result<Vec<DiscoveredServer>> {
        let content = tokio::fs::read_to_string(path)
            .await
            .map_err(|e| Error::Config(format!("Failed to read config: {e}")))?;

        let config: Value = serde_json::from_str(&content)
            .map_err(|e| Error::Config(format!("Failed to parse JSON: {e}")))?;

        let mut servers = Vec::new();

        // Claude Desktop format: { "mcpServers": { "name": { "command": "...", ... } } }
        if let Some(mcp_servers) = config.get("mcpServers").and_then(|v| v.as_object()) {
            for (name, server_config) in mcp_servers {
                if let Some(server) = Self::parse_server_config(name, server_config, &source, path) {
                    servers.push(server);
                }
            }
        }

        Ok(servers)
    }

    /// Parse VS Code format config
    async fn parse_vscode_config(
        &self,
        path: &Path,
        source: DiscoverySource,
    ) -> Result<Vec<DiscoveredServer>> {
        let content = tokio::fs::read_to_string(path)
            .await
            .map_err(|e| Error::Config(format!("Failed to read config: {e}")))?;

        let config: Value = serde_json::from_str(&content)
            .map_err(|e| Error::Config(format!("Failed to parse JSON: {e}")))?;

        let mut servers = Vec::new();

        // VS Code might have MCP config under various keys
        if let Some(mcp_config) = config.get("mcp").and_then(|v| v.as_object()) {
            for (name, server_config) in mcp_config {
                if let Some(server) = Self::parse_server_config(name, server_config, &source, path) {
                    servers.push(server);
                }
            }
        }

        Ok(servers)
    }

    /// Parse individual server config
    fn parse_server_config(
        name: &str,
        config: &Value,
        source: &DiscoverySource,
        config_path: &Path,
    ) -> Option<DiscoveredServer> {
        // Extract command (stdio transport)
        if let Some(command) = config.get("command").and_then(|v| v.as_str()) {
            let args = config
                .get("args")
                .and_then(|v| v.as_array())
                .map(|arr| {
                    arr.iter()
                        .filter_map(|v| v.as_str().map(String::from))
                        .collect::<Vec<_>>()
                })
                .unwrap_or_default();

            let full_command = if args.is_empty() {
                command.to_string()
            } else {
                format!("{} {}", command, args.join(" "))
            };

            let working_dir = config
                .get("cwd")
                .and_then(|v| v.as_str())
                .map(PathBuf::from);

            return Some(DiscoveredServer {
                name: name.to_string(),
                description: format!("MCP server from {source:?}"),
                source: source.clone(),
                transport: TransportConfig::Stdio {
                    command: full_command.clone(),
                    cwd: working_dir.as_ref().map(|p| p.to_string_lossy().into_owned()),
                },
                metadata: ServerMetadata {
                    config_path: Some(config_path.to_path_buf()),
                    pid: None,
                    port: None,
                    command: Some(full_command),
                    working_dir,
                },
            });
        }

        // Extract URL (HTTP transport)
        if let Some(url) = config.get("url").and_then(|v| v.as_str()) {
            return Some(DiscoveredServer {
                name: name.to_string(),
                description: format!("MCP server from {source:?}"),
                source: source.clone(),
                transport: TransportConfig::Http {
                    http_url: url.to_string(),
                    streamable_http: false,
                    protocol_version: None,
                },
                metadata: ServerMetadata {
                    config_path: Some(config_path.to_path_buf()),
                    pid: None,
                    port: Self::extract_port_from_url(url),
                    command: None,
                    working_dir: None,
                },
            });
        }

        warn!("Unsupported server config format for {name}");
        None
    }

    /// Extract port number from URL
    fn extract_port_from_url(url: &str) -> Option<u16> {
        url::Url::parse(url)
            .ok()
            .and_then(|u| u.port())
    }

    /// Get Claude Desktop config path
    fn claude_desktop_config_path() -> Result<PathBuf> {
        let home = dirs::home_dir()
            .ok_or_else(|| Error::Config("Could not determine home directory".to_string()))?;

        #[cfg(target_os = "macos")]
        let path = home.join("Library/Application Support/Claude/claude_desktop_config.json");

        #[cfg(target_os = "linux")]
        let path = home.join(".config/Claude/claude_desktop_config.json");

        #[cfg(target_os = "windows")]
        let path = home.join("AppData/Roaming/Claude/claude_desktop_config.json");

        Ok(path)
    }

    /// Get VS Code settings path
    fn vscode_config_path() -> Result<PathBuf> {
        let home = dirs::home_dir()
            .ok_or_else(|| Error::Config("Could not determine home directory".to_string()))?;

        #[cfg(target_os = "macos")]
        let path = home.join("Library/Application Support/Code/User/settings.json");

        #[cfg(target_os = "linux")]
        let path = home.join(".config/Code/User/settings.json");

        #[cfg(target_os = "windows")]
        let path = home.join("AppData/Roaming/Code/User/settings.json");

        Ok(path)
    }

    /// Get Cursor settings path
    fn cursor_config_path() -> Result<PathBuf> {
        let home = dirs::home_dir()
            .ok_or_else(|| Error::Config("Could not determine home directory".to_string()))?;

        #[cfg(target_os = "macos")]
        let path = home.join("Library/Application Support/Cursor/User/settings.json");

        #[cfg(target_os = "linux")]
        let path = home.join(".config/Cursor/User/settings.json");

        #[cfg(target_os = "windows")]
        let path = home.join("AppData/Roaming/Cursor/User/settings.json");

        Ok(path)
    }

    /// Get Windsurf config path
    fn windsurf_config_path() -> Result<PathBuf> {
        let home = dirs::home_dir()
            .ok_or_else(|| Error::Config("Could not determine home directory".to_string()))?;

        #[cfg(target_os = "macos")]
        let path = home.join("Library/Application Support/Windsurf/windsurf_config.json");

        #[cfg(target_os = "linux")]
        let path = home.join(".config/Windsurf/windsurf_config.json");

        #[cfg(target_os = "windows")]
        let path = home.join("AppData/Roaming/Windsurf/windsurf_config.json");

        Ok(path)
    }

    /// Get generic MCP config directory
    fn mcp_config_dir() -> Result<PathBuf> {
        let home = dirs::home_dir()
            .ok_or_else(|| Error::Config("Could not determine home directory".to_string()))?;

        Ok(home.join(".config/mcp"))
    }
}

impl Default for ConfigScanner {
    fn default() -> Self {
        Self::new()
    }
}
