//! Activate command - install fyi hooks for AI coding frameworks.

use std::path::PathBuf;

use anyhow::{Context, Result, bail};

use super::otel_server::{is_fyi_server, is_server_running};
use fyi_core::init::{
    InitOptions as CoreInitOptions, InstallSettingsOptions, generate_otel_env, get_settings_path,
    install_settings, json_to_toml_string,
};
use fyi_core::{
    Config, ConfigFormat, FrameworkAdapter, all_adapters, get_adapter, installed_frameworks,
};

/// Result of running activate - used by the binary to determine what storage to create
pub struct ActivateResult {
    pub should_init_db: bool,
    pub db_path: Option<PathBuf>,
}

/// Options for the activate command.
///
/// This struct wraps [`fyi_core::init::InitOptions`] and adds CLI-specific
/// options like `print`, `db_only`, and `hooks_only`.
pub struct ActivateOptions {
    /// Core initialization options (frameworks, local, settings_local, otel settings).
    pub core: CoreInitOptions,
    /// Print hooks JSON to stdout instead of writing to file.
    pub print: bool,
    /// Only initialize database, skip hook installation.
    pub db_only: bool,
    /// Only install hooks, skip database initialization.
    pub hooks_only: bool,
}

impl ActivateOptions {
    /// Create new ActivateOptions with auto-detection of frameworks.
    pub fn new() -> Self {
        Self {
            core: CoreInitOptions::new(),
            print: false,
            db_only: false,
            hooks_only: false,
        }
    }

    /// Create ActivateOptions for specific frameworks.
    pub fn for_frameworks(frameworks: Vec<String>) -> Self {
        Self {
            core: CoreInitOptions::for_frameworks(frameworks),
            print: false,
            db_only: false,
            hooks_only: false,
        }
    }

    /// Set install location to project config instead of global.
    pub fn local(mut self, local: bool) -> Self {
        self.core = self.core.local(local);
        self
    }

    /// Set install location to project local config (not committed to git).
    pub fn settings_local(mut self, settings_local: bool) -> Self {
        self.core = self.core.settings_local(settings_local);
        self
    }

    /// Enable OpenTelemetry configuration for token tracking.
    pub fn otel(mut self, otel: bool) -> Self {
        self.core = self.core.otel(otel);
        self
    }

    /// Set the port for OTel server.
    pub fn otel_port(mut self, port: u16) -> Self {
        self.core = self.core.otel_port(port);
        self
    }

    /// Remove OTel configuration (disables token tracking).
    pub fn no_otel(mut self, no_otel: bool) -> Self {
        self.core = self.core.remove_otel(no_otel);
        self
    }

    /// Print hooks JSON to stdout instead of writing to file.
    pub fn print(mut self, print: bool) -> Self {
        self.print = print;
        self
    }

    /// Only initialize database, skip hook installation.
    pub fn db_only(mut self, db_only: bool) -> Self {
        self.db_only = db_only;
        self
    }

    /// Only install hooks, skip database initialization.
    pub fn hooks_only(mut self, hooks_only: bool) -> Self {
        self.hooks_only = hooks_only;
        self
    }
}

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

/// Find an available port starting from the given port
fn find_available_port(start_port: u16) -> u16 {
    for port in start_port..start_port + 100 {
        if !is_server_running(port) {
            return port;
        }
        // If it's our server, that's fine too
        if is_fyi_server(port) {
            return port;
        }
    }
    // Fallback to original port
    start_port
}

/// Resolve which frameworks to activate.
/// Returns a list of framework adapters to activate (deduplicated).
fn resolve_frameworks(frameworks: &[String]) -> Result<Vec<&'static dyn FrameworkAdapter>> {
    if frameworks.is_empty() {
        // Auto-detect installed frameworks
        let installed = installed_frameworks();
        if installed.is_empty() {
            let all_names: Vec<&str> = all_adapters().iter().map(|a| a.name()).collect();
            bail!(
                "No supported AI coding frameworks detected.\n\
                 Supported frameworks: {}\n\
                 Install one first, or specify explicitly: fyi activate claude",
                all_names.join(", ")
            );
        }
        Ok(installed)
    } else {
        // Resolve each specified framework, deduplicating by name
        let mut adapters: Vec<&'static dyn FrameworkAdapter> = Vec::new();
        let mut seen = std::collections::HashSet::new();
        for name in frameworks {
            let adapter =
                get_adapter(name).ok_or_else(|| anyhow::anyhow!("unknown framework: {}", name))?;
            if seen.insert(adapter.name()) {
                adapters.push(adapter);
            }
        }
        Ok(adapters)
    }
}

/// Run the activate command.
/// Returns ActivateResult so the binary can create the storage if needed.
pub fn run_activate(opts: ActivateOptions) -> Result<ActivateResult> {
    let config = Config::load().context("failed to load config")?;

    // Get frameworks from core options
    let frameworks = opts.core.get_frameworks();

    // Resolve which frameworks to activate
    let adapters = resolve_frameworks(&frameworks)?;

    // Show which frameworks were detected if auto-detecting
    if frameworks.is_empty() && !opts.print {
        let names: Vec<&str> = adapters.iter().map(|a| a.name()).collect();
        eprintln!("Detected frameworks: {}", names.join(", "));
    }

    let mut result = ActivateResult {
        should_init_db: !opts.hooks_only,
        db_path: None,
    };

    if !opts.hooks_only {
        result.db_path = Some(
            config
                .db_path()
                .context("failed to determine database path")?,
        );
    }

    // Check for port collision if otel is enabled
    if opts.core.otel
        && is_server_running(opts.core.otel_port)
        && !is_fyi_server(opts.core.otel_port)
    {
        let available_port = find_available_port(opts.core.otel_port + 1);
        bail!(
            "port {} is in use by another service\n\n\
             Try using a different port:\n  \
             fyi activate --otel --otel-port {}",
            opts.core.otel_port,
            available_port
        );
    }

    // Install hooks unless db_only
    if !opts.db_only {
        for adapter in adapters {
            activate_framework(adapter, &opts, &config)?;
        }
    }

    Ok(result)
}

/// Activate fyi for a single framework
fn activate_framework(
    adapter: &dyn FrameworkAdapter,
    opts: &ActivateOptions,
    config: &Config,
) -> Result<()> {
    let enabled_events = config.hooks.enabled_hooks();
    let hooks_json =
        adapter.generate_hooks_config(&enabled_events, "fyi", opts.core.otel, opts.core.otel_port);

    let otel_env = if opts.core.otel {
        Some(generate_otel_env(opts.core.otel_port))
    } else {
        None
    };

    if opts.print {
        let mut output = hooks_json;
        if let Some(env) = otel_env {
            output["env"] = env;
        }
        // Print in the appropriate format for this framework
        match adapter.config_format() {
            ConfigFormat::Json => {
                println!(
                    "{}",
                    serde_json::to_string_pretty(&output).context("failed to serialize hooks")?
                );
            }
            ConfigFormat::Toml => {
                println!(
                    "{}",
                    json_to_toml_string(&output).context("failed to serialize hooks")?
                );
            }
        }
    } else {
        let settings_path = get_settings_path(adapter, opts.core.local, opts.core.settings_local)?;

        eprint!("Activating fyi for {}... ", adapter.name());

        install_settings(
            adapter,
            &settings_path,
            &hooks_json,
            &InstallSettingsOptions {
                otel_env,
                remove_otel: opts.core.remove_otel,
            },
        )?;

        eprintln!("done");
        eprintln!("  Installed hooks to: {}", settings_path.display());

        if opts.core.settings_local {
            eprintln!(
                "  Note: Add '{}' to your project's .gitignore",
                settings_path.display()
            );
        }
        if opts.core.otel {
            eprintln!(
                "  OpenTelemetry configured on port {} for automatic token tracking.",
                opts.core.otel_port
            );
        }
    }

    Ok(())
}
