// Copyright 2018-2026 the Deno authors. MIT license.

use std::collections::BTreeMap;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;

use deno_cache_dir::file_fetcher::CacheSetting;
use deno_core::anyhow;
use deno_core::anyhow::Context;
use deno_core::error::AnyError;
use deno_npm_installer::PackagesAllowedScripts;
use deno_runtime::deno_permissions::PathQueryDescriptor;
use deno_semver::jsr::JsrPackageReqReference;
use deno_semver::npm::NpmPackageReqReference;
use deno_semver::package::PackageReq;
use node_resolver::BinValue;

use crate::args::DenoXShimName;
use crate::args::Flags;
use crate::args::FlagsExt;
use crate::args::XFlags;
use crate::args::XFlagsKind;
use crate::factory::CliFactory;
use crate::node::CliNodeResolver;
use crate::npm::CliManagedNpmResolver;
use crate::npm::CliNpmResolver;
use crate::tools::pm::CacheTopLevelDepsOptions;
use crate::util::console::ConfirmOptions;
use crate::util::console::confirm;
use crate::util::draw_thread::DrawThread;
use crate::util::fs::canonicalize_path;

async fn resolve_local_bins(
  node_resolver: &CliNodeResolver,
  npm_resolver: &CliNpmResolver,
  factory: &CliFactory,
) -> Result<BTreeMap<String, BinValue>, AnyError> {
  match &npm_resolver {
    deno_resolver::npm::NpmResolver::Byonm(npm_resolver) => {
      let node_modules_dir = npm_resolver.root_node_modules_path().unwrap();
      let bin_dir = node_modules_dir.join(".bin");
      Ok(node_resolver.resolve_npm_commands_from_bin_dir(&bin_dir))
    }
    deno_resolver::npm::NpmResolver::Managed(npm_resolver) => {
      let mut all_bins = BTreeMap::new();
      for id in npm_resolver.resolution().top_level_packages() {
        let package_folder =
          npm_resolver.resolve_pkg_folder_from_pkg_id(&id)?;
        let bins = match node_resolver
          .resolve_npm_binary_commands_for_package(&package_folder)
        {
          Ok(bins) => bins,
          Err(_) => {
            crate::tools::pm::cache_top_level_deps(
              factory,
              None,
              CacheTopLevelDepsOptions {
                lockfile_only: false,
              },
            )
            .await?;
            node_resolver
              .resolve_npm_binary_commands_for_package(&package_folder)?
          }
        };
        for (command, bin_value) in bins {
          all_bins.insert(command.clone(), bin_value.clone());
        }
      }
      Ok(all_bins)
    }
  }
}

fn run_js_file(
  main_module: &deno_core::url::Url,
  deno_args: &[String],
  argv: &[String],
  npm_process_state: Option<String>,
  npm: bool,
) -> Result<i32, AnyError> {
  use deno_runtime::deno_io::FromRawIoHandle;

  let deno_exe = std::env::current_exe()
    .and_then(|p| canonicalize_path(&p))
    .context("Failed to get current executable path")?;

  let mut args: Vec<std::ffi::OsString> = vec!["run".into()];
  args.extend(deno_args.iter().map(|s| s.into()));
  args.push(main_module.as_str().into());
  args.extend(argv.iter().map(|s| s.into()));

  let mut command = std::process::Command::new(deno_exe);
  command.args(&args);

  let _temp_file = if let Some(state) = &npm_process_state {
    let fd =
      deno_runtime::deno_process::npm_process_state_tempfile(state.as_bytes())
        .context("Failed to create npm process state tempfile")?;
    command.env(
      deno_runtime::deno_process::NPM_RESOLUTION_STATE_FD_ENV_VAR_NAME,
      (fd as usize).to_string(),
    );
    // Keep the file alive until the subprocess completes
    // SAFETY: fd is valid
    Some(unsafe { std::fs::File::from_raw_io_handle(fd) })
  } else {
    None
  };

  if npm {
    crate::tools::run::set_npm_user_agent();
  }

  #[cfg(unix)]
  {
    use std::os::unix::process::CommandExt;
    Err(command.exec().into())
  }
  #[cfg(not(unix))]
  {
    let mut child =
      command.spawn().context("Failed to spawn deno subprocess")?;
    let status = child.wait().context("Failed to wait for deno subprocess")?;
    Ok(status.code().unwrap_or(1))
  }
}

pub(crate) fn get_npm_process_state(
  npm_resolver: &CliNpmResolver,
) -> Option<String> {
  match npm_resolver {
    deno_resolver::npm::NpmResolver::Managed(managed) => {
      let linker_mode = match managed.linker_mode() {
        deno_config::deno_json::NodeModulesLinkerMode::Hoisted => {
          deno_npm_installer::process_state::NpmProcessStateLinkerMode::Hoisted
        }
        deno_config::deno_json::NodeModulesLinkerMode::Isolated => {
          deno_npm_installer::process_state::NpmProcessStateLinkerMode::Isolated
        }
      };
      Some(
        deno_npm_installer::process_state::NpmProcessState::new_managed(
          managed.resolution().serialized_valid_snapshot(),
          managed.root_node_modules_path(),
          linker_mode,
        )
        .as_serialized(),
      )
    }
    deno_resolver::npm::NpmResolver::Byonm(_) => None,
  }
}

pub(crate) fn run_bin_value(
  factory: &CliFactory,
  flags: &Flags,
  bin_value: BinValue,
  npm_process_state: Option<String>,
  unstable_args: &[String],
) -> Result<i32, AnyError> {
  match bin_value {
    BinValue::JsFile(path_buf) => {
      let path = deno_path_util::url_from_file_path(path_buf.as_ref())?;
      let mut deno_args = flags.to_permission_args();
      deno_args.extend(unstable_args.iter().cloned());

      run_js_file(&path, &deno_args, &flags.argv, npm_process_state, true)
    }
    BinValue::Executable(mut path_buf) => {
      if cfg!(windows) && path_buf.extension().is_none() {
        // prefer cmd shim over sh
        path_buf.set_extension("cmd");
        if !path_buf.exists() {
          //  just fall back to original path
          path_buf.set_extension("");
        }
      }
      let permissions = factory.root_permissions_container()?;
      permissions.check_run(
        &deno_runtime::deno_permissions::RunQueryDescriptor::Path(
          PathQueryDescriptor::new(
            &factory.sys(),
            std::borrow::Cow::Borrowed(path_buf.as_ref()),
          )?,
        ),
        "entrypoint",
      )?;
      let mut child = std::process::Command::new(path_buf)
        .args(&flags.argv)
        .spawn()
        .context("Failed to spawn command")?;
      let status = child.wait()?;
      Ok(status.code().unwrap_or(1))
    }
  }
}

/// Try to find a bin value from a map of bins, with fallbacks for scoped package names
/// and single-bin packages.
pub(crate) fn find_bin_value(
  bins: &BTreeMap<String, BinValue>,
  bin_name: &str,
) -> Option<BinValue> {
  bins
    .get(bin_name)
    .or_else(|| {
      // Try the package name without scope as fallback
      if bin_name.starts_with('@') && bin_name.contains('/') {
        bin_name
          .split('/')
          .next_back()
          .and_then(|name| bins.get(name))
      } else {
        None
      }
    })
    .or_else(|| {
      // If there's only one bin, use it
      if bins.len() == 1 {
        bins.values().next()
      } else {
        None
      }
    })
    .cloned()
}

async fn maybe_run_local_npm_bin(
  factory: &CliFactory,
  flags: &Flags,
  node_resolver: &CliNodeResolver,
  npm_resolver: &CliNpmResolver,
  command: &str,
  unstable_args: &[String],
) -> Result<Option<i32>, AnyError> {
  let mut bins =
    resolve_local_bins(node_resolver, npm_resolver, factory).await?;
  let bin_value = if let Some(bin_value) = bins.remove(command) {
    bin_value
  } else if let Some(bin_value) = {
    let command = if command.starts_with("@") && command.contains("/") {
      command.split("/").last().unwrap()
    } else {
      command
    };
    bins.remove(command)
  } {
    bin_value
  } else {
    return Ok(None);
  };

  let npm_process_state = get_npm_process_state(npm_resolver);
  run_bin_value(factory, flags, bin_value, npm_process_state, unstable_args)
    .map(Some)
}

enum XTempDir {
  Existing(PathBuf),
  New(PathBuf),
}
impl XTempDir {
  fn path(&self) -> &PathBuf {
    match self {
      XTempDir::Existing(path) => path,
      XTempDir::New(path) => path,
    }
  }
}

fn create_package_temp_dir(
  prefix: Option<&str>,
  package_req: &PackageReq,
  reload: bool,
  deno_dir: &Path,
) -> Result<XTempDir, AnyError> {
  let mut package_req_folder = String::from(prefix.unwrap_or(""));
  package_req_folder.push_str(
    &package_req
      .to_string()
      .replace("/", "__")
      .replace(">", "gt")
      .replace("<", "lt"),
  );
  let temp_dir = deno_dir.join("deno_x_cache").join(package_req_folder);
  if temp_dir.exists() {
    if reload || !temp_dir.join("deno.lock").exists() {
      std::fs::remove_dir_all(&temp_dir)?;
    } else {
      let temp_dir = canonicalize_path(&temp_dir).unwrap_or(temp_dir);
      return Ok(XTempDir::Existing(temp_dir));
    }
  }
  std::fs::create_dir_all(&temp_dir)?;
  let package_json_path = temp_dir.join("package.json");
  std::fs::write(&package_json_path, "{}")?;
  let deno_json_path = temp_dir.join("deno.json");
  std::fs::write(&deno_json_path, r#"{"nodeModulesDir": "auto"}"#)?;

  let temp_dir = canonicalize_path(&temp_dir).unwrap_or(temp_dir);
  Ok(XTempDir::New(temp_dir))
}

fn write_shim(
  out_dir: &Path,
  shim_name: DenoXShimName,
) -> Result<(), AnyError> {
  #[cfg(unix)]
  {
    use std::os::unix::fs::PermissionsExt;
    let out_path = out_dir.join(shim_name.name());
    if let DenoXShimName::Other(_) = shim_name {
      std::fs::write(
        &out_path,
        r##"#!/bin/sh
SCRIPT_DIR="$(dirname -- "$(readlink -f -- "$0")")"
exec "$SCRIPT_DIR/deno" x "$@"
"##
          .as_bytes(),
      )?;
      let mut permissions = std::fs::metadata(&out_path)?.permissions();
      permissions.set_mode(0o755);
      std::fs::set_permissions(&out_path, permissions)?;
    } else {
      match std::os::unix::fs::symlink("./deno", &out_path) {
        Ok(_) => {}
        Err(e) => match e.kind() {
          std::io::ErrorKind::AlreadyExists => {
            std::fs::remove_file(&out_path)?;
            std::os::unix::fs::symlink("./deno", &out_path)?;
          }
          _ => return Err(e.into()),
        },
      }
    }
  }

  #[cfg(windows)]
  {
    let out_path = out_dir.join(format!("{}.cmd", shim_name.name()));
    std::fs::write(
      out_path,
      r##"@echo off
"%~dp0deno.exe" x %*
exit /b %ERRORLEVEL%
"##,
    )?;
  }

  Ok(())
}

pub async fn run(flags: Arc<Flags>, x_flags: XFlags) -> Result<i32, AnyError> {
  let command_flags = match x_flags.kind {
    XFlagsKind::InstallAlias(shim_name) => {
      let exe = std::env::current_exe()?;
      let out_dir = exe.parent().unwrap();
      write_shim(out_dir, shim_name)?;
      return Ok(0);
    }
    XFlagsKind::Command(command) => command,
    XFlagsKind::Print => {
      let factory = CliFactory::from_flags(flags.clone());
      let npm_resolver = factory.npm_resolver().await?;
      let node_resolver = factory.node_resolver().await?;
      let bins =
        resolve_local_bins(node_resolver, npm_resolver, &factory).await?;
      if bins.is_empty() {
        log::info!("No local commands found");
        return Ok(0);
      }
      log::info!("Available (local) commands:");
      for command in bins.keys() {
        log::info!("  {}", command);
      }
      return Ok(0);
    }
  };
  let factory = CliFactory::from_flags(flags.clone());
  let cli_options = factory.cli_options()?;
  let unstable_args = cli_options.unstable_args();
  let npm_resolver = factory.npm_resolver().await?;
  let node_resolver = factory.node_resolver().await?;
  let result = maybe_run_local_npm_bin(
    &factory,
    &flags,
    node_resolver,
    npm_resolver,
    &command_flags.command,
    &unstable_args,
  )
  .await?;
  if let Some(exit_code) = result {
    return Ok(exit_code);
  }

  // When --package is specified, the command is the binary name and
  // the package flag specifies which package to install. Combine them
  // into a single specifier like "npm:package/binary" so the existing
  // resolution flow handles it correctly.
  let effective_command = if let Some(ref package) = command_flags.package {
    format!("{}/{}", package, command_flags.command)
  } else {
    command_flags.command.clone()
  };

  let is_file_like = effective_command.starts_with('.')
    || effective_command.starts_with('/')
    || effective_command.starts_with('~')
    || effective_command.starts_with('\\')
    || Path::new(&effective_command).extension().is_some();
  if is_file_like && Path::new(&effective_command).is_file() {
    return Err(anyhow::anyhow!(
      "Use 'deno run' to run a local file directly, 'deno x' is intended for running commands from packages."
    ));
  }

  let thing_to_run = match deno_core::url::Url::parse(&effective_command) {
    Ok(url) => {
      if url.scheme() == "npm" {
        let req_ref = NpmPackageReqReference::from_specifier(&url)?;
        ReqRefOrUrl::Npm(req_ref)
      } else if url.scheme() == "jsr" {
        let req_ref = JsrPackageReqReference::from_specifier(&url)?;
        ReqRefOrUrl::Jsr(req_ref)
      } else {
        ReqRefOrUrl::Url(url)
      }
    }
    Err(deno_core::url::ParseError::RelativeUrlWithoutBase) => {
      let new_command = format!("npm:{}", effective_command);
      let req_ref = NpmPackageReqReference::from_str(&new_command)?;
      ReqRefOrUrl::Npm(req_ref)
    }
    Err(e) => {
      return Err(e.into());
    }
  };

  let cache_setting = cli_options.cache_setting();
  let reload = matches!(cache_setting, CacheSetting::ReloadAll);
  match thing_to_run {
    ReqRefOrUrl::Npm(npm_package_req_reference) => {
      // First try to resolve from the local project
      if let Ok(package_folder) = npm_resolver
        .resolve_pkg_folder_from_deno_module_req(
          npm_package_req_reference.req(),
          &deno_path_util::url_from_directory_path(cli_options.initial_cwd())
            .unwrap(),
        )
      {
        let bin_name =
          if let Some(sub_path) = npm_package_req_reference.sub_path() {
            sub_path
          } else {
            npm_package_req_reference.req().name.as_str()
          };

        let bins = node_resolver
          .resolve_npm_binary_commands_for_package(&package_folder)?;
        if let Some(bin_value) = find_bin_value(&bins, bin_name) {
          let npm_process_state = get_npm_process_state(npm_resolver);
          return run_bin_value(
            &factory,
            &flags,
            bin_value,
            npm_process_state,
            &unstable_args,
          );
        }
      }

      // Fall back to autoinstall
      let (managed_flags, managed_factory) = autoinstall_package(
        ReqRef::Npm(&npm_package_req_reference),
        &flags,
        reload,
        command_flags.yes,
        &command_flags.ignore_scripts,
        &factory.deno_dir()?.root,
      )
      .await?;
      let mut runner_flags = (*managed_flags).clone();
      runner_flags.node_modules_dir =
        Some(deno_config::deno_json::NodeModulesDirMode::Manual);
      let runner_flags = Arc::new(runner_flags);
      let runner_factory = CliFactory::from_flags(runner_flags.clone());
      let runner_node_resolver = runner_factory.node_resolver().await?;
      let runner_npm_resolver = runner_factory.npm_resolver().await?;

      let bin_name =
        if let Some(sub_path) = npm_package_req_reference.sub_path() {
          sub_path
        } else {
          npm_package_req_reference.req().name.as_str()
        };

      let res = maybe_run_local_npm_bin(
        &runner_factory,
        &runner_flags,
        runner_node_resolver,
        runner_npm_resolver,
        bin_name,
        &unstable_args,
      )
      .await?;
      if let Some(exit_code) = res {
        Ok(exit_code)
      } else {
        let managed_npm_resolver =
          managed_factory.npm_resolver().await?.as_managed().unwrap();
        let bin_commands = bin_commands_for_package(
          runner_node_resolver,
          managed_npm_resolver,
          npm_package_req_reference.req(),
        )?;
        let fallback_name = if bin_commands.len() == 1 {
          Some(bin_commands.keys().next().unwrap())
        } else {
          None
        };

        if let Some(fallback_name) = fallback_name
          && let Some(exit_code) = maybe_run_local_npm_bin(
            &runner_factory,
            &runner_flags,
            runner_node_resolver,
            runner_npm_resolver,
            fallback_name.as_ref(),
            &unstable_args,
          )
          .await?
        {
          return Ok(exit_code);
        }

        Err(anyhow::anyhow!(
          "Unable to choose binary for {}\n  Available bins:\n{}",
          effective_command,
          bin_commands
            .keys()
            .map(|k| format!("    {}", k))
            .collect::<Vec<_>>()
            .join("\n")
        ))
      }
    }
    ReqRefOrUrl::Jsr(jsr_package_req_reference) => {
      let (_new_flags, new_factory) = autoinstall_package(
        ReqRef::Jsr(&jsr_package_req_reference),
        &flags,
        reload,
        command_flags.yes,
        &command_flags.ignore_scripts,
        &factory.deno_dir()?.root,
      )
      .await?;

      let npm_resolver = new_factory.npm_resolver().await?;
      let npm_process_state = get_npm_process_state(npm_resolver);

      let mut deno_args = flags.to_permission_args();
      deno_args.extend(unstable_args.iter().cloned());
      let url =
        deno_core::url::Url::parse(&jsr_package_req_reference.to_string())?;
      run_js_file(&url, &deno_args, &flags.argv, npm_process_state, false)
    }
    ReqRefOrUrl::Url(url) => {
      let mut deno_args = flags.to_permission_args();
      deno_args.extend(unstable_args.iter().cloned());
      run_js_file(&url, &deno_args, &flags.argv, None, false)
    }
  }
}

fn bin_commands_for_package(
  node_resolver: &CliNodeResolver,
  managed_npm_resolver: &CliManagedNpmResolver,
  package_req: &PackageReq,
) -> Result<BTreeMap<String, BinValue>, AnyError> {
  let pkg_id =
    managed_npm_resolver.resolve_pkg_id_from_deno_module_req(package_req)?;
  let package_folder =
    managed_npm_resolver.resolve_pkg_folder_from_pkg_id(&pkg_id)?;
  node_resolver
    .resolve_npm_binary_commands_for_package(&package_folder)
    .map_err(Into::into)
}

async fn autoinstall_package(
  req_ref: ReqRef<'_>,
  old_flags: &Flags,
  reload: bool,
  yes: bool,
  ignore_scripts: &PackagesAllowedScripts,
  deno_dir: &Path,
) -> Result<(Arc<Flags>, CliFactory), AnyError> {
  fn make_new_flags(
    old_flags: &Flags,
    temp_dir: &Path,
    ignore_scripts: &PackagesAllowedScripts,
  ) -> Arc<Flags> {
    let mut new_flags = (*old_flags).clone();
    new_flags.node_modules_dir =
      Some(deno_config::deno_json::NodeModulesDirMode::Auto);
    let temp_node_modules = temp_dir.join("node_modules");
    new_flags.internal.root_node_modules_dir_override = Some(temp_node_modules);
    new_flags.config_flag = crate::args::ConfigFlag::Path(
      temp_dir.join("deno.json").to_string_lossy().into_owned(),
    );
    match ignore_scripts {
      PackagesAllowedScripts::All => {
        new_flags.allow_scripts = PackagesAllowedScripts::None;
        new_flags.deny_scripts.clear();
      }
      PackagesAllowedScripts::Some(package_reqs) => {
        if matches!(old_flags.allow_scripts, PackagesAllowedScripts::None) {
          new_flags.allow_scripts = PackagesAllowedScripts::All;
        }
        new_flags.deny_scripts = package_reqs.clone();
      }
      PackagesAllowedScripts::None => {
        if matches!(old_flags.allow_scripts, PackagesAllowedScripts::None) {
          new_flags.allow_scripts = PackagesAllowedScripts::All;
        }
      }
    };

    log::debug!("new_flags: {:?}", new_flags);

    Arc::new(new_flags)
  }
  let temp_dir = create_package_temp_dir(
    Some(req_ref.prefix()),
    req_ref.req(),
    reload,
    deno_dir,
  )?;

  let new_flags = make_new_flags(old_flags, temp_dir.path(), ignore_scripts);
  let new_factory = CliFactory::from_flags(new_flags.clone());

  match temp_dir {
    XTempDir::Existing(_) => Ok((new_flags, new_factory)),

    XTempDir::New(temp_dir) => {
      let confirmed = yes
        || match confirm(ConfirmOptions {
          default: true,
          message: format!("Install {}?", req_ref),
        }) {
          Some(true) => true,
          Some(false) => false,
          None if !DrawThread::is_supported() => {
            log::warn!(
              "Unable to prompt, installing {} without confirmation",
              req_ref.req()
            );
            true
          }
          None => false,
        };
      if !confirmed {
        return Err(anyhow::anyhow!("Installation rejected"));
      }
      match req_ref {
        ReqRef::Npm(req_ref) => {
          let pkg_json = temp_dir.join("package.json");
          std::fs::write(
            &pkg_json,
            format!(
              "{{\"dependencies\": {{\"{}\": \"{}\"}} }}",
              req_ref.req().name,
              req_ref.req().version_req
            ),
          )?;
        }
        ReqRef::Jsr(req_ref) => {
          let deno_json = temp_dir.join("deno.json");
          std::fs::write(
            &deno_json,
            format!(
              "{{ \"nodeModulesDir\": \"manual\", \"imports\": {{ \"{}\": \"{}\" }} }}",
              req_ref.req().name,
              format_args!("jsr:{}", req_ref.req())
            ),
          )?;
        }
      }

      crate::tools::pm::cache_top_level_deps(
        &new_factory,
        None,
        CacheTopLevelDepsOptions {
          lockfile_only: false,
        },
      )
      .await?;

      if let Some(lockfile) = new_factory.maybe_lockfile().await? {
        lockfile.write_if_changed()?;
      }
      Ok((new_flags, new_factory))
    }
  }
}

#[derive(Debug, Clone, Copy)]
enum ReqRef<'a> {
  Npm(&'a NpmPackageReqReference),
  Jsr(&'a JsrPackageReqReference),
}
impl<'a> std::fmt::Display for ReqRef<'a> {
  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    match self {
      ReqRef::Npm(req) => write!(f, "{}", req),
      ReqRef::Jsr(req) => write!(f, "{}", req),
    }
  }
}
impl<'a> ReqRef<'a> {
  fn req(&self) -> &PackageReq {
    match self {
      ReqRef::Npm(req) => req.req(),
      ReqRef::Jsr(req) => req.req(),
    }
  }

  fn prefix(&self) -> &str {
    match self {
      ReqRef::Npm(_) => "npm-",
      ReqRef::Jsr(_) => "jsr-",
    }
  }
}

enum ReqRefOrUrl {
  Npm(NpmPackageReqReference),
  Jsr(JsrPackageReqReference),
  Url(deno_core::url::Url),
}
