// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.

use std::fmt::Debug;
use std::path::PathBuf;
use std::sync::Arc;

use boxed_error::Boxed;
use deno_semver::npm::NpmPackageReqReference;
use deno_semver::package::PackageReq;
use node_resolver::env::NodeResolverEnv;
use node_resolver::errors::NodeResolveError;
use node_resolver::errors::NodeResolveErrorKind;
use node_resolver::errors::PackageFolderResolveErrorKind;
use node_resolver::errors::PackageFolderResolveIoError;
use node_resolver::errors::PackageNotFoundError;
use node_resolver::errors::PackageResolveErrorKind;
use node_resolver::errors::PackageSubpathResolveError;
use node_resolver::InNpmPackageChecker;
use node_resolver::NodeResolution;
use node_resolver::NodeResolutionKind;
use node_resolver::NodeResolver;
use node_resolver::ResolutionMode;
use thiserror::Error;
use url::Url;

use crate::fs::DenoResolverFs;

pub use byonm::ByonmInNpmPackageChecker;
pub use byonm::ByonmNpmResolver;
pub use byonm::ByonmNpmResolverCreateOptions;
pub use byonm::ByonmResolvePkgFolderFromDenoReqError;
pub use local::normalize_pkg_name_for_node_modules_deno_folder;

mod byonm;
mod local;

#[derive(Debug, Error)]
#[error("Could not resolve \"{}\", but found it in a package.json. Deno expects the node_modules/ directory to be up to date. Did you forget to run `deno install`?", specifier)]
pub struct NodeModulesOutOfDateError {
  pub specifier: String,
}

#[derive(Debug, Error)]
#[error("Could not find '{}'. Deno expects the node_modules/ directory to be up to date. Did you forget to run `deno install`?", package_json_path.display())]
pub struct MissingPackageNodeModulesFolderError {
  pub package_json_path: PathBuf,
}

#[derive(Debug, Boxed)]
pub struct ResolveIfForNpmPackageError(
  pub Box<ResolveIfForNpmPackageErrorKind>,
);

#[derive(Debug, Error)]
pub enum ResolveIfForNpmPackageErrorKind {
  #[error(transparent)]
  NodeResolve(#[from] NodeResolveError),
  #[error(transparent)]
  NodeModulesOutOfDate(#[from] NodeModulesOutOfDateError),
}

#[derive(Debug, Boxed)]
pub struct ResolveReqWithSubPathError(pub Box<ResolveReqWithSubPathErrorKind>);

#[derive(Debug, Error)]
pub enum ResolveReqWithSubPathErrorKind {
  #[error(transparent)]
  MissingPackageNodeModulesFolder(#[from] MissingPackageNodeModulesFolderError),
  #[error(transparent)]
  ResolvePkgFolderFromDenoReq(#[from] ResolvePkgFolderFromDenoReqError),
  #[error(transparent)]
  PackageSubpathResolve(#[from] PackageSubpathResolveError),
}

#[derive(Debug, Error)]
pub enum ResolvePkgFolderFromDenoReqError {
  // todo(dsherret): don't use anyhow here
  #[error(transparent)]
  Managed(anyhow::Error),
  #[error(transparent)]
  Byonm(#[from] ByonmResolvePkgFolderFromDenoReqError),
}

// todo(dsherret): a temporary trait until we extract
// out the CLI npm resolver into here
pub trait CliNpmReqResolver: Debug + Send + Sync {
  fn resolve_pkg_folder_from_deno_module_req(
    &self,
    req: &PackageReq,
    referrer: &Url,
  ) -> Result<PathBuf, ResolvePkgFolderFromDenoReqError>;
}

pub struct NpmReqResolverOptions<
  Fs: DenoResolverFs,
  TNodeResolverEnv: NodeResolverEnv,
> {
  /// The resolver when "bring your own node_modules" is enabled where Deno
  /// does not setup the node_modules directories automatically, but instead
  /// uses what already exists on the file system.
  pub byonm_resolver: Option<Arc<ByonmNpmResolver<Fs, TNodeResolverEnv>>>,
  pub fs: Fs,
  pub in_npm_pkg_checker: Arc<dyn InNpmPackageChecker>,
  pub node_resolver: Arc<NodeResolver<TNodeResolverEnv>>,
  pub npm_req_resolver: Arc<dyn CliNpmReqResolver>,
}

#[derive(Debug)]
pub struct NpmReqResolver<Fs: DenoResolverFs, TNodeResolverEnv: NodeResolverEnv>
{
  byonm_resolver: Option<Arc<ByonmNpmResolver<Fs, TNodeResolverEnv>>>,
  fs: Fs,
  in_npm_pkg_checker: Arc<dyn InNpmPackageChecker>,
  node_resolver: Arc<NodeResolver<TNodeResolverEnv>>,
  npm_resolver: Arc<dyn CliNpmReqResolver>,
}

impl<Fs: DenoResolverFs, TNodeResolverEnv: NodeResolverEnv>
  NpmReqResolver<Fs, TNodeResolverEnv>
{
  pub fn new(options: NpmReqResolverOptions<Fs, TNodeResolverEnv>) -> Self {
    Self {
      byonm_resolver: options.byonm_resolver,
      fs: options.fs,
      in_npm_pkg_checker: options.in_npm_pkg_checker,
      node_resolver: options.node_resolver,
      npm_resolver: options.npm_req_resolver,
    }
  }

  pub fn resolve_req_reference(
    &self,
    req_ref: &NpmPackageReqReference,
    referrer: &Url,
    resolution_mode: ResolutionMode,
    resolution_kind: NodeResolutionKind,
  ) -> Result<Url, ResolveReqWithSubPathError> {
    self.resolve_req_with_sub_path(
      req_ref.req(),
      req_ref.sub_path(),
      referrer,
      resolution_mode,
      resolution_kind,
    )
  }

  pub fn resolve_req_with_sub_path(
    &self,
    req: &PackageReq,
    sub_path: Option<&str>,
    referrer: &Url,
    resolution_mode: ResolutionMode,
    resolution_kind: NodeResolutionKind,
  ) -> Result<Url, ResolveReqWithSubPathError> {
    let package_folder = self
      .npm_resolver
      .resolve_pkg_folder_from_deno_module_req(req, referrer)?;
    let resolution_result =
      self.node_resolver.resolve_package_subpath_from_deno_module(
        &package_folder,
        sub_path,
        Some(referrer),
        resolution_mode,
        resolution_kind,
      );
    match resolution_result {
      Ok(url) => Ok(url),
      Err(err) => {
        if self.byonm_resolver.is_some() {
          let package_json_path = package_folder.join("package.json");
          if !self.fs.exists_sync(&package_json_path) {
            return Err(
              MissingPackageNodeModulesFolderError { package_json_path }.into(),
            );
          }
        }
        Err(err.into())
      }
    }
  }

  pub fn resolve_if_for_npm_pkg(
    &self,
    specifier: &str,
    referrer: &Url,
    resolution_mode: ResolutionMode,
    resolution_kind: NodeResolutionKind,
  ) -> Result<Option<NodeResolution>, ResolveIfForNpmPackageError> {
    let resolution_result = self.node_resolver.resolve(
      specifier,
      referrer,
      resolution_mode,
      resolution_kind,
    );
    match resolution_result {
      Ok(res) => Ok(Some(res)),
      Err(err) => {
        let err = err.into_kind();
        match err {
          NodeResolveErrorKind::RelativeJoin(_)
          | NodeResolveErrorKind::PackageImportsResolve(_)
          | NodeResolveErrorKind::UnsupportedEsmUrlScheme(_)
          | NodeResolveErrorKind::DataUrlReferrer(_)
          | NodeResolveErrorKind::TypesNotFound(_)
          | NodeResolveErrorKind::FinalizeResolution(_) => Err(
            ResolveIfForNpmPackageErrorKind::NodeResolve(err.into()).into_box(),
          ),
          NodeResolveErrorKind::PackageResolve(err) => {
            let err = err.into_kind();
            match err {
              PackageResolveErrorKind::ClosestPkgJson(_)
              | PackageResolveErrorKind::InvalidModuleSpecifier(_)
              | PackageResolveErrorKind::ExportsResolve(_)
              | PackageResolveErrorKind::SubpathResolve(_) => Err(
                ResolveIfForNpmPackageErrorKind::NodeResolve(
                  NodeResolveErrorKind::PackageResolve(err.into()).into(),
                )
                .into_box(),
              ),
              PackageResolveErrorKind::PackageFolderResolve(err) => {
                match err.as_kind() {
                  PackageFolderResolveErrorKind::Io(
                    PackageFolderResolveIoError { package_name, .. },
                  )
                  | PackageFolderResolveErrorKind::PackageNotFound(
                    PackageNotFoundError { package_name, .. },
                  ) => {
                    if self.in_npm_pkg_checker.in_npm_package(referrer) {
                      return Err(
                        ResolveIfForNpmPackageErrorKind::NodeResolve(
                          NodeResolveErrorKind::PackageResolve(err.into())
                            .into(),
                        )
                        .into_box(),
                      );
                    }
                    if let Some(byonm_npm_resolver) = &self.byonm_resolver {
                      if byonm_npm_resolver
                        .find_ancestor_package_json_with_dep(
                          package_name,
                          referrer,
                        )
                        .is_some()
                      {
                        return Err(
                          ResolveIfForNpmPackageErrorKind::NodeModulesOutOfDate(
                            NodeModulesOutOfDateError {
                              specifier: specifier.to_string(),
                            },
                          ).into_box(),
                        );
                      }
                    }
                    Ok(None)
                  }
                  PackageFolderResolveErrorKind::ReferrerNotFound(_) => {
                    if self.in_npm_pkg_checker.in_npm_package(referrer) {
                      return Err(
                        ResolveIfForNpmPackageErrorKind::NodeResolve(
                          NodeResolveErrorKind::PackageResolve(err.into())
                            .into(),
                        )
                        .into_box(),
                      );
                    }
                    Ok(None)
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}