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

use std::borrow::Cow;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;

use deno_ast::ModuleSpecifier;
use deno_config::deno_json::ConfigFile;
use deno_config::deno_json::ConfigFileRc;
use deno_config::workspace::Workspace;
use deno_config::workspace::WorkspaceDirectory;
use deno_core::anyhow::Context;
use deno_core::anyhow::bail;
use deno_core::error::AnyError;
use deno_core::futures::FutureExt;
use deno_core::futures::StreamExt;
use deno_core::futures::future::try_join;
use deno_core::futures::stream::FuturesOrdered;
use deno_core::futures::stream::FuturesUnordered;
use deno_core::serde_json;
use deno_core::url::Url;
use deno_graph::JsrPackageReqNotFoundError;
use deno_graph::packages::JsrPackageVersionInfo;
use deno_npm::resolution::NpmVersionResolver;
use deno_package_json::PackageJsonDepsMap;
use deno_package_json::PackageJsonRc;
use deno_runtime::deno_permissions::PermissionsContainer;
use deno_semver::StackString;
use deno_semver::Version;
use deno_semver::VersionReq;
use deno_semver::jsr::JsrDepPackageReq;
use deno_semver::jsr::JsrPackageReqReference;
use deno_semver::npm::NpmPackageReqReference;
use deno_semver::package::PackageNv;
use deno_semver::package::PackageReq;
use deno_semver::package::PackageReqReference;
use import_map::ImportMap;
use import_map::ImportMapWithDiagnostics;
use import_map::SpecifierMapEntry;
use tokio::sync::Semaphore;

use super::ConfigUpdater;
use crate::args::CliLockfile;
use crate::graph_container::MainModuleGraphContainer;
use crate::graph_container::ModuleGraphContainer;
use crate::graph_container::ModuleGraphUpdatePermit;
use crate::jsr::JsrFetchResolver;
use crate::module_loader::ModuleLoadPreparer;
use crate::npm::CliNpmInstaller;
use crate::npm::CliNpmResolver;
use crate::npm::NpmFetchResolver;
use crate::npm::PackageInfoLoadError;
use crate::util::progress_bar::ProgressBar;
use crate::util::sync::AtomicFlag;

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ImportMapKind {
  Inline,
  Outline(PathBuf),
}

#[derive(Clone)]
pub enum DepLocation {
  DenoJson(ConfigFileRc, KeyPath, ImportMapKind),
  PackageJson(PackageJsonRc, KeyPath),
  /// A `catalog`/`catalogs` entry defined in the workspace root config
  /// (deno.json or package.json). Unlike the other locations the value is a
  /// bare version requirement (e.g. `^1.0.0`) rather than a full specifier.
  /// `key_paths` lists candidate paths to the entry; the first one that
  /// resolves is updated (package.json may declare catalogs at the top level
  /// or nested under `workspaces`).
  Catalog {
    path: PathBuf,
    kind: super::ConfigKind,
    key_paths: Vec<KeyPath>,
  },
}

impl DepLocation {
  pub fn is_deno_json(&self) -> bool {
    matches!(self, DepLocation::DenoJson(..))
  }

  pub fn file_path(&self) -> Cow<'_, std::path::Path> {
    match self {
      DepLocation::DenoJson(arc, _, kind) => match kind {
        ImportMapKind::Inline => {
          Cow::Owned(arc.specifier.to_file_path().unwrap())
        }
        ImportMapKind::Outline(path) => Cow::Borrowed(path.as_path()),
      },
      DepLocation::PackageJson(arc, _) => Cow::Borrowed(arc.path.as_ref()),
      DepLocation::Catalog { path, .. } => Cow::Borrowed(path.as_path()),
    }
  }
  fn config_kind(&self) -> super::ConfigKind {
    match self {
      DepLocation::DenoJson(_, _, _) => super::ConfigKind::DenoJson,
      DepLocation::PackageJson(_, _) => super::ConfigKind::PackageJson,
      DepLocation::Catalog { kind, .. } => *kind,
    }
  }
}

struct DebugAdapter<T>(T);

impl std::fmt::Debug for DebugAdapter<&ConfigFileRc> {
  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    f.debug_struct("ConfigFile")
      .field("specifier", &self.0.specifier)
      .finish()
  }
}
impl std::fmt::Debug for DebugAdapter<&PackageJsonRc> {
  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    f.debug_struct("PackageJson")
      .field("path", &self.0.path)
      .finish()
  }
}

impl std::fmt::Debug for DepLocation {
  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    match self {
      DepLocation::DenoJson(arc, key_path, kind) => {
        let mut debug = f.debug_tuple("DenoJson");
        debug
          .field(&DebugAdapter(arc))
          .field(key_path)
          .field(kind)
          .finish()
      }
      DepLocation::PackageJson(arc, key_path) => {
        let mut debug = f.debug_tuple("PackageJson");
        debug.field(&DebugAdapter(arc)).field(key_path).finish()
      }
      DepLocation::Catalog {
        path,
        kind,
        key_paths,
      } => f
        .debug_struct("Catalog")
        .field("path", path)
        .field("kind", kind)
        .field("key_paths", key_paths)
        .finish(),
    }
  }
}

#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum DepKind {
  Jsr,
  Npm,
}

impl DepKind {
  pub fn scheme(&self) -> &'static str {
    match self {
      DepKind::Npm => "npm",
      DepKind::Jsr => "jsr",
    }
  }
}

#[derive(Clone, Debug)]
pub enum KeyPart {
  Imports,
  Scopes,
  Dependencies,
  DevDependencies,
  String(StackString),
}

impl From<PackageJsonDepKind> for KeyPart {
  fn from(value: PackageJsonDepKind) -> Self {
    match value {
      PackageJsonDepKind::Normal => Self::Dependencies,
      PackageJsonDepKind::Dev => Self::DevDependencies,
    }
  }
}

impl KeyPart {
  pub fn as_str(&self) -> &str {
    match self {
      KeyPart::Imports => "imports",
      KeyPart::Scopes => "scopes",
      KeyPart::Dependencies => "dependencies",
      KeyPart::DevDependencies => "devDependencies",
      KeyPart::String(s) => s.as_str(),
    }
  }
}

#[derive(Clone, Debug)]
pub struct KeyPath {
  pub parts: Vec<KeyPart>,
}

impl KeyPath {
  fn from_parts(parts: impl IntoIterator<Item = KeyPart>) -> Self {
    Self {
      parts: parts.into_iter().collect(),
    }
  }
  fn last(&self) -> Option<&KeyPart> {
    self.parts.last()
  }
  fn push(&mut self, part: KeyPart) {
    self.parts.push(part)
  }
}

#[derive(Clone, Debug)]
pub struct Dep {
  pub req: PackageReq,
  pub kind: DepKind,
  pub location: DepLocation,
  #[allow(dead_code, reason = "not used yet")]
  pub id: DepId,
  #[allow(dead_code, reason = "not used yet")]
  pub alias: Option<String>,
}

impl Dep {
  pub fn alias_or_name(&self) -> &str {
    self.alias.as_deref().unwrap_or_else(|| &self.req.name)
  }

  /// Whether this dependency is a development dependency. Only `package.json`
  /// `devDependencies` are considered dev; `deno.json` imports and `catalog`
  /// entries have no such distinction and are always treated as regular
  /// dependencies.
  pub fn is_dev(&self) -> bool {
    match &self.location {
      DepLocation::PackageJson(_, key_path) => {
        matches!(key_path.parts.first(), Some(KeyPart::DevDependencies))
      }
      DepLocation::DenoJson(..) | DepLocation::Catalog { .. } => false,
    }
  }
}

fn import_map_entries(
  import_map: &ImportMap,
) -> impl Iterator<Item = (KeyPath, SpecifierMapEntry<'_>)> {
  import_map
    .imports()
    .entries()
    .map(|entry| {
      (
        KeyPath::from_parts([
          KeyPart::Imports,
          KeyPart::String(entry.raw_key.into()),
        ]),
        entry,
      )
    })
    .chain(import_map.scopes().flat_map(|scope| {
      let path = KeyPath::from_parts([
        KeyPart::Scopes,
        KeyPart::String(scope.raw_key.into()),
      ]);

      scope.imports.entries().map(move |entry| {
        let mut full_path = path.clone();
        full_path.push(KeyPart::String(entry.raw_key.into()));
        (full_path, entry)
      })
    }))
}

fn to_import_map_value_from_imports(
  deno_json: &ConfigFile,
) -> serde_json::Value {
  let mut value = serde_json::Map::with_capacity(2);
  if let Some(imports) = &deno_json.json.imports {
    value.insert("imports".to_string(), imports.clone());
  }
  if let Some(scopes) = &deno_json.json.scopes {
    value.insert("scopes".to_string(), scopes.clone());
  }
  serde_json::Value::Object(value)
}

fn deno_json_import_map(
  deno_json: &ConfigFile,
) -> Result<Option<(ImportMapWithDiagnostics, ImportMapKind)>, AnyError> {
  let (value, kind) = if deno_json.json.imports.is_some()
    || deno_json.json.scopes.is_some()
  {
    (
      to_import_map_value_from_imports(deno_json),
      ImportMapKind::Inline,
    )
  } else {
    match deno_json.to_import_map_path()? {
      Some(path) => {
        let err_context = || {
          format!(
            "loading import map at '{}' (from \"importMap\" field in '{}')",
            path.display(),
            deno_json.specifier
          )
        };
        let text = std::fs::read_to_string(&path).with_context(err_context)?;
        let value = serde_json::from_str(&text).with_context(err_context)?;
        (value, ImportMapKind::Outline(path))
      }
      None => return Ok(None),
    }
  };

  import_map::parse_from_value(deno_json.specifier.clone(), value)
    .map_err(Into::into)
    .map(|import_map| Some((import_map, kind)))
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum PackageJsonDepKind {
  Normal,
  Dev,
}

fn add_deps_from_deno_json(
  deno_json: &Arc<ConfigFile>,
  mut filter: impl DepFilter,
  deps: &mut Vec<Dep>,
) {
  let (import_map, import_map_kind) = match deno_json_import_map(deno_json) {
    Ok(Some((import_map, import_map_kind))) => (import_map, import_map_kind),
    Ok(None) => return,
    Err(e) => {
      log::warn!("failed to parse imports from {}: {e}", &deno_json.specifier);
      return;
    }
  };
  for (key_path, entry) in import_map_entries(&import_map.import_map) {
    let Some(value) = entry.value else { continue };
    let kind = match value.scheme() {
      "npm" => DepKind::Npm,
      "jsr" => DepKind::Jsr,
      _ => continue,
    };
    let req = match parse_req_reference(value.as_str(), kind) {
      Ok(req) => req.req,
      Err(err) => {
        log::warn!("failed to parse package req \"{}\": {err}", value.as_str());
        continue;
      }
    };
    let alias: &str = key_path.last().unwrap().as_str().trim_end_matches('/');
    let alias = (alias != req.name).then(|| alias.to_string());
    if !filter.should_include(alias.as_deref(), &req, kind) {
      continue;
    }
    let id = DepId(deps.len());
    deps.push(Dep {
      location: DepLocation::DenoJson(
        deno_json.clone(),
        key_path,
        import_map_kind.clone(),
      ),
      kind,
      req,
      id,
      alias,
    });
  }
}

fn add_deps_from_package_json(
  package_json: &PackageJsonRc,
  filter: impl DepFilter,
  deps: &mut Vec<Dep>,
) {
  let package_json_deps = package_json.resolve_local_package_json_deps();

  fn iterate(
    package_json: &PackageJsonRc,
    mut filter: impl DepFilter,
    package_dep_kind: PackageJsonDepKind,
    package_json_deps: &PackageJsonDepsMap,
    deps: &mut Vec<Dep>,
  ) {
    for (k, v) in package_json_deps {
      let v = match v {
        Ok(v) => v,
        Err(e) => {
          log::warn!("bad package json dep value: {e}");
          continue;
        }
      };
      match v {
        deno_package_json::PackageJsonDepValue::File(_) => {
          // ignore
        }
        deno_package_json::PackageJsonDepValue::Req(req) => {
          let alias = k.as_str();
          let alias = (alias != req.name).then(|| alias.to_string());
          if !filter.should_include(alias.as_deref(), req, DepKind::Npm) {
            continue;
          }
          let id = DepId(deps.len());
          deps.push(Dep {
            id,
            kind: DepKind::Npm,
            location: DepLocation::PackageJson(
              package_json.clone(),
              KeyPath::from_parts([
                package_dep_kind.into(),
                KeyPart::String(k.clone()),
              ]),
            ),
            req: req.clone(),
            alias,
          })
        }
        deno_package_json::PackageJsonDepValue::Workspace { .. }
        | deno_package_json::PackageJsonDepValue::Catalog(_) => continue,
      }
    }
  }

  iterate(
    package_json,
    filter,
    PackageJsonDepKind::Normal,
    &package_json_deps.dependencies,
    deps,
  );
  iterate(
    package_json,
    filter,
    PackageJsonDepKind::Dev,
    &package_json_deps.dev_dependencies,
    deps,
  );
}

/// Collects dependencies declared in the workspace root `catalog`/`catalogs`
/// (referenced from members via the `catalog:` protocol). These live in a
/// single place in the root config, so they are gathered once here rather than
/// per member reference.
fn add_deps_from_catalogs(
  workspace: &Workspace,
  mut filter: impl DepFilter,
  deps: &mut Vec<Dep>,
) {
  // Mirror `Workspace`'s own resolution: a root package.json catalog takes
  // precedence over deno.json, and if it's present deno.json catalogs are
  // ignored entirely.
  let pkg_json = workspace
    .root_pkg_json()
    .filter(|pj| pj.catalog.is_some() || pj.catalogs.is_some());

  let (path, kind, catalog, catalogs, nested) = if let Some(pj) = pkg_json {
    (
      pj.path.clone(),
      super::ConfigKind::PackageJson,
      pj.catalog.as_ref(),
      pj.catalogs.as_ref(),
      // package.json may declare catalogs nested under `workspaces`.
      true,
    )
  } else if let Some(dj) = workspace.root_deno_json() {
    let Ok(path) = dj.specifier.to_file_path() else {
      return;
    };
    (
      path,
      super::ConfigKind::DenoJson,
      dj.catalog(),
      dj.catalogs(),
      false,
    )
  } else {
    return;
  };

  let mut add_entry = |name: &str,
                       version_req_str: &str,
                       key_paths: Vec<KeyPath>| {
    let version_req = match VersionReq::parse_from_npm(version_req_str) {
      Ok(version_req) => version_req,
      Err(err) => {
        log::warn!(
          "failed to parse catalog version requirement \"{version_req_str}\" for \"{name}\": {err}"
        );
        return;
      }
    };
    let req = PackageReq {
      name: name.into(),
      version_req,
    };
    // Catalog entries are keyed by the package name itself, so there is no
    // separate alias.
    if !filter.should_include(None, &req, DepKind::Npm) {
      return;
    }
    let id = DepId(deps.len());
    deps.push(Dep {
      id,
      kind: DepKind::Npm,
      location: DepLocation::Catalog {
        path: path.clone(),
        kind,
        key_paths,
      },
      req,
      alias: None,
    });
  };

  if let Some(catalog) = catalog {
    for (name, version_req) in catalog {
      let mut key_paths = vec![KeyPath::from_parts([
        KeyPart::String("catalog".into()),
        KeyPart::String(name.as_str().into()),
      ])];
      if nested {
        key_paths.push(KeyPath::from_parts([
          KeyPart::String("workspaces".into()),
          KeyPart::String("catalog".into()),
          KeyPart::String(name.as_str().into()),
        ]));
      }
      add_entry(name, version_req, key_paths);
    }
  }

  if let Some(catalogs) = catalogs {
    for (catalog_name, entries) in catalogs {
      for (name, version_req) in entries {
        let mut key_paths = vec![KeyPath::from_parts([
          KeyPart::String("catalogs".into()),
          KeyPart::String(catalog_name.as_str().into()),
          KeyPart::String(name.as_str().into()),
        ])];
        if nested {
          key_paths.push(KeyPath::from_parts([
            KeyPart::String("workspaces".into()),
            KeyPart::String("catalogs".into()),
            KeyPart::String(catalog_name.as_str().into()),
            KeyPart::String(name.as_str().into()),
          ]));
        }
        add_entry(name, version_req, key_paths);
      }
    }
  }
}

fn deps_from_workspace(
  workspace: &Arc<Workspace>,
  dep_filter: impl DepFilter,
) -> Result<Vec<Dep>, AnyError> {
  let mut deps = Vec::with_capacity(256);
  for deno_json in workspace.deno_jsons() {
    add_deps_from_deno_json(deno_json, dep_filter, &mut deps);
  }
  for package_json in workspace.package_jsons() {
    add_deps_from_package_json(package_json, dep_filter, &mut deps);
  }
  add_deps_from_catalogs(workspace, dep_filter, &mut deps);

  Ok(deps)
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct DepId(usize);

#[derive(Debug, Clone)]
pub enum Change {
  Update(DepId, VersionReq),
}

pub trait DepFilter: Copy {
  fn should_include(
    &mut self,
    alias: Option<&str>,
    package_req: &PackageReq,
    dep_kind: DepKind,
  ) -> bool;
}

impl<T> DepFilter for T
where
  T: FnMut(Option<&str>, &PackageReq, DepKind) -> bool + Copy,
{
  fn should_include<'a>(
    &mut self,
    alias: Option<&'a str>,
    package_req: &'a PackageReq,
    dep_kind: DepKind,
  ) -> bool {
    (*self)(alias, package_req, dep_kind)
  }
}

#[derive(Clone, Debug)]
pub struct PackageLatestVersion {
  pub semver_compatible: Option<PackageNv>,
  pub latest: Option<PackageNv>,
  /// Set when fetching the package's metadata failed (e.g. an unreachable or
  /// unauthorized private registry). The package is dropped from the outdated
  /// table, so this is used to warn the user instead of skipping silently.
  pub fetch_error: Option<Arc<PackageInfoLoadError>>,
}

pub struct DepManager {
  deps: Vec<Dep>,
  resolved_versions: Vec<Option<PackageNv>>,
  latest_versions: Vec<PackageLatestVersion>,

  pending_changes: Vec<Change>,

  dependencies_resolved: AtomicFlag,
  module_load_preparer: Arc<ModuleLoadPreparer>,
  // TODO(nathanwhit): probably shouldn't be pub
  pub(crate) jsr_fetch_resolver: Arc<JsrFetchResolver>,
  pub(crate) npm_fetch_resolver: Arc<NpmFetchResolver>,
  npm_resolver: CliNpmResolver,
  npm_version_resolver: Arc<NpmVersionResolver>,
  npm_installer: Arc<CliNpmInstaller>,
  permissions_container: PermissionsContainer,
  progress_bar: ProgressBar,
  main_module_graph_container: Arc<MainModuleGraphContainer>,
  lockfile: Option<Arc<CliLockfile>>,
}

pub struct DepManagerArgs {
  pub module_load_preparer: Arc<ModuleLoadPreparer>,
  pub jsr_fetch_resolver: Arc<JsrFetchResolver>,
  pub npm_fetch_resolver: Arc<NpmFetchResolver>,
  pub npm_installer: Arc<CliNpmInstaller>,
  pub npm_resolver: CliNpmResolver,
  pub npm_version_resolver: Arc<NpmVersionResolver>,
  pub permissions_container: PermissionsContainer,
  pub progress_bar: ProgressBar,
  pub main_module_graph_container: Arc<MainModuleGraphContainer>,
  pub lockfile: Option<Arc<CliLockfile>>,
}

impl DepManager {
  pub fn reloaded_after_modification(self, args: DepManagerArgs) -> Self {
    let mut new = Self::with_deps_args(self.deps, args);
    new.latest_versions = self.latest_versions;
    new
  }

  fn with_deps_args(deps: Vec<Dep>, args: DepManagerArgs) -> Self {
    let DepManagerArgs {
      module_load_preparer,
      jsr_fetch_resolver,
      npm_fetch_resolver,
      npm_installer,
      npm_resolver,
      npm_version_resolver,
      progress_bar,
      permissions_container,
      main_module_graph_container,
      lockfile,
    } = args;
    Self {
      deps,
      resolved_versions: Vec::new(),
      latest_versions: Vec::new(),
      jsr_fetch_resolver,
      dependencies_resolved: AtomicFlag::lowered(),
      module_load_preparer,
      npm_fetch_resolver,
      npm_installer,
      npm_resolver,
      npm_version_resolver,
      progress_bar,
      permissions_container,
      main_module_graph_container,
      lockfile,
      pending_changes: Vec::new(),
    }
  }

  pub fn from_workspace_dir(
    workspace_dir: &Arc<WorkspaceDirectory>,
    dep_filter: impl DepFilter,
    args: DepManagerArgs,
  ) -> Result<Self, AnyError> {
    let mut deps = Vec::with_capacity(256);
    if let Some(deno_json) = workspace_dir.member_deno_json() {
      if deno_json.specifier.scheme() != "file" {
        bail!("remote deno.json files are not supported");
      }
      let path = deno_json.specifier.to_file_path().unwrap();
      if path.parent().unwrap() == workspace_dir.dir_path() {
        add_deps_from_deno_json(deno_json, dep_filter, &mut deps);
      }
    }
    if let Some(package_json) = workspace_dir.member_pkg_json() {
      add_deps_from_package_json(package_json, dep_filter, &mut deps);
    }
    // Catalogs are a workspace-root concept, so only include them when running
    // in the root directory (the recursive path handles them via the workspace).
    if workspace_dir.dir_path() == workspace_dir.workspace.root_dir_path() {
      add_deps_from_catalogs(&workspace_dir.workspace, dep_filter, &mut deps);
    }

    Ok(Self::with_deps_args(deps, args))
  }

  pub fn from_workspace(
    workspace: &Arc<Workspace>,
    dep_filter: impl DepFilter,
    args: DepManagerArgs,
  ) -> Result<Self, AnyError> {
    let deps = deps_from_workspace(workspace, dep_filter)?;
    Ok(Self::with_deps_args(deps, args))
  }

  async fn run_dependency_resolution(&self) -> Result<(), AnyError> {
    if self.dependencies_resolved.is_raised() {
      return Ok(());
    }
    let _clear_guard = self.progress_bar.deferred_keep_initialize_alive();

    let mut graph_permit = self
      .main_module_graph_container
      .acquire_update_permit()
      .await;
    let graph = graph_permit.graph_mut();
    // populate the information from the lockfile
    if let Some(lockfile) = &self.lockfile {
      lockfile.fill_graph(graph);
    }

    let npm_resolver = self.npm_resolver.as_managed().unwrap();
    if self.deps.iter().all(|dep| match dep.kind {
      DepKind::Npm => npm_resolver
        .resolution()
        .resolve_pkg_id_from_pkg_req(&dep.req)
        .is_ok(),
      DepKind::Jsr => graph.packages.mappings().contains_key(&dep.req),
    }) {
      self.dependencies_resolved.raise();
      graph_permit.commit();
      return Ok(());
    }

    self
      .npm_installer
      .ensure_top_level_package_json_install()
      .await?;
    let mut roots = Vec::new();
    let mut info_futures = FuturesUnordered::new();
    for dep in &self.deps {
      if dep.location.is_deno_json() {
        match dep.kind {
          DepKind::Npm => roots.push(
            ModuleSpecifier::parse(&format!("npm:/{}/", dep.req)).unwrap(),
          ),
          DepKind::Jsr => {
            // Note: a tagged req (e.g. `@latest`) never appears in the graph
            // mappings because the graph build rejects jsr version tags, so
            // the VersionReq::matches call below (which panics on tags)
            // cannot be reached with one.
            let resolved_nv = graph.packages.mappings().get(&dep.req);
            let resolved_nv = resolved_nv
              .and_then(|nv| {
                let versions =
                  graph.packages.versions_by_name(&dep.req.name)?;
                let mut best = nv;
                for version in versions {
                  if version.version > best.version
                    && dep.req.version_req.matches(&version.version)
                  {
                    best = version;
                  }
                }
                Some(best)
              })
              .cloned();
            info_futures.push(async {
              let nv = if let Some(nv) = resolved_nv {
                nv
              } else if let Some(nv) =
                self.jsr_fetch_resolver.req_to_nv(&dep.req).await?
              {
                nv
              } else {
                return Result::<
                  Option<(Url, Arc<JsrPackageVersionInfo>)>,
                  JsrPackageReqNotFoundError,
                >::Ok(None);
              };
              if let Some(info) =
                self.jsr_fetch_resolver.package_version_info(&nv).await
              {
                let specifier =
                  ModuleSpecifier::parse(&format!("jsr:/{}/", &dep.req))
                    .unwrap();
                return Ok(Some((specifier, info)));
              }
              Ok(None)
            });
          }
        }
      }
    }

    while let Some(info_future) = info_futures.next().await {
      if let Some((specifier, info)) = info_future? {
        let exports = info.exports();
        for (k, _) in exports {
          if let Ok(spec) = specifier.join(k) {
            roots.push(spec);
          }
        }
      }
    }

    self
      .module_load_preparer
      .prepare_module_load(
        graph,
        &roots,
        crate::module_loader::PrepareModuleLoadOptions {
          is_dynamic: false,
          lib: deno_config::workspace::TsTypeLib::DenoWindow,
          permissions: self.permissions_container.clone(),
          file_permission_api_name: None,
          ext_overwrite: None,
          allow_unknown_media_types: true,
          skip_graph_roots_validation: false,
          file_content_overrides: Default::default(),
          file_header_overrides: Default::default(),
        },
      )
      .await?;

    self.dependencies_resolved.raise();
    graph_permit.commit();

    Ok(())
  }

  pub fn resolved_version(&self, id: DepId) -> Option<&PackageNv> {
    self.resolved_versions[id.0].as_ref()
  }

  pub async fn resolve_current_versions(&mut self) -> Result<(), AnyError> {
    self.run_dependency_resolution().await?;

    let graph = self.main_module_graph_container.graph();

    let mut resolved = Vec::with_capacity(self.deps.len());
    let snapshot = self
      .npm_resolver
      .as_managed()
      .unwrap()
      .resolution()
      .snapshot();
    let resolved_npm = snapshot.package_reqs();
    let resolved_jsr = graph.packages.mappings();
    for dep in &self.deps {
      match dep.kind {
        DepKind::Npm => {
          let resolved_version = resolved_npm.get(&dep.req).cloned();
          resolved.push(resolved_version);
        }
        DepKind::Jsr => {
          let resolved_version = resolved_jsr.get(&dep.req).cloned();
          resolved.push(resolved_version)
        }
      }
    }

    self.resolved_versions = resolved;

    Ok(())
  }

  async fn load_latest_versions(
    &self,
  ) -> Result<Vec<PackageLatestVersion>, AnyError> {
    if self.latest_versions.len() == self.deps.len() {
      return Ok(self.latest_versions.clone());
    }
    let mut latest_versions = Vec::with_capacity(self.deps.len());

    let npm_sema = Semaphore::new(32);
    let jsr_sema = Semaphore::new(32);
    let mut futs = FuturesOrdered::new();

    for dep in &self.deps {
      match dep.kind {
        DepKind::Npm => futs.push_back(
          async {
            let semver_req = &dep.req;
            let _permit = npm_sema.acquire().await;
            let mut semver_compatible = self
              .npm_fetch_resolver
              .req_to_nv(semver_req)
              .await
              .ok()
              .flatten();
            let info_result = self
              .npm_fetch_resolver
              .package_info_with_reason(&semver_req.name)
              .await;
            let fetch_error = info_result.as_ref().err().cloned();
            let latest = info_result
              .ok()
              .and_then(|info| {
                let version_resolver =
                  self.npm_version_resolver.get_for_package(&info);
                let latest_tag = info.dist_tags.get("latest")?;
                let can_use_latest = version_resolver
                  .version_req_satisfies_and_matches_newest_dependency_date(
                    &semver_req.version_req,
                    latest_tag,
                  )
                  .ok()?;

                if can_use_latest {
                  semver_compatible = Some(PackageNv {
                    name: semver_req.name.clone(),
                    version: latest_tag.clone(),
                  });
                  return Some(latest_tag.clone());
                }

                // Use the `latest` dist-tag directly, matching npm/pnpm/bun
                // behavior. Computing the max across all versions is incorrect
                // when packages publish pre-release versions with commit hashes
                // (e.g., 1.0.0-beta.9-commit.d91dfb5) because semver considers
                // alphanumeric pre-release identifiers higher than numeric ones,
                // so beta.9-commit.hash > beta.32.
                // Ref: https://github.com/denoland/deno/issues/29647
                if version_resolver.matches_newest_dependency_date(latest_tag) {
                  Some(latest_tag.clone())
                } else {
                  None
                }
              })
              .map(|version| PackageNv {
                name: semver_req.name.clone(),
                version,
              });
            PackageLatestVersion {
              latest,
              semver_compatible,
              fetch_error,
            }
          }
          .boxed_local(),
        ),
        DepKind::Jsr => futs.push_back(
          async {
            let semver_req = &dep.req;
            let _permit = jsr_sema.acquire().await;
            let semver_compatible = self
              .jsr_fetch_resolver
              .req_to_nv(semver_req)
              .await
              .ok()
              .flatten();
            let info_result = self
              .jsr_fetch_resolver
              .package_info_with_reason(&semver_req.name)
              .await;
            let fetch_error = info_result.as_ref().err().cloned();
            let latest = info_result
              .ok()
              .and_then(|info| {
                let version_resolver = self
                  .jsr_fetch_resolver
                  .version_resolver_for_package(&semver_req.name, &info);
                let lower_bound = &semver_compatible.as_ref()?.version;
                // Only consider pre-release versions when the currently
                // resolved version is itself a pre-release. Otherwise
                // `deno outdated --latest` reports a pre-release (e.g.
                // 2.0.3-alpha.0) as the latest version even though JSR
                // designates the newest stable release as latest. This mirrors
                // npm packages, which resolve latest from the `latest`
                // dist-tag. (#36320)
                //
                // JSR's `meta.json` also has a `latest` field (exposed as
                // `info.latest`), but it can't replace the scan below on its
                // own: it never points at a pre-release, so a project already
                // tracking a pre-release line would stop being offered newer
                // pre-releases, and it may fall outside the
                // `minimumDependencyAge` window, in which case we still want
                // the newest version that is old enough. That last fallback is
                // also where the two registries deliberately differ: the npm
                // path above reports no latest version at all when its `latest`
                // dist-tag is too new, rather than walking back to an older
                // version.
                let allow_pre_release = !lower_bound.pre.is_empty();
                {
                  let mut best: Option<&Version> = Some(lower_bound);
                  for version in info.versions.iter().filter_map(
                    |(version, version_info)| {
                      if !version_info.yanked
                        && (allow_pre_release || version.pre.is_empty())
                        && version_resolver
                          .matches_newest_dependency_date(version_info)
                      {
                        Some(version)
                      } else {
                        None
                      }
                    },
                  ) {
                    if best.is_none_or(|b| version > b) {
                      best = Some(version);
                    }
                  }
                  best.cloned()
                }
              })
              .map(|version| PackageNv {
                name: semver_req.name.clone(),
                version,
              });
            PackageLatestVersion {
              latest,
              semver_compatible,
              fetch_error,
            }
          }
          .boxed_local(),
        ),
      }
    }
    while let Some(nv) = futs.next().await {
      latest_versions.push(nv);
    }

    Ok(latest_versions)
  }

  pub async fn resolve_versions(&mut self) -> Result<(), AnyError> {
    let (_, latest_versions) = try_join(
      self.run_dependency_resolution(),
      self.load_latest_versions(),
    )
    .await?;

    self.latest_versions = latest_versions;

    self.resolve_current_versions().await?;

    Ok(())
  }

  pub fn deps_with_resolved_latest_versions(
    &self,
  ) -> impl IntoIterator<Item = (DepId, Option<PackageNv>, PackageLatestVersion)> + '_
  {
    self
      .resolved_versions
      .iter()
      .zip(self.latest_versions.iter())
      .enumerate()
      .map(|(i, (resolved, latest))| {
        (DepId(i), resolved.clone(), latest.clone())
      })
  }

  pub fn get_dep(&self, id: DepId) -> &Dep {
    &self.deps[id.0]
  }

  pub fn deps_with_ids(&self) -> impl Iterator<Item = (DepId, &Dep)> {
    self.deps.iter().enumerate().map(|(i, dep)| (DepId(i), dep))
  }

  pub fn update_dep(&mut self, dep_id: DepId, new_version_req: VersionReq) {
    self
      .pending_changes
      .push(Change::Update(dep_id, new_version_req));
  }

  pub fn commit_changes(&mut self) -> Result<(), AnyError> {
    let changes = std::mem::take(&mut self.pending_changes);
    let mut config_updaters = HashMap::new();
    for change in changes {
      match change {
        Change::Update(dep_id, version_req) => {
          // TODO: move most of this to ConfigUpdater
          let dep = &mut self.deps[dep_id.0];
          dep.req.version_req = version_req.clone();
          match &dep.location {
            DepLocation::DenoJson(arc, key_path, _) => {
              let updater =
                get_or_create_updater(&mut config_updaters, &dep.location)?;

              let Some(property) = updater.get_property_for_mutation(key_path)
              else {
                log::warn!(
                  "failed to find property at path {key_path:?} for file {}",
                  arc.specifier
                );
                continue;
              };
              let Some(string_value) = cst_string_literal(&property) else {
                continue;
              };
              let mut req_reference = match dep.kind {
                DepKind::Npm => NpmPackageReqReference::from_str(&string_value)
                  .unwrap()
                  .into_inner(),
                DepKind::Jsr => JsrPackageReqReference::from_str(&string_value)
                  .unwrap()
                  .into_inner(),
              };
              req_reference.req.version_req = version_req;
              let mut new_value =
                format!("{}:{}", dep.kind.scheme(), req_reference);
              if string_value.ends_with('/') && !new_value.ends_with('/') {
                // the display impl for PackageReqReference maps `/` to the root
                // subpath, but for the import map the trailing `/` is significant
                new_value.push('/');
              }
              if string_value
                .trim_start_matches(format!("{}:", dep.kind.scheme()).as_str())
                .starts_with('/')
              {
                // this is gross
                new_value = new_value.replace(':', ":/");
              }
              property
                .set_value(jsonc_parser::cst::CstInputValue::String(new_value));
            }
            DepLocation::PackageJson(arc, key_path) => {
              let updater =
                get_or_create_updater(&mut config_updaters, &dep.location)?;
              let Some(property) = updater.get_property_for_mutation(key_path)
              else {
                log::warn!(
                  "failed to find property at path {key_path:?} for file {}",
                  arc.path.display()
                );
                continue;
              };
              let Some(string_value) = cst_string_literal(&property) else {
                continue;
              };
              let new_value = if string_value.starts_with("npm:") {
                // aliased
                let rest = string_value.trim_start_matches("npm:");
                let mut parts = rest.split('@');
                let first = parts.next().unwrap();
                if first.is_empty() {
                  let scope_and_name = parts.next().unwrap();
                  format!("npm:@{scope_and_name}@{version_req}")
                } else {
                  format!("npm:{first}@{version_req}")
                }
              } else if string_value.contains(":") {
                bail!(
                  "Unexpected package json dependency string: \"{string_value}\" in {}",
                  arc.path.display()
                );
              } else {
                version_req.to_string()
              };
              property
                .set_value(jsonc_parser::cst::CstInputValue::String(new_value));
            }
            DepLocation::Catalog {
              path, key_paths, ..
            } => {
              let updater =
                get_or_create_updater(&mut config_updaters, &dep.location)?;
              if !updater
                .update_catalog_entry(key_paths, &version_req.to_string())
              {
                log::warn!(
                  "failed to find catalog entry for \"{}\" in {}",
                  dep.req.name,
                  path.display()
                );
              }
            }
          }
        }
      }
    }

    for (_, updater) in config_updaters {
      updater.commit()?;
    }

    Ok(())
  }

  /// Removes the lockfile specifier entries for the given dependencies and
  /// writes the lockfile to disk. The next install will re-resolve the
  /// missing entries to the latest version that matches each dependency's
  /// existing version requirement, bumping the lockfile within ranges
  /// without modifying `deno.json`/`package.json`.
  pub fn invalidate_lockfile_for_update(
    &self,
    dep_ids: impl IntoIterator<Item = DepId>,
  ) -> Result<(), AnyError> {
    let Some(lockfile_lock) = &self.lockfile else {
      return Ok(());
    };
    {
      let mut lockfile = lockfile_lock.lock();
      for dep_id in dep_ids {
        let dep = &self.deps[dep_id.0];
        let req = match dep.kind {
          DepKind::Npm => JsrDepPackageReq::npm(dep.req.clone()),
          DepKind::Jsr => JsrDepPackageReq::jsr(dep.req.clone()),
        };
        if lockfile.content.packages.specifiers.remove(&req).is_some() {
          lockfile.has_content_changed = true;
        }
      }
    }
    lockfile_lock.write_if_changed()?;
    Ok(())
  }
}

fn get_or_create_updater<'a>(
  config_updaters: &'a mut HashMap<std::path::PathBuf, ConfigUpdater>,
  location: &DepLocation,
) -> Result<&'a mut ConfigUpdater, AnyError> {
  match config_updaters.entry(location.file_path().into_owned()) {
    std::collections::hash_map::Entry::Occupied(occupied_entry) => {
      Ok(occupied_entry.into_mut())
    }
    std::collections::hash_map::Entry::Vacant(vacant_entry) => {
      let updater = ConfigUpdater::new(
        location.config_kind(),
        location.file_path().into_owned(),
      )?;
      Ok(vacant_entry.insert(updater))
    }
  }
}

fn cst_string_literal(
  property: &jsonc_parser::cst::CstObjectProp,
) -> Option<String> {
  // TODO(nathanwhit): ensure this unwrap is safe
  let value = property.value().unwrap();
  let Some(string) = value.as_string_lit() else {
    log::warn!("malformed entry");
    return None;
  };
  let Ok(string_value) = string.decoded_value() else {
    log::warn!("malformed string: {string:?}");
    return None;
  };
  Some(string_value)
}

fn parse_req_reference(
  input: &str,
  kind: DepKind,
) -> Result<
  PackageReqReference,
  deno_semver::package::PackageReqReferenceParseError,
> {
  Ok(match kind {
    DepKind::Npm => NpmPackageReqReference::from_str(input)?.into_inner(),
    DepKind::Jsr => JsrPackageReqReference::from_str(input)?.into_inner(),
  })
}
