a3s 0.8.2

a3s — A3S coding agent CLI; `a3s code` launches the interactive TUI
//! Search-engine and headless-browser runtime management.

use std::path::Path;

use a3s_search::browser_management::{
    browser_status, browser_statuses, install_browser, repair_browser, update_browser,
    BrowserRuntimeStatus, ManagedBrowser,
};

const HTTP_ENGINES: &[(&str, &str)] = &[
    ("ddg", "DuckDuckGo"),
    ("brave", "Brave Search"),
    ("wiki", "Wikipedia"),
    ("sogou", "Sogou"),
    ("360 / so360", "360 Search"),
];
const HEADLESS_ENGINES: &[(&str, &str, ManagedBrowser)] = &[
    ("google / g", "Google", ManagedBrowser::Chrome),
    ("baidu", "Baidu", ManagedBrowser::Chrome),
    ("bing_cn", "Bing China", ManagedBrowser::Chrome),
];

pub(crate) fn usage_text() -> &'static str {
    "usage:\n\
       a3s search status\n\
       a3s search engines\n\
       a3s search doctor\n\
       a3s search browser list\n\
       a3s search browser install <chrome|lightpanda>\n\
       a3s search browser update <chrome|lightpanda>\n\
       a3s search browser repair <chrome|lightpanda>\n"
}

pub(crate) async fn run(args: Vec<String>) -> anyhow::Result<()> {
    match args.as_slice() {
        [] => {
            print!("{}", usage_text());
            Ok(())
        }
        [arg] if matches!(arg.as_str(), "-h" | "--help" | "help") => {
            print!("{}", usage_text());
            Ok(())
        }
        [command] if command == "status" => {
            print_statuses();
            Ok(())
        }
        [command] if command == "engines" => {
            print_engines();
            Ok(())
        }
        [command] if command == "doctor" => doctor(),
        [family, command] if family == "browser" && command == "list" => {
            print_statuses();
            Ok(())
        }
        [family, action, browser] if family == "browser" => {
            let browser = parse_browser(browser)?;
            let status = match action.as_str() {
                "install" => install_browser(browser).await?,
                "update" => update_browser(browser).await?,
                "repair" => repair_browser(browser).await?,
                _ => return Err(usage_error(format!("unknown browser action '{action}'"))),
            };
            println!("{} is ready", browser.as_str());
            print_browser_status(&status);
            Ok(())
        }
        _ => Err(usage_error("invalid search command")),
    }
}

fn usage_error(message: impl std::fmt::Display) -> anyhow::Error {
    anyhow::anyhow!("{message}\n\n{}", usage_text())
}

fn parse_browser(value: &str) -> anyhow::Result<ManagedBrowser> {
    match value {
        "chrome" | "chromium" => Ok(ManagedBrowser::Chrome),
        "lightpanda" => Ok(ManagedBrowser::Lightpanda),
        _ => Err(usage_error(format!(
            "unknown browser '{value}'; choose chrome or lightpanda"
        ))),
    }
}

fn print_engines() {
    println!("HTTP engines (no browser required)");
    for (id, name) in HTTP_ENGINES {
        println!("  {id:<14} {name}");
    }
    println!("\nHeadless engines");
    for (id, name, browser) in HEADLESS_ENGINES {
        println!("  {id:<14} {name} ({})", browser.as_str());
    }
    println!("\nBrowser backends");
    println!("  chrome         Chrome/Chromium via CDP");
    println!("  lightpanda     Lightpanda via CDP (select with config.acl headless.backend)");
}

fn print_statuses() {
    println!("headless browser runtimes");
    for status in browser_statuses() {
        print_browser_status(&status);
    }
}

pub(crate) fn print_browser_status(status: &BrowserRuntimeStatus) {
    println!("  {}", status.browser.as_str());
    println!(
        "    available: {}",
        if status.available { "yes" } else { "no" }
    );
    println!("    source:    {:?}", status.source);
    println!(
        "    version:   {}",
        status.version.as_deref().unwrap_or("-")
    );
    println!(
        "    path:      {}",
        status
            .path
            .as_deref()
            .map(Path::display)
            .map(|path| path.to_string())
            .unwrap_or_else(|| "-".to_string())
    );
    println!("    health:    {}", status.detail);
}

fn doctor() -> anyhow::Result<()> {
    print_statuses();
    let config_path = crate::config::find_config();
    let Some(config_path) = config_path else {
        println!("\nconfig: not found");
        println!(
            "result: search HTTP engines remain available; no configured headless engine to check"
        );
        return Ok(());
    };
    let config = a3s_code_core::config::CodeConfig::from_file(Path::new(&config_path))
        .map_err(|error| anyhow::anyhow!("failed to parse {config_path}: {error}"))?;
    println!("\nconfig: {config_path}");
    let Some(search) = config.search else {
        println!("result: no search block; web_search uses its HTTP defaults");
        return Ok(());
    };
    let requested = search
        .engines
        .iter()
        .filter(|(_, config)| config.enabled)
        .filter(|(name, _)| matches!(name.as_str(), "g" | "google" | "baidu" | "bing_cn"))
        .map(|(name, _)| name.as_str())
        .collect::<Vec<_>>();
    if requested.is_empty() {
        println!("result: no enabled headless engines; HTTP search is ready");
        return Ok(());
    }
    let backend = search
        .headless
        .as_ref()
        .map(|headless| headless.backend)
        .unwrap_or_default();
    let browser = if backend.is_lightpanda() {
        ManagedBrowser::Lightpanda
    } else {
        ManagedBrowser::Chrome
    };
    let status = browser_status(browser);
    println!("enabled headless engines: {}", requested.join(", "));
    println!("selected browser backend: {}", browser.as_str());
    if search.headless.is_none() {
        return Err(anyhow::anyhow!(
            "headless engines are enabled but search.headless is missing; add it to config.acl or use HTTP engines"
        ));
    }
    if !status.available {
        return Err(anyhow::anyhow!(
            "{} is unavailable; run `a3s search browser install {}`",
            browser.as_str(),
            browser.as_str()
        ));
    }
    println!("result: ready");
    Ok(())
}

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

    #[test]
    fn browser_parser_accepts_only_supported_backends() {
        assert_eq!(parse_browser("chrome").unwrap(), ManagedBrowser::Chrome);
        assert_eq!(
            parse_browser("lightpanda").unwrap(),
            ManagedBrowser::Lightpanda
        );
        assert!(parse_browser("firefox").is_err());
    }

    #[test]
    fn usage_lists_the_complete_browser_lifecycle() {
        let usage = usage_text();
        for action in ["list", "install", "update", "repair"] {
            assert!(usage.contains(action));
        }
    }
}