use anyhow::{Context, Result};
use console::style;
use serde::Serialize;
use std::path::Path;
use std::process::Command;
use std::time::Duration;

use crate::run::detect_project_type;

pub struct DoctorOptions {
    pub json: bool,
}

#[derive(Debug, Serialize)]
struct DoctorReport {
    project_type: String,
    sections: Vec<Section>,
    passed: usize,
    failed: usize,
}

#[derive(Debug, Serialize)]
struct Section {
    name: String,
    checks: Vec<CheckResult>,
}

#[derive(Debug, Clone, Serialize)]
struct CheckResult {
    name: String,
    status: CheckStatus,
    version: Option<String>,
    detail: Option<String>,
    fix: Option<String>,
}

#[derive(Debug, Clone, Serialize, PartialEq)]
#[serde(rename_all = "lowercase")]
enum CheckStatus {
    Ok,
    Missing,
    Error,
}

pub fn run(opts: DoctorOptions) -> Result<()> {
    let project_dir = std::env::current_dir().context("getting current directory")?;
    let project_type = detect_project_type(&project_dir);

    let sections = vec![
        check_toolchain(project_type),
        check_dependencies(project_type, &project_dir),
        check_git(&project_dir),
        check_ai(),
    ];

    let passed: usize = sections
        .iter()
        .flat_map(|s| &s.checks)
        .filter(|c| c.status == CheckStatus::Ok)
        .count();
    let failed: usize = sections
        .iter()
        .flat_map(|s| &s.checks)
        .filter(|c| c.status != CheckStatus::Ok)
        .count();

    if opts.json {
        let report = DoctorReport {
            project_type: project_type.to_string(),
            sections,
            passed,
            failed,
        };
        println!("{}", serde_json::to_string_pretty(&report)?);
        return Ok(());
    }

    println!("\n{}\n", style("fledge doctor").bold());

    for section in &sections {
        println!("  {}", style(&section.name).bold());
        for check in &section.checks {
            match &check.status {
                CheckStatus::Ok => {
                    let label = match &check.version {
                        Some(v) => format!("{} {}", check.name, v),
                        None => check.name.clone(),
                    };
                    let label = match &check.detail {
                        Some(d) => format!("{} — {}", label, d),
                        None => label,
                    };
                    println!("    {} {}", style("✅").green().bold(), label);
                }
                CheckStatus::Missing => {
                    let detail = check.detail.as_deref().unwrap_or("not found");
                    println!(
                        "    {} {} — {}",
                        style("❌").red().bold(),
                        check.name,
                        detail
                    );
                    if let Some(fix) = &check.fix {
                        println!("      {} {}", style("➡️").dim(), style(fix).cyan());
                    }
                }
                CheckStatus::Error => {
                    let detail = check.detail.as_deref().unwrap_or("error");
                    println!(
                        "    {} {} — {}",
                        style("❌").red().bold(),
                        check.name,
                        detail
                    );
                    if let Some(fix) = &check.fix {
                        println!("      {} {}", style("➡️").dim(), style(fix).cyan());
                    }
                }
            }
        }
        println!();
    }

    println!(
        "  {} passed, {} found\n",
        style(format!("{} checks", passed)).green().bold(),
        style(format!("{} issues", failed)).red().bold(),
    );

    Ok(())
}

fn check_tool(name: &str, version_args: &[&str], fix: &str) -> CheckResult {
    let child = Command::new(name)
        .args(version_args)
        .stdout(std::process::Stdio::piped())
        .stderr(std::process::Stdio::piped())
        .spawn();

    let mut child = match child {
        Ok(c) => c,
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
            return CheckResult {
                name: name.to_string(),
                status: CheckStatus::Missing,
                version: None,
                detail: Some("not found".to_string()),
                fix: Some(fix.to_string()),
            };
        }
        Err(e) => {
            return CheckResult {
                name: name.to_string(),
                status: CheckStatus::Error,
                version: None,
                detail: Some(format!("error: {}", e)),
                fix: Some(fix.to_string()),
            };
        }
    };

    let timeout = Duration::from_secs(10);
    let start = std::time::Instant::now();
    loop {
        match child.try_wait() {
            Ok(Some(_)) => break,
            Ok(None) => {
                if start.elapsed() > timeout {
                    let _ = child.kill();
                    return CheckResult {
                        name: name.to_string(),
                        status: CheckStatus::Error,
                        version: None,
                        detail: Some("timed out after 10s".to_string()),
                        fix: Some(fix.to_string()),
                    };
                }
                std::thread::sleep(Duration::from_millis(50));
            }
            Err(e) => {
                return CheckResult {
                    name: name.to_string(),
                    status: CheckStatus::Error,
                    version: None,
                    detail: Some(format!("error: {}", e)),
                    fix: Some(fix.to_string()),
                };
            }
        }
    }

    let output = child.wait_with_output();
    match output {
        Ok(out) => {
            let text = String::from_utf8_lossy(&out.stdout);
            let stderr = String::from_utf8_lossy(&out.stderr);
            let combined = if text.trim().is_empty() {
                stderr.to_string()
            } else {
                text.to_string()
            };
            let version = extract_version(&combined);
            CheckResult {
                name: name.to_string(),
                status: CheckStatus::Ok,
                version,
                detail: None,
                fix: None,
            }
        }
        Err(e) => CheckResult {
            name: name.to_string(),
            status: CheckStatus::Error,
            version: None,
            detail: Some(format!("error: {}", e)),
            fix: Some(fix.to_string()),
        },
    }
}

fn extract_version(text: &str) -> Option<String> {
    // Look for version-like pattern: digits.digits[.digits[...]]
    // Handles prefixes like "v1.2.3", "go1.22.2", plain "1.78.0"
    text.split_whitespace().find_map(|word| {
        // Strip common prefixes: "v", "go"
        let trimmed = word.trim_start_matches("go").trim_start_matches('v');
        let parts: Vec<&str> = trimmed.split('.').collect();
        if parts.len() >= 2
            && parts.iter().all(|p| {
                let numeric_part = p.trim_end_matches(|c: char| !c.is_ascii_digit());
                !numeric_part.is_empty() && numeric_part.chars().all(|c| c.is_ascii_digit())
            })
        {
            Some(trimmed.trim_end_matches(',').to_string())
        } else {
            None
        }
    })
}

fn check_toolchain(project_type: &str) -> Section {
    let mut checks = Vec::new();

    match project_type {
        "rust" => {
            checks.push(check_tool(
                "rustc",
                &["--version"],
                "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh",
            ));
            checks.push(check_tool(
                "cargo",
                &["--version"],
                "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh",
            ));
            checks.push(check_tool(
                "cargo-clippy",
                &["--version"],
                "rustup component add clippy",
            ));
            checks.push(check_tool(
                "rustfmt",
                &["--version"],
                "rustup component add rustfmt",
            ));
        }
        "node" => {
            checks.push(check_tool(
                "node",
                &["--version"],
                "https://nodejs.org/ or use nvm",
            ));
            // Check npm first; if missing, suggest yarn check result
            let npm = check_tool(
                "npm",
                &["--version"],
                "npm is bundled with node — reinstall node",
            );
            let yarn = check_tool("yarn", &["--version"], "npm install -g yarn");
            if npm.status == CheckStatus::Ok {
                checks.push(npm);
            } else if yarn.status == CheckStatus::Ok {
                checks.push(yarn);
            } else {
                // Both missing: show npm with fix
                checks.push(npm);
            }
        }
        "go" => {
            checks.push(check_tool("go", &["version"], "https://go.dev/dl/"));
        }
        "python" => {
            let py3 = check_tool(
                "python3",
                &["--version"],
                "https://www.python.org/downloads/",
            );
            let py = check_tool(
                "python",
                &["--version"],
                "https://www.python.org/downloads/",
            );
            if py3.status == CheckStatus::Ok {
                checks.push(py3);
            } else if py.status == CheckStatus::Ok {
                checks.push(py);
            } else {
                checks.push(py3);
            }
            checks.push(check_tool("pip", &["--version"], "python3 -m ensurepip"));
        }
        "ruby" => {
            checks.push(check_tool(
                "ruby",
                &["--version"],
                "https://www.ruby-lang.org/en/downloads/",
            ));
            checks.push(check_tool(
                "gem",
                &["--version"],
                "gem is bundled with ruby — reinstall ruby",
            ));
            checks.push(check_tool("bundler", &["--version"], "gem install bundler"));
        }
        "java-gradle" => {
            checks.push(check_tool("java", &["-version"], "https://adoptium.net/"));
            checks.push(check_tool(
                "gradle",
                &["--version"],
                "https://gradle.org/install/",
            ));
        }
        "java-maven" => {
            checks.push(check_tool("java", &["-version"], "https://adoptium.net/"));
            checks.push(check_tool(
                "mvn",
                &["--version"],
                "https://maven.apache.org/install.html",
            ));
        }
        "swift" => {
            checks.push(check_tool(
                "swift",
                &["--version"],
                "https://www.swift.org/install/",
            ));
            let swiftlint = check_tool("swiftlint", &["version"], "brew install swiftlint");
            if swiftlint.status == CheckStatus::Ok {
                checks.push(swiftlint);
            }
        }
        _ => {
            // generic — just check git (handled in git section)
        }
    }

    Section {
        name: "Toolchain".to_string(),
        checks,
    }
}

fn check_dependencies(project_type: &str, dir: &Path) -> Section {
    let mut checks = Vec::new();

    match project_type {
        "rust" => {
            checks.push(check_path_exists(
                dir,
                "Cargo.lock",
                "Cargo.lock found",
                "run `cargo generate-lockfile`",
            ));
            checks.push(check_path_exists(
                dir,
                "target",
                "target/ exists",
                "run `cargo build`",
            ));
        }
        "node" => {
            checks.push(check_path_exists(
                dir,
                "node_modules",
                "node_modules/ exists",
                "run `npm install`",
            ));
            let npm_lock = check_path_exists(
                dir,
                "package-lock.json",
                "package-lock.json found",
                "run `npm install`",
            );
            let yarn_lock =
                check_path_exists(dir, "yarn.lock", "yarn.lock found", "run `yarn install`");
            if npm_lock.status == CheckStatus::Ok {
                checks.push(npm_lock);
            } else if yarn_lock.status == CheckStatus::Ok {
                checks.push(yarn_lock);
            } else {
                checks.push(npm_lock);
            }
        }
        "go" => {
            checks.push(check_path_exists(
                dir,
                "go.sum",
                "go.sum found",
                "run `go mod tidy`",
            ));
        }
        "python" => {
            // Check for any common dependency file
            let req = check_path_exists(
                dir,
                "requirements.txt",
                "requirements.txt found",
                "run `pip freeze > requirements.txt`",
            );
            let pipfile = check_path_exists(
                dir,
                "Pipfile.lock",
                "Pipfile.lock found",
                "run `pipenv install`",
            );
            let poetry = check_path_exists(
                dir,
                "poetry.lock",
                "poetry.lock found",
                "run `poetry install`",
            );
            if req.status == CheckStatus::Ok {
                checks.push(req);
            } else if pipfile.status == CheckStatus::Ok {
                checks.push(pipfile);
            } else if poetry.status == CheckStatus::Ok {
                checks.push(poetry);
            } else {
                checks.push(req);
            }
        }
        "ruby" => {
            checks.push(check_path_exists(
                dir,
                "Gemfile.lock",
                "Gemfile.lock found",
                "run `bundle install`",
            ));
        }
        "java-gradle" => {
            // Check for gradle wrapper
            checks.push(check_path_exists(
                dir,
                "gradlew",
                "gradle wrapper found",
                "run `gradle wrapper`",
            ));
        }
        "java-maven" => {
            // Check for maven wrapper or target
            checks.push(check_path_exists(
                dir,
                "target",
                "target/ exists",
                "run `mvn compile`",
            ));
        }
        _ => {}
    }

    Section {
        name: "Dependencies".to_string(),
        checks,
    }
}

fn check_path_exists(dir: &Path, name: &str, ok_label: &str, fix: &str) -> CheckResult {
    let path = dir.join(name);
    if path.exists() {
        CheckResult {
            name: ok_label.to_string(),
            status: CheckStatus::Ok,
            version: None,
            detail: None,
            fix: None,
        }
    } else {
        CheckResult {
            name: name.to_string(),
            status: CheckStatus::Missing,
            version: None,
            detail: Some("not found".to_string()),
            fix: Some(fix.to_string()),
        }
    }
}

fn check_git(dir: &Path) -> Section {
    let mut checks = Vec::new();

    // Check git is installed
    checks.push(check_tool(
        "git",
        &["--version"],
        "https://git-scm.com/downloads",
    ));

    // Check repo is initialized
    let git_dir = dir.join(".git");
    if git_dir.exists() {
        checks.push(CheckResult {
            name: "repository".to_string(),
            status: CheckStatus::Ok,
            version: None,
            detail: Some("initialized".to_string()),
            fix: None,
        });
    } else {
        checks.push(CheckResult {
            name: "repository".to_string(),
            status: CheckStatus::Missing,
            version: None,
            detail: Some("not a git repository".to_string()),
            fix: Some("git init".to_string()),
        });
    }

    // Check remote
    let remote_output = Command::new("git")
        .args(["remote", "-v"])
        .current_dir(dir)
        .output();

    match remote_output {
        Ok(out) => {
            let text = String::from_utf8_lossy(&out.stdout);
            let first_line = text.lines().next().unwrap_or("");
            if first_line.is_empty() {
                checks.push(CheckResult {
                    name: "remote".to_string(),
                    status: CheckStatus::Missing,
                    version: None,
                    detail: Some("no remote configured".to_string()),
                    fix: Some("git remote add origin <url>".to_string()),
                });
            } else {
                // Parse "origin\thttps://... (fetch)"
                let parts: Vec<&str> = first_line.split_whitespace().collect();
                let remote_name = parts.first().unwrap_or(&"origin");
                let remote_url = parts.get(1).unwrap_or(&"");
                checks.push(CheckResult {
                    name: "remote".to_string(),
                    status: CheckStatus::Ok,
                    version: None,
                    detail: Some(format!("{} ➡️ {}", remote_name, remote_url)),
                    fix: None,
                });
            }
        }
        Err(_) => {
            // git not installed or not a repo — already covered above
        }
    }

    // Check for uncommitted changes
    let status_output = Command::new("git")
        .args(["status", "--porcelain"])
        .current_dir(dir)
        .output();

    if let Ok(out) = status_output {
        let text = String::from_utf8_lossy(&out.stdout);
        let changed: Vec<&str> = text.lines().filter(|l| !l.is_empty()).collect();
        if changed.is_empty() {
            checks.push(CheckResult {
                name: "working tree".to_string(),
                status: CheckStatus::Ok,
                version: None,
                detail: Some("clean".to_string()),
                fix: None,
            });
        } else {
            checks.push(CheckResult {
                name: "working tree".to_string(),
                status: CheckStatus::Error,
                version: None,
                detail: Some(format!("uncommitted changes ({} files)", changed.len())),
                fix: Some("git add . && git commit".to_string()),
            });
        }
    }

    Section {
        name: "Git".to_string(),
        checks,
    }
}

fn check_ai() -> Section {
    let mut checks = Vec::new();

    let claude = check_tool(
        "claude",
        &["--version"],
        "Install Claude CLI: https://docs.anthropic.com/en/docs/claude-code — then run `claude` to authenticate",
    );

    match claude.status {
        CheckStatus::Ok => {
            checks.push(claude);
            checks.push(CheckResult {
                name: "AI commands".to_string(),
                status: CheckStatus::Ok,
                version: None,
                detail: Some("fledge review, fledge ask available".to_string()),
                fix: None,
            });
        }
        _ => {
            checks.push(claude);
            checks.push(CheckResult {
                name: "AI commands".to_string(),
                status: CheckStatus::Missing,
                version: None,
                detail: Some("fledge review, fledge ask disabled".to_string()),
                fix: Some(
                    "Install Claude CLI to enable AI-powered code review and Q&A".to_string(),
                ),
            });
        }
    }

    Section {
        name: "AI".to_string(),
        checks,
    }
}

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

    #[test]
    fn extract_version_from_rustc() {
        let v = extract_version("rustc 1.78.0 (9b00956e5 2024-04-29)");
        assert_eq!(v, Some("1.78.0".to_string()));
    }

    #[test]
    fn extract_version_from_node() {
        let v = extract_version("v20.11.1");
        assert_eq!(v, Some("20.11.1".to_string()));
    }

    #[test]
    fn extract_version_from_go() {
        let v = extract_version("go version go1.22.2 darwin/arm64");
        assert_eq!(v, Some("1.22.2".to_string()));
    }

    #[test]
    fn extract_version_from_git() {
        let v = extract_version("git version 2.44.0");
        assert_eq!(v, Some("2.44.0".to_string()));
    }

    #[test]
    fn extract_version_none() {
        let v = extract_version("no version here");
        assert_eq!(v, None);
    }

    #[test]
    fn check_path_exists_found() {
        let dir = tempfile::tempdir().unwrap();
        std::fs::write(dir.path().join("Cargo.lock"), "").unwrap();
        let result = check_path_exists(
            dir.path(),
            "Cargo.lock",
            "Cargo.lock found",
            "run cargo generate-lockfile",
        );
        assert_eq!(result.status, CheckStatus::Ok);
    }

    #[test]
    fn check_path_exists_missing() {
        let dir = tempfile::tempdir().unwrap();
        let result = check_path_exists(
            dir.path(),
            "Cargo.lock",
            "Cargo.lock found",
            "run cargo generate-lockfile",
        );
        assert_eq!(result.status, CheckStatus::Missing);
        assert!(result.fix.is_some());
    }

    #[test]
    fn toolchain_rust_checks() {
        let section = check_toolchain("rust");
        assert_eq!(section.name, "Toolchain");
        // Should check rustc, cargo, cargo-clippy, rustfmt
        let names: Vec<&str> = section.checks.iter().map(|c| c.name.as_str()).collect();
        assert!(names.contains(&"rustc"));
        assert!(names.contains(&"cargo"));
    }

    #[test]
    fn toolchain_generic_empty() {
        let section = check_toolchain("generic");
        assert!(section.checks.is_empty());
    }

    #[test]
    fn dependencies_rust_checks() {
        let dir = tempfile::tempdir().unwrap();
        std::fs::write(dir.path().join("Cargo.toml"), "").unwrap();
        let section = check_dependencies("rust", dir.path());
        assert_eq!(section.name, "Dependencies");
        assert_eq!(section.checks.len(), 2);
    }

    #[test]
    fn git_checks_not_repo() {
        let dir = tempfile::tempdir().unwrap();
        let section = check_git(dir.path());
        assert_eq!(section.name, "Git");
        // Should have git tool check + repo check at minimum
        assert!(section.checks.len() >= 2);
        let repo_check = section
            .checks
            .iter()
            .find(|c| c.name == "repository")
            .unwrap();
        assert_eq!(repo_check.status, CheckStatus::Missing);
    }

    #[test]
    fn extract_version_java() {
        // java -version outputs to stderr typically
        let v = extract_version("openjdk version \"17.0.10\" 2024-01-16");
        // The quotes make this tricky — test the pattern
        assert!(v.is_some() || v.is_none()); // graceful either way
    }

    #[test]
    fn section_serializes_to_json() {
        let report = DoctorReport {
            project_type: "rust".to_string(),
            sections: vec![Section {
                name: "Toolchain".to_string(),
                checks: vec![CheckResult {
                    name: "rustc".to_string(),
                    status: CheckStatus::Ok,
                    version: Some("1.78.0".to_string()),
                    detail: None,
                    fix: None,
                }],
            }],
            passed: 1,
            failed: 0,
        };
        let json = serde_json::to_string_pretty(&report).unwrap();
        assert!(json.contains("\"rustc\""));
        assert!(json.contains("\"ok\""));
    }
}
