fusible 0.4.0

Electronic linter: a fuse that blows to block firmware code generation
//! Subcommand: `fusible doctor` / `setup` — inspect toolchain health and available runners.

use std::path::Path;
use std::process::Command;

use miette::Diagnostic;
use thiserror::Error;

use crate::config::FusibleConfig;
use crate::tscircuit::detect::find_binary_in_path;
use crate::tscircuit::Runner;

/// Errors that can occur during toolchain inspection or automated installation.
#[derive(Debug, Error, Diagnostic)]
pub enum DoctorError {
    #[error("failed to run package manager: {command}")]
    #[diagnostic(
        code(fusible::doctor::install_failed),
        help(
            "verify your internet connection or install manually with `npm install -g tscircuit`"
        )
    )]
    InstallFailed {
        command: String,
        #[source]
        source: std::io::Error,
    },

    #[error("package manager command `{command}` exited with failure ({code:?})")]
    #[diagnostic(
        code(fusible::doctor::install_non_zero),
        help("check stderr output for permission or network issues")
    )]
    InstallNonZero { command: String, code: Option<i32> },

    #[error("no supported package manager found to install tscircuit")]
    #[diagnostic(
        code(fusible::doctor::no_package_manager),
        help("install bun (https://bun.sh) or node/npm (https://nodejs.org) first")
    )]
    NoPackageManager,
}

fn report_prerequisites() -> (bool, bool) {
    let path_var = std::env::var_os("PATH");
    let has_bun = path_var
        .as_deref()
        .and_then(|p| find_binary_in_path("bun", p));
    let has_node = path_var
        .as_deref()
        .and_then(|p| find_binary_in_path("node", p));
    let has_npm = path_var
        .as_deref()
        .and_then(|p| find_binary_in_path("npm", p));
    let has_tsci = path_var.as_deref().and_then(|p| {
        find_binary_in_path("tsci", p).or_else(|| find_binary_in_path("tscircuit", p))
    });

    println!("Runtime prerequisites:");
    print_status("tsci (tscircuit)", has_tsci.as_deref());
    print_status("bun", has_bun.as_deref());
    print_status("node", has_node.as_deref());
    print_status("npm", has_npm.as_deref());
    (has_bun.is_some(), has_npm.is_some())
}

fn report_manifest() {
    println!("\nMonorepo configuration:");
    match FusibleConfig::find_and_load(Path::new(".")) {
        Ok(Some((path, cfg))) => {
            let mcu = cfg
                .mcu
                .and_then(|m| m.chip)
                .unwrap_or_else(|| "none".into());
            let entry = cfg
                .hardware
                .and_then(|h| h.entry)
                .map(|e| e.display().to_string())
                .unwrap_or_else(|| "default".into());
            println!("  ✅ Found manifest at \"{}\"", path.display());
            println!("     • entry: {entry}");
            println!("     • silicon target: {mcu}");
        }
        Ok(None) => {
            println!("  ℹ️  No fusible.yml found in current directory (run `fusible init` to create one)");
        }
        Err(e) => {
            println!("  ⚠️  Manifest error: {e}");
        }
    }
}

fn report_runner(install: bool, has_bun: bool, has_npm: bool) -> Result<(), DoctorError> {
    println!("\ntscircuit runner status:");
    match Runner::detect() {
        Ok(runner) => {
            println!(
                "  ✅ Ready — active backend: {}",
                runner.kind().description()
            );
        }
        Err(_) => {
            println!("  ⚠️  No active runner detected.");
            println!(
                "     Fusible can still lint pre-existing circuit.json files, but TSX compilation,"
            );
            println!("     dev webview, and exports require tscircuit.");

            if install {
                println!("\n🔧 Installing tscircuit CLI...");
                install_tscircuit(has_bun, has_npm)?;
                println!("✅ tscircuit installed successfully!");
            } else {
                println!(
                    "\nTip: Run `fusible doctor --install` to automatically install tscircuit,"
                );
                println!("     or run: `npm install -g tscircuit` / `bun add -g tscircuit`");
            }
        }
    }
    Ok(())
}

/// Runs the environment diagnostics and optionally installs missing tools.
pub fn run(install: bool) -> Result<(), DoctorError> {
    println!("🩺 Fusible Toolchain Doctor\n");
    let (has_bun, has_npm) = report_prerequisites();
    report_manifest();
    report_runner(install, has_bun, has_npm)?;
    println!();
    Ok(())
}

fn print_status(name: &str, path: Option<&Path>) {
    match path {
        Some(p) => println!("  ✅ {name:<14} -> {}", p.display()),
        None => println!("  ⚪ {name:<14} -> not found"),
    }
}

fn install_tscircuit(has_bun: bool, has_npm: bool) -> Result<(), DoctorError> {
    let (prog, args): (&str, &[&str]) = if has_bun {
        ("bun", &["add", "-g", "tscircuit"])
    } else if has_npm {
        ("npm", &["install", "-g", "tscircuit"])
    } else {
        return Err(DoctorError::NoPackageManager);
    };

    println!("   Executing: {prog} {}", args.join(" "));
    let status =
        Command::new(prog)
            .args(args)
            .status()
            .map_err(|source| DoctorError::InstallFailed {
                command: format!("{prog} {}", args.join(" ")),
                source,
            })?;

    if !status.success() {
        return Err(DoctorError::InstallNonZero {
            command: format!("{prog} {}", args.join(" ")),
            code: status.code(),
        });
    }

    Ok(())
}