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

use std::cell::RefCell;
use std::collections::HashSet;
use std::collections::VecDeque;
use std::future::Future;
use std::pin::Pin;
use std::rc::Rc;
use std::task::Context;
use std::task::Poll;

use futures::future::FutureExt;
use futures::stream::FuturesUnordered;
use futures::stream::Stream;
use futures::stream::StreamExt;
use futures::stream::TryStreamExt;

use super::loaders::ModuleLoadOptions;
use crate::ModuleLoadResponse;
use crate::ModuleLoader;
use crate::ModuleSource;
use crate::ModuleSourceCode;
use crate::error::CoreError;
use crate::module_specifier::ModuleSpecifier;
use crate::modules::ModuleError;
use crate::modules::ModuleId;
use crate::modules::ModuleImportPhase;
use crate::modules::ModuleLoadId;
use crate::modules::ModuleLoaderError;
use crate::modules::ModuleReference;
use crate::modules::ModuleRequest;
use crate::modules::RequestedModuleType;
use crate::modules::ResolutionKind;
use crate::modules::loaders::ModuleLoadReferrer;
use crate::modules::map::ModuleMap;
use crate::modules::map::NewModuleResult;
use crate::modules::module_map_data::ModuleSourceKind;
use crate::source_map::SourceMapApplication;
use crate::source_map::SourceMapper;

type ModuleLoadFuture = dyn Future<
  Output = Result<Option<(ModuleRequest, ModuleSource)>, ModuleLoaderError>,
>;

#[derive(Debug, Copy, Clone)]
pub enum SideModuleKind {
  Async,
  Sync,
}

/// Describes the entrypoint of a recursive module load.
#[derive(Debug)]
enum LoadInit {
  /// Main module specifier.
  Main(String),
  /// Module specifier for side module.
  Side {
    specifier: String,
    kind: SideModuleKind,
    code: Option<String>,
  },
  /// Dynamic import specifier with referrer and expected
  /// module type (which is determined by import assertion).
  DynamicImport(String, String, RequestedModuleType, ModuleImportPhase),
}

#[derive(Debug, Eq, PartialEq)]
enum LoadState {
  Init,
  LoadingRoot,
  LoadingImports,
  Done,
}

/// This future is used to implement parallel async module loading.
pub(crate) struct RecursiveModuleLoad {
  id: ModuleLoadId,
  root_module_id: Option<ModuleId>,
  init: LoadInit,
  state: LoadState,
  pub(crate) module_map_rc: Rc<ModuleMap>,
  pending: FuturesUnordered<Pin<Box<ModuleLoadFuture>>>,
  visited: HashSet<ModuleReference>,
  visited_as_alias: Rc<RefCell<HashSet<String>>>,
  root_module_reference: Option<ModuleReference>,
  resolved_specifier: Option<Result<ModuleSpecifier, CoreError>>,
  // The loader is copied from `module_map_rc`, but its reference is cloned
  // ahead of time to avoid already-borrowed errors.
  loader: Rc<dyn ModuleLoader>,
}

impl Drop for RecursiveModuleLoad {
  fn drop(&mut self) {
    self.loader.finish_load();
  }
}

impl RecursiveModuleLoad {
  /// Starts a new asynchronous load of the module graph for given specifier.
  ///
  /// The module corresponding for the given `specifier` will be marked as
  // "the main module" (`import.meta.main` will return `true` for this module).
  pub(crate) async fn main(
    specifier: String,
    module_map_rc: Rc<ModuleMap>,
  ) -> Result<Self, CoreError> {
    let mut load = Self::new(LoadInit::Main(specifier), module_map_rc);
    load.prepare().await?;
    Ok(load)
  }

  /// Starts a new asynchronous load of the module graph for given specifier.
  pub(crate) async fn side(
    specifier: String,
    module_map_rc: Rc<ModuleMap>,
    kind: SideModuleKind,
    code: Option<String>,
  ) -> Result<Self, CoreError> {
    let mut load = Self::new(
      LoadInit::Side {
        specifier,
        kind,
        code,
      },
      module_map_rc,
    );
    load.prepare().await?;
    Ok(load)
  }

  pub(crate) fn new_dynamic_import(
    specifier: String,
    referrer: String,
    requested_module_type: RequestedModuleType,
    phase: ModuleImportPhase,
    module_map_rc: Rc<ModuleMap>,
    resolved_specifier: Result<ModuleSpecifier, ModuleLoaderError>,
  ) -> Self {
    Self::new_with_resolved_specifier(
      LoadInit::DynamicImport(
        specifier,
        referrer,
        requested_module_type,
        phase,
      ),
      module_map_rc,
      resolved_specifier,
    )
  }

  fn new(init: LoadInit, module_map_rc: Rc<ModuleMap>) -> Self {
    let resolve_response = Self::resolve_root_from_init(&init, &module_map_rc);
    Self::new_with_resolved_specifier(init, module_map_rc, resolve_response)
  }

  fn new_with_resolved_specifier(
    init: LoadInit,
    module_map_rc: Rc<ModuleMap>,
    resolve_response: Result<ModuleSpecifier, ModuleLoaderError>,
  ) -> Self {
    let id = module_map_rc.next_load_id();
    let loader = module_map_rc.loader.borrow().clone();
    let resolved_specifier = Some(resolve_response.map_err(CoreError::from));
    let root_module_id = resolved_specifier
      .as_ref()
      .and_then(|r| r.as_ref().ok())
      .and_then(|spec| {
        module_map_rc
          .get_id(spec.as_str(), Self::requested_module_type_from_init(&init))
      });
    Self {
      id,
      root_module_id,
      init,
      state: LoadState::Init,
      module_map_rc: module_map_rc.clone(),
      loader,
      pending: FuturesUnordered::new(),
      visited: HashSet::new(),
      visited_as_alias: Default::default(),
      root_module_reference: None,
      resolved_specifier,
    }
  }

  pub(crate) fn id(&self) -> ModuleLoadId {
    self.id
  }

  pub(crate) fn root_module_id(&self) -> Option<ModuleId> {
    self.root_module_id
  }

  pub(crate) fn root_module_reference(&self) -> Option<&ModuleReference> {
    self.root_module_reference.as_ref()
  }

  fn requested_module_type_from_init(init: &LoadInit) -> &RequestedModuleType {
    match init {
      LoadInit::DynamicImport(_, _, module_type, _) => module_type,
      _ => &RequestedModuleType::None,
    }
  }

  fn resolve_root_from_init(
    init: &LoadInit,
    module_map_rc: &ModuleMap,
  ) -> Result<ModuleSpecifier, ModuleLoaderError> {
    match init {
      LoadInit::Main(specifier) => {
        module_map_rc.resolve(specifier, ".", ResolutionKind::MainModule)
      }
      LoadInit::Side { specifier, .. } => {
        module_map_rc.resolve(specifier, ".", ResolutionKind::Import)
      }
      LoadInit::DynamicImport(specifier, referrer, _, _) => module_map_rc
        .resolve(specifier, referrer, ResolutionKind::DynamicImport),
    }
  }

  pub(crate) async fn prepare(&mut self) -> Result<(), CoreError> {
    let module_specifier = match self.resolved_specifier.take() {
      Some(Ok(spec)) => {
        // Put it back for poll_next to use later.
        self.resolved_specifier = Some(Ok(spec.clone()));
        spec
      }
      Some(Err(err)) => return Err(err),
      None => {
        unreachable!("root specifier is resolved in RecursiveModuleLoad::new")
      }
    };
    let (maybe_referrer, maybe_code, requested_module_type, is_synchronous) =
      match &mut self.init {
        LoadInit::Main(_) => (None, None, RequestedModuleType::None, false),
        LoadInit::Side { kind, code, .. } => (
          None,
          code.take(),
          RequestedModuleType::None,
          match kind {
            SideModuleKind::Async => false,
            SideModuleKind::Sync => true,
          },
        ),
        LoadInit::DynamicImport(_, referrer, requested_module_type, _) => (
          Some(referrer.to_string()),
          None,
          requested_module_type.clone(),
          false,
        ),
      };

    // Skip prepare_load for lazy-loaded ESM sources -- they are embedded
    // in the binary and do not need fetching/preparation. Same goes for
    // `synthetic_esm` modules: their exports come from a backing polyfill
    // script, not an external fetch.
    if self
      .module_map_rc
      .has_lazy_esm_source(module_specifier.as_str())
      || self
        .module_map_rc
        .has_synthetic_esm_module(module_specifier.as_str())
        && !self
          .loader
          .should_load_synthetic_esm(module_specifier.as_str())
    {
      return Ok(());
    }

    self
      .loader
      .prepare_load(
        &module_specifier,
        maybe_referrer,
        maybe_code,
        ModuleLoadOptions {
          is_synchronous,
          is_dynamic_import: self.is_dynamic_import(),
          requested_module_type,
        },
      )
      .await
      .map_err(|e| e.into())
  }

  fn is_currently_loading_main_module(&self) -> bool {
    !self.is_dynamic_import()
      && matches!(self.init, LoadInit::Main(..))
      && self.state == LoadState::LoadingRoot
  }

  fn is_dynamic_import(&self) -> bool {
    matches!(self.init, LoadInit::DynamicImport(..))
  }

  fn is_synchronous(&self) -> bool {
    matches!(
      self.init,
      LoadInit::Side {
        kind: SideModuleKind::Sync,
        ..
      }
    )
  }

  pub(crate) fn register_and_recurse(
    &mut self,
    scope: &mut v8::PinScope,
    module_request: &ModuleRequest,
    module_source: ModuleSource,
  ) -> Result<RegisterOutcome, ModuleError> {
    self.register_and_recurse_impl(scope, module_request, module_source)
  }

  fn register_and_recurse_impl(
    &mut self,
    scope: &mut v8::PinScope,
    module_request: &ModuleRequest,
    mut module_source: ModuleSource,
  ) -> Result<RegisterOutcome, ModuleError> {
    // Synthetic_esm specifiers carry a sentinel `ModuleSource` from
    // the load step. Divert to building the synthetic module from the
    // backing script's cached exports instead of going through
    // `new_module`.
    if self
      .module_map_rc
      .has_synthetic_esm_module(module_request.reference.specifier.as_str())
    {
      let module_id = self
        .module_map_rc
        .build_synthetic_esm_module(
          scope,
          module_request.reference.specifier.as_str(),
        )
        .map_err(|e| {
          let exception = e.to_v8_error(scope);
          ModuleError::Exception(v8::Global::new(scope, exception))
        })?;
      self.finalize_module(module_id, &module_request.reference, None);
      return Ok(RegisterOutcome::Done);
    }

    if let Some(source_kind) =
      ModuleSourceKind::from_module_type(&module_source.module_type)
    {
      match source_kind {
        ModuleSourceKind::Wasm => {
          module_source = self.module_map_rc.new_wasm_module_source(
            scope,
            &module_request.reference,
            module_source,
          )?;
        }
      }
      if module_request.phase == ModuleImportPhase::Source {
        if self.pending.is_empty() {
          self.state = LoadState::Done;
        }
        return Ok(RegisterOutcome::Done);
      }
    } else if module_request.phase == ModuleImportPhase::Source {
      let message = v8::String::new(
        scope,
        &format!(
          "Source phase imports are not supported for {} modules",
          &module_source.module_type
        ),
      )
      .unwrap();
      let exception = v8::Exception::reference_error(scope, message);
      return Err(ModuleError::Exception(v8::Global::new(scope, exception)));
    }

    let code = module_source.cheap_copy_code();

    match self.module_map_rc.new_module_with_pending(
      scope,
      self.is_currently_loading_main_module(),
      self.is_dynamic_import(),
      module_source,
    )? {
      NewModuleResult::Ready(module_id) => {
        // If the import statement used a custom `type` attribute that
        // doesn't correspond to a builtin module type
        // (e.g. `with { type: "x-css" }`) and the loaded module was
        // registered under a different requested type (e.g. JavaScript ->
        // `RequestedModuleType::None` for a `module.registerHooks()` load
        // hook that returned `format: "module"`), also register the module
        // under the import's requested type so subsequent lookups for the
        // same `(specifier, attribute)` pair resolve to it.
        let imported_type = &module_request.reference.requested_module_type;
        if matches!(imported_type, RequestedModuleType::Other(_))
          && self
            .module_map_rc
            .get_id(module_request.reference.specifier.as_str(), imported_type)
            != Some(module_id)
        {
          self.module_map_rc.register_under_type(
            module_request.reference.specifier.clone().into(),
            imported_type,
            module_id,
          );
        }
        self.finalize_module(module_id, &module_request.reference, Some(&code));
        Ok(RegisterOutcome::Done)
      }
    }
  }

  fn finalize_module(
    &mut self,
    module_id: ModuleId,
    reference: &ModuleReference,
    code: Option<&ModuleSourceCode>,
  ) {
    self.register_and_recurse_inner(module_id, reference, code);
    if self.state == LoadState::LoadingRoot {
      self.root_module_id = Some(module_id);
      self.state = LoadState::LoadingImports;
    }
    if self.pending.is_empty() {
      self.state = LoadState::Done;
    }
  }

  fn register_and_recurse_inner(
    &mut self,
    module_id: usize,
    module_reference: &ModuleReference,
    code: Option<&ModuleSourceCode>,
  ) {
    // Recurse the module's imports. There are two cases for each import:
    // 1. If the module is not in the module map, start a new load for it in
    //    `self.pending`. The result of that load should eventually be passed to
    //    this function for recursion.
    // 2. If the module is already in the module map, queue it up to be
    //    recursed synchronously here.
    // This robustly ensures that the whole graph is in the module map before
    // `LoadState::Done` is set.
    let mut already_registered = VecDeque::new();
    already_registered.push_back((module_id, module_reference.clone()));
    self.visited.insert(module_reference.clone());
    while let Some((module_id, module_reference)) =
      already_registered.pop_front()
    {
      // Use the module's canonical (post-redirect) URL as the referrer for
      // its child imports. After `new_module` aliases e.g. `npm:foo@^1` to
      // its `file:///.../node_modules/...` location, V8 records the module
      // under the file URL, and Deno's resolver requires that file URL to
      // resolve bare specifiers via the package's `package.json`. Using the
      // original specifier (e.g. `npm:foo@^1`) here strands intra-package
      // imports without resolution context.
      let canonical_specifier = self
        .module_map_rc
        .get_name_by_id(module_id)
        .and_then(|n| ModuleSpecifier::parse(&n).ok())
        .unwrap_or_else(|| module_reference.specifier.clone());
      let referrer = &canonical_specifier;
      let imports = self
        .module_map_rc
        .get_requested_modules(module_id)
        .unwrap()
        .clone();
      for module_request in imports {
        if !self.visited.contains(&module_request.reference)
          && !self
            .visited_as_alias
            .borrow()
            .contains(module_request.reference.specifier.as_str())
        {
          match self.module_map_rc.get_id(
            module_request.reference.specifier.as_str(),
            &module_request.reference.requested_module_type,
          ) {
            Some(module_id) => {
              already_registered
                .push_back((module_id, module_request.reference.clone()));
            }
            _ => {
              let request = module_request.clone();
              let visited_as_alias = self.visited_as_alias.clone();
              let referrer_info = code.and_then(|code| {
                let source_offset = request.referrer_source_offset?;
                source_mapped_module_load_referrer(
                  &self.module_map_rc.source_mapper,
                  referrer,
                  code,
                  source_offset,
                )
              });
              let loader = self.loader.clone();
              let is_dynamic_import = self.is_dynamic_import();
              let is_synchronous = self.is_synchronous();
              let requested_module_type =
                request.reference.requested_module_type.clone();
              let module_map_rc = self.module_map_rc.clone();
              let fut = async move {
                // `visited_as_alias` unlike `visited` is checked as late as
                // possible because it can only be populated after completed
                // loads, meaning a duplicate load future may have already been
                // dispatched before we know it's a duplicate.
                if visited_as_alias
                  .borrow()
                  .contains(request.reference.specifier.as_str())
                {
                  return Ok(None);
                }

                // Imports are pre-resolved in `new_module_from_js_source` --
                // `request.reference.specifier` already holds the final URL.
                let resolved_specifier = request.reference.specifier.clone();

                // First check if this module is a lazy-loaded ESM source
                // (embedded in binary but not snapshotted).
                let load_result = if let Some(source_code) = module_map_rc
                  .take_lazy_esm_source(resolved_specifier.as_str())
                {
                  Ok(ModuleSource::new(
                    crate::ModuleType::JavaScript,
                    ModuleSourceCode::String(source_code),
                    &resolved_specifier,
                    None,
                  ))
                } else if module_map_rc
                  .has_synthetic_esm_module(resolved_specifier.as_str())
                  && !loader
                    .should_load_synthetic_esm(resolved_specifier.as_str())
                {
                  // Sentinel source. `register_and_recurse` detects the
                  // synthetic_esm specifier and constructs the module from
                  // its backing script's cached exports object instead of
                  // calling `new_module` with this source.
                  Ok(ModuleSource::new(
                    crate::ModuleType::JavaScript,
                    ModuleSourceCode::String("".to_string().into()),
                    &resolved_specifier,
                    None,
                  ))
                } else {
                  let load_response = loader.load(
                    &resolved_specifier,
                    referrer_info.as_ref(),
                    ModuleLoadOptions {
                      is_dynamic_import,
                      is_synchronous,
                      requested_module_type,
                    },
                  );
                  match load_response {
                    ModuleLoadResponse::Sync(result) => result,
                    ModuleLoadResponse::Async(fut) => fut.await,
                  }
                };
                if let Ok(source) = &load_result
                  && let Some(found_specifier) = &source.module_url_found
                {
                  visited_as_alias
                    .borrow_mut()
                    .insert(found_specifier.as_str().to_string());
                }
                load_result.map(|s| Some((request, s)))
              };
              self.pending.push(fut.boxed_local());
            }
          }
          self.visited.insert(module_request.reference);
        }
      }
    }
  }

  /// Drives the module load stream to completion, calling `on_module` for each
  /// loaded module. Returns the root module ID once the full graph is loaded.
  pub(crate) async fn run_to_completion(
    mut self,
    mut step: impl FnMut(
      &mut Self,
      RegisterStep<'_>,
    ) -> Result<RegisterOutcome, CoreError>,
  ) -> Result<ModuleId, CoreError> {
    while let Some(load_result) = self.next().await {
      let (request, source) = load_result?;
      match step(
        &mut self,
        RegisterStep::Register {
          request: &request,
          source,
        },
      )? {
        RegisterOutcome::Done => {}
      }
    }
    Ok(self.root_module_id.expect("Root module should be loaded"))
  }
}

/// Action requested by [`RecursiveModuleLoad::run_to_completion`] of its
/// driver. A single `FnMut` closure handles both — passing one closure rather
/// than two side-steps the borrow checker (both phases need `&mut isolate`,
/// but they're never active simultaneously).
pub(crate) enum RegisterStep<'a> {
  /// Compile a freshly-loaded module's source, resolve its imports, and
  /// register it. Returns [`RegisterOutcome`].
  Register {
    request: &'a ModuleRequest,
    source: ModuleSource,
  },
}

/// Outcome of a single `register_and_recurse` call.
pub(crate) enum RegisterOutcome {
  /// Module was registered synchronously; no further work for this entry.
  Done,
}

impl RecursiveModuleLoad {
  /// Shared logic for Init once the root specifier has been resolved.
  fn init_with_resolved_root(
    inner: &mut Self,
    module_specifier: ModuleSpecifier,
    cx: &mut Context,
  ) -> Poll<Option<Result<(ModuleRequest, ModuleSource), CoreError>>> {
    let (requested_module_type, phase) = match &inner.init {
      LoadInit::DynamicImport(_, _, module_type, phase) => {
        (module_type.clone(), *phase)
      }
      _ => (RequestedModuleType::None, ModuleImportPhase::Evaluation),
    };
    let module_request = ModuleRequest {
      reference: ModuleReference {
        specifier: module_specifier.clone(),
        requested_module_type: requested_module_type.clone(),
      },
      specifier_key: None,
      referrer_source_offset: None,
      phase,
    };
    inner.root_module_reference = Some(module_request.reference.clone());
    let load_fut = if phase == ModuleImportPhase::Evaluation
      && let Some(module_id) = inner.root_module_id
    {
      // If the inner future is already in the map, we might be done (assuming there are no pending
      // loads).
      inner.register_and_recurse_inner(
        module_id,
        &module_request.reference,
        None,
      );
      if inner.pending.is_empty() {
        inner.state = LoadState::Done;
      } else {
        inner.state = LoadState::LoadingImports;
      }
      // Internally re-poll using the new state to avoid spinning the event loop again.
      return Pin::new(inner).poll_next(cx);
    } else if inner
      .module_map_rc
      .has_synthetic_esm_module(module_specifier.as_str())
      && !inner
        .loader
        .should_load_synthetic_esm(module_specifier.as_str())
    {
      let module_request = module_request.clone();
      let module_specifier = module_specifier.clone();
      async move {
        Ok(Some((
          module_request,
          ModuleSource::new(
            crate::ModuleType::JavaScript,
            ModuleSourceCode::String("".to_string().into()),
            &module_specifier,
            None,
          ),
        )))
      }
      .boxed_local()
    } else {
      let loader = inner.loader.clone();
      let is_dynamic_import = inner.is_dynamic_import();
      let is_synchronous = inner.is_synchronous();
      let requested_module_type = requested_module_type.clone();
      async move {
        let load_response = loader.load(
          &module_specifier,
          None,
          ModuleLoadOptions {
            is_dynamic_import,
            is_synchronous,
            requested_module_type,
          },
        );
        let result = match load_response {
          ModuleLoadResponse::Sync(result) => result,
          ModuleLoadResponse::Async(fut) => fut.await,
        };
        result.map(|s| Some((module_request, s)))
      }
      .boxed_local()
    };
    inner.pending.push(load_fut);
    inner.state = LoadState::LoadingRoot;
    inner.try_poll_next_unpin(cx)
  }
}

impl Stream for RecursiveModuleLoad {
  type Item = Result<(ModuleRequest, ModuleSource), CoreError>;

  fn poll_next(
    self: Pin<&mut Self>,
    cx: &mut Context,
  ) -> Poll<Option<Self::Item>> {
    let inner = self.get_mut();
    // IMPORTANT: Do not borrow `inner.module_map_rc` here. It may not be
    // available.
    match inner.state {
      LoadState::Init => {
        let module_specifier = match inner.resolved_specifier.take() {
          Some(Ok(spec)) => spec,
          Some(Err(error)) => {
            return Poll::Ready(Some(Err(error)));
          }
          None => unreachable!("root specifier is resolved before polling"),
        };
        Self::init_with_resolved_root(inner, module_specifier, cx)
      }
      LoadState::LoadingRoot | LoadState::LoadingImports => {
        // Poll the futures that load the source code of the modules
        match inner.pending.try_poll_next_unpin(cx)? {
          Poll::Ready(None) => unreachable!(),
          Poll::Ready(Some(None)) => {
            // The future resolves to None when loading an already visited redirect
            if inner.pending.is_empty() {
              inner.state = LoadState::Done;
              Poll::Ready(None)
            } else {
              // Force re-poll to make sure new ModuleLoadFuture's wakers are registered
              inner.try_poll_next_unpin(cx)
            }
          }
          Poll::Ready(Some(Some(info))) => Poll::Ready(Some(Ok(info))),
          Poll::Pending => Poll::Pending,
        }
      }
      LoadState::Done => Poll::Ready(None),
    }
  }
}

fn source_mapped_module_load_referrer(
  source_mapper: &RefCell<SourceMapper>,
  referrer: &ModuleSpecifier,
  code: &ModuleSourceCode,
  source_offset: i32,
) -> Option<ModuleLoadReferrer> {
  // 1-based.
  let (line_number, column_number) = code
    .as_bytes()
    .split_at_checked(source_offset as usize)?
    .0
    .iter()
    .enumerate()
    .filter(|(_, c)| **c as char == '\n')
    .enumerate()
    .last()
    .map(|(n, (i, _))| (n as u32 + 2, source_offset as u32 - i as u32))
    .unwrap_or_else(|| (1, source_offset as u32 + 1));
  let (specifier, line_number, column_number) = match source_mapper
    .borrow_mut()
    .apply_source_map(referrer.as_str(), line_number, column_number)
  {
    SourceMapApplication::Unchanged => {
      (referrer.clone(), line_number as _, column_number as _)
    }
    SourceMapApplication::LineAndColumn {
      line_number,
      column_number,
    } => (referrer.clone(), line_number as _, column_number as _),
    SourceMapApplication::LineAndColumnAndFileName {
      file_name,
      line_number,
      column_number,
    } => (
      ModuleSpecifier::parse(&file_name).ok()?,
      line_number as _,
      column_number as _,
    ),
  };
  Some(ModuleLoadReferrer {
    specifier,
    line_number,
    column_number,
  })
}
