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

use std::borrow::Cow;
use std::collections::HashMap;
use std::ffi::OsString;
use std::io::ErrorKind;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::OnceLock;

use deno_core::FastString;
use deno_core::ModuleCodeBytes;
use deno_core::ModuleSourceCode;
use deno_core::ModuleType;
use deno_core::anyhow::Context;
use deno_core::anyhow::bail;
use deno_core::error::AnyError;
use deno_core::serde_json;
use deno_core::url::Url;
use deno_error::JsError;
use deno_error::JsErrorBox;
use deno_lib::standalone::binary::DenoRtDeserializable;
use deno_lib::standalone::binary::MAGIC_BYTES;
use deno_lib::standalone::binary::Metadata;
use deno_lib::standalone::binary::RemoteModuleEntry;
use deno_lib::standalone::binary::SpecifierDataStore;
use deno_lib::standalone::binary::SpecifierId;
use deno_lib::standalone::virtual_fs::VfsEntry;
use deno_lib::standalone::virtual_fs::VirtualDirectory;
use deno_lib::standalone::virtual_fs::VirtualDirectoryEntries;
use deno_media_type::MediaType;
use deno_npm::NpmPackageId;
use deno_npm::resolution::SerializedNpmResolutionSnapshot;
use deno_npm::resolution::SerializedNpmResolutionSnapshotPackage;
use deno_npm::resolution::ValidSerializedNpmResolutionSnapshot;
use deno_semver::StackString;
use deno_semver::package::PackageReq;
use indexmap::IndexMap;
use sys_traits::FsCanonicalize;
use sys_traits::FsRead;
use thiserror::Error;

use crate::file_system::FileBackedVfs;
use crate::file_system::VfsRoot;

pub struct StandaloneData {
  pub metadata: Metadata,
  pub modules: Arc<StandaloneModules>,
  pub npm_snapshot: Option<ValidSerializedNpmResolutionSnapshot>,
  pub root_path: PathBuf,
  pub vfs: Arc<FileBackedVfs>,
}

/// This function will try to run this binary as a standalone binary
/// produced by `deno compile`. It determines if this is a standalone
/// binary by skipping over the trailer width at the end of the file,
/// then checking for the magic trailer string `d3n0l4nd`. If found,
/// the bundle is executed. If not, this function exits with `Ok(None)`.
pub fn extract_standalone(
  cli_args: Cow<[OsString]>,
) -> Result<StandaloneData, AnyError> {
  extract_standalone_with_finder(cli_args, find_section)
}

/// Like `extract_standalone`, but allows providing a custom section finder.
/// This is used by the desktop cdylib to search its own image rather than
/// the main executable.
pub fn extract_standalone_with_finder(
  cli_args: Cow<[OsString]>,
  section_finder: fn() -> Result<&'static [u8], AnyError>,
) -> Result<StandaloneData, AnyError> {
  let data = section_finder()?;

  // read metadata first to determine the root path
  let (mut metadata, remaining) = read_section_metadata(data)?;

  // for self-extracting executables, use the extraction directory as root
  // so that module specifiers resolve to extracted file paths
  let root_path = if let Some(hash) = &metadata.self_extracting {
    let dir = choose_and_create_extraction_dir(hash)?;
    sys_traits::impls::RealSys
      .fs_canonicalize(&dir)
      .unwrap_or(dir)
  } else {
    let maybe_current_exe = std::env::current_exe().ok();
    let current_exe_name = maybe_current_exe
      .as_ref()
      .and_then(|p| p.file_name())
      .map(|p| p.to_string_lossy())
      // should never happen
      .unwrap_or_else(|| Cow::Borrowed("binary"));
    std::env::temp_dir().join(format!("deno-compile-{}", current_exe_name))
  };
  let root_url = deno_path_util::url_from_directory_path(&root_path)?;

  let DeserializedDataSection {
    npm_snapshot,
    modules_store: remote_modules,
    vfs_root_entries,
    vfs_files_data,
  } = deserialize_binary_data_section(&root_url, remaining)?;

  let cli_args = cli_args.into_owned();
  let current_exe = std::env::current_exe().ok();
  let mut args_iter = cli_args.into_iter();
  args_iter.next(); // skip argv[0]

  // Node.js apps relaunch with spawn(process.execPath, [process.argv[1], ...args]).
  // In standalone mode process.argv[1] === execPath (#32990), so the first arg
  // after argv[0] is a duplicate of the exe path. Strip it.
  //
  // NOTE: this means `./myapp ./myapp --foo` would silently lose the first
  // `./myapp` arg. In practice standalone binaries are never invoked this way,
  // and this matches how Node.js SEA handles the relaunch pattern.
  if let Some(first) = args_iter.next() {
    let is_exe_dup = current_exe
      .as_ref()
      .is_some_and(|exe| Path::new(&first) == exe.as_path());
    if !is_exe_dup {
      metadata.argv.push(first.into_string().unwrap());
    }
  }
  for arg in args_iter {
    metadata.argv.push(arg.into_string().unwrap());
  }
  let vfs = {
    let fs_root = VfsRoot {
      dir: VirtualDirectory {
        // align the name of the directory with the root dir
        name: root_path
          .file_name()
          .unwrap()
          .to_string_lossy()
          .into_owned(),
        entries: vfs_root_entries,
      },
      root_path: root_path.clone(),
      start_file_offset: 0,
    };
    Arc::new(FileBackedVfs::new(
      Cow::Borrowed(vfs_files_data),
      fs_root,
      metadata.vfs_case_sensitivity,
    ))
  };
  Ok(StandaloneData {
    metadata,
    modules: Arc::new(StandaloneModules {
      modules: remote_modules,
      vfs: vfs.clone(),
    }),
    npm_snapshot,
    root_path,
    vfs,
  })
}

/// Extracts the embedded file system to disk for a self-extracting
/// executable. The extraction_dir is the directory where files will be
/// extracted, which should match the VFS root_path.
pub fn extract_vfs_to_disk(
  vfs: &FileBackedVfs,
  extraction_dir: &Path,
) -> Result<(), AnyError> {
  // check if already extracted
  let done_marker = extraction_dir.join(".done");
  if done_marker.exists() {
    log::debug!("Already extracted to {}", extraction_dir.display());
    return Ok(());
  }

  log::debug!("Extracting to {}", extraction_dir.display());

  let start = std::time::Instant::now();
  std::fs::create_dir_all(extraction_dir).with_context(|| {
    format!(
      "Failed to create extraction directory: {}",
      extraction_dir.display()
    )
  })?;
  extract_vfs_dir(
    vfs,
    vfs.root_dir(),
    &mut extraction_dir.to_path_buf(),
    extraction_dir,
  )
  .context("Failed to extract embedded files to disk")?;

  // write the done marker
  std::fs::File::create(&done_marker)
    .context("Failed to write extraction done marker")?;

  log::debug!("Extracted in {}ms", start.elapsed().as_millis());

  Ok(())
}

fn choose_and_create_extraction_dir(
  hash_str: &str,
) -> Result<PathBuf, AnyError> {
  let current_exe = std::env::current_exe()
    .context("Failed to determine current executable path")?;
  let exe_name = current_exe
    .file_stem()
    .map(|s| s.to_string_lossy().into_owned())
    .unwrap_or_else(|| "binary".to_string());

  // try next to the executable first
  if let Some(exe_dir) = current_exe.parent() {
    let dir = exe_dir.join(format!(".{}", exe_name)).join(hash_str);
    match std::fs::create_dir_all(&dir) {
      Ok(()) => return Ok(dir),
      Err(err) => {
        log::debug!(
          "Could not create extraction dir next to executable ({}), falling back to data dir: {}",
          err,
          dir.display()
        );
      }
    }
  }

  // fall back to platform-specific data directory
  let data_dir = get_data_local_dir().context(
    "Failed to determine local data directory for self-extracting executable",
  )?;
  let dir = data_dir.join(&exe_name).join(hash_str);
  std::fs::create_dir_all(&dir).with_context(|| {
    format!("Failed to create extraction directory: {}", dir.display())
  })?;
  Ok(dir)
}

pub(crate) fn get_data_local_dir() -> Option<PathBuf> {
  #[cfg(target_os = "windows")]
  {
    std::env::var_os("LOCALAPPDATA").map(PathBuf::from)
  }
  #[cfg(target_os = "macos")]
  {
    #[allow(clippy::disallowed_types, reason = "setup code")]
    sys_traits::EnvHomeDir::env_home_dir(&sys_traits::impls::RealSys)
      .map(|h| h.join("Library").join("Application Support"))
  }
  #[cfg(not(any(target_os = "windows", target_os = "macos")))]
  {
    std::env::var_os("XDG_DATA_HOME")
      .map(PathBuf::from)
      .or_else(|| {
        #[allow(clippy::disallowed_types, reason = "setup code")]
        sys_traits::EnvHomeDir::env_home_dir(&sys_traits::impls::RealSys)
          .map(|h| h.join(".local").join("share"))
      })
  }
}

fn extract_vfs_dir(
  vfs: &FileBackedVfs,
  dir: &VirtualDirectory,
  disk_path: &mut PathBuf,
  extraction_root: &Path,
) -> Result<(), AnyError> {
  for entry in dir.entries.iter() {
    match entry {
      VfsEntry::Dir(sub_dir) => {
        disk_path.push(&sub_dir.name);
        // parent is guaranteed to exist since we recurse top-down
        std::fs::create_dir(&*disk_path)
          .or_else(|e| {
            if e.kind() == std::io::ErrorKind::AlreadyExists {
              Ok(())
            } else {
              Err(e)
            }
          })
          .with_context(|| {
            format!("Failed to create directory: {}", disk_path.display())
          })?;
        extract_vfs_dir(vfs, sub_dir, disk_path, extraction_root)?;
        disk_path.pop();
      }
      VfsEntry::File(file) => {
        disk_path.push(&file.name);
        let data = vfs
          .read_file_all(file)
          .with_context(|| format!("Failed to read VFS file: {}", file.name))?;
        #[cfg(unix)]
        {
          use std::io::Write;
          use std::os::unix::fs::OpenOptionsExt;
          let mode = if file.executable { 0o755 } else { 0o644 };
          std::fs::OpenOptions::new()
            .write(true)
            .create(true)
            .truncate(true)
            .mode(mode)
            .open(&*disk_path)
            .and_then(|mut f| f.write_all(&data))
            .with_context(|| {
              format!("Failed to write file: {}", disk_path.display())
            })?;
        }
        #[cfg(not(unix))]
        {
          std::fs::write(&*disk_path, &*data).with_context(|| {
            format!("Failed to write file: {}", disk_path.display())
          })?;
        }
        disk_path.pop();
      }
      VfsEntry::Symlink(symlink) => {
        disk_path.push(&symlink.name);
        // symlink dest_parts are relative to the VFS root,
        // so resolve them relative to the extraction root
        let absolute_target = symlink.resolve_dest_from_root(extraction_root);
        create_symlink(&absolute_target, disk_path, symlink.dest_is_dir)?;
        disk_path.pop();
      }
    }
  }

  Ok(())
}

fn create_symlink(
  target: &Path,
  link_path: &Path,
  dest_is_dir: bool,
) -> Result<(), AnyError> {
  #[cfg(unix)]
  {
    _ = dest_is_dir; // unused on unix
    std::os::unix::fs::symlink(target, link_path)
      .or_else(|_| {
        // may be left over from an interrupted extraction
        let _ = std::fs::remove_file(link_path);
        std::os::unix::fs::symlink(target, link_path)
      })
      .with_context(|| {
        format!(
          "Failed to create symlink: {} -> {}",
          link_path.display(),
          target.display()
        )
      })?;
  }
  #[cfg(windows)]
  {
    // on Windows, symlink_file and symlink_dir are distinct — using the
    // wrong type causes PermissionDenied when accessing through the link
    let create_link = |target: &Path, link: &Path| {
      if dest_is_dir {
        std::os::windows::fs::symlink_dir(target, link)
      } else {
        std::os::windows::fs::symlink_file(target, link)
      }
    };
    create_link(target, link_path)
      .or_else(|_| {
        // may be left over from an interrupted extraction
        let _ = std::fs::remove_file(link_path)
          .or_else(|_| std::fs::remove_dir(link_path));
        create_link(target, link_path)
      })
      .or_else(|_| {
        // symlinks may require elevated privileges on Windows,
        // fall back to junctions for dirs or copying for files
        if dest_is_dir {
          junction::create(target, link_path)
        } else {
          std::fs::copy(target, link_path).map(|_| ())
        }
      })
      .with_context(|| {
        format!(
          "Failed to create symlink: {} -> {}",
          link_path.display(),
          target.display()
        )
      })?;
  }

  Ok(())
}

fn find_section() -> Result<&'static [u8], AnyError> {
  #[cfg(windows)]
  if std::env::var_os("DENO_INTERNAL_RT_USE_FILE_FALLBACK").is_some() {
    return read_from_file_fallback();
  }

  match libsui::find_section("d3n0l4nd")
    .context("Failed reading standalone binary section.")
  {
    Ok(Some(data)) => Ok(data),
    Ok(None) => bail!("Could not find standalone binary section."),
    Err(err) => {
      #[cfg(windows)]
      if let Ok(data) = read_from_file_fallback() {
        return Ok(data);
      }

      Err(err)
    }
  }
}

/// This is a temporary hacky fallback until we can find
/// a fix for https://github.com/denoland/deno/issues/28982
#[cfg(windows)]
fn read_from_file_fallback() -> Result<&'static [u8], AnyError> {
  use std::sync::OnceLock;

  fn find_in_bytes(bytes: &[u8], needle: &[u8]) -> Option<usize> {
    bytes.windows(needle.len()).position(|n| n == needle)
  }

  static FILE: OnceLock<std::fs::File> = OnceLock::new();
  static MMAP_FILE: OnceLock<memmap2::Mmap> = OnceLock::new();

  // DENOLAND in utf16
  const RESOURCE_SECTION_HEADER_NAME: &[u8] = &[
    0x44, 0x00, 0x33, 0x00, 0x4E, 0x00, 0x30, 0x00, 0x4C, 0x00, 0x34, 0x00,
    0x4E, 0x00, 0x44, 0x00,
  ];
  const MAGIC_BYTES: &[u8] = b"d3n0l4nd";

  let file_path = std::env::current_exe()?;
  let file = FILE.get_or_init(|| std::fs::File::open(file_path).unwrap());
  let mmap = MMAP_FILE.get_or_init(|| {
    // SAFETY: memory mapped file creation
    unsafe { memmap2::Mmap::map(file).unwrap() }
  });

  // the code in this file will cause this to appear twice in the binary,
  // so skip over the first one
  let Some(marker_pos) = find_in_bytes(mmap, RESOURCE_SECTION_HEADER_NAME)
  else {
    bail!("Failed to find first section name.");
  };
  let next_bytes = &mmap[marker_pos + RESOURCE_SECTION_HEADER_NAME.len()..];
  let Some(marker_pos) =
    find_in_bytes(next_bytes, RESOURCE_SECTION_HEADER_NAME)
  else {
    bail!("Failed to find second section name.");
  };
  let next_bytes =
    &next_bytes[marker_pos + RESOURCE_SECTION_HEADER_NAME.len()..];
  let Some(ascii_pos) = find_in_bytes(next_bytes, MAGIC_BYTES) else {
    bail!("Failed to find first magic bytes.");
  };
  let next_bytes = &next_bytes[ascii_pos..];
  let Some(last_pos) = next_bytes
    .windows(MAGIC_BYTES.len())
    .rposition(|w| w == MAGIC_BYTES)
  else {
    bail!("Failed to find end magic bytes.")
  };

  Ok(&next_bytes[..last_pos + MAGIC_BYTES.len()])
}

pub struct DeserializedDataSection {
  pub npm_snapshot: Option<ValidSerializedNpmResolutionSnapshot>,
  pub modules_store: RemoteModulesStore,
  pub vfs_root_entries: VirtualDirectoryEntries,
  pub vfs_files_data: &'static [u8],
}

fn read_magic_bytes(input: &[u8]) -> Result<(&[u8], bool), AnyError> {
  if input.len() < MAGIC_BYTES.len() {
    bail!("Unexpected end of data. Could not find magic bytes.");
  }
  let (magic_bytes, input) = input.split_at(MAGIC_BYTES.len());
  if magic_bytes != MAGIC_BYTES {
    return Ok((input, false));
  }
  Ok((input, true))
}

/// Reads the magic bytes and metadata from the beginning of the data section.
/// Returns the metadata and the remaining input after the metadata.
fn read_section_metadata(
  data: &'static [u8],
) -> Result<(Metadata, &'static [u8]), AnyError> {
  let (input, found) = read_magic_bytes(data)?;
  if !found {
    bail!("Did not find magic bytes.");
  }
  let (input, metadata_bytes) =
    read_bytes_with_u64_len(input).context("reading metadata")?;
  let metadata: Metadata =
    serde_json::from_slice(metadata_bytes).context("deserializing metadata")?;
  Ok((metadata, input))
}

/// Deserializes the binary data section after the metadata has already been
/// parsed by `read_section_metadata`.
fn deserialize_binary_data_section(
  root_dir_url: &Url,
  input: &'static [u8],
) -> Result<DeserializedDataSection, AnyError> {
  // 1. Was the metadata above.
  // 2. Npm snapshot
  let (input, data) =
    read_bytes_with_u64_len(input).context("reading npm snapshot")?;
  let npm_snapshot = if data.is_empty() {
    None
  } else {
    Some(deserialize_npm_snapshot(data).context("deserializing npm snapshot")?)
  };
  // 3. Specifiers
  let (input, specifiers_store) =
    SpecifierStore::deserialize(root_dir_url, input)
      .context("deserializing specifiers")?;
  // 4. Redirects
  let (input, redirects_store) =
    SpecifierDataStore::<SpecifierId>::deserialize(input)
      .context("deserializing redirects")?;
  // 5. Remote modules
  let (input, remote_modules_store) =
    SpecifierDataStore::<RemoteModuleEntry<'static>>::deserialize(input)
      .context("deserializing remote modules")?;
  // 6. VFS
  let (input, data) = read_bytes_with_u64_len(input).context("vfs")?;
  let vfs_root_entries: VirtualDirectoryEntries =
    serde_json::from_slice(data).context("deserializing vfs data")?;
  let (input, vfs_files_data) =
    read_bytes_with_u64_len(input).context("reading vfs files data")?;

  // finally ensure we read the magic bytes at the end
  let (_input, found) = read_magic_bytes(input)?;
  if !found {
    bail!("Could not find magic bytes at end of data.");
  }

  let modules_store = RemoteModulesStore::new(
    specifiers_store,
    redirects_store,
    remote_modules_store,
  );

  Ok(DeserializedDataSection {
    npm_snapshot,
    modules_store,
    vfs_root_entries,
    vfs_files_data,
  })
}

struct SpecifierStore {
  data: IndexMap<Arc<Url>, SpecifierId>,
  reverse: IndexMap<SpecifierId, Arc<Url>>,
}

impl SpecifierStore {
  pub fn deserialize<'a>(
    root_dir_url: &Url,
    input: &'a [u8],
  ) -> std::io::Result<(&'a [u8], Self)> {
    let (input, len) = read_u32_as_usize(input)?;
    let mut data = IndexMap::with_capacity(len);
    let mut reverse = IndexMap::with_capacity(len);
    let mut input = input;
    for _ in 0..len {
      let (new_input, specifier_str) = read_string_lossy(input)?;
      let specifier = match Url::parse(&specifier_str) {
        Ok(url) => url,
        Err(err) => match root_dir_url.join(&specifier_str) {
          Ok(url) => url,
          Err(_) => {
            return Err(std::io::Error::new(
              std::io::ErrorKind::InvalidData,
              err,
            ));
          }
        },
      };
      let (new_input, id) = SpecifierId::deserialize(new_input)?;
      let specifier = Arc::new(specifier);
      data.insert(specifier.clone(), id);
      reverse.insert(id, specifier);
      input = new_input;
    }
    Ok((input, Self { data, reverse }))
  }

  pub fn get_id(&self, specifier: &Url) -> Option<SpecifierId> {
    self.data.get(specifier).cloned()
  }

  pub fn get_specifier(&self, specifier_id: SpecifierId) -> Option<&Url> {
    self.reverse.get(&specifier_id).map(|url| url.as_ref())
  }
}

pub struct StandaloneModules {
  modules: RemoteModulesStore,
  vfs: Arc<FileBackedVfs>,
}

impl StandaloneModules {
  pub fn resolve_specifier<'a>(
    &'a self,
    specifier: &'a Url,
  ) -> Result<Option<&'a Url>, TooManyRedirectsError> {
    if specifier.scheme() == "file" {
      Ok(Some(specifier))
    } else {
      self.modules.resolve_specifier(specifier)
    }
  }

  pub fn path_in_root(&self, path: &Path) -> bool {
    deno_path_util::normalize_path(Cow::Borrowed(path))
      .starts_with(self.vfs.root())
  }

  pub fn read<'a>(
    &'a self,
    specifier: &'a Url,
  ) -> Result<Option<DenoCompileModuleData<'a>>, JsErrorBox> {
    if specifier.scheme() == "file" {
      let path = deno_path_util::url_to_file_path(specifier)
        .map_err(JsErrorBox::from_err)?;
      let mut transpiled = None;
      let mut source_map = None;
      let mut cjs_export_analysis = None;
      let mut is_valid_utf8 = false;
      let bytes = match self.vfs.file_entry(&path) {
        Ok(entry) => {
          let bytes = self
            .vfs
            .read_file_all(entry)
            .map_err(JsErrorBox::from_err)?;
          is_valid_utf8 = entry.is_valid_utf8;
          transpiled = entry
            .transpiled_offset
            .and_then(|t| self.vfs.read_file_offset_with_len(t).ok());
          source_map = entry
            .source_map_offset
            .and_then(|t| self.vfs.read_file_offset_with_len(t).ok());
          cjs_export_analysis = entry
            .cjs_export_analysis_offset
            .and_then(|t| self.vfs.read_file_offset_with_len(t).ok());
          bytes
        }
        Err(err) if err.kind() == ErrorKind::NotFound => {
          #[allow(
            clippy::disallowed_types,
            reason = "use real file system because not in vfs"
          )]
          let bytes = match sys_traits::impls::RealSys.fs_read(&path) {
            Ok(bytes) => bytes,
            Err(err) if err.kind() == ErrorKind::NotFound => {
              return Ok(None);
            }
            Err(err) => return Err(JsErrorBox::from_err(err)),
          };
          // A file read from disk at runtime (e.g. a plugin discovered by a
          // compiled program) hasn't been transpiled at compile time, so
          // transpile TypeScript/JSX here, mirroring `deno run`.
          let media_type = MediaType::from_specifier(specifier);
          if matches!(
            media_type,
            MediaType::TypeScript
              | MediaType::Mts
              | MediaType::Cts
              | MediaType::Jsx
              | MediaType::Tsx
          ) {
            let text = String::from_utf8_lossy(&bytes).into_owned();
            let transpiled_text =
              transpile_runtime_module(specifier, media_type, text)?;
            transpiled = Some(Cow::Owned(transpiled_text.into_bytes()));
          }
          bytes
        }
        Err(err) => return Err(JsErrorBox::from_err(err)),
      };
      Ok(Some(DenoCompileModuleData {
        media_type: MediaType::from_specifier(specifier),
        specifier,
        is_valid_utf8,
        data: bytes,
        transpiled,
        source_map,
        cjs_export_analysis,
      }))
    } else {
      self.modules.read(specifier).map_err(JsErrorBox::from_err)
    }
  }
}

/// Transpile TypeScript/JSX source to JavaScript for modules that are not
/// embedded in the binary, namely `data:`/`blob:` URLs and local files read
/// from disk at runtime. Embedded modules are already transpiled at
/// `deno compile` time, but a compiled program can still dynamically `import()`
/// TypeScript that it builds or discovers while running. `deno_ast` is already
/// linked into the binary via `ext/node`, so transpiling here adds no extra
/// binary size.
///
/// JSX configuration (`jsx`, `jsxImportSource`, `jsxFactory`, etc.) is
/// discovered by walking up from the source file to find the user's
/// `deno.json` / `deno.jsonc` once and caching the result. Without this,
/// every `.tsx` from a Preact-based project would be emitted as
/// `React.createElement(...)` and fail at runtime with `React is not defined`.
pub(crate) fn transpile_runtime_module(
  specifier: &Url,
  media_type: MediaType,
  source: String,
) -> Result<String, JsErrorBox> {
  match media_type {
    MediaType::TypeScript
    | MediaType::Mts
    | MediaType::Cts
    | MediaType::Jsx
    | MediaType::Tsx => {}
    // JavaScript, JSON, Wasm, etc. are served verbatim.
    _ => return Ok(source),
  }

  let parsed = deno_ast::parse_module(deno_ast::ParseParams {
    specifier: specifier.clone(),
    text: source.into(),
    media_type,
    capture_tokens: false,
    scope_analysis: false,
    maybe_syntax: None,
  })
  .map_err(JsErrorBox::from_err)?;
  let opts = runtime_transpile_options(specifier);
  let transpiled = parsed
    .transpile(
      opts,
      &deno_ast::TranspileModuleOptions { module_kind: None },
      &deno_ast::EmitOptions {
        source_map: deno_ast::SourceMapOption::Inline,
        ..Default::default()
      },
    )
    .map_err(JsErrorBox::from_err)?;
  Ok(transpiled.into_source().text)
}

/// Resolved JSX/decorator options for runtime transpiles, discovered lazily
/// from the user's project `deno.json[c]` and cached for the lifetime of the
/// process.
static RUNTIME_TRANSPILE_OPTIONS: OnceLock<deno_ast::TranspileOptions> =
  OnceLock::new();

/// Walk up from `specifier`'s parent directory looking for `deno.json` or
/// `deno.jsonc`, parse `compilerOptions.jsx` + related, and return a matching
/// `TranspileOptions`. Falls back to defaults (with `ImportsNotUsedAsValues::Remove`)
/// if no config is found or parsing fails — those are non-fatal here.
fn runtime_transpile_options(
  specifier: &Url,
) -> &'static deno_ast::TranspileOptions {
  RUNTIME_TRANSPILE_OPTIONS.get_or_init(|| {
    let fallback = deno_ast::TranspileOptions {
      imports_not_used_as_values: deno_ast::ImportsNotUsedAsValues::Remove,
      ..Default::default()
    };
    let Ok(path) = deno_path_util::url_to_file_path(specifier) else {
      return fallback;
    };
    let Some(mut dir) = path.parent().map(|p| p.to_path_buf()) else {
      return fallback;
    };
    let cfg_path = 'outer: loop {
      for name in ["deno.json", "deno.jsonc"] {
        let candidate = dir.join(name);
        if candidate.exists() {
          break 'outer Some(candidate);
        }
      }
      if !dir.pop() {
        break 'outer None;
      }
    };
    let Some(cfg_path) = cfg_path else {
      return fallback;
    };
    let Ok(text) = std::fs::read_to_string(&cfg_path) else {
      return fallback;
    };
    let Some(json) =
      jsonc_parser::parse_to_serde_value(&text, &Default::default())
        .ok()
        .flatten()
    else {
      return fallback;
    };
    let json: &serde_json::Value = &json;
    transpile_options_from_compiler_options(
      json.get("compilerOptions"),
      &fallback,
    )
  })
}

/// Lower the relevant subset of `compilerOptions` (the same one
/// `cli/tools/pack/mod.rs::create_transpile_options` handles) into
/// `TranspileOptions`. Anything missing falls back to `fallback`.
fn transpile_options_from_compiler_options(
  compiler_options: Option<&serde_json::Value>,
  fallback: &deno_ast::TranspileOptions,
) -> deno_ast::TranspileOptions {
  let get_str = |key: &str| -> Option<String> {
    compiler_options?.get(key)?.as_str().map(|s| s.to_string())
  };
  let get_bool = |key: &str| -> bool {
    compiler_options
      .and_then(|opts| opts.get(key))
      .and_then(|v| v.as_bool())
      .unwrap_or(false)
  };
  let jsx = get_str("jsx");
  let jsx_import_source = get_str("jsxImportSource");
  let jsx_factory = get_str("jsxFactory");
  let jsx_fragment_factory = get_str("jsxFragmentFactory");
  let jsx_runtime = match jsx.as_deref() {
    Some("react") => {
      Some(deno_ast::JsxRuntime::Classic(deno_ast::JsxClassicOptions {
        factory: jsx_factory
          .unwrap_or_else(|| "React.createElement".to_string()),
        fragment_factory: jsx_fragment_factory
          .unwrap_or_else(|| "React.Fragment".to_string()),
      }))
    }
    Some("react-jsx") => Some(deno_ast::JsxRuntime::Automatic(
      deno_ast::JsxAutomaticOptions {
        development: false,
        import_source: jsx_import_source,
      },
    )),
    Some("react-jsxdev") => Some(deno_ast::JsxRuntime::Automatic(
      deno_ast::JsxAutomaticOptions {
        development: true,
        import_source: jsx_import_source,
      },
    )),
    Some("precompile") => Some(deno_ast::JsxRuntime::Precompile(
      deno_ast::JsxPrecompileOptions {
        automatic: deno_ast::JsxAutomaticOptions {
          development: false,
          import_source: jsx_import_source,
        },
        skip_elements: None,
        dynamic_props: None,
      },
    )),
    _ => fallback.jsx.clone(),
  };
  let experimental_decorators = get_bool("experimentalDecorators");
  let emit_decorator_metadata = get_bool("emitDecoratorMetadata");
  deno_ast::TranspileOptions {
    jsx: jsx_runtime,
    decorators: if experimental_decorators {
      deno_ast::DecoratorsTranspileOption::LegacyTypeScript {
        emit_metadata: emit_decorator_metadata,
      }
    } else {
      deno_ast::DecoratorsTranspileOption::Ecma
    },
    imports_not_used_as_values: deno_ast::ImportsNotUsedAsValues::Remove,
    var_decl_imports: false,
    verbatim_module_syntax: false,
  }
}

pub struct DenoCompileModuleData<'a> {
  pub specifier: &'a Url,
  pub media_type: MediaType,
  pub is_valid_utf8: bool,
  pub data: Cow<'static, [u8]>,
  pub transpiled: Option<Cow<'static, [u8]>>,
  pub source_map: Option<Cow<'static, [u8]>>,
  pub cjs_export_analysis: Option<Cow<'static, [u8]>>,
}

impl<'a> DenoCompileModuleData<'a> {
  pub fn into_parts(self) -> (&'a Url, ModuleType, DenoCompileModuleSource) {
    fn into_string_unsafe(
      is_valid_utf8: bool,
      data: Cow<'static, [u8]>,
    ) -> DenoCompileModuleSource {
      match data {
        Cow::Borrowed(d) if is_valid_utf8 => {
          DenoCompileModuleSource::String(
            // SAFETY: we know this is a valid utf8 string
            unsafe { std::str::from_utf8_unchecked(d) },
          )
        }
        Cow::Borrowed(_) => DenoCompileModuleSource::Bytes(data),
        Cow::Owned(d) => DenoCompileModuleSource::Bytes(Cow::Owned(d)),
      }
    }

    let data = self.transpiled.unwrap_or(self.data);
    let (media_type, source) = match self.media_type {
      MediaType::JavaScript
      | MediaType::Jsx
      | MediaType::Mjs
      | MediaType::Cjs
      | MediaType::TypeScript
      | MediaType::Mts
      | MediaType::Cts
      | MediaType::Dts
      | MediaType::Dmts
      | MediaType::Dcts
      | MediaType::Tsx => (
        ModuleType::JavaScript,
        into_string_unsafe(self.is_valid_utf8, data),
      ),
      MediaType::Json => (
        ModuleType::Json,
        into_string_unsafe(self.is_valid_utf8, data),
      ),
      MediaType::Wasm => {
        (ModuleType::Wasm, DenoCompileModuleSource::Bytes(data))
      }
      // just assume javascript if we made it here
      MediaType::Css
      | MediaType::Html
      | MediaType::Jsonc
      | MediaType::Json5
      | MediaType::Markdown
      | MediaType::SourceMap
      | MediaType::Sql
      | MediaType::Unknown => {
        (ModuleType::JavaScript, DenoCompileModuleSource::Bytes(data))
      }
    };
    (self.specifier, media_type, source)
  }
}

#[derive(Debug)]
pub enum DenoCompileModuleSource {
  String(&'static str),
  Bytes(Cow<'static, [u8]>),
}

impl DenoCompileModuleSource {
  pub fn into_for_v8(self) -> ModuleSourceCode {
    match self {
      // todo(https://github.com/denoland/deno_core/pull/943): store whether
      // the string is ascii or not ahead of time so we can avoid the is_ascii()
      // check in FastString::from_static
      Self::String(s) => ModuleSourceCode::String(FastString::from_static(s)),
      Self::Bytes(b) => ModuleSourceCode::Bytes(module_source_into_bytes(b)),
    }
  }

  pub fn into_bytes_for_v8(self) -> ModuleCodeBytes {
    match self {
      DenoCompileModuleSource::String(text) => text.as_bytes().into(),
      DenoCompileModuleSource::Bytes(b) => module_source_into_bytes(b),
    }
  }
}

fn module_source_into_bytes(data: Cow<'static, [u8]>) -> ModuleCodeBytes {
  match data {
    Cow::Borrowed(d) => d.into(),
    Cow::Owned(d) => d.into_boxed_slice().into(),
  }
}

#[derive(Debug, Error, JsError)]
#[class(generic)]
#[error("Too many redirects resolving: {0}")]
pub struct TooManyRedirectsError(Url);

pub struct RemoteModulesStore {
  specifiers: SpecifierStore,
  redirects: SpecifierDataStore<SpecifierId>,
  remote_modules: SpecifierDataStore<RemoteModuleEntry<'static>>,
}

impl RemoteModulesStore {
  fn new(
    specifiers: SpecifierStore,
    redirects: SpecifierDataStore<SpecifierId>,
    remote_modules: SpecifierDataStore<RemoteModuleEntry<'static>>,
  ) -> Self {
    Self {
      specifiers,
      redirects,
      remote_modules,
    }
  }

  pub fn resolve_specifier<'a>(
    &'a self,
    specifier: &'a Url,
  ) -> Result<Option<&'a Url>, TooManyRedirectsError> {
    let Some(mut current) = self.specifiers.get_id(specifier) else {
      return Ok(None);
    };
    let mut count = 0;
    loop {
      if count > 10 {
        return Err(TooManyRedirectsError(specifier.clone()));
      }
      match self.redirects.get(current) {
        Some(to) => {
          current = *to;
          count += 1;
        }
        None => {
          if count == 0 {
            return Ok(Some(specifier));
          } else {
            return Ok(self.specifiers.get_specifier(current));
          }
        }
      }
    }
  }

  pub fn read<'a>(
    &'a self,
    original_specifier: &'a Url,
  ) -> Result<Option<DenoCompileModuleData<'a>>, TooManyRedirectsError> {
    #[allow(clippy::ptr_arg, reason = "Cow on data is relevant here")]
    fn handle_cow_ref(data: &Cow<'static, [u8]>) -> Cow<'static, [u8]> {
      match data {
        Cow::Borrowed(data) => Cow::Borrowed(data),
        Cow::Owned(data) => {
          // this variant should never happen because the data
          // should always be borrowed static in denort
          debug_assert!(false);
          Cow::Owned(data.clone())
        }
      }
    }

    let mut count = 0;
    let Some(mut specifier) = self.specifiers.get_id(original_specifier) else {
      return Ok(None);
    };
    loop {
      if count > 10 {
        return Err(TooManyRedirectsError(original_specifier.clone()));
      }
      match self.redirects.get(specifier) {
        Some(to) => {
          specifier = *to;
          count += 1;
        }
        None => {
          let Some(entry) = self.remote_modules.get(specifier) else {
            return Ok(None);
          };
          return Ok(Some(DenoCompileModuleData {
            specifier: if count == 0 {
              original_specifier
            } else {
              self.specifiers.get_specifier(specifier).unwrap()
            },
            media_type: entry.media_type,
            is_valid_utf8: entry.is_valid_utf8,
            data: handle_cow_ref(&entry.data),
            transpiled: entry.maybe_transpiled.as_ref().map(handle_cow_ref),
            source_map: entry.maybe_source_map.as_ref().map(handle_cow_ref),
            cjs_export_analysis: entry
              .maybe_cjs_export_analysis
              .as_ref()
              .map(handle_cow_ref),
          }));
        }
      }
    }
  }
}

fn deserialize_npm_snapshot(
  input: &[u8],
) -> Result<ValidSerializedNpmResolutionSnapshot, AnyError> {
  fn parse_id(input: &[u8]) -> Result<(&[u8], NpmPackageId), AnyError> {
    let (input, id) = read_string_lossy(input)?;
    let id = NpmPackageId::from_serialized(&id)?;
    Ok((input, id))
  }

  #[allow(clippy::needless_lifetimes, reason = "clippy bug")]
  #[allow(clippy::type_complexity, reason = "private code")]
  fn parse_root_package<'a>(
    id_to_npm_id: &'a impl Fn(usize) -> Result<NpmPackageId, AnyError>,
  ) -> impl Fn(&[u8]) -> Result<(&[u8], (PackageReq, NpmPackageId)), AnyError> + 'a
  {
    |input| {
      let (input, req) = read_string_lossy(input)?;
      let req = PackageReq::from_str(&req)?;
      let (input, id) = read_u32_as_usize(input)?;
      Ok((input, (req, id_to_npm_id(id)?)))
    }
  }

  #[allow(clippy::needless_lifetimes, reason = "clippy bug")]
  #[allow(clippy::type_complexity, reason = "private code")]
  fn parse_package_dep<'a>(
    id_to_npm_id: &'a impl Fn(usize) -> Result<NpmPackageId, AnyError>,
  ) -> impl Fn(&[u8]) -> Result<(&[u8], (StackString, NpmPackageId)), AnyError> + 'a
  {
    |input| {
      let (input, req) = read_string_lossy(input)?;
      let (input, id) = read_u32_as_usize(input)?;
      let req = StackString::from_cow(req);
      Ok((input, (req, id_to_npm_id(id)?)))
    }
  }

  fn parse_package<'a>(
    input: &'a [u8],
    id: NpmPackageId,
    id_to_npm_id: &impl Fn(usize) -> Result<NpmPackageId, AnyError>,
  ) -> Result<(&'a [u8], SerializedNpmResolutionSnapshotPackage), AnyError> {
    let (input, deps_len) = read_u32_as_usize(input)?;
    let (input, dependencies) =
      parse_hashmap_n_times(input, deps_len, parse_package_dep(id_to_npm_id))?;
    Ok((
      input,
      SerializedNpmResolutionSnapshotPackage {
        id,
        system: Default::default(),
        dist: Default::default(),
        dependencies,
        optional_dependencies: Default::default(),
        optional_peer_dependencies: Default::default(),
        has_bin: false,
        has_scripts: false,
        is_deprecated: false,
        extra: Default::default(),
      },
    ))
  }

  let (input, packages_len) = read_u32_as_usize(input)?;

  // get a hashmap of all the npm package ids to their serialized ids
  let (input, data_ids_to_npm_ids) =
    parse_vec_n_times(input, packages_len, parse_id)
      .context("deserializing id")?;
  let data_id_to_npm_id = |id: usize| {
    data_ids_to_npm_ids
      .get(id)
      .cloned()
      .ok_or_else(|| deno_core::anyhow::anyhow!("Invalid npm package id"))
  };

  let (input, root_packages_len) = read_u32_as_usize(input)?;
  let (input, root_packages) = parse_hashmap_n_times(
    input,
    root_packages_len,
    parse_root_package(&data_id_to_npm_id),
  )
  .context("deserializing root package")?;
  let (input, packages) =
    parse_vec_n_times_with_index(input, packages_len, |input, index| {
      parse_package(input, data_id_to_npm_id(index)?, &data_id_to_npm_id)
    })
    .context("deserializing package")?;

  if !input.is_empty() {
    bail!("Unexpected data left over");
  }

  Ok(
    SerializedNpmResolutionSnapshot {
      packages,
      root_packages,
    }
    // this is ok because we have already verified that all the
    // identifiers found in the snapshot are valid via the
    // npm package id -> npm package id mapping
    .into_valid_unsafe(),
  )
}

fn parse_hashmap_n_times<TKey: std::cmp::Eq + std::hash::Hash, TValue>(
  mut input: &[u8],
  times: usize,
  parse: impl Fn(&[u8]) -> Result<(&[u8], (TKey, TValue)), AnyError>,
) -> Result<(&[u8], HashMap<TKey, TValue>), AnyError> {
  let mut results = HashMap::with_capacity(times);
  for _ in 0..times {
    let result = parse(input);
    let (new_input, (key, value)) = result?;
    results.insert(key, value);
    input = new_input;
  }
  Ok((input, results))
}

fn parse_vec_n_times<TResult>(
  input: &[u8],
  times: usize,
  parse: impl Fn(&[u8]) -> Result<(&[u8], TResult), AnyError>,
) -> Result<(&[u8], Vec<TResult>), AnyError> {
  parse_vec_n_times_with_index(input, times, |input, _index| parse(input))
}

fn parse_vec_n_times_with_index<TResult>(
  mut input: &[u8],
  times: usize,
  parse: impl Fn(&[u8], usize) -> Result<(&[u8], TResult), AnyError>,
) -> Result<(&[u8], Vec<TResult>), AnyError> {
  let mut results = Vec::with_capacity(times);
  for i in 0..times {
    let result = parse(input, i);
    let (new_input, result) = result?;
    results.push(result);
    input = new_input;
  }
  Ok((input, results))
}

fn read_bytes_with_u64_len(input: &[u8]) -> std::io::Result<(&[u8], &[u8])> {
  let (input, len) = read_u64(input)?;
  let (input, data) = read_bytes(input, len as usize)?;
  Ok((input, data))
}

fn read_bytes_with_u32_len(input: &[u8]) -> std::io::Result<(&[u8], &[u8])> {
  let (input, len) = read_u32_as_usize(input)?;
  let (input, data) = read_bytes(input, len)?;
  Ok((input, data))
}

fn read_bytes(input: &[u8], len: usize) -> std::io::Result<(&[u8], &[u8])> {
  check_has_len(input, len)?;
  let (len_bytes, input) = input.split_at(len);
  Ok((input, len_bytes))
}

#[inline(always)]
fn check_has_len(input: &[u8], len: usize) -> std::io::Result<()> {
  if input.len() < len {
    Err(std::io::Error::new(
      std::io::ErrorKind::InvalidData,
      "Unexpected end of data",
    ))
  } else {
    Ok(())
  }
}

fn read_string_lossy(input: &[u8]) -> std::io::Result<(&[u8], Cow<'_, str>)> {
  let (input, data_bytes) = read_bytes_with_u32_len(input)?;
  Ok((input, String::from_utf8_lossy(data_bytes)))
}

fn read_u32_as_usize(input: &[u8]) -> std::io::Result<(&[u8], usize)> {
  let (input, len_bytes) = read_bytes(input, 4)?;
  let len = u32::from_le_bytes(len_bytes.try_into().unwrap());
  Ok((input, len as usize))
}

fn read_u64(input: &[u8]) -> std::io::Result<(&[u8], u64)> {
  let (input, len_bytes) = read_bytes(input, 8)?;
  let len = u64::from_le_bytes(len_bytes.try_into().unwrap());
  Ok((input, len))
}
