//! Registry metadata parsing for Zed ACP agents.
//!
//! Reads the local `registry.json` file that Zed caches from the ACP registry CDN.
//! Provides enrichment data (display names, descriptions, command args, icon paths)
//! for discovered agents.

use serde::Deserialize;
use std::collections::HashMap;
use std::path::{Path, PathBuf};

/// Metadata about an agent from the ACP registry.
#[derive(Debug, Clone)]
pub struct RegistryAgentInfo {
    /// Registry identifier (e.g. "claude-acp", "codex-acp").
    pub id: String,
    /// Human-friendly display name (e.g. "Claude Agent", "Codex CLI").
    pub display_name: String,
    /// Short description of the agent.
    pub description: String,
    /// Icon URL from the CDN.
    pub icon_url: Option<String>,
    /// Path to the locally cached icon file (SVG), if it exists.
    pub icon_local_path: Option<PathBuf>,
    /// Version string from the registry.
    pub version: String,
    /// Command arguments from the distribution config.
    ///
    /// For npx-distributed agents this may include flags like `["--acp"]`.
    /// For binary-distributed agents this is the platform-specific `cmd` value.
    pub args: Vec<String>,
    /// The command to run, extracted from the platform-appropriate distribution.
    ///
    /// For binary distributions this is the `cmd` field (e.g. `"./codex-acp"`).
    /// For npx distributions this is the npx package specifier.
    pub command: Option<String>,
    /// Environment variables from the distribution config.
    pub env: HashMap<String, String>,
}

// ---------------------------------------------------------------------------
// Raw serde models for registry.json
// ---------------------------------------------------------------------------

#[derive(Debug, Deserialize)]
struct RegistryFile {
    #[serde(default)]
    agents: Vec<RawRegistryAgent>,
}

#[derive(Debug, Deserialize)]
struct RawRegistryAgent {
    id: String,
    name: String,
    #[serde(default)]
    version: String,
    #[serde(default)]
    description: String,
    #[serde(default)]
    icon: Option<String>,
    #[serde(default)]
    distribution: Option<RawDistribution>,
}

#[derive(Debug, Deserialize)]
struct RawDistribution {
    #[serde(default)]
    binary: Option<HashMap<String, RawBinaryTarget>>,
    #[serde(default)]
    npx: Option<RawNpxDistribution>,
    #[serde(default)]
    uvx: Option<RawUvxDistribution>,
}

#[derive(Debug, Deserialize)]
struct RawBinaryTarget {
    #[serde(default)]
    cmd: Option<String>,
}

#[derive(Debug, Deserialize)]
struct RawNpxDistribution {
    #[serde(default)]
    package: Option<String>,
    #[serde(default)]
    args: Vec<String>,
    #[serde(default)]
    env: HashMap<String, String>,
}

#[derive(Debug, Deserialize)]
struct RawUvxDistribution {
    #[serde(default)]
    package: Option<String>,
    #[serde(default)]
    args: Vec<String>,
    #[serde(default)]
    env: HashMap<String, String>,
}

/// Determine the current platform key used in the registry's binary distribution map.
///
/// Returns keys like `"windows-x86_64"`, `"linux-aarch64"`, `"darwin-aarch64"`, etc.
fn current_platform_key() -> Option<&'static str> {
    #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
    {
        Some("windows-x86_64")
    }
    #[cfg(all(target_os = "windows", target_arch = "aarch64"))]
    {
        Some("windows-aarch64")
    }
    #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
    {
        Some("darwin-aarch64")
    }
    #[cfg(all(target_os = "macos", target_arch = "x86_64"))]
    {
        Some("darwin-x86_64")
    }
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    {
        Some("linux-x86_64")
    }
    #[cfg(all(target_os = "linux", target_arch = "aarch64"))]
    {
        Some("linux-aarch64")
    }
    #[cfg(not(any(
        all(target_os = "windows", target_arch = "x86_64"),
        all(target_os = "windows", target_arch = "aarch64"),
        all(target_os = "macos", target_arch = "aarch64"),
        all(target_os = "macos", target_arch = "x86_64"),
        all(target_os = "linux", target_arch = "x86_64"),
        all(target_os = "linux", target_arch = "aarch64"),
    )))]
    {
        None
    }
}

/// Parse the local Zed registry.json and return a map of agent id -> metadata.
///
/// The registry file lives at `{data_dir}/external_agents/registry/registry.json`.
/// Icons are cached at `{data_dir}/external_agents/registry/icons/{id}.svg`.
///
/// Returns an empty map if the file doesn't exist or can't be parsed.
pub fn parse_registry(data_dir: &Path) -> HashMap<String, RegistryAgentInfo> {
    let registry_dir = data_dir.join("external_agents").join("registry");
    let registry_path = registry_dir.join("registry.json");

    let content = match std::fs::read_to_string(&registry_path) {
        Ok(c) => c,
        Err(e) => {
            tracing::debug!(
                path = %registry_path.display(),
                error = %e,
                "Could not read Zed registry.json"
            );
            return HashMap::new();
        }
    };

    let registry: RegistryFile = match serde_json::from_str(&content) {
        Ok(r) => r,
        Err(e) => {
            tracing::warn!(
                path = %registry_path.display(),
                error = %e,
                "Failed to parse Zed registry.json"
            );
            return HashMap::new();
        }
    };

    let icons_dir = registry_dir.join("icons");
    let platform_key = current_platform_key();

    let mut map = HashMap::with_capacity(registry.agents.len());

    for agent in registry.agents {
        let icon_local_path = {
            let candidate = icons_dir.join(format!("{}.svg", agent.id));
            if candidate.exists() {
                Some(candidate)
            } else {
                None
            }
        };

        let (command, args, env) =
            extract_distribution_info(agent.distribution.as_ref(), platform_key);

        let info = RegistryAgentInfo {
            id: agent.id.clone(),
            display_name: agent.name,
            description: agent.description,
            icon_url: agent.icon,
            icon_local_path,
            version: agent.version,
            args,
            command,
            env,
        };

        map.insert(agent.id, info);
    }

    tracing::debug!(
        count = map.len(),
        "Parsed Zed registry with {} agents",
        map.len()
    );

    map
}

/// Extract command, args, and env from the distribution config.
///
/// Priority: binary (platform-specific) > npx > uvx.
fn extract_distribution_info(
    distribution: Option<&RawDistribution>,
    platform_key: Option<&str>,
) -> (Option<String>, Vec<String>, HashMap<String, String>) {
    let dist = match distribution {
        Some(d) => d,
        None => return (None, Vec::new(), HashMap::new()),
    };

    // Prefer binary distribution for the current platform.
    if let Some(ref binary) = dist.binary {
        if let Some(key) = platform_key {
            if let Some(target) = binary.get(key) {
                let cmd = target.cmd.clone();
                return (cmd, Vec::new(), HashMap::new());
            }
        }
    }

    // Fall back to npx distribution.
    if let Some(ref npx) = dist.npx {
        return (
            npx.package.clone(),
            npx.args.clone(),
            npx.env.clone(),
        );
    }

    // Fall back to uvx distribution.
    if let Some(ref uvx) = dist.uvx {
        return (
            uvx.package.clone(),
            uvx.args.clone(),
            uvx.env.clone(),
        );
    }

    (None, Vec::new(), HashMap::new())
}

/// Look up a registry entry by matching an agent name or directory name to a registry id.
///
/// The matching strategy:
/// 1. Exact match on registry id
/// 2. Substring: registry id contained in agent name, or vice versa
/// 3. Core-name match: strip common ACP suffixes and compare base names
///    (e.g. "claude-agent-acp" and "claude-acp" both have core name "claude")
pub fn find_registry_match<'a>(
    agent_name: &str,
    registry: &'a HashMap<String, RegistryAgentInfo>,
) -> Option<&'a RegistryAgentInfo> {
    let name_lower = agent_name.to_lowercase();

    // 1. Exact match on registry id.
    if let Some(info) = registry.get(&name_lower) {
        return Some(info);
    }

    // 2. Substring match: registry id contained in agent name, or vice versa.
    let mut best: Option<&'a RegistryAgentInfo> = None;
    let mut best_len = 0;

    for (id, info) in registry {
        let id_lower = id.to_lowercase();
        if name_lower.contains(&id_lower) || id_lower.contains(&name_lower) {
            if id_lower.len() > best_len {
                best = Some(info);
                best_len = id_lower.len();
            }
        }
    }

    if best.is_some() {
        return best;
    }

    // 3. Core-name match: strip ACP-related suffixes and compare.
    let agent_core = strip_acp_suffixes(&name_lower);
    for (id, info) in registry {
        let id_lower = id.to_lowercase();
        let id_core = strip_acp_suffixes(&id_lower);
        if !agent_core.is_empty() && agent_core == id_core {
            return Some(info);
        }
    }

    None
}

/// Strip common ACP-related suffixes to extract the core agent name.
///
/// For example:
/// - "claude-agent-acp" -> "claude"
/// - "claude-acp" -> "claude"
/// - "claude-code-acp" -> "claude"
/// - "codex" -> "codex"
pub fn strip_acp_suffixes(name: &str) -> &str {
    // Strip known suffixes in order of specificity (longest first).
    for suffix in &["-agent-acp", "-code-acp", "-acp", "-cli"] {
        if let Some(core) = name.strip_suffix(suffix) {
            return core;
        }
    }
    name
}

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

    fn sample_registry_json() -> &'static str {
        r#"{
  "version": "1.0.0",
  "agents": [
    {
      "id": "claude-acp",
      "name": "Claude Agent",
      "version": "0.24.2",
      "description": "ACP wrapper for Anthropic's Claude",
      "icon": "https://cdn.agentclientprotocol.com/registry/v1/latest/claude-acp.svg",
      "distribution": {
        "npx": {
          "package": "@agentclientprotocol/claude-agent-acp@0.24.2"
        }
      }
    },
    {
      "id": "codex-acp",
      "name": "Codex CLI",
      "version": "0.10.0",
      "description": "ACP adapter for OpenAI's coding assistant",
      "icon": "https://cdn.agentclientprotocol.com/registry/v1/latest/codex-acp.svg",
      "distribution": {
        "binary": {
          "linux-x86_64": {
            "archive": "https://example.com/codex.tar.gz",
            "cmd": "./codex-acp"
          },
          "windows-x86_64": {
            "archive": "https://example.com/codex.zip",
            "cmd": "./codex-acp.exe"
          },
          "darwin-aarch64": {
            "archive": "https://example.com/codex-mac.tar.gz",
            "cmd": "./codex-acp"
          }
        },
        "npx": {
          "package": "@zed-industries/codex-acp@0.10.0"
        }
      }
    },
    {
      "id": "auggie",
      "name": "Auggie CLI",
      "version": "0.21.0",
      "description": "Augment Code's powerful software agent",
      "distribution": {
        "npx": {
          "package": "@augmentcode/auggie@0.21.0",
          "args": ["--acp"],
          "env": { "AUGMENT_DISABLE_AUTO_UPDATE": "1" }
        }
      }
    }
  ]
}"#
    }

    #[test]
    fn test_parse_registry_basic() {
        let dir = tempfile::tempdir().unwrap();
        let registry_dir = dir
            .path()
            .join("external_agents")
            .join("registry");
        std::fs::create_dir_all(&registry_dir).unwrap();

        let mut f =
            std::fs::File::create(registry_dir.join("registry.json")).unwrap();
        f.write_all(sample_registry_json().as_bytes()).unwrap();

        let map = parse_registry(dir.path());
        assert_eq!(map.len(), 3);

        let claude = map.get("claude-acp").unwrap();
        assert_eq!(claude.display_name, "Claude Agent");
        assert_eq!(claude.description, "ACP wrapper for Anthropic's Claude");
        assert!(claude.icon_url.is_some());
        // No local icon file created in test.
        assert!(claude.icon_local_path.is_none());

        let auggie = map.get("auggie").unwrap();
        assert_eq!(auggie.args, vec!["--acp"]);
        assert_eq!(
            auggie.env.get("AUGMENT_DISABLE_AUTO_UPDATE"),
            Some(&"1".to_string())
        );
    }

    #[test]
    fn test_parse_registry_with_local_icon() {
        let dir = tempfile::tempdir().unwrap();
        let registry_dir = dir
            .path()
            .join("external_agents")
            .join("registry");
        let icons_dir = registry_dir.join("icons");
        std::fs::create_dir_all(&icons_dir).unwrap();

        std::fs::File::create(registry_dir.join("registry.json"))
            .unwrap()
            .write_all(sample_registry_json().as_bytes())
            .unwrap();

        // Create a fake icon file.
        std::fs::write(icons_dir.join("claude-acp.svg"), "<svg/>").unwrap();

        let map = parse_registry(dir.path());
        let claude = map.get("claude-acp").unwrap();
        assert!(claude.icon_local_path.is_some());
        assert!(claude
            .icon_local_path
            .as_ref()
            .unwrap()
            .to_string_lossy()
            .contains("claude-acp.svg"));
    }

    #[test]
    fn test_parse_registry_missing_file() {
        let dir = tempfile::tempdir().unwrap();
        let map = parse_registry(dir.path());
        assert!(map.is_empty());
    }

    #[test]
    fn test_parse_registry_invalid_json() {
        let dir = tempfile::tempdir().unwrap();
        let registry_dir = dir
            .path()
            .join("external_agents")
            .join("registry");
        std::fs::create_dir_all(&registry_dir).unwrap();
        std::fs::write(registry_dir.join("registry.json"), "not json").unwrap();

        let map = parse_registry(dir.path());
        assert!(map.is_empty());
    }

    #[test]
    fn test_find_registry_match_exact() {
        let dir = tempfile::tempdir().unwrap();
        let registry_dir = dir
            .path()
            .join("external_agents")
            .join("registry");
        std::fs::create_dir_all(&registry_dir).unwrap();
        std::fs::File::create(registry_dir.join("registry.json"))
            .unwrap()
            .write_all(sample_registry_json().as_bytes())
            .unwrap();

        let map = parse_registry(dir.path());

        // Exact match.
        let info = find_registry_match("claude-acp", &map).unwrap();
        assert_eq!(info.display_name, "Claude Agent");
    }

    #[test]
    fn test_find_registry_match_fuzzy() {
        let dir = tempfile::tempdir().unwrap();
        let registry_dir = dir
            .path()
            .join("external_agents")
            .join("registry");
        std::fs::create_dir_all(&registry_dir).unwrap();
        std::fs::File::create(registry_dir.join("registry.json"))
            .unwrap()
            .write_all(sample_registry_json().as_bytes())
            .unwrap();

        let map = parse_registry(dir.path());

        // Agent name "claude" should fuzzy-match "claude-acp".
        let info = find_registry_match("claude", &map).unwrap();
        assert_eq!(info.display_name, "Claude Agent");

        // Directory name "claude-agent-acp" should fuzzy-match "claude-acp".
        let info2 = find_registry_match("claude-agent-acp", &map).unwrap();
        assert_eq!(info2.display_name, "Claude Agent");
    }

    #[test]
    fn test_find_registry_match_no_match() {
        let map = HashMap::new();
        assert!(find_registry_match("nonexistent", &map).is_none());
    }

    #[test]
    fn test_binary_distribution_platform_cmd() {
        let dir = tempfile::tempdir().unwrap();
        let registry_dir = dir
            .path()
            .join("external_agents")
            .join("registry");
        std::fs::create_dir_all(&registry_dir).unwrap();
        std::fs::File::create(registry_dir.join("registry.json"))
            .unwrap()
            .write_all(sample_registry_json().as_bytes())
            .unwrap();

        let map = parse_registry(dir.path());
        let codex = map.get("codex-acp").unwrap();

        // On any supported platform, the binary distribution should produce a command.
        // The exact value depends on the compile target.
        if current_platform_key().is_some() {
            assert!(
                codex.command.is_some(),
                "codex-acp should have a command from binary distribution"
            );
        }
    }
}
