//! Claude Code framework adapter.
//!
//! mi6 installs as a Claude Code plugin via the official marketplace system.
//! The plugin files are embedded in the binary and written to `~/.mi6/claude-plugin/`
//! during installation. This avoids needing a separate GitHub repo for the marketplace.
//!
//! # Installation Flow
//!
//! 1. `mi6 enable` writes embedded plugin files to `~/.mi6/claude-plugin/`
//! 2. Registers the local directory as a marketplace via `claude plugin marketplace add`
//! 3. Installs the plugin to user scope via `claude plugin install mi6@mi6 --scope user`
//!
//! # Uninstallation
//!
//! `mi6 disable` uninstalls via `claude plugin uninstall mi6@mi6`. It detects the
//! installation scope to handle legacy project-scope installs correctly.

use super::{
    FrameworkAdapter, InstallHooksResult, ParsedHookInput, ParsedHookInputBuilder,
    UninstallHooksResult, common,
};
use crate::model::EventType;
use crate::model::error::InitError;
use std::path::{Path, PathBuf};
use std::process::Command;

/// Plugin identifier in format `plugin@marketplace`.
const PLUGIN_ID: &str = "mi6@mi6";

/// Embedded marketplace.json content.
const MARKETPLACE_JSON: &str = include_str!("claude-plugin/marketplace.json");

/// Embedded plugin.json content.
const PLUGIN_JSON: &str = include_str!("claude-plugin/plugin.json");

/// Embedded hooks.json content.
const HOOKS_JSON: &str = include_str!("claude-plugin/hooks.json");

/// Claude Code framework adapter.
pub struct ClaudeAdapter;

impl ClaudeAdapter {
    /// Run a claude CLI command, optionally ignoring specific error patterns.
    ///
    /// When `ignore_patterns` contains strings that match the error output,
    /// the command is treated as successful (for idempotent operations).
    ///
    /// Returns the full command string that was executed for verbose logging.
    fn run_claude_command_opt(
        args: &[&str],
        cwd: Option<&Path>,
        ignore_patterns: &[&str],
    ) -> Result<String, InitError> {
        let mut cmd = Command::new("claude");
        cmd.args(args);
        if let Some(dir) = cwd {
            cmd.current_dir(dir);
        }

        // Build the command string for verbose output
        let cmd_str = format!("claude {}", args.join(" "));

        let output = cmd
            .output()
            .map_err(|e| InitError::Config(format!("failed to run claude CLI: {e}")))?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            let stdout = String::from_utf8_lossy(&output.stdout);
            let combined = format!("{} {}", stderr.trim(), stdout.trim());

            // Check if this is an expected "already done" error
            if ignore_patterns.iter().any(|p| combined.contains(p)) {
                return Ok(cmd_str);
            }

            return Err(InitError::Config(format!(
                "claude {} failed: {}",
                args.first().unwrap_or(&"command"),
                combined.trim()
            )));
        }

        Ok(cmd_str)
    }

    /// Run a claude CLI command.
    ///
    /// Returns the full command string that was executed for verbose logging.
    fn run_claude_command(args: &[&str]) -> Result<String, InitError> {
        Self::run_claude_command_opt(args, None, &[])
    }

    /// Get the path to the local marketplace cache directory (`~/.mi6/claude-plugin/`).
    fn marketplace_cache_path() -> Result<PathBuf, InitError> {
        let home = dirs::home_dir()
            .ok_or_else(|| InitError::Config("could not determine home directory".to_string()))?;
        Ok(home.join(".mi6/claude-plugin"))
    }

    /// Write embedded marketplace files to the local cache.
    ///
    /// Creates:
    /// ```text
    /// ~/.mi6/claude-plugin/
    /// ├── .claude-plugin/marketplace.json
    /// └── plugins/mi6/
    ///     ├── .claude-plugin/plugin.json
    ///     └── hooks/hooks.json
    /// ```
    fn write_marketplace_to_cache() -> Result<PathBuf, InitError> {
        let cache_path = Self::marketplace_cache_path()?;

        let marketplace_meta = cache_path.join(".claude-plugin");
        let plugin_meta = cache_path.join("plugins/mi6/.claude-plugin");
        let hooks_dir = cache_path.join("plugins/mi6/hooks");

        // Create all directories
        for dir in [&marketplace_meta, &plugin_meta, &hooks_dir] {
            std::fs::create_dir_all(dir).map_err(|e| {
                InitError::Config(format!("failed to create {}: {e}", dir.display()))
            })?;
        }

        // Write embedded files
        let files = [
            (marketplace_meta.join("marketplace.json"), MARKETPLACE_JSON),
            (plugin_meta.join("plugin.json"), PLUGIN_JSON),
            (hooks_dir.join("hooks.json"), HOOKS_JSON),
        ];

        for (path, content) in files {
            std::fs::write(&path, content).map_err(|e| {
                InitError::Config(format!("failed to write {}: {e}", path.display()))
            })?;
        }

        Ok(cache_path)
    }

    /// Check if the mi6 plugin is enabled in settings.json.
    fn is_plugin_installed() -> bool {
        let Some(home) = dirs::home_dir() else {
            return false;
        };
        let settings_path = home.join(".claude/settings.json");
        let Ok(contents) = std::fs::read_to_string(&settings_path) else {
            return false;
        };
        let Ok(json) = serde_json::from_str::<serde_json::Value>(&contents) else {
            return false;
        };
        json.get("enabledPlugins")
            .is_some_and(|enabled| enabled.get(PLUGIN_ID).is_some())
    }

    /// Get the installation scope and project path from installed_plugins.json.
    ///
    /// Returns `Some((scope, project_path))` where:
    /// - `scope` is "user" or "project"
    /// - `project_path` is only present for project-scope installs
    fn get_plugin_install_scope() -> Option<(String, Option<PathBuf>)> {
        let home = dirs::home_dir()?;
        let plugins_path = home.join(".claude/plugins/installed_plugins.json");
        let contents = std::fs::read_to_string(&plugins_path).ok()?;
        let json: serde_json::Value = serde_json::from_str(&contents).ok()?;

        // Structure: { "plugins": { "mi6@mi6": [{ "scope": "...", "projectPath": "..." }] } }
        let install = json.get("plugins")?.get(PLUGIN_ID)?.as_array()?.first()?;

        let scope = install.get("scope")?.as_str()?.to_string();
        let project_path = install
            .get("projectPath")
            .and_then(|v| v.as_str())
            .map(PathBuf::from);

        Some((scope, project_path))
    }
}

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

    fn display_name(&self) -> &'static str {
        "Claude Code"
    }

    fn project_config_path(&self) -> PathBuf {
        // For marketplace plugins, there's no local config to edit
        // Return a placeholder path
        PathBuf::from(".claude/plugins/mi6/hooks/hooks.json")
    }

    fn user_config_path(&self) -> Option<PathBuf> {
        // For marketplace plugins, Claude manages the installation path
        None
    }

    fn generate_hooks_config(
        &self,
        enabled_events: &[EventType],
        mi6_bin: &str,
        otel_enabled: bool,
        otel_port: u16,
    ) -> serde_json::Value {
        // This is used for --print mode to show what hooks would be configured
        let mut hooks = serde_json::Map::new();

        for event in enabled_events {
            let command = if otel_enabled && *event == EventType::SessionStart {
                format!(
                    "{} otel start --port {} </dev/null >/dev/null 2>&1; {} ingest event {} --framework claude",
                    mi6_bin, otel_port, mi6_bin, event
                )
            } else {
                format!("{} ingest event {} --framework claude", mi6_bin, event)
            };

            let hook_entry = serde_json::json!([{
                "matcher": "*",
                "hooks": [{
                    "type": "command",
                    "command": command,
                    "timeout": 10
                }]
            }]);
            hooks.insert(event.to_string(), hook_entry);
        }

        serde_json::json!({ "hooks": hooks })
    }

    fn merge_config(
        &self,
        generated: serde_json::Value,
        _existing: Option<serde_json::Value>,
    ) -> serde_json::Value {
        generated
    }

    fn parse_hook_input(
        &self,
        _event_type: &str,
        stdin_json: &serde_json::Value,
    ) -> ParsedHookInput {
        ParsedHookInputBuilder::new(stdin_json)
            .session_id("session_id")
            .tool_use_id("tool_use_id")
            .tool_name("tool_name")
            .cwd("cwd")
            .permission_mode("permission_mode")
            .transcript_path("transcript_path")
            .subagent_type("tool_input.subagent_type")
            .spawned_agent_id("tool_response.agentId")
            .session_source("source")
            .agent_id("agent_id")
            .agent_transcript_path("agent_transcript_path")
            .compact_trigger("trigger")
            .build()
        // Note: Claude doesn't provide model/duration/tokens/cost via hooks (uses OTel)
    }

    fn map_event_type(&self, framework_event: &str) -> EventType {
        framework_event
            .parse()
            .unwrap_or_else(|_| EventType::Custom(framework_event.to_string()))
    }

    fn supported_events(&self) -> Vec<&'static str> {
        vec![
            "SessionStart",
            "SessionEnd",
            "PreToolUse",
            "PostToolUse",
            "PermissionRequest",
            "PreCompact",
            "Stop",
            "SubagentStart",
            "SubagentStop",
            "Notification",
            "UserPromptSubmit",
        ]
    }

    fn detection_env_vars(&self) -> &[&'static str] {
        &["CLAUDE_SESSION_ID", "CLAUDE_PROJECT_DIR"]
    }

    fn is_installed(&self) -> bool {
        common::is_framework_installed(dirs::home_dir().map(|h| h.join(".claude")), "claude")
    }

    fn otel_support(&self) -> super::OtelSupport {
        use super::OtelSupport;
        // Claude supports OTEL via env vars in settings.json
        let Some(home) = dirs::home_dir() else {
            return OtelSupport::Disabled;
        };
        let settings_path = home.join(".claude/settings.json");
        let Ok(contents) = std::fs::read_to_string(&settings_path) else {
            return OtelSupport::Disabled;
        };
        let Ok(json) = serde_json::from_str::<serde_json::Value>(&contents) else {
            return OtelSupport::Disabled;
        };

        // Check for OTEL env vars in the "env" section
        if let Some(env) = json.get("env")
            && (env.get("OTEL_EXPORTER_OTLP_ENDPOINT").is_some()
                || env.get("OTEL_LOGS_EXPORTER").is_some())
        {
            return OtelSupport::Enabled;
        }
        OtelSupport::Disabled
    }

    fn remove_hooks(&self, _existing: serde_json::Value) -> Option<serde_json::Value> {
        None
    }

    // ========================================================================
    // Marketplace-based installation
    // ========================================================================

    fn settings_path(&self, _local: bool, _settings_local: bool) -> Result<PathBuf, InitError> {
        // Return the actual plugin directory path for display purposes
        Self::marketplace_cache_path()
    }

    fn has_mi6_hooks(&self, _local: bool, _settings_local: bool) -> bool {
        Self::is_plugin_installed()
    }

    fn install_hooks(
        &self,
        _path: &std::path::Path,
        _hooks: &serde_json::Value,
        _otel_env: Option<serde_json::Value>,
        _remove_otel: bool,
    ) -> Result<InstallHooksResult, InitError> {
        let mut commands_run = Vec::new();

        // Write embedded marketplace files to local cache
        let cache_path = Self::write_marketplace_to_cache()?;
        let cache_path_str = cache_path.to_string_lossy();

        // Add the marketplace (idempotent - ignore if already registered)
        let cmd = Self::run_claude_command_opt(
            &["plugin", "marketplace", "add", &cache_path_str],
            None,
            &["already installed", "already added", "already registered"],
        )?;
        commands_run.push(cmd);

        // Install the plugin to user scope (idempotent - ignore if already installed)
        let cmd = Self::run_claude_command_opt(
            &["plugin", "install", PLUGIN_ID, "--scope", "user"],
            None,
            &["already installed", "already enabled"],
        )?;
        commands_run.push(cmd);

        Ok(InstallHooksResult { commands_run })
    }

    fn uninstall_hooks(
        &self,
        _local: bool,
        _settings_local: bool,
    ) -> Result<UninstallHooksResult, InitError> {
        if !Self::is_plugin_installed() {
            return Ok(UninstallHooksResult {
                hooks_removed: false,
                commands_run: vec![],
            });
        }

        let mut commands_run = Vec::new();

        // Determine the installation scope to use the correct uninstall command
        match Self::get_plugin_install_scope() {
            Some((scope, Some(project_path))) if scope == "project" => {
                // Project-scope: must run from the project directory
                let cmd = Self::run_claude_command_opt(
                    &["plugin", "uninstall", PLUGIN_ID, "--scope", "project"],
                    Some(&project_path),
                    &[],
                )?;
                commands_run.push(cmd);
            }
            _ => {
                // User scope (default) or unknown - try user scope
                let cmd = Self::run_claude_command(&[
                    "plugin",
                    "uninstall",
                    PLUGIN_ID,
                    "--scope",
                    "user",
                ])?;
                commands_run.push(cmd);
            }
        }

        Ok(UninstallHooksResult {
            hooks_removed: true,
            commands_run,
        })
    }
}

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

    #[test]
    fn test_name() {
        let adapter = ClaudeAdapter;
        assert_eq!(adapter.name(), "claude");
        assert_eq!(adapter.display_name(), "Claude Code");
    }

    #[test]
    fn test_map_event_type() {
        let adapter = ClaudeAdapter;
        assert_eq!(
            adapter.map_event_type("SessionStart"),
            EventType::SessionStart
        );
        assert_eq!(adapter.map_event_type("PreToolUse"), EventType::PreToolUse);
        assert_eq!(
            adapter.map_event_type("CustomEvent"),
            EventType::Custom("CustomEvent".to_string())
        );
    }

    #[test]
    fn test_parse_hook_input() {
        let adapter = ClaudeAdapter;
        let input = serde_json::json!({
            "session_id": "test-session",
            "tool_use_id": "tool-123",
            "tool_name": "Bash",
            "cwd": "/projects/test",
            "permission_mode": "default",
            "tool_input": {
                "subagent_type": "Explore"
            },
            "tool_response": {
                "agentId": "agent-456"
            }
        });

        let parsed = adapter.parse_hook_input("PreToolUse", &input);

        assert_eq!(parsed.session_id, Some("test-session".to_string()));
        assert_eq!(parsed.tool_use_id, Some("tool-123".to_string()));
        assert_eq!(parsed.tool_name, Some("Bash".to_string()));
        assert_eq!(parsed.cwd, Some("/projects/test".to_string()));
        assert_eq!(parsed.permission_mode, Some("default".to_string()));
        assert_eq!(parsed.subagent_type, Some("Explore".to_string()));
        assert_eq!(parsed.spawned_agent_id, Some("agent-456".to_string()));
    }

    #[test]
    fn test_parse_hook_input_new_fields() {
        let adapter = ClaudeAdapter;

        // Test SessionStart with source field
        let session_start_input = serde_json::json!({
            "session_id": "test-session",
            "source": "startup",
            "cwd": "/projects/test"
        });
        let parsed = adapter.parse_hook_input("SessionStart", &session_start_input);
        assert_eq!(parsed.session_source, Some("startup".to_string()));

        // Test SubagentStop with agent_id and agent_transcript_path
        let subagent_stop_input = serde_json::json!({
            "session_id": "parent-session",
            "agent_id": "subagent-123",
            "agent_transcript_path": "/tmp/transcripts/subagent.jsonl"
        });
        let parsed = adapter.parse_hook_input("SubagentStop", &subagent_stop_input);
        assert_eq!(parsed.agent_id, Some("subagent-123".to_string()));
        assert_eq!(
            parsed.agent_transcript_path,
            Some("/tmp/transcripts/subagent.jsonl".to_string())
        );

        // Test PreCompact with trigger field
        let pre_compact_input = serde_json::json!({
            "session_id": "test-session",
            "trigger": "auto"
        });
        let parsed = adapter.parse_hook_input("PreCompact", &pre_compact_input);
        assert_eq!(parsed.compact_trigger, Some("auto".to_string()));
    }

    #[test]
    fn test_generate_hooks_config() -> Result<(), String> {
        let adapter = ClaudeAdapter;
        let events = vec![EventType::SessionStart, EventType::PreToolUse];

        let config = adapter.generate_hooks_config(&events, "mi6", false, 4318);

        let hooks = config
            .get("hooks")
            .ok_or("missing hooks")?
            .as_object()
            .ok_or("hooks not an object")?;
        assert!(hooks.contains_key("SessionStart"));
        assert!(hooks.contains_key("PreToolUse"));

        let session_start = &hooks["SessionStart"][0]["hooks"][0];
        let command = session_start["command"].as_str().ok_or("missing command")?;
        assert!(command.contains("mi6 ingest event SessionStart"));
        assert!(command.contains("--framework claude"));
        Ok(())
    }

    #[test]
    fn test_generate_hooks_config_with_otel() -> Result<(), String> {
        let adapter = ClaudeAdapter;
        let events = vec![EventType::SessionStart];

        let config = adapter.generate_hooks_config(&events, "mi6", true, 4318);

        let hooks = config
            .get("hooks")
            .ok_or("missing hooks")?
            .as_object()
            .ok_or("hooks not an object")?;
        let session_start = &hooks["SessionStart"][0]["hooks"][0];
        let command = session_start["command"].as_str().ok_or("missing command")?;

        assert!(command.contains("otel start"));
        assert!(command.contains("--port 4318"));
        assert!(command.contains("--framework claude"));
        Ok(())
    }

    #[test]
    fn test_generate_hooks_config_matcher_structure() -> Result<(), String> {
        let adapter = ClaudeAdapter;
        let events = vec![
            EventType::SessionStart,
            EventType::PreToolUse,
            EventType::PostToolUse,
        ];

        let config = adapter.generate_hooks_config(&events, "mi6", false, 4318);
        let hooks = config.get("hooks").ok_or("missing hooks")?;

        // All events should have matcher field with "*" for wildcard
        for event in &events {
            let hook = &hooks[event.to_string()][0];
            assert_eq!(
                hook.get("matcher").and_then(|m| m.as_str()),
                Some("*"),
                "{} should have matcher: \"*\"",
                event
            );
        }

        Ok(())
    }

    #[test]
    fn test_plugin_constants() {
        assert_eq!(PLUGIN_ID, "mi6@mi6");
    }

    #[test]
    fn test_embedded_marketplace_files() {
        // Verify embedded files are valid JSON
        let _: serde_json::Value =
            serde_json::from_str(MARKETPLACE_JSON).expect("marketplace.json should be valid JSON");
        let _: serde_json::Value =
            serde_json::from_str(PLUGIN_JSON).expect("plugin.json should be valid JSON");
        let _: serde_json::Value =
            serde_json::from_str(HOOKS_JSON).expect("hooks.json should be valid JSON");
    }
}
