//! Framework adapter system for multi-platform AI coding assistant support.
//!
//! This module provides a trait-based abstraction for different AI coding assistant
//! platforms (Claude, Cursor, Gemini, OpenCode, Codex), allowing mi6 to capture
//! hook events from any supported platform.
//!
//! # Architecture
//!
//! Each platform has its own adapter implementing [`FrameworkAdapter`] that handles:
//! - Hook configuration generation for `mi6 enable`
//! - Event parsing and normalization for `mi6 ingest event`
//! - Framework detection from environment variables
//!
//! # Supported Frameworks
//!
//! - **claude**: The default, fully supported framework (Anthropic's Claude Code)
//! - **cursor**: Cursor IDE with agent mode hooks
//! - **gemini**: Google's Gemini CLI tool
//! - **codex**: OpenAI's Codex CLI tool
//! - **opencode**: OpenCode AI coding agent
//! - **pi**: Pi Coding Agent (TypeScript extension-based)
//!
//! Future support planned for:
//! - Additional framework integrations

mod claude;
mod codex;
pub mod common;
mod cursor;
mod gemini;
mod install;
mod opencode;
mod pi;

pub use claude::ClaudeAdapter;
pub use codex::CodexAdapter;
pub use common::{ParsedHookInputBuilder, get_first_array_element};
pub use cursor::CursorAdapter;
pub use gemini::GeminiAdapter;
pub use install::{
    InitOptions, InitResult, generate_config, initialize, initialize_all, initialize_framework,
    json_to_toml_string,
};
pub use opencode::OpenCodeAdapter;
pub use pi::PiAdapter;

// Re-export InitError from model/error for backward compatibility
pub use crate::model::error::InitError;

use crate::model::EventType;
use crate::model::error::FrameworkResolutionError;
use std::collections::HashSet;
use std::path::{Path, PathBuf};

/// Configuration file format used by a framework.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ConfigFormat {
    /// JSON configuration (default for most frameworks)
    #[default]
    Json,
    /// TOML configuration (used by Codex CLI)
    Toml,
}

/// OpenTelemetry support status for a framework.
///
/// This enum replaces the ambiguous `Option<bool>` pattern to provide
/// clear semantics for OTEL support detection.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum OtelSupport {
    /// OTEL is configured and enabled for this framework.
    Enabled,
    /// OTEL is supported by this framework but not currently enabled.
    Disabled,
    /// This framework does not support OTEL.
    #[default]
    Unsupported,
}

/// Framework adapter trait for AI coding assistant integrations.
///
/// Implementations of this trait handle the platform-specific details of:
/// - Generating hook configuration files
/// - Parsing hook input from stdin
/// - Mapping platform events to canonical event types
pub trait FrameworkAdapter: Send + Sync {
    /// Framework identifier (e.g., "claude", "cursor", "gemini", "opencode").
    fn name(&self) -> &'static str;

    /// Human-readable display name for the framework.
    fn display_name(&self) -> &'static str;

    /// Config file path for project-level settings.
    ///
    /// Returns the path relative to the project root where hooks should be installed.
    fn project_config_path(&self) -> PathBuf;

    /// Config file path for user-level settings.
    ///
    /// Returns the absolute path to the user's global settings file.
    fn user_config_path(&self) -> Option<PathBuf>;

    /// Generate hook configuration JSON for this framework.
    ///
    /// # Arguments
    /// * `enabled_events` - List of event types to enable hooks for
    /// * `mi6_bin` - Path to the mi6 binary (or "mi6" if in PATH)
    /// * `otel_enabled` - Whether OpenTelemetry is enabled
    /// * `otel_port` - Port for OTel server
    ///
    /// # Returns
    /// JSON value representing the hooks configuration for this framework.
    fn generate_hooks_config(
        &self,
        enabled_events: &[EventType],
        mi6_bin: &str,
        otel_enabled: bool,
        otel_port: u16,
    ) -> serde_json::Value;

    /// Merge generated hooks into existing settings.
    ///
    /// # Arguments
    /// * `generated` - The generated hooks configuration
    /// * `existing` - The existing settings file content (if any)
    ///
    /// # Returns
    /// The merged settings JSON.
    fn merge_config(
        &self,
        generated: serde_json::Value,
        existing: Option<serde_json::Value>,
    ) -> serde_json::Value;

    /// Parse hook input JSON from stdin into canonical fields.
    ///
    /// # Arguments
    /// * `event_type` - The framework-specific event type string
    /// * `stdin_json` - Raw JSON from stdin
    ///
    /// # Returns
    /// Parsed hook data with normalized fields.
    fn parse_hook_input(&self, event_type: &str, stdin_json: &serde_json::Value)
    -> ParsedHookInput;

    /// Map a framework-specific event type to a canonical EventType.
    ///
    /// # Arguments
    /// * `framework_event` - The event type string from the framework
    ///
    /// # Returns
    /// The corresponding canonical EventType.
    fn map_event_type(&self, framework_event: &str) -> EventType;

    /// List of hook event types supported by this framework.
    fn supported_events(&self) -> Vec<&'static str>;

    /// Framework-specific events that have no canonical equivalent.
    ///
    /// These events are unique to this framework and will be configured
    /// alongside canonical events. They are stored as `Custom(name)` EventType.
    ///
    /// Override this method to add framework-specific events that don't map
    /// to any canonical event type.
    fn framework_specific_events(&self) -> Vec<&'static str> {
        vec![]
    }

    /// Configuration file format used by this framework.
    ///
    /// Most frameworks use JSON, but some (like Codex) use TOML.
    /// Override this method if the framework uses a non-JSON format.
    fn config_format(&self) -> ConfigFormat {
        ConfigFormat::Json
    }

    /// Environment variables that indicate this framework is the caller.
    fn detection_env_vars(&self) -> &[&'static str];

    /// Detect if this framework is currently calling mi6.
    ///
    /// Checks environment variables to determine if a hook from this
    /// framework is invoking mi6.
    fn detect(&self) -> bool {
        self.detection_env_vars()
            .iter()
            .any(|var| std::env::var(var).is_ok())
    }

    /// Check if this framework is installed on the system.
    ///
    /// Returns true if the framework's config directory exists or
    /// the framework's CLI tool is in PATH.
    fn is_installed(&self) -> bool;

    /// Check OpenTelemetry support status for this framework.
    ///
    /// Returns:
    /// - [`OtelSupport::Enabled`] if OTEL is configured and enabled
    /// - [`OtelSupport::Disabled`] if OTEL is supported but not enabled
    /// - [`OtelSupport::Unsupported`] if the framework doesn't support OTEL
    fn otel_support(&self) -> OtelSupport {
        OtelSupport::Unsupported // Default: framework doesn't support OTEL
    }

    /// Remove mi6 hooks from the framework's settings.
    ///
    /// Returns the modified settings with mi6 hooks removed,
    /// or None if there are no mi6 hooks to remove.
    ///
    /// # Arguments
    /// * `existing` - The existing settings file content
    ///
    /// # Returns
    /// Some(modified settings) if hooks were removed, None if no mi6 hooks found.
    fn remove_hooks(&self, existing: serde_json::Value) -> Option<serde_json::Value>;

    // ========================================================================
    // Default implementations for hook installation
    // ========================================================================

    /// Resolve the settings path for this framework.
    ///
    /// Determines where the hooks configuration should be installed based on
    /// the user's preferences (local vs global, settings_local).
    ///
    /// # Arguments
    /// * `local` - If true, use project-level config instead of user-level.
    /// * `settings_local` - If true, use a `.local` variant (not committed to git).
    fn settings_path(&self, local: bool, settings_local: bool) -> Result<PathBuf, InitError> {
        install::default_settings_path(self, local, settings_local)
    }

    /// Check if this framework has mi6 hooks active in its config file.
    ///
    /// # Arguments
    /// * `local` - If true, check project-level config instead of user-level.
    /// * `settings_local` - If true, check the `.local` variant.
    fn has_mi6_hooks(&self, local: bool, settings_local: bool) -> bool {
        install::default_has_mi6_hooks(self, local, settings_local)
    }

    /// Install hooks configuration to a settings file.
    ///
    /// This method merges the new hooks configuration with any existing settings
    /// and writes the result to the specified path.
    ///
    /// # Arguments
    /// * `path` - Path where settings should be written.
    /// * `hooks` - The hooks configuration to install.
    /// * `otel_env` - Optional OTel environment variables to add.
    /// * `remove_otel` - Whether to remove existing OTel configuration.
    ///
    /// # Returns
    /// An [`InstallHooksResult`] containing any shell commands that were executed.
    fn install_hooks(
        &self,
        path: &Path,
        hooks: &serde_json::Value,
        otel_env: Option<serde_json::Value>,
        remove_otel: bool,
    ) -> Result<InstallHooksResult, InitError> {
        install::default_install_hooks(self, path, hooks, otel_env, remove_otel)
    }

    /// Serialize configuration to string in the appropriate format.
    ///
    /// Returns the configuration as a string in either JSON or TOML format,
    /// depending on the framework's configuration format.
    fn serialize_config(&self, config: &serde_json::Value) -> Result<String, InitError> {
        install::default_serialize_config(self, config)
    }

    /// Uninstall mi6 hooks from this framework.
    ///
    /// This method handles the complete removal of mi6 integration from the framework.
    /// For config-based frameworks, it removes mi6 hooks from the settings file.
    /// For plugin-based frameworks (like Claude Code), it removes the plugin directory.
    ///
    /// # Arguments
    /// * `local` - If true, uninstall from project-level config instead of user-level.
    /// * `settings_local` - If true, uninstall from the `.local` variant.
    ///
    /// # Returns
    /// An [`UninstallHooksResult`] containing whether hooks were removed and
    /// any shell commands that were executed, or an error if uninstallation failed.
    fn uninstall_hooks(
        &self,
        local: bool,
        settings_local: bool,
    ) -> Result<UninstallHooksResult, InitError> {
        install::default_uninstall_hooks(self, local, settings_local)
    }

    /// Get the JSON response to write to stdout for blocking hooks.
    ///
    /// Some frameworks (like Cursor) have blocking hooks that expect a JSON response
    /// to allow/deny the operation. This method returns the appropriate response
    /// for the given event type.
    ///
    /// # Arguments
    /// * `event_type` - The framework-specific event type string
    ///
    /// # Returns
    /// `Some(response)` if this event expects a response, `None` for observation-only hooks.
    ///
    /// # Example
    /// For Cursor's `beforeShellExecution`, returns `{"permission":"allow"}` to allow
    /// the shell command to proceed.
    fn hook_response(&self, _event_type: &str) -> Option<&'static str> {
        None
    }
}

/// Result of hook installation.
///
/// Contains information about what was done during installation,
/// including any shell commands that were executed.
#[derive(Debug, Default, Clone)]
pub struct InstallHooksResult {
    /// Shell commands that were executed during installation.
    /// Each string is the full command that was run.
    pub commands_run: Vec<String>,
}

/// Result of hook uninstallation.
///
/// Contains information about what was done during uninstallation,
/// including any shell commands that were executed.
#[derive(Debug, Default, Clone)]
pub struct UninstallHooksResult {
    /// Whether hooks were actually removed (false if no mi6 hooks were present).
    pub hooks_removed: bool,
    /// Shell commands that were executed during uninstallation.
    /// Each string is the full command that was run.
    pub commands_run: Vec<String>,
}

/// Parsed hook input with normalized fields.
///
/// This struct contains fields extracted from framework-specific hook JSON,
/// normalized to a common format for storage.
#[derive(Debug, Default, Clone)]
pub struct ParsedHookInput {
    /// Session identifier
    pub session_id: Option<String>,
    /// Tool use correlation ID
    pub tool_use_id: Option<String>,
    /// Tool name (Bash, Read, Write, etc.)
    pub tool_name: Option<String>,
    /// Subagent type for Task tool
    pub subagent_type: Option<String>,
    /// Spawned agent ID from PostToolUse
    pub spawned_agent_id: Option<String>,
    /// Permission mode
    pub permission_mode: Option<String>,
    /// Transcript file path
    pub transcript_path: Option<String>,
    /// Working directory
    pub cwd: Option<String>,
    /// SessionStart source (startup, resume, clear)
    pub session_source: Option<String>,
    /// Agent ID from SubagentStop/SubagentStart
    pub agent_id: Option<String>,
    /// Path to agent's transcript file (from SubagentStop)
    pub agent_transcript_path: Option<String>,
    /// PreCompact trigger type (manual, auto)
    pub compact_trigger: Option<String>,
    /// Model name (e.g., "claude-3-opus", "gpt-4")
    pub model: Option<String>,
    /// Duration in milliseconds (for tool execution, API calls, etc.)
    pub duration_ms: Option<i64>,
    /// Input tokens used
    pub tokens_input: Option<i64>,
    /// Output tokens generated
    pub tokens_output: Option<i64>,
    /// Tokens read from cache
    pub tokens_cache_read: Option<i64>,
    /// Tokens written to cache
    pub tokens_cache_write: Option<i64>,
    /// Cost in USD
    pub cost_usd: Option<f64>,
    /// User prompt text (for UserPromptSubmit events)
    pub prompt: Option<String>,
}

/// Mode for auto-detecting frameworks when no names are specified.
#[derive(Debug, Clone, Copy)]
pub enum FrameworkResolutionMode {
    /// Find frameworks that are installed on the system (for activation).
    Installed,
    /// Find frameworks that have mi6 hooks currently active (for deactivation).
    Active {
        /// Check project-level config instead of user-level.
        local: bool,
        /// Check the `.local` variant (not committed to git).
        settings_local: bool,
    },
}

/// Resolve framework names to adapters with deduplication and validation.
///
/// When `names` is empty, auto-detection is performed based on the `mode`:
/// - [`FrameworkResolutionMode::Installed`]: Returns frameworks installed on the system.
/// - [`FrameworkResolutionMode::Active`]: Returns frameworks with active mi6 hooks.
///
/// When `names` contains framework names, each is validated and the list is deduplicated.
///
/// # Arguments
/// * `names` - Framework names to resolve (empty for auto-detection).
/// * `mode` - How to auto-detect frameworks when `names` is empty. Pass `None` to
///   require explicit framework names and error if `names` is empty.
///
/// # Returns
/// A deduplicated list of framework adapters, or an error if:
/// - A framework name is unknown
/// - Auto-detection found no frameworks
///
/// # Example
/// ```
/// use mi6_core::framework::{resolve_frameworks, FrameworkResolutionMode};
///
/// // Resolve specific frameworks
/// let adapters = resolve_frameworks(&["claude".to_string()], None).unwrap();
/// assert_eq!(adapters.len(), 1);
///
/// // Auto-detect installed frameworks
/// let installed = resolve_frameworks(&[], Some(FrameworkResolutionMode::Installed));
/// ```
pub fn resolve_frameworks(
    names: &[String],
    mode: Option<FrameworkResolutionMode>,
) -> Result<Vec<&'static dyn FrameworkAdapter>, FrameworkResolutionError> {
    if names.is_empty() {
        // Auto-detect based on mode
        let Some(mode) = mode else {
            return Err(FrameworkResolutionError::NoFrameworksFound(
                "no frameworks specified".to_string(),
            ));
        };

        let adapters = match mode {
            FrameworkResolutionMode::Installed => {
                let installed = installed_frameworks();
                if installed.is_empty() {
                    let all_names: Vec<&str> = all_adapters().iter().map(|a| a.name()).collect();
                    return Err(FrameworkResolutionError::NoFrameworksFound(format!(
                        "No supported AI coding frameworks detected.\n\
                         Supported frameworks: {}\n\
                         Install one first, or specify explicitly: mi6 enable claude",
                        all_names.join(", ")
                    )));
                }
                installed
            }
            FrameworkResolutionMode::Active {
                local,
                settings_local,
            } => {
                let active: Vec<_> = all_adapters()
                    .into_iter()
                    .filter(|a| a.has_mi6_hooks(local, settings_local))
                    .collect();
                if active.is_empty() {
                    return Err(FrameworkResolutionError::NoFrameworksFound(
                        "No frameworks have mi6 enabled".to_string(),
                    ));
                }
                active
            }
        };

        Ok(adapters)
    } else {
        // Resolve each specified framework, deduplicating by name
        let mut adapters: Vec<&'static dyn FrameworkAdapter> = Vec::new();
        let mut seen = HashSet::new();
        for name in names {
            let adapter = get_adapter(name)
                .ok_or_else(|| FrameworkResolutionError::UnknownFramework(name.clone()))?;
            if seen.insert(adapter.name()) {
                adapters.push(adapter);
            }
        }
        Ok(adapters)
    }
}

/// Get all available framework adapters.
pub fn all_adapters() -> Vec<&'static dyn FrameworkAdapter> {
    vec![
        &ClaudeAdapter,
        &CursorAdapter,
        &GeminiAdapter,
        &CodexAdapter,
        &OpenCodeAdapter,
        &PiAdapter,
    ]
}

/// Get a framework adapter by name.
pub fn get_adapter(name: &str) -> Option<&'static dyn FrameworkAdapter> {
    match name.to_lowercase().as_str() {
        "claude" => Some(&ClaudeAdapter),
        "cursor" => Some(&CursorAdapter),
        "gemini" => Some(&GeminiAdapter),
        "codex" => Some(&CodexAdapter),
        "opencode" => Some(&OpenCodeAdapter),
        "pi" => Some(&PiAdapter),
        _ => None,
    }
}

/// Detect the current framework from environment variables.
///
/// Checks environment variables set by each framework to determine
/// which one is currently invoking mi6. Returns the first matching
/// framework if multiple are detected.
pub fn detect_framework() -> Option<&'static dyn FrameworkAdapter> {
    all_adapters().into_iter().find(|adapter| adapter.detect())
}

/// Detect all frameworks that match current environment variables.
///
/// Useful for diagnosing ambiguous detection scenarios.
pub fn detect_all_frameworks() -> Vec<&'static dyn FrameworkAdapter> {
    all_adapters()
        .into_iter()
        .filter(|adapter| adapter.detect())
        .collect()
}

/// Get all frameworks that are installed on the system.
///
/// Uses the `is_installed()` method on each adapter to determine
/// if the framework's config directory exists or CLI tool is available.
pub fn installed_frameworks() -> Vec<&'static dyn FrameworkAdapter> {
    all_adapters()
        .into_iter()
        .filter(|adapter| adapter.is_installed())
        .collect()
}

/// Get the default framework (Claude Code).
pub fn default_adapter() -> &'static dyn FrameworkAdapter {
    &ClaudeAdapter
}

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

    #[test]
    fn test_get_adapter_claude() {
        assert!(get_adapter("claude").is_some());
        assert!(get_adapter("CLAUDE").is_some());
    }

    #[test]
    fn test_get_adapter_gemini() {
        assert!(get_adapter("gemini").is_some());
        assert!(get_adapter("GEMINI").is_some());
    }

    #[test]
    fn test_get_adapter_unknown() {
        assert!(get_adapter("unknown").is_none());
        assert!(get_adapter("").is_none());
    }

    #[test]
    fn test_get_adapter_codex() {
        assert!(get_adapter("codex").is_some());
        assert!(get_adapter("CODEX").is_some());
    }

    #[test]
    fn test_get_adapter_cursor() {
        assert!(get_adapter("cursor").is_some());
        assert!(get_adapter("CURSOR").is_some());
    }

    #[test]
    fn test_get_adapter_opencode() {
        assert!(get_adapter("opencode").is_some());
        assert!(get_adapter("OPENCODE").is_some());
    }

    #[test]
    fn test_get_adapter_pi() {
        assert!(get_adapter("pi").is_some());
        assert!(get_adapter("PI").is_some());
    }

    #[test]
    fn test_all_adapters() {
        let adapters = all_adapters();
        assert!(!adapters.is_empty());
        assert!(adapters.iter().any(|a| a.name() == "claude"));
        assert!(adapters.iter().any(|a| a.name() == "cursor"));
        assert!(adapters.iter().any(|a| a.name() == "gemini"));
        assert!(adapters.iter().any(|a| a.name() == "codex"));
        assert!(adapters.iter().any(|a| a.name() == "opencode"));
        assert!(adapters.iter().any(|a| a.name() == "pi"));
    }

    #[test]
    fn test_default_adapter() {
        let adapter = default_adapter();
        assert_eq!(adapter.name(), "claude");
    }

    #[test]
    fn test_resolve_frameworks_single() {
        let adapters = resolve_frameworks(&["claude".to_string()], None).unwrap();
        assert_eq!(adapters.len(), 1);
        assert_eq!(adapters[0].name(), "claude");
    }

    #[test]
    fn test_resolve_frameworks_multiple() {
        let adapters =
            resolve_frameworks(&["claude".to_string(), "gemini".to_string()], None).unwrap();
        assert_eq!(adapters.len(), 2);
        assert!(adapters.iter().any(|a| a.name() == "claude"));
        assert!(adapters.iter().any(|a| a.name() == "gemini"));
    }

    #[test]
    fn test_resolve_frameworks_deduplicates() {
        // Same framework specified twice should be deduplicated
        let adapters = resolve_frameworks(
            &[
                "claude".to_string(),
                "claude".to_string(),
                "CLAUDE".to_string(),
            ],
            None,
        )
        .unwrap();
        assert_eq!(adapters.len(), 1);
        assert_eq!(adapters[0].name(), "claude");
    }

    #[test]
    fn test_resolve_frameworks_unknown_framework() {
        let result = resolve_frameworks(&["unknown_framework".to_string()], None);
        match result {
            Err(FrameworkResolutionError::UnknownFramework(name)) => {
                assert_eq!(name, "unknown_framework");
            }
            _ => panic!("expected UnknownFramework error"),
        }
    }

    #[test]
    fn test_resolve_frameworks_empty_no_mode() {
        // Empty names with None mode should error
        let result = resolve_frameworks(&[], None);
        match result {
            Err(FrameworkResolutionError::NoFrameworksFound(_)) => {}
            _ => panic!("expected NoFrameworksFound error"),
        }
    }

    #[test]
    fn test_resolve_frameworks_case_insensitive() {
        // Framework names should be case-insensitive
        let adapters = resolve_frameworks(&["CLAUDE".to_string()], None).unwrap();
        assert_eq!(adapters.len(), 1);
        assert_eq!(adapters[0].name(), "claude");
    }

    #[test]
    fn test_resolve_frameworks_mixed_case_deduplication() {
        // Mixed case should deduplicate correctly
        let adapters = resolve_frameworks(
            &[
                "claude".to_string(),
                "CLAUDE".to_string(),
                "Claude".to_string(),
            ],
            None,
        )
        .unwrap();
        assert_eq!(adapters.len(), 1);
    }
}
