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

use std::borrow::Cow;
use std::collections::BTreeSet;
use std::path::Path;
use std::path::PathBuf;

use anyhow::bail;
use boxed_error::Boxed;
use deno_cache_dir::DenoDirResolutionError;
use deno_cache_dir::GlobalHttpCacheRc;
use deno_cache_dir::GlobalOrLocalHttpCache;
use deno_cache_dir::LocalHttpCache;
use deno_cache_dir::npm::NpmCacheDir;
use deno_config::deno_json::MinimumDependencyAgeConfig;
use deno_config::deno_json::NewestDependencyDate;
use deno_config::deno_json::NodeModulesDirMode;
use deno_config::deno_json::NodeModulesLinkerMode;
use deno_config::workspace::FolderConfigs;
use deno_config::workspace::VendorEnablement;
use deno_config::workspace::WorkspaceDirectory;
use deno_config::workspace::WorkspaceDirectoryEmptyOptions;
use deno_config::workspace::WorkspaceDirectoryRc;
use deno_config::workspace::WorkspaceDiscoverError;
use deno_config::workspace::WorkspaceDiscoverOptions;
use deno_config::workspace::WorkspaceDiscoverStart;
use deno_maybe_sync::MaybeSend;
use deno_maybe_sync::MaybeSync;
use deno_maybe_sync::new_rc;
pub use deno_npm::NpmSystemInfo;
use deno_npm::resolution::NpmOverrides;
use deno_npm::resolution::NpmVersionResolver;
use deno_path_util::fs::canonicalize_path_maybe_not_exists;
use deno_semver::package::PackageName;
use futures::future::FutureExt;
use node_resolver::DenoIsBuiltInNodeModuleChecker;
use node_resolver::NodeResolver;
use node_resolver::NodeResolverOptions;
use node_resolver::NodeResolverRc;
use node_resolver::PackageJsonResolver;
use node_resolver::PackageJsonResolverRc;
use node_resolver::analyze::CjsModuleExportAnalyzerRc;
use node_resolver::analyze::NodeCodeTranslator;
use node_resolver::analyze::NodeCodeTranslatorRc;
use node_resolver::cache::NodeResolutionSys;
use thiserror::Error;
use url::Url;

use crate::DefaultRawDenoResolverRc;
use crate::DenoResolverOptions;
use crate::NodeAndNpmResolvers;
use crate::NpmCacheDirRc;
use crate::RawDenoResolver;
use crate::WorkspaceResolverRc;
use crate::cache::DenoDir;
use crate::cache::DenoDirOptions;
use crate::cache::DenoDirProvider;
use crate::cache::DenoDirProviderRc;
use crate::cache::DenoDirSys;
use crate::cache::EmitCache;
use crate::cache::EmitCacheRc;
use crate::cjs::CjsTracker;
use crate::cjs::CjsTrackerRc;
use crate::cjs::IsCjsResolutionMode;
use crate::cjs::analyzer::DenoCjsCodeAnalyzer;
use crate::cjs::analyzer::NodeAnalysisCacheRc;
use crate::cjs::analyzer::NullNodeAnalysisCache;
use crate::deno_json::CompilerOptionsOverrides;
use crate::deno_json::CompilerOptionsResolver;
use crate::deno_json::CompilerOptionsResolverRc;
use crate::import_map::WorkspaceExternalImportMapLoader;
use crate::import_map::WorkspaceExternalImportMapLoaderRc;
use crate::loader::AllowJsonImports;
use crate::loader::DenoNpmModuleLoaderRc;
use crate::loader::NpmModuleLoader;
use crate::lockfile::LockfileLock;
use crate::lockfile::LockfileLockRc;
use crate::npm::ByonmNpmResolverCreateOptions;
use crate::npm::CreateInNpmPkgCheckerOptions;
use crate::npm::DenoInNpmPackageChecker;
use crate::npm::NpmReqResolver;
use crate::npm::NpmReqResolverOptions;
use crate::npm::NpmReqResolverRc;
use crate::npm::NpmResolver;
use crate::npm::NpmResolverCreateOptions;
use crate::npm::managed::ManagedInNpmPkgCheckerCreateOptions;
use crate::npm::managed::ManagedNpmResolverCreateOptions;
use crate::npm::managed::NpmResolutionCellRc;
use crate::npmrc::NpmRcDiscoverError;
use crate::npmrc::ResolvedNpmRcRc;
use crate::npmrc::discover_npmrc_from_workspace;
use crate::workspace::FsCacheOptions;
use crate::workspace::PackageJsonDepResolution;
use crate::workspace::SloppyImportsOptions;
use crate::workspace::WorkspaceNpmLinkPackagesRc;
use crate::workspace::WorkspaceResolver;

const DEFAULT_MINIMUM_DEPENDENCY_AGE_MINUTES: i64 = 1440;

// todo(https://github.com/rust-lang/rust/issues/109737): remove once_cell after get_or_try_init is stabilized
#[cfg(feature = "sync")]
type Deferred<T> = once_cell::sync::OnceCell<T>;
#[cfg(not(feature = "sync"))]
type Deferred<T> = once_cell::unsync::OnceCell<T>;

pub type DenoCjsModuleExportAnalyzerRc<TSys> = CjsModuleExportAnalyzerRc<
  DenoCjsCodeAnalyzer<TSys>,
  DenoInNpmPackageChecker,
  DenoIsBuiltInNodeModuleChecker,
  NpmResolver<TSys>,
  TSys,
>;
pub type DenoNodeCodeTranslatorRc<TSys> = NodeCodeTranslatorRc<
  DenoCjsCodeAnalyzer<TSys>,
  DenoInNpmPackageChecker,
  DenoIsBuiltInNodeModuleChecker,
  NpmResolver<TSys>,
  TSys,
>;
#[allow(clippy::disallowed_types, reason = "definition")]
pub type NpmVersionResolverRc = deno_maybe_sync::MaybeArc<NpmVersionResolver>;
#[cfg(feature = "graph")]
#[allow(clippy::disallowed_types, reason = "definition")]
pub type JsrVersionResolverRc =
  deno_maybe_sync::MaybeArc<deno_graph::packages::JsrVersionResolver>;

#[derive(Debug, Boxed)]
pub struct HttpCacheCreateError(pub Box<HttpCacheCreateErrorKind>);

#[derive(Debug, Error)]
pub enum HttpCacheCreateErrorKind {
  #[error(transparent)]
  DenoDirResolution(#[from] DenoDirResolutionError),
  #[error(transparent)]
  WorkspaceDiscover(#[from] WorkspaceDiscoverError),
}

#[derive(Debug, Boxed)]
pub struct NpmCacheDirCreateError(pub Box<NpmCacheDirCreateErrorKind>);

#[derive(Debug, Error)]
pub enum NpmCacheDirCreateErrorKind {
  #[error(transparent)]
  DenoDirResolution(#[from] DenoDirResolutionError),
  #[error(transparent)]
  NpmRcCreate(#[from] NpmRcCreateError),
}

#[derive(Debug, Boxed)]
pub struct NpmRcCreateError(pub Box<NpmRcCreateErrorKind>);

#[derive(Debug, Error)]
pub enum NpmRcCreateErrorKind {
  #[error(transparent)]
  WorkspaceDiscover(#[from] WorkspaceDiscoverError),
  #[error(transparent)]
  NpmRcDiscover(#[from] NpmRcDiscoverError),
}

#[derive(Debug, Default)]
pub enum ConfigDiscoveryOption {
  #[default]
  DiscoverCwd,
  Discover {
    start_paths: Vec<PathBuf>,
  },
  Path(PathBuf),
  Disabled,
}

/// Resolves the JSR registry URL to use for the given system.
pub fn resolve_jsr_url(sys: &impl sys_traits::EnvVar) -> Url {
  let env_var_name = "JSR_URL";
  if let Ok(registry_url) = sys.env_var(env_var_name) {
    // ensure there is a trailing slash for the directory
    let registry_url = format!("{}/", registry_url.trim_end_matches('/'));
    match Url::parse(&registry_url) {
      Ok(url) => {
        return url;
      }
      Err(err) => {
        log::debug!("Invalid {} environment variable: {:#}", env_var_name, err,);
      }
    }
  }

  Url::parse("https://jsr.io/").unwrap()
}

#[async_trait::async_trait(?Send)]
pub trait SpecifiedImportMapProvider:
  std::fmt::Debug + MaybeSend + MaybeSync
{
  async fn get(
    &self,
  ) -> Result<Option<crate::workspace::SpecifiedImportMap>, anyhow::Error>;
}

#[derive(Debug)]
pub struct NpmProcessStateOptions {
  pub node_modules_dir: Option<Cow<'static, str>>,
  pub is_byonm: bool,
  pub linker_mode: Option<NodeModulesLinkerMode>,
}

#[derive(Debug, Default)]
pub struct WorkspaceFactoryOptions {
  pub additional_config_file_names: &'static [&'static str],
  pub config_discovery: ConfigDiscoveryOption,
  pub is_package_manager_subcommand: bool,
  pub frozen_lockfile: Option<bool>,
  pub lock_arg: Option<PathBuf>,
  /// Whether to skip writing to the lockfile.
  pub lockfile_skip_write: bool,
  pub maybe_custom_deno_dir_root: Option<PathBuf>,
  pub node_modules_dir: Option<NodeModulesDirMode>,
  pub node_modules_linker: Option<NodeModulesLinkerMode>,
  pub no_lock: bool,
  pub no_npm: bool,
  /// When no `deno.lock` exists, attempt to seed one by translating a
  /// sibling `package-lock.json`. Currently set by `deno install`.
  pub import_npm_lockfile: bool,
  /// The process state if using ext/node and the current process was "forked".
  /// This value is found at `deno_lib::args::NPM_PROCESS_STATE`
  /// but in most scenarios this can probably just be `None`.
  pub npm_process_state: Option<NpmProcessStateOptions>,
  /// Override the path to the root node_modules directory.
  pub root_node_modules_dir_override: Option<PathBuf>,
  pub vendor: Option<bool>,
}

#[allow(clippy::disallowed_types, reason = "definition")]
pub type WorkspaceFactoryRc<TSys> =
  deno_maybe_sync::MaybeArc<WorkspaceFactory<TSys>>;

#[sys_traits::auto_impl]
pub trait WorkspaceFactorySys:
  DenoDirSys
  + crate::lockfile::LockfileSys
  + crate::npm::NpmResolverSys
  + deno_cache_dir::GlobalHttpCacheSys
  + deno_cache_dir::LocalHttpCacheSys
  + crate::loader::NpmModuleLoaderSys
  + sys_traits::SystemTimeNow
{
}

pub struct WorkspaceFactory<TSys: WorkspaceFactorySys> {
  sys: TSys,
  deno_dir_provider: Deferred<DenoDirProviderRc<TSys>>,
  emit_cache: Deferred<EmitCacheRc<TSys>>,
  global_http_cache: Deferred<GlobalHttpCacheRc<TSys>>,
  http_cache: Deferred<GlobalOrLocalHttpCache<TSys>>,
  jsr_url: Deferred<Url>,
  lockfile: async_once_cell::OnceCell<Option<LockfileLockRc<TSys>>>,
  node_modules_dir_path: Deferred<Option<PathBuf>>,
  npm_cache_dir: Deferred<NpmCacheDirRc>,
  npmrc: Deferred<(ResolvedNpmRcRc, Option<PathBuf>)>,
  node_modules_dir_mode: Deferred<NodeModulesDirMode>,
  node_modules_linker_mode: Deferred<NodeModulesLinkerMode>,
  workspace_directory: Deferred<WorkspaceDirectoryRc>,
  workspace_external_import_map_loader:
    Deferred<WorkspaceExternalImportMapLoaderRc<TSys>>,
  workspace_npm_link_packages: Deferred<WorkspaceNpmLinkPackagesRc>,
  initial_cwd: PathBuf,
  options: WorkspaceFactoryOptions,
}

impl<TSys: WorkspaceFactorySys> WorkspaceFactory<TSys> {
  pub fn new(
    sys: TSys,
    initial_cwd: PathBuf,
    options: WorkspaceFactoryOptions,
  ) -> Self {
    Self {
      sys,
      deno_dir_provider: Default::default(),
      emit_cache: Default::default(),
      global_http_cache: Default::default(),
      http_cache: Default::default(),
      jsr_url: Default::default(),
      lockfile: Default::default(),
      node_modules_dir_path: Default::default(),
      npm_cache_dir: Default::default(),
      npmrc: Default::default(),
      node_modules_dir_mode: Default::default(),
      node_modules_linker_mode: Default::default(),
      workspace_directory: Default::default(),
      workspace_external_import_map_loader: Default::default(),
      workspace_npm_link_packages: Default::default(),
      initial_cwd,
      options,
    }
  }

  pub fn set_workspace_directory(
    &mut self,
    workspace_directory: WorkspaceDirectoryRc,
  ) {
    self.workspace_directory = Deferred::from(workspace_directory);
  }

  pub fn deno_dir_provider(&self) -> &DenoDirProviderRc<TSys> {
    self.deno_dir_provider.get_or_init(|| {
      new_rc(DenoDirProvider::new(
        self.sys.clone(),
        DenoDirOptions {
          maybe_initial_cwd: Some(self.initial_cwd.clone()),
          maybe_custom_root: self.options.maybe_custom_deno_dir_root.clone(),
        },
      ))
    })
  }

  pub fn emit_cache(&self) -> Result<&EmitCacheRc<TSys>, anyhow::Error> {
    self.emit_cache.get_or_try_init(|| {
      Ok(new_rc(EmitCache::new(
        &self.sys,
        self.deno_dir()?.gen_cache.clone(),
        #[cfg(feature = "deno_ast")]
        Cow::Borrowed(deno_ast::VERSION),
        #[cfg(not(feature = "deno_ast"))]
        Cow::Borrowed(env!("CARGO_PKG_VERSION")),
      )))
    })
  }

  pub fn jsr_url(&self) -> &Url {
    self.jsr_url.get_or_init(|| resolve_jsr_url(&self.sys))
  }

  pub fn initial_cwd(&self) -> &PathBuf {
    &self.initial_cwd
  }

  pub fn no_npm(&self) -> bool {
    self.options.no_npm
  }

  /// Resolves the raw node_modules dir mode before any promotion
  /// (e.g. Manual → Auto for package manager subcommands).
  fn raw_node_modules_dir_mode(
    &self,
  ) -> Result<NodeModulesDirMode, anyhow::Error> {
    if let Some(process_state) = &self.options.npm_process_state {
      if process_state.is_byonm {
        return Ok(NodeModulesDirMode::Manual);
      }
      if process_state.node_modules_dir.is_some() {
        return Ok(NodeModulesDirMode::Auto);
      } else {
        return Ok(NodeModulesDirMode::None);
      }
    }
    if let Some(flag) = self.options.node_modules_dir {
      return Ok(flag);
    }
    let workspace = &self.workspace_directory()?.workspace;
    if let Some(mode) = workspace.node_modules_dir()? {
      return Ok(mode);
    }

    let workspace = &self.workspace_directory()?.workspace;

    if let Some(pkg_json) = workspace.root_pkg_json() {
      if let Ok(deno_dir) = self.deno_dir() {
        let deno_dir = &deno_dir.root;
        // `deno_dir` can be symlink in macOS or on the CI
        if let Ok(deno_dir) =
          canonicalize_path_maybe_not_exists(&self.sys, deno_dir)
          && pkg_json.path.starts_with(deno_dir)
        {
          // if the package.json is in deno_dir, then do not use node_modules
          // next to it as local node_modules dir
          return Ok(NodeModulesDirMode::None);
        }
      }

      Ok(NodeModulesDirMode::Manual)
    } else if workspace.vendor_dir_path().is_some() {
      Ok(NodeModulesDirMode::Auto)
    } else {
      // use the global cache
      Ok(NodeModulesDirMode::None)
    }
  }

  pub fn node_modules_dir_mode(
    &self,
  ) -> Result<NodeModulesDirMode, anyhow::Error> {
    self
      .node_modules_dir_mode
      .get_or_try_init(|| {
        let mode = self.raw_node_modules_dir_mode()?;
        if mode == NodeModulesDirMode::Manual
          && self.options.is_package_manager_subcommand
        {
          // force using the managed resolver for package management
          // sub commands so that it sets up the node_modules directory
          Ok(NodeModulesDirMode::Auto)
        } else {
          Ok(mode)
        }
      })
      .copied()
  }

  pub fn node_modules_linker_mode(
    &self,
  ) -> Result<NodeModulesLinkerMode, anyhow::Error> {
    self
      .node_modules_linker_mode
      .get_or_try_init(|| {
        if let Some(process_state) = &self.options.npm_process_state
          && let Some(mode) = process_state.linker_mode
        {
          return Ok(mode);
        }
        let mode = if let Some(flag) = self.options.node_modules_linker {
          flag
        } else {
          let workspace = &self.workspace_directory()?.workspace;
          workspace
            .node_modules_linker()?
            .unwrap_or_default()
        };
        if mode == NodeModulesLinkerMode::Hoisted {
          let dir_mode = self.raw_node_modules_dir_mode()?;
          if dir_mode != NodeModulesDirMode::Manual {
            bail!(
              "The hoisted node_modules linker requires --node-modules-dir=manual. \
               Add \"nodeModulesDir\": \"manual\" to your deno.json or pass \
               --node-modules-dir=manual on the command line."
            );
          }
        }
        Ok(mode)
      })
      .copied()
  }

  /// Resolves the path to use for a local node_modules folder.
  pub fn node_modules_dir_path(&self) -> Result<Option<&Path>, anyhow::Error> {
    fn resolve_from_root(root_folder: &FolderConfigs, cwd: &Path) -> PathBuf {
      root_folder
        .deno_json
        .as_ref()
        .map(|c| Cow::Owned(c.dir_path()))
        .or_else(|| {
          root_folder
            .pkg_json
            .as_ref()
            .map(|c| Cow::Borrowed(c.dir_path()))
        })
        .unwrap_or(Cow::Borrowed(cwd))
        .join("node_modules")
    }

    self
      .node_modules_dir_path
      .get_or_try_init(|| {
        if let Some(path) = &self.options.root_node_modules_dir_override {
          return Ok(Some(path.clone()));
        }
        if let Some(process_state) = &self.options.npm_process_state {
          return Ok(
            process_state
              .node_modules_dir
              .as_ref()
              .map(|p| PathBuf::from(p.as_ref())),
          );
        }

        let mode = self.node_modules_dir_mode()?;
        let workspace = &self.workspace_directory()?.workspace;
        let root_folder = workspace.root_folder_configs();
        if !mode.uses_node_modules_dir() {
          return Ok(None);
        }

        let node_modules_dir =
          resolve_from_root(root_folder, &self.initial_cwd);

        Ok(Some(canonicalize_path_maybe_not_exists(
          &self.sys,
          &node_modules_dir,
        )?))
      })
      .map(|p| p.as_deref())
  }

  pub fn deno_dir(&self) -> Result<&DenoDir<TSys>, DenoDirResolutionError> {
    self.deno_dir_provider().get_or_create()
  }

  pub fn global_http_cache(
    &self,
  ) -> Result<&GlobalHttpCacheRc<TSys>, DenoDirResolutionError> {
    self.global_http_cache.get_or_try_init(|| {
      let global_cache_dir = self.deno_dir()?.remote_folder_path();
      let global_http_cache = new_rc(deno_cache_dir::GlobalHttpCache::new(
        self.sys.clone(),
        global_cache_dir,
      ));
      Ok(global_http_cache)
    })
  }

  pub fn http_cache(
    &self,
  ) -> Result<&deno_cache_dir::GlobalOrLocalHttpCache<TSys>, HttpCacheCreateError>
  {
    self.http_cache.get_or_try_init(|| {
      let global_cache = self.global_http_cache()?.clone();
      match self.workspace_directory()?.workspace.vendor_dir_path() {
        Some(local_path) => {
          let local_cache = LocalHttpCache::new(
            local_path.clone(),
            global_cache,
            deno_cache_dir::GlobalToLocalCopy::Allow,
            self.jsr_url().clone(),
          );
          Ok(new_rc(local_cache).into())
        }
        None => Ok(global_cache.into()),
      }
    })
  }

  pub async fn maybe_lockfile(
    &self,
    npm_package_info_provider: &dyn deno_lockfile::NpmPackageInfoProvider,
  ) -> Result<Option<&LockfileLockRc<TSys>>, anyhow::Error> {
    self
      .lockfile
      .get_or_try_init(async move {
        let workspace_directory = self.workspace_directory()?;
        let maybe_external_import_map =
          self.workspace_external_import_map_loader()?.get_or_load()?;

        let maybe_lock_file = LockfileLock::discover(
          self.sys().clone(),
          crate::lockfile::LockfileFlags {
            no_lock: self.options.no_lock,
            frozen_lockfile: self.options.frozen_lockfile,
            lock: self.options.lock_arg.as_ref().map(|path| {
              #[cfg(not(target_arch = "wasm32"))]
              debug_assert!(path.is_absolute());
              path.clone()
            }),
            skip_write: self.options.lockfile_skip_write,
            no_config: matches!(
              self.options.config_discovery,
              ConfigDiscoveryOption::Disabled
            ),
            no_npm: self.options.no_npm,
            import_npm_lockfile: self.options.import_npm_lockfile,
          },
          &workspace_directory.workspace,
          maybe_external_import_map.as_ref().map(|v| &v.value),
          npm_package_info_provider,
        )
        .await?
        .map(deno_maybe_sync::new_rc);

        Ok(maybe_lock_file)
      })
      .await
      .map(|c| c.as_ref())
  }

  pub fn npm_cache_dir(
    &self,
  ) -> Result<&NpmCacheDirRc, NpmCacheDirCreateError> {
    self.npm_cache_dir.get_or_try_init(|| {
      let npm_cache_dir = self.deno_dir()?.npm_folder_path();
      Ok(new_rc(NpmCacheDir::new(
        &self.sys,
        npm_cache_dir,
        self.npmrc()?.get_all_known_registries_urls(),
      )))
    })
  }

  pub fn npmrc(&self) -> Result<&ResolvedNpmRcRc, NpmRcCreateError> {
    self.npmrc_with_path().map(|(npmrc, _)| npmrc)
  }

  pub fn npmrc_with_path(
    &self,
  ) -> Result<&(ResolvedNpmRcRc, Option<PathBuf>), NpmRcCreateError> {
    self.npmrc.get_or_try_init(|| {
      let (npmrc, path) = discover_npmrc_from_workspace(
        &self.sys,
        &self.workspace_directory()?.workspace,
      )?;
      Ok((new_rc(npmrc), path))
    })
  }

  pub fn sys(&self) -> &TSys {
    &self.sys
  }

  pub fn workspace_directory(
    &self,
  ) -> Result<&WorkspaceDirectoryRc, WorkspaceDiscoverError> {
    self.workspace_directory.get_or_try_init(|| {
      let maybe_vendor_override = self.options.vendor.map(|v| match v {
        true => VendorEnablement::Enable {
          cwd: &self.initial_cwd,
        },
        false => VendorEnablement::Disable,
      });
      let resolve_workspace_discover_options = || {
        let discover_pkg_json = !self.options.no_npm
          && !self.has_flag_env_var("DENO_NO_PACKAGE_JSON");
        if !discover_pkg_json {
          log::debug!("package.json auto-discovery is disabled");
        }
        WorkspaceDiscoverOptions {
          deno_json_cache: None,
          pkg_json_cache: Some(&node_resolver::PackageJsonThreadLocalCache),
          workspace_cache: None,
          additional_config_file_names: self
            .options
            .additional_config_file_names,
          discover_pkg_json,
          maybe_vendor_override,
        }
      };
      let resolve_empty_options = || WorkspaceDirectoryEmptyOptions {
        root_dir: new_rc(
          deno_path_util::url_from_directory_path(&self.initial_cwd).unwrap(),
        ),
        use_vendor_dir: maybe_vendor_override
          .unwrap_or(VendorEnablement::Disable),
      };

      let dir = match &self.options.config_discovery {
        ConfigDiscoveryOption::DiscoverCwd => WorkspaceDirectory::discover(
          &self.sys,
          WorkspaceDiscoverStart::Paths(std::slice::from_ref(
            &self.initial_cwd,
          )),
          &resolve_workspace_discover_options(),
        )?,
        ConfigDiscoveryOption::Discover { start_paths } => {
          WorkspaceDirectory::discover(
            &self.sys,
            WorkspaceDiscoverStart::Paths(start_paths),
            &resolve_workspace_discover_options(),
          )?
        }
        ConfigDiscoveryOption::Path(path) => {
          #[cfg(not(target_arch = "wasm32"))]
          debug_assert!(path.is_absolute());
          WorkspaceDirectory::discover(
            &self.sys,
            WorkspaceDiscoverStart::ConfigFile(path),
            &resolve_workspace_discover_options(),
          )?
        }
        ConfigDiscoveryOption::Disabled => {
          WorkspaceDirectory::empty(resolve_empty_options())
        }
      };
      Ok(dir)
    })
  }

  pub fn workspace_external_import_map_loader(
    &self,
  ) -> Result<&WorkspaceExternalImportMapLoaderRc<TSys>, WorkspaceDiscoverError>
  {
    self
      .workspace_external_import_map_loader
      .get_or_try_init(|| {
        Ok(new_rc(WorkspaceExternalImportMapLoader::new(
          self.sys().clone(),
          self.workspace_directory()?.workspace.clone(),
        )))
      })
  }

  pub fn workspace_npm_link_packages(
    &self,
  ) -> Result<&WorkspaceNpmLinkPackagesRc, anyhow::Error> {
    self
      .workspace_npm_link_packages
      .get_or_try_init(|| {
        let workspace_dir = self.workspace_directory()?;
        let npm_packages = WorkspaceNpmLinkPackagesRc::from_workspace(
          workspace_dir.workspace.as_ref(),
        );
        if !npm_packages.0.is_empty() && !matches!(self.node_modules_dir_mode()?, NodeModulesDirMode::Auto | NodeModulesDirMode::Manual) {
          bail!("Linking npm packages requires using a node_modules directory. Ensure you have a package.json or set the \"nodeModulesDir\" option to \"auto\" or \"manual\" in your workspace root deno.json.")
        } else {
          Ok(npm_packages)
        }
      })
  }

  fn has_flag_env_var(&self, name: &str) -> bool {
    let value = self.sys.env_var_os(name);
    match value {
      Some(value) => value == "1",
      None => false,
    }
  }
}

#[derive(Default)]
pub struct ResolverFactoryOptions {
  pub compiler_options_overrides: CompilerOptionsOverrides,
  pub is_cjs_resolution_mode: IsCjsResolutionMode,
  /// Prevents installing packages newer than the specified date.
  pub newest_dependency_date: Option<NewestDependencyDate>,
  pub node_analysis_cache: Option<NodeAnalysisCacheRc>,
  pub node_code_translator_mode: node_resolver::analyze::NodeCodeTranslatorMode,
  pub node_resolver_options: NodeResolverOptions,
  pub node_resolution_cache: Option<node_resolver::NodeResolutionCacheRc>,
  pub npm_system_info: NpmSystemInfo,
  pub package_json_cache: Option<node_resolver::PackageJsonCacheRc>,
  pub package_json_dep_resolution: Option<PackageJsonDepResolution>,
  pub specified_import_map: Option<Box<dyn SpecifiedImportMapProvider>>,
  pub unstable_sloppy_imports: bool,
  #[cfg(feature = "graph")]
  pub on_mapped_resolution_diagnostic:
    Option<crate::graph::OnMappedResolutionDiagnosticFn>,
  pub allow_json_imports: AllowJsonImports,
  /// Modules loaded via --require flag that should always be treated as CommonJS
  pub require_modules: Vec<Url>,
}

pub struct ResolverFactory<TSys: WorkspaceFactorySys> {
  options: ResolverFactoryOptions,
  sys: NodeResolutionSys<TSys>,
  cjs_module_export_analyzer: Deferred<DenoCjsModuleExportAnalyzerRc<TSys>>,
  cjs_tracker: Deferred<CjsTrackerRc<DenoInNpmPackageChecker, TSys>>,
  compiler_options_resolver: Deferred<CompilerOptionsResolverRc>,
  #[cfg(feature = "graph")]
  deno_resolver:
    async_once_cell::OnceCell<crate::graph::DefaultDenoResolverRc<TSys>>,
  #[cfg(feature = "deno_ast")]
  emitter: Deferred<crate::emit::EmitterRc<DenoInNpmPackageChecker, TSys>>,
  #[cfg(feature = "graph")]
  found_package_json_dep_flag: crate::graph::FoundPackageJsonDepFlagRc,
  in_npm_package_checker: Deferred<DenoInNpmPackageChecker>,
  #[cfg(feature = "graph")]
  jsr_version_resolver: Deferred<JsrVersionResolverRc>,
  minimum_dependency_age: Deferred<MinimumDependencyAgeConfig>,
  node_code_translator: Deferred<DenoNodeCodeTranslatorRc<TSys>>,
  node_resolver: Deferred<
    NodeResolverRc<
      DenoInNpmPackageChecker,
      DenoIsBuiltInNodeModuleChecker,
      NpmResolver<TSys>,
      TSys,
    >,
  >,
  npm_module_loader: Deferred<DenoNpmModuleLoaderRc<TSys>>,
  npm_req_resolver: Deferred<
    NpmReqResolverRc<
      DenoInNpmPackageChecker,
      DenoIsBuiltInNodeModuleChecker,
      NpmResolver<TSys>,
      TSys,
    >,
  >,
  npm_resolver: Deferred<NpmResolver<TSys>>,
  npm_resolution: NpmResolutionCellRc,
  npm_version_resolver: Deferred<NpmVersionResolverRc>,
  #[cfg(feature = "deno_ast")]
  parsed_source_cache: crate::cache::ParsedSourceCacheRc,
  pkg_json_resolver: Deferred<PackageJsonResolverRc<TSys>>,
  #[cfg(all(feature = "graph", feature = "deno_ast"))]
  module_loader: Deferred<crate::loader::ModuleLoaderRc<TSys>>,
  raw_deno_resolver: async_once_cell::OnceCell<DefaultRawDenoResolverRc<TSys>>,
  workspace_factory: WorkspaceFactoryRc<TSys>,
  workspace_resolver: async_once_cell::OnceCell<WorkspaceResolverRc<TSys>>,
}

impl<TSys: WorkspaceFactorySys> ResolverFactory<TSys> {
  pub fn new(
    workspace_factory: WorkspaceFactoryRc<TSys>,
    options: ResolverFactoryOptions,
  ) -> Self {
    Self {
      sys: NodeResolutionSys::new(
        workspace_factory.sys.clone(),
        options.node_resolution_cache.clone(),
      ),
      cjs_module_export_analyzer: Default::default(),
      cjs_tracker: Default::default(),
      compiler_options_resolver: Default::default(),
      raw_deno_resolver: Default::default(),
      #[cfg(feature = "graph")]
      deno_resolver: Default::default(),
      #[cfg(feature = "deno_ast")]
      emitter: Default::default(),
      #[cfg(feature = "graph")]
      found_package_json_dep_flag: Default::default(),
      in_npm_package_checker: Default::default(),
      #[cfg(feature = "graph")]
      jsr_version_resolver: Default::default(),
      minimum_dependency_age: Default::default(),
      node_code_translator: Default::default(),
      node_resolver: Default::default(),
      npm_module_loader: Default::default(),
      npm_req_resolver: Default::default(),
      npm_resolution: Default::default(),
      npm_resolver: Default::default(),
      npm_version_resolver: Default::default(),
      #[cfg(feature = "deno_ast")]
      parsed_source_cache: Default::default(),
      pkg_json_resolver: Default::default(),
      #[cfg(all(feature = "graph", feature = "deno_ast"))]
      module_loader: Default::default(),
      workspace_factory,
      workspace_resolver: Default::default(),
      options,
    }
  }

  pub async fn raw_deno_resolver(
    &self,
  ) -> Result<&DefaultRawDenoResolverRc<TSys>, anyhow::Error> {
    self
      .raw_deno_resolver
      .get_or_try_init(
        async {
          Ok(new_rc(RawDenoResolver::new(DenoResolverOptions {
            in_npm_pkg_checker: self.in_npm_package_checker()?.clone(),
            node_and_req_resolver: if self.workspace_factory.no_npm() {
              None
            } else {
              Some(NodeAndNpmResolvers {
                node_resolver: self.node_resolver()?.clone(),
                npm_resolver: self.npm_resolver()?.clone(),
                npm_req_resolver: self.npm_req_resolver()?.clone(),
              })
            },
            is_byonm: self.use_byonm()?,
            maybe_vendor_dir: self
              .workspace_factory
              .workspace_directory()?
              .workspace
              .vendor_dir_path(),
            workspace_resolver: self.workspace_resolver().await?.clone(),
          })))
        }
        // boxed to prevent the futures getting big and exploding the stack
        .boxed_local(),
      )
      .await
  }

  pub fn cjs_module_export_analyzer(
    &self,
  ) -> Result<&DenoCjsModuleExportAnalyzerRc<TSys>, anyhow::Error> {
    self.cjs_module_export_analyzer.get_or_try_init(|| {
      let code_analyzer = DenoCjsCodeAnalyzer::new(
        self
          .options
          .node_analysis_cache
          .clone()
          .unwrap_or_else(|| new_rc(NullNodeAnalysisCache)),
        self.cjs_tracker()?.clone(),
        #[cfg(feature = "deno_ast")]
        new_rc(crate::cjs::analyzer::DenoAstModuleExportAnalyzer::new(
          self.parsed_source_cache().clone(),
        )),
        #[cfg(not(feature = "deno_ast"))]
        new_rc(crate::cjs::analyzer::NotImplementedModuleExportAnalyzer),
      );
      Ok(new_rc(
        node_resolver::analyze::CjsModuleExportAnalyzer::new(
          code_analyzer,
          self.in_npm_package_checker()?.clone(),
          self.node_resolver()?.clone(),
          self.npm_resolver()?.clone(),
          self.pkg_json_resolver().clone(),
          self.workspace_factory.sys().clone(),
        ),
      ))
    })
  }

  pub fn cjs_tracker(
    &self,
  ) -> Result<&CjsTrackerRc<DenoInNpmPackageChecker, TSys>, anyhow::Error> {
    self.cjs_tracker.get_or_try_init(|| {
      Ok(new_rc(CjsTracker::new(
        self.in_npm_package_checker()?.clone(),
        self.pkg_json_resolver().clone(),
        self.options.is_cjs_resolution_mode,
        self.options.require_modules.clone(),
      )))
    })
  }

  pub fn compiler_options_resolver(
    &self,
  ) -> Result<&CompilerOptionsResolverRc, anyhow::Error> {
    self.compiler_options_resolver.get_or_try_init(|| {
      Ok(new_rc(CompilerOptionsResolver::new(
        &self.sys,
        &self.workspace_factory.workspace_directory()?.workspace,
        self.node_resolver()?,
        &self.workspace_factory.options.config_discovery,
        &self.options.compiler_options_overrides,
      )))
    })
  }

  #[cfg(feature = "deno_ast")]
  pub fn emitter(
    &self,
  ) -> Result<
    &crate::emit::EmitterRc<DenoInNpmPackageChecker, TSys>,
    anyhow::Error,
  > {
    self.emitter.get_or_try_init(|| {
      Ok(new_rc(crate::emit::Emitter::new(
        self.cjs_tracker()?.clone(),
        self.workspace_factory.emit_cache()?.clone(),
        self.parsed_source_cache().clone(),
        self.compiler_options_resolver()?.clone(),
      )))
    })
  }

  #[cfg(feature = "graph")]
  pub fn found_package_json_dep_flag(
    &self,
  ) -> &crate::graph::FoundPackageJsonDepFlagRc {
    &self.found_package_json_dep_flag
  }

  #[cfg(feature = "graph")]
  pub async fn deno_resolver(
    &self,
  ) -> Result<&crate::graph::DefaultDenoResolverRc<TSys>, anyhow::Error> {
    self
      .deno_resolver
      .get_or_try_init(async {
        Ok(new_rc(crate::graph::DenoResolver::new(
          self.raw_deno_resolver().await?.clone(),
          self.workspace_factory.sys.clone(),
          self.found_package_json_dep_flag.clone(),
          self.options.on_mapped_resolution_diagnostic.clone(),
        )))
      })
      .await
  }

  pub fn in_npm_package_checker(
    &self,
  ) -> Result<&DenoInNpmPackageChecker, anyhow::Error> {
    self.in_npm_package_checker.get_or_try_init(|| {
      let options = match self.use_byonm()? {
        true => CreateInNpmPkgCheckerOptions::Byonm,
        false => CreateInNpmPkgCheckerOptions::Managed(
          ManagedInNpmPkgCheckerCreateOptions {
            root_cache_dir_url: self
              .workspace_factory
              .npm_cache_dir()?
              .root_dir_url(),
            maybe_node_modules_path: self
              .workspace_factory
              .node_modules_dir_path()?,
          },
        ),
      };
      Ok(DenoInNpmPackageChecker::new(options))
    })
  }

  #[cfg(feature = "graph")]
  pub fn jsr_version_resolver(
    &self,
  ) -> Result<&JsrVersionResolverRc, anyhow::Error> {
    self.jsr_version_resolver.get_or_try_init(|| {
      let minimum_dependency_age_config =
        self.minimum_dependency_age_config()?;

      let (exclude_jsr_pkgs, exclude_jsr_pkg_prefixes) =
        split_minimum_dependency_age_excludes(
          &minimum_dependency_age_config.exclude,
          "jsr:",
        );

      Ok(new_rc(deno_graph::packages::JsrVersionResolver {
        newest_dependency_date_options:
          deno_graph::packages::NewestDependencyDateOptions {
            date: minimum_dependency_age_config
              .age
              .as_ref()
              .and_then(|d| d.into_option())
              .map(deno_graph::packages::NewestDependencyDate),
            exclude_jsr_pkgs,
            exclude_jsr_pkg_prefixes,
          },
      }))
    })
  }

  /// The newest allowed dependency date.
  pub fn minimum_dependency_age_config(
    &self,
  ) -> Result<&MinimumDependencyAgeConfig, anyhow::Error> {
    self.minimum_dependency_age.get_or_try_init(|| {
      let workspace_factory = self.workspace_factory();
      let mut config = if let Some(date) = self.options.newest_dependency_date {
        MinimumDependencyAgeConfig {
          age: Some(date),
          exclude: Vec::new(),
        }
      } else {
        let workspace = &workspace_factory.workspace_directory()?.workspace;
        workspace.minimum_dependency_age(workspace_factory.sys())?
      };
      if config.age.is_none() {
        apply_minimum_dependency_age_fallbacks(
          &mut config,
          workspace_factory.npmrc()?.min_release_age_days,
          chrono::DateTime::<chrono::Utc>::from(
            workspace_factory.sys().sys_time_now(),
          ),
        );
      }
      if let Some(newest_dependency_date) =
        config.age.and_then(|d| d.into_option())
      {
        log::debug!("Newest dependency date: {}", newest_dependency_date);
      }
      Ok(config)
    })
  }

  pub fn node_code_translator(
    &self,
  ) -> Result<&DenoNodeCodeTranslatorRc<TSys>, anyhow::Error> {
    self.node_code_translator.get_or_try_init(|| {
      Ok(new_rc(NodeCodeTranslator::new(
        self.cjs_module_export_analyzer()?.clone(),
        self.options.node_code_translator_mode,
      )))
    })
  }

  pub fn node_resolver(
    &self,
  ) -> Result<
    &NodeResolverRc<
      DenoInNpmPackageChecker,
      DenoIsBuiltInNodeModuleChecker,
      NpmResolver<TSys>,
      TSys,
    >,
    anyhow::Error,
  > {
    self.node_resolver.get_or_try_init(|| {
      Ok(new_rc(NodeResolver::new(
        self.in_npm_package_checker()?.clone(),
        DenoIsBuiltInNodeModuleChecker,
        self.npm_resolver()?.clone(),
        self.pkg_json_resolver().clone(),
        self.sys.clone(),
        self.options.node_resolver_options.clone(),
      )))
    })
  }

  pub fn npm_module_loader(
    &self,
  ) -> Result<&DenoNpmModuleLoaderRc<TSys>, anyhow::Error> {
    self.npm_module_loader.get_or_try_init(|| {
      Ok(new_rc(NpmModuleLoader::new(
        self.cjs_tracker()?.clone(),
        self.in_npm_package_checker()?.clone(),
        self.node_code_translator()?.clone(),
        self.workspace_factory.sys.clone(),
      )))
    })
  }

  pub fn npm_resolution(&self) -> &NpmResolutionCellRc {
    &self.npm_resolution
  }

  pub fn npm_req_resolver(
    &self,
  ) -> Result<
    &NpmReqResolverRc<
      DenoInNpmPackageChecker,
      DenoIsBuiltInNodeModuleChecker,
      NpmResolver<TSys>,
      TSys,
    >,
    anyhow::Error,
  > {
    self.npm_req_resolver.get_or_try_init(|| {
      Ok(new_rc(NpmReqResolver::new(NpmReqResolverOptions {
        in_npm_pkg_checker: self.in_npm_package_checker()?.clone(),
        node_resolver: self.node_resolver()?.clone(),
        npm_resolver: self.npm_resolver()?.clone(),
        sys: self.workspace_factory.sys.clone(),
      })))
    })
  }

  pub fn npm_resolver(&self) -> Result<&NpmResolver<TSys>, anyhow::Error> {
    self.npm_resolver.get_or_try_init(|| {
      Ok(NpmResolver::<TSys>::new::<TSys>(if self.use_byonm()? {
        NpmResolverCreateOptions::Byonm(ByonmNpmResolverCreateOptions {
          sys: self.sys.clone(),
          pkg_json_resolver: self.pkg_json_resolver().clone(),
          root_node_modules_dir: Some(
            match self.workspace_factory.node_modules_dir_path()? {
              Some(node_modules_path) => node_modules_path.to_path_buf(),
              // path needs to be canonicalized for node resolution
              // (node_modules_dir_path above is already canonicalized)
              None => canonicalize_path_maybe_not_exists(
                &self.workspace_factory.sys,
                self.workspace_factory.initial_cwd(),
              )?
              .join("node_modules"),
            },
          ),
          search_stop_dir: None,
        })
      } else {
        NpmResolverCreateOptions::Managed(ManagedNpmResolverCreateOptions {
          sys: self.sys.clone(),
          npm_resolution: self.npm_resolution().clone(),
          npm_cache_dir: self.workspace_factory.npm_cache_dir()?.clone(),
          maybe_node_modules_path: self
            .workspace_factory
            .node_modules_dir_path()?
            .map(|p| p.to_path_buf()),
          npm_system_info: self.options.npm_system_info.clone(),
          npmrc: self.workspace_factory.npmrc()?.clone(),
          linker_mode: self.workspace_factory.node_modules_linker_mode()?,
        })
      }))
    })
  }

  pub fn npm_version_resolver(
    &self,
  ) -> Result<&NpmVersionResolverRc, anyhow::Error> {
    self.npm_version_resolver.get_or_try_init(|| {
      let minimum_dependency_age_config =
        self.minimum_dependency_age_config()?;

      // parse npm overrides from root package.json
      let workspace = &self.workspace_factory.workspace_directory()?.workspace;
      let overrides = npm_overrides_from_workspace(workspace);

      // the `trust-policy` / `trust-policy-ignore-after` npmrc settings drive
      // the `no-downgrade` publishing-trust policy
      let npmrc = self.workspace_factory.npmrc()?;
      let trust_policy = deno_npm::resolution::TrustPolicyOptions {
        policy: match npmrc.trust_policy {
          deno_npmrc::TrustPolicyConfig::NoDowngrade => {
            deno_npm::resolution::NpmTrustPolicy::NoDowngrade
          }
          deno_npmrc::TrustPolicyConfig::Off => {
            deno_npm::resolution::NpmTrustPolicy::Off
          }
        },
        // turn the relative `trust-policy-ignore-after` minutes into an
        // absolute cutoff (`now - minutes`) so the resolver stays clock-free
        ignore_after_cutoff: npmrc.trust_policy_ignore_after_minutes.map(
          |minutes| {
            let now = chrono::DateTime::<chrono::Utc>::from(
              self.workspace_factory.sys().sys_time_now(),
            );
            now - chrono::Duration::minutes(minutes as i64)
          },
        ),
        // `trust-policy-exclude[]` package names exempted from the policy
        #[allow(
          clippy::disallowed_types,
          reason = "Arc needed for shared exclude set"
        )]
        exclude: std::sync::Arc::new(
          npmrc
            .trust_policy_exclude
            .iter()
            .map(|s| s.to_ascii_lowercase())
            .collect(),
        ),
      };

      let (exclude, exclude_prefixes) = split_minimum_dependency_age_excludes(
        &minimum_dependency_age_config.exclude,
        "npm:",
      );

      Ok(new_rc(NpmVersionResolver {
        newest_dependency_date_options:
          deno_npm::resolution::NewestDependencyDateOptions {
            date: minimum_dependency_age_config
              .age
              .as_ref()
              .and_then(|d| d.into_option())
              .map(deno_npm::resolution::NewestDependencyDate),
            exclude,
            exclude_prefixes,
          },
        link_packages: self
          .workspace_factory
          .workspace_npm_link_packages()?
          .0
          .clone(),
        #[allow(
          clippy::disallowed_types,
          reason = "Arc needed for shared overrides"
        )]
        overrides: std::sync::Arc::new(overrides),
        trust_policy,
      }))
    })
  }

  #[cfg(feature = "deno_ast")]
  pub fn parsed_source_cache(&self) -> &crate::cache::ParsedSourceCacheRc {
    &self.parsed_source_cache
  }

  pub fn pkg_json_resolver(&self) -> &PackageJsonResolverRc<TSys> {
    self.pkg_json_resolver.get_or_init(|| {
      new_rc(PackageJsonResolver::new(
        self.workspace_factory.sys.clone(),
        self.options.package_json_cache.clone(),
      ))
    })
  }

  #[cfg(all(feature = "graph", feature = "deno_ast"))]
  pub fn module_loader(
    &self,
  ) -> Result<&crate::loader::ModuleLoaderRc<TSys>, anyhow::Error> {
    self.module_loader.get_or_try_init(|| {
      let cjs_tracker = self.cjs_tracker()?;
      Ok(new_rc(crate::loader::ModuleLoader::new(
        cjs_tracker.clone(),
        self.emitter()?.clone(),
        self.in_npm_package_checker()?.clone(),
        self.node_code_translator()?.clone(),
        self.npm_module_loader()?.clone(),
        self.parsed_source_cache.clone(),
        self.workspace_factory.sys.clone(),
        self.options.allow_json_imports,
      )))
    })
  }

  pub fn workspace_factory(&self) -> &WorkspaceFactoryRc<TSys> {
    &self.workspace_factory
  }

  pub async fn workspace_resolver(
    &self,
  ) -> Result<&WorkspaceResolverRc<TSys>, anyhow::Error> {
    self
      .workspace_resolver
      .get_or_try_init(
        async {
          let directory = self.workspace_factory.workspace_directory()?;
          let workspace = &directory.workspace;
          let specified_import_map = match &self.options.specified_import_map {
            Some(import_map) => import_map.get().await?,
            None => None,
          };
          let options = crate::workspace::CreateResolverOptions {
            pkg_json_dep_resolution: match self
              .options
              .package_json_dep_resolution
            {
              Some(value) => value,
              None => {
                match self.workspace_factory.node_modules_dir_mode()? {
                  NodeModulesDirMode::Manual => {
                    PackageJsonDepResolution::Disabled
                  }
                  NodeModulesDirMode::Auto | NodeModulesDirMode::None => {
                    // todo(dsherret): should this be disabled for auto?
                    PackageJsonDepResolution::Enabled
                  }
                }
              }
            },
            specified_import_map,
            sloppy_imports_options: if self.options.unstable_sloppy_imports
              || workspace.has_unstable("sloppy-imports")
            {
              SloppyImportsOptions::Enabled
            } else {
              SloppyImportsOptions::Unspecified
            },
            fs_cache_options: FsCacheOptions::Enabled,
          };
          let resolver = WorkspaceResolver::from_workspace(
            workspace,
            self.workspace_factory.sys.clone(),
            options,
          )?;
          resolver.set_compiler_options_resolver(
            self.compiler_options_resolver()?.clone(),
          );
          if !resolver.diagnostics().is_empty() {
            // todo(dsherret): do not log this in this crate... that should be
            // a CLI responsibility
            log::warn!(
              "Resolver diagnostics:\n{}",
              resolver
                .diagnostics()
                .iter()
                .map(|d| format!("  - {d}"))
                .collect::<Vec<_>>()
                .join("\n")
            );
          }
          Ok(new_rc(resolver))
        }
        // boxed to prevent the futures getting big and exploding the stack
        .boxed_local(),
      )
      .await
  }

  pub fn npm_system_info(&self) -> &NpmSystemInfo {
    &self.options.npm_system_info
  }

  pub fn use_byonm(&self) -> Result<bool, anyhow::Error> {
    Ok(
      self.workspace_factory.node_modules_dir_mode()?
        == NodeModulesDirMode::Manual,
    )
  }
}

/// Splits `minimumDependencyAge.exclude` entries with the given scheme
/// (`npm:` or `jsr:`) into exact package names and package name prefixes
/// (from trailing `*` wildcard entries like `npm:@scope/*`).
fn split_minimum_dependency_age_excludes(
  exclude: &[String],
  scheme: &str,
) -> (BTreeSet<PackageName>, Vec<PackageName>) {
  let mut exact = BTreeSet::new();
  let mut prefixes = Vec::new();
  for entry in exclude.iter().filter_map(|v| v.strip_prefix(scheme)) {
    match entry.strip_suffix('*') {
      // an empty prefix (from an invalid entry like `npm:*` that a config
      // diagnostic warns about) would exclude every package
      Some(prefix) if !prefix.is_empty() => prefixes.push(prefix.into()),
      Some(_) => {}
      None => {
        exact.insert(entry.into());
      }
    }
  }
  (exact, prefixes)
}

fn apply_minimum_dependency_age_fallbacks(
  config: &mut MinimumDependencyAgeConfig,
  npmrc_min_release_age_days: Option<u64>,
  now: chrono::DateTime<chrono::Utc>,
) {
  if config.age.is_some() {
    return;
  }

  // Fall back to .npmrc's `min-release-age` when not configured in deno.json.
  config.age = Some(if let Some(days) = npmrc_min_release_age_days {
    if days == 0 {
      NewestDependencyDate::Disabled
    } else {
      NewestDependencyDate::Enabled(now - chrono::Duration::days(days as i64))
    }
  } else {
    NewestDependencyDate::Enabled(
      now - chrono::Duration::minutes(DEFAULT_MINIMUM_DEPENDENCY_AGE_MINUTES),
    )
  });
}

/// Parses npm overrides from a workspace's root package.json.
///
/// Returns `NpmOverrides::default()` if no overrides are present or if parsing fails.
/// Logs a warning on parse failure.
pub fn npm_overrides_from_workspace(
  workspace: &deno_config::workspace::Workspace,
) -> NpmOverrides {
  let Some(overrides_json) = workspace.npm_overrides() else {
    return NpmOverrides::default();
  };
  let root_deps = workspace.root_deps_for_npm_overrides();
  let catalogs = workspace.catalogs();
  match NpmOverrides::from_value(
    serde_json::Value::Object(overrides_json.clone()),
    &root_deps,
    catalogs,
  ) {
    Ok(overrides) => overrides,
    Err(e) => {
      log::warn!(
        "{} failed to parse npm overrides: {}",
        deno_terminal::colors::yellow("Warning"),
        e
      );
      NpmOverrides::default()
    }
  }
}

#[cfg(test)]
mod tests {
  use chrono::TimeZone;
  use deno_config::deno_json::MinimumDependencyAgeConfig;
  use deno_config::deno_json::NewestDependencyDate;

  use super::apply_minimum_dependency_age_fallbacks;

  fn now() -> chrono::DateTime<chrono::Utc> {
    chrono::Utc.with_ymd_and_hms(2025, 6, 1, 0, 0, 0).unwrap()
  }

  #[test]
  fn minimum_dependency_age_falls_back_to_default() {
    let mut config = MinimumDependencyAgeConfig::default();
    apply_minimum_dependency_age_fallbacks(&mut config, None, now());
    assert_eq!(
      config.age,
      Some(NewestDependencyDate::Enabled(
        now() - chrono::Duration::minutes(1440)
      ))
    );
  }

  #[test]
  fn minimum_dependency_age_preserves_exclude_with_default() {
    let mut config = MinimumDependencyAgeConfig {
      age: None,
      exclude: vec!["npm:chalk".to_string()],
    };
    apply_minimum_dependency_age_fallbacks(&mut config, None, now());
    assert_eq!(
      config.age,
      Some(NewestDependencyDate::Enabled(
        now() - chrono::Duration::minutes(1440)
      ))
    );
    assert_eq!(config.exclude, vec!["npm:chalk".to_string()]);
  }

  #[test]
  fn minimum_dependency_age_prefers_npmrc_over_default() {
    let mut config = MinimumDependencyAgeConfig::default();
    apply_minimum_dependency_age_fallbacks(&mut config, Some(2), now());
    assert_eq!(
      config.age,
      Some(NewestDependencyDate::Enabled(
        now() - chrono::Duration::days(2)
      ))
    );
  }

  #[test]
  fn minimum_dependency_age_preserves_explicit_disable() {
    let mut config = MinimumDependencyAgeConfig {
      age: Some(NewestDependencyDate::Disabled),
      exclude: Vec::new(),
    };
    apply_minimum_dependency_age_fallbacks(&mut config, Some(2), now());
    assert_eq!(config.age, Some(NewestDependencyDate::Disabled));
  }
}
