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

use std::collections::HashSet;
use std::sync::Arc;

use deno_core::anyhow::bail;
use deno_core::error::AnyError;
use deno_core::url::Url;
use deno_npm::NpmPackageId;
use deno_resolver::workspace::WorkspaceResolver;

use super::InstallReporter;
use super::print_install_report;
use crate::args::AddFlags;
use crate::args::CiFlags;
use crate::args::Flags;
use crate::args::InstallFlagsLocal;
use crate::args::InstallTopLevelFlags;
use crate::args::SyncTypesFlags;
use crate::factory::CliFactory;
use crate::npm::CliNpmResolver;
use crate::sys::CliSys;

pub async fn install_local(
  flags: Arc<Flags>,
  install_flags: InstallFlagsLocal,
) -> Result<(), AnyError> {
  match install_flags {
    InstallFlagsLocal::Add(add_flags) => {
      crate::tools::pm::add(
        flags,
        add_flags,
        crate::tools::pm::AddCommandName::Install,
      )
      .await
    }
    InstallFlagsLocal::Entrypoints(entrypoints) => {
      super::install_from_entrypoints(flags, entrypoints).await
    }
    InstallFlagsLocal::TopLevel(top_level_flags) => {
      install_top_level(flags, top_level_flags).await
    }
  }
}

#[derive(Debug, Default)]
pub struct CategorizedInstalledDeps {
  pub normal_deps: Vec<NpmPackageId>,
  pub dev_deps: Vec<NpmPackageId>,
}

pub fn categorize_installed_npm_deps(
  npm_resolver: &CliNpmResolver,
  workspace: &WorkspaceResolver<CliSys>,
  install_reporter: &InstallReporter,
) -> CategorizedInstalledDeps {
  let Some(managed_resolver) = npm_resolver.as_managed() else {
    return CategorizedInstalledDeps::default();
  };
  // compute the summary info
  let snapshot = managed_resolver.resolution().snapshot();

  let top_level_packages = snapshot.top_level_packages();

  // all this nonsense is to categorize into normal and dev deps
  let mut normal_deps = HashSet::new();
  let mut dev_deps = HashSet::new();

  for package_json in workspace.package_jsons() {
    let deps = package_json.resolve_local_package_json_deps();
    for (k, v) in deps.dependencies.iter() {
      let Ok(s) = v else {
        continue;
      };
      match s {
        deno_package_json::PackageJsonDepValue::File(_) => {
          // TODO(nathanwhit)
          // TODO(bartlomieju)
        }
        deno_package_json::PackageJsonDepValue::Req(package_req) => {
          normal_deps.insert(package_req.name.to_string());
        }
        deno_package_json::PackageJsonDepValue::Workspace { .. } => {
          // ignore workspace deps
        }
        deno_package_json::PackageJsonDepValue::Catalog(catalog_name) => {
          if workspace
            .resolve_catalog_dep(k.as_str(), catalog_name)
            .is_some()
          {
            normal_deps.insert(k.to_string());
          }
        }
      }
    }

    for (k, v) in deps.dev_dependencies.iter() {
      let Ok(s) = v else {
        continue;
      };
      match s {
        deno_package_json::PackageJsonDepValue::File(_) => {
          // TODO(nathanwhit)
          // TODO(bartlomieju)
        }
        deno_package_json::PackageJsonDepValue::Req(package_req) => {
          dev_deps.insert(package_req.name.to_string());
        }
        deno_package_json::PackageJsonDepValue::Workspace { .. } => {
          // ignore workspace deps
        }
        deno_package_json::PackageJsonDepValue::Catalog(catalog_name) => {
          if workspace
            .resolve_catalog_dep(k.as_str(), catalog_name)
            .is_some()
          {
            dev_deps.insert(k.to_string());
          }
        }
      }
    }
  }

  let mut installed_normal_deps = Vec::new();
  let mut installed_dev_deps = Vec::new();

  let npm_installed_set = if npm_resolver.root_node_modules_path().is_some() {
    &install_reporter.stats.intialized_npm
  } else {
    &install_reporter.stats.downloaded_npm
  };
  for pkg in top_level_packages {
    if !npm_installed_set.contains(&pkg.nv.to_string()) {
      continue;
    }
    if normal_deps.contains(&pkg.nv.name.to_string()) {
      installed_normal_deps.push(pkg);
    } else if dev_deps.contains(&pkg.nv.name.to_string()) {
      installed_dev_deps.push(pkg);
    } else {
      installed_normal_deps.push(pkg);
    }
  }

  installed_normal_deps.sort_by(|a, b| a.nv.name.cmp(&b.nv.name));
  installed_dev_deps.sort_by(|a, b| a.nv.name.cmp(&b.nv.name));

  CategorizedInstalledDeps {
    normal_deps: installed_normal_deps.into_iter().cloned().collect(),
    dev_deps: installed_dev_deps.into_iter().cloned().collect(),
  }
}

async fn install_top_level(
  flags: Arc<Flags>,
  top_level_flags: InstallTopLevelFlags,
) -> Result<(), AnyError> {
  let start_instant = std::time::Instant::now();
  let factory = CliFactory::from_flags(flags);
  // surface any errors in the package.json
  factory
    .npm_installer()
    .await?
    .ensure_no_pkg_json_dep_errors()?;
  let npm_installer = factory.npm_installer().await?;
  npm_installer.ensure_no_pkg_json_dep_errors()?;

  // the actual work
  crate::tools::pm::cache_top_level_deps(
    &factory,
    None,
    crate::tools::pm::CacheTopLevelDepsOptions {
      lockfile_only: top_level_flags.lockfile_only,
      additional_roots: vec![],
    },
  )
  .await?;

  if let Some(lockfile) = factory.maybe_lockfile().await? {
    lockfile.write_if_changed()?;
  }

  let install_reporter = factory.install_reporter()?.unwrap().clone();
  let workspace = factory.workspace_resolver().await?;
  let npm_resolver = factory.npm_resolver().await?;
  print_install_report(
    &factory.sys(),
    start_instant.elapsed(),
    &install_reporter,
    workspace,
    npm_resolver,
  );

  Ok(())
}

pub async fn ci_command(
  flags: Arc<Flags>,
  ci_flags: CiFlags,
) -> Result<(), AnyError> {
  let factory = CliFactory::from_flags(flags.clone());
  if factory.maybe_lockfile().await?.is_none() {
    bail!(
      "deno ci requires a lockfile, but none was found.\n  hint: run `deno install` to create one."
    );
  }
  if let Some(node_modules_dir) = factory.node_modules_dir_path()?
    && node_modules_dir.exists()
  {
    log::info!(
      "{} {}",
      deno_terminal::colors::gray("Removing"),
      node_modules_dir.display()
    );
    std::fs::remove_dir_all(node_modules_dir).map_err(|err| {
      deno_core::anyhow::anyhow!(
        "failed to remove {}: {err}",
        node_modules_dir.display()
      )
    })?;
  }
  drop(factory);
  install_top_level(
    flags,
    InstallTopLevelFlags {
      lockfile_only: false,
      production: ci_flags.production,
      skip_types: ci_flags.skip_types,
    },
  )
  .await
}

/// `deno sync-types`: generate a tsconfig + type mappings so stock TypeScript
/// tooling can type-check the project. Assumes dependencies are already
/// installed (run `deno install` first); materializes `jsr:`/`http(s):` types
/// and writes `.deno/tsconfig.json`.
/// How `sync_types_command` should treat the root `tsconfig.json`.
#[derive(Debug, Clone, Copy)]
pub enum RootTsConfigMode {
  /// Always create/update a root `tsconfig.json` extending `.deno/tsconfig.json`
  /// (used by the standalone `deno sync-types` command, for IDE setup).
  Always,
  /// Only manage the root `tsconfig.json` when Deno is honoring a user's
  /// tsconfig (a deno.json/package.json is present and config isn't disabled).
  /// Used by `deno check`, which otherwise points tsc at `.deno/tsconfig.json`
  /// directly and must not rewrite a tsconfig Deno is ignoring.
  CheckMode,
}

pub async fn sync_types_command(
  flags: Arc<Flags>,
  sync_types_flags: SyncTypesFlags,
  root_tsconfig_mode: RootTsConfigMode,
) -> Result<(), AnyError> {
  use crate::args::TsTypeLib;
  use crate::graph_container::CollectSpecifiersOptions;

  let factory = CliFactory::from_flags(flags);
  let cli_options = factory.cli_options()?;
  let file_fetcher = factory.file_fetcher()?.clone();
  let http_client = factory.http_client_provider().get_or_create()?;
  let permissions = factory.root_permissions_container()?.clone();

  // Generate at the workspace/config root, not the current working directory,
  // so running `deno sync-types` from a subdirectory still writes the config
  // next to deno.json and picks up the root import map.
  let project_root = cli_options
    .workspace()
    .root_dir_url()
    .to_file_path()
    .map_err(|_| {
      deno_core::anyhow::anyhow!("workspace root is not a local directory")
    })?;

  let has_explicit_roots = !sync_types_flags.roots.is_empty();
  let root_patterns = if has_explicit_roots {
    sync_types_flags.roots
  } else {
    vec![".".to_string()]
  };

  // Build the module graph over either the requested roots or the whole
  // project to discover every external (npm:/jsr:/http(s):) specifier the code
  // actually uses — including specifiers written directly in source and across
  // workspace members, not just those declared in the root import map.
  let (graph_specifiers, local_wasm_modules) = {
    let graph_container = factory.main_module_graph_container().await?;
    let roots = graph_container.collect_specifiers(
      &root_patterns,
      CollectSpecifiersOptions {
        include_ignored_specified: has_explicit_roots,
      },
    )?;
    // `collect_specifiers` honors deno.json excludes and the vendor dir, but not
    // `node_modules`, our generated `.deno/`, or the DENO_DIR cache. On a real
    // project those trees hold tens of thousands of files; feeding them as graph
    // roots makes the build choke. Keep only the project's own source so we
    // discover the external specifiers it actually imports.
    //
    // The `node_modules`/`.deno` check is on the path RELATIVE to the project
    // root, so a project that happens to live beneath an ancestor directory
    // named `node_modules` (e.g. developed in place) isn't filtered away
    // entirely. DENO_DIR is filtered by prefix when resolvable; it's normally
    // outside the project so its files aren't collected as roots regardless.
    let deno_dir_root = factory.deno_dir().ok().map(|d| d.root.clone());
    let roots: Vec<_> = roots
      .into_iter()
      .filter(|u| {
        let Ok(path) = u.to_file_path() else {
          return true;
        };
        if let Some(root) = &deno_dir_root
          && path.starts_with(root)
        {
          return false;
        }
        let rel = path.strip_prefix(&project_root).unwrap_or(&path);
        !rel.components().any(|c| {
          matches!(c.as_os_str().to_str(), Some("node_modules") | Some(".deno"))
        })
      })
      .collect();
    if has_explicit_roots && roots.is_empty() {
      bail!("No matching module graph roots found.");
    }

    // Build the graph error-tolerantly: `create_graph_with_options` populates
    // the graph and records unresolved modules as error entries without failing
    // the whole build (unlike `check_specifiers`, which validates and discards
    // everything on the first missing module). A real project always has some
    // broken file (dead example scripts, etc.); we want every specifier that
    // *did* resolve regardless.
    let graph_creator = factory.module_graph_creator().await?;
    // Silence warn-level diagnostics during this discovery build. It's an
    // internal step (not the user's `deno check`), and the graph the user
    // installed was already validated by `deno install` — re-emitting e.g.
    // "workspace member ... was not used" warnings here is just noise.
    let prev_log_level = log::max_level();
    log::set_max_level(log::LevelFilter::Error);
    let graph_result = graph_creator
      .create_graph_with_options(crate::graph_util::CreateGraphOptions {
        graph_kind: deno_graph::GraphKind::All,
        roots: roots.clone(),
        imports: vec![],
        is_dynamic: false,
        loader: None,
        npm_caching: cli_options.default_npm_caching_strategy(),
      })
      .await;
    log::set_max_level(prev_log_level);
    let graph = match graph_result {
      Ok(graph) => graph,
      Err(e) => {
        log::debug!("sync-types: graph build failed (continuing): {e}");
        deno_graph::ModuleGraph::new(deno_graph::GraphKind::All)
      }
    };

    let mut specifiers = std::collections::BTreeSet::new();
    // Remote graph roots do not appear as a dependency edge, but still need to
    // be mirrored for stock TypeScript and included in its project.
    for root in &roots {
      if matches!(root.scheme(), "http" | "https") {
        specifiers.insert(root.to_string());
      }
    }
    // Local `.wasm` modules (scheme `file:`) can't be type-checked as binaries:
    // stock tsc reports a spurious `TS2307 Cannot find module './foo.wasm'`.
    // deno_graph already materialized each wasm module's typed exports into a
    // `.d.ts` (`source_dts`); collect them here so `setup_npm_compat` can write
    // them out and point tsc at them (mirroring the remote-wasm treatment in
    // `install_http_modules`).
    let mut local_wasm_modules: Vec<(Url, String)> = Vec::new();
    for module in graph.modules() {
      if let deno_graph::Module::Wasm(wasm) = module
        && wasm.specifier.scheme() == "file"
      {
        let dts = if !wasm.source_dts.is_empty() {
          wasm.source_dts.to_string()
        } else {
          match deno_graph::source::wasm::wasm_module_to_dts(&wasm.source) {
            Ok(dts) => dts,
            Err(e) => {
              log::debug!(
                "wasm dts generation failed for {}: {e}",
                wasm.specifier
              );
              continue;
            }
          }
        };
        local_wasm_modules.push((wasm.specifier.clone(), dts));
      }
      for (raw, _dep) in module.dependencies() {
        // Collect scheme specifiers (npm:/jsr:/http:) and bare specifiers
        // (import-map aliases like `@std/fmt/colors`, `fresh/runtime`). Skip
        // relative imports and node: builtins. setup_npm_compat resolves the
        // bare ones against the import map.
        if raw.starts_with('.')
          || raw.starts_with('/')
          || raw.starts_with("node:")
        {
          continue;
        }
        specifiers.insert(raw.clone());
      }
    }
    (
      specifiers.into_iter().collect::<Vec<_>>(),
      local_wasm_modules,
    )
  };

  // The stock TypeScript compatibility config needs the managed npm
  // resolution snapshot in global-cache mode. It uses that snapshot to give
  // each resolved package copy its own referenced tsconfig and dependency
  // paths, preserving the contextual resolution that a node_modules tree
  // would normally provide.
  let npm_resolver = factory.npm_resolver().await?.clone();

  // Deno's *resolved* compiler options for the project root. These fold in
  // Deno's source-kind defaults (deno.json vs tsconfig.json) and CLI overrides
  // like `--check-js`, which the raw deno.json read inside `setup_npm_compat`
  // can't see. Resolve against a representative file under the root so any
  // detected tsconfig `include`/`files` filter matches.
  let root_specifier = cli_options
    .workspace()
    .root_dir_url()
    .join("__deno_root__.ts")
    .ok();
  let resolved_compiler_options = root_specifier.as_ref().and_then(|spec| {
    factory
      .compiler_options_resolver()
      .ok()
      .and_then(|resolver| {
        resolver
          .for_specifier(spec)
          .compiler_options_for_lib(TsTypeLib::DenoWindow)
          .ok()
      })
      .map(|opts| opts.0.clone())
  });

  // Whether to manage the root `tsconfig.json`. `deno check` (CheckMode) never
  // rewrites a committed tsconfig (that would dirty the working tree). When it
  // honors a user tsconfig it instead builds a throwaway overlay in a temp file
  // (see `native_check` / `build_check_root_overlay`), leaving the user's tree
  // untouched. `deno sync-types` (Always) still sets up the root tsconfig for
  // IDEs.
  let manage_root_tsconfig = match root_tsconfig_mode {
    RootTsConfigMode::Always => true,
    RootTsConfigMode::CheckMode => false,
  };

  // `deno check --all` type-checks remote modules; otherwise mirrored remote
  // scripts get `// @ts-nocheck` so tsc skips their internals.
  let type_check_remote =
    cli_options.type_check_mode() == crate::args::TypeCheckMode::All;

  let installed = super::npm_compat::setup_npm_compat(
    &project_root,
    &file_fetcher,
    &http_client,
    &permissions,
    &graph_specifiers,
    &local_wasm_modules,
    &npm_resolver,
    resolved_compiler_options.as_ref(),
    manage_root_tsconfig,
    type_check_remote,
  )
  .await?;

  log::info!(
    "{} tsconfig for stock TypeScript at {}",
    deno_terminal::colors::green("Synced"),
    project_root.join("tsconfig.json").display(),
  );
  for pkg in &installed {
    log::debug!("  installed jsr package {}@{}", pkg.name, pkg.version);
  }
  if !installed.is_empty() {
    log::info!("  installed {} jsr package(s)", installed.len());
  }
  Ok(())
}

pub fn check_if_installs_a_single_package_globally(
  maybe_add_flags: Option<&AddFlags>,
) -> Result<(), AnyError> {
  let Some(add_flags) = maybe_add_flags else {
    return Ok(());
  };
  if add_flags.packages.len() != 1 {
    return Ok(());
  }
  let Ok(url) = Url::parse(&add_flags.packages[0]) else {
    return Ok(());
  };
  if matches!(url.scheme(), "http" | "https") {
    bail!(
      "Failed to install \"{}\" specifier. If you are trying to install {} globally, run again with `-g` flag:\n  deno install -g {}",
      url.scheme(),
      url.as_str(),
      url.as_str()
    );
  }
  Ok(())
}
