//! The commands available in the Wasmer binary.
mod add;
mod app;
mod auth;
#[cfg(target_os = "linux")]
mod binfmt;
mod cache;
#[cfg(feature = "compiler")]
mod compile;
mod config;
mod connect;
mod container;
#[cfg(any(feature = "static-artifact-create", feature = "wasmer-artifact-create"))]
mod create_exe;
#[cfg(feature = "static-artifact-create")]
mod create_obj;
pub(crate) mod domain;
#[cfg(feature = "static-artifact-create")]
mod gen_c_header;
mod gen_completions;
mod gen_manpage;
mod init;
mod inspect;
#[cfg(feature = "journal")]
mod journal;
pub(crate) mod namespace;
mod package;
mod run;
mod self_update;
pub mod ssh;
mod validate;
#[cfg(feature = "wast")]
mod wast;
use itertools::Itertools;
use std::io::IsTerminal as _;
use tokio::task::JoinHandle;

#[cfg(target_os = "linux")]
pub use binfmt::*;
use clap::{CommandFactory, Parser};
#[cfg(feature = "compiler")]
pub use compile::*;
#[cfg(any(feature = "static-artifact-create", feature = "wasmer-artifact-create"))]
pub use create_exe::*;
#[cfg(feature = "wast")]
pub use wast::*;
#[cfg(feature = "static-artifact-create")]
#[allow(unused_imports)]
pub use {create_obj::*, gen_c_header::*};

#[cfg(feature = "journal")]
pub use self::journal::*;
pub use self::{
    add::*, auth::*, cache::*, config::*, container::*, init::*, inspect::*, package::*,
    publish::*, run::Run, self_update::*, validate::*,
};
use crate::error::PrettyError;
use git_version::git_version;

/// An executable CLI command.
pub(crate) trait CliCommand {
    type Output;

    fn run(self) -> Result<(), anyhow::Error>;
}

/// An executable CLI command that runs in an async context.
///
/// An [`AsyncCliCommand`] automatically implements [`CliCommand`] by creating
/// a new tokio runtime and blocking.
#[async_trait::async_trait]
pub(crate) trait AsyncCliCommand: Send + Sync {
    type Output: Send + Sync;

    async fn run_async(self) -> Result<Self::Output, anyhow::Error>;

    fn setup(
        &self,
        done: tokio::sync::oneshot::Receiver<()>,
    ) -> Option<JoinHandle<anyhow::Result<()>>> {
        if std::io::stdin().is_terminal() {
            return Some(tokio::task::spawn(async move {
                tokio::select! {
                    _ = done => {}

                    _ = tokio::signal::ctrl_c() => {
                        let term = console::Term::stdout();
                        let _ = term.show_cursor();
                        // https://learn.microsoft.com/en-us/cpp/c-runtime-library/signal-constants
                        #[cfg(target_os = "windows")]
                        std::process::exit(3);

                        // POSIX compliant OSs: 128 + SIGINT (2)
                        #[cfg(not(target_os = "windows"))]
                        std::process::exit(130);
                    }
                }

                Ok::<(), anyhow::Error>(())
            }));
        }

        None
    }
}

impl<O: Send + Sync, C: AsyncCliCommand<Output = O>> CliCommand for C {
    type Output = O;

    fn run(self) -> Result<(), anyhow::Error> {
        tokio::runtime::Runtime::new()?.block_on(async {
            let (snd, rcv) = tokio::sync::oneshot::channel();
            let handle = self.setup(rcv);

            if let Err(e) = AsyncCliCommand::run_async(self).await {
                if let Some(handle) = handle {
                    handle.abort();
                }
                return Err(e);
            }

            if let Some(handle) = handle {
                if snd.send(()).is_err() {
                    tracing::warn!("Failed to send 'done' signal to setup thread!");
                    handle.abort();
                } else {
                    handle.await??;
                }
            }

            Ok::<(), anyhow::Error>(())
        })?;

        Ok(())
    }
}

/// Command-line arguments for the Wasmer CLI.
#[derive(clap::Parser, Debug)]
#[clap(author, version)]
#[clap(disable_version_flag = true)] // handled manually
#[cfg_attr(feature = "headless", clap(
    name = "wasmer-headless",
    about = concat!("wasmer-headless ", env!("CARGO_PKG_VERSION")),
))]
#[cfg_attr(not(feature = "headless"), clap(
    name = "wasmer",
    about = concat!("wasmer ", env!("CARGO_PKG_VERSION")),
))]
pub struct WasmerCmd {
    /// Print version info and exit.
    #[clap(short = 'V', long)]
    version: bool,
    #[clap(flatten)]
    output: crate::logging::Output,
    #[clap(subcommand)]
    cmd: Option<Cmd>,
}

impl WasmerCmd {
    fn execute(self) -> Result<(), anyhow::Error> {
        let WasmerCmd {
            cmd,
            version,
            output,
        } = self;

        output.initialize_logging();

        if version {
            return print_version(output.is_verbose());
        }

        match cmd {
            Some(Cmd::GenManPage(cmd)) => cmd.execute(),
            Some(Cmd::GenCompletions(cmd)) => cmd.execute(),
            Some(Cmd::Run(options)) => options.execute(output),
            Some(Cmd::SelfUpdate(options)) => options.execute(),
            Some(Cmd::Cache(cache)) => cache.execute(),
            Some(Cmd::Validate(validate)) => validate.execute(),
            #[cfg(feature = "compiler")]
            Some(Cmd::Compile(compile)) => compile.execute(),
            // CreateExe and CreateObj commands are temporarily disabled
            // #[cfg(any(feature = "static-artifact-create", feature = "wasmer-artifact-create"))]
            // Some(Cmd::CreateExe(create_exe)) => create_exe.run(),
            // #[cfg(feature = "static-artifact-create")]
            // Some(Cmd::CreateObj(create_obj)) => create_obj.execute(),
            Some(Cmd::Config(config)) => config.run(),
            Some(Cmd::Inspect(inspect)) => inspect.execute(),
            Some(Cmd::Init(init)) => init.run(),
            Some(Cmd::Login(login)) => login.run(),
            Some(Cmd::Auth(auth)) => auth.run(),
            Some(Cmd::Publish(publish)) => publish.run().map(|_| ()),
            Some(Cmd::Package(cmd)) => match cmd {
                Package::Download(cmd) => cmd.execute(),
                Package::Build(cmd) => cmd.execute().map(|_| ()),
                Package::Tag(cmd) => cmd.run(),
                Package::Push(cmd) => cmd.run(),
                Package::Publish(cmd) => cmd.run().map(|_| ()),
                Package::Unpack(cmd) => cmd.execute(),
            },
            Some(Cmd::Container(cmd)) => match cmd {
                crate::commands::Container::Unpack(cmd) => cmd.execute(),
            },
            #[cfg(feature = "static-artifact-create")]
            Some(Cmd::GenCHeader(gen_heder)) => gen_heder.execute(),
            #[cfg(feature = "wast")]
            Some(Cmd::Wast(wast)) => wast.execute(),
            #[cfg(target_os = "linux")]
            Some(Cmd::Binfmt(binfmt)) => binfmt.execute(),
            Some(Cmd::Whoami(whoami)) => whoami.run(),
            Some(Cmd::Add(add)) => add.run(),

            // Deploy commands.
            Some(Cmd::Deploy(c)) => c.run(),
            Some(Cmd::App(apps)) => apps.run(),
            #[cfg(feature = "journal")]
            Some(Cmd::Journal(journal)) => journal.run(),
            Some(Cmd::Ssh(ssh)) => ssh.run(),
            Some(Cmd::Namespace(namespace)) => namespace.run(),
            Some(Cmd::Domain(namespace)) => namespace.run(),
            None => {
                WasmerCmd::command().print_long_help()?;
                // Note: clap uses an exit code of 2 when CLI parsing fails
                std::process::exit(2);
            }
        }
    }

    /// The main function for the Wasmer CLI tool.
    pub fn run() {
        // We allow windows to print properly colors
        #[cfg(windows)]
        colored::control::set_virtual_terminal(true).unwrap();

        PrettyError::report(Self::run_inner())
    }

    fn run_inner() -> Result<(), anyhow::Error> {
        let mut args_os = std::env::args_os();

        let args = args_os.next().into_iter();

        let mut binfmt_args = Vec::new();
        if is_binfmt_interpreter() {
            // In case of binfmt misc the first argument is wasmer-binfmt-interpreter, the second is the full path to the executable
            // and the third is the original string for the executable as originally called by the user.

            // For now we are only using the real path and ignoring the original executable name.
            // Ideally we would use the real path to load the file and the original name to pass it as argv[0] to the wasm module.

            let current_dir = std::env::current_dir().unwrap();
            let mut mount_paths = ["/home", "/etc", "/tmp", "/var", "/nix", "/opt", "/root"]
                .into_iter()
                .map(std::path::PathBuf::from)
                .filter(|path| {
                    if !path.is_dir() {
                        // Not a directory
                        return false;
                    }
                    if std::fs::read_dir(path).is_err() {
                        // No permissions
                        return false;
                    }
                    true
                })
                .collect_vec();
            if mount_paths
                .iter()
                .all(|path| !current_dir.starts_with(path))
            {
                // Mount the current dir if it is not already covered by a common path
                mount_paths.push(current_dir.clone());
            }

            binfmt_args.push("run".into());
            binfmt_args.push("--net".into());
            // TODO: This does not seem to work, needs further investigation.
            binfmt_args.push("--forward-host-env".into());
            for mount_path in mount_paths {
                if let Some(mount_path_str) = mount_path.to_str() {
                    binfmt_args.push(format!("--volume={mount_path_str}:{mount_path_str}").into());
                }
            }
            if let Some(current_dir_str) = current_dir.to_str() {
                binfmt_args.push(format!("--cwd={current_dir_str}").into());
            }
            binfmt_args.push("--quiet".into());
            binfmt_args.push("--".into());
            binfmt_args.push(args_os.next().unwrap());
            args_os.next().unwrap();
        };
        let args_vec = args.chain(binfmt_args).chain(args_os).collect_vec();

        match WasmerCmd::try_parse_from(args_vec.iter()) {
            Ok(args) => args.execute(),
            Err(e) => {
                let first_arg_is_subcommand = if let Some(first_arg) = args_vec.get(1) {
                    let mut ret = false;
                    let cmd = WasmerCmd::command();

                    for cmd in cmd.get_subcommands() {
                        if cmd.get_name() == first_arg {
                            ret = true;
                            break;
                        }
                    }

                    ret
                } else {
                    false
                };

                let might_be_wasmer_run = matches!(
                    e.kind(),
                    clap::error::ErrorKind::InvalidSubcommand
                        | clap::error::ErrorKind::UnknownArgument
                ) && !first_arg_is_subcommand;

                if might_be_wasmer_run && let Ok(run) = Run::try_parse_from(args_vec.iter()) {
                    // Try to parse the command using the `wasmer some/package`
                    // shorthand. Note that this has discoverability issues
                    // because it's not shown as part of the main argument
                    // parser's help, but that's fine.
                    let output = crate::logging::Output::default();
                    output.initialize_logging();
                    run.execute(output);
                }

                e.exit();
            }
        }
    }
}

#[derive(clap::Parser, Debug)]
#[allow(clippy::large_enum_variant)]
/// The options for the wasmer Command Line Interface
enum Cmd {
    /// Login into Wasmer
    Login(Login),

    #[clap(subcommand)]
    Auth(CmdAuth),

    /// Publish a package to a registry [alias: package publish]
    #[clap(name = "publish")]
    Publish(PackagePublish),

    /// Manage the local Wasmer cache
    Cache(Cache),

    /// Validate a WebAssembly binary
    Validate(Validate),

    /// Compile a WebAssembly binary
    #[cfg(feature = "compiler")]
    Compile(Compile),

    // Compile a WebAssembly binary into a native executable
    //
    // To use, you need to set the `WASMER_DIR` environment variable
    // to the location of your Wasmer installation. This will probably be `~/.wasmer`. It
    // should include a `lib`, `include` and `bin` subdirectories. To create an executable
    // you will need `libwasmer`, so by setting `WASMER_DIR` the CLI knows where to look for
    // header files and libraries.
    //
    // Example usage:
    //
    // ```text
    // $ # in two lines:
    // $ export WASMER_DIR=/home/user/.wasmer/
    // $ wasmer create-exe qjs.wasm -o qjs.exe # or in one line:
    // $ WASMER_DIR=/home/user/.wasmer/ wasmer create-exe qjs.wasm -o qjs.exe
    // $ file qjs.exe
    // qjs.exe: ELF 64-bit LSB pie executable, x86-64 ...
    // ```
    //
    // ## Cross-compilation
    //
    // Accepted target triple values must follow the
    // ['target_lexicon'](https://crates.io/crates/target-lexicon) crate format.
    //
    // The recommended targets we try to support are:
    //
    // - "x86_64-linux-gnu"
    // - "aarch64-linux-gnu"
    // - "x86_64-apple-darwin"
    // - "arm64-apple-darwin"
    // #[cfg(any(feature = "static-artifact-create", feature = "wasmer-artifact-create"))]
    // #[clap(name = "create-exe", verbatim_doc_comment)]
    // CreateExe(CreateExe),
    /// Compile a WebAssembly binary into an object file
    ///
    /// To use, you need to set the `WASMER_DIR` environment variable to the location of your
    /// Wasmer installation. This will probably be `~/.wasmer`. It should include a `lib`,
    /// `include` and `bin` subdirectories. To create an object you will need `libwasmer`, so by
    /// setting `WASMER_DIR` the CLI knows where to look for header files and libraries.
    ///
    /// Example usage:
    ///
    /// ```text
    /// $ # in two lines:
    /// $ export WASMER_DIR=/home/user/.wasmer/
    /// $ wasmer create-obj qjs.wasm --object-format symbols -o qjs.obj # or in one line:
    /// $ WASMER_DIR=/home/user/.wasmer/ wasmer create-exe qjs.wasm --object-format symbols -o qjs.obj
    /// $ file qjs.obj
    /// qjs.obj: ELF 64-bit LSB relocatable, x86-64 ...
    /// ```
    ///
    /// ## Cross-compilation
    ///
    /// Accepted target triple values must follow the
    /// ['target_lexicon'](https://crates.io/crates/target-lexicon) crate format.
    ///
    /// The recommended targets we try to support are:
    ///
    /// - "x86_64-linux-gnu"
    /// - "aarch64-linux-gnu"
    /// - "x86_64-apple-darwin"
    /// - "arm64-apple-darwin"
    // #[cfg(feature = "static-artifact-create")]
    // #[structopt(name = "create-obj", verbatim_doc_comment)]
    // CreateObj(CreateObj),

    ///
    /// Generate the C static_defs.h header file for the input .wasm module
    ///
    #[cfg(feature = "static-artifact-create")]
    GenCHeader(GenCHeader),

    /// Get various configuration information needed
    /// to compile programs which use Wasmer
    Config(Config),

    /// Update wasmer to the latest version
    #[clap(name = "self-update")]
    SelfUpdate(SelfUpdate),

    /// Inspect a WebAssembly file
    Inspect(Inspect),

    /// Initializes a new wasmer.toml file
    #[clap(name = "init")]
    Init(Init),

    /// Run spec testsuite
    #[cfg(feature = "wast")]
    Wast(Wast),

    /// Unregister and/or register wasmer as binfmt interpreter
    #[cfg(target_os = "linux")]
    Binfmt(Binfmt),

    /// Shows the current logged in user for the current active registry
    Whoami(Whoami),

    /// Add a Wasmer package's bindings to your application
    Add(CmdAdd),

    /// Run a WebAssembly file or Wasmer container
    #[clap(alias = "run-unstable")]
    Run(Run),

    /// Manage journals (compacting, inspecting, filtering, ...)
    #[cfg(feature = "journal")]
    #[clap(subcommand)]
    Journal(CmdJournal),

    #[clap(subcommand)]
    Package(crate::commands::Package),

    #[clap(subcommand)]
    Container(crate::commands::Container),

    // Edge commands
    /// Deploy apps to Wasmer Edge [alias: app deploy]
    Deploy(crate::commands::app::deploy::CmdAppDeploy),

    /// Create and manage Wasmer Edge apps
    #[clap(subcommand, alias = "apps")]
    App(crate::commands::app::CmdApp),

    /// Run commands/packages on Wasmer Edge in an interactive shell session
    Ssh(crate::commands::ssh::CmdSsh),

    /// Manage Wasmer namespaces
    #[clap(subcommand, alias = "namespaces")]
    Namespace(crate::commands::namespace::CmdNamespace),

    /// Manage DNS records
    #[clap(subcommand, alias = "domains")]
    Domain(crate::commands::domain::CmdDomain),

    /// Generate autocompletion for different shells
    #[clap(name = "gen-completions")]
    GenCompletions(crate::commands::gen_completions::CmdGenCompletions),

    /// Generate man pages
    #[clap(name = "gen-man", hide = true)]
    GenManPage(crate::commands::gen_manpage::CmdGenManPage),
}

fn is_binfmt_interpreter() -> bool {
    cfg_if::cfg_if! {
        if #[cfg(target_os = "linux")] {
            // Note: we'll be invoked by the kernel as Binfmt::FILENAME
            let binary_path = match std::env::args_os().next() {
                Some(path) => std::path::PathBuf::from(path),
                None => return false,
            };
            binary_path.file_name().and_then(|f| f.to_str()) == Some(Binfmt::FILENAME)
        } else {
            false
        }
    }
}

fn print_version(verbose: bool) -> Result<(), anyhow::Error> {
    if !verbose {
        println!("wasmer {}", env!("CARGO_PKG_VERSION"));
        return Ok(());
    }

    println!("wasmer {}", env!("CARGO_PKG_VERSION"));
    println!("binary: {}", env!("CARGO_PKG_NAME"));
    let git_hash = git_version!(
        args = [
            "--abbrev=40",
            "--always",
            "--dirty=-modified",
            "--exclude=*"
        ],
        fallback = "",
    );
    if !git_hash.is_empty() {
        println!("commit-hash: {git_hash}",);
    }
    if !env!("WASMER_REPRODUCIBLE_BUILD")
        .parse::<bool>()
        .expect("build-time variable expected")
    {
        println!("commit-date: {}", env!("WASMER_BUILD_DATE"));
    }
    println!("host: {}", target_lexicon::HOST);

    let cpu_features = wasmer_types::target::CpuFeature::for_host()
        .iter()
        .map(|f| f.to_string())
        .join(" ");
    println!("CPU flags: {cpu_features}");

    let mut runtimes = Vec::new();
    if cfg!(feature = "singlepass") {
        runtimes.push("Singlepass");
    }
    if cfg!(feature = "cranelift") {
        runtimes.push("Cranelift");
    }
    if cfg!(feature = "llvm") {
        runtimes.push("LLVM");
    }
    if cfg!(feature = "wamr") {
        runtimes.push("WAMR");
    }
    if cfg!(feature = "wasmi") {
        runtimes.push("Wasmi");
    }
    if cfg!(feature = "v8") {
        runtimes.push("V8");
    }
    println!("runtimes: {}", runtimes.join(", "));

    #[allow(clippy::useless_vec)]
    #[allow(unused_mut)]
    let mut features = vec!["wasix".to_string()];
    #[cfg(feature = "napi-v8")]
    {
        for napi_version in enum_iterator::all::<wasmer_napi::NapiVersion>() {
            if !matches!(napi_version, wasmer_napi::NapiVersion::Unknown) {
                features.push(napi_version.to_string());
            }
        }
        features.push(wasmer_napi::NAPI_EXTENSION_WASMER_MODULE_NAME.to_string());
    }
    println!("features: {}", features.join(", "));

    Ok(())
}
