// MCP config reference: https://code.claude.com/docs/en/mcp
// Config file: ~/.claude.json top-level "mcpServers" (user scope), .mcp.json (project scope)
// Format: JSON, top-level key "mcpServers", sub-keys: command, args, env, type, url, headers
//
// Plugin reference:
//   https://code.claude.com/docs/en/discover-plugins
//   https://code.claude.com/docs/en/plugins
// Plugins: ~/.claude/plugins/, registry at installed_plugins.json, manifest at .claude-plugin/plugin.json

use super::{AgentAdapter, HookEntry, McpServerEntry, PluginEntry, ProjectMarker};
use std::path::{Path, PathBuf};

pub struct ClaudeAdapter {
    home: PathBuf,
}

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

impl ClaudeAdapter {
    pub fn new() -> Self {
        Self {
            home: dirs::home_dir().unwrap_or_default(),
        }
    }

    #[cfg(test)]
    pub fn with_home(home: PathBuf) -> Self {
        Self { home }
    }

    fn read_settings(&self) -> Option<serde_json::Value> {
        let path = self.base_dir().join("settings.json");
        Self::parse_json(&path)
    }

    fn parse_json(path: &Path) -> Option<serde_json::Value> {
        let content = std::fs::read_to_string(path).ok()?;
        serde_json::from_str(&content).ok()
    }
}

impl AgentAdapter for ClaudeAdapter {
    fn name(&self) -> &str {
        "claude"
    }

    fn base_dir(&self) -> PathBuf {
        self.home.join(".claude")
    }

    fn detect(&self) -> bool {
        self.base_dir().exists()
    }

    fn skill_dirs(&self) -> Vec<PathBuf> {
        vec![self.base_dir().join("skills")]
    }

    fn mcp_config_path(&self) -> PathBuf {
        self.home.join(".claude.json")
    }

    fn hook_config_path(&self) -> PathBuf {
        self.base_dir().join("settings.json")
    }

    fn plugin_dirs(&self) -> Vec<PathBuf> {
        vec![self.base_dir().join("plugins")]
    }

    fn read_mcp_servers(&self) -> Vec<McpServerEntry> {
        // MCP servers are in ~/.claude.json (not settings.json)
        self.read_mcp_servers_from(&self.mcp_config_path())
    }

    fn read_mcp_servers_from(&self, path: &Path) -> Vec<McpServerEntry> {
        let Some(settings) = Self::parse_json(path) else {
            return vec![];
        };
        let Some(servers) = settings.get("mcpServers").and_then(|v| v.as_object()) else {
            return vec![];
        };

        servers
            .iter()
            .map(|(name, val)| McpServerEntry {
                name: name.clone(),
                command: val
                    .get("command")
                    .and_then(|v| v.as_str())
                    .unwrap_or("")
                    .into(),
                args: val
                    .get("args")
                    .and_then(|v| v.as_array())
                    .map(|arr| {
                        arr.iter()
                            .filter_map(|v| v.as_str().map(String::from))
                            .collect()
                    })
                    .unwrap_or_default(),
                env: val
                    .get("env")
                    .and_then(|v| v.as_object())
                    .map(|obj| {
                        obj.iter()
                            .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
                            .collect()
                    })
                    .unwrap_or_default(),
                // Claude's MCP schema has no agent-native disable concept.
                enabled: true,
            })
            .collect()
    }

    fn translate_hook_event(&self, event: &str) -> Option<String> {
        super::hook_events::to_claude(event)
    }

    fn read_hooks(&self) -> Vec<HookEntry> {
        self.read_hooks_from(&self.hook_config_path())
    }

    fn read_hooks_from(&self, path: &Path) -> Vec<HookEntry> {
        let Some(settings) = Self::parse_json(path) else {
            return vec![];
        };
        let Some(hooks) = settings.get("hooks").and_then(|v| v.as_object()) else {
            return vec![];
        };

        let mut entries = Vec::new();
        for (event, hook_list) in hooks {
            let Some(arr) = hook_list.as_array() else {
                continue;
            };
            for hook in arr {
                let matcher = hook
                    .get("matcher")
                    .and_then(|v| v.as_str())
                    .map(String::from);
                if let Some(cmds) = hook.get("hooks").and_then(|v| v.as_array()) {
                    for cmd in cmds {
                        // String format: "echo test"
                        let cmd_str = if let Some(s) = cmd.as_str() {
                            Some(s.to_string())
                        }
                        // Object format: {"type": "command", "command": "echo test"}
                        else if let Some(s) = cmd.get("command").and_then(|v| v.as_str()) {
                            Some(s.to_string())
                        }
                        // Prompt/agent hook: {"type": "prompt", "prompt": "..."}
                        else {
                            cmd.get("prompt")
                                .and_then(|v| v.as_str())
                                .map(|s| s.to_string())
                        };
                        if let Some(command) = cmd_str {
                            entries.push(HookEntry {
                                event: event.clone(),
                                matcher: matcher.clone(),
                                command,
                            });
                        }
                    }
                }
            }
        }
        entries
    }

    fn global_rules_files(&self) -> Vec<PathBuf> {
        let mut files = vec![self.base_dir().join("CLAUDE.md")];
        // Also scan ~/.claude/rules/*.md
        files.extend(super::files_with_ext(&self.base_dir().join("rules"), "md"));
        files
    }

    fn global_memory_files(&self) -> Vec<PathBuf> {
        // ~/.claude/projects/*/memory/*.md — outer level iterates project
        // dirs (no extension filter); inner level reuses the helper.
        let projects_dir = self.base_dir().join("projects");
        let mut files = Vec::new();
        if let Ok(entries) = std::fs::read_dir(&projects_dir) {
            for entry in entries.flatten() {
                let memory_dir = entry.path().join("memory");
                if memory_dir.is_dir() {
                    files.extend(super::files_with_ext(&memory_dir, "md"));
                }
            }
        }
        files
    }

    fn global_settings_files(&self) -> Vec<PathBuf> {
        let mut files = vec![
            self.home.join(".claude.json"),
            self.base_dir().join("settings.json"),
            self.base_dir().join("settings.local.json"),
            self.base_dir().join("keybindings.json"),
        ];
        // ~/.claude/commands/*.md (legacy, still functional)
        files.extend(super::files_with_ext(&self.base_dir().join("commands"), "md"));
        // ~/.claude/output-styles/*.md
        files.extend(super::files_with_ext(&self.base_dir().join("output-styles"), "md"));
        files
    }

    fn global_subagent_files(&self) -> Vec<PathBuf> {
        // ~/.claude/agents/*.md
        super::files_with_ext(&self.base_dir().join("agents"), "md").collect()
    }

    fn project_markers(&self) -> Vec<ProjectMarker> {
        vec![
            ProjectMarker::Dir(".claude"),
            ProjectMarker::File(".mcp.json"),
        ]
    }

    fn project_rules_patterns(&self) -> Vec<String> {
        vec![
            "CLAUDE.md".into(),
            ".claude/CLAUDE.md".into(),
            ".claude/rules/*.md".into(),
        ]
    }

    fn project_settings_patterns(&self) -> Vec<String> {
        vec![
            ".claude/settings.json".into(),
            ".claude/settings.local.json".into(),
            ".mcp.json".into(),
        ]
    }

    fn project_subagent_patterns(&self) -> Vec<String> {
        vec![".claude/agents/*.md".into()]
    }

    fn project_ignore_patterns(&self) -> Vec<String> {
        vec![] // Claude Code does NOT have .claudeignore
    }

    fn project_skill_dirs(&self) -> Vec<String> {
        vec![".claude/skills".into()]
    }

    fn project_mcp_config_relpath(&self) -> Option<String> {
        Some(".mcp.json".into())
    }

    fn project_hook_config_relpath(&self) -> Option<String> {
        // Claude project hooks live in `.claude/settings.json` alongside other settings.
        Some(".claude/settings.json".into())
    }

    fn read_plugins(&self) -> Vec<PluginEntry> {
        // Read from installed_plugins.json which has precise per-plugin timestamps
        let registry_path = self
            .base_dir()
            .join("plugins")
            .join("installed_plugins.json");
        let content = match std::fs::read_to_string(&registry_path) {
            Ok(c) => c,
            Err(_) => return vec![],
        };
        let json: serde_json::Value = match serde_json::from_str(&content) {
            Ok(v) => v,
            Err(_) => return vec![],
        };
        let Some(plugins) = json.get("plugins").and_then(|v| v.as_object()) else {
            return vec![];
        };

        // Also read enabledPlugins from settings.json to know which are enabled
        let enabled_set: std::collections::HashSet<String> = self
            .read_settings()
            .and_then(|s| s.get("enabledPlugins")?.as_object().cloned())
            .map(|obj| {
                obj.into_iter()
                    .filter(|(_, v)| v.as_bool().unwrap_or(false))
                    .map(|(k, _)| k)
                    .collect()
            })
            .unwrap_or_default();

        let mut entries = Vec::new();
        for (key, installs) in plugins {
            // key format: "plugin-name@marketplace"
            let (name, source) = key
                .rsplit_once('@')
                .map(|(n, s)| (n.to_string(), s.to_string()))
                .unwrap_or_else(|| (key.clone(), String::new()));

            // installs is an array; take the first entry (user scope)
            let Some(install) = installs.as_array().and_then(|a| a.first()) else {
                continue;
            };

            let install_path = install
                .get("installPath")
                .and_then(|v| v.as_str())
                .map(PathBuf::from)
                .and_then(|p| p.parent().map(PathBuf::from)); // strip version component

            let installed_at = install
                .get("installedAt")
                .and_then(|v| v.as_str())
                .and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok())
                .map(|dt| dt.with_timezone(&chrono::Utc));

            let updated_at = install
                .get("lastUpdated")
                .and_then(|v| v.as_str())
                .and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok())
                .map(|dt| dt.with_timezone(&chrono::Utc));

            entries.push(PluginEntry {
                name,
                source: source.clone(),
                enabled: enabled_set.contains(key),
                path: install_path,
                uri: None,
                installed_at,
                updated_at,
            });
        }
        entries
    }
}

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

    #[test]
    fn test_claude_adapter_name() {
        let adapter = ClaudeAdapter::new();
        assert_eq!(adapter.name(), "claude");
    }

    #[test]
    fn test_claude_detect_with_dir() {
        let dir = TempDir::new().unwrap();
        let claude_dir = dir.path().join(".claude");
        std::fs::create_dir_all(&claude_dir).unwrap();
        let adapter = ClaudeAdapter::with_home(dir.path().to_path_buf());
        assert!(adapter.detect());
    }

    #[test]
    fn test_claude_detect_without_dir() {
        let dir = TempDir::new().unwrap();
        let adapter = ClaudeAdapter::with_home(dir.path().to_path_buf());
        assert!(!adapter.detect());
    }

    #[test]
    fn test_claude_skill_dirs() {
        let dir = TempDir::new().unwrap();
        let adapter = ClaudeAdapter::with_home(dir.path().to_path_buf());
        let dirs = adapter.skill_dirs();
        assert_eq!(dirs.len(), 1);
        assert!(dirs[0].ends_with(".claude/skills"));
    }

    #[test]
    fn test_claude_read_mcp_servers() {
        let dir = TempDir::new().unwrap();
        // MCP config lives at ~/.claude.json (not ~/.claude/settings.json)
        std::fs::write(
            dir.path().join(".claude.json"),
            r#"{"mcpServers":{"github":{"command":"npx","args":["-y","@modelcontextprotocol/server-github"],"env":{"GITHUB_TOKEN":"ghp_test"}}}}"#,
        ).unwrap();
        let adapter = ClaudeAdapter::with_home(dir.path().to_path_buf());
        let servers = adapter.read_mcp_servers();
        assert_eq!(servers.len(), 1);
        assert_eq!(servers[0].name, "github");
        assert_eq!(servers[0].command, "npx");
    }

    #[test]
    fn test_claude_read_hooks_string_format() {
        let dir = TempDir::new().unwrap();
        let claude_dir = dir.path().join(".claude");
        std::fs::create_dir_all(&claude_dir).unwrap();
        std::fs::write(
            claude_dir.join("settings.json"),
            r#"{"hooks":{"PreToolUse":[{"matcher":"Bash","hooks":["echo test"]}]}}"#,
        )
        .unwrap();
        let adapter = ClaudeAdapter::with_home(dir.path().to_path_buf());
        let hooks = adapter.read_hooks();
        assert_eq!(hooks.len(), 1);
        assert_eq!(hooks[0].event, "PreToolUse");
        assert_eq!(hooks[0].command, "echo test");
    }

    #[test]
    fn test_claude_read_hooks_object_format() {
        let dir = TempDir::new().unwrap();
        let claude_dir = dir.path().join(".claude");
        std::fs::create_dir_all(&claude_dir).unwrap();
        std::fs::write(
            claude_dir.join("settings.json"),
            r#"{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"afplay /System/Library/Sounds/Glass.aiff"}]}]}}"#,
        ).unwrap();
        let adapter = ClaudeAdapter::with_home(dir.path().to_path_buf());
        let hooks = adapter.read_hooks();
        assert_eq!(hooks.len(), 1);
        assert_eq!(hooks[0].event, "Stop");
        assert_eq!(hooks[0].command, "afplay /System/Library/Sounds/Glass.aiff");
    }

    #[test]
    fn test_claude_config_methods() {
        let tmp = tempfile::tempdir().unwrap();
        let adapter = ClaudeAdapter::with_home(tmp.path().to_path_buf());

        let global_rules = adapter.global_rules_files();
        // Without a rules dir, only CLAUDE.md is returned
        assert_eq!(global_rules.len(), 1);
        assert!(global_rules[0].ends_with("CLAUDE.md"));

        let global_settings = adapter.global_settings_files();
        assert!(global_settings.len() >= 4);
        assert!(global_settings[0].ends_with(".claude.json"));
        assert!(global_settings[1].ends_with("settings.json"));
        assert!(global_settings[2].ends_with("settings.local.json"));
        assert!(global_settings[3].ends_with("keybindings.json"));

        let project_rules = adapter.project_rules_patterns();
        assert!(project_rules.contains(&"CLAUDE.md".to_string()));
        assert!(project_rules.contains(&".claude/CLAUDE.md".to_string()));
        assert!(project_rules.contains(&".claude/rules/*.md".to_string()));

        let project_settings = adapter.project_settings_patterns();
        assert!(project_settings.contains(&".claude/settings.json".to_string()));
        assert!(project_settings.contains(&".claude/settings.local.json".to_string()));
        assert!(project_settings.contains(&".mcp.json".to_string()));

        let project_ignore = adapter.project_ignore_patterns();
        assert!(project_ignore.is_empty());
    }

    #[test]
    fn test_claude_subagent_methods() {
        let tmp = tempfile::tempdir().unwrap();
        let adapter = ClaudeAdapter::with_home(tmp.path().to_path_buf());

        // Missing agents/ dir → empty.
        assert!(adapter.global_subagent_files().is_empty());

        // Populate agents/ with one .md and one non-.md (must be filtered).
        let agents_dir = adapter.base_dir().join("agents");
        std::fs::create_dir_all(&agents_dir).unwrap();
        std::fs::write(agents_dir.join("reviewer.md"), "# reviewer").unwrap();
        std::fs::write(agents_dir.join("notes.txt"), "ignore me").unwrap();

        let subagents = adapter.global_subagent_files();
        assert!(subagents.iter().any(|p| p.ends_with("agents/reviewer.md")));
        assert!(
            !subagents.iter().any(|p| p.ends_with("notes.txt")),
            "non-.md files in agents/ must be filtered"
        );

        // Subagent dir contents must NOT leak into settings anymore.
        let settings = adapter.global_settings_files();
        assert!(
            !settings.iter().any(|p| p.ends_with("agents/reviewer.md")),
            "agents/ moved to global_subagent_files; must not appear in settings"
        );

        // Project pattern is the canonical .claude/agents/*.md.
        assert_eq!(
            adapter.project_subagent_patterns(),
            vec![".claude/agents/*.md".to_string()]
        );
    }
}
