use crate::config::{Config, Settings};
use crate::task::TaskOutput;
use crate::ui::{self, ctrlc};
use crate::{Result, backend, request_exit};
use crate::{cli::args::ToolArg, path::PathExt};
use crate::{hook_env as hook_env_module, logger, migrate, shims};
use clap::{ArgAction, CommandFactory, Subcommand};
use eyre::{Report, bail};
use std::path::PathBuf;

mod activate;
pub mod args;
mod asdf;
pub mod backends;
mod bin_paths;
mod bootstrap;
mod cache;
mod completion;
mod config;
mod current;
mod deactivate;
mod direnv;
mod doctor;
mod dotfiles;
mod en;
mod env;
pub mod exec;
mod external;
mod fmt;
mod generate;
mod github;
mod global;
mod hook_env;
mod hook_not_found;
mod tool_alias;

pub use hook_env::HookReason;
mod command_effects;
mod deps;
pub(crate) mod edit;
mod implode;
mod install;
mod install_into;
mod latest;
mod link;
mod local;
mod lock;
mod ls;
mod ls_remote;
mod mcp;
mod oci;
mod outdated;
mod patrons;
mod plugins;
mod prune;
mod registry;
#[cfg(debug_assertions)]
mod render_help;
mod reshim;
pub mod run;
mod search;
#[cfg_attr(not(feature = "self_update"), path = "self_update_stub.rs")]
pub mod self_update;
mod set;
mod settings;
mod shell;
mod shell_alias;
mod sponsors;
mod sync;
pub(crate) mod system;
mod tasks;
mod test_tool;
mod token;
mod tool;
pub mod tool_stub;
mod trust;
mod uninstall;
mod unset;
mod untrust;
mod unuse;
mod upgrade;
mod usage;
mod r#use;
pub mod version;
mod watch;
mod r#where;
mod r#which;

#[derive(clap::ValueEnum, Debug, Clone, strum::Display)]
#[strum(serialize_all = "kebab-case")]
pub enum LevelFilter {
    Trace,
    Debug,
    Info,
    Warning,
    Error,
}

#[derive(clap::Parser)]
#[clap(name = "mise", about, long_about = LONG_ABOUT, after_long_help = AFTER_LONG_HELP, author = "Jeff Dickey <@jdx>", arg_required_else_help = true)]
pub struct Cli {
    #[clap(subcommand)]
    pub command: Option<Commands>,
    /// Task to run
    #[clap(name = "TASK", long_help = LONG_TASK_ABOUT)]
    pub task: Option<String>,
    /// Task arguments
    #[clap(allow_hyphen_values = true, hide = true)]
    pub task_args: Option<Vec<String>>,
    #[clap(last = true, hide = true)]
    pub task_args_last: Vec<String>,
    /// Continue running tasks even if one fails
    #[clap(long, short = 'c', hide = true, verbatim_doc_comment)]
    pub continue_on_error: bool,
    /// Change directory before running command
    #[clap(short='C', long, global=true, value_name="DIR", value_hint=clap::ValueHint::DirPath)]
    pub cd: Option<PathBuf>,
    /// Set the environment for loading `mise.<ENV>.toml`
    #[clap(short = 'E', long, global = true)]
    pub env: Option<Vec<String>>,
    /// Force the operation
    #[clap(long, short, hide = true)]
    pub force: bool,
    /// How many jobs to run in parallel; values below 1 are treated as 1 [default: 8]
    #[clap(long, short, global = true, env = "MISE_JOBS")]
    pub jobs: Option<usize>,
    /// Dry run, don't actually do anything
    #[clap(short = 'n', long, hide = true)]
    pub dry_run: bool,
    /// Set the profile (environment)
    #[clap(short = 'P', long, global = true, hide = true, conflicts_with = "env")]
    pub profile: Option<Vec<String>>,
    /// Suppress non-error messages
    #[clap(short = 'q', long, global = true, overrides_with_all = &["silent", "trace", "verbose", "debug", "log_level"])]
    pub quiet: bool,
    #[clap(long, short, hide = true)]
    pub shell: Option<String>,
    /// Tool(s) to run in addition to what is in mise.toml files
    /// e.g.: node@20 python@3.10
    #[clap(
        short,
        long,
        hide = true,
        value_name = "TOOL@VERSION",
        env = "MISE_QUIET"
    )]
    pub tool: Vec<ToolArg>,
    /// Show extra output (use -vv for even more)
    #[clap(short='v', long, global=true, action=ArgAction::Count, overrides_with_all = &["quiet", "silent", "trace", "debug"])]
    pub verbose: u8,
    #[clap(long, short = 'V', hide = true)]
    pub version: bool,
    /// Answer yes to all confirmation prompts
    #[clap(short = 'y', long, global = true)]
    pub yes: bool,
    /// Sets log level to debug
    #[clap(long, global = true, hide = true, overrides_with_all = &["quiet", "trace", "verbose", "silent", "log_level"])]
    pub debug: bool,
    #[clap(long, global = true, hide = true, value_name = "LEVEL", value_enum, overrides_with_all = &["quiet", "trace", "verbose", "silent", "debug"])]
    pub log_level: Option<LevelFilter>,
    /// Do not load any config files
    ///
    /// Can also use `MISE_NO_CONFIG=1`
    #[clap(long)]
    pub no_config: bool,
    /// Do not load environment variables from config files
    ///
    /// Can also use `MISE_NO_ENV=1`
    #[clap(long)]
    pub no_env: bool,
    /// Do not execute hooks from config files
    ///
    /// Can also use `MISE_NO_HOOKS=1`
    #[clap(long)]
    pub no_hooks: bool,
    /// Hides elapsed time after each task completes
    ///
    /// Default to always hide with `MISE_TASK_TIMINGS=0`
    #[clap(long, alias = "no-timing", hide = true, verbatim_doc_comment)]
    pub no_timings: bool,
    #[clap(long)]
    pub output: Option<TaskOutput>,
    /// Read/write directly to stdin/stdout/stderr instead of by line
    #[clap(long, global = true)]
    pub raw: bool,
    /// Require lockfile URLs to be present during installation
    ///
    /// Fails if tools don't have pre-resolved URLs in the lockfile for the current platform.
    /// This prevents API calls to GitHub, aqua registry, etc.
    /// Can also be enabled via MISE_LOCKED=1 or settings.locked=true
    #[clap(long, global = true, verbatim_doc_comment)]
    pub locked: bool,
    /// Suppress all task output and mise non-error messages
    #[clap(long, global = true, overrides_with_all = &["quiet", "trace", "verbose", "debug", "log_level"])]
    pub silent: bool,
    /// Shows elapsed time after each task completes
    ///
    /// Default to always show with `MISE_TASK_TIMINGS=1`
    #[clap(long, alias = "timing", verbatim_doc_comment, hide = true)]
    pub timings: bool,
    /// Sets log level to trace
    #[clap(long, global = true, hide = true, overrides_with_all = &["quiet", "silent", "verbose", "debug", "log_level"])]
    pub trace: bool,
}

#[derive(Subcommand, strum::Display)]
#[strum(serialize_all = "kebab-case")]
pub enum Commands {
    Activate(activate::Activate),
    ToolAlias(Box<tool_alias::ToolAlias>),
    Asdf(asdf::Asdf),
    Backends(backends::Backends),
    BinPaths(bin_paths::BinPaths),
    Bootstrap(bootstrap::DeferredBootstrap),
    Cache(cache::Cache),
    Completion(completion::Completion),
    Config(config::Config),
    Current(current::Current),
    Deactivate(deactivate::Deactivate),
    Direnv(direnv::Direnv),
    Dotfiles(dotfiles::Dotfiles),
    Doctor(doctor::Doctor),
    En(en::En),
    Env(env::Env),
    Exec(exec::Exec),
    Fmt(fmt::Fmt),
    Generate(generate::Generate),
    Github(github::Github),
    Global(global::Global),
    HookEnv(hook_env::HookEnv),
    HookNotFound(hook_not_found::HookNotFound),
    Implode(implode::Implode),
    Edit(edit::Edit),
    Install(install::Install),
    InstallInto(install_into::InstallInto),
    Latest(latest::Latest),
    Link(link::Link),
    Local(local::Local),
    Lock(lock::Lock),
    Ls(ls::Ls),
    LsRemote(ls_remote::LsRemote),
    Mcp(mcp::Mcp),
    Oci(oci::Oci),
    Outdated(outdated::Outdated),
    Patrons(patrons::Patrons),
    Plugins(plugins::Plugins),
    Deps(deps::Deps),
    Prune(prune::Prune),
    Registry(registry::Registry),
    #[cfg(debug_assertions)]
    RenderHelp(render_help::RenderHelp),
    Reshim(reshim::Reshim),
    Run(Box<run::Run>),
    Search(search::Search),
    SelfUpdate(self_update::SelfUpdate),
    Set(set::Set),
    Settings(settings::Settings),
    Shell(shell::Shell),
    ShellAlias(shell_alias::ShellAlias),
    Sponsors(sponsors::Sponsors),
    Sync(sync::Sync),
    Tasks(tasks::Tasks),
    TestTool(test_tool::TestTool),
    Token(token::Token),
    Tool(tool::Tool),
    ToolStub(tool_stub::ToolStub),
    Trust(trust::Trust),
    Uninstall(uninstall::Uninstall),
    Unset(unset::Unset),
    Untrust(untrust::Untrust),
    Unuse(unuse::Unuse),
    Upgrade(upgrade::Upgrade),
    Usage(usage::Usage),
    Use(r#use::Use),
    Version(version::Version),
    Watch(Box<watch::Watch>),
    Where(r#where::Where),
    Which(which::Which),
}

/// Expand command sections that are deferred during normal startup. Use this
/// only for full-tree introspection such as generated docs and validation.
pub(crate) fn expand_deferred_subcommands(mut command: clap::Command) -> clap::Command {
    *command
        .find_subcommand_mut("bootstrap")
        .expect("bootstrap command is registered") = bootstrap::full_command();
    command
}

impl Commands {
    pub async fn run(self) -> Result<()> {
        match self {
            Self::Activate(cmd) => cmd.run(),
            Self::ToolAlias(cmd) => cmd.run().await,
            Self::Asdf(cmd) => cmd.run().await,
            Self::Backends(cmd) => cmd.run().await,
            Self::BinPaths(cmd) => cmd.run().await,
            Self::Bootstrap(cmd) => cmd.run().await,
            Self::Cache(cmd) => cmd.run().await,
            Self::Completion(cmd) => cmd.run().await,
            Self::Config(cmd) => cmd.run().await,
            Self::Current(cmd) => cmd.run().await,
            Self::Deactivate(cmd) => cmd.run(),
            Self::Direnv(cmd) => cmd.run().await,
            Self::Dotfiles(cmd) => cmd.run().await,
            Self::Doctor(cmd) => cmd.run().await,
            Self::En(cmd) => cmd.run().await,
            Self::Env(cmd) => cmd.run().await,
            Self::Exec(cmd) => cmd.run().await,
            Self::Fmt(cmd) => cmd.run(),
            Self::Generate(cmd) => cmd.run().await,
            Self::Github(cmd) => cmd.run().await,
            Self::Global(cmd) => cmd.run().await,
            Self::HookEnv(cmd) => cmd.run().await,
            Self::HookNotFound(cmd) => cmd.run().await,
            Self::Implode(cmd) => cmd.run(),
            Self::Edit(cmd) => cmd.run().await,
            Self::Install(cmd) => cmd.run().await,
            Self::InstallInto(cmd) => cmd.run().await,
            Self::Latest(cmd) => cmd.run().await,
            Self::Link(cmd) => cmd.run().await,
            Self::Local(cmd) => cmd.run().await,
            Self::Lock(cmd) => cmd.run().await,
            Self::Ls(cmd) => cmd.run().await,
            Self::LsRemote(cmd) => cmd.run().await,
            Self::Mcp(cmd) => cmd.run().await,
            Self::Oci(cmd) => cmd.run().await,
            Self::Outdated(cmd) => cmd.run().await,
            Self::Patrons(cmd) => cmd.run().await,
            Self::Plugins(cmd) => cmd.run().await,
            Self::Deps(cmd) => cmd.run().await,
            Self::Prune(cmd) => cmd.run().await,
            Self::Registry(cmd) => cmd.run().await,
            #[cfg(debug_assertions)]
            Self::RenderHelp(cmd) => cmd.run(),
            Self::Reshim(cmd) => cmd.run().await,
            Self::Run(cmd) => (*cmd).run().await,
            Self::Search(cmd) => cmd.run().await,
            Self::SelfUpdate(cmd) => cmd.run().await,
            Self::Set(cmd) => cmd.run().await,
            Self::Settings(cmd) => cmd.run().await,
            Self::Shell(cmd) => cmd.run().await,
            Self::ShellAlias(cmd) => cmd.run().await,
            Self::Sponsors(cmd) => cmd.run(),
            Self::Sync(cmd) => cmd.run().await,
            Self::Tasks(cmd) => cmd.run().await,
            Self::TestTool(cmd) => cmd.run().await,
            Self::Token(cmd) => cmd.run().await,
            Self::Tool(cmd) => cmd.run().await,
            Self::ToolStub(cmd) => cmd.run().await,
            Self::Trust(cmd) => cmd.run().await,
            Self::Uninstall(cmd) => cmd.run().await,
            Self::Unset(cmd) => cmd.run().await,
            Self::Untrust(cmd) => cmd.run(),
            Self::Unuse(cmd) => cmd.run().await,
            Self::Upgrade(cmd) => cmd.run().await,
            Self::Usage(cmd) => cmd.run(),
            Self::Use(cmd) => cmd.run().await,
            Self::Version(cmd) => cmd.run().await,
            Self::Watch(cmd) => cmd.run().await,
            Self::Where(cmd) => cmd.run().await,
            Self::Which(cmd) => cmd.run().await,
        }
    }
}

fn get_global_flags(cmd: &clap::Command) -> (Vec<String>, Vec<String>) {
    let mut flags_with_values = Vec::new();
    let mut boolean_flags = Vec::new();

    for arg in cmd.get_arguments() {
        let takes_value = matches!(
            arg.get_action(),
            clap::ArgAction::Set | clap::ArgAction::Append
        );
        let is_bool = matches!(
            arg.get_action(),
            clap::ArgAction::SetTrue | clap::ArgAction::SetFalse
        );

        if takes_value {
            if let Some(long) = arg.get_long() {
                flags_with_values.push(format!("--{}", long));
            }
            if let Some(short) = arg.get_short() {
                flags_with_values.push(format!("-{}", short));
            }
        } else if is_bool {
            if let Some(long) = arg.get_long() {
                boolean_flags.push(format!("--{}", long));
            }
            if let Some(short) = arg.get_short() {
                boolean_flags.push(format!("-{}", short));
            }
        }
    }

    (flags_with_values, boolean_flags)
}

/// Get all flags (with values and boolean) from both global Cli and Run subcommand
fn get_all_run_flags(cmd: &clap::Command) -> (Vec<String>, Vec<String>) {
    // Get global flags from Cli
    let (mut flags_with_values, mut boolean_flags) = get_global_flags(cmd);

    // Get run-specific flags from Run subcommand
    if let Some(run_cmd) = cmd.get_subcommands().find(|s| s.get_name() == "run") {
        let (run_vals, run_bools) = get_global_flags(run_cmd);
        flags_with_values.extend(run_vals);
        boolean_flags.extend(run_bools);
    }

    (flags_with_values, boolean_flags)
}

fn get_value_taking_short_flags(cmd: &clap::Command) -> Vec<(String, String)> {
    cmd.get_arguments()
        .filter(|arg| {
            matches!(
                arg.get_action(),
                clap::ArgAction::Set | clap::ArgAction::Append
            )
        })
        .filter_map(|arg| Some((arg.get_short()?, arg.get_long()?)))
        .map(|(short, long)| (format!("-{short}"), format!("--{long}")))
        .collect()
}

fn get_all_run_value_taking_short_flags(cmd: &clap::Command) -> Vec<(String, String)> {
    let mut flags = get_value_taking_short_flags(cmd);
    if let Some(run_cmd) = cmd
        .get_subcommands()
        .find(|subcommand| subcommand.get_name() == "run")
    {
        flags.extend(get_value_taking_short_flags(run_cmd));
    }
    flags
}

/// Prefix used to escape flags that should be passed to tasks, not mise
const TASK_ARG_ESCAPE_PREFIX: &str = "\x00MISE_TASK_ARG\x00";

fn escape_args_after_separator(args: &[String], separator_idx: usize) -> Vec<String> {
    let mut result = args[..=separator_idx].to_vec();
    for arg in &args[separator_idx + 1..] {
        if arg.starts_with('-') && arg != "-" {
            result.push(format!("{}{}", TASK_ARG_ESCAPE_PREFIX, arg));
        } else {
            result.push(arg.clone());
        }
    }
    result
}

/// Long and short forms of the top-level flags that consume a following argument.
///
/// Hardcoded rather than derived because `env.rs` needs it from `Lazy` statics
/// during startup — before anything has parsed arguments — and deriving it means
/// building the entire clap tree, which costs ~3.1M instructions. Doing that
/// there is what made every mise command ~6.3M instructions more expensive.
///
/// `test_global_flags_with_values_matches_clap` asserts this equals what clap
/// reports, so adding a value-taking flag to [`Cli`] without updating this list
/// fails CI rather than silently mis-parsing arguments.
pub(crate) const GLOBAL_FLAGS_WITH_VALUES: &[&str] = &[
    "--cd",
    "-C",
    "--env",
    "-E",
    "--jobs",
    "-j",
    "--profile",
    "-P",
    "--shell",
    "-s",
    "--tool",
    "-t",
    "--log-level",
    "--output",
];

/// Index of the first argument that is not a global flag or one of its values.
///
/// Takes a `&Command` so callers that have already built one (argument parsing)
/// use its real flag set. Callers without one want
/// [`first_non_global_arg_idx_cached`].
pub(crate) fn first_non_global_arg_idx(cmd: &clap::Command, args: &[String]) -> Option<usize> {
    let flags = get_global_flags(cmd).0;
    first_non_global_arg_idx_with(|f| flags.iter().any(|x| x == f), args)
}

/// As [`first_non_global_arg_idx`], against [`GLOBAL_FLAGS_WITH_VALUES`].
///
/// For callers with no `Command` to hand, which would otherwise build the whole
/// tree just to read its top-level arguments.
pub(crate) fn first_non_global_arg_idx_cached(args: &[String]) -> Option<usize> {
    first_non_global_arg_idx_with(|f| GLOBAL_FLAGS_WITH_VALUES.contains(&f), args)
}

fn first_non_global_arg_idx_with(
    takes_value: impl Fn(&str) -> bool,
    args: &[String],
) -> Option<usize> {
    let mut i = 1;
    while i < args.len() {
        let arg = &args[i];

        if arg == "--" {
            return None;
        }

        if !arg.starts_with('-') {
            return Some(i);
        }

        let flag_takes_separate_value = if arg.starts_with("--") {
            if arg.contains('=') {
                false
            } else {
                let flag_name = arg.split('=').next().unwrap();
                takes_value(flag_name)
            }
        } else if let Some(flag_name) = arg.get(..2) {
            // `arg.get(..2)` (not `&arg[..2]`) avoids panicking when the arg is
            // not valid UTF-8 in the first place: args are read lossily, so a
            // malformed byte becomes a multi-byte U+FFFD and byte index 2 may not
            // be a char boundary. A short flag is always ASCII, so a non-ASCII
            // prefix simply matches no value-taking flag.
            arg.len() == 2 && takes_value(flag_name)
        } else {
            false
        };

        if flag_takes_separate_value && i + 1 < args.len() {
            i += 2;
        } else {
            i += 1;
        }
    }
    None
}

fn is_known_subcommand(cmd: &clap::Command, arg: &str) -> bool {
    cmd.get_subcommands()
        .flat_map(|s| std::iter::once(s.get_name()).chain(s.get_all_aliases()))
        .any(|name| name == arg)
}

fn uses_deprecated_backends_alias(cmd: &clap::Command, args: &[String]) -> bool {
    matches!(
        first_non_global_arg_idx(cmd, args).and_then(|idx| args.get(idx)),
        Some(arg) if arg == "b"
    )
}

fn warn_deprecated_backends_alias(uses_alias: bool) {
    if uses_alias {
        deprecated_at!(
            "2026.4.0",
            "2027.4.0",
            "cli.backends.b",
            "`mise b` is deprecated. Use `mise backends` instead."
        );
    }
}

/// Escape flags after task names so clap doesn't parse them as mise flags.
/// This preserves ::: separators for multi-task handling while preventing
/// clap from consuming flags like --jobs that appear after task names.
fn escape_task_args(cmd: &clap::Command, args: &[String]) -> Vec<String> {
    // Find the mise `run` subcommand position. Do not scan past `--`; values
    // after that boundary belong to another command or a task.
    let first_idx = first_non_global_arg_idx(cmd, args);
    let run_pos = first_idx.filter(|&pos| args[pos] == "run");
    let run_pos = match run_pos {
        Some(pos) => pos,
        None => {
            if let (Some(task_idx), Some(separator_idx)) =
                (first_idx, args.iter().position(|a| a == "--"))
            {
                if is_known_subcommand(cmd, &args[task_idx]) || separator_idx <= task_idx {
                    return args.to_vec();
                }
                return escape_args_after_separator(args, separator_idx);
            }
            return args.to_vec();
        }
    };

    if let Some(separator_idx) = args[run_pos + 1..].iter().position(|a| a == "--") {
        return escape_args_after_separator(args, run_pos + 1 + separator_idx);
    }

    let (flags_with_values, _) = get_all_run_flags(cmd);
    let short_flags_with_values = get_all_run_value_taking_short_flags(cmd);

    // Build result, escaping flags that appear after task names
    let mut result = args[..=run_pos].to_vec(); // Include up to and including "run"
    let mut in_task_args = false; // true after we've seen a task name

    let mut i = run_pos + 1;
    while i < args.len() {
        let arg = &args[i];

        // ::: starts a new task, so reset to looking for task name
        if arg == ":::" {
            result.push(arg.clone());
            in_task_args = false;
            i += 1;
            continue;
        }

        if !in_task_args {
            // Looking for task name - skip any mise flags
            if arg.starts_with('-') {
                // clap treats attached values for short options as positional
                // values when the positional allows hyphens. Normalize them to
                // unambiguous long options before parsing.
                let attached_short_value =
                    short_flags_with_values.iter().find_map(|(short, long)| {
                        arg.strip_prefix(short)
                            .filter(|value| !value.is_empty())
                            .map(|value| (long, value))
                    });

                if let Some((long, value)) = attached_short_value {
                    let value = value.strip_prefix('=').unwrap_or(value);
                    result.push(format!("{long}={value}"));
                    i += 1;
                    continue;
                }

                // It's a flag - keep it as-is for mise to parse
                result.push(arg.clone());

                // Check if this flag takes a value (and needs to consume the next arg)
                let flag_takes_value = if arg.starts_with("--") {
                    if arg.contains('=') {
                        false // --flag=value, no separate value
                    } else {
                        flags_with_values.iter().any(|f| f == arg)
                    }
                } else if arg.len() > 2 {
                    // Short flag with embedded value (e.g., -j4), no separate value needed
                    false
                } else if arg.len() == 2 {
                    let flag_name = &arg[..2];
                    flags_with_values.iter().any(|f| f == flag_name)
                } else {
                    false
                };

                if flag_takes_value && i + 1 < args.len() && !args[i + 1].starts_with('-') {
                    i += 1;
                    result.push(args[i].clone());
                }
            } else {
                // Found task name
                result.push(arg.clone());
                in_task_args = true;
            }
        } else {
            // In task args - escape flags so clap doesn't parse them
            if arg.starts_with('-') && arg != "-" {
                // Escape the flag
                result.push(format!("{}{}", TASK_ARG_ESCAPE_PREFIX, arg));
            } else {
                result.push(arg.clone());
            }
        }

        i += 1;
    }

    result
}

/// Unescape task args that were escaped by escape_task_args
pub fn unescape_task_args(args: &[String]) -> Vec<String> {
    args.iter()
        .map(|arg| {
            if let Some(stripped) = arg.strip_prefix(TASK_ARG_ESCAPE_PREFIX) {
                stripped.to_string()
            } else {
                arg.clone()
            }
        })
        .collect()
}

fn preprocess_args_for_naked_run(cmd: &clap::Command, args: &[String]) -> Vec<String> {
    // Check if this might be a naked run (no subcommand)
    if args.len() < 2 {
        return args.to_vec();
    }

    // If there's already a '--' separator, let clap handle the naked task path.
    // escape_task_args will still protect task-side flags after the separator.
    if args.contains(&"--".to_string()) {
        return args.to_vec();
    }

    // Skip global flags to find the first non-flag argument (subcommand or task)
    let Some(i) = first_non_global_arg_idx(cmd, args) else {
        return args.to_vec();
    };

    // Check if the first non-flag argument is a known subcommand
    if is_known_subcommand(cmd, &args[i]) {
        return args.to_vec();
    }

    // Special case: "help" should print help, not be treated as a task
    if args[i] == "help" || args[i] == "-h" || args[i] == "--help" {
        return args.to_vec();
    }

    // This is a naked run - inject "run" subcommand so clap routes it correctly
    // Format: ["mise", "-q", "task", "arg1"] becomes ["mise", "-q", "run", "task", "arg1"]
    // This preserves global flags while making it an explicit run command
    let mut result = args[..i].to_vec(); // Keep program name + global flags
    result.push("run".to_string()); // Insert "run" subcommand
    result.extend_from_slice(&args[i..]); // Add task name and args
    result
}

impl Cli {
    pub async fn run(args: &Vec<String>) -> Result<()> {
        run_with_exit_signal(Self::run_inner(args), ctrlc::exit_signal()).await
    }

    async fn run_inner(args: &Vec<String>) -> Result<()> {
        crate::env::ARGS.write().unwrap().clone_from(args);
        // Load .miserc.toml early, before MISE_ENV and other early settings are accessed.
        // This allows setting MISE_ENV in a config file instead of only via env vars.
        crate::config::miserc::init()?;
        if *crate::env::MISE_TOOL_STUB && args.len() >= 2 {
            tool_stub::short_circuit_stub(&args[2..]).await?;
        }
        // Fast-path for hook-env: exit early if nothing has changed
        // This avoids expensive backend::load_tools() and config loading
        if hook_env_module::should_exit_early_fast() {
            measure!("logger", { logger::init() });
            Settings::flush_deprecated_warnings_for_fast_exit();
            return Ok(());
        }
        measure!("logger", { logger::init() });
        check_working_directory();
        measure!("handle_shim", { shims::handle_shim().await })?;
        let print_version = version::print_version_if_requested(args)?;
        // Clap's tool argument parsers consult installed plugin/tool metadata while
        // resolving registry options. Initialize that filesystem-only state before
        // parsing, while leaving full backend loading until after registry refresh.
        if !print_version {
            measure!("install_state::init", {
                crate::toolset::install_state::init().await?
            });
        }
        // Pre-process args to handle naked runs before clap parsing
        let cmd = measure!("build_cli_command", { Cli::command() });
        let processed_args = preprocess_args_for_naked_run(&cmd, args);
        // Escape flags after task names so they go to tasks, not mise
        let processed_args = escape_task_args(&cmd, &processed_args);
        let deprecated_backends_alias = uses_deprecated_backends_alias(&cmd, args);

        // Reuse the already-built clap command rather than letting
        // `Cli::parse_from` construct the whole tree a second time.
        let cli = measure!("get_matches_from", {
            let matches = cmd
                .try_get_matches_from(processed_args.iter())
                .map_err(clap_error)?;
            <Cli as clap::FromArgMatches>::from_arg_matches(&matches)
                .map_err(|err| clap_error(err.format(&mut Cli::command())))
        })?;
        // Validate --cd path BEFORE Settings processes it and changes the directory
        validate_cd_path(&cli.cd)?;
        measure!("add_cli_matches", { Settings::add_cli_matches(&cli) });
        let _ = measure!("settings", { Settings::try_get() });
        measure!("logger", { logger::init() });
        if !print_version {
            measure!("registry::refresh", { crate::registry::refresh().await });
            let _ = measure!("backend::load_tools", { backend::load_tools().await });
        }
        warn_deprecated_backends_alias(deprecated_backends_alias);
        measure!("migrate", { migrate::run().await });
        if let Err(err) = crate::cache::auto_prune() {
            warn!("auto_prune failed: {err:?}");
        }

        debug!("ARGS: {}", &args.join(" "));
        trace!("MISE_BIN: {}", crate::env::MISE_BIN.display_user());
        if print_version {
            version::show_latest().await;
            return Err(request_exit(0));
        }
        let _remote_task_artifacts = crate::task::task_fetcher::RemoteTaskArtifactsGuard::new();
        let cmd = cli.get_command().await?;
        measure!("run {cmd}", { cmd.run().await })
    }

    async fn get_command(self) -> Result<Commands> {
        if let Some(cmd) = self.command {
            Ok(cmd)
        } else {
            if let Some(task) = self.task {
                // Handle special case: "help", "-h", or "--help" as task should print help
                if task == "help" || task == "-h" || task == "--help" {
                    Cli::command().print_help()?;
                    return Err(request_exit(0));
                }

                let config = Config::get().await?;

                // Expand :task pattern to match tasks in current directory's config root
                let task = crate::task::expand_colon_task_syntax(&task, &config)?;

                // For monorepo task patterns (starting with //), we need to load
                // tasks from the entire monorepo, not just the current hierarchy
                let tasks = if task.starts_with("//") {
                    let ctx = crate::task::TaskLoadContext::from_pattern(&task);
                    config.tasks_with_context(Some(&ctx)).await?
                } else {
                    config.tasks().await?
                };
                if tasks.iter().any(|(_, t)| t.is_match(&task)) {
                    return Ok(Commands::Run(Box::new(run::Run {
                        task: Some(task),
                        args: self.task_args.unwrap_or_default(),
                        args_last: self.task_args_last,
                        all: false,
                        affected: false,
                        affected_base: None,
                        affected_head: None,
                        affected_explain: false,
                        affected_json: false,
                        cd: self.cd,
                        continue_on_error: self.continue_on_error,
                        dry_run: self.dry_run,
                        force: self.force,
                        is_linear: false,
                        jobs: self.jobs,
                        no_timings: self.no_timings,
                        output: self.output,
                        shell: self.shell,
                        quiet: self.quiet,
                        silent: self.silent,
                        raw: self.raw,
                        timings: self.timings,
                        tmpdir: Default::default(),
                        tool: Default::default(),
                        output_handler: None,
                        context_builder: Default::default(),
                        executor: None,
                        cache_session: None,
                        no_cache: Default::default(),
                        task_cache: crate::task::TaskCacheMode::from_env()?,
                        task_cache_explain: false,
                        task_cache_explain_json: false,
                        task_cache_stats: false,
                        timeout: None,
                        skip_deps: false,
                        skip_tools: false,
                        no_deps: false,
                        fresh_env: false,
                        deny_all: false,
                        deny_read: false,
                        deny_write: false,
                        deny_net: false,
                        deny_env: false,
                        allow_read: vec![],
                        allow_write: vec![],
                        allow_net: vec![],
                        allow_env: vec![],
                    })));
                } else if let Some(cmd) = external::COMMANDS.get(&task) {
                    external::execute(
                        &task.into(),
                        cmd.clone(),
                        self.task_args
                            .unwrap_or_default()
                            .into_iter()
                            .chain(self.task_args_last)
                            .collect(),
                    )?;
                    return Err(request_exit(0));
                }
            }
            Cli::command().print_help()?;
            Err(request_exit(1))
        }
    }
}

async fn run_with_exit_signal<T>(
    command: impl std::future::Future<Output = Result<T>>,
    exit_signal: impl std::future::Future<Output = i32>,
) -> Result<T> {
    tokio::select! {
        result = command => result,
        code = exit_signal => Err(request_exit(code)),
    }
}

fn clap_error(err: clap::Error) -> Report {
    let code = err.exit_code();
    match err.print() {
        Ok(()) => request_exit(code),
        Err(err) => err.into(),
    }
}

const LONG_ABOUT: &str = "mise prepares your development environment before each command runs. https://github.com/jdx/mise";

const LONG_TASK_ABOUT: &str = r#"Task to run.

Shorthand for `mise tasks run <TASK>`."#;

static AFTER_LONG_HELP: &str = color_print::cstr!(
    r#"<bold><underline>Examples:</underline></bold>

    $ <bold>mise install node@20.0.0</bold>       Install a specific node version
    $ <bold>mise install node@20</bold>           Install a version matching a prefix
    $ <bold>mise install node</bold>              Install the node version defined in config
    $ <bold>mise install</bold>                   Install all plugins/tools defined in config

    $ <bold>mise install cargo:ripgrep</bold>     Install something via cargo
    $ <bold>mise install npm:prettier</bold>      Install something via npm

    $ <bold>mise use node@20</bold>               Use node-20.x in current project
    $ <bold>mise use -g node@20</bold>            Use node-20.x as default
    $ <bold>mise use node@latest</bold>           Use latest node in current directory

    $ <bold>mise up --interactive</bold>          Show a menu to upgrade tools

    $ <bold>mise x -- npm install</bold>          `npm install` w/ config loaded into PATH
    $ <bold>mise x node@20 -- node app.js</bold>  `node app.js` w/ config + node-20.x on PATH

    $ <bold>mise set NODE_ENV=production</bold>   Set NODE_ENV=production in config

    $ <bold>mise run build</bold>                 Run `build` tasks
    $ <bold>mise watch build</bold>               Run `build` tasks repeatedly when files change

    $ <bold>mise settings</bold>                  Show settings in use
    $ <bold>mise settings color=0</bold>          Disable color by modifying global config file
"#
);

/// Check if the current working directory exists and warn if not
fn check_working_directory() {
    if std::env::current_dir().is_err() {
        // Try to get the directory path from PWD env var, which might still contain the old path
        let dir_path = std::env::var("PWD")
            .or_else(|_| std::env::var("OLDPWD"))
            .unwrap_or_else(|_| "(unknown)".to_string());
        warn!(
            "Current directory does not exist or is not accessible: {}",
            dir_path
        );
    }
}

/// Validate the --cd path if provided and return an error if it doesn't exist
fn validate_cd_path(cd: &Option<PathBuf>) -> Result<()> {
    if let Some(path) = cd {
        if !path.exists() {
            bail!(
                "Directory specified with --cd does not exist: {}\n\
                 Please check the path and try again.",
                ui::style::epath(path)
            );
        }
        if !path.is_dir() {
            bail!(
                "Path specified with --cd is not a directory: {}\n\
                 Please provide a valid directory path.",
                ui::style::epath(path)
            );
        }
    }
    Ok(())
}

#[cfg(test)]
mod tests {

    /// Guards [`GLOBAL_FLAGS_WITH_VALUES`]. It is hardcoded so that startup does
    /// not have to build the clap tree; this keeps it honest. If you added a
    /// value-taking flag to `Cli`, add it to that list too.
    #[test]
    fn test_global_flags_with_values_matches_clap() {
        let derived = get_global_flags(&Cli::command()).0;
        let mut derived_sorted = derived.clone();
        derived_sorted.sort();
        let mut hardcoded: Vec<String> = GLOBAL_FLAGS_WITH_VALUES
            .iter()
            .map(|s| (*s).to_string())
            .collect();
        hardcoded.sort();
        assert_eq!(
            hardcoded, derived_sorted,
            "GLOBAL_FLAGS_WITH_VALUES is stale; clap reports {derived:?}"
        );
    }
    use super::*;

    #[tokio::test]
    async fn exit_signal_drops_command_future_before_returning() {
        struct DropGuard(std::sync::Arc<std::sync::atomic::AtomicBool>);

        impl Drop for DropGuard {
            fn drop(&mut self) {
                self.0.store(true, std::sync::atomic::Ordering::SeqCst);
            }
        }

        let dropped = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
        let guard = DropGuard(dropped.clone());
        let command = async move {
            let _guard = guard;
            std::future::pending::<Result<()>>().await
        };

        let err = run_with_exit_signal(command, std::future::ready(42))
            .await
            .unwrap_err();

        assert_eq!(crate::exit::requested_exit_code(&err), Some(42));
        assert!(dropped.load(std::sync::atomic::Ordering::SeqCst));
    }

    #[test]
    fn test_subcommands_are_sorted() {
        let cmd = Cli::command();
        // Check all subcommands except watch (which has many watchexec passthrough args)
        for subcmd in cmd.get_subcommands() {
            if subcmd.get_name() != "watch" {
                clap_sort::assert_sorted(subcmd);
            }
        }
        let cmd = expand_deferred_subcommands(Cli::command());
        let bootstrap = cmd
            .get_subcommands()
            .find(|subcommand| subcommand.get_name() == "bootstrap")
            .unwrap();
        for subcommand in bootstrap.get_subcommands() {
            clap_sort::assert_sorted(subcommand);
        }
    }

    #[test]
    fn test_bootstrap_command_tree_is_deferred_until_parsing() {
        let cmd = Cli::command();
        let bootstrap = cmd
            .get_subcommands()
            .find(|subcommand| subcommand.get_name() == "bootstrap")
            .unwrap();
        assert_eq!(bootstrap.get_subcommands().count(), 0);

        let alias_args = vec![
            "mise".to_string(),
            "bs".to_string(),
            "status".to_string(),
            "--json".to_string(),
        ];
        assert_eq!(preprocess_args_for_naked_run(&cmd, &alias_args), alias_args);
        assert!(cmd.clone().try_get_matches_from(alias_args).is_ok());

        assert!(
            cmd.try_get_matches_from(["mise", "bootstrap", "status", "--json"])
                .is_ok()
        );
    }

    /// Commands that name a config file to write to accept both spellings, so it
    /// does not matter which one you remember. See
    /// <https://github.com/jdx/mise/discussions/4881>.
    ///
    /// Asserted through clap rather than end-to-end because several of these
    /// commands install or apply something when actually run.
    #[test]
    fn test_config_target_options_accept_both_names() {
        // (command path, argument id, expected alias, available on this platform)
        // `use` and `dotfiles add` are deliberately absent: `-f` is `--force` on both, so
        // they carry `--path` only. See the shadowing test below.
        let cases: &[(&[&str], &str, &str, bool)] = &[
            (&["unuse"], "path", "file", true),
            (&["set"], "file", "path", true),
            (&["unset"], "file", "path", true),
            (&["config", "get"], "file", "path", true),
            (&["config", "set"], "file", "path", true),
            (&["bootstrap", "packages", "use"], "path", "file", true),
            (&["bootstrap", "packages", "import"], "path", "file", true),
            // the brew manager is not registered on Windows
            (
                &["bootstrap", "packages", "brew", "tap"],
                "path",
                "file",
                cfg!(not(windows)),
            ),
            (
                &["bootstrap", "packages", "brew", "untap"],
                "path",
                "file",
                cfg!(not(windows)),
            ),
        ];
        let root = expand_deferred_subcommands(Cli::command());

        for (path, arg_name, alias, available) in cases {
            if !available {
                continue;
            }
            let command = path.iter().fold(&root, |command, name| {
                command
                    .get_subcommands()
                    .find(|subcommand| subcommand.get_name() == *name)
                    .unwrap_or_else(|| panic!("missing command path {}", path.join(" ")))
            });
            let arg = command
                .get_arguments()
                .find(|arg| arg.get_id() == *arg_name)
                .unwrap_or_else(|| panic!("missing --{arg_name} on {}", path.join(" ")));

            assert!(
                arg.get_visible_aliases()
                    .is_some_and(|aliases| aliases.contains(alias)),
                "missing visible --{alias} alias for --{arg_name} on {}",
                path.join(" ")
            );
        }
    }

    /// A `--file`/`--path` alias whose natural short form belongs to a *different* argument
    /// on the same command teaches the wrong flag. `mise dotfiles add` carried `--file` as an
    /// alias of `--path` while `-f` was `--force`, and because its targets accept any string,
    /// `mise dotfiles add -f <path>` silently adopted that config file as a dotfile instead of
    /// writing to it — no error, and `--force` meant no prompt either.
    ///
    /// Walks the whole CLI rather than a fixed list, so re-adding the alias anywhere fails
    /// even if the case table above is left alone.
    #[test]
    fn config_target_aliases_do_not_shadow_another_short_flag() {
        fn check(command: &clap::Command, path: &mut Vec<String>) {
            for arg in command.get_arguments() {
                let Some(aliases) = arg.get_visible_aliases() else {
                    continue;
                };
                for alias in aliases {
                    // Only this vocabulary — an unrelated alias sharing a letter with some
                    // other flag is ordinary and not what this is about.
                    if alias != "file" && alias != "path" {
                        continue;
                    }
                    let short = alias.chars().next().unwrap();
                    if let Some(owner) = command.get_arguments().find(|other| {
                        other.get_id() != arg.get_id() && other.get_short() == Some(short)
                    }) {
                        panic!(
                            "mise {}: --{alias} aliases --{}, but -{short} is --{} — \
                             drop the alias or the collision",
                            path.join(" "),
                            arg.get_id().as_str(),
                            owner.get_id().as_str()
                        );
                    }
                }
            }
            for subcommand in command.get_subcommands() {
                path.push(subcommand.get_name().to_string());
                check(subcommand, path);
                path.pop();
            }
        }

        let root = expand_deferred_subcommands(Cli::command());
        check(&root, &mut Vec::new());
    }

    #[test]
    fn test_escape_task_args_preserves_task_separator_tail() {
        let cmd = Cli::command();
        let args = vec![
            "mise".to_string(),
            "run".to_string(),
            "atask".to_string(),
            "-q".to_string(),
            "--".to_string(),
            "--".to_string(),
            "--help".to_string(),
        ];

        let escaped = escape_task_args(&cmd, &args);
        let separator_idx = escaped.iter().position(|arg| arg == "--").unwrap();
        assert_eq!(escaped[..=separator_idx], args[..=separator_idx]);
        assert!(escaped[separator_idx + 1].starts_with(TASK_ARG_ESCAPE_PREFIX));
        assert!(escaped[separator_idx + 2].starts_with(TASK_ARG_ESCAPE_PREFIX));
        assert_eq!(
            unescape_task_args(&escaped[separator_idx + 1..]),
            vec!["--".to_string(), "--help".to_string()]
        );
    }

    #[test]
    fn test_escape_task_args_splits_attached_short_option_values_before_task() {
        let cmd = Cli::command();
        let args = vec![
            "mise".to_string(),
            "run".to_string(),
            "-j1".to_string(),
            "-Ctmp".to_string(),
            "-oprefix".to_string(),
            "atask".to_string(),
        ];

        assert_eq!(
            escape_task_args(&cmd, &args),
            [
                "mise",
                "run",
                "--jobs=1",
                "--cd=tmp",
                "--output=prefix",
                "atask",
            ]
            .map(str::to_string)
        );
    }

    #[test]
    fn test_escape_task_args_preserves_equals_attached_option_values() {
        let cmd = Cli::command();
        let args = ["mise", "run", "-C=/tmp", "-o=prefix", "atask"].map(str::to_string);
        let processed = escape_task_args(&cmd, &args);

        assert_eq!(
            processed,
            ["mise", "run", "--cd=/tmp", "--output=prefix", "atask"].map(str::to_string)
        );
        let matches = cmd.try_get_matches_from(processed).unwrap();
        let cli = <Cli as clap::FromArgMatches>::from_arg_matches(&matches).unwrap();
        assert_eq!(cli.cd, Some(PathBuf::from("/tmp")));
        let Some(Commands::Run(run)) = cli.command else {
            panic!("expected run command");
        };
        assert_eq!(run.task.as_deref(), Some("atask"));
    }

    #[test]
    fn test_escape_task_args_keeps_hyphen_prefixed_attached_value_bound_to_option() {
        let cmd = Cli::command();
        let args = ["mise", "run", "-C-dir", "atask"].map(str::to_string);
        let processed = escape_task_args(&cmd, &args);

        assert_eq!(
            processed,
            ["mise", "run", "--cd=-dir", "atask"].map(str::to_string)
        );

        let matches = cmd.try_get_matches_from(processed).unwrap();
        let cli = <Cli as clap::FromArgMatches>::from_arg_matches(&matches).unwrap();
        assert_eq!(cli.cd, Some(PathBuf::from("-dir")));
        let Some(Commands::Run(run)) = cli.command else {
            panic!("expected run command");
        };
        assert_eq!(run.task.as_deref(), Some("atask"));
    }

    #[test]
    fn test_escape_task_args_preserves_naked_task_separator_tail() {
        let cmd = Cli::command();
        let args = vec![
            "mise".to_string(),
            "atask".to_string(),
            "-q".to_string(),
            "--".to_string(),
            "--help".to_string(),
        ];

        assert_eq!(preprocess_args_for_naked_run(&cmd, &args), args);
        let escaped = escape_task_args(&cmd, &args);
        let separator_idx = escaped.iter().position(|arg| arg == "--").unwrap();
        assert_eq!(escaped[..=separator_idx], args[..=separator_idx]);
        assert!(escaped[separator_idx + 1].starts_with(TASK_ARG_ESCAPE_PREFIX));
        assert_eq!(
            unescape_task_args(&escaped[separator_idx + 1..]),
            vec!["--help".to_string()]
        );
    }

    #[test]
    fn test_escape_task_args_leaves_subcommand_separator_tail_alone() {
        let cmd = Cli::command();
        let args = vec![
            "mise".to_string(),
            "exec".to_string(),
            "--".to_string(),
            "sh".to_string(),
            "--flag".to_string(),
        ];

        assert_eq!(escape_task_args(&cmd, &args), args);
    }

    #[test]
    fn test_uses_deprecated_backends_alias() {
        let cmd = Cli::command();
        let args = vec!["mise".to_string(), "b".to_string()];

        assert!(uses_deprecated_backends_alias(&cmd, &args));
    }

    #[test]
    fn test_uses_deprecated_backends_alias_after_global_flag() {
        let cmd = Cli::command();
        let args = vec![
            "mise".to_string(),
            "--cd".to_string(),
            "project".to_string(),
            "b".to_string(),
        ];

        assert!(uses_deprecated_backends_alias(&cmd, &args));
    }

    #[test]
    fn test_uses_deprecated_backends_alias_ignores_global_flag_value() {
        let cmd = Cli::command();
        let args = vec![
            "mise".to_string(),
            "--cd".to_string(),
            "b".to_string(),
            "backends".to_string(),
        ];

        assert!(!uses_deprecated_backends_alias(&cmd, &args));
    }

    #[test]
    fn test_first_non_global_arg_idx_handles_attached_short_flag_values() {
        let cmd = Cli::command();
        let args = |args: &[&str]| {
            args.iter()
                .map(|arg| (*arg).to_string())
                .collect::<Vec<_>>()
        };

        for args in [
            args(&["mise", "-C", "/tmp", "lock"]),
            args(&["mise", "-C/tmp", "lock"]),
            args(&["mise", "-C=/tmp", "lock"]),
            args(&["mise", "-j8", "lock"]),
        ] {
            let idx = first_non_global_arg_idx(&cmd, &args).unwrap();
            assert_eq!(args[idx], "lock");
        }
    }

    #[test]
    fn test_uses_deprecated_backends_alias_ignores_task_arg() {
        let cmd = Cli::command();
        let args = vec!["mise".to_string(), "run".to_string(), "b".to_string()];

        assert!(!uses_deprecated_backends_alias(&cmd, &args));
    }

    #[test]
    fn test_escape_task_args_ignores_run_after_subcommand_separator() {
        let cmd = Cli::command();
        let args = vec![
            "mise".to_string(),
            "exec".to_string(),
            "--".to_string(),
            "npm".to_string(),
            "run".to_string(),
            "test".to_string(),
            "--help".to_string(),
        ];

        assert_eq!(escape_task_args(&cmd, &args), args);
    }
}
