use anyhow::{bail, Result};
use fission_command_core::Target;
use std::env;
use std::ffi::OsStr;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum Status {
    Ok,
    Warn,
    Error,
}

#[derive(Debug)]
struct Check {
    area: &'static str,
    name: String,
    status: Status,
    detail: String,
    suggestion: Option<String>,
}

impl Check {
    fn ok(area: &'static str, name: impl Into<String>, detail: impl Into<String>) -> Self {
        Self {
            area,
            name: name.into(),
            status: Status::Ok,
            detail: detail.into(),
            suggestion: None,
        }
    }

    fn warn(
        area: &'static str,
        name: impl Into<String>,
        detail: impl Into<String>,
        suggestion: impl Into<String>,
    ) -> Self {
        Self {
            area,
            name: name.into(),
            status: Status::Warn,
            detail: detail.into(),
            suggestion: Some(suggestion.into()),
        }
    }

    fn error(
        area: &'static str,
        name: impl Into<String>,
        detail: impl Into<String>,
        suggestion: impl Into<String>,
    ) -> Self {
        Self {
            area,
            name: name.into(),
            status: Status::Error,
            detail: detail.into(),
            suggestion: Some(suggestion.into()),
        }
    }
}

pub fn run_doctor(project_dir: &Path, targets: &[Target], strict: bool) -> Result<()> {
    let targets = normalized_targets(targets);
    let mut checks = Vec::new();

    checks.extend(check_project(project_dir, &targets));
    checks.extend(check_rust_targets(&targets));

    if targets.contains(&Target::Web) {
        checks.extend(check_web());
    }
    if targets.contains(&Target::Android) {
        checks.extend(check_android());
    }
    if targets.contains(&Target::Ios) {
        checks.extend(check_ios());
    }

    print_checks(project_dir, &targets, &checks);

    if strict && checks.iter().any(|check| check.status == Status::Error) {
        bail!("fission doctor found required toolchain errors");
    }

    Ok(())
}

fn normalized_targets(targets: &[Target]) -> Vec<Target> {
    if targets.is_empty() {
        return vec![Target::Web, Target::Ios, Target::Android];
    }

    targets.iter().copied().collect()
}

fn check_project(project_dir: &Path, targets: &[Target]) -> Vec<Check> {
    let mut checks = Vec::new();
    let fission_toml = project_dir.join("fission.toml");
    if fission_toml.exists() {
        checks.push(Check::ok(
            "project",
            "fission.toml",
            format!("found {}", fission_toml.display()),
        ));
    } else {
        checks.push(Check::warn(
            "project",
            "fission.toml",
            format!("{} does not exist", fission_toml.display()),
            "run doctor from a Fission app root, or pass --project-dir <path>",
        ));
    }

    for target in targets {
        let path = project_dir.join(target.scaffold_relative_path());
        if path.exists() {
            checks.push(Check::ok(
                "project",
                format!("{} scaffold", target.as_str()),
                format!("found {}", path.display()),
            ));
        } else {
            checks.push(Check::warn(
                "project",
                format!("{} scaffold", target.as_str()),
                format!("{} does not exist", path.display()),
                format!(
                    "run `fission add-target {} --project-dir {}`",
                    target.as_str(),
                    project_dir.display()
                ),
            ));
        }
    }

    checks
}

fn check_rust_targets(targets: &[Target]) -> Vec<Check> {
    let mut checks = Vec::new();
    let Some(rustup) = find_in_path("rustup") else {
        checks.push(Check::error(
            "rust",
            "rustup",
            "rustup is not on PATH",
            "install Rust from https://rustup.rs/ and restart your shell",
        ));
        return checks;
    };

    checks.push(Check::ok(
        "rust",
        "rustup",
        format!("found {}", rustup.display()),
    ));

    let installed = command_stdout(&rustup, ["target", "list", "--installed"])
        .unwrap_or_else(|_| String::new());
    for rust_target in required_rust_targets(targets) {
        if installed.lines().any(|line| line.trim() == rust_target) {
            checks.push(Check::ok(
                "rust",
                rust_target,
                "target is installed".to_string(),
            ));
        } else {
            checks.push(Check::error(
                "rust",
                rust_target,
                "target is not installed".to_string(),
                format!("run `rustup target add {}`", rust_target),
            ));
        }
    }

    checks
}

fn required_rust_targets(targets: &[Target]) -> Vec<&'static str> {
    let mut required = Vec::new();
    if targets.contains(&Target::Web) {
        required.push("wasm32-unknown-unknown");
    }
    if targets.contains(&Target::Android) {
        required.push("aarch64-linux-android");
    }
    if targets.contains(&Target::Ios) {
        required.push("aarch64-apple-ios");
        required.push("aarch64-apple-ios-sim");
    }
    required
}

fn check_web() -> Vec<Check> {
    let mut checks = Vec::new();
    check_command(
        &mut checks,
        "web",
        "wasm-pack",
        "install with `cargo install wasm-pack`",
    );
    check_command(
        &mut checks,
        "web",
        "python3",
        "install Python 3 so generated web scripts can serve the app locally",
    );
    check_node_websocket(&mut checks);

    match detect_chrome() {
        Some(path) => checks.push(Check::ok(
            "web",
            "Chrome/Chromium",
            format!("found {}", path.display()),
        )),
        None => checks.push(Check::warn(
            "web",
            "Chrome/Chromium",
            "no Chrome-compatible browser was detected for headless CDP smoke tests",
            "install Chrome/Chromium, or set FISSION_CHROME=/path/to/chrome",
        )),
    }

    checks
}

fn check_node_websocket(checks: &mut Vec<Check>) {
    let Some(node) = find_in_path("node") else {
        checks.push(Check::error(
            "web",
            "Node.js",
            "node is not on PATH",
            "install Node 22+ so generated browser smoke tests can inspect Chrome CDP console/runtime errors",
        ));
        return;
    };

    let supports_websocket = Command::new(&node)
        .args([
            "-e",
            "process.exit(typeof WebSocket === 'function' ? 0 : 1)",
        ])
        .status()
        .map(|status| status.success())
        .unwrap_or(false);
    if supports_websocket {
        checks.push(Check::ok(
            "web",
            "Node.js WebSocket",
            format!("found {}", node.display()),
        ));
    } else {
        checks.push(Check::error(
            "web",
            "Node.js WebSocket",
            format!(
                "{} does not expose the built-in WebSocket client",
                node.display()
            ),
            "install Node 22+ for Chrome CDP smoke tests",
        ));
    }
}

fn check_android() -> Vec<Check> {
    let mut checks = Vec::new();
    let android_home = detect_android_home();
    if android_home.is_dir() {
        checks.push(Check::ok(
            "android",
            "ANDROID_HOME",
            format!("using {}", android_home.display()),
        ));
    } else {
        checks.push(Check::error(
            "android",
            "ANDROID_HOME",
            format!("{} does not exist", android_home.display()),
            "install Android Studio/SDK, or set ANDROID_HOME to your SDK path",
        ));
        return checks;
    }

    check_file(
        &mut checks,
        "android",
        "adb",
        android_home.join("platform-tools/adb"),
        "install Android platform-tools with sdkmanager",
    );
    check_file(
        &mut checks,
        "android",
        "emulator",
        android_home.join("emulator/emulator"),
        "install the Android emulator package with sdkmanager",
    );
    check_file(
        &mut checks,
        "android",
        "sdkmanager",
        android_home.join("cmdline-tools/latest/bin/sdkmanager"),
        "install Android SDK command-line tools",
    );
    check_file(
        &mut checks,
        "android",
        "avdmanager",
        android_home.join("cmdline-tools/latest/bin/avdmanager"),
        "install Android SDK command-line tools",
    );

    let platforms_dir = android_home.join("platforms");
    match latest_android_api(&platforms_dir) {
        Some(api) => checks.push(Check::ok(
            "android",
            "Android platform",
            format!("latest installed API level is {}", api),
        )),
        None => checks.push(Check::error(
            "android",
            "Android platform",
            format!("no platforms were found under {}", platforms_dir.display()),
            "install one with `sdkmanager \"platforms;android-35\"` or newer",
        )),
    }

    match latest_android_system_image_api(&android_home) {
        Some(api) => checks.push(Check::ok(
            "android",
            "Android emulator image",
            format!("latest installed google_apis arm64 image is API {}", api),
        )),
        None => checks.push(Check::warn(
            "android",
            "Android emulator image",
            format!(
                "no google_apis arm64 emulator image found under {}",
                android_home.join("system-images").display()
            ),
            "install one with `sdkmanager \"system-images;android-35;google_apis;arm64-v8a\"` or set ANDROID_SYSTEM_IMAGE",
        )),
    }

    let build_tools_dir = android_home.join("build-tools");
    match latest_child_dir(&build_tools_dir) {
        Some(path) => checks.push(Check::ok(
            "android",
            "build-tools",
            format!("using {}", path.display()),
        )),
        None => checks.push(Check::error(
            "android",
            "build-tools",
            format!(
                "no build-tools were found under {}",
                build_tools_dir.display()
            ),
            "install build-tools with `sdkmanager \"build-tools;35.0.0\"` or newer",
        )),
    }

    let ndk = detect_android_ndk(&android_home);
    if ndk.is_dir() {
        checks.push(Check::ok(
            "android",
            "Android NDK",
            format!("using {}", ndk.display()),
        ));
    } else {
        checks.push(Check::error(
            "android",
            "Android NDK",
            format!("{} does not exist", ndk.display()),
            "install the NDK with sdkmanager, or set ANDROID_NDK to an installed NDK path",
        ));
        return checks;
    }

    match detect_android_toolchain(&ndk) {
        Some(toolchain) => {
            checks.push(Check::ok(
                "android",
                "NDK LLVM toolchain",
                format!("using {}", toolchain.display()),
            ));
            let min_api = env::var("ANDROID_MIN_API_LEVEL").unwrap_or_else(|_| "24".to_string());
            let clang = toolchain.join(format!("aarch64-linux-android{}-clang", min_api));
            if clang.exists() {
                checks.push(Check::ok(
                    "android",
                    "aarch64 clang",
                    format!("found {}", clang.display()),
                ));
            } else {
                checks.push(Check::error(
                    "android",
                    "aarch64 clang",
                    format!("{} does not exist", clang.display()),
                    "set ANDROID_MIN_API_LEVEL to an API supported by the installed NDK",
                ));
            }
        }
        None => checks.push(Check::error(
            "android",
            "NDK LLVM toolchain",
            format!(
                "no prebuilt LLVM toolchain found under {}",
                ndk.join("toolchains/llvm/prebuilt").display()
            ),
            "set ANDROID_TOOLCHAIN to the NDK LLVM bin directory",
        )),
    }

    checks
}

fn check_ios() -> Vec<Check> {
    let mut checks = Vec::new();
    if !cfg!(target_os = "macos") {
        checks.push(Check::error(
            "ios",
            "macOS host",
            "iOS builds require macOS with Xcode",
            "run iOS packaging on macOS",
        ));
        return checks;
    }

    check_command(
        &mut checks,
        "ios",
        "xcrun",
        "install Xcode and run `xcode-select --install` if command-line tools are missing",
    );

    match command_stdout("xcrun", ["--sdk", "iphonesimulator", "--show-sdk-path"]) {
        Ok(path) if !path.trim().is_empty() => checks.push(Check::ok(
            "ios",
            "iPhoneSimulator SDK",
            path.trim().to_string(),
        )),
        _ => checks.push(Check::error(
            "ios",
            "iPhoneSimulator SDK",
            "xcrun could not resolve the iPhoneSimulator SDK",
            "install Xcode and open it once to finish component installation",
        )),
    }

    match command_stdout("xcrun", ["simctl", "list", "devices", "available", "-j"]) {
        Ok(output) if output.contains("iPhone") => checks.push(Check::ok(
            "ios",
            "iPhone simulator",
            "at least one available iPhone simulator is installed",
        )),
        Ok(_) => checks.push(Check::error(
            "ios",
            "iPhone simulator",
            "simctl did not report any available iPhone simulator",
            "install an iOS simulator runtime from Xcode Settings > Platforms",
        )),
        Err(_) => checks.push(Check::error(
            "ios",
            "simctl",
            "xcrun simctl is not available",
            "install Xcode and ensure xcrun is on PATH",
        )),
    }

    checks
}

fn check_command(checks: &mut Vec<Check>, area: &'static str, name: &str, suggestion: &str) {
    match find_in_path(name) {
        Some(path) => checks.push(Check::ok(area, name, format!("found {}", path.display()))),
        None => checks.push(Check::error(
            area,
            name,
            format!("{} is not on PATH", name),
            suggestion,
        )),
    }
}

fn check_file(
    checks: &mut Vec<Check>,
    area: &'static str,
    name: &str,
    path: PathBuf,
    suggestion: &str,
) {
    if path.exists() {
        checks.push(Check::ok(area, name, format!("found {}", path.display())));
    } else {
        checks.push(Check::error(
            area,
            name,
            format!("{} does not exist", path.display()),
            suggestion,
        ));
    }
}

fn print_checks(project_dir: &Path, targets: &[Target], checks: &[Check]) {
    println!("Fission doctor");
    println!("Project: {}", project_dir.display());
    println!(
        "Targets: {}",
        targets
            .iter()
            .map(|target| target.as_str())
            .collect::<Vec<_>>()
            .join(", ")
    );
    println!();

    for check in checks {
        let marker = match check.status {
            Status::Ok => "[ok]",
            Status::Warn => "[warn]",
            Status::Error => "[error]",
        };
        println!(
            "{} {} - {}: {}",
            marker, check.area, check.name, check.detail
        );
        if let Some(suggestion) = &check.suggestion {
            println!("       suggestion: {}", suggestion);
        }
    }

    let ok = checks
        .iter()
        .filter(|check| check.status == Status::Ok)
        .count();
    let warn = checks
        .iter()
        .filter(|check| check.status == Status::Warn)
        .count();
    let error = checks
        .iter()
        .filter(|check| check.status == Status::Error)
        .count();
    println!();
    println!("Summary: {} ok, {} warnings, {} errors", ok, warn, error);
}

fn command_stdout<I, S>(program: impl AsRef<OsStr>, args: I) -> Result<String>
where
    I: IntoIterator<Item = S>,
    S: AsRef<OsStr>,
{
    let output = Command::new(program).args(args).output()?;
    if !output.status.success() {
        bail!("command exited with {}", output.status);
    }
    Ok(String::from_utf8_lossy(&output.stdout).to_string())
}

fn find_in_path(name: &str) -> Option<PathBuf> {
    let explicit = Path::new(name);
    if explicit.components().count() > 1 && explicit.exists() {
        return Some(explicit.to_path_buf());
    }

    let path = env::var_os("PATH")?;
    for dir in env::split_paths(&path) {
        let candidate = dir.join(name);
        if candidate.exists() {
            return Some(candidate);
        }
        #[cfg(windows)]
        {
            let candidate = dir.join(format!("{}.exe", name));
            if candidate.exists() {
                return Some(candidate);
            }
        }
    }
    None
}

fn detect_chrome() -> Option<PathBuf> {
    for var in ["FISSION_CHROME", "CHROME", "CHROME_BIN"] {
        if let Ok(value) = env::var(var) {
            let path = PathBuf::from(value);
            if path.exists() {
                return Some(path);
            }
        }
    }

    for name in ["google-chrome", "chromium", "chromium-browser", "chrome"] {
        if let Some(path) = find_in_path(name) {
            return Some(path);
        }
    }

    for path in [
        "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
        "/Applications/Chromium.app/Contents/MacOS/Chromium",
        "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
    ] {
        let path = PathBuf::from(path);
        if path.exists() {
            return Some(path);
        }
    }

    None
}

fn detect_android_home() -> PathBuf {
    if let Ok(value) = env::var("ANDROID_HOME").or_else(|_| env::var("ANDROID_SDK_ROOT")) {
        return PathBuf::from(value);
    }
    default_android_home()
}

fn default_android_home() -> PathBuf {
    let home = env::var_os("HOME")
        .or_else(|| env::var_os("USERPROFILE"))
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from("."));

    if cfg!(target_os = "macos") {
        home.join("Library/Android/sdk")
    } else if cfg!(target_os = "windows") {
        env::var_os("LOCALAPPDATA")
            .map(PathBuf::from)
            .unwrap_or(home)
            .join("Android/Sdk")
    } else {
        home.join("Android/Sdk")
    }
}

fn detect_android_ndk(android_home: &Path) -> PathBuf {
    if let Ok(value) = env::var("ANDROID_NDK").or_else(|_| env::var("ANDROID_NDK_HOME")) {
        return PathBuf::from(value);
    }
    latest_child_dir(&android_home.join("ndk")).unwrap_or_else(|| android_home.join("ndk"))
}

fn detect_android_toolchain(ndk: &Path) -> Option<PathBuf> {
    if let Ok(value) = env::var("ANDROID_TOOLCHAIN") {
        let path = PathBuf::from(value);
        if path.exists() {
            return Some(path);
        }
    }

    let prebuilt = ndk.join("toolchains/llvm/prebuilt");
    for host in android_host_candidates() {
        let path = prebuilt.join(host).join("bin");
        if path.exists() {
            return Some(path);
        }
    }

    latest_child_dir(&prebuilt).map(|path| path.join("bin"))
}

fn android_host_candidates() -> Vec<&'static str> {
    if cfg!(target_os = "macos") {
        vec!["darwin-aarch64", "darwin-x86_64"]
    } else if cfg!(target_os = "windows") {
        vec!["windows-x86_64"]
    } else {
        vec!["linux-x86_64"]
    }
}

fn latest_android_api(platforms_dir: &Path) -> Option<u32> {
    let mut apis = Vec::new();
    for entry in fs::read_dir(platforms_dir).ok()? {
        let entry = entry.ok()?;
        let name = entry.file_name();
        let name = name.to_string_lossy();
        if let Some(value) = name.strip_prefix("android-") {
            if let Ok(api) = value.parse::<u32>() {
                apis.push(api);
            }
        }
    }
    apis.into_iter().max()
}

fn latest_android_system_image_api(android_home: &Path) -> Option<u32> {
    let root = android_home.join("system-images");
    let mut apis = Vec::new();
    for entry in fs::read_dir(&root).ok()? {
        let entry = entry.ok()?;
        let name = entry.file_name();
        let name = name.to_string_lossy();
        let Some(value) = name.strip_prefix("android-") else {
            continue;
        };
        let Ok(api) = value.parse::<u32>() else {
            continue;
        };
        if entry.path().join("google_apis/arm64-v8a").is_dir() {
            apis.push(api);
        }
    }
    apis.into_iter().max()
}

fn latest_child_dir(path: &Path) -> Option<PathBuf> {
    let mut dirs = fs::read_dir(path)
        .ok()?
        .filter_map(|entry| entry.ok().map(|entry| entry.path()))
        .filter(|path| path.is_dir())
        .collect::<Vec<_>>();
    dirs.sort_by(|left, right| {
        version_key(left)
            .cmp(&version_key(right))
            .then_with(|| left.cmp(right))
    });
    dirs.pop()
}

fn version_key(path: &Path) -> Vec<u32> {
    path.file_name()
        .and_then(|name| name.to_str())
        .unwrap_or_default()
        .split(|ch: char| !ch.is_ascii_digit())
        .filter(|part| !part.is_empty())
        .filter_map(|part| part.parse::<u32>().ok())
        .collect()
}
