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

use std::collections::HashMap;
use std::mem::MaybeUninit;
use std::os::raw::c_void;
use std::path::PathBuf;

use deno_error::JsErrorBox;
use deno_error::JsErrorClass;
use url::Url;
use v8::MapFnTo;

use super::jsruntime::BUILTIN_SOURCES;
use super::jsruntime::CONTEXT_SETUP_SOURCES;
use super::v8_static_strings::*;
use crate::_ops::OpMethodDecl;
use crate::AccessorType;
use crate::FastStaticString;
use crate::FastString;
use crate::JsRuntime;
use crate::ModuleType;
use crate::OpDecl;
use crate::cppgc::FunctionTemplateData;
use crate::cppgc::cppgc_template_constructor;
use crate::error::CoreError;
use crate::error::CoreModuleExecuteError;
use crate::error::CoreModuleParseError;
use crate::error::JsStackFrame;
use crate::error::callsite_fns;
use crate::error::has_call_site;
use crate::error::is_instance_of_error;
use crate::extension_set::LoadedSource;
use crate::extension_set::LoadedSources;
use crate::modules::ImportAttributesKind;
use crate::modules::ModuleImportPhase;
use crate::modules::ModuleMap;
use crate::modules::ModuleName;
use crate::modules::get_requested_module_type_from_attributes;
use crate::modules::parse_import_attributes;
use crate::modules::synthetic_module_evaluation_steps;
use crate::ops::OpCtx;
use crate::runtime::InitMode;
use crate::runtime::JsRealm;

pub(crate) fn create_external_references(
  ops: &[OpCtx],
  additional_references: &[v8::ExternalReference],
  sources: &[v8::OneByteConst],
  ops_in_snapshot: usize,
  sources_in_snapshot: usize,
) -> Vec<v8::ExternalReference> {
  // Overallocate a bit, it's better than having to resize the vector.
  let mut references = Vec::with_capacity(
    6 + CONTEXT_SETUP_SOURCES.len()
      + BUILTIN_SOURCES.len()
      + (ops.len() * 4)
      + additional_references.len()
      + sources.len()
      + 18 // for callsite_fns
      + 1, // nullptr
  );

  references.push(v8::ExternalReference {
    function: call_console.map_fn_to(),
  });
  references.push(v8::ExternalReference {
    function: import_meta_resolve.map_fn_to(),
  });
  references.push(v8::ExternalReference {
    function: catch_dynamic_import_promise_error.map_fn_to(),
  });
  references.push(v8::ExternalReference {
    function: op_disabled_fn.map_fn_to(),
  });
  let syn_module_eval_fn: v8::SyntheticModuleEvaluationSteps =
    synthetic_module_evaluation_steps.map_fn_to();
  references.push(v8::ExternalReference {
    pointer: syn_module_eval_fn as *mut c_void,
  });

  references.push(v8::ExternalReference {
    function: cppgc_template_constructor.map_fn_to(),
  });

  // Using v8::OneByteConst and passing external references to it
  // allows V8 to take an optimized path when deserializing the snapshot.
  for source_file in &CONTEXT_SETUP_SOURCES {
    references.push(v8::ExternalReference {
      pointer: source_file.source.into_v8_const_ptr() as _,
    });
  }

  for source_file in &BUILTIN_SOURCES {
    references.push(v8::ExternalReference {
      pointer: source_file.source.into_v8_const_ptr() as _,
    });
  }

  references.extend_from_slice(additional_references);

  for ctx in &ops[..ops_in_snapshot] {
    references.extend_from_slice(&ctx.external_references());
  }

  for source in &sources[..sources_in_snapshot] {
    references.push(v8::ExternalReference {
      pointer: source as *const _ as _,
    })
  }

  for ctx in &ops[ops_in_snapshot..] {
    references.extend_from_slice(&ctx.external_references());
  }

  for source in &sources[sources_in_snapshot..] {
    references.push(v8::ExternalReference {
      pointer: source as *const _ as _,
    })
  }

  references.push(v8::ExternalReference {
    function: callsite_fns::get_this.map_fn_to(),
  });
  references.push(v8::ExternalReference {
    function: callsite_fns::get_type_name.map_fn_to(),
  });
  references.push(v8::ExternalReference {
    function: callsite_fns::get_function.map_fn_to(),
  });
  references.push(v8::ExternalReference {
    function: callsite_fns::get_function_name.map_fn_to(),
  });
  references.push(v8::ExternalReference {
    function: callsite_fns::get_method_name.map_fn_to(),
  });
  references.push(v8::ExternalReference {
    function: callsite_fns::get_file_name.map_fn_to(),
  });
  references.push(v8::ExternalReference {
    function: callsite_fns::get_script_name_or_source_url.map_fn_to(),
  });
  references.push(v8::ExternalReference {
    function: callsite_fns::get_line_number.map_fn_to(),
  });
  references.push(v8::ExternalReference {
    function: callsite_fns::get_column_number.map_fn_to(),
  });
  references.push(v8::ExternalReference {
    function: callsite_fns::get_eval_origin.map_fn_to(),
  });
  references.push(v8::ExternalReference {
    function: callsite_fns::is_toplevel.map_fn_to(),
  });
  references.push(v8::ExternalReference {
    function: callsite_fns::is_eval.map_fn_to(),
  });
  references.push(v8::ExternalReference {
    function: callsite_fns::is_native.map_fn_to(),
  });
  references.push(v8::ExternalReference {
    function: callsite_fns::is_constructor.map_fn_to(),
  });
  references.push(v8::ExternalReference {
    function: callsite_fns::is_async.map_fn_to(),
  });
  references.push(v8::ExternalReference {
    function: callsite_fns::is_promise_all.map_fn_to(),
  });
  references.push(v8::ExternalReference {
    function: callsite_fns::get_promise_index.map_fn_to(),
  });
  references.push(v8::ExternalReference {
    function: callsite_fns::to_string.map_fn_to(),
  });

  // null terminate so rusty_v8 doesn't have to make a copy.
  references.push(v8::ExternalReference {
    pointer: std::ptr::null_mut(),
  });

  references
}

/// Result of [`externalize_sources`]: the `v8::OneByteConst` backings, the
/// original source strings they borrow from (kept alive), and the specifiers of
/// the externalized `lazy_loaded_js` sources (parallel to the trailing
/// `original_sources` entries).
pub(crate) type ExternalizedSources = (
  Box<[v8::OneByteConst]>,
  Box<[FastString]>,
  Box<[ModuleName]>,
);

/// Combine the snapshotted sources (which may be empty) with the loaded sources, and ensure that
/// each of the loaded source files passed to this function has a correct `v8::OneByteConst` backing
/// that can be used for compilation.
pub(crate) fn externalize_sources(
  sources: &mut LoadedSources,
  snapshot_sources: Vec<&'static [u8]>,
  externalize_lazy_js: bool,
) -> ExternalizedSources {
  // This is a complex method partly because we're still waiting on the `Copy` trait on v8::OneByteConst
  // to land.

  // Create an uninitialized Box<[v8::OneByteConst]. This will be simplified when we get Copy on OneByteConst.
  // Because we don't have that trait, we work around it with [usize; 3].
  const INIT_VALUE: MaybeUninit<[usize; 3]> =
    MaybeUninit::<[usize; 3]>::uninit();
  // Size to match the actual iteration below. The count must EXACTLY match the
  // number of sources iterated (and the `source_count` recorded into the
  // snapshot sidecar in `jsruntime`): a mismatch leaves trailing uninitialized
  // slots that flow into V8's external_references list and shift snapshot
  // indices, causing miscompilation of JIT'd code that references those
  // entries. `lazy_loaded_js` is only externalized when building the snapshot
  // (`externalize_lazy_js`); at runtime those scripts stay as static
  // registrations and consumed ones re-link via `snapshot_sources`.
  let sources_count = sources.js.len()
    + sources.esm.len()
    + sources.lazy_esm.len()
    + if externalize_lazy_js {
      sources.lazy_js.len()
    } else {
      0
    };
  // Record the externalized `lazy_loaded_js` specifiers in iteration order
  // (these become the trailing entries of `original_sources`). `snapshot()`
  // uses them to drop non-consumed scripts' bytes from the sidecar.
  let lazy_js_specifiers: Box<[ModuleName]> = if externalize_lazy_js {
    sources
      .lazy_js
      .iter()
      .map(|s| s.specifier.try_clone().unwrap())
      .collect()
  } else {
    Box::default()
  };
  let externals =
    vec![INIT_VALUE; sources_count + snapshot_sources.len()].into_boxed_slice();

  // Keep the original sources around, since we're borrowing from them.
  let mut original_sources = Vec::with_capacity(sources_count);

  // SAFETY: We are creating `v8::OneByteConst`s here for each of the input sources. Because
  // we keep the original source alive, we can safely make a `v8::OneByteConst` from _any_
  // source type. We'll make this lifetime static elsewhere in the code so we can safely
  // use it with v8 strings. These setup sources are ASCII; debug builds retain V8's
  // checked constructor for that invariant.
  unsafe {
    let mut externals: Box<[v8::OneByteConst]> = std::mem::transmute(externals);

    // First, add all the snapshot sources. These must be done first because we need
    // to ensure that snapshotted sources are added to the externalrefs before non-snapshotted
    // sources so that they line up correct.
    let offset = 0;
    for (index, source) in snapshot_sources.iter().enumerate() {
      externals[index + offset] =
        FastStaticString::create_external_onebyte_const_unchecked_in_release(
          source,
        );
    }

    // Next, add the non-snapshot sources. For each source file, we swap its `code`
    // member to use this new external string. Note that this is only safe because
    // we keep the original source alive.
    //
    // Iterate `js + esm + lazy_esm` always, plus `lazy_js` only when building the
    // snapshot. The empty slice keeps the iterator type identical in both arms.
    let lazy_js: &mut [LoadedSource] = if externalize_lazy_js {
      &mut sources.lazy_js
    } else {
      &mut []
    };
    let source_iter = sources
      .js
      .iter_mut()
      .chain(sources.esm.iter_mut())
      .chain(sources.lazy_esm.iter_mut())
      .chain(lazy_js.iter_mut());
    let offset = snapshot_sources.len();
    for (index, source) in source_iter.enumerate() {
      externals[index + offset] =
        FastStaticString::create_external_onebyte_const_unchecked_in_release(
          std::mem::transmute::<&[u8], &[u8]>(source.code.as_bytes()),
        );
      let ptr = &externals[index + offset] as *const v8::OneByteConst;
      let original_source = std::mem::replace(
        &mut source.code,
        FastStaticString::from(&*ptr).into(),
      );
      original_sources.push(original_source)
    }

    (
      externals,
      original_sources.into_boxed_slice(),
      lazy_js_specifiers,
    )
  }
}

pub(crate) fn get<'s, 'i, T>(
  scope: &mut v8::PinScope<'s, 'i>,
  from: v8::Local<'s, v8::Object>,
  key: FastStaticString,
  path: &'static str,
) -> T
where
  v8::Local<'s, v8::Value>: TryInto<T>,
{
  let key = key.v8_string(scope).unwrap();
  from
    .get(scope, key.into())
    .unwrap_or_else(|| panic!("{path} exists"))
    .try_into()
    .unwrap_or_else(|_| panic!("unable to convert"))
}

/// Create an object on the `globalThis` that looks like this:
/// ```ignore
/// globalThis.Deno = {
///   core: {
///     ops: {},
///   },
///   // console from V8
///   console,
///   // wrapper fn to forward message to V8 console
///   callConsole,
/// };
/// ```
pub(crate) fn initialize_deno_core_namespace<'s, 'i>(
  scope: &mut v8::PinScope<'s, 'i>,
  context: v8::Local<'s, v8::Context>,
  init_mode: InitMode,
) {
  let global = context.global(scope);
  let deno_str = DENO.v8_string(scope).unwrap();

  let maybe_deno_obj_val = global.get(scope, deno_str.into());

  // If `Deno.core` is already set up, let's exit early.
  if let Some(deno_obj_val) = maybe_deno_obj_val
    && !deno_obj_val.is_undefined()
  {
    return;
  }

  let deno_obj = v8::Object::new(scope);
  let deno_core_key = CORE.v8_string(scope).unwrap();
  // Set up `Deno.core.ops` object
  let deno_core_ops_obj = v8::Object::new(scope);
  let deno_core_ops_key = OPS.v8_string(scope).unwrap();

  let deno_core_obj = v8::Object::new(scope);
  deno_core_obj
    .set(scope, deno_core_ops_key.into(), deno_core_ops_obj.into())
    .unwrap();

  // If we're initializing fresh context set up the console
  if init_mode == InitMode::New {
    // Bind `call_console` to Deno.core.callConsole
    let call_console_fn = v8::Function::new(scope, call_console).unwrap();
    let call_console_key = CALL_CONSOLE.v8_string(scope).unwrap();
    deno_core_obj.set(scope, call_console_key.into(), call_console_fn.into());

    // Bind v8 console object to Deno.core.console
    let extra_binding_obj = context.get_extras_binding_object(scope);
    let console_obj: v8::Local<v8::Object> = get(
      scope,
      extra_binding_obj,
      CONSOLE,
      "ExtrasBindingObject.console",
    );
    let console_key = CONSOLE.v8_string(scope).unwrap();
    deno_core_obj.set(scope, console_key.into(), console_obj.into());
  }

  deno_obj.set(scope, deno_core_key.into(), deno_core_obj.into());
  global.set(scope, deno_str.into(), deno_obj.into());
}

/// Execute `00_primordials.js` and `00_infra.js` that are required for ops
/// to function properly
pub(crate) fn initialize_primordials_and_infra(
  scope: &mut v8::PinScope,
) -> Result<(), CoreError> {
  for source_file in &CONTEXT_SETUP_SOURCES {
    let name = source_file.specifier.v8_string(scope).unwrap();
    let source = source_file.source.v8_string(scope).unwrap();

    let origin = crate::modules::script_origin(scope, name, false, None);
    // TODO(bartlomieju): these two calls will panic if there's any problem in the JS code
    let script = v8::Script::compile(scope, source, Some(&origin))
      .ok_or(CoreModuleParseError(source_file.specifier))?;
    script
      .run(scope)
      .ok_or(CoreModuleExecuteError(source_file.specifier))?;
  }

  Ok(())
}

fn name_key<'s, 'i>(
  scope: &mut v8::PinScope<'s, 'i>,
  decl: &OpDecl,
) -> v8::Local<'s, v8::Name> {
  let key_str = decl.name_fast.v8_string(scope).unwrap();
  if decl.symbol_for {
    v8::Symbol::for_key(scope, key_str).into()
  } else {
    key_str.into()
  }
}

/// Set up JavaScript bindings for ops.
pub(crate) fn initialize_deno_core_ops_bindings<'s, 'i>(
  scope: &mut v8::PinScope<'s, 'i>,
  context: v8::Local<'s, v8::Context>,
  op_ctxs: &[OpCtx],
  op_method_decls: &[OpMethodDecl],
  methods_ctx_offset: usize,
  fn_template_store: &mut FunctionTemplateData,
  will_snapshot: bool,
) {
  let global = context.global(scope);

  // Set up JavaScript bindings for the defined op - this will insert proper
  // `v8::Function` into `Deno.core.ops` object. For async ops, there a bit
  // more machinery involved, see comment below.
  let deno_obj = get(scope, global, DENO, "Deno");
  let deno_core_obj = get(scope, deno_obj, CORE, "Deno.core");
  let deno_core_ops_obj: v8::Local<v8::Object> =
    get(scope, deno_core_obj, OPS, "Deno.core.ops");

  let set_up_async_stub_fn: v8::Local<v8::Function> = get(
    scope,
    deno_core_obj,
    SET_UP_ASYNC_STUB,
    "Deno.core.setUpAsyncStub",
  );

  let prototype_key = v8::String::new(scope, "prototype").unwrap();
  let undefined = v8::undefined(scope);
  let mut index = 0;

  for decl in op_method_decls {
    if index == methods_ctx_offset {
      break;
    }

    let tmpl = if decl.constructor.is_some() {
      let constructor_ctx = &op_ctxs[index];

      let tmpl = op_ctx_template(
        scope,
        constructor_ctx,
        v8::ConstructorBehavior::Allow,
        will_snapshot,
      );

      index += 1;

      tmpl
    } else {
      crate::cppgc::make_cppgc_template(scope)
    };

    let key = decl.name.1.v8_string(scope).unwrap();

    let method_ctxs = &op_ctxs[index..index + decl.methods.len()];
    let accessor_store = create_accessor_store(method_ctxs);

    if !will_snapshot {
      let prototype = tmpl.prototype_template(scope);
      for method in method_ctxs.iter() {
        // Skip async methods, we are going to register them later.
        if method.decl.is_async {
          continue;
        }

        op_ctx_template_or_accessor(
          &accessor_store,
          set_up_async_stub_fn,
          scope,
          prototype,
          tmpl,
          method,
          will_snapshot,
        );
      }
    }

    index += decl.methods.len();

    let static_method_ctxs = &op_ctxs[index..index + decl.static_methods.len()];
    if !will_snapshot {
      for method in static_method_ctxs.iter() {
        let op_fn = op_ctx_template(
          scope,
          method,
          v8::ConstructorBehavior::Throw,
          will_snapshot,
        );
        let method_key = name_key(scope, &method.decl);

        tmpl.set(method_key, op_fn.into());
      }
    }

    index += decl.static_methods.len();

    if !will_snapshot {
      let prototype = tmpl.prototype_template(scope);
      // Register async methods at the end since we need to create the template instance.
      for method in method_ctxs.iter() {
        if method.decl.is_async {
          op_ctx_template_or_accessor(
            &accessor_store,
            set_up_async_stub_fn,
            scope,
            prototype,
            tmpl,
            method,
            will_snapshot,
          );
        }
      }
    }

    if let Some(e) = (decl.inherits_type_name)() {
      let parent = fn_template_store.get_raw(e).unwrap();
      tmpl.inherit(v8::Local::new(scope, parent));
    }

    let op_fn = tmpl.get_function(scope).unwrap();
    op_fn.set_name(key);

    if will_snapshot {
      let prototype = op_fn
        .get(scope, prototype_key.into())
        .and_then(|p| v8::Local::<v8::Object>::try_from(p).ok())
        .unwrap();

      for method in method_ctxs.iter() {
        op_ctx_function_or_accessor(
          &accessor_store,
          set_up_async_stub_fn,
          scope,
          prototype,
          op_fn,
          method,
        );
      }

      for method in static_method_ctxs.iter() {
        let method_fn =
          op_ctx_plain_function(scope, method, v8::ConstructorBehavior::Throw);
        let method_key = name_key(scope, &method.decl);

        op_fn.set(scope, method_key.into(), method_fn.into());
      }
    }

    deno_core_ops_obj.set(scope, key.into(), op_fn.into());

    let id = (decl.type_name)().to_string();
    fn_template_store.insert(id, v8::Global::new(scope, tmpl));
  }

  let op_ctxs = &op_ctxs[index..];
  for op_ctx in op_ctxs {
    let constructor_behavior = op_ctx_constructor_behavior(op_ctx);
    let mut op_fn = if will_snapshot && !op_ctx.decl.constructable {
      op_ctx_plain_function(scope, op_ctx, constructor_behavior)
    } else {
      op_ctx_function(scope, op_ctx, constructor_behavior, will_snapshot)
    };
    let key = op_ctx.decl.name_fast.v8_string(scope).unwrap();

    // For async ops we need to set them up, by calling `Deno.core.setUpAsyncStub` -
    // this call will generate an optimized function that binds to the provided
    // op, while keeping track of promises and error remapping.
    if op_ctx.decl.is_async {
      let result = set_up_async_stub_fn
        .call(scope, undefined.into(), &[key.into(), op_fn.into()])
        .unwrap();
      op_fn = result.try_into().unwrap()
    }

    deno_core_ops_obj.set(scope, key.into(), op_fn.into());
  }
}

/// Re-attach fast-call overloads to snapshotted ops after deserialization.
///
/// Fast-call setup creates V8 `Managed<CFunctionWithSignature>` resources,
/// which the V8 14.9 snapshot serializer rejects (see `op_ctx_template`), so
/// the snapshot bakes the slow version of every op function. This pass builds
/// fresh fast-call-equipped functions for each op that declares a `fast_fn`
/// and overwrites:
///
/// 1. the top-level entries in `Deno.core.ops`,
/// 2. instance methods on each cppgc class prototype,
/// 3. static methods on each cppgc class function itself.
///
/// Accessors stay as-is because they're built with plain
/// `FunctionTemplate::build` (no `Managed`) and survived the snapshot.
pub(crate) fn upgrade_snapshotted_ops_with_fast_calls<'s, 'i>(
  scope: &mut v8::PinScope<'s, 'i>,
  // `Deno.core.ops` and `Deno.core.setUpAsyncStub`, passed in directly because
  // `Deno.core` is scrubbed from the public `Deno` after bootstrap and the
  // deferred caller can no longer read them from the global (see
  // `snapshotted_fast_op_refs` / `ensure_fast_ops_upgraded`).
  deno_core_ops_obj: v8::Local<'s, v8::Object>,
  // The captured-bootstrap clone of `Deno.core.ops` (`capturedCore.ops` in
  // 01_core.js) that residual ext modules read ops through. The flat-op `.set`s
  // below replace slots on the ops object, which the shallow clone doesn't see;
  // mirror them here so residual ext modules get the fast functions too.
  // (Method/static-method upgrades mutate the shared class objects that the
  // clone already references, so they need no mirroring.)
  clone_ops_obj: Option<v8::Local<'s, v8::Object>>,
  set_up_async_stub_fn: v8::Local<'s, v8::Function>,
  op_ctxs: &[OpCtx],
  op_method_decls: &[OpMethodDecl],
  methods_ctx_offset: usize,
) {
  let prototype_key = v8::String::new(scope, "prototype").unwrap();
  let undefined = v8::undefined(scope);

  let mut index = 0;
  for decl in op_method_decls {
    if index == methods_ctx_offset {
      break;
    }

    if decl.constructor.is_some() {
      index += 1;
    }

    // Resolve the class function from `Deno.core.ops`; we need it both as the
    // anchor for prototype methods and to receive static methods.
    let class_key = decl.name.1.v8_string(scope).unwrap();
    let class_fn_val = deno_core_ops_obj.get(scope, class_key.into());
    let class_fn =
      class_fn_val.and_then(|v| v8::Local::<v8::Function>::try_from(v).ok());
    let prototype = class_fn.and_then(|f| {
      let p = f.get(scope, prototype_key.into())?;
      v8::Local::<v8::Object>::try_from(p).ok()
    });

    let method_ctxs = &op_ctxs[index..index + decl.methods.len()];
    for method in method_ctxs {
      let needs_upgrade =
        method_needs_fast_call_upgrade(method) && !method.decl.is_accessor();
      if !needs_upgrade {
        continue;
      }
      let Some(prototype) = prototype else { continue };
      let method_fn =
        op_ctx_function(scope, method, v8::ConstructorBehavior::Throw, false);
      let method_key = name_key(scope, &method.decl);
      if method.decl.is_async {
        // `setUpAsyncStub` installs the wrapped fn on `class_fn.prototype`
        // when given the class as the third argument.
        let Some(class_fn) = class_fn else { continue };
        let _ = set_up_async_stub_fn.call(
          scope,
          undefined.into(),
          &[method_key.into(), method_fn.into(), class_fn.into()],
        );
      } else {
        prototype.set(scope, method_key.into(), method_fn.into());
      }
    }
    index += decl.methods.len();

    let static_method_ctxs = &op_ctxs[index..index + decl.static_methods.len()];
    for method in static_method_ctxs {
      if !method_needs_fast_call_upgrade(method) {
        continue;
      }
      let Some(class_fn) = class_fn else { continue };
      let method_fn =
        op_ctx_function(scope, method, v8::ConstructorBehavior::Throw, false);
      let method_key = name_key(scope, &method.decl);
      class_fn.set(scope, method_key.into(), method_fn.into());
    }
    index += decl.static_methods.len();
  }

  for op_ctx in &op_ctxs[methods_ctx_offset..] {
    if !method_needs_fast_call_upgrade(op_ctx) {
      continue;
    }

    let constructor_behavior = op_ctx_constructor_behavior(op_ctx);
    let mut op_fn = op_ctx_function(scope, op_ctx, constructor_behavior, false);
    let key = op_ctx.decl.name_fast.v8_string(scope).unwrap();

    if op_ctx.decl.is_async {
      let result = set_up_async_stub_fn
        .call(scope, undefined.into(), &[key.into(), op_fn.into()])
        .unwrap();
      op_fn = result.try_into().unwrap();
    }

    deno_core_ops_obj.set(scope, key.into(), op_fn.into());
    if let Some(clone_ops_obj) = clone_ops_obj {
      clone_ops_obj.set(scope, key.into(), op_fn.into());
    }
  }
}

/// Run the deferred fast-call op upgrade exactly once, lazily. Called from the
/// residual ext-module loaders (`op_lazy_load_esm` / `op_load_ext_script`)
/// before the loaded module's body captures its op references, so runtime
/// modules see the fast-call overloads. Programs that never load a residual
/// module (e.g. `deno run empty.js`) never trigger it, saving the ~0.9ms pass.
/// No-op during snapshot build / fresh-bind (flag pre-set in `new_inner`).
/// Read `Deno.core.ops` + `Deno.core.setUpAsyncStub` from the global. Valid
/// only while `Deno.core` is still present (during `new_inner`, before the
/// bootstrap scrubs `Deno.core` from the public `Deno`). The deferred upgrade
/// stashes the result (`ContextState::deferred_fast_ops`) for later use.
pub(crate) fn snapshotted_fast_op_refs<'s, 'i>(
  scope: &mut v8::PinScope<'s, 'i>,
  context: v8::Local<'s, v8::Context>,
) -> (v8::Local<'s, v8::Object>, v8::Local<'s, v8::Function>) {
  let global = context.global(scope);
  let deno_obj = get(scope, global, DENO, "Deno");
  let deno_core_obj = get(scope, deno_obj, CORE, "Deno.core");
  let deno_core_ops_obj: v8::Local<v8::Object> =
    get(scope, deno_core_obj, OPS, "Deno.core.ops");
  let set_up_async_stub_fn: v8::Local<v8::Function> = get(
    scope,
    deno_core_obj,
    SET_UP_ASYNC_STUB,
    "Deno.core.setUpAsyncStub",
  );
  (deno_core_ops_obj, set_up_async_stub_fn)
}

pub(crate) fn ensure_fast_ops_upgraded(scope: &mut v8::PinScope) {
  let context_state = JsRealm::state_from_scope(scope);
  if context_state.fast_ops_upgraded.get() {
    return;
  }
  // Set the flag BEFORE running the upgrade so any reentrant residual load
  // during the upgrade (which calls back into JS via setUpAsyncStub) no-ops.
  context_state.fast_ops_upgraded.set(true);
  let refs = context_state.deferred_fast_ops.borrow().clone();
  let Some((ops_global, stub_global)) = refs else {
    return;
  };
  let deno_core_ops_obj = v8::Local::new(scope, &ops_global);
  let set_up_async_stub_fn = v8::Local::new(scope, &stub_global);

  // Residual ext modules read ops through the captured-bootstrap clone of
  // `core.ops` (`capturedCore.ops` in 01_core.js), a shallow copy that the
  // flat-op `.set`s below otherwise wouldn't reach. Resolve `<captured>.core
  // .ops` so the upgrade can mirror the fast functions onto it.
  let module_map = JsRealm::module_map_from(scope);
  let clone_ops_obj = module_map.captured_bootstrap().and_then(|g| {
    let captured = v8::Local::new(scope, &g);
    let captured = v8::Local::<v8::Object>::try_from(captured).ok()?;
    let core_key = CORE.v8_string(scope).unwrap();
    let core = captured.get(scope, core_key.into())?;
    let core = v8::Local::<v8::Object>::try_from(core).ok()?;
    let ops_key = OPS.v8_string(scope).unwrap();
    let ops = core.get(scope, ops_key.into())?;
    v8::Local::<v8::Object>::try_from(ops).ok()
  });

  upgrade_snapshotted_ops_with_fast_calls(
    scope,
    deno_core_ops_obj,
    clone_ops_obj,
    set_up_async_stub_fn,
    &context_state.op_ctxs,
    &context_state.op_method_decls,
    context_state.methods_ctx_offset,
  );
}

fn method_needs_fast_call_upgrade(op_ctx: &OpCtx) -> bool {
  if op_ctx.decl.constructable {
    return false;
  }
  if op_ctx.metrics_enabled() {
    op_ctx.decl.fast_fn_with_metrics.is_some()
  } else {
    op_ctx.decl.fast_fn.is_some()
  }
}

fn op_ctx_constructor_behavior(op_ctx: &OpCtx) -> v8::ConstructorBehavior {
  if op_ctx.decl.constructable {
    v8::ConstructorBehavior::Allow
  } else {
    v8::ConstructorBehavior::Throw
  }
}

fn op_ctx_template_or_accessor<'s, 'i>(
  accessor_store: &AccessorStore,
  set_up_async_stub_fn: v8::Local<'s, v8::Function>,
  scope: &mut v8::PinScope<'s, 'i>,
  tmpl: v8::Local<'s, v8::ObjectTemplate>,
  constructor: v8::Local<'s, v8::FunctionTemplate>,
  op_ctx: &OpCtx,
  will_snapshot: bool,
) {
  if !op_ctx.decl.is_accessor() {
    let op_fn = op_ctx_template(
      scope,
      op_ctx,
      v8::ConstructorBehavior::Throw,
      will_snapshot,
    );
    let method_key = name_key(scope, &op_ctx.decl);
    if op_ctx.decl.is_async {
      let undefined = v8::undefined(scope);
      let op_fn = op_fn.get_function(scope).unwrap();

      let tmpl_fn = constructor.get_function(scope).unwrap();

      let _result = set_up_async_stub_fn
        .call(
          scope,
          undefined.into(),
          &[method_key.into(), op_fn.into(), tmpl_fn.into()],
        )
        .unwrap();

      return;
    }

    tmpl.set(method_key, op_fn.into());

    return;
  }

  let op_ctx_ptr = op_ctx as *const OpCtx as *const c_void;
  let external = v8::External::new(scope, op_ctx_ptr as *mut c_void);

  if let Some((named_getter, named_setter)) =
    accessor_store.get(op_ctx.decl.name)
  {
    let getter_fn = if let Some(getter) = named_getter {
      let getter_raw = if getter.metrics_enabled() {
        getter.decl.slow_fn_with_metrics
      } else {
        getter.decl.slow_fn
      };

      let tmpl = v8::FunctionTemplate::builder_raw(getter_raw)
        .data(external.into())
        .constructor_behavior(v8::ConstructorBehavior::Throw)
        .build(scope);
      let op_fn = tmpl.get_function(scope).unwrap();
      let method_name = format!("get {}", op_ctx.decl.name_fast);
      let method_name = v8::String::new(scope, method_name.as_str()).unwrap();
      op_fn.set_name(method_name);

      Some(tmpl)
    } else {
      None
    };

    let setter_fn = if let Some(setter) = named_setter {
      let setter_raw = if setter.metrics_enabled() {
        setter.decl.slow_fn_with_metrics
      } else {
        setter.decl.slow_fn
      };

      let tmpl = v8::FunctionTemplate::builder_raw(setter_raw)
        .data(external.into())
        .constructor_behavior(v8::ConstructorBehavior::Throw)
        .length(1)
        .build(scope);
      let op_fn = tmpl.get_function(scope).unwrap();
      let method_name = format!("set {}", op_ctx.decl.name_fast);
      let method_name = v8::String::new(scope, method_name.as_str()).unwrap();
      op_fn.set_name(method_name);

      Some(tmpl)
    } else {
      None
    };

    let key = op_ctx.decl.name_fast.v8_string(scope).unwrap();
    tmpl.set_accessor_property(
      key.into(),
      getter_fn,
      setter_fn,
      v8::PropertyAttribute::default(),
    );
  }
}

fn op_ctx_function_or_accessor<'s, 'i>(
  accessor_store: &AccessorStore,
  set_up_async_stub_fn: v8::Local<'s, v8::Function>,
  scope: &mut v8::PinScope<'s, 'i>,
  target: v8::Local<'s, v8::Object>,
  constructor: v8::Local<'s, v8::Function>,
  op_ctx: &OpCtx,
) {
  if !op_ctx.decl.is_accessor() {
    let op_fn =
      op_ctx_plain_function(scope, op_ctx, v8::ConstructorBehavior::Throw);
    let method_key = name_key(scope, &op_ctx.decl);
    if op_ctx.decl.is_async {
      let undefined = v8::undefined(scope);
      let _result = set_up_async_stub_fn
        .call(
          scope,
          undefined.into(),
          &[method_key.into(), op_fn.into(), constructor.into()],
        )
        .unwrap();

      return;
    }

    target.set(scope, method_key.into(), op_fn.into());

    return;
  }

  if let Some((named_getter, named_setter)) =
    accessor_store.get(op_ctx.decl.name)
  {
    let should_define = if let Some(getter) = named_getter {
      std::ptr::eq(*getter, op_ctx)
    } else if let Some(setter) = named_setter {
      std::ptr::eq(*setter, op_ctx)
    } else {
      false
    };
    if !should_define {
      return;
    }

    let getter_fn: v8::Local<v8::Value> = if let Some(getter) = named_getter {
      let op_fn = op_ctx_plain_function_with_length(
        scope,
        getter,
        v8::ConstructorBehavior::Throw,
        0,
      );
      let method_name = format!("get {}", op_ctx.decl.name_fast);
      let method_name = v8::String::new(scope, method_name.as_str()).unwrap();
      op_fn.set_name(method_name);

      op_fn.into()
    } else {
      v8::undefined(scope).into()
    };

    let setter_fn: v8::Local<v8::Value> = if let Some(setter) = named_setter {
      let op_fn = op_ctx_plain_function_with_length(
        scope,
        setter,
        v8::ConstructorBehavior::Throw,
        1,
      );
      let method_name = format!("set {}", op_ctx.decl.name_fast);
      let method_name = v8::String::new(scope, method_name.as_str()).unwrap();
      op_fn.set_name(method_name);

      op_fn.into()
    } else {
      v8::undefined(scope).into()
    };

    let key = op_ctx.decl.name_fast.v8_string(scope).unwrap();
    let mut desc =
      v8::PropertyDescriptor::new_from_get_set(getter_fn, setter_fn);
    desc.set_enumerable(true);
    desc.set_configurable(true);
    target.define_property(scope, key.into(), &desc).unwrap();
  }
}

pub(crate) fn op_ctx_template<'s, 'i>(
  scope: &mut v8::PinScope<'s, 'i>,
  op_ctx: &OpCtx,
  constructor_behaviour: v8::ConstructorBehavior,
  will_snapshot: bool,
) -> v8::Local<'s, v8::FunctionTemplate> {
  let op_ctx_ptr = op_ctx as *const OpCtx as *const c_void;
  let external = v8::External::new(scope, op_ctx_ptr as *mut c_void);

  let (slow_fn, fast_fn) = if op_ctx.metrics_enabled() {
    (
      op_ctx.decl.slow_fn_with_metrics,
      op_ctx.decl.fast_fn_with_metrics,
    )
  } else {
    (op_ctx.decl.slow_fn, op_ctx.decl.fast_fn)
  };

  let builder: v8::FunctionBuilder<v8::FunctionTemplate> =
    v8::FunctionTemplate::builder_raw(slow_fn)
      .data(external.into())
      .constructor_behavior(constructor_behaviour)
      .side_effect_type(if op_ctx.decl.no_side_effects {
        v8::SideEffectType::HasNoSideEffect
      } else {
        v8::SideEffectType::HasSideEffect
      })
      .length(op_ctx.decl.arg_count as i32);

  // V8 14.9 wraps every fast-call overload in a Managed<CFunctionWithSignature>
  // whose external pointer is a process-local ManagedPtrDestructor allocation.
  // Such managed resources are not serializable into a startup snapshot (see
  // v8/src/snapshot/deserializer.cc: "we cannot normally serialize managed
  // resources"), so skip the fast-call setup during snapshot creation. The
  // template is still wired to slow_fn; ops fall back to the slow callback
  // path until a follow-up runtime hook re-attaches the fast overloads after
  // snapshot deserialization.
  let template = if let Some(fast_function) = fast_fn
    && !will_snapshot
    && !op_ctx.decl.constructable
  {
    builder.build_fast(scope, &[fast_function])
  } else {
    builder.build(scope)
  };
  template.set_class_name(op_ctx.decl.name_fast.v8_string(scope).unwrap());

  template
}

fn op_ctx_function<'s, 'i>(
  scope: &mut v8::PinScope<'s, 'i>,
  op_ctx: &OpCtx,
  constructor_behaviour: v8::ConstructorBehavior,
  will_snapshot: bool,
) -> v8::Local<'s, v8::Function> {
  let v8name = op_ctx.decl.name_fast.v8_string(scope).unwrap();
  let template =
    op_ctx_template(scope, op_ctx, constructor_behaviour, will_snapshot);
  let v8fn = template.get_function(scope).unwrap();
  v8fn.set_name(v8name);
  v8fn
}

fn op_ctx_plain_function<'s, 'i>(
  scope: &mut v8::PinScope<'s, 'i>,
  op_ctx: &OpCtx,
  constructor_behaviour: v8::ConstructorBehavior,
) -> v8::Local<'s, v8::Function> {
  op_ctx_plain_function_with_length(
    scope,
    op_ctx,
    constructor_behaviour,
    op_ctx.decl.arg_count as i32,
  )
}

fn op_ctx_plain_function_with_length<'s, 'i>(
  scope: &mut v8::PinScope<'s, 'i>,
  op_ctx: &OpCtx,
  constructor_behaviour: v8::ConstructorBehavior,
  length: i32,
) -> v8::Local<'s, v8::Function> {
  let op_ctx_ptr = op_ctx as *const OpCtx as *const c_void;
  let external = v8::External::new(scope, op_ctx_ptr as *mut c_void);
  let slow_fn = if op_ctx.metrics_enabled() {
    op_ctx.decl.slow_fn_with_metrics
  } else {
    op_ctx.decl.slow_fn
  };

  let v8fn = v8::Function::builder_raw(slow_fn)
    .data(external.into())
    .constructor_behavior(constructor_behaviour)
    .side_effect_type(if op_ctx.decl.no_side_effects {
      v8::SideEffectType::HasNoSideEffect
    } else {
      v8::SideEffectType::HasSideEffect
    })
    .length(length)
    .build(scope)
    .unwrap();
  let v8name = op_ctx.decl.name_fast.v8_string(scope).unwrap();
  v8fn.set_name(v8name);
  v8fn
}

type AccessorStore<'a> =
  HashMap<String, (Option<&'a OpCtx>, Option<&'a OpCtx>)>;

fn create_accessor_store(method_ctxs: &[OpCtx]) -> AccessorStore<'_> {
  let mut store = AccessorStore::new();

  for method in method_ctxs.iter() {
    // Populate all setters first.
    if method.decl.accessor_type == AccessorType::Setter {
      let key = method.decl.name_fast;

      let key_str = key.to_string();
      // There must be a getter for each setter.
      let getter = method_ctxs.iter().find(|m| {
        m.decl.name == key_str && m.decl.accessor_type == AccessorType::Getter
      });

      store.insert(key.to_string(), (getter, Some(method)));
    }
  }

  // Populate getters without setters.
  for method in method_ctxs.iter() {
    if method.decl.accessor_type == AccessorType::Getter {
      let key = method.decl.name_fast.to_string();

      store.entry(key).or_insert((Some(method), None));
    }
  }

  store
}

pub extern "C" fn wasm_async_resolve_promise_callback(
  _isolate: v8::UnsafeRawIsolatePtr,
  context: v8::Local<v8::Context>,
  resolver: v8::Local<v8::PromiseResolver>,
  compilation_result: v8::Local<v8::Value>,
  success: v8::WasmAsyncSuccess,
) {
  // SAFETY: `CallbackScope` can be safely constructed from `Local<Context>`
  v8::callback_scope!(unsafe scope, context);
  if success == v8::WasmAsyncSuccess::Success {
    resolver.resolve(scope, compilation_result).unwrap();
  } else {
    resolver.reject(scope, compilation_result).unwrap();
  }
}

#[allow(
  clippy::unnecessary_wraps,
  reason = "signature required by v8 callback API"
)]
pub fn host_import_module_dynamically_callback<'s, 'i>(
  scope: &mut v8::PinScope<'s, 'i>,
  host_defined_options: v8::Local<'s, v8::Data>,
  resource_name: v8::Local<'s, v8::Value>,
  specifier: v8::Local<'s, v8::String>,
  import_attributes: v8::Local<'s, v8::FixedArray>,
) -> Option<v8::Local<'s, v8::Promise>> {
  host_import_module_with_phase_dynamically_callback(
    scope,
    host_defined_options,
    resource_name,
    specifier,
    v8::ModuleImportPhase::kEvaluation,
    import_attributes,
  )
}

#[allow(
  clippy::unnecessary_wraps,
  reason = "signature required by v8 callback API"
)]
pub fn host_import_module_with_phase_dynamically_callback<'s, 'i>(
  scope: &mut v8::PinScope<'s, 'i>,
  host_defined_options: v8::Local<'s, v8::Data>,
  resource_name: v8::Local<'s, v8::Value>,
  specifier: v8::Local<'s, v8::String>,
  phase: v8::ModuleImportPhase,
  import_attributes: v8::Local<'s, v8::FixedArray>,
) -> Option<v8::Local<'s, v8::Promise>> {
  let host_defined_options_kind =
    crate::runtime::host_defined_options::read_host_defined_options_kind(
      scope,
      host_defined_options,
    );

  // Scripts compiled by `node:vm` without an `importModuleDynamically`
  // callback tag their host-defined options so that dynamic `import()` at
  // runtime rejects with `ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING` instead
  // of falling through to the embedder's module loader (which would let
  // the sandboxed script reach arbitrary modules).
  if matches!(
    host_defined_options_kind,
    Some(
      crate::runtime::host_defined_options::host_defined_options_kind::VM_DYNAMIC_IMPORT_MISSING,
    )
  ) {
    let resolver = v8::PromiseResolver::new(scope).unwrap();
    let promise = resolver.get_promise(scope);
    let exception = vm_dynamic_import_callback_missing_exception(scope);
    resolver.reject(scope, exception);
    return Some(promise);
  }

  if matches!(
    host_defined_options_kind,
    Some(
      crate::runtime::host_defined_options::host_defined_options_kind::VM_DYNAMIC_IMPORT_CALLBACK,
    )
  ) {
    return Some(host_import_module_with_vm_dynamic_import_callback(
      scope,
      host_defined_options,
      specifier,
      import_attributes,
    ));
  }

  // NOTE(bartlomieju): will crash for non-UTF-8 specifier
  let specifier_str = specifier
    .to_string(scope)
    .unwrap()
    .to_rust_string_lossy(scope);

  let phase = match phase {
    v8::ModuleImportPhase::kEvaluation => ModuleImportPhase::Evaluation,
    v8::ModuleImportPhase::kSource => ModuleImportPhase::Source,
    v8::ModuleImportPhase::kDefer => ModuleImportPhase::Defer,
  };

  let cped = scope.get_continuation_preserved_embedder_data();
  let referrer_name_str = resource_name
    .to_string(scope)
    .unwrap()
    .to_rust_string_lossy(scope);

  let resolver = v8::PromiseResolver::new(scope).unwrap();
  let promise = resolver.get_promise(scope);

  let assertions = parse_import_attributes(
    scope,
    import_attributes,
    ImportAttributesKind::DynamicImport,
  );

  v8::tc_scope!(let tc_scope, scope);

  {
    {
      let state = JsRuntime::state_from(tc_scope);
      if let Some(validate_import_attributes_cb) =
        &state.validate_import_attributes_cb
      {
        // Dynamic imports don't expose a source offset, so line/column are
        // unavailable here.
        let context = crate::modules::ImportAttributesContext {
          referrer: referrer_name_str.clone(),
          specifier: specifier_str.clone(),
          line_number: None,
          column_number: None,
        };
        (validate_import_attributes_cb)(tc_scope, &assertions, &context);
      }
    }

    if tc_scope.has_caught() {
      let e = tc_scope.exception().unwrap();
      resolver.reject(tc_scope, e);
    }
  }
  let requested_module_type =
    get_requested_module_type_from_attributes(&assertions);

  let resolver_handle = v8::Global::new(tc_scope, resolver);
  let cped_handle = v8::Global::new(tc_scope, cped);
  {
    let state = JsRuntime::state_from(tc_scope);
    let module_map_rc = JsRealm::module_map_from(tc_scope);

    if !ModuleMap::load_dynamic_import(
      module_map_rc,
      tc_scope,
      specifier_str,
      referrer_name_str,
      requested_module_type,
      phase,
      resolver_handle,
      cped_handle,
    ) {
      // Short-circuit if the module is already cached and we know it won't error.
      return Some(promise);
    }

    state.notify_new_dynamic_import();
  }
  // Map errors from module resolution (not JS errors from module execution) to
  // ones rethrown from this scope, so they include the call stack of the
  // dynamic import site. Error objects without any stack frames are assumed to
  // be module resolution errors, other exception values are left as they are.
  let builder = v8::FunctionBuilder::new(catch_dynamic_import_promise_error);

  let map_err =
    v8::FunctionBuilder::<v8::Function>::build(builder, tc_scope).unwrap();

  let Some(promise_new) = promise.catch(tc_scope, map_err) else {
    if tc_scope.has_caught() {
      let e = tc_scope.exception().unwrap();
      resolver.reject(tc_scope, e);
    }

    return Some(promise);
  };

  Some(promise_new)
}

fn host_import_module_with_vm_dynamic_import_callback<'s, 'i>(
  scope: &mut v8::PinScope<'s, 'i>,
  host_defined_options: v8::Local<'s, v8::Data>,
  specifier: v8::Local<'s, v8::String>,
  import_attributes: v8::Local<'s, v8::FixedArray>,
) -> v8::Local<'s, v8::Promise> {
  let resolver = v8::PromiseResolver::new(scope).unwrap();
  let promise = resolver.get_promise(scope);

  let Some(callback_id) =
    crate::runtime::host_defined_options::read_host_defined_options_key(
      scope,
      host_defined_options,
    )
  else {
    let exception = vm_dynamic_import_callback_missing_exception(scope);
    resolver.reject(scope, exception);
    return promise;
  };

  let assertions = parse_import_attributes(
    scope,
    import_attributes,
    ImportAttributesKind::DynamicImport,
  );
  let attributes_obj = v8::Object::new(scope);
  for (key, value) in assertions {
    let key = v8::String::new(scope, &key).unwrap();
    let value = v8::String::new(scope, &value).unwrap();
    attributes_obj.create_data_property(scope, key.into(), value.into());
  }

  v8::tc_scope!(let tc_scope, scope);
  let callback = {
    let state = JsRuntime::state_from(tc_scope);
    state
      .vm_dynamic_import_callbacks
      .borrow()
      .get(&callback_id)
      .cloned()
  };

  let Some(callback) = callback else {
    let exception = vm_dynamic_import_callback_missing_exception(tc_scope);
    resolver.reject(tc_scope, exception);
    return promise;
  };

  let callback = v8::Local::new(tc_scope, callback);
  let recv = v8::undefined(tc_scope).into();
  let args = [specifier.into(), attributes_obj.into()];
  match callback.call(tc_scope, recv, &args) {
    Some(value) => {
      resolver.resolve(tc_scope, value);
    }
    None => {
      if tc_scope.has_caught() {
        let exception = tc_scope.exception().unwrap();
        resolver.reject(tc_scope, exception);
      } else {
        let exception = vm_dynamic_import_callback_missing_exception(tc_scope);
        resolver.reject(tc_scope, exception);
      }
    }
  }

  promise
}

/// Build a Node-style `ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING` TypeError.
/// The error code and message string match Node.js exactly so that user
/// code switching on `err.code` behaves the same under Deno.
fn vm_dynamic_import_callback_missing_exception<'s>(
  scope: &mut v8::PinScope<'s, '_>,
) -> v8::Local<'s, v8::Value> {
  let message = v8::String::new_external_onebyte_static(
    scope,
    b"A dynamic import callback was not specified.",
  )
  .unwrap();
  let exception = v8::Exception::type_error(scope, message);
  let code_key =
    v8::String::new_external_onebyte_static(scope, b"code").unwrap();
  let code_val = v8::String::new_external_onebyte_static(
    scope,
    b"ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING",
  )
  .unwrap();
  if let Ok(obj) = v8::Local::<v8::Object>::try_from(exception) {
    obj.set(scope, code_key.into(), code_val.into());
  }
  exception
}

pub extern "C" fn host_initialize_import_meta_object_callback(
  context: v8::Local<v8::Context>,
  module: v8::Local<v8::Module>,
  meta: v8::Local<v8::Object>,
) {
  // SAFETY: `CallbackScope` can be safely constructed from `Local<Context>`
  v8::callback_scope!(unsafe scope, context);
  let module_map = JsRealm::module_map_from(scope);
  let state = JsRealm::state_from_scope(scope);

  let module_global = v8::Global::new(scope, module);
  let Some(name) = module_map.get_name_by_module(&module_global) else {
    // The module is not tracked by deno_core's module map. This is the
    // happy path for `node:vm` `SourceTextModule`, whose v8::Module is
    // created directly by the `node:vm` op and never registered. Delegate
    // to the external hook (set by `node:vm`) if one is registered; if
    // not, just leave `import.meta` empty rather than panicking — V8 will
    // hand the empty object to user code.
    let runtime_state = JsRuntime::state_from(scope);
    if let Some(cb) = runtime_state.external_module_import_meta_cb.get() {
      cb(scope, module, meta);
    }
    return;
  };
  let module_type = module_map
    .get_type_by_module(&module_global)
    .expect("Module not found");

  let url_key = URL.v8_string(scope).unwrap();
  let url_val = v8::String::new(scope, &name).unwrap();
  meta.create_data_property(scope, url_key.into(), url_val.into());

  let main_key = MAIN.v8_string(scope).unwrap();
  let main = module_map.is_main_module(&module_global);
  let main_val = v8::Boolean::new(scope, main);
  meta.create_data_property(scope, main_key.into(), main_val.into());

  // Add special method that allows Wasm module to instantiate themselves.
  if module_type == ModuleType::Wasm {
    let wasm_instance_key = WASM_INSTANCE.v8_string(scope).unwrap();
    match state.wasm_instance_fn.borrow().as_ref() {
      Some(f) => {
        let wasm_instance_val = v8::Local::new(scope, f.clone());
        meta.create_data_property(
          scope,
          wasm_instance_key.into(),
          wasm_instance_val.into(),
        );
      }
      _ => {
        let message = v8::String::new(
          scope,
          "WebAssembly is not available in this environment",
        )
        .unwrap();
        let exception = v8::Exception::error(scope, message);
        scope.throw_exception(exception);
        return;
      }
    }
    // Registry of instance exports keyed by module namespace, used so that
    // Wasm-to-Wasm global imports link against the original
    // `WebAssembly.Global` object rather than the unwrapped snapshot value.
    //
    // Exposed only on `.wasm` modules' `import.meta` (this branch is gated on
    // `ModuleType::Wasm`), since only the generated `.wasm` source reads it.
    // The map itself is a per-realm `WeakMap` shared across all of them; Node
    // keeps the equivalent registry in its translator closure rather than on
    // `import.meta`, but our synthetic source reads it from `import.meta`.
    if let Some(m) = state.wasm_instances_map.borrow().as_ref() {
      let wasm_instances_key = WASM_INSTANCES.v8_string(scope).unwrap();
      let wasm_instances_val = v8::Local::new(scope, m.clone());
      meta.create_data_property(
        scope,
        wasm_instances_key.into(),
        wasm_instances_val.into(),
      );
    }
  }

  let builder =
    v8::FunctionBuilder::new(import_meta_resolve).data(url_val.into());
  let val = v8::FunctionBuilder::<v8::Function>::build(builder, scope).unwrap();
  let resolve_key = RESOLVE.v8_string(scope).unwrap();
  meta.set(scope, resolve_key.into(), val.into());

  maybe_add_import_meta_filename_dirname(scope, meta, &name);

  if name.starts_with("ext:")
    && let Some(proto) = state.ext_import_meta_proto.borrow().clone()
  {
    let prototype = v8::Local::new(scope, proto);
    meta.set_prototype(scope, prototype.into());
  }
}

fn maybe_add_import_meta_filename_dirname<'s, 'i>(
  scope: &mut v8::PinScope<'s, 'i>,
  meta: v8::Local<'s, v8::Object>,
  name: &str,
) {
  // For `file:` URL we provide additional `filename` and `dirname` values
  let Ok(name_url) = Url::parse(name) else {
    return;
  };

  if name_url.scheme() != "file" {
    return;
  }

  // If something goes wrong acquiring a filepath, let skip instead of crashing
  // (mostly concerned about file paths on Windows).
  let Ok(file_path) = deno_path_util::url_to_file_path(&name_url) else {
    return;
  };

  // Use display() here so that Rust takes care of proper forward/backward slash
  // formatting depending on the OS.
  let escaped_filename = file_path.display().to_string();
  let Some(filename_val) = v8::String::new(scope, &escaped_filename) else {
    return;
  };
  let filename_key = FILENAME.v8_string(scope).unwrap();
  meta.create_data_property(scope, filename_key.into(), filename_val.into());

  let dir_path = file_path
    .parent()
    .map(|p| p.to_owned())
    .unwrap_or_else(|| PathBuf::from("/"));
  let escaped_dirname = dir_path.display().to_string();
  let Some(dirname_val) = v8::String::new(scope, &escaped_dirname) else {
    return;
  };
  let dirname_key = DIRNAME.v8_string(scope).unwrap();
  meta.create_data_property(scope, dirname_key.into(), dirname_val.into());
}

fn import_meta_resolve(
  scope: &mut v8::PinScope,
  args: v8::FunctionCallbackArguments,
  mut rv: v8::ReturnValue,
) {
  if args.length() > 1 {
    return crate::error::throw_js_error_class(
      scope,
      &JsErrorBox::type_error("Invalid arguments"),
    );
  }

  let maybe_arg_str = args.get(0).to_string(scope);
  if maybe_arg_str.is_none() {
    return crate::error::throw_js_error_class(
      scope,
      &JsErrorBox::type_error("Invalid arguments"),
    );
  }
  let specifier = maybe_arg_str.unwrap();
  let mut referrer_buf: [std::mem::MaybeUninit<u8>; 1024] =
    [std::mem::MaybeUninit::uninit(); 1024];
  let referrer = {
    let url_prop: v8::Local<v8::String> = args.data().try_cast().unwrap();
    url_prop.to_rust_cow_lossy(scope, &mut referrer_buf)
  };
  let module_map_rc = JsRealm::module_map_from(scope);
  let loader = module_map_rc.loader.clone();
  let mut specifier_buf: [std::mem::MaybeUninit<u8>; 1024] =
    [std::mem::MaybeUninit::uninit(); 1024];
  let specifier_str = specifier.to_rust_cow_lossy(scope, &mut specifier_buf);

  let import_meta_resolve_result = {
    let loader = loader.borrow();
    let loader = loader.as_ref();
    loader.import_meta_resolve(&specifier_str, &referrer)
  };

  match import_meta_resolve_result {
    Ok(resolved) => {
      let resolved_val = serde_v8::to_v8(scope, resolved.as_str()).unwrap();
      rv.set(resolved_val);
    }
    Err(err) => {
      // Per the HTML spec, `import.meta.resolve()` throws a `TypeError` when
      // the specifier can't be resolved, whatever the underlying loader error
      // class is (e.g. a URL parse error surfaces as a `URIError`). Coerce to
      // `TypeError` to match the spec. This used to happen implicitly because
      // `throw_js_error_class` always built a `TypeError`; now that it faithfully
      // rebuilds the registered class, the coercion has to be explicit.
      crate::error::throw_js_error_class(
        scope,
        &JsErrorBox::type_error(err.get_message().into_owned()),
      );
    }
  };
}

pub(crate) fn op_disabled_fn(
  scope: &mut v8::PinScope,
  _args: v8::FunctionCallbackArguments,
  _rv: v8::ReturnValue,
) {
  let message = v8::String::new(scope, "op is disabled").unwrap();
  let exception = v8::Exception::error(scope, message);
  scope.throw_exception(exception);
}

fn catch_dynamic_import_promise_error<'s, 'i>(
  scope: &mut v8::PinScope<'s, 'i>,
  args: v8::FunctionCallbackArguments<'s>,
  _rv: v8::ReturnValue,
) {
  let arg = args.get(0);
  if is_instance_of_error(scope, arg) {
    let e: crate::error::NativeJsError =
      serde_v8::from_v8(scope, arg).unwrap_or_default();
    let name = e.name.unwrap_or_else(|| {
      deno_error::builtin_classes::GENERIC_ERROR.to_string()
    });
    if !has_call_site(scope, arg) {
      let msg = v8::Exception::create_message(scope, arg);
      let arg: v8::Local<v8::Object> = arg.try_into().unwrap();
      let message_key = MESSAGE.v8_string(scope).unwrap();
      let message = arg.get(scope, message_key.into()).unwrap();
      let mut message: v8::Local<v8::String> = message.try_into().unwrap();
      if let Some(stack_frame) = JsStackFrame::from_v8_message(scope, msg)
        && let Some(location) = stack_frame.maybe_format_location()
      {
        let str =
          format!("{} at {location}", message.to_rust_string_lossy(scope));
        message = v8::String::new(scope, &str).unwrap();
      }
      let exception = match name.as_str() {
        deno_error::builtin_classes::RANGE_ERROR => {
          v8::Exception::range_error(scope, message)
        }
        deno_error::builtin_classes::TYPE_ERROR => {
          v8::Exception::type_error(scope, message)
        }
        deno_error::builtin_classes::SYNTAX_ERROR => {
          v8::Exception::syntax_error(scope, message)
        }
        deno_error::builtin_classes::REFERENCE_ERROR => {
          v8::Exception::reference_error(scope, message)
        }
        _ => v8::Exception::error(scope, message),
      };
      // V8 module linking errors (e.g. a missing named export) are
      // spec-defined `SyntaxError`s and must not carry a host-defined `code`.
      // Only genuine resolution/loading failures get `ERR_MODULE_NOT_FOUND`.
      if name.as_str() != deno_error::builtin_classes::SYNTAX_ERROR {
        let code_key = CODE.v8_string(scope).unwrap();
        let code_value = ERR_MODULE_NOT_FOUND.v8_string(scope).unwrap();
        let exception_obj = exception.to_object(scope).unwrap();
        exception_obj.set(scope, code_key.into(), code_value.into());
      }
      scope.throw_exception(exception);
      return;
    }
  }
  scope.throw_exception(arg);
}

pub extern "C" fn promise_reject_callback(message: v8::PromiseRejectMessage) {
  // SAFETY: `CallbackScope` can be safely constructed from `&PromiseRejectMessage`
  v8::callback_scope!(unsafe scope, &message);

  let exception_state = JsRealm::exception_state_from_scope(scope);
  exception_state.track_promise_rejection(
    scope,
    message.get_promise(),
    message.get_event(),
    message.get_value(),
  );

  // Set hasRejectionToWarn in the shared tick_info buffer so that JS
  // runNextTicks() enters processTicksAndRejections() even when no
  // ticks are queued (matching Node.js behavior).
  if message.get_event() == v8::PromiseRejectEvent::PromiseRejectWithNoHandler {
    let context_state = JsRealm::state_from_scope(scope);
    // SAFETY: We're in a single-threaded environment so if we're here
    // we're guaranteed that JS will not change this value.
    unsafe {
      let ptr = context_state.tick_info.as_ptr() as *mut u8;
      *ptr.add(1) = 1;
    }
  }
}

/// This binding should be used if there's a custom console implementation
/// available. Using it will make sure that proper stack frames are displayed
/// in the inspector console.
///
/// Each method on console object should be bound to this function, eg:
/// ```ignore
/// function wrapConsole(consoleFromDeno, consoleFromV8) {
///   const callConsole = core.callConsole;
///
///   for (const key of Object.keys(consoleFromV8)) {
///     if (consoleFromDeno.hasOwnProperty(key)) {
///       consoleFromDeno[key] = callConsole.bind(
///         consoleFromDeno,
///         consoleFromV8[key],
///         consoleFromDeno[key],
///       );
///     }
///   }
/// }
/// ```
///
/// Inspired by:
/// https://github.com/nodejs/node/blob/1317252dfe8824fd9cfee125d2aaa94004db2f3b/src/inspector_js_api.cc#L194-L222
fn call_console(
  scope: &mut v8::PinScope,
  args: v8::FunctionCallbackArguments,
  _rv: v8::ReturnValue,
) {
  if args.length() < 2
    || !args.get(0).is_function()
    || !args.get(1).is_function()
  {
    return crate::error::throw_js_error_class(
      scope,
      &JsErrorBox::type_error("Invalid arguments"),
    );
  }

  let mut call_args = vec![];
  for i in 2..args.length() {
    call_args.push(args.get(i));
  }

  let receiver = args.this();
  let inspector_console_method =
    v8::Local::<v8::Function>::try_from(args.get(0)).unwrap();
  let deno_console_method =
    v8::Local::<v8::Function>::try_from(args.get(1)).unwrap();

  inspector_console_method.call(scope, receiver.into(), &call_args);
  deno_console_method.call(scope, receiver.into(), &call_args);
}

fn cast_closure<F>(f: F) -> F
where
  F: for<'a, 'b> Fn(
      &mut v8::PinScope<'a, 'b>,
      v8::FunctionCallbackArguments<'a>,
      v8::ReturnValue<'a>,
    ) + 'static,
{
  f
}

/// Wrap a promise with `then` handlers allowing us to watch the resolution progress from a Rust closure.
/// This has a side-effect of preventing unhandled rejection handlers from triggering. If that is
/// desired, the final handler may choose to rethrow the exception.
pub(crate) fn watch_promise<'s, 'i, F>(
  scope: &mut v8::PinScope<'s, 'i>,
  promise: v8::Local<'s, v8::Promise>,
  f: F,
) -> Option<v8::Local<'s, v8::Promise>>
where
  F: for<'a, 'b> FnOnce(
      &mut v8::PinScope<'a, 'b>,
      v8::ReturnValue<'a>,
      Result<v8::Local<'a, v8::Value>, v8::Local<'a, v8::Value>>,
    ) + 'static,
{
  let external =
    v8::External::new(scope, Box::into_raw(Box::new(Some(f))) as _);

  fn get_handler<F>(external: v8::Local<v8::External>) -> F {
    unsafe { Box::<Option<F>>::from_raw(external.value() as _) }
      .take()
      .unwrap()
  }

  let on_fulfilled = v8::Function::builder(cast_closure(
    |scope: &mut v8::PinScope,
     args: v8::FunctionCallbackArguments,
     rv: v8::ReturnValue| {
      let data = v8::Local::<v8::External>::try_from(args.data()).unwrap();
      let f = get_handler::<F>(data);
      f(scope, rv, Ok(args.get(0)));
    },
  ))
  .data(external.into())
  .build(scope);

  let on_rejected = v8::Function::builder(cast_closure(
    |scope: &mut v8::PinScope,
     args: v8::FunctionCallbackArguments,
     rv: v8::ReturnValue| {
      let data = v8::Local::<v8::External>::try_from(args.data()).unwrap();
      let f = get_handler::<F>(data);
      f(scope, rv, Err(args.get(0)));
    },
  ))
  .data(external.into())
  .build(scope);

  // function builders will return None if the runtime is shutting down
  let (Some(on_fulfilled), Some(on_rejected)) = (on_fulfilled, on_rejected)
  else {
    _ = get_handler::<F>(external);
    return None;
  };

  // then2 will return None if the runtime is shutting down
  let Some(promise) = promise.then2(scope, on_fulfilled, on_rejected) else {
    _ = get_handler::<F>(external);
    return None;
  };

  Some(promise)
}

/// This function generates a list of tuples, that are a mapping of `<op_name>`
/// to a JavaScript function that executes and op.
pub fn create_exports_for_ops_virtual_module<'s, 'i>(
  op_ctxs: &[OpCtx],
  op_method_decls: &[OpMethodDecl],
  methods_ctx_offset: usize,
  scope: &mut v8::PinScope<'s, 'i>,
  global: v8::Local<'s, v8::Object>,
) -> Vec<(FastStaticString, v8::Local<'s, v8::Value>)> {
  let mut exports = Vec::with_capacity(op_ctxs.len());

  let deno_obj = get(scope, global, DENO, "Deno");
  let deno_core_obj = get(scope, deno_obj, CORE, "Deno.core");
  let ops_obj = get(scope, deno_core_obj, OPS, "Deno.core.ops");

  let mut index = 0;

  for decl in op_method_decls {
    if index == methods_ctx_offset {
      break;
    }

    if decl.constructor.is_some() {
      index += 1;
    }

    let name = decl.name.1;
    let op_fn = get(scope, ops_obj, name, "op");
    exports.push((name, op_fn));

    index += decl.methods.len() + decl.static_methods.len();
  }

  let op_ctxs = &op_ctxs[index..];
  for op_ctx in op_ctxs {
    let op_fn = get(scope, ops_obj, op_ctx.decl.name_fast, "op");
    exports.push((op_ctx.decl.name_fast, op_fn));
  }

  exports
}
