//! High-level enable API for programmatic hook installation.
//!
//! This module provides a simple, high-level API for enabling mi6 hooks
//! that can be used by other Rust programs (like mi6) without depending
//! on the CLI crate.
//!
//! # Example
//!
//! ```ignore
//! use mi6_core::{InitOptions, enable};
//!
//! // Enable with auto-detection
//! let result = enable(InitOptions::default())?;
//!
//! for framework in &result.frameworks {
//!     println!("Enabled {} at {}", framework.name, framework.settings_path.display());
//! }
//!
//! // Enable specific frameworks with OTel
//! let result = enable(
//!     InitOptions::for_frameworks(vec!["claude".into(), "gemini".into()])
//!         .otel(true)
//!         .otel_port(4318)
//! )?;
//! ```

use std::path::PathBuf;

use crate::config::Config;
use crate::framework::{
    ConfigFormat, FrameworkAdapter, FrameworkResolutionMode, InitOptions, all_adapters,
    generate_config, initialize_framework, resolve_frameworks,
};
use crate::model::error::{FrameworkResolutionError, InitError};

/// Result of enabling a single framework.
#[derive(Debug, Clone)]
pub struct FrameworkEnablement {
    /// The framework name (e.g., "claude", "gemini", "codex").
    pub name: String,
    /// The display name (e.g., "Claude Code", "Gemini CLI").
    pub display_name: String,
    /// Path where hooks were installed.
    pub settings_path: PathBuf,
    /// The hooks configuration that was installed.
    pub hooks_config: serde_json::Value,
    /// Shell commands that were executed during installation.
    pub commands_run: Vec<String>,
}

/// Result of a failed framework enable attempt.
#[derive(Debug, Clone)]
pub struct FrameworkFailure {
    /// The framework name (e.g., "claude", "gemini", "codex").
    pub name: String,
    /// Error message describing what went wrong.
    pub error: String,
}

/// Result of the enable process.
#[derive(Debug, Clone)]
pub struct EnableResult {
    /// Frameworks that were enabled.
    pub frameworks: Vec<FrameworkEnablement>,
    /// Frameworks that failed to enable.
    pub failures: Vec<FrameworkFailure>,
    /// Path to the database (if database initialization was requested).
    pub db_path: Option<PathBuf>,
    /// Whether the database should be initialized.
    pub should_init_db: bool,
}

/// Error during enable.
#[derive(Debug)]
pub enum EnableError {
    /// No frameworks detected or specified.
    NoFrameworks {
        /// List of all supported frameworks.
        supported: Vec<String>,
    },
    /// Unknown framework name.
    UnknownFramework(String),
    /// Configuration error.
    Config(String),
    /// Initialization error from the core library.
    Init(InitError),
}

impl std::fmt::Display for EnableError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::NoFrameworks { supported } => {
                write!(
                    f,
                    "no supported AI coding frameworks detected.\n\
                     Supported frameworks: {}\n\
                     Install one first, or specify explicitly.",
                    supported.join(", ")
                )
            }
            Self::UnknownFramework(name) => write!(f, "unknown framework: {name}"),
            Self::Config(msg) => write!(f, "configuration error: {msg}"),
            Self::Init(e) => write!(f, "{e}"),
        }
    }
}

impl std::error::Error for EnableError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            Self::Init(e) => Some(e),
            _ => None,
        }
    }
}

impl From<InitError> for EnableError {
    fn from(e: InitError) -> Self {
        Self::Init(e)
    }
}

impl From<FrameworkResolutionError> for EnableError {
    fn from(e: FrameworkResolutionError) -> Self {
        match e {
            FrameworkResolutionError::NoFrameworksFound(_) => Self::NoFrameworks {
                supported: all_adapters()
                    .iter()
                    .map(|a| a.name().to_string())
                    .collect(),
            },
            FrameworkResolutionError::UnknownFramework(name) => Self::UnknownFramework(name),
        }
    }
}

/// Result of previewing enable for a single framework.
#[derive(Debug, Clone)]
pub struct PreviewResult {
    /// The framework name (e.g., "claude", "gemini", "codex").
    pub name: String,
    /// The hooks configuration that would be installed.
    pub hooks_config: serde_json::Value,
    /// The config format for this framework (JSON or TOML).
    pub config_format: ConfigFormat,
}

/// Enable mi6 hooks for AI coding frameworks.
///
/// This is the main entry point for programmatic enabling. It:
/// 1. Resolves which frameworks to enable (auto-detect or explicit)
/// 2. Installs hooks for each framework
///
/// Note: Port conflict checking should be done by the caller using
/// `mi6_otel_server::is_server_running()` and `mi6_otel_server::find_available_port()`
/// before calling this function if OTel is enabled.
///
/// # Example
/// ```ignore
/// use mi6_core::{InitOptions, enable};
///
/// let result = enable(InitOptions::for_framework("claude").otel(true))?;
/// ```
pub fn enable(options: InitOptions) -> Result<EnableResult, EnableError> {
    let config = Config::load().map_err(|e| EnableError::Config(e.to_string()))?;

    let mut result = EnableResult {
        frameworks: Vec::new(),
        failures: Vec::new(),
        db_path: None,
        should_init_db: !options.hooks_only,
    };

    if result.should_init_db {
        result.db_path = Config::db_path().ok();
    }

    if options.db_only {
        return Ok(result);
    }

    let adapters = resolve_frameworks(
        &options.frameworks,
        Some(FrameworkResolutionMode::Installed),
    )?;

    // Try to enable each framework individually, collecting successes and failures
    for adapter in adapters {
        match enable_single_framework(&config, adapter, &options) {
            Ok(enablement) => {
                result.frameworks.push(enablement);
            }
            Err(e) => {
                result.failures.push(FrameworkFailure {
                    name: adapter.name().to_string(),
                    error: e.to_string(),
                });
            }
        }
    }

    // If all frameworks failed, return an error
    if result.frameworks.is_empty() && !result.failures.is_empty() {
        // Return the first error for backwards compatibility when all fail
        let first_failure = &result.failures[0];
        return Err(EnableError::Config(first_failure.error.clone()));
    }

    Ok(result)
}

/// Enable mi6 for a single framework adapter.
fn enable_single_framework(
    config: &Config,
    adapter: &dyn FrameworkAdapter,
    options: &InitOptions,
) -> Result<FrameworkEnablement, InitError> {
    let init_result = initialize_framework(config, adapter.name(), options)?;

    Ok(FrameworkEnablement {
        name: adapter.name().to_string(),
        display_name: adapter.display_name().to_string(),
        settings_path: init_result.settings_path,
        hooks_config: init_result.hooks_config,
        commands_run: init_result.commands_run,
    })
}

/// Generate hooks configuration without installing.
///
/// This is useful for previewing what would be installed (e.g., `--print` mode).
pub fn preview_enable(options: &InitOptions) -> Result<Vec<PreviewResult>, EnableError> {
    let config = Config::load().map_err(|e| EnableError::Config(e.to_string()))?;
    let adapters = resolve_frameworks(
        &options.frameworks,
        Some(FrameworkResolutionMode::Installed),
    )?;

    Ok(adapters
        .into_iter()
        .map(|adapter| PreviewResult {
            name: adapter.name().to_string(),
            hooks_config: generate_config(adapter, &config, options),
            config_format: adapter.config_format(),
        })
        .collect())
}

/// Result of disabling a single framework.
#[derive(Debug, Clone)]
pub struct FrameworkDisablement {
    /// The framework name (e.g., "claude", "gemini", "codex").
    pub name: String,
    /// The display name (e.g., "Claude Code", "Gemini CLI").
    pub display_name: String,
    /// Path where hooks were removed from.
    pub settings_path: PathBuf,
    /// Whether hooks were actually removed (false if no mi6 hooks were present).
    pub hooks_removed: bool,
    /// Shell commands that were executed during uninstallation.
    pub commands_run: Vec<String>,
}

/// Result of the disable process.
#[derive(Debug, Clone)]
pub struct DisableResult {
    /// Frameworks that were disabled.
    pub frameworks: Vec<FrameworkDisablement>,
}

/// Disable mi6 hooks for AI coding frameworks.
///
/// This removes mi6 hooks from the specified frameworks' configuration files.
/// For plugin-based frameworks (like Claude Code), this removes the plugin directory.
/// If no frameworks are specified, it will auto-detect installed frameworks
/// that have mi6 hooks enabled.
///
/// # Example
/// ```ignore
/// use mi6_core::{InitOptions, disable};
///
/// // Disable all frameworks with mi6 hooks
/// let result = disable(&[])?;
///
/// // Disable specific framework
/// let result = disable(&["claude"])?;
/// ```
pub fn disable(frameworks: &[&str]) -> Result<DisableResult, EnableError> {
    disable_with_options(frameworks, false, false)
}

/// Disable mi6 hooks with specific options.
///
/// # Arguments
/// * `frameworks` - Framework names to disable, or empty to auto-detect
/// * `local` - If true, disable from project-level config instead of user-level
/// * `settings_local` - If true, disable from the `.local` variant
pub fn disable_with_options(
    frameworks: &[&str],
    local: bool,
    settings_local: bool,
) -> Result<DisableResult, EnableError> {
    use crate::framework::get_adapter;

    let adapters = if frameworks.is_empty() {
        // Auto-detect frameworks with mi6 hooks
        all_adapters()
            .into_iter()
            .filter(|a| a.has_mi6_hooks(local, settings_local))
            .collect::<Vec<_>>()
    } else {
        // Use specified frameworks
        let mut adapters = Vec::new();
        for name in frameworks {
            let adapter = get_adapter(name)
                .ok_or_else(|| EnableError::UnknownFramework((*name).to_string()))?;
            adapters.push(adapter);
        }
        adapters
    };

    let mut result = DisableResult {
        frameworks: Vec::new(),
    };

    for adapter in adapters {
        let settings_path = match adapter.settings_path(local, settings_local) {
            Ok(path) => path,
            Err(_) => continue,
        };

        // Use the uninstall_hooks method which handles both config-based and plugin-based frameworks
        let uninstall_result = adapter
            .uninstall_hooks(local, settings_local)
            .unwrap_or_default();

        result.frameworks.push(FrameworkDisablement {
            name: adapter.name().to_string(),
            display_name: adapter.display_name().to_string(),
            settings_path,
            hooks_removed: uninstall_result.hooks_removed,
            commands_run: uninstall_result.commands_run,
        });
    }

    Ok(result)
}

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

    #[test]
    fn test_enable_error_display() {
        let err = EnableError::NoFrameworks {
            supported: vec!["claude".into(), "gemini".into()],
        };
        assert!(
            err.to_string()
                .contains("no supported AI coding frameworks")
        );

        let err = EnableError::UnknownFramework("foo".into());
        assert!(err.to_string().contains("unknown framework: foo"));
    }

    #[test]
    fn test_enable_error_from_framework_resolution_error() {
        let err: EnableError = FrameworkResolutionError::UnknownFramework("foo".to_string()).into();
        assert!(matches!(err, EnableError::UnknownFramework(name) if name == "foo"));

        let err: EnableError =
            FrameworkResolutionError::NoFrameworksFound("no frameworks".to_string()).into();
        assert!(matches!(err, EnableError::NoFrameworks { .. }));
    }

    #[test]
    fn test_framework_failure_struct() {
        let failure = FrameworkFailure {
            name: "codex".to_string(),
            error: "config.toml has invalid TOML syntax".to_string(),
        };
        assert_eq!(failure.name, "codex");
        assert!(failure.error.contains("invalid TOML"));
    }

    #[test]
    fn test_enable_result_has_failures_field() {
        let result = EnableResult {
            frameworks: vec![],
            failures: vec![FrameworkFailure {
                name: "codex".to_string(),
                error: "some error".to_string(),
            }],
            db_path: None,
            should_init_db: true,
        };
        assert!(result.frameworks.is_empty());
        assert_eq!(result.failures.len(), 1);
        assert_eq!(result.failures[0].name, "codex");
    }

    #[test]
    fn test_enable_result_with_mixed_success_failure() {
        let result = EnableResult {
            frameworks: vec![FrameworkEnablement {
                name: "claude".to_string(),
                display_name: "Claude Code".to_string(),
                settings_path: PathBuf::from("/home/user/.claude/settings.json"),
                hooks_config: serde_json::json!({}),
                commands_run: vec![],
            }],
            failures: vec![FrameworkFailure {
                name: "codex".to_string(),
                error: "config error".to_string(),
            }],
            db_path: None,
            should_init_db: true,
        };
        assert_eq!(result.frameworks.len(), 1);
        assert_eq!(result.frameworks[0].name, "claude");
        assert_eq!(result.failures.len(), 1);
        assert_eq!(result.failures[0].name, "codex");
    }
}
