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

use std::borrow::Cow;

use boxed_error::Boxed;
use deno_ast::ModuleKind;
use deno_graph::JsModule;
use deno_graph::JsonModule;
use deno_graph::ModuleGraph;
use deno_graph::WasmModule;
use deno_media_type::MediaType;
use node_resolver::InNpmPackageChecker;
use node_resolver::errors::PackageJsonLoadError;
use url::Url;

use super::AllowJsonImports;
use super::DenoNpmModuleLoaderRc;
use super::LoadedModule;
use super::LoadedModuleOrAsset;
use super::LoadedModuleSource;
use super::NpmModuleLoadError;
use super::RequestedModuleType;
use crate::cache::ParsedSourceCacheRc;
use crate::cjs::CjsTrackerRc;
use crate::emit::EmitParsedSourceHelperError;
use crate::emit::EmitterRc;
use crate::factory::DenoNodeCodeTranslatorRc;
use crate::graph::EnhanceGraphErrorMode;
use crate::graph::EnhancedGraphError;
use crate::graph::enhance_graph_error;
use crate::npm::DenoInNpmPackageChecker;

#[allow(
  clippy::disallowed_types,
  reason = "source text is always stored as Arc<str>"
)]
type ArcStr = std::sync::Arc<str>;

#[derive(Debug, deno_error::JsError, Boxed)]
#[class(inherit)]
pub struct LoadPreparedModuleError(pub Box<LoadPreparedModuleErrorKind>);

#[derive(Debug, thiserror::Error, deno_error::JsError)]
pub enum LoadPreparedModuleErrorKind {
  #[class(inherit)]
  #[error(transparent)]
  Graph(#[from] EnhancedGraphError),
  #[class(inherit)]
  #[error(transparent)]
  ClosestPkgJson(#[from] PackageJsonLoadError),
  #[class(inherit)]
  #[error(transparent)]
  LoadMaybeCjs(#[from] LoadMaybeCjsError),
  #[class(inherit)]
  #[error(transparent)]
  Emit(#[from] EmitParsedSourceHelperError),
}

#[derive(Debug, thiserror::Error, deno_error::JsError)]
pub enum LoadMaybeCjsError {
  #[class(inherit)]
  #[error(transparent)]
  NpmModuleLoad(#[from] crate::emit::EmitParsedSourceHelperError),
  #[class(inherit)]
  #[error(transparent)]
  TranslateCjsToEsm(#[from] node_resolver::analyze::TranslateCjsToEsmError),
}

#[derive(Debug, deno_error::JsError, Boxed)]
#[class(inherit)]
pub struct LoadCodeSourceError(pub Box<LoadCodeSourceErrorKind>);

#[derive(Debug, thiserror::Error, deno_error::JsError)]
pub enum LoadCodeSourceErrorKind {
  #[class(inherit)]
  #[error(transparent)]
  LoadPreparedModule(#[from] LoadPreparedModuleError),
  #[class(inherit)]
  #[error(transparent)]
  LoadUnpreparedModule(#[from] LoadUnpreparedModuleError),
  #[class(generic)]
  #[error(
    "Attempted to load JSON module without specifying \"type\": \"json\" attribute in the import statement."
  )]
  MissingJsonAttribute,
  #[class(inherit)]
  #[error(transparent)]
  NpmModuleLoad(#[from] NpmModuleLoadError),
  #[class(inherit)]
  #[error(transparent)]
  PathToUrl(#[from] deno_path_util::PathToUrlError),
  #[class(inherit)]
  #[error(transparent)]
  UnsupportedScheme(#[from] UnsupportedSchemeError),
}

// this message list additional `npm` and `jsr` schemes, but they should actually be handled
// before these APIs are even hit.
#[derive(Debug, thiserror::Error, deno_error::JsError)]
#[class(type)]
#[error(
  "Unsupported scheme \"{}\" for module \"{}\". Supported schemes:\n - \"blob\"\n - \"data\"\n - \"file\"\n - \"http\"\n - \"https\"\n - \"jsr\"\n - \"npm\"", url.scheme(), url
)]
pub struct UnsupportedSchemeError {
  pub url: Url,
}

#[derive(Debug, thiserror::Error, deno_error::JsError)]
#[class(generic)]
#[error("Loading unprepared module: {}{}", .specifier, .maybe_referrer.as_ref().map(|r| format!(", imported from: {}", r)).unwrap_or_default())]
pub struct LoadUnpreparedModuleError {
  specifier: Url,
  maybe_referrer: Option<Url>,
}

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

#[sys_traits::auto_impl]
pub trait ModuleLoaderSys:
  super::NpmModuleLoaderSys
  + crate::emit::EmitterSys
  + node_resolver::analyze::NodeCodeTranslatorSys
  + crate::cjs::analyzer::DenoCjsCodeAnalyzerSys
  + crate::npm::NpmResolverSys
{
}

enum CodeOrDeferredEmit<'a> {
  Source(LoadedModule<'a>),
  DeferredEmit {
    specifier: &'a Url,
    media_type: MediaType,
    source: &'a ArcStr,
  },
  Cjs {
    specifier: &'a Url,
    media_type: MediaType,
    source: &'a ArcStr,
  },
  ExternalAsset {
    specifier: &'a Url,
  },
}

pub struct ModuleLoader<TSys: ModuleLoaderSys> {
  in_npm_pkg_checker: DenoInNpmPackageChecker,
  npm_module_loader: DenoNpmModuleLoaderRc<TSys>,
  prepared_module_loader: PreparedModuleLoader<TSys>,
  allow_json_imports: AllowJsonImports,
}

impl<TSys: ModuleLoaderSys> ModuleLoader<TSys> {
  #[allow(clippy::too_many_arguments, reason = "all arguments are needed")]
  pub fn new(
    cjs_tracker: CjsTrackerRc<DenoInNpmPackageChecker, TSys>,
    emitter: EmitterRc<DenoInNpmPackageChecker, TSys>,
    in_npm_pkg_checker: DenoInNpmPackageChecker,
    node_code_translator: DenoNodeCodeTranslatorRc<TSys>,
    npm_module_loader: DenoNpmModuleLoaderRc<TSys>,
    parsed_source_cache: ParsedSourceCacheRc,
    sys: TSys,
    allow_json_imports: AllowJsonImports,
  ) -> Self {
    Self {
      in_npm_pkg_checker,
      npm_module_loader,
      prepared_module_loader: PreparedModuleLoader {
        cjs_tracker,
        emitter,
        node_code_translator,
        parsed_source_cache,
        sys,
      },
      allow_json_imports,
    }
  }

  /// Loads a module using the graph or file system.
  ///
  /// Note that the referrer is only used to enhance error messages and
  /// doesn't need to be provided.
  pub async fn load<'a>(
    &self,
    graph: &'a ModuleGraph,
    specifier: &'a Url,
    // todo(#30074): we should remove passing the referrer in here and remove the
    // referrer from all error messages. This should be up to deno_core to display.
    maybe_referrer: Option<&Url>,
    requested_module_type: &RequestedModuleType<'_>,
  ) -> Result<LoadedModuleOrAsset<'a>, LoadCodeSourceError> {
    let source = match self
      .prepared_module_loader
      .load_prepared_module(graph, specifier, requested_module_type)
      .await
      .map_err(LoadCodeSourceError::from)?
    {
      Some(module_or_asset) => module_or_asset,
      None => {
        if !matches!(
          specifier.scheme(),
          "https" | "http" | "file" | "blob" | "data"
        ) {
          return Err(
            UnsupportedSchemeError {
              url: specifier.clone(),
            }
            .into(),
          );
        } else if self.in_npm_pkg_checker.in_npm_package(specifier) {
          let loaded_module = self
            .npm_module_loader
            .load(
              Cow::Borrowed(specifier),
              maybe_referrer,
              requested_module_type,
            )
            .await
            .map_err(LoadCodeSourceError::from)?;
          LoadedModuleOrAsset::Module(loaded_module)
        } else {
          match requested_module_type {
            RequestedModuleType::Text
            | RequestedModuleType::Bytes
            | RequestedModuleType::Other("css") => {
              LoadedModuleOrAsset::ExternalAsset {
                specifier: Cow::Borrowed(specifier),
                statically_analyzable: false,
              }
            }
            _ => {
              return Err(LoadCodeSourceError::from(
                LoadUnpreparedModuleError {
                  specifier: specifier.clone(),
                  maybe_referrer: maybe_referrer.cloned(),
                },
              ));
            }
          }
        }
      }
    };

    match &source {
      LoadedModuleOrAsset::Module(loaded_module) => {
        // If we loaded a JSON file, but the "requested_module_type" (that is computed from
        // import attributes) is not JSON we need to fail.
        if loaded_module.media_type == MediaType::Json
          && !matches!(requested_module_type, RequestedModuleType::Json)
          && matches!(self.allow_json_imports, AllowJsonImports::WithAttribute)
        {
          Err(LoadCodeSourceErrorKind::MissingJsonAttribute.into_box())
        } else {
          Ok(source)
        }
      }
      LoadedModuleOrAsset::ExternalAsset { .. } => {
        // these are never type: "json"

        Ok(source)
      }
    }
  }

  pub fn try_load_prepared_module_sync<'graph>(
    &self,
    graph: &'graph ModuleGraph,
    specifier: &Url,
    requested_module_type: &RequestedModuleType<'_>,
  ) -> Result<Option<LoadedModule<'graph>>, LoadCodeSourceError> {
    let Some(loaded_module) = self
      .prepared_module_loader
      .try_load_prepared_module_sync(graph, specifier, requested_module_type)
      .map_err(LoadCodeSourceError::from)?
    else {
      return Ok(None);
    };

    if loaded_module.media_type == MediaType::Json
      && !matches!(requested_module_type, RequestedModuleType::Json)
      && matches!(self.allow_json_imports, AllowJsonImports::WithAttribute)
    {
      return Err(LoadCodeSourceErrorKind::MissingJsonAttribute.into_box());
    }

    Ok(Some(loaded_module))
  }

  pub fn load_prepared_module_for_source_map_sync<'graph>(
    &self,
    graph: &'graph ModuleGraph,
    specifier: &Url,
  ) -> Result<Option<LoadedModule<'graph>>, anyhow::Error> {
    self
      .prepared_module_loader
      .load_prepared_module_for_source_map_sync(graph, specifier)
  }
}

struct PreparedModuleLoader<TSys: ModuleLoaderSys> {
  cjs_tracker: CjsTrackerRc<DenoInNpmPackageChecker, TSys>,
  emitter: EmitterRc<DenoInNpmPackageChecker, TSys>,
  node_code_translator: DenoNodeCodeTranslatorRc<TSys>,
  parsed_source_cache: ParsedSourceCacheRc,
  sys: TSys,
}

impl<TSys: ModuleLoaderSys> PreparedModuleLoader<TSys> {
  pub async fn load_prepared_module<'graph>(
    &self,
    graph: &'graph ModuleGraph,
    specifier: &Url,
    requested_module_type: &RequestedModuleType<'_>,
  ) -> Result<Option<LoadedModuleOrAsset<'graph>>, LoadPreparedModuleError> {
    // Note: keep this in sync with the sync version below
    match self.load_prepared_module_or_defer_emit(
      graph,
      specifier,
      requested_module_type,
    )? {
      Some(CodeOrDeferredEmit::Source(source)) => {
        Ok(Some(LoadedModuleOrAsset::Module(source)))
      }
      Some(CodeOrDeferredEmit::DeferredEmit {
        specifier,
        media_type,
        source,
      }) => {
        let transpile_result = self
          .emitter
          .maybe_emit_source(specifier, media_type, ModuleKind::Esm, source)
          .await?;

        // at this point, we no longer need the parsed source in memory, so free it
        self.parsed_source_cache.free(specifier);

        Ok(Some(LoadedModuleOrAsset::Module(LoadedModule {
          // note: it's faster to provide a string to v8 if we know it's a string
          source: LoadedModuleSource::ArcStr(transpile_result),
          specifier: Cow::Borrowed(specifier),
          media_type,
        })))
      }
      Some(CodeOrDeferredEmit::Cjs {
        specifier,
        media_type,
        source,
      }) => self
        .load_maybe_cjs(specifier, media_type, source)
        .await
        .map(|text| {
          Some(LoadedModuleOrAsset::Module(LoadedModule {
            specifier: Cow::Borrowed(specifier),
            media_type,
            source: LoadedModuleSource::ArcStr(text),
          }))
        })
        .map_err(|e| LoadPreparedModuleErrorKind::LoadMaybeCjs(e).into_box()),
      Some(CodeOrDeferredEmit::ExternalAsset { specifier }) => {
        Ok(Some(LoadedModuleOrAsset::ExternalAsset {
          specifier: Cow::Borrowed(specifier),
          // came from graph, so yes
          statically_analyzable: true,
        }))
      }
      None => Ok(None),
    }
  }

  pub fn try_load_prepared_module_sync<'graph>(
    &self,
    graph: &'graph ModuleGraph,
    specifier: &Url,
    requested_module_type: &RequestedModuleType<'_>,
  ) -> Result<Option<LoadedModule<'graph>>, LoadPreparedModuleError> {
    // Note: keep this in sync with the async version above
    match self.load_prepared_module_or_defer_emit(
      graph,
      specifier,
      requested_module_type,
    )? {
      Some(CodeOrDeferredEmit::Source(source)) => Ok(Some(source)),
      Some(CodeOrDeferredEmit::DeferredEmit {
        specifier,
        media_type,
        source,
      }) => {
        let transpile_result = self.emitter.maybe_emit_source_sync(
          specifier,
          media_type,
          ModuleKind::Esm,
          source,
        )?;

        // at this point, we no longer need the parsed source in memory, so free it
        self.parsed_source_cache.free(specifier);

        Ok(Some(LoadedModule {
          // note: it's faster to provide a string if we know it's a string
          source: LoadedModuleSource::ArcStr(transpile_result),
          specifier: Cow::Borrowed(specifier),
          media_type,
        }))
      }
      Some(CodeOrDeferredEmit::Cjs { .. })
      | Some(CodeOrDeferredEmit::ExternalAsset { .. })
      | None => Ok(None),
    }
  }

  pub fn load_prepared_module_for_source_map_sync<'graph>(
    &self,
    graph: &'graph ModuleGraph,
    specifier: &Url,
  ) -> Result<Option<LoadedModule<'graph>>, anyhow::Error> {
    // Note: keep this in sync with the async version above
    match self.load_prepared_module_or_defer_emit(
      graph,
      specifier,
      &RequestedModuleType::None,
    )? {
      Some(CodeOrDeferredEmit::Source(code_source)) => Ok(Some(code_source)),
      Some(CodeOrDeferredEmit::DeferredEmit {
        specifier,
        media_type,
        source,
      }) => {
        let transpile_result = self.emitter.maybe_emit_source_sync(
          specifier,
          media_type,
          ModuleKind::Esm,
          source,
        )?;

        // at this point, we no longer need the parsed source in memory, so free it
        self.parsed_source_cache.free(specifier);

        Ok(Some(LoadedModule {
          // note: it's faster to provide a string if we know it's a string
          source: LoadedModuleSource::ArcStr(transpile_result),
          specifier: Cow::Borrowed(specifier),
          media_type,
        }))
      }
      Some(CodeOrDeferredEmit::Cjs {
        specifier,
        media_type,
        source,
      }) => {
        let transpile_result = self.emitter.maybe_emit_source_sync(
          specifier,
          media_type,
          ModuleKind::Cjs,
          source,
        )?;

        self.parsed_source_cache.free(specifier);

        Ok(Some(LoadedModule {
          source: LoadedModuleSource::ArcStr(transpile_result),
          specifier: Cow::Borrowed(specifier),
          media_type,
        }))
      }
      Some(CodeOrDeferredEmit::ExternalAsset { .. }) | None => Ok(None),
    }
  }

  fn load_prepared_module_or_defer_emit<'graph>(
    &self,
    graph: &'graph ModuleGraph,
    specifier: &Url,
    requested_module_type: &RequestedModuleType,
  ) -> Result<Option<CodeOrDeferredEmit<'graph>>, LoadPreparedModuleError> {
    let maybe_module = graph.try_get(specifier).map_err(|err| {
      enhance_graph_error(
        &self.sys,
        deno_graph::ModuleGraphError::ModuleError(err.clone()),
        EnhanceGraphErrorMode::ShowRange,
        // This is the post-build module-load path; bare-specifier resolution
        // hints don't apply here.
        &[],
      )
    })?;

    match maybe_module {
      Some(deno_graph::Module::Json(JsonModule {
        source,
        media_type,
        specifier,
        ..
      })) => match requested_module_type {
        RequestedModuleType::Bytes => match source.try_get_original_bytes() {
          Some(bytes) => Ok(Some(CodeOrDeferredEmit::Source(LoadedModule {
            source: LoadedModuleSource::ArcBytes(bytes),
            specifier: Cow::Borrowed(specifier),
            media_type: *media_type,
          }))),
          None => Ok(Some(CodeOrDeferredEmit::ExternalAsset { specifier })),
        },
        RequestedModuleType::Text | RequestedModuleType::Other("css") => {
          Ok(Some(CodeOrDeferredEmit::Source(LoadedModule {
            source: LoadedModuleSource::ArcStr(source.text.clone()),
            specifier: Cow::Borrowed(specifier),
            media_type: *media_type,
          })))
        }
        _ => Ok(Some(CodeOrDeferredEmit::Source(LoadedModule {
          source: LoadedModuleSource::ArcStr(source.text.clone()),
          specifier: Cow::Borrowed(specifier),
          media_type: *media_type,
        }))),
      },
      Some(deno_graph::Module::Js(JsModule {
        source,
        media_type,
        specifier,
        is_script,
        ..
      })) => match requested_module_type {
        RequestedModuleType::Bytes => match source.try_get_original_bytes() {
          Some(bytes) => Ok(Some(CodeOrDeferredEmit::Source(LoadedModule {
            source: LoadedModuleSource::ArcBytes(bytes),
            specifier: Cow::Borrowed(specifier),
            media_type: *media_type,
          }))),
          None => Ok(Some(CodeOrDeferredEmit::ExternalAsset { specifier })),
        },
        RequestedModuleType::Text | RequestedModuleType::Other("css") => {
          Ok(Some(CodeOrDeferredEmit::Source(LoadedModule {
            source: LoadedModuleSource::ArcStr(source.text.clone()),
            specifier: Cow::Borrowed(specifier),
            media_type: *media_type,
          })))
        }
        _ => {
          if self.cjs_tracker.is_cjs_with_known_is_script(
            specifier,
            *media_type,
            *is_script,
          )? {
            return Ok(Some(CodeOrDeferredEmit::Cjs {
              specifier,
              media_type: *media_type,
              source: &source.text,
            }));
          }
          let code = match media_type {
            MediaType::JavaScript
            | MediaType::Unknown
            | MediaType::Mjs
            | MediaType::Json => source.text.clone(),
            MediaType::Dts | MediaType::Dcts | MediaType::Dmts => {
              Default::default()
            }
            MediaType::Cjs | MediaType::Cts => {
              return Ok(Some(CodeOrDeferredEmit::Cjs {
                specifier,
                media_type: *media_type,
                source: &source.text,
              }));
            }
            MediaType::TypeScript
            | MediaType::Mts
            | MediaType::Jsx
            | MediaType::Tsx => {
              return Ok(Some(CodeOrDeferredEmit::DeferredEmit {
                specifier,
                media_type: *media_type,
                source: &source.text,
              }));
            }
            MediaType::Css
            | MediaType::Html
            | MediaType::Jsonc
            | MediaType::Json5
            | MediaType::Markdown
            | MediaType::Sql
            | MediaType::Wasm
            | MediaType::SourceMap => {
              panic!("Unexpected media type {media_type} for {specifier}")
            }
          };

          // at this point, we no longer need the parsed source in memory, so free it
          self.parsed_source_cache.free(specifier);

          Ok(Some(CodeOrDeferredEmit::Source(LoadedModule {
            source: LoadedModuleSource::ArcStr(code),
            specifier: Cow::Borrowed(specifier),
            media_type: *media_type,
          })))
        }
      },
      Some(deno_graph::Module::Wasm(WasmModule {
        source, specifier, ..
      })) => Ok(Some(CodeOrDeferredEmit::Source(LoadedModule {
        source: LoadedModuleSource::ArcBytes(source.clone()),
        specifier: Cow::Borrowed(specifier),
        media_type: MediaType::Wasm,
      }))),
      Some(deno_graph::Module::External(module)) => {
        if module.specifier.as_str().contains("/node_modules/") {
          return Ok(None);
        }
        Ok(Some(CodeOrDeferredEmit::ExternalAsset {
          specifier: &module.specifier,
        }))
      }
      Some(deno_graph::Module::Node(_) | deno_graph::Module::Npm(_)) | None => {
        Ok(None)
      }
    }
  }

  async fn load_maybe_cjs(
    &self,
    specifier: &Url,
    media_type: MediaType,
    original_source: &ArcStr,
  ) -> Result<ArcStr, LoadMaybeCjsError> {
    let js_source = self
      .emitter
      .maybe_emit_source(
        specifier,
        media_type,
        ModuleKind::Cjs,
        original_source,
      )
      .await?;
    let text = self
      .node_code_translator
      .translate_cjs_to_esm(specifier, Some(Cow::Borrowed(js_source.as_ref())))
      .await?;
    // Apply load-time security mitigations for known React Server Components
    // CVEs to the translated source. Opt in via `DENO_PATCH_REACT_CVE`.
    let text = if crate::is_react_cve_patch_enabled(&self.sys) {
      crate::patch_react_cves(specifier.as_str(), text)
    } else {
      text
    };
    // at this point, we no longer need the parsed source in memory, so free it
    self.parsed_source_cache.free(specifier);
    Ok(match text {
      // perf: if the text is borrowed, that means it didn't make any changes
      // to the original source, so we can just provide that instead of cloning
      // the borrowed text
      Cow::Borrowed(_) => js_source.clone(),
      Cow::Owned(text) => text.into(),
    })
  }
}
