use anyhow::Result;

use vtcode_core::config::ToolPolicy;
use vtcode_core::config::api_keys::{ApiKeySources, get_api_key};
use vtcode_core::config::loader::{ConfigManager, VTCodeConfig};
use vtcode_core::config::types::AgentConfig as CoreAgentConfig;
use vtcode_core::tools::ripgrep_binary::RIPGREP_INSTALL_COMMAND;
use vtcode_core::utils::ansi::{AnsiRenderer, MessageStyle};

use super::async_mcp_manager::{AsyncMcpManager, McpInitStatus};
use super::workspace_links::LinkedDirectory;

#[derive(Clone, Copy, Debug, Default)]
pub(crate) struct DoctorOptions {
    pub quick: bool,
}

#[derive(Default)]
struct DoctorSummary {
    passed: usize,
    warnings: usize,
    failures: usize,
}

impl DoctorSummary {
    fn record(&mut self, outcome: &DoctorCheckOutcome) {
        match outcome {
            DoctorCheckOutcome::Pass(_) => self.passed += 1,
            DoctorCheckOutcome::Warn(_) => self.warnings += 1,
            DoctorCheckOutcome::Fail(_) => self.failures += 1,
        }
    }
}

enum DoctorCheckOutcome {
    Pass(String),
    Warn(String),
    Fail(String),
}

pub(crate) async fn run_doctor_diagnostics(
    renderer: &mut AnsiRenderer,
    config: &CoreAgentConfig,
    vt_cfg: Option<&VTCodeConfig>,
    provider_name: &str,
    async_mcp_manager: Option<&AsyncMcpManager>,
    linked_directories: &[LinkedDirectory],
    loaded_skills: Option<
        &std::sync::Arc<
            tokio::sync::RwLock<hashbrown::HashMap<String, vtcode_core::skills::types::Skill>>,
        >,
    >,
    options: DoctorOptions,
) -> Result<()> {
    let mut summary = DoctorSummary::default();

    renderer.line(MessageStyle::Info, "")?;
    renderer.line(
        MessageStyle::Status,
        "═══════════════════════════════════════════════════════════════",
    )?;
    renderer.line(
        MessageStyle::Status,
        &format!("VT Code Doctor v{}", env!("CARGO_PKG_VERSION")),
    )?;
    renderer.line(
        MessageStyle::Status,
        "═══════════════════════════════════════════════════════════════",
    )?;
    if options.quick {
        renderer.line(
            MessageStyle::Info,
            "Mode: quick (skips dependency, external service, and session context checks)",
        )?;
    }
    renderer.line(MessageStyle::Info, "")?;

    renderer.line(MessageStyle::Status, "[Core Environment]")?;

    let workspace_result = if config.workspace.is_dir() {
        DoctorCheckOutcome::Pass(format!("{}", config.workspace.display()))
    } else {
        DoctorCheckOutcome::Fail("Workspace directory is missing or inaccessible".to_string())
    };
    render_doctor_check(renderer, &mut summary, "Workspace", workspace_result)?;

    let cli_version = format!("VT Code {}", env!("CARGO_PKG_VERSION"));
    render_doctor_check(
        renderer,
        &mut summary,
        "CLI Version",
        DoctorCheckOutcome::Pass(cli_version),
    )?;

    renderer.line(MessageStyle::Info, "")?;
    renderer.line(MessageStyle::Status, "[Configuration]")?;

    let config_result = match ConfigManager::load_from_workspace(&config.workspace) {
        Ok(manager) => {
            if let Some(path) = manager.config_path() {
                DoctorCheckOutcome::Pass(format!("Loaded from {}", path.display()))
            } else {
                DoctorCheckOutcome::Warn(
                    "Using runtime defaults (no vtcode.toml found)".to_string(),
                )
            }
        }
        Err(err) => DoctorCheckOutcome::Fail(err.to_string()),
    };
    render_doctor_check(renderer, &mut summary, "Config File", config_result)?;

    if let Some(cfg) = vt_cfg {
        render_doctor_check(
            renderer,
            &mut summary,
            "Theme",
            DoctorCheckOutcome::Pass(cfg.agent.theme.clone()),
        )?;

        let model_info = if cfg.agent.small_model.enabled {
            let lightweight_model = if cfg.agent.small_model.model.is_empty() {
                "auto-select".to_string()
            } else {
                cfg.agent.small_model.model.clone()
            };
            format!(
                "{} (+ lightweight model: {})",
                cfg.agent.default_model, lightweight_model
            )
        } else {
            cfg.agent.default_model.clone()
        };
        render_doctor_check(
            renderer,
            &mut summary,
            "Model",
            DoctorCheckOutcome::Pass(model_info),
        )?;

        render_doctor_check(
            renderer,
            &mut summary,
            "Max Turns",
            DoctorCheckOutcome::Pass(format!("{}", cfg.agent.max_conversation_turns)),
        )?;

        render_doctor_check(
            renderer,
            &mut summary,
            "Context Tokens",
            DoctorCheckOutcome::Pass(format!("{}", cfg.context.max_context_tokens)),
        )?;

        render_doctor_check(
            renderer,
            &mut summary,
            "Token Budget",
            DoctorCheckOutcome::Pass("Disabled".to_string()),
        )?;

        let ledger_status = if cfg.context.ledger.enabled {
            format!("Enabled (max {} entries)", cfg.context.ledger.max_entries)
        } else {
            "Disabled".to_string()
        };
        render_doctor_check(
            renderer,
            &mut summary,
            "Decision Ledger",
            DoctorCheckOutcome::Pass(ledger_status),
        )?;

        render_doctor_check(
            renderer,
            &mut summary,
            "Max Tool Loops",
            DoctorCheckOutcome::Pass(format!("{}", cfg.tools.max_tool_loops)),
        )?;

        render_doctor_check(
            renderer,
            &mut summary,
            "HITL Enabled",
            DoctorCheckOutcome::Pass(
                (if cfg.security.human_in_the_loop {
                    "Yes"
                } else {
                    "No"
                })
                .to_string(),
            ),
        )?;

        let policy = match cfg.tools.default_policy {
            ToolPolicy::Allow => "Allow all (no confirmation)",
            ToolPolicy::Deny => "Deny all (security)",
            ToolPolicy::Prompt => "Prompt on tool use",
        };
        render_doctor_check(
            renderer,
            &mut summary,
            "Tool Policy",
            DoctorCheckOutcome::Pass(policy.to_string()),
        )?;

        render_doctor_check(
            renderer,
            &mut summary,
            "PTY Enabled",
            DoctorCheckOutcome::Pass((if cfg.pty.enabled { "Yes" } else { "No" }).to_string()),
        )?;
    }

    renderer.line(MessageStyle::Info, "")?;
    renderer.line(MessageStyle::Status, "[API & Providers]")?;

    let configured_provider = canonical_provider_name(&config.provider);
    let runtime_provider_source = if provider_name.trim().is_empty() {
        config.provider.as_str()
    } else {
        provider_name
    };
    let runtime_provider = canonical_provider_name(runtime_provider_source);

    if runtime_provider == "openresponses" {
        render_doctor_check(
            renderer,
            &mut summary,
            "Provider",
            DoctorCheckOutcome::Pass(format!(
                "Configured '{}', runtime adapter '{}'",
                configured_provider, runtime_provider
            )),
        )?;
    } else if configured_provider == runtime_provider {
        render_doctor_check(
            renderer,
            &mut summary,
            "Provider",
            DoctorCheckOutcome::Pass(format!("Configured and active: {}", runtime_provider)),
        )?;
    } else {
        render_doctor_check(
            renderer,
            &mut summary,
            "Provider",
            DoctorCheckOutcome::Warn(format!(
                "Configured '{}', active '{}'",
                configured_provider, runtime_provider
            )),
        )?;
    }

    let provider_for_api_check = if runtime_provider == "openresponses" {
        configured_provider.as_str()
    } else {
        runtime_provider.as_str()
    };

    let api_key_result = if provider_for_api_check.eq_ignore_ascii_case("openai")
        && config.openai_chatgpt_auth.is_some()
    {
        DoctorCheckOutcome::Pass("ChatGPT subscription active for OpenAI".to_string())
    } else if provider_requires_api_key(provider_for_api_check) {
        match get_api_key(provider_for_api_check, &ApiKeySources::default()) {
            Ok(_) => DoctorCheckOutcome::Pass(format!(
                "API key configured for '{}'",
                provider_for_api_check
            )),
            Err(err) => {
                let detail = err.to_string();
                if detail.contains("Unsupported provider") {
                    DoctorCheckOutcome::Warn(format!(
                        "Skipped API-key validation for unsupported provider '{}'",
                        provider_for_api_check
                    ))
                } else {
                    DoctorCheckOutcome::Fail(format!(
                        "Missing API key for '{}': {} (expected: {})",
                        provider_for_api_check,
                        detail,
                        api_key_env_hint(provider_for_api_check)
                    ))
                }
            }
        }
    } else {
        DoctorCheckOutcome::Pass(format!(
            "Not required for local provider '{}'",
            provider_for_api_check
        ))
    };
    render_doctor_check(renderer, &mut summary, "API Key", api_key_result)?;

    if options.quick {
        renderer.line(MessageStyle::Info, "")?;
        renderer.line(MessageStyle::Status, "[Skipped Checks]")?;
        renderer.line(
            MessageStyle::Info,
            "Dependencies, MCP status, workspace links, and loaded skills are skipped in quick mode.",
        )?;
    } else {
        renderer.line(MessageStyle::Info, "")?;
        renderer.line(MessageStyle::Status, "[Dependencies]")?;

        let node_result = match detect_command_version("node", &["--version"]) {
            Ok(version) => DoctorCheckOutcome::Pass(format!("Node.js {}", version)),
            Err(err) => DoctorCheckOutcome::Warn(err),
        };
        render_doctor_check(renderer, &mut summary, "Node.js", node_result)?;

        let npm_result = match detect_command_version("npm", &["--version"]) {
            Ok(version) => DoctorCheckOutcome::Pass(format!("npm {}", version)),
            Err(err) => DoctorCheckOutcome::Warn(err),
        };
        render_doctor_check(renderer, &mut summary, "npm", npm_result)?;

        let ripgrep_result = match detect_command_version("rg", &["--version"]) {
            Ok(version) => DoctorCheckOutcome::Pass(format!(
                "Ripgrep {}",
                version.lines().next().unwrap_or(&version)
            )),
            Err(err) => {
                if err.contains("not found") {
                    DoctorCheckOutcome::Warn(
                        "Not installed (searches will fall back to built-in grep)".to_string(),
                    )
                } else {
                    DoctorCheckOutcome::Warn(err)
                }
            }
        };
        render_doctor_check(renderer, &mut summary, "Ripgrep", ripgrep_result)?;

        renderer.line(MessageStyle::Info, "")?;
        renderer.line(MessageStyle::Status, "[External Services]")?;

        let mcp_result = if let Some(cfg) = vt_cfg {
            if cfg.mcp.enabled {
                if let Some(manager) = async_mcp_manager {
                    let status = manager.get_status().await;
                    match status {
                        McpInitStatus::Ready { client } => {
                            let runtime_status = client.get_status();
                            DoctorCheckOutcome::Pass(format!(
                                "{} configured, {} active connection(s)",
                                runtime_status.configured_providers.len(),
                                runtime_status.active_connections
                            ))
                        }
                        McpInitStatus::Initializing { progress } => {
                            DoctorCheckOutcome::Warn(format!("Initializing: {}", progress))
                        }
                        McpInitStatus::Error { message } => {
                            DoctorCheckOutcome::Fail(format!("Init error: {}", message))
                        }
                        McpInitStatus::Disabled => {
                            DoctorCheckOutcome::Pass("Disabled in config".to_string())
                        }
                    }
                } else {
                    DoctorCheckOutcome::Fail(
                        "Enabled in config but manager not initialized".to_string(),
                    )
                }
            } else {
                DoctorCheckOutcome::Pass("Disabled".to_string())
            }
        } else {
            DoctorCheckOutcome::Warn("No MCP configuration".to_string())
        };
        render_doctor_check(renderer, &mut summary, "MCP", mcp_result)?;

        renderer.line(MessageStyle::Info, "")?;
        renderer.line(MessageStyle::Status, "[Workspace Links]")?;

        if linked_directories.is_empty() {
            renderer.line(MessageStyle::Output, "  No linked directories")?;
        } else {
            for (idx, entry) in linked_directories.iter().enumerate() {
                renderer.line(
                    MessageStyle::Output,
                    &format!(
                        "  [{}] {} -> {}",
                        idx + 1,
                        entry.display_path,
                        entry.original.display()
                    ),
                )?;
            }
        }

        renderer.line(MessageStyle::Info, "")?;
        renderer.line(MessageStyle::Status, "[Skills]")?;

        if let Some(skills_map) = loaded_skills {
            let skills = skills_map.read().await;
            if skills.is_empty() {
                renderer.line(MessageStyle::Output, "  No skills loaded in session")?;
            } else {
                renderer.line(
                    MessageStyle::Output,
                    &format!("  {} loaded skill(s):", skills.len()),
                )?;
                let mut sorted_skills: Vec<_> = skills.iter().collect();
                sorted_skills.sort_by(|(name_a, _), (name_b, _)| name_a.cmp(name_b));
                for (idx, (name, skill)) in sorted_skills.iter().enumerate() {
                    let scope = format!("{:?}", skill.scope).to_lowercase();
                    renderer.line(
                        MessageStyle::Output,
                        &format!("    [{}] {} ({})", idx + 1, name, scope),
                    )?;
                }
            }
        } else {
            renderer.line(MessageStyle::Output, "  Skills context unavailable")?;
        }
    }

    renderer.line(MessageStyle::Info, "")?;
    renderer.line(
        MessageStyle::Status,
        "═══════════════════════════════════════════════════════════════",
    )?;

    renderer.line(MessageStyle::Info, "")?;
    renderer.line(MessageStyle::Status, "[Summary]")?;
    renderer.line(
        MessageStyle::Status,
        &format!(
            "  {} passed, {} warning(s), {} failure(s)",
            summary.passed, summary.warnings, summary.failures
        ),
    )?;
    if summary.failures > 0 {
        renderer.line(
            MessageStyle::Error,
            "[FAIL] Resolve failures first, then rerun `/doctor`.",
        )?;
    } else if summary.warnings > 0 {
        renderer.line(
            MessageStyle::Warning,
            "[WARN] Core checks passed, but there are warnings to review.",
        )?;
    } else {
        renderer.line(
            MessageStyle::Status,
            "[OK] All checks passed. You are ready to use VT Code.",
        )?;
    }

    renderer.line(MessageStyle::Info, "")?;
    renderer.line(MessageStyle::Status, "[Recommended Next Actions]")?;
    if summary.failures > 0 {
        renderer.line(
            MessageStyle::Info,
            "[FAIL] Follow the remediation hints above and run `/doctor` again.",
        )?;
    }
    if summary.warnings > 0 {
        renderer.line(
            MessageStyle::Info,
            "[WARN] Address warnings for better reliability (they are not always blocking).",
        )?;
    }
    if options.quick {
        renderer.line(
            MessageStyle::Info,
            "[TIP] Run `/doctor --full` to include dependencies, MCP, links, and skills checks.",
        )?;
    }
    renderer.line(
        MessageStyle::Info,
        "[TIP] For more details: `/skills list` (available skills), `/status` (session), `/compact` (conversation compaction)",
    )?;
    renderer.line(
        MessageStyle::Info,
        "[DOCS] See docs/ide/troubleshooting.md for troubleshooting guidance.",
    )?;

    renderer.line(MessageStyle::Info, "")?;
    renderer.line(
        MessageStyle::Status,
        "═══════════════════════════════════════════════════════════════",
    )?;
    renderer.line(MessageStyle::Info, "")?;
    Ok(())
}

fn render_doctor_check(
    renderer: &mut AnsiRenderer,
    summary: &mut DoctorSummary,
    label: &str,
    outcome: DoctorCheckOutcome,
) -> Result<()> {
    summary.record(&outcome);
    match outcome {
        DoctorCheckOutcome::Pass(detail) => {
            renderer.line(MessageStyle::Status, &format!("✓  {}: {}", label, detail))?
        }
        DoctorCheckOutcome::Warn(detail) => {
            renderer.line(MessageStyle::Warning, &format!("!  {}: {}", label, detail))?;
            if let Some(suggestion) = get_suggestion_for_issue(label, &detail) {
                renderer.line(MessageStyle::Info, &format!("   -> {}", suggestion))?;
            }
        }
        DoctorCheckOutcome::Fail(detail) => {
            renderer.line(MessageStyle::Error, &format!("✗  {}: {}", label, detail))?;
            if let Some(suggestion) = get_suggestion_for_issue(label, &detail) {
                renderer.line(MessageStyle::Info, &format!("   -> {}", suggestion))?;
            }
        }
    }
    Ok(())
}

fn get_suggestion_for_issue(label: &str, detail: &str) -> Option<String> {
    let label_lower = label.to_ascii_lowercase();
    let detail_lower = detail.to_ascii_lowercase();

    if label_lower.contains("workspace") {
        Some("Ensure workspace directory is accessible and not deleted.".to_string())
    } else if label_lower.contains("api key") {
        Some(
            "Set the provider API key in your environment or vtcode.toml, then rerun `/doctor`."
                .to_string(),
        )
    } else if label_lower.contains("config file") && detail_lower.contains("no vtcode.toml") {
        Some("Run `/init` to generate vtcode.toml with guided setup.".to_string())
    } else if label_lower.contains("node.js") {
        Some("Install Node.js: brew install node (macOS) or see nodejs.org".to_string())
    } else if label_lower == "npm" {
        Some("Install npm with Node.js or update it: npm install -g npm@latest".to_string())
    } else if label_lower.contains("ripgrep") {
        Some(format!(
            "Run `{RIPGREP_INSTALL_COMMAND}` for guided installation, or install `ripgrep` manually with your package manager."
        ))
    } else if label_lower.contains("mcp") && detail_lower.contains("error") {
        Some(
            "Check MCP configuration in vtcode.toml, server availability, and timeouts."
                .to_string(),
        )
    } else {
        None
    }
}

fn canonical_provider_name(raw: &str) -> String {
    let normalized = raw.trim().to_ascii_lowercase();
    if normalized.is_empty() {
        return "gemini".to_string();
    }

    match normalized.as_str() {
        "open-responses" => "openresponses".to_string(),
        "lm studio" | "lm_studio" => "lmstudio".to_string(),
        "llama.cpp" | "llama-cpp" | "llama cpp" => "llamacpp".to_string(),
        "z.ai" | "z-ai" => "zai".to_string(),
        _ => normalized,
    }
}

fn provider_requires_api_key(provider: &str) -> bool {
    !matches!(provider, "ollama" | "lmstudio" | "llamacpp" | "llama.cpp" | "llama-cpp")
}

fn api_key_env_hint(provider: &str) -> String {
    match provider {
        "gemini" => "GEMINI_API_KEY or GOOGLE_API_KEY".to_string(),
        "openai" => "OPENAI_API_KEY".to_string(),
        "anthropic" => "ANTHROPIC_API_KEY".to_string(),
        "deepseek" => "DEEPSEEK_API_KEY".to_string(),
        "openrouter" => "OPENROUTER_API_KEY".to_string(),
        "zai" => "ZAI_API_KEY".to_string(),
        "moonshot" => "MOONSHOT_API_KEY".to_string(),
        "minimax" => "MINIMAX_API_KEY".to_string(),
        "huggingface" => "HF_TOKEN".to_string(),
        "ollama" => "OLLAMA_API_KEY".to_string(),
        "lmstudio" => "LMSTUDIO_API_KEY".to_string(),
        "llamacpp" => "LLAMACPP_API_KEY".to_string(),
        other => format!("{}_API_KEY", other.to_ascii_uppercase()),
    }
}

fn detect_command_version(command: &str, args: &[&str]) -> std::result::Result<String, String> {
    use std::process::Command;

    match Command::new(command).args(args).output() {
        Ok(output) => {
            if output.status.success() {
                let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
                let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
                let version_text = if !stdout.is_empty() { stdout } else { stderr };
                if version_text.is_empty() {
                    Err(format!(
                        "{} {} returned no version output",
                        command,
                        args.join(" ")
                    ))
                } else {
                    Ok(version_text)
                }
            } else {
                let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
                let reason = if !stderr.is_empty() {
                    stderr
                } else {
                    output.status.to_string()
                };
                Err(format!(
                    "{} {} exited with error: {}",
                    command,
                    args.join(" "),
                    reason
                ))
            }
        }
        Err(err) => {
            if err.kind() == std::io::ErrorKind::NotFound {
                Err(format!("{} not found in PATH", command))
            } else {
                Err(format!("Failed to execute {}: {}", command, err))
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::{api_key_env_hint, canonical_provider_name, provider_requires_api_key};

    #[test]
    fn canonical_provider_keeps_openresponses_runtime_adapter() {
        assert_eq!(canonical_provider_name("openresponses"), "openresponses");
        assert_eq!(canonical_provider_name("open-responses"), "openresponses");
    }

    #[test]
    fn canonical_provider_normalizes_variants() {
        assert_eq!(canonical_provider_name(" LM Studio "), "lmstudio");
        assert_eq!(canonical_provider_name("Z.AI"), "zai");
        assert_eq!(canonical_provider_name(""), "gemini");
    }

    #[test]
    fn local_providers_do_not_require_api_keys() {
        assert!(!provider_requires_api_key("ollama"));
        assert!(!provider_requires_api_key("lmstudio"));
        assert!(provider_requires_api_key("openai"));
    }

    #[test]
    fn api_key_hint_is_provider_specific() {
        assert_eq!(api_key_env_hint("openrouter"), "OPENROUTER_API_KEY");
        assert_eq!(api_key_env_hint("huggingface"), "HF_TOKEN");
    }
}
