//! Zed editor agent detection and connector config generation.
//!
//! This module detects Zed editor installations and generates `ConnectorConfig`
//! entries for discovered ACP agents. It is called during runtime initialization
//! to auto-populate connectors from Zed's agent configuration.

use crate::config::{ConnectorConfig, CoreConfig};
use crate::connectors::acp::config::ConnectorAgentType;
use crate::connectors::acp::{AcpConfig, TransportKind};
use crate::types::ConnectorKind;
use dirigent_tools::EmbeddingConfig;
use tracing::{debug, info, warn};

/// Default supported features for known ACP agent types.
///
/// These are conservative defaults based on confirmed agent capabilities.
/// Users can override via the connector config UI.
fn default_features_for_agent(agent_type: &ConnectorAgentType) -> Vec<String> {
    match agent_type {
        ConnectorAgentType::Claude => vec![
            "cancellation".to_string(),
            "session_resume".to_string(),
            "session_list".to_string(),
        ],
        ConnectorAgentType::Codex => vec![
            "session_resume".to_string(),
        ],
        // Gemini: no confirmed features yet (hangs on connect — BUG-7)
        ConnectorAgentType::Gemini => vec![],
        ConnectorAgentType::Custom => vec![],
    }
}

/// Convert a discovered Zed agent into a `ConnectorConfig` for the runtime.
///
/// Only creates connectors for agents with resolved binary paths (typically
/// registry agents whose binaries have been downloaded by Zed).
///
/// Returns `None` if the agent has no binary path.
pub fn zed_agent_to_connector_config(agent: &dirigent_zed::ZedAgent) -> Option<ConnectorConfig> {
    let binary_path = agent.binary_path.as_ref()?;

    // Map agent names to proper types. Handles both Zed settings keys
    // (e.g. "claude-acp") and external_agents directory names (e.g. "claude-agent-acp").
    let name_lower = agent.name.to_lowercase();
    let (default_title, default_icon, agent_type): (&str, &str, ConnectorAgentType) =
        if name_lower.contains("claude") {
            ("Claude (Zed)", "claude", ConnectorAgentType::Claude)
        } else if name_lower.contains("codex") {
            ("Codex (Zed)", "codex", ConnectorAgentType::Codex)
        } else if name_lower.contains("gemini") {
            ("Gemini (Zed)", "gemini", ConnectorAgentType::Gemini)
        } else {
            (agent.name.as_str(), "acp", ConnectorAgentType::Custom)
        };

    // Use registry display name with "(Zed)" suffix when available, falling
    // back to the hardcoded title.
    let title: String = match agent.display_name.as_deref() {
        Some(display) => format!("{display} (Zed)"),
        None => default_title.to_string(),
    };

    // Use the locally cached SVG icon path from the registry when available,
    // otherwise fall back to the built-in icon name.
    let icon: String = match agent.icon_local_path.as_ref() {
        Some(path) => path.to_string_lossy().to_string(),
        None => default_icon.to_string(),
    };

    let features = default_features_for_agent(&agent_type);

    // Build a proper AcpConfig with stdio transport pointing to the Zed-managed binary.
    let env: Vec<(String, String)> = agent
        .env_overrides
        .iter()
        .map(|(k, v)| (k.clone(), v.clone()))
        .collect();

    // Use args from registry metadata (e.g. ["--acp"]) when available.
    let args = if agent.args.is_empty() {
        vec![]
    } else {
        agent.args.clone()
    };

    let acp_config = AcpConfig {
        transport: TransportKind::Stdio {
            command: binary_path.to_string_lossy().to_string(),
            args,
            cwd: None,
            env,
        },
        protocol_version: 1,
        cwd: ".".to_string(),
        retry: Default::default(),
        embedding: EmbeddingConfig::default(),
        default_ownership: Default::default(),
        acp_log_dir: None,
        agent_type,
    };

    let params = match serde_json::to_value(&acp_config) {
        Ok(v) => v,
        Err(e) => {
            warn!(
                agent = %agent.name,
                error = %e,
                "Failed to serialize AcpConfig for Zed agent"
            );
            return None;
        }
    };

    Some(ConnectorConfig {
        id: None,
        kind: ConnectorKind::Acp,
        owner: None,
        title: Some(title),
        working_directory: None,
        params,
        icon_path: Some(icon),
        show_type_overlay: false,
        supported_features: features,
        tool_configuration: None,
        plugin_assignments: vec![],
        use_in_new_projects: true,
        source: None,
        zed_agent_name: Some(agent.name.clone()),
    })
}

/// Refresh binary paths for Zed-sourced connectors in the config.
///
/// When Zed upgrades agent binaries in the background, the binary path changes
/// (e.g. new version directory). This function re-detects the current binary
/// paths from Zed installations and updates any connector that has a
/// `zed_agent_name` set.
///
/// Returns the number of connectors updated.
pub fn refresh_zed_connector_binaries(config: &mut CoreConfig) -> usize {
    let installations = dirigent_zed::detect_installations();
    if installations.is_empty() {
        debug!("No Zed installations detected, skipping binary refresh");
        return 0;
    }

    // Build a map of agent_name -> latest binary path from all Zed installations.
    let mut agent_binaries: std::collections::HashMap<String, String> =
        std::collections::HashMap::new();
    for installation in &installations {
        for agent in &installation.agents {
            if let Some(ref binary_path) = agent.binary_path {
                agent_binaries
                    .insert(agent.name.clone(), binary_path.to_string_lossy().to_string());
            }
        }
    }

    let mut updated = 0usize;

    for connector in &mut config.connectors {
        let zed_name = match connector.zed_agent_name.as_deref() {
            Some(n) => n,
            None => continue,
        };

        let new_binary = match agent_binaries.get(zed_name) {
            Some(b) => b.clone(),
            None => continue,
        };

        // Parse current ACP config to check the existing binary path.
        let mut acp_config: AcpConfig = match serde_json::from_value(connector.params.clone()) {
            Ok(c) => c,
            Err(_) => continue,
        };

        let current_command = match &acp_config.transport {
            TransportKind::Stdio { command, .. } => command.clone(),
            _ => continue,
        };

        if current_command == new_binary {
            continue;
        }

        // Update the transport command to the new binary path.
        match &mut acp_config.transport {
            TransportKind::Stdio { command, .. } => {
                info!(
                    zed_agent = %zed_name,
                    old = %current_command,
                    new = %new_binary,
                    "Updating Zed connector binary path"
                );
                *command = new_binary;
            }
            _ => continue,
        }

        // Re-serialize back into params.
        match serde_json::to_value(&acp_config) {
            Ok(v) => {
                connector.params = v;
                updated += 1;
            }
            Err(e) => {
                warn!(
                    zed_agent = %zed_name,
                    error = %e,
                    "Failed to re-serialize AcpConfig after binary update"
                );
            }
        }
    }

    if updated > 0 {
        info!(count = updated, "Updated Zed connector binary paths");
    }

    updated
}

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

    #[test]
    fn test_zed_agent_to_connector_config_no_binary() {
        let agent = dirigent_zed::ZedAgent {
            name: "claude-acp".to_string(),
            agent_type: dirigent_zed::AgentServerType::Registry,
            binary_path: None,
            env_overrides: std::collections::HashMap::new(),
            display_name: None,
            description: None,
            args: Vec::new(),
            icon_local_path: None,
            icon_url: None,
        };
        assert!(zed_agent_to_connector_config(&agent).is_none());
    }

    #[test]
    fn test_zed_agent_to_connector_config_with_binary() {
        let agent = dirigent_zed::ZedAgent {
            name: "claude-acp".to_string(),
            agent_type: dirigent_zed::AgentServerType::Registry,
            binary_path: Some(std::path::PathBuf::from("/usr/local/bin/claude-acp")),
            env_overrides: std::collections::HashMap::new(),
            display_name: None,
            description: None,
            args: Vec::new(),
            icon_local_path: None,
            icon_url: None,
        };

        let config = zed_agent_to_connector_config(&agent).unwrap();
        assert_eq!(config.kind, ConnectorKind::Acp);
        assert_eq!(config.title.as_deref(), Some("Claude (Zed)"));
        assert_eq!(config.icon_path.as_deref(), Some("claude"));
        assert_eq!(config.source, None);

        // Verify the params contain a proper AcpConfig
        let acp_config: AcpConfig = serde_json::from_value(config.params).unwrap();
        match &acp_config.transport {
            TransportKind::Stdio { command, .. } => {
                assert_eq!(command, "/usr/local/bin/claude-acp");
            }
            _ => panic!("Expected stdio transport"),
        }
        assert_eq!(acp_config.agent_type, ConnectorAgentType::Claude);
        assert_eq!(config.supported_features, vec!["cancellation", "session_resume", "session_list"]);
    }

    #[test]
    fn test_zed_agent_to_connector_config_with_env() {
        let mut env = std::collections::HashMap::new();
        env.insert(
            "CLAUDE_CODE_EXECUTABLE".to_string(),
            "/usr/bin/claude".to_string(),
        );

        let agent = dirigent_zed::ZedAgent {
            name: "claude-acp".to_string(),
            agent_type: dirigent_zed::AgentServerType::Registry,
            binary_path: Some(std::path::PathBuf::from("/path/to/binary")),
            env_overrides: env,
            display_name: None,
            description: None,
            args: Vec::new(),
            icon_local_path: None,
            icon_url: None,
        };

        let config = zed_agent_to_connector_config(&agent).unwrap();
        let acp_config: AcpConfig = serde_json::from_value(config.params).unwrap();
        match &acp_config.transport {
            TransportKind::Stdio { env, .. } => {
                assert!(env
                    .iter()
                    .any(|(k, v)| k == "CLAUDE_CODE_EXECUTABLE" && v == "/usr/bin/claude"));
            }
            _ => panic!("Expected stdio transport"),
        }
    }

    #[test]
    fn test_zed_agent_to_connector_config_unknown_agent() {
        let agent = dirigent_zed::ZedAgent {
            name: "my-custom-agent".to_string(),
            agent_type: dirigent_zed::AgentServerType::Custom,
            binary_path: Some(std::path::PathBuf::from("/path/to/custom")),
            env_overrides: std::collections::HashMap::new(),
            display_name: None,
            description: None,
            args: Vec::new(),
            icon_local_path: None,
            icon_url: None,
        };

        let config = zed_agent_to_connector_config(&agent).unwrap();
        assert_eq!(config.title.as_deref(), Some("my-custom-agent"));
        assert_eq!(config.icon_path.as_deref(), Some("acp"));

        let acp_config: AcpConfig = serde_json::from_value(config.params).unwrap();
        assert_eq!(acp_config.agent_type, ConnectorAgentType::Custom);
        assert!(config.supported_features.is_empty());
    }

    #[test]
    fn test_zed_agent_codex() {
        let agent = dirigent_zed::ZedAgent {
            name: "codex-acp".to_string(),
            agent_type: dirigent_zed::AgentServerType::Registry,
            binary_path: Some(std::path::PathBuf::from("/path/to/codex")),
            env_overrides: std::collections::HashMap::new(),
            display_name: None,
            description: None,
            args: Vec::new(),
            icon_local_path: None,
            icon_url: None,
        };

        let config = zed_agent_to_connector_config(&agent).unwrap();
        assert_eq!(config.title.as_deref(), Some("Codex (Zed)"));
        assert_eq!(config.icon_path.as_deref(), Some("codex"));

        let acp_config: AcpConfig = serde_json::from_value(config.params).unwrap();
        assert_eq!(acp_config.agent_type, ConnectorAgentType::Codex);
        assert_eq!(config.supported_features, vec!["session_resume"]);
    }

    #[test]
    fn test_zed_agent_gemini() {
        let agent = dirigent_zed::ZedAgent {
            name: "gemini".to_string(),
            agent_type: dirigent_zed::AgentServerType::Registry,
            binary_path: Some(std::path::PathBuf::from("/path/to/gemini")),
            env_overrides: std::collections::HashMap::new(),
            display_name: None,
            description: None,
            args: Vec::new(),
            icon_local_path: None,
            icon_url: None,
        };

        let config = zed_agent_to_connector_config(&agent).unwrap();
        assert_eq!(config.title.as_deref(), Some("Gemini (Zed)"));
        assert_eq!(config.icon_path.as_deref(), Some("gemini"));

        let acp_config: AcpConfig = serde_json::from_value(config.params).unwrap();
        assert_eq!(acp_config.agent_type, ConnectorAgentType::Gemini);
        assert!(config.supported_features.is_empty());
    }

    #[test]
    fn test_dismissed_zed_agent_title_matches_generated_config() {
        // Verify that a dismissed title like "Claude (Zed)" matches the title
        // generated by zed_agent_to_connector_config for a claude-acp agent
        let mut core_config = CoreConfig::default();
        core_config
            .dismissed_zed_agents
            .push("Claude (Zed)".to_string());

        let agent = dirigent_zed::ZedAgent {
            name: "claude-acp".to_string(),
            agent_type: dirigent_zed::AgentServerType::Registry,
            binary_path: Some(std::path::PathBuf::from("/usr/local/bin/claude-acp")),
            env_overrides: std::collections::HashMap::new(),
            display_name: None,
            description: None,
            args: Vec::new(),
            icon_local_path: None,
            icon_url: None,
        };

        let connector_config = zed_agent_to_connector_config(&agent).unwrap();
        assert_eq!(connector_config.title.as_deref(), Some("Claude (Zed)"));
        // The dismissed list should contain the generated title
        assert!(core_config
            .dismissed_zed_agents
            .contains(connector_config.title.as_ref().unwrap()));
    }

    #[test]
    fn test_dismissed_list_does_not_block_other_agents() {
        // Dismissing Claude should not block Codex or Gemini
        let mut core_config = CoreConfig::default();
        core_config
            .dismissed_zed_agents
            .push("Claude (Zed)".to_string());

        let codex_title = "Codex (Zed)".to_string();
        let gemini_title = "Gemini (Zed)".to_string();

        assert!(!core_config.dismissed_zed_agents.contains(&codex_title));
        assert!(!core_config.dismissed_zed_agents.contains(&gemini_title));
    }

    #[test]
    fn test_dismissed_zed_agents_serde_roundtrip() {
        // Verify dismissed_zed_agents survives serialization/deserialization
        let mut core_config = CoreConfig::default();
        core_config
            .dismissed_zed_agents
            .push("Claude (Zed)".to_string());
        core_config
            .dismissed_zed_agents
            .push("Gemini (Zed)".to_string());

        let json = serde_json::to_string(&core_config).unwrap();
        let deserialized: CoreConfig = serde_json::from_str(&json).unwrap();

        assert_eq!(deserialized.dismissed_zed_agents.len(), 2);
        assert!(deserialized
            .dismissed_zed_agents
            .contains(&"Claude (Zed)".to_string()));
        assert!(deserialized
            .dismissed_zed_agents
            .contains(&"Gemini (Zed)".to_string()));
    }

    #[test]
    fn test_dismissed_zed_agents_empty_not_serialized() {
        // With skip_serializing_if = "Vec::is_empty", empty list should not appear in JSON
        let core_config = CoreConfig::default();
        let json = serde_json::to_string(&core_config).unwrap();
        assert!(
            !json.contains("dismissed_zed_agents"),
            "Empty dismissed_zed_agents should be omitted from serialization"
        );
    }

    #[test]
    fn test_dismissed_zed_agents_deserialized_from_missing_field() {
        // Old config files without dismissed_zed_agents should still deserialize
        // thanks to #[serde(default)]
        let json = r#"{"project_dir":".","connectors":[]}"#;
        let config: CoreConfig = serde_json::from_str(json).unwrap();
        assert!(config.dismissed_zed_agents.is_empty());
    }

    #[test]
    fn test_zed_agent_name_is_set() {
        let agent = dirigent_zed::ZedAgent {
            name: "claude-acp".to_string(),
            agent_type: dirigent_zed::AgentServerType::Registry,
            binary_path: Some(std::path::PathBuf::from("/usr/local/bin/claude-acp")),
            env_overrides: std::collections::HashMap::new(),
            display_name: None,
            description: None,
            args: Vec::new(),
            icon_local_path: None,
            icon_url: None,
        };
        let config = zed_agent_to_connector_config(&agent).unwrap();
        assert_eq!(config.zed_agent_name.as_deref(), Some("claude-acp"));
    }

    #[test]
    fn test_zed_agent_name_preserves_original_name() {
        // The zed_agent_name should be the exact Zed agent name, not the display title
        let agent = dirigent_zed::ZedAgent {
            name: "claude-agent-acp".to_string(),
            agent_type: dirigent_zed::AgentServerType::Registry,
            binary_path: Some(std::path::PathBuf::from("/path/to/binary")),
            env_overrides: std::collections::HashMap::new(),
            display_name: None,
            description: None,
            args: Vec::new(),
            icon_local_path: None,
            icon_url: None,
        };
        let config = zed_agent_to_connector_config(&agent).unwrap();
        assert_eq!(config.title.as_deref(), Some("Claude (Zed)"));
        assert_eq!(config.zed_agent_name.as_deref(), Some("claude-agent-acp"));
    }

    #[test]
    fn test_zed_agent_name_serde_roundtrip() {
        let agent = dirigent_zed::ZedAgent {
            name: "codex".to_string(),
            agent_type: dirigent_zed::AgentServerType::Registry,
            binary_path: Some(std::path::PathBuf::from("/path/to/codex")),
            env_overrides: std::collections::HashMap::new(),
            display_name: None,
            description: None,
            args: Vec::new(),
            icon_local_path: None,
            icon_url: None,
        };
        let config = zed_agent_to_connector_config(&agent).unwrap();
        let json = serde_json::to_string(&config).unwrap();
        assert!(json.contains("\"zed_agent_name\":\"codex\""));
        let deserialized: ConnectorConfig = serde_json::from_str(&json).unwrap();
        assert_eq!(deserialized.zed_agent_name.as_deref(), Some("codex"));
    }

    #[test]
    fn test_non_zed_connector_has_no_zed_agent_name() {
        // ConnectorConfig created via template should not have zed_agent_name
        let config = ConnectorConfig::default();
        assert!(config.zed_agent_name.is_none());
    }

    #[test]
    fn test_enriched_display_name_used_in_title() {
        let agent = dirigent_zed::ZedAgent {
            name: "claude-acp".to_string(),
            agent_type: dirigent_zed::AgentServerType::Registry,
            binary_path: Some(std::path::PathBuf::from("/path/to/claude")),
            env_overrides: std::collections::HashMap::new(),
            display_name: Some("Claude Agent".to_string()),
            description: Some("ACP wrapper for Anthropic's Claude".to_string()),
            args: Vec::new(),
            icon_local_path: None,
            icon_url: None,
        };

        let config = zed_agent_to_connector_config(&agent).unwrap();
        // Title should use the registry display name with "(Zed)" suffix.
        assert_eq!(config.title.as_deref(), Some("Claude Agent (Zed)"));
    }

    #[test]
    fn test_enriched_args_passed_to_transport() {
        let agent = dirigent_zed::ZedAgent {
            name: "auggie".to_string(),
            agent_type: dirigent_zed::AgentServerType::Registry,
            binary_path: Some(std::path::PathBuf::from("/path/to/auggie")),
            env_overrides: std::collections::HashMap::new(),
            display_name: Some("Auggie CLI".to_string()),
            description: None,
            args: vec!["--acp".to_string()],
            icon_local_path: None,
            icon_url: None,
        };

        let config = zed_agent_to_connector_config(&agent).unwrap();
        let acp_config: AcpConfig = serde_json::from_value(config.params).unwrap();
        match &acp_config.transport {
            TransportKind::Stdio { args, .. } => {
                assert_eq!(args, &["--acp"]);
            }
            _ => panic!("Expected stdio transport"),
        }
    }

    #[test]
    fn test_enriched_icon_path_used() {
        let agent = dirigent_zed::ZedAgent {
            name: "claude-acp".to_string(),
            agent_type: dirigent_zed::AgentServerType::Registry,
            binary_path: Some(std::path::PathBuf::from("/path/to/claude")),
            env_overrides: std::collections::HashMap::new(),
            display_name: None,
            description: None,
            args: Vec::new(),
            icon_local_path: Some(std::path::PathBuf::from("/icons/claude-acp.svg")),
            icon_url: None,
        };

        let config = zed_agent_to_connector_config(&agent).unwrap();
        assert_eq!(config.icon_path.as_deref(), Some("/icons/claude-acp.svg"));
    }
}
