use std::path::{Path, PathBuf};

/// A single diagnostic check result.
#[derive(Debug)]
struct Check {
    name: &'static str,
    passed: bool,
    detail: String,
}

/// Run all diagnostic checks and print results.
pub fn run() -> anyhow::Result<()> {
    let working_dir = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
    let global_dir = crab_config::config::global_config_dir();
    let ctx = crab_config::ResolveContext::new()
        .with_project_dir(Some(working_dir.clone()))
        .with_process_env();
    let settings = crab_config::resolve(&ctx).unwrap_or_default();

    let checks = vec![
        check_api_key(&settings),
        check_settings_file(&global_dir),
        check_project_settings(&working_dir),
        check_git(),
        check_working_dir(&working_dir),
        check_sessions_dir(&global_dir),
        check_memory_dir(&global_dir),
        check_mcp_config(&settings),
        check_disk_space(&global_dir),
        check_version(),
        check_package_managers(),
        check_update_available(),
        check_deep_link_support(),
    ];

    let mut pass_count = 0;
    let mut fail_count = 0;

    eprintln!("Crab Code Doctor");
    eprintln!("================");
    eprintln!();

    for check in &checks {
        let icon = if check.passed { "ok" } else { "FAIL" };
        eprintln!("  [{}] {} — {}", icon, check.name, check.detail);
        if check.passed {
            pass_count += 1;
        } else {
            fail_count += 1;
        }
    }

    eprintln!();
    eprintln!(
        "{} checks passed, {} failed out of {} total.",
        pass_count,
        fail_count,
        checks.len()
    );

    if fail_count > 0 {
        eprintln!();
        eprintln!("Run 'crab auth login' for help configuring API keys.");
    }

    Ok(())
}

fn check_api_key(settings: &crab_config::Config) -> Check {
    let has_auth_token_env = std::env::var("ANTHROPIC_AUTH_TOKEN").is_ok_and(|v| !v.is_empty());
    let has_anthropic_env = std::env::var("ANTHROPIC_API_KEY").is_ok_and(|v| !v.is_empty());
    let has_openai_env = std::env::var("OPENAI_API_KEY").is_ok_and(|v| !v.is_empty());
    let has_helper = settings
        .api_key_helper
        .as_ref()
        .is_some_and(|s| !s.trim().is_empty());
    let resolved = crab_auth::resolve_auth_key(settings).is_some();

    let passed =
        resolved || has_auth_token_env || has_anthropic_env || has_openai_env || has_helper;

    let detail = if has_auth_token_env {
        "ANTHROPIC_AUTH_TOKEN set".into()
    } else if has_anthropic_env {
        "ANTHROPIC_API_KEY set".into()
    } else if has_openai_env {
        "OPENAI_API_KEY set".into()
    } else if has_helper {
        "apiKeyHelper configured".into()
    } else if resolved {
        "credential resolved (keychain or auth/tokens.json)".into()
    } else {
        "no API key found (set ANTHROPIC_API_KEY or run 'crab auth setup-token')".into()
    };

    Check {
        name: "API key",
        passed,
        detail,
    }
}

fn check_settings_file(global_dir: &Path) -> Check {
    let path = global_dir.join(crab_config::config::config_file_name());
    if !path.exists() {
        return Check {
            name: "Config file",
            passed: true,
            detail: format!("{} (not created yet, using defaults)", path.display()),
        };
    }

    match std::fs::read_to_string(&path) {
        Ok(content) => match toml::from_str::<toml::Value>(&content) {
            Ok(_) => Check {
                name: "Config file",
                passed: true,
                detail: format!("{} (valid TOML)", path.display()),
            },
            Err(e) => Check {
                name: "Config file",
                passed: false,
                detail: format!("{} (invalid TOML: {})", path.display(), e),
            },
        },
        Err(e) => Check {
            name: "Config file",
            passed: false,
            detail: format!("{} (read error: {})", path.display(), e),
        },
    }
}

fn check_git() -> Check {
    match std::process::Command::new("git").arg("--version").output() {
        Ok(output) if output.status.success() => {
            let version = String::from_utf8_lossy(&output.stdout).trim().to_string();
            Check {
                name: "Git",
                passed: true,
                detail: version,
            }
        }
        Ok(_) => Check {
            name: "Git",
            passed: false,
            detail: "git command failed".into(),
        },
        Err(_) => Check {
            name: "Git",
            passed: false,
            detail: "git not found in PATH".into(),
        },
    }
}

fn check_working_dir(working_dir: &Path) -> Check {
    let readable = working_dir.exists() && working_dir.is_dir();
    let writable = if readable {
        // Try creating a temp file to test write access
        let test_path = working_dir.join(".crab_doctor_test");
        let w = std::fs::write(&test_path, "").is_ok();
        let _ = std::fs::remove_file(&test_path);
        w
    } else {
        false
    };

    Check {
        name: "Working directory",
        passed: readable && writable,
        detail: if readable && writable {
            format!("{} (readable, writable)", working_dir.display())
        } else if readable {
            format!("{} (readable, NOT writable)", working_dir.display())
        } else {
            format!("{} (not accessible)", working_dir.display())
        },
    }
}

fn check_sessions_dir(global_dir: &Path) -> Check {
    check_dir_exists_writable(global_dir, "sessions", "Sessions directory")
}

fn check_memory_dir(global_dir: &Path) -> Check {
    check_dir_exists_writable(global_dir, "memory", "Memory directory")
}

fn check_dir_exists_writable(global_dir: &Path, subdir: &str, name: &'static str) -> Check {
    let dir = global_dir.join(subdir);
    if !dir.exists() {
        return Check {
            name,
            passed: true,
            detail: format!("{} (will be created on first use)", dir.display()),
        };
    }

    let writable = {
        let test_path = dir.join(".crab_doctor_test");
        let w = std::fs::write(&test_path, "").is_ok();
        let _ = std::fs::remove_file(&test_path);
        w
    };

    Check {
        name,
        passed: writable,
        detail: if writable {
            format!("{} (exists, writable)", dir.display())
        } else {
            format!("{} (exists, NOT writable)", dir.display())
        },
    }
}

fn check_project_settings(working_dir: &Path) -> Check {
    let project_dir = working_dir.join(".crab");
    let settings_path = project_dir.join(crab_config::config::config_file_name());
    let local_path = project_dir.join(crab_config::config::local_config_file_name());

    if !project_dir.exists() {
        return Check {
            name: "Project config",
            passed: true,
            detail: "no .crab/ directory (using global config only)".into(),
        };
    }

    let mut parts = Vec::new();
    if settings_path.exists() {
        match std::fs::read_to_string(&settings_path) {
            Ok(content) => match toml::from_str::<toml::Value>(&content) {
                Ok(_) => parts.push("config.toml (valid)"),
                Err(_) => {
                    return Check {
                        name: "Project config",
                        passed: false,
                        detail: "config.toml has invalid TOML".into(),
                    };
                }
            },
            Err(_) => parts.push("config.toml (unreadable)"),
        }
    }
    if local_path.exists() {
        parts.push("config.local.toml");
    }

    Check {
        name: "Project config",
        passed: true,
        detail: if parts.is_empty() {
            ".crab/ exists but no config files".into()
        } else {
            parts.join(", ")
        },
    }
}

fn check_disk_space(global_dir: &Path) -> Check {
    let sessions_dir = global_dir.join("sessions");
    if !sessions_dir.exists() {
        return Check {
            name: "Disk usage",
            passed: true,
            detail: "no sessions directory yet".into(),
        };
    }

    // Calculate total size of sessions directory
    let total_bytes = dir_size(&sessions_dir);
    let size_mb = total_bytes as f64 / (1024.0 * 1024.0);
    let passed = size_mb < 500.0; // warn if > 500 MB

    Check {
        name: "Disk usage",
        passed,
        detail: if size_mb < 1.0 {
            format!("sessions: {:.0} KB", total_bytes as f64 / 1024.0)
        } else {
            format!(
                "sessions: {:.1} MB{}",
                size_mb,
                if passed {
                    ""
                } else {
                    " (consider running 'crab session delete' to clean up)"
                }
            )
        },
    }
}

fn dir_size(path: &Path) -> u64 {
    let mut total = 0;
    if let Ok(entries) = std::fs::read_dir(path) {
        for entry in entries.flatten() {
            if let Ok(meta) = entry.metadata() {
                if meta.is_file() {
                    total += meta.len();
                } else if meta.is_dir() {
                    total += dir_size(&entry.path());
                }
            }
        }
    }
    total
}

fn check_version() -> Check {
    let current = env!("CARGO_PKG_VERSION");
    Check {
        name: "Version",
        passed: true,
        detail: format!("crab-code v{current}"),
    }
}

fn check_package_managers() -> Check {
    let found = crate::installer::detect_package_managers();
    if found.is_empty() {
        return Check {
            name: "Package managers",
            passed: true,
            detail: "none detected (manual upgrade only)".into(),
        };
    }
    let names: Vec<_> = found.iter().map(ToString::to_string).collect();
    Check {
        name: "Package managers",
        passed: true,
        detail: format!("detected: {}", names.join(", ")),
    }
}

fn check_deep_link_support() -> Check {
    // Report the platform-specific registration steps so users know how
    // to make `crab-cli://` links launch this binary. We can't actually
    // test whether the OS has the scheme registered without touching the
    // registry / Launch Services / xdg, so we always pass and surface the
    // instructions as informational detail.
    let platform = if cfg!(target_os = "windows") {
        "Windows"
    } else if cfg!(target_os = "macos") {
        "macOS"
    } else {
        "Linux"
    };
    // Keep the raw instructions available via `--help` style rendering
    // in the UI later; the doctor line stays terse.
    let _instructions = crate::deep_link::register_url_scheme();
    Check {
        name: "Deep link scheme",
        passed: true,
        detail: format!("crab-cli:// ({platform}: see docs for registration)"),
    }
}

fn check_update_available() -> Check {
    match super::update::startup_version_check() {
        Some(latest) => Check {
            name: "Update",
            passed: true,
            detail: format!("v{latest} available — run 'crab update' to install"),
        },
        None => Check {
            name: "Update",
            passed: true,
            detail: "up to date (or no cache yet; run 'crab update --check')".into(),
        },
    }
}

fn check_mcp_config(settings: &crab_config::Config) -> Check {
    match &settings.mcp_servers {
        None => Check {
            name: "MCP servers",
            passed: true,
            detail: "no MCP servers configured".into(),
        },
        Some(value) => {
            if let Some(obj) = value.as_object() {
                Check {
                    name: "MCP servers",
                    passed: true,
                    detail: format!("{} server(s) configured", obj.len()),
                }
            } else {
                Check {
                    name: "MCP servers",
                    passed: false,
                    detail: "mcpServers must be a JSON object".into(),
                }
            }
        }
    }
}

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

    #[test]
    fn check_api_key_with_helper() {
        let settings = crab_config::Config {
            api_key_helper: Some("/usr/local/bin/get-key.sh".into()),
            ..Default::default()
        };
        let result = check_api_key(&settings);
        // The helper does not actually run during this check (we only look at
        // whether it is configured and whether resolve_auth_key returns Some);
        // env vars may dominate in CI, so just verify it produces a sensible result.
        assert_eq!(result.name, "API key");
        // Detail should mention helper when no env vars beat it.
        if !std::env::var("ANTHROPIC_AUTH_TOKEN").is_ok_and(|v| !v.is_empty())
            && !std::env::var("ANTHROPIC_API_KEY").is_ok_and(|v| !v.is_empty())
            && !std::env::var("OPENAI_API_KEY").is_ok_and(|v| !v.is_empty())
        {
            assert!(result.passed);
            assert!(result.detail.contains("apiKeyHelper"));
        }
    }

    #[test]
    fn check_api_key_without_any() {
        let settings = crab_config::Config::default();
        // May or may not pass depending on env vars in CI
        let result = check_api_key(&settings);
        // Just verify it runs without panic
        assert!(!result.name.is_empty());
    }

    #[test]
    fn check_git_runs() {
        let result = check_git();
        // Git should be available in most dev environments
        assert!(!result.name.is_empty());
        if result.passed {
            assert!(result.detail.contains("git"));
        }
    }

    #[test]
    fn check_working_dir_current() {
        let wd = std::env::current_dir().unwrap();
        let result = check_working_dir(&wd);
        assert!(result.passed);
        assert!(result.detail.contains("writable"));
    }

    #[test]
    fn check_working_dir_nonexistent() {
        let result = check_working_dir(Path::new("/nonexistent/path"));
        assert!(!result.passed);
    }

    #[test]
    fn check_settings_file_nonexistent_global() {
        let result = check_settings_file(Path::new("/nonexistent/crab"));
        assert!(result.passed); // Not created yet is fine
        assert!(result.detail.contains("defaults"));
    }

    #[test]
    fn check_mcp_no_config() {
        let settings = crab_config::Config::default();
        let result = check_mcp_config(&settings);
        assert!(result.passed);
    }

    #[test]
    fn check_mcp_with_servers() {
        let settings = crab_config::Config {
            mcp_servers: Some(serde_json::json!({
                "playwright": { "command": "npx", "args": ["playwright"] }
            })),
            ..Default::default()
        };
        let result = check_mcp_config(&settings);
        assert!(result.passed);
        assert!(result.detail.contains("1 server"));
    }

    #[test]
    fn check_mcp_invalid_format() {
        let settings = crab_config::Config {
            mcp_servers: Some(serde_json::json!("not an object")),
            ..Default::default()
        };
        let result = check_mcp_config(&settings);
        assert!(!result.passed);
    }

    #[test]
    fn check_dir_exists_writable_nonexistent() {
        let result = check_dir_exists_writable(
            Path::new("/nonexistent/crab"),
            "sessions",
            "Sessions directory",
        );
        assert!(result.passed); // Will be created on first use
    }

    #[test]
    fn check_project_settings_no_crab_dir() {
        let dir = std::env::temp_dir().join("crab-doctor-test-no-crab");
        let _ = std::fs::create_dir_all(&dir);
        let result = check_project_settings(&dir);
        assert!(result.passed);
        assert!(result.detail.contains("no .crab/"));
        let _ = std::fs::remove_dir_all(&dir);
    }

    #[test]
    fn check_project_settings_with_valid_toml() {
        let dir = std::env::temp_dir().join("crab-doctor-test-proj-settings");
        let crab_dir = dir.join(".crab");
        let _ = std::fs::create_dir_all(&crab_dir);
        std::fs::write(crab_dir.join("config.toml"), r#"model = "test""#).unwrap();

        let result = check_project_settings(&dir);
        assert!(result.passed);
        assert!(result.detail.contains("config.toml"));

        let _ = std::fs::remove_dir_all(&dir);
    }

    #[test]
    fn check_disk_space_no_sessions_dir() {
        let result = check_disk_space(Path::new("/nonexistent/crab"));
        assert!(result.passed);
        assert!(result.detail.contains("no sessions"));
    }

    #[test]
    fn check_disk_space_with_files() {
        let dir = std::env::temp_dir().join("crab-doctor-test-disk");
        let sessions = dir.join("sessions");
        let _ = std::fs::create_dir_all(&sessions);
        std::fs::write(sessions.join("test.json"), "{}").unwrap();

        let result = check_disk_space(&dir);
        assert!(result.passed);

        let _ = std::fs::remove_dir_all(&dir);
    }

    #[test]
    fn check_version_includes_version_string() {
        let result = check_version();
        assert!(result.passed);
        assert!(result.detail.contains("crab-code v"));
    }

    #[test]
    fn dir_size_empty() {
        let dir = std::env::temp_dir().join("crab-doctor-test-empty-dir");
        let _ = std::fs::create_dir_all(&dir);
        assert_eq!(dir_size(&dir), 0);
        let _ = std::fs::remove_dir_all(&dir);
    }

    #[test]
    fn dir_size_with_files() {
        let dir = std::env::temp_dir().join("crab-doctor-test-sized-dir");
        let _ = std::fs::create_dir_all(&dir);
        std::fs::write(dir.join("a.txt"), "hello").unwrap();
        std::fs::write(dir.join("b.txt"), "world!").unwrap();
        assert!(dir_size(&dir) > 0);
        let _ = std::fs::remove_dir_all(&dir);
    }
}
