// Copyright (c) 2025 Salesforce, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.

#include "shell/renderer/preload_realm_context.h"

#include "base/command_line.h"
#include "base/containers/span.h"
#include "base/process/process.h"
#include "base/process/process_metrics.h"
#include "mojo/public/cpp/base/big_buffer.h"
#include "shell/common/api/electron_bindings.h"
#include "shell/common/gc_plugin.h"
#include "shell/common/gin_helper/dictionary.h"
#include "shell/common/js2c_bundle_ids.h"
#include "shell/common/node_includes.h"
#include "shell/common/node_util.h"
#include "shell/renderer/preload_utils.h"
#include "shell/renderer/service_worker_data.h"
#include "third_party/blink/renderer/bindings/core/v8/script_controller.h"  // nogncheck
#include "third_party/blink/renderer/core/execution_context/execution_context.h"  // nogncheck
#include "third_party/blink/renderer/core/inspector/worker_thread_debugger.h"  // nogncheck
#include "third_party/blink/renderer/core/shadow_realm/shadow_realm_global_scope.h"  // nogncheck
#include "third_party/blink/renderer/core/workers/worker_or_worklet_global_scope.h"  // nogncheck
#include "third_party/blink/renderer/platform/bindings/script_state.h"  // nogncheck
#include "third_party/blink/renderer/platform/bindings/v8_dom_wrapper.h"  // nogncheck
#include "third_party/blink/renderer/platform/bindings/v8_per_context_data.h"  // nogncheck
#include "third_party/blink/renderer/platform/context_lifecycle_observer.h"  // nogncheck
#include "v8/include/v8-context.h"

namespace electron::preload_realm {

namespace {

static constexpr int kElectronContextEmbedderDataIndex =
    static_cast<int>(gin::kPerContextDataStartIndex) +
    static_cast<int>(gin::kEmbedderElectron);

// This is a helper class to make the initiator ExecutionContext the owner
// of a ShadowRealmGlobalScope and its ScriptState. When the initiator
// ExecutionContext is destroyed, the ShadowRealmGlobalScope is destroyed,
// too.
class PreloadRealmLifetimeController
    : public blink::GarbageCollected<PreloadRealmLifetimeController>,
      public blink::ContextLifecycleObserver {
 public:
  explicit PreloadRealmLifetimeController(
      blink::ExecutionContext* initiator_execution_context,
      blink::ScriptState* initiator_script_state,
      blink::ShadowRealmGlobalScope* shadow_realm_global_scope,
      blink::ScriptState* shadow_realm_script_state,
      electron::ServiceWorkerData* service_worker_data)
      : initiator_script_state_(initiator_script_state),
        is_initiator_worker_or_worklet_(
            initiator_execution_context->IsWorkerOrWorkletGlobalScope()),
        shadow_realm_global_scope_(shadow_realm_global_scope),
        shadow_realm_script_state_(shadow_realm_script_state),
        service_worker_data_(service_worker_data) {
    // Align lifetime of this controller to that of the initiator's context.
    self_ = this;

    SetContextLifecycleNotifier(initiator_execution_context);
    RegisterDebugger(initiator_execution_context);

    initiator_context()->SetAlignedPointerInEmbedderData(
        kElectronContextEmbedderDataIndex, static_cast<void*>(this),
        v8::kEmbedderDataTypeTagDefault);
    realm_context()->SetAlignedPointerInEmbedderData(
        kElectronContextEmbedderDataIndex, static_cast<void*>(this),
        v8::kEmbedderDataTypeTagDefault);

    metrics_ = base::ProcessMetrics::CreateCurrentProcessMetrics();
    RunInitScript();
  }

  static PreloadRealmLifetimeController* From(v8::Local<v8::Context> context) {
    if (context->GetNumberOfEmbedderDataFields() <=
        kElectronContextEmbedderDataIndex) {
      return nullptr;
    }
    auto* controller = static_cast<PreloadRealmLifetimeController*>(
        context->GetAlignedPointerFromEmbedderData(
            kElectronContextEmbedderDataIndex,
            v8::kEmbedderDataTypeTagDefault));
    CHECK(controller);
    return controller;
  }

  void Trace(blink::Visitor* visitor) const override {
    visitor->Trace(initiator_script_state_);
    visitor->Trace(shadow_realm_global_scope_);
    visitor->Trace(shadow_realm_script_state_);
    ContextLifecycleObserver::Trace(visitor);
  }

  v8::MaybeLocal<v8::Context> GetContext() {
    return shadow_realm_script_state_->ContextIsValid()
               ? shadow_realm_script_state_->GetContext()
               : v8::MaybeLocal<v8::Context>();
  }

  v8::MaybeLocal<v8::Context> GetInitiatorContext() {
    return initiator_script_state_->ContextIsValid()
               ? initiator_script_state_->GetContext()
               : v8::MaybeLocal<v8::Context>();
  }

  electron::ServiceWorkerData* service_worker_data() {
    return service_worker_data_;
  }

 protected:
  void ContextDestroyed() override {
    v8::HandleScope handle_scope(realm_isolate());
    realm_context()->SetAlignedPointerInEmbedderData(
        kElectronContextEmbedderDataIndex, nullptr,
        v8::kEmbedderDataTypeTagDefault);

    // See ShadowRealmGlobalScope::ContextDestroyed
    shadow_realm_script_state_->DisposePerContextData();
    if (is_initiator_worker_or_worklet_) {
      shadow_realm_script_state_->DissociateContext();
    }
    shadow_realm_script_state_.Clear();
    shadow_realm_global_scope_->NotifyContextDestroyed();
    shadow_realm_global_scope_.Clear();

    self_.Clear();
  }

 private:
  v8::Isolate* realm_isolate() {
    return shadow_realm_script_state_->GetIsolate();
  }
  v8::Local<v8::Context> realm_context() {
    return shadow_realm_script_state_->GetContext();
  }
  v8::Local<v8::Context> initiator_context() {
    return initiator_script_state_->GetContext();
  }

  void RegisterDebugger(blink::ExecutionContext* initiator_execution_context) {
    v8::Isolate* isolate = realm_isolate();
    v8::Local<v8::Context> context = realm_context();

    blink::WorkerThreadDebugger* debugger =
        blink::WorkerThreadDebugger::From(isolate);
    ;
    const auto* worker_context =
        To<blink::WorkerOrWorkletGlobalScope>(initiator_execution_context);

    // Override path to make preload realm easier to find in debugger.
    blink::KURL url_for_debugger(worker_context->Url());
    url_for_debugger.SetPath("electron-preload-realm");

    debugger->ContextCreated(worker_context->GetThread(), url_for_debugger,
                             context);
  }

  void RunInitScript() {
    v8::Isolate* isolate = realm_isolate();
    v8::Local<v8::Context> context = realm_context();

    v8::Context::Scope context_scope(context);
    v8::MicrotasksScope microtasks_scope(
        isolate, context->GetMicrotaskQueue(),
        v8::MicrotasksScope::kDoNotRunMicrotasks);

    v8::Local<v8::Object> binding = v8::Object::New(isolate);

    gin_helper::Dictionary b(isolate, binding);
    b.SetMethod("get", preload_utils::GetBinding);
    // The browser attached the service-worker preload set + process info to
    // this worker's EmbeddedWorkerStartParams (see
    // ContentBrowserClient::GetServiceWorkerStartupData); Chromium marshalled
    // it onto the worker thread with the rest of the start params, ordered
    // with worker creation. Deserialize and capture it once on the
    // ServiceWorkerData so createPreloadScript can look up preload contents
    // and code caches without marshaling them through V8.
    if (const std::optional<mojo_base::BigBuffer>& blob =
            service_worker_data_->proxy()->ElectronPreloadData()) {
      mojom::RendererStartupDataPtr data;
      if (mojom::RendererStartupData::Deserialize(
              base::span<const uint8_t>(*blob), &data)) {
        service_worker_data_->SetWorkerStartupData(std::move(data));
      }
    }
    // No RenderFrame in the SW preload realm — no per-frame cache lookup or
    // ship-back channel — but contents are still looked up from the captured
    // worker startup data instead of crossing the V8 boundary.
    b.SetMethod(
        "createPreloadScript",
        base::BindRepeating(&preload_utils::CreatePreloadScript, nullptr,
                            base::Unretained(service_worker_data_.get())));

    gin_helper::Dictionary process = gin::Dictionary::CreateEmpty(isolate);
    b.Set("process", process);

    ElectronBindings::BindProcess(isolate, &process, metrics_.get());

    process.SetMethod("uptime", preload_utils::Uptime);
    process.Set("argv", base::CommandLine::ForCurrentProcess()->argv());
    process.SetReadOnly("pid", base::GetCurrentProcId());
    process.SetReadOnly("sandboxed", true);
    process.SetReadOnly("type", "service-worker");
    process.SetReadOnly("contextIsolated", true);

    v8::Local<v8::Value> startup_data;
    if (!preload_utils::BuildStartupData(
             isolate, service_worker_data_->worker_startup_data())
             .ToLocal(&startup_data)) {
      startup_data = v8::Null(isolate);
    }
    b.Set("startupData", startup_data);

    v8::LocalVector<v8::String> preload_realm_bundle_params =
        js2c::MakeBundleParams(isolate, js2c::kPreloadRealmBundleParams);

    v8::LocalVector<v8::Value> preload_realm_bundle_args(isolate, {binding});

    util::CompileAndCall(isolate, context, js2c::kPreloadRealmBundleId,
                         &preload_realm_bundle_params,
                         &preload_realm_bundle_args);
  }

  const blink::WeakMember<blink::ScriptState> initiator_script_state_;
  bool is_initiator_worker_or_worklet_;
  blink::Member<blink::ShadowRealmGlobalScope> shadow_realm_global_scope_;
  blink::Member<blink::ScriptState> shadow_realm_script_state_;

  std::unique_ptr<base::ProcessMetrics> metrics_;
  raw_ptr<ServiceWorkerData> service_worker_data_;

  GC_PLUGIN_IGNORE(
      "Intentional GC root to keep this object alive until the context is "
      "destroyed")
  blink::Persistent<PreloadRealmLifetimeController> self_;
};

}  // namespace

v8::MaybeLocal<v8::Context> GetInitiatorContext(
    v8::Local<v8::Context> context) {
  DCHECK(!context.IsEmpty());
  blink::ExecutionContext* execution_context =
      blink::ExecutionContext::From(context);
  if (!execution_context->IsShadowRealmGlobalScope())
    return v8::MaybeLocal<v8::Context>();
  auto* controller = PreloadRealmLifetimeController::From(context);
  if (controller) {
    return controller->GetInitiatorContext();
  }
  return v8::MaybeLocal<v8::Context>();
}

v8::MaybeLocal<v8::Context> GetPreloadRealmContext(
    v8::Local<v8::Context> context) {
  DCHECK(!context.IsEmpty());
  blink::ExecutionContext* execution_context =
      blink::ExecutionContext::From(context);
  if (!execution_context->IsServiceWorkerGlobalScope())
    return v8::MaybeLocal<v8::Context>();
  auto* controller = PreloadRealmLifetimeController::From(context);
  if (controller)
    return controller->GetContext();
  return v8::MaybeLocal<v8::Context>();
}

electron::ServiceWorkerData* GetServiceWorkerData(
    v8::Local<v8::Context> context) {
  auto* controller = PreloadRealmLifetimeController::From(context);
  return controller ? controller->service_worker_data() : nullptr;
}

void OnCreatePreloadableV8Context(
    v8::Isolate* const isolate,
    v8::Local<v8::Context> initiator_context,
    electron::ServiceWorkerData* service_worker_data) {
  blink::ScriptState* initiator_script_state =
      blink::ScriptState::MaybeFrom(isolate, initiator_context);
  DCHECK(initiator_script_state);
  blink::ExecutionContext* initiator_execution_context =
      blink::ExecutionContext::From(initiator_context);
  DCHECK(initiator_execution_context);
  blink::DOMWrapperWorld* world = blink::DOMWrapperWorld::Create(
      isolate, blink::DOMWrapperWorld::WorldType::kShadowRealm);
  CHECK(world);  // Not yet run out of the world id.

  // Create a new ShadowRealmGlobalScope.
  blink::ShadowRealmGlobalScope* shadow_realm_global_scope =
      blink::MakeGarbageCollected<blink::ShadowRealmGlobalScope>(
          initiator_execution_context);
  const blink::WrapperTypeInfo* wrapper_type_info =
      shadow_realm_global_scope->GetWrapperTypeInfo();

  // Create a new v8::Context.
  // Initialize V8 extensions before creating the context.
  v8::ExtensionConfiguration extension_configuration =
      blink::ScriptController::ExtensionsFor(shadow_realm_global_scope);

  v8::Local<v8::ObjectTemplate> global_template =
      wrapper_type_info->GetV8ClassTemplate(isolate, *world)
          .As<v8::FunctionTemplate>()
          ->InstanceTemplate();
  v8::Local<v8::Object> global_proxy;  // Will request a new global proxy.
  v8::Local<v8::Context> context =
      v8::Context::New(isolate, &extension_configuration, global_template,
                       global_proxy, v8::DeserializeInternalFieldsCallback(),
                       initiator_execution_context->GetMicrotaskQueue());
  context->UseDefaultSecurityToken();

  // Associate the Blink object with the v8::Context.
  blink::ScriptState* script_state =
      blink::ScriptState::Create(context, world, shadow_realm_global_scope);

  // Associate the Blink object with the v8::Objects.
  global_proxy = context->Global();
  blink::V8DOMWrapper::SetNativeInfoForGlobal(isolate, global_proxy,
                                              shadow_realm_global_scope);

  // Install context-dependent properties.
  std::ignore =
      script_state->PerContextData()->ConstructorForType(wrapper_type_info);

  // Make the initiator execution context the owner of the
  // ShadowRealmGlobalScope and the ScriptState.
  blink::MakeGarbageCollected<PreloadRealmLifetimeController>(
      initiator_execution_context, initiator_script_state,
      shadow_realm_global_scope, script_state, service_worker_data);
}

}  // namespace electron::preload_realm
