//! Command dispatch and shared helpers.

pub mod ask;
pub mod config;
pub mod init;
pub mod lint;
#[cfg(feature = "mcp")]
pub mod mcp;
#[cfg(feature = "web")]
pub mod serve;
pub mod status;
pub mod update;

use crate::cli::{Cli, Command, GlobalArgs};
use crate::config::{Config, Overrides, Paths};
use crate::onboarding;
use crate::progress_term::Verbosity;
use crate::util;
use anyhow::{bail, Context, Result};
use indicatif::{ProgressBar, ProgressStyle};
use std::future::Future;
use std::path::{Path, PathBuf};
use std::time::Duration;

pub async fn run(cli: Cli) -> Result<()> {
    match cli.command {
        Command::Init(a) => init::run(&cli.global, a).await,
        Command::Update(a) => update::run(&cli.global, a).await,
        Command::Ask(a) => ask::run(&cli.global, a).await,
        Command::Lint(a) => lint::run(&cli.global, a).await,
        Command::Status(a) => status::run(&cli.global, a).await,
        Command::Config(a) => config::run(a).await,
        Command::Providers => config::providers().await,
        Command::Login(a) => config::login(&cli.global, a).await,
        #[cfg(feature = "mcp")]
        Command::Mcp(a) => mcp::run(&cli.global, a).await,
        #[cfg(feature = "web")]
        Command::Serve(a) => serve::run(&cli.global, a).await,
    }
}

/// Resolve a repo path argument to an absolute directory.
pub fn repo_root(path: Option<PathBuf>) -> Result<PathBuf> {
    let p = match path {
        Some(p) => p,
        None => std::env::current_dir().context("getting current directory")?,
    };
    let abs = if p.is_absolute() {
        p
    } else {
        std::env::current_dir()?.join(p)
    };
    if !abs.is_dir() {
        bail!("not a directory: {}", abs.display());
    }
    // Best-effort canonicalization (keeps symlinks tidy); fall back if it fails.
    Ok(std::fs::canonicalize(&abs).unwrap_or(abs))
}

/// Build base overrides from the global flags.
pub fn base_overrides(g: &GlobalArgs) -> Overrides {
    Overrides {
        provider: g.provider,
        model: g.model.clone(),
        wiki_dir: g.wiki_dir.clone(),
        concurrency: g.concurrency,
        ..Default::default()
    }
}

/// Resolve config, running the onboarding selector when unconfigured + on a TTY.
pub async fn ensure_config(paths: &Paths, ov: &Overrides, allow_wizard: bool) -> Result<Config> {
    if let Some(cfg) = crate::config::resolve(paths, ov)? {
        return Ok(cfg);
    }
    if allow_wizard && util::is_interactive() {
        return onboarding::run_wizard(paths, ov, None).await;
    }
    bail!(
        "no provider configured — run `hephaes login`, set --provider/HEPHAES_PROVIDER, \
         or add .hephaes/config.toml"
    );
}

/// Absolute wiki directory for a config.
pub fn wiki_abs(root: &Path, cfg: &Config) -> PathBuf {
    let p = Path::new(&cfg.wiki_dir);
    if p.is_absolute() {
        p.to_path_buf()
    } else {
        root.join(p)
    }
}

/// Run a future under a spinner.
pub async fn with_spinner<F, T>(msg: &str, fut: F) -> T
where
    F: Future<Output = T>,
{
    let pb = ProgressBar::new_spinner();
    pb.set_style(
        ProgressStyle::with_template("{spinner:.cyan} {msg}")
            .unwrap_or_else(|_| ProgressStyle::default_spinner()),
    );
    pb.set_message(msg.to_string());
    pb.enable_steady_tick(Duration::from_millis(120));
    let out = fut.await;
    pb.finish_and_clear();
    out
}

/// Verbosity selected by the global -v/-q flags.
pub fn verbosity(g: &GlobalArgs) -> Verbosity {
    Verbosity::from_flags(g.quiet, g.verbose)
}

/// The trailing metrics for an agent run: steps, cost, elapsed.
pub fn run_metrics(o: &crate::agent::AgentOutcome, elapsed: std::time::Duration) -> String {
    let mut parts = Vec::new();
    if o.tool_calls > 0 {
        parts.push(format!("{} steps", o.tool_calls));
    }
    if let Some(c) = o.cost_usd {
        parts.push(format!("${c:.4}"));
    }
    parts.push(format!("{:.0}s", elapsed.as_secs_f64()));
    parts.join(" · ")
}
