a3s 0.8.0

a3s — A3S coding agent CLI; `a3s code` launches the interactive TUI
use a3s_code_core::config::CodeConfig;
use std::path::Path;

use crate::config;

use super::catalog::ModelCatalog;
use super::route::ModelRoute;
use super::selection::{self, ModelSelection};

pub(crate) const USAGE: &str = "usage:\n\
  a3s model list\n\
  a3s model current\n\
  a3s model use <route>\n\
  a3s model reset\n\
  a3s model config\n\n\
Routes from config.acl use provider/model. Account routes use\n\
claude-code/model, codex/model, workbuddy/model, or a3s-os/model.\n";

pub(crate) async fn run(args: &[String]) -> anyhow::Result<()> {
    match args.first().map(String::as_str) {
        None | Some("list") => {
            ensure_exact_args("model list", &args[usize::from(!args.is_empty())..], 0)?;
            list().await
        }
        Some("current") => {
            ensure_exact_args("model current", &args[1..], 0)?;
            current()
        }
        Some("use") => {
            ensure_exact_args("model use", &args[1..], 1)?;
            use_route(&args[1]).await
        }
        Some("reset") => {
            ensure_exact_args("model reset", &args[1..], 0)?;
            reset()
        }
        Some("config") => {
            ensure_exact_args("model config", &args[1..], 0)?;
            match config::find_config() {
                Some(path) => println!("{path}"),
                None => println!(
                    "{}",
                    config::default_config_path()
                        .map(|path| path.display().to_string())
                        .unwrap_or_else(|| "(HOME is not set)".to_string())
                ),
            }
            Ok(())
        }
        Some("-h" | "--help" | "help") if args.len() == 1 => {
            print!("{USAGE}");
            Ok(())
        }
        Some(other) => anyhow::bail!("unknown model command `{other}`\n\n{USAGE}"),
    }
}

async fn list() -> anyhow::Result<()> {
    let catalog = ModelCatalog::discover(true).await;
    let current = selection::load()
        .and_then(|selection| selection.route().ok())
        .or_else(|| catalog.config_default.as_deref()?.parse().ok());
    if catalog.entries.is_empty() {
        println!("No models available.");
    } else {
        println!("ROUTE\tSOURCE\tNAME\tCAPABILITIES");
        for entry in &catalog.entries {
            let marker = if current.as_ref() == Some(&entry.route) {
                "*"
            } else {
                " "
            };
            let mut capabilities = Vec::new();
            if entry.reasoning {
                capabilities.push("reasoning".to_string());
            }
            if entry.tool_call {
                capabilities.push("tools".to_string());
            }
            if let Some(context) = entry.context_window {
                capabilities.push(format!("context={context}"));
            }
            println!(
                "{marker} {}\t{}\t{}\t{}",
                entry.route,
                entry.route.source.label(),
                entry.display_name,
                capabilities.join(",")
            );
        }
    }
    for warning in catalog.warnings {
        eprintln!("warning: {warning}");
    }
    Ok(())
}

fn current() -> anyhow::Result<()> {
    match selection::load() {
        Some(selection) => println!("{}", selection.route()?),
        None => match configured_default() {
            Some(default) => println!("{default} (config.acl default)"),
            None => println!("(not selected)"),
        },
    }
    Ok(())
}

async fn use_route(value: &str) -> anyhow::Result<()> {
    let route: ModelRoute = value.parse()?;
    if !ModelCatalog::route_available(&route).await {
        anyhow::bail!(
            "model route `{route}` is not available; run `a3s model list` to inspect available routes"
        );
    }
    selection::save(&ModelSelection::from(route.clone()))?;
    println!("Active model: {route}");
    println!("Controller: A3S Code");
    Ok(())
}

fn reset() -> anyhow::Result<()> {
    let removed = selection::reset()?;
    match configured_default() {
        Some(default) => println!("Active model: {default} (config.acl default)"),
        None if removed => println!("Model selection cleared; no config.acl default is set."),
        None => println!("No saved model selection; no config.acl default is set."),
    }
    Ok(())
}

fn configured_default() -> Option<String> {
    let path = config::find_config()?;
    CodeConfig::from_file(Path::new(&path)).ok()?.default_model
}

fn ensure_exact_args(command: &str, args: &[String], expected: usize) -> anyhow::Result<()> {
    if args.len() == expected {
        Ok(())
    } else {
        anyhow::bail!("{command} expects {expected} argument(s)\n\n{USAGE}")
    }
}