cargo-bubba 0.1.0

cargo subcommand for the Bubba mobile framework
//! `cargo bubba doctor` — diagnose the developer environment.

use anyhow::Result;
use colored::Colorize;
use std::process::Command;

struct Check {
    name: &'static str,
    status: CheckStatus,
    detail: String,
    fix: Option<&'static str>,
}

enum CheckStatus {
    Ok,
    Warning,
    Error,
}

impl CheckStatus {
    fn icon(&self) -> colored::ColoredString {
        match self {
            CheckStatus::Ok      => "".green().bold(),
            CheckStatus::Warning => "".yellow().bold(),
            CheckStatus::Error   => "".red().bold(),
        }
    }
}

pub fn run() -> Result<()> {
    println!("{}\n", "Checking your Bubba development environment…".bold());

    let checks = vec![
        check_rust(),
        check_cargo(),
        check_rustup(),
        check_android_target("aarch64-linux-android"),
        check_android_target("x86_64-linux-android"),
        check_android_sdk(),
        check_android_ndk(),
        check_aapt2(),
        check_adb(),
        check_java(),
    ];

    let mut errors = 0;
    let mut warnings = 0;

    for check in &checks {
        let status_icon = check.status.icon();
        println!("  {} {}", status_icon, check.name.bold());
        if !check.detail.is_empty() {
            println!("    {}", check.detail.dimmed());
        }
        if let Some(fix) = check.fix {
            println!("    {} {}", "Fix:".yellow(), fix);
        }

        match check.status {
            CheckStatus::Error   => errors += 1,
            CheckStatus::Warning => warnings += 1,
            CheckStatus::Ok      => {}
        }
    }

    println!();
    if errors == 0 && warnings == 0 {
        println!("{} Your environment is ready! Run `cargo bubba build` to compile.\n", "".green().bold());
    } else {
        if errors > 0 {
            println!(
                "{} {} error(s) found. Fix them before building.",
                "".red().bold(), errors
            );
        }
        if warnings > 0 {
            println!(
                "{} {} warning(s) found. Some features may not work.",
                "".yellow().bold(), warnings
            );
        }
        println!();
        println!(
            "  Full setup guide: {}\n",
            "https://bubba-rs.netlify.app/docs/setup".cyan().underline()
        );
    }

    Ok(())
}

// ── Individual checks ─────────────────────────────────────────────────────────

fn check_rust() -> Check {
    match Command::new("rustc").arg("--version").output() {
        Ok(out) => {
            let version = String::from_utf8_lossy(&out.stdout).trim().to_string();
            Check { name: "Rust compiler (rustc)", status: CheckStatus::Ok, detail: version, fix: None }
        }
        Err(_) => Check {
            name: "Rust compiler (rustc)",
            status: CheckStatus::Error,
            detail: "rustc not found in PATH".to_string(),
            fix: Some("curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh"),
        },
    }
}

fn check_cargo() -> Check {
    match Command::new("cargo").arg("--version").output() {
        Ok(out) => {
            let version = String::from_utf8_lossy(&out.stdout).trim().to_string();
            Check { name: "Cargo", status: CheckStatus::Ok, detail: version, fix: None }
        }
        Err(_) => Check {
            name: "Cargo",
            status: CheckStatus::Error,
            detail: "cargo not found".to_string(),
            fix: Some("Install via rustup (comes with Rust)"),
        },
    }
}

fn check_rustup() -> Check {
    match Command::new("rustup").arg("--version").output() {
        Ok(out) => {
            let version = String::from_utf8_lossy(&out.stdout).trim().to_string();
            Check { name: "rustup", status: CheckStatus::Ok, detail: version, fix: None }
        }
        Err(_) => Check {
            name: "rustup",
            status: CheckStatus::Warning,
            detail: "rustup not found — target management may be manual".to_string(),
            fix: Some("https://rustup.rs"),
        },
    }
}

fn check_android_target(target: &'static str) -> Check {
    let name = Box::leak(format!("Rust target: {}", target).into_boxed_str());

    let out = Command::new("rustup").args(["target", "list", "--installed"]).output();
    match out {
        Ok(out) => {
            let installed = String::from_utf8_lossy(&out.stdout);
            if installed.contains(target) {
                Check { name, status: CheckStatus::Ok, detail: "installed".to_string(), fix: None }
            } else {
                let fix = Box::leak(
                    format!("rustup target add {}", target).into_boxed_str()
                );
                Check {
                    name,
                    status: CheckStatus::Error,
                    detail: "not installed".to_string(),
                    fix: Some(fix),
                }
            }
        }
        Err(_) => Check {
            name,
            status: CheckStatus::Warning,
            detail: "Could not check rustup targets".to_string(),
            fix: Some("Ensure rustup is installed"),
        },
    }
}

fn check_android_sdk() -> Check {
    for env_var in &["ANDROID_SDK_ROOT", "ANDROID_HOME"] {
        if let Ok(val) = std::env::var(env_var) {
            if std::path::Path::new(&val).exists() {
                return Check {
                    name: "Android SDK",
                    status: CheckStatus::Ok,
                    detail: format!("{}={}", env_var, val),
                    fix: None,
                };
            }
        }
    }
    Check {
        name: "Android SDK",
        status: CheckStatus::Error,
        detail: "ANDROID_SDK_ROOT / ANDROID_HOME not set or path does not exist".to_string(),
        fix: Some("Install Android Studio → SDK Manager, then set ANDROID_SDK_ROOT"),
    }
}

fn check_android_ndk() -> Check {
    // Check ANDROID_NDK_ROOT
    if let Ok(val) = std::env::var("ANDROID_NDK_ROOT").or_else(|_| std::env::var("NDK_HOME")) {
        if std::path::Path::new(&val).exists() {
            return Check {
                name: "Android NDK",
                status: CheckStatus::Ok,
                detail: format!("NDK found at {}", val),
                fix: None,
            };
        }
    }

    // Check inside SDK
    for env_var in &["ANDROID_SDK_ROOT", "ANDROID_HOME"] {
        if let Ok(sdk) = std::env::var(env_var) {
            let ndk_dir = std::path::PathBuf::from(&sdk).join("ndk");
            if ndk_dir.exists() {
                return Check {
                    name: "Android NDK",
                    status: CheckStatus::Ok,
                    detail: format!("NDK found under {}", sdk),
                    fix: None,
                };
            }
        }
    }

    Check {
        name: "Android NDK",
        status: CheckStatus::Error,
        detail: "Android NDK not found".to_string(),
        fix: Some("Install via Android Studio → SDK Manager → NDK (Side by side), then set ANDROID_NDK_ROOT"),
    }
}

fn check_tool(binary: &'static str, display_name: &'static str, fix: &'static str) -> Check {
    match which::which(binary) {
        Ok(path) => Check {
            name: display_name,
            status: CheckStatus::Ok,
            detail: path.to_string_lossy().into_owned(),
            fix: None,
        },
        Err(_) => Check {
            name: display_name,
            status: CheckStatus::Warning,
            detail: format!("`{}` not found in PATH", binary),
            fix: Some(fix),
        },
    }
}

fn check_aapt2() -> Check {
    check_tool(
        "aapt2",
        "aapt2 (Android Asset Packaging Tool)",
        "Install Android Build Tools via SDK Manager",
    )
}

fn check_adb() -> Check {
    check_tool(
        "adb",
        "adb (Android Debug Bridge)",
        "Install Android Platform Tools via SDK Manager",
    )
}

fn check_java() -> Check {
    match Command::new("java").arg("-version").output() {
        Ok(_) => Check {
            name: "Java (required for apksigner)",
            status: CheckStatus::Ok,
            detail: "java found".to_string(),
            fix: None,
        },
        Err(_) => Check {
            name: "Java (required for apksigner)",
            status: CheckStatus::Warning,
            detail: "java not found — APK signing may fail".to_string(),
            fix: Some("Install JDK 17+: https://adoptium.net"),
        },
    }
}