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

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

use boxed_error::Boxed;
use deno_config::workspace::Workspace;
use deno_npmrc::NpmRc;
use deno_npmrc::NpmRegistryUrl;
use deno_npmrc::RegistryConfigWithUrl;
use deno_npmrc::ResolvedNpmRc;
use sys_traits::EnvHomeDir;
use sys_traits::EnvVar;
use sys_traits::FsRead;
use thiserror::Error;

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

#[derive(Debug, Boxed)]
pub struct NpmRcDiscoverError(pub Box<NpmRcDiscoverErrorKind>);

#[derive(Debug, Error)]
pub enum NpmRcDiscoverErrorKind {
  #[error(transparent)]
  Load(#[from] NpmRcLoadError),
  #[error(transparent)]
  Parse(#[from] NpmRcParseError),
  #[error(transparent)]
  Resolve(#[from] NpmRcOptionsResolveError),
  #[error(transparent)]
  UrlToFilePath(#[from] deno_path_util::UrlToFilePathError),
}

#[derive(Debug, Error)]
#[error("Error loading .npmrc at {}.", path.display())]
pub struct NpmRcLoadError {
  path: PathBuf,
  #[source]
  source: std::io::Error,
}

#[derive(Debug, Error)]
#[error("Failed to parse .npmrc at {}.", path.display())]
pub struct NpmRcParseError {
  path: PathBuf,
  #[source]
  source: deno_npmrc::NpmRcParseError,
}

#[derive(Debug, Error)]
#[error("Failed to resolve .npmrc options at {}.", path.display())]
pub struct NpmRcOptionsResolveError {
  path: PathBuf,
  #[source]
  source: deno_npmrc::ResolveError,
}

/// Discover `.npmrc` file - currently we only support it next to `package.json`,
/// next to `deno.json`, or in the user's home directory.
///
/// In the future we will need to support it in the global directory
/// as per https://docs.npmjs.com/cli/v10/configuring-npm/npmrc#files.
pub fn discover_npmrc_from_workspace<TSys: EnvVar + EnvHomeDir + FsRead>(
  sys: &TSys,
  workspace: &Workspace,
) -> Result<(ResolvedNpmRc, Option<PathBuf>), NpmRcDiscoverError> {
  let root_folder = workspace.root_folder_configs();
  discover_npmrc(
    sys,
    root_folder.pkg_json.as_ref().map(|p| p.path.clone()),
    match &root_folder.deno_json {
      Some(cf) if cf.specifier.scheme() == "file" => {
        Some(deno_path_util::url_to_file_path(&cf.specifier)?)
      }
      _ => None,
    },
  )
}

fn discover_npmrc<TSys: EnvVar + EnvHomeDir + FsRead>(
  sys: &TSys,
  maybe_package_json_path: Option<PathBuf>,
  maybe_deno_json_path: Option<PathBuf>,
) -> Result<(ResolvedNpmRc, Option<PathBuf>), NpmRcDiscoverError> {
  const NPMRC_NAME: &str = ".npmrc";

  fn try_to_read_npmrc(
    sys: &impl FsRead,
    dir: &Path,
  ) -> Result<Option<(Cow<'static, str>, PathBuf)>, NpmRcLoadError> {
    let path = dir.join(NPMRC_NAME);
    let maybe_source = match sys.fs_read_to_string(&path) {
      Ok(source) => Some(source),
      Err(err) if err.kind() == std::io::ErrorKind::NotFound => None,
      Err(err) => return Err(NpmRcLoadError { path, source: err }),
    };

    Ok(maybe_source.map(|source| (source, path)))
  }

  fn try_to_parse_npmrc(
    sys: &impl EnvVar,
    source: &str,
    path: &Path,
  ) -> Result<NpmRc, NpmRcDiscoverError> {
    let npmrc =
      NpmRc::parse(sys, source).map_err(|source| NpmRcParseError {
        path: path.to_path_buf(),
        source,
      })?;
    log::debug!(".npmrc found at: '{}'", path.display());
    Ok(npmrc)
  }

  fn merge_npm_rc(project_rc: NpmRc, home_rc: NpmRc) -> NpmRc {
    fn merge_maps<TValue>(
      mut project: HashMap<String, TValue>,
      home: HashMap<String, TValue>,
    ) -> HashMap<String, TValue> {
      for (key, value) in home {
        project.entry(key).or_insert(value);
      }
      project
    }

    NpmRc {
      registry: project_rc.registry.or(home_rc.registry),
      scope_registries: merge_maps(
        project_rc.scope_registries,
        home_rc.scope_registries,
      ),
      registry_configs: merge_maps(
        project_rc.registry_configs,
        home_rc.registry_configs,
      ),
      replace_registry_host: project_rc
        .replace_registry_host
        .or(home_rc.replace_registry_host),
      min_release_age_days: project_rc
        .min_release_age_days
        .or(home_rc.min_release_age_days),
      trust_policy: if project_rc.trust_policy
        != deno_npmrc::TrustPolicyConfig::Off
      {
        project_rc.trust_policy
      } else {
        home_rc.trust_policy
      },
      trust_policy_ignore_after_minutes: project_rc
        .trust_policy_ignore_after_minutes
        .or(home_rc.trust_policy_ignore_after_minutes),
      // union of project and home excludes (project entries first), so an
      // exemption in either file applies
      trust_policy_exclude: {
        let mut excludes = project_rc.trust_policy_exclude;
        for pkg in home_rc.trust_policy_exclude {
          if !excludes.contains(&pkg) {
            excludes.push(pkg);
          }
        }
        excludes
      },
    }
  }

  let mut home_npmrc = None;
  let mut project_npmrc = None;

  // 1. Try `.npmrc` in the user's home directory
  if let Some(home_dir) = sys.env_home_dir() {
    match try_to_read_npmrc(sys, &home_dir) {
      Ok(Some((source, path))) => {
        let npmrc = try_to_parse_npmrc(sys, &source, &path)?;
        home_npmrc = Some((path, npmrc));
      }
      Ok(None) => {}
      Err(err) if err.source.kind() == std::io::ErrorKind::PermissionDenied => {
        log::debug!(
          "Skipping .npmrc in home directory due to permission denied error. {:#}",
          err
        );
      }
      Err(err) => {
        return Err(err.into());
      }
    }
  }

  // 2. Try `.npmrc` next to `package.json`
  if let Some(package_json_path) = maybe_package_json_path
    && let Some(package_json_dir) = package_json_path.parent()
    && let Some((source, path)) = try_to_read_npmrc(sys, package_json_dir)?
  {
    let npmrc = try_to_parse_npmrc(sys, &source, &path)?;
    project_npmrc = Some((path, npmrc));
  }

  // 3. Try `.npmrc` next to `deno.json(c)` when not found `package.json`
  if project_npmrc.is_none()
    && let Some(deno_json_path) = maybe_deno_json_path
    && let Some(deno_json_dir) = deno_json_path.parent()
    && let Some((source, path)) = try_to_read_npmrc(sys, deno_json_dir)?
  {
    let npmrc = try_to_parse_npmrc(sys, &source, &path)?;
    project_npmrc = Some((path, npmrc));
  }

  let resolve_npmrc = |path: PathBuf, npm_rc: NpmRc| {
    let registry_url = NpmRegistryUrl::for_npm(sys);
    let mut resolved_npm_rc =
      npm_rc.as_resolved(&registry_url).map_err(|source| {
        NpmRcOptionsResolveError {
          path: path.to_path_buf(),
          source,
        }
      })?;
    resolved_npm_rc
      .scopes
      .entry("jsr".to_string())
      .or_insert_with(|| RegistryConfigWithUrl {
        registry_url: NpmRegistryUrl::for_jsr(sys).url,
        config: Default::default(),
      });
    Ok((resolved_npm_rc, Some(path)))
  };

  match (home_npmrc, project_npmrc) {
    (None, None) => {
      log::debug!("No .npmrc file found");
      Ok((create_default_npmrc(sys), None))
    }
    (None, Some((npmrc_path, project_rc))) => {
      log::debug!("Only project .npmrc file found");
      resolve_npmrc(npmrc_path, project_rc)
    }
    (Some((npmrc_path, home_rc)), None) => {
      log::debug!("Only home .npmrc file found");
      resolve_npmrc(npmrc_path, home_rc)
    }
    (Some((_, home_rc)), Some((npmrc_path, project_rc))) => {
      log::debug!("Both home and project .npmrc files found");
      let merged_npmrc = merge_npm_rc(project_rc, home_rc);
      resolve_npmrc(npmrc_path, merged_npmrc)
    }
  }
}

pub fn create_default_npmrc(sys: &impl EnvVar) -> ResolvedNpmRc {
  ResolvedNpmRc {
    default_config: deno_npmrc::RegistryConfigWithUrl {
      registry_url: NpmRegistryUrl::for_npm(sys).url,
      config: Default::default(),
    },
    scopes: HashMap::from([(
      "jsr".to_string(),
      RegistryConfigWithUrl {
        registry_url: NpmRegistryUrl::for_jsr(sys).url,
        config: Default::default(),
      },
    )]),
    registry_configs: Default::default(),
    replace_registry_host: deno_npmrc::ReplaceRegistryHost::for_npm(sys)
      .unwrap_or_default(),
    min_release_age_days: deno_npmrc::min_release_age_days_from_env(sys),
    trust_policy: Default::default(),
    trust_policy_ignore_after_minutes: None,
    trust_policy_exclude: Vec::new(),
  }
}
