//! CLI command for `ralph doctor`.

use anyhow::Result;
use clap::Parser;
use ralph_adapters::{CliBackend, DEFAULT_PRIORITY};
use ralph_core::{CheckResult, CheckStatus, ConfigError, HatBackend, PreflightReport, RalphConfig};
use std::collections::HashSet;
use std::env;
use std::ffi::OsString;
use std::path::Path;
use std::process::Command;

use crate::{ConfigSource, HatsSource};

/// Run first-run diagnostics and environment validation.
#[derive(Parser, Debug)]
pub struct DoctorArgs {}

pub async fn execute(
    config_sources: &[ConfigSource],
    hats_source: Option<&HatsSource>,
    _args: DoctorArgs,
    use_colors: bool,
) -> Result<()> {
    let source_label = crate::preflight::config_source_label(config_sources, hats_source);
    let config = crate::preflight::load_config_for_preflight(config_sources, hats_source).await?;

    let runner = ralph_core::PreflightRunner::default_checks();
    let preflight_report = runner.run_all(&config).await;

    let mut config_check = None;
    let mut other_checks = Vec::new();
    for check in preflight_report.checks {
        match check.name.as_str() {
            "config" => config_check = Some(check),
            "backend" => {}
            _ => other_checks.push(check),
        }
    }

    let mut checks = Vec::new();
    if let Some(check) = config_check {
        checks.push(check);
    }

    checks.push(hat_collection_check(&config));

    let backend_checks = backend_checks(&config, command_version_ok, command_exists);
    checks.extend(backend_checks);

    let auth_backends = auth_backend_names(&config);
    checks.push(auth_hint_check(&auth_backends, |key| env::var(key).ok()));

    checks.extend(other_checks);

    let report = report_from_checks(checks);
    print_human_report(&report, &source_label, use_colors);

    if report.failures > 0 {
        std::process::exit(1);
    }

    Ok(())
}

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum CommandCheckMode {
    Version,
    PathOnly,
}

fn backend_checks<F, G>(
    _config: &RalphConfig,
    _command_version_ok: F,
    _command_exists: G,
) -> Vec<CheckResult>
where
    F: Fn(&str) -> bool,
    G: Fn(&str) -> bool,
{
    let config = _config;
    let command_version_ok = _command_version_ok;
    let command_exists = _command_exists;

    let mut checks = Vec::new();
    let mut seen: HashSet<String> = HashSet::new();

    match config.cli.backend.trim() {
        "auto" => {
            for backend in DEFAULT_PRIORITY {
                let command = command_for_backend(backend);
                push_backend_check(
                    &mut checks,
                    &mut seen,
                    backend,
                    &command,
                    false,
                    CommandCheckMode::Version,
                    &command_version_ok,
                    &command_exists,
                    None,
                );
            }

            let any_available = checks.iter().any(|check| check.status == CheckStatus::Pass);

            let summary = if any_available {
                CheckResult::pass("backend:auto", "Auto backend available")
            } else {
                CheckResult::fail(
                    "backend:auto",
                    "No supported backend found",
                    format!("Checked: {}", DEFAULT_PRIORITY.join(", ")),
                )
            };
            checks.push(summary);
        }
        "custom" => {
            let command = config.cli.command.clone().unwrap_or_default();
            if command.trim().is_empty() {
                checks.push(CheckResult::fail(
                    "backend:custom",
                    "Custom backend command missing",
                    "Set cli.command in ralph.yml",
                ));
            } else {
                let backend = canonical_backend_name("custom", Some(&command));
                push_backend_check(
                    &mut checks,
                    &mut seen,
                    &backend,
                    &command,
                    true,
                    CommandCheckMode::PathOnly,
                    &command_version_ok,
                    &command_exists,
                    None,
                );
            }
        }
        backend => {
            let backend = backend.trim().to_lowercase();
            match command_for_named_backend(&backend, config.cli.command.as_deref()) {
                Ok(command) => {
                    push_backend_check(
                        &mut checks,
                        &mut seen,
                        &backend,
                        &command,
                        true,
                        CommandCheckMode::Version,
                        &command_version_ok,
                        &command_exists,
                        None,
                    );
                }
                Err(err) => {
                    checks.push(CheckResult::fail(
                        &format!("backend:{backend}"),
                        "Unknown backend",
                        err,
                    ));
                }
            }
        }
    }

    for (hat_id, hat_config) in &config.hats {
        let Some(hat_backend) = &hat_config.backend else {
            continue;
        };

        let check_mode = match hat_backend {
            HatBackend::Custom { .. } => CommandCheckMode::PathOnly,
            _ => CommandCheckMode::Version,
        };

        match CliBackend::from_hat_backend(hat_backend) {
            Ok(cli_backend) => {
                let backend_name = canonical_backend_name(
                    &hat_backend.to_cli_backend(),
                    Some(cli_backend.command.as_str()),
                );
                push_backend_check(
                    &mut checks,
                    &mut seen,
                    &backend_name,
                    &cli_backend.command,
                    true,
                    check_mode,
                    &command_version_ok,
                    &command_exists,
                    None,
                );
            }
            Err(_) => {
                checks.push(CheckResult::fail(
                    &format!("backend:hat:{hat_id}"),
                    "Unknown hat backend",
                    format!("Hat '{hat_id}' specifies an unknown backend"),
                ));
            }
        }
    }

    checks
}

fn push_backend_check<F, G>(
    checks: &mut Vec<CheckResult>,
    seen: &mut HashSet<String>,
    backend: &str,
    command: &str,
    required: bool,
    check_mode: CommandCheckMode,
    command_version_ok: &F,
    command_exists: &G,
    detail: Option<String>,
) where
    F: Fn(&str) -> bool,
    G: Fn(&str) -> bool,
{
    let name = backend_check_name(backend, command);
    if !seen.insert(name.clone()) {
        return;
    }

    let available = match check_mode {
        CommandCheckMode::Version => command_version_ok(command),
        CommandCheckMode::PathOnly => command_exists(command),
    };

    let status = if available {
        CheckStatus::Pass
    } else if required {
        CheckStatus::Fail
    } else {
        CheckStatus::Warn
    };

    let label = match status {
        CheckStatus::Pass => format!("{backend} CLI available ({command})"),
        CheckStatus::Warn => format!("{backend} CLI missing (optional for auto)"),
        CheckStatus::Fail => format!("{backend} CLI missing"),
    };

    let message = if available {
        None
    } else if let Some(detail) = detail {
        Some(detail)
    } else {
        Some(format!("Command not found or not executable: {command}"))
    };

    checks.push(CheckResult {
        name,
        label,
        status,
        message,
    });
}

fn auth_hint_check<F>(_backends: &[String], _env_lookup: F) -> CheckResult
where
    F: Fn(&str) -> Option<String>,
{
    let env_lookup = _env_lookup;
    let mut missing = Vec::new();

    let mut backends: Vec<String> = _backends
        .iter()
        .map(|backend| backend.trim().to_lowercase())
        .collect();
    backends.sort();
    backends.dedup();

    for backend in backends {
        let Some(envs) = auth_env_vars(&backend) else {
            missing.push(format!("{backend}: authenticate via the CLI"));
            continue;
        };

        if envs.iter().any(|key| env_lookup(key).is_some()) {
            continue;
        }

        missing.push(format!("{backend}: set {}", envs.join(" or ")));
    }

    if missing.is_empty() {
        CheckResult::pass("auth", "Auth hints satisfied")
    } else {
        CheckResult::warn(
            "auth",
            "Authentication not detected for some backends",
            missing.join("\n"),
        )
    }
}

fn hat_collection_check(_config: &RalphConfig) -> CheckResult {
    let config = _config;

    match config.validate() {
        Ok(_) => {
            if config.hats.is_empty() {
                CheckResult::pass("hats", "No custom hats configured (solo mode)")
            } else {
                CheckResult::pass(
                    "hats",
                    format!("Hat collection parsed ({} hat(s))", config.hats.len()),
                )
            }
        }
        Err(err) => match err {
            ConfigError::AmbiguousRouting { .. }
            | ConfigError::ReservedTrigger { .. }
            | ConfigError::MissingDescription { .. } => {
                CheckResult::fail("hats", "Hat collection invalid", err.to_string())
            }
            _ => CheckResult::pass("hats", "Hat collection parsed"),
        },
    }
}

fn auth_backend_names(config: &RalphConfig) -> Vec<String> {
    let mut names = HashSet::new();

    match config.cli.backend.trim() {
        "auto" => {
            for backend in DEFAULT_PRIORITY {
                names.insert((*backend).to_string());
            }
        }
        "custom" => {
            if let Some(command) = config.cli.command.as_deref() {
                names.insert(canonical_backend_name("custom", Some(command)));
            } else {
                names.insert("custom".to_string());
            }
        }
        backend => {
            names.insert(backend.to_lowercase());
        }
    }

    for hat in config.hats.values() {
        let Some(backend) = &hat.backend else {
            continue;
        };

        let name = match backend {
            HatBackend::Named(name) => name.clone(),
            HatBackend::NamedWithArgs { backend_type, .. } => backend_type.clone(),
            HatBackend::KiroAgent { backend_type, .. } => backend_type.clone(),
            HatBackend::Custom { command, .. } => canonical_backend_name("custom", Some(command)),
        };

        names.insert(name.to_lowercase());
    }

    names.into_iter().collect()
}

fn auth_env_vars(backend: &str) -> Option<Vec<&'static str>> {
    match backend {
        "claude" => Some(vec!["ANTHROPIC_API_KEY"]),
        "gemini" => Some(vec!["GEMINI_API_KEY"]),
        "codex" => Some(vec!["OPENAI_API_KEY", "CODEX_API_KEY"]),
        "kiro" => Some(vec!["KIRO_API_KEY"]),
        "kiro-acp" => Some(vec!["KIRO_API_KEY"]),
        "opencode" => Some(vec![
            "OPENCODE_API_KEY",
            "ANTHROPIC_API_KEY",
            "OPENAI_API_KEY",
        ]),
        "pi" => Some(vec![
            "ANTHROPIC_API_KEY",
            "OPENAI_API_KEY",
            "GEMINI_API_KEY",
        ]),
        "roo" => Some(vec![
            "ANTHROPIC_API_KEY",
            "OPENAI_API_KEY",
            "GEMINI_API_KEY",
        ]),
        _ => None,
    }
}

fn command_for_backend(backend: &str) -> String {
    CliBackend::from_name(backend)
        .map(|backend| backend.command)
        .unwrap_or_else(|_| backend.to_string())
}

fn command_for_named_backend(
    backend: &str,
    command_override: Option<&str>,
) -> Result<String, String> {
    let backend = backend.trim().to_lowercase();
    if let Some(command) = command_override
        && !command.trim().is_empty()
    {
        return Ok(command.to_string());
    }

    CliBackend::from_name(&backend)
        .map(|backend| backend.command)
        .map_err(|_| format!("Unknown backend: {backend}"))
}

fn canonical_backend_name(backend: &str, command: Option<&str>) -> String {
    if backend != "custom" {
        return backend.to_lowercase();
    }

    let Some(command) = command else {
        return "custom".to_string();
    };

    let basename = Path::new(command)
        .file_name()
        .and_then(|name| name.to_str())
        .unwrap_or(command);

    let mut normalized = basename.to_string();
    let normalized_lower = normalized.to_lowercase();
    for ext in [".exe", ".cmd", ".bat", ".com"] {
        if normalized_lower.ends_with(ext) {
            let new_len = normalized.len().saturating_sub(ext.len());
            normalized.truncate(new_len);
            break;
        }
    }

    let normalized_lower = normalized.to_lowercase();
    match normalized_lower.as_str() {
        "kiro-cli" => "kiro".to_string(),
        "claude" => "claude".to_string(),
        "gemini" => "gemini".to_string(),
        "codex" => "codex".to_string(),
        "forge" => "forge".to_string(),
        "amp" => "amp".to_string(),
        "copilot" => "copilot".to_string(),
        "opencode" => "opencode".to_string(),
        "pi" => "pi".to_string(),
        "roo" => "roo".to_string(),
        _ => normalized,
    }
}

fn backend_check_name(backend: &str, command: &str) -> String {
    if backend.eq_ignore_ascii_case(command) {
        format!("backend:{backend}")
    } else {
        format!("backend:{backend}@{command}")
    }
}

fn command_version_ok(command: &str) -> bool {
    Command::new(command)
        .arg("--version")
        .output()
        .map(|output| output.status.success())
        .unwrap_or(false)
}

fn command_exists(command: &str) -> bool {
    let path = Path::new(command);
    if path.components().count() > 1 {
        return path.is_file();
    }

    let Some(path_var) = env::var_os("PATH") else {
        return false;
    };
    let extensions = executable_extensions();

    for dir in env::split_paths(&path_var) {
        for ext in &extensions {
            let candidate = if ext.is_empty() {
                dir.join(command)
            } else {
                dir.join(format!("{}{}", command, ext.to_string_lossy()))
            };

            if candidate.is_file() {
                return true;
            }
        }
    }

    false
}

fn executable_extensions() -> Vec<OsString> {
    if cfg!(windows) {
        let exts = env::var("PATHEXT").unwrap_or_else(|_| ".COM;.EXE;.BAT;.CMD".to_string());
        exts.split(';')
            .filter(|ext| !ext.trim().is_empty())
            .map(|ext| OsString::from(ext.trim().to_string()))
            .collect()
    } else {
        vec![OsString::new()]
    }
}

fn report_from_checks(checks: Vec<CheckResult>) -> PreflightReport {
    let warnings = checks
        .iter()
        .filter(|check| check.status == CheckStatus::Warn)
        .count();
    let failures = checks
        .iter()
        .filter(|check| check.status == CheckStatus::Fail)
        .count();

    PreflightReport {
        passed: failures == 0,
        warnings,
        failures,
        checks,
    }
}

fn print_human_report(report: &PreflightReport, source: &str, use_colors: bool) {
    use crate::display::colors;

    println!("Doctor checks for {}", source);
    println!();

    let name_width = report
        .checks
        .iter()
        .map(|check| check.name.len())
        .max()
        .unwrap_or(4)
        .max(4);

    for check in &report.checks {
        print_check_line(check, name_width, use_colors);
    }

    println!();

    let result = if report.passed { "PASS" } else { "FAIL" };
    let mut details = Vec::new();
    if report.failures > 0 {
        details.push(format!("{} failure(s)", report.failures));
    }
    if report.warnings > 0 {
        details.push(format!("{} warning(s)", report.warnings));
    }

    let detail_text = if details.is_empty() {
        String::new()
    } else {
        format!(" ({})", details.join(", "))
    };

    if use_colors {
        let color = if report.passed {
            colors::GREEN
        } else {
            colors::RED
        };
        println!(
            "Result: {color}{result}{reset}{detail}",
            reset = colors::RESET,
            detail = detail_text
        );
    } else {
        println!("Result: {result}{detail}", detail = detail_text);
    }
}

fn print_check_line(check: &CheckResult, name_width: usize, use_colors: bool) {
    use crate::display::colors;

    let (status_text, color) = match check.status {
        CheckStatus::Pass => ("OK", colors::GREEN),
        CheckStatus::Warn => ("WARN", colors::YELLOW),
        CheckStatus::Fail => ("FAIL", colors::RED),
    };

    let status_padded = format!("{status_text:<4}");
    let status_display = if use_colors {
        format!(
            "{color}{status}{reset}",
            status = status_padded,
            reset = colors::RESET
        )
    } else {
        status_padded
    };

    println!(
        "  {status} {name:<width$} {label}",
        status = status_display,
        name = check.name,
        width = name_width,
        label = check.label
    );

    if let Some(message) = &check.message {
        for line in message.lines() {
            println!("      {line}");
        }
    }
}

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

    fn base_hat(name: &str, backend: Option<HatBackend>) -> HatConfig {
        HatConfig {
            name: name.to_string(),
            description: Some("Test hat".to_string()),
            triggers: vec!["work.start".to_string()],
            publishes: vec![],
            instructions: String::new(),
            extra_instructions: vec![],
            backend_args: None,
            backend,
            default_publishes: None,
            max_activations: None,
            scratchpad: None,
            disallowed_tools: vec![],
            timeout: None,
            concurrency: 1,
            aggregate: None,
        }
    }

    #[test]
    fn backend_checks_include_cli_and_hat_backends() {
        let mut config = RalphConfig::default();
        config.cli.backend = "claude".to_string();
        config.hats.insert(
            "reviewer".to_string(),
            base_hat("Reviewer", Some(HatBackend::Named("gemini".to_string()))),
        );
        let checks = backend_checks(&config, |cmd| cmd == "claude", |_| false);
        let names: HashSet<_> = checks.iter().map(|check| check.name.as_str()).collect();

        assert!(names.contains("backend:claude"));
        assert!(names.contains("backend:gemini"));
    }

    #[test]
    fn backend_checks_map_custom_command_to_known_backend() {
        let mut config = RalphConfig::default();
        config.cli.backend = "custom".to_string();
        config.cli.command = Some("opencode".to_string());
        let checks = backend_checks(&config, |_| false, |cmd| cmd == "opencode");
        let names: Vec<_> = checks.iter().map(|check| check.name.as_str()).collect();

        assert!(names.contains(&"backend:opencode"));
    }

    #[test]
    fn backend_checks_fail_required_missing() {
        let mut config = RalphConfig::default();
        config.cli.backend = "claude".to_string();

        let checks = backend_checks(&config, |_| false, |_| false);
        let claude = checks
            .iter()
            .find(|check| check.name == "backend:claude")
            .expect("expected claude backend check");

        assert_eq!(claude.status, CheckStatus::Fail);
    }

    #[test]
    fn auth_hint_warns_when_env_missing() {
        let backends = vec!["codex".to_string(), "gemini".to_string()];
        let check = auth_hint_check(&backends, |key| match key {
            "OPENAI_API_KEY" => Some("present".to_string()),
            _ => None,
        });

        assert_eq!(check.status, CheckStatus::Warn);
        assert!(check.message.as_deref().unwrap_or("").contains("gemini"));
    }

    #[test]
    fn auth_hint_passes_when_all_env_present() {
        let backends = vec!["codex".to_string(), "gemini".to_string()];
        let check = auth_hint_check(&backends, |key| match key {
            "OPENAI_API_KEY" => Some("present".to_string()),
            "GEMINI_API_KEY" => Some("present".to_string()),
            _ => None,
        });

        assert_eq!(check.status, CheckStatus::Pass);
    }

    #[test]
    fn canonical_backend_name_strips_exe_extension() {
        assert_eq!(
            canonical_backend_name("custom", Some("claude.exe")),
            "claude"
        );
    }

    #[test]
    fn canonical_backend_name_strips_extension_for_unknown_command() {
        assert_eq!(
            canonical_backend_name("custom", Some("my-cli.exe")),
            "my-cli"
        );
    }
}
