// 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/browser/api/electron_api_service_worker_main.h"

#include <string>
#include <utility>

#include "base/logging.h"
#include "base/no_destructor.h"
#include "content/browser/service_worker/service_worker_context_wrapper.h"  // nogncheck
#include "content/browser/service_worker/service_worker_version.h"  // nogncheck
#include "gin/handle.h"
#include "gin/object_template_builder.h"
#include "services/service_manager/public/cpp/interface_provider.h"
#include "shell/browser/api/message_port.h"
#include "shell/browser/browser.h"
#include "shell/browser/javascript_environment.h"
#include "shell/common/api/api.mojom.h"
#include "shell/common/gin_converters/blink_converter.h"
#include "shell/common/gin_converters/gurl_converter.h"
#include "shell/common/gin_converters/value_converter.h"
#include "shell/common/gin_helper/dictionary.h"
#include "shell/common/gin_helper/error_thrower.h"
#include "shell/common/gin_helper/object_template_builder.h"
#include "shell/common/gin_helper/promise.h"
#include "shell/common/node_includes.h"
#include "shell/common/v8_util.h"
#include "third_party/abseil-cpp/absl/container/flat_hash_map.h"

namespace {

// Use private API to get the live version of the service worker. This will
// exist while in starting, stopping, or stopped running status.
content::ServiceWorkerVersion* GetLiveVersion(
    content::ServiceWorkerContext* service_worker_context,
    int64_t version_id) {
  auto* wrapper = static_cast<content::ServiceWorkerContextWrapper*>(
      service_worker_context);
  return wrapper->GetLiveVersion(version_id);
}

// Get a public ServiceWorkerVersionBaseInfo object directly from the service
// worker.
std::optional<content::ServiceWorkerVersionBaseInfo> GetLiveVersionInfo(
    content::ServiceWorkerContext* service_worker_context,
    int64_t version_id) {
  auto* version = GetLiveVersion(service_worker_context, version_id);
  if (version) {
    return version->GetInfo();
  }
  return std::nullopt;
}

}  // namespace

namespace electron::api {

// ServiceWorkerKey -> ServiceWorkerMain*
using VersionIdMap = absl::flat_hash_map<ServiceWorkerKey,
                                         ServiceWorkerMain*,
                                         ServiceWorkerKey::Hasher>;

VersionIdMap& GetVersionIdMap() {
  static base::NoDestructor<VersionIdMap> instance;
  return *instance;
}

ServiceWorkerMain* FromServiceWorkerKey(const ServiceWorkerKey& key) {
  VersionIdMap& version_map = GetVersionIdMap();
  auto iter = version_map.find(key);
  auto* service_worker = iter == version_map.end() ? nullptr : iter->second;
  return service_worker;
}

// static
ServiceWorkerMain* ServiceWorkerMain::FromVersionID(
    int64_t version_id,
    const content::StoragePartition* storage_partition) {
  ServiceWorkerKey key(version_id, storage_partition);
  return FromServiceWorkerKey(key);
}

gin::WrapperInfo ServiceWorkerMain::kWrapperInfo = {gin::kEmbedderNativeGin};

ServiceWorkerMain::ServiceWorkerMain(content::ServiceWorkerContext* sw_context,
                                     int64_t version_id,
                                     const ServiceWorkerKey& key)
    : version_id_(version_id), key_(key), service_worker_context_(sw_context) {
  GetVersionIdMap().emplace(key_, this);
  InvalidateVersionInfo();
}

ServiceWorkerMain::~ServiceWorkerMain() {
  Destroy();
}

void ServiceWorkerMain::Destroy() {
  version_destroyed_ = true;
  InvalidateVersionInfo();
  MaybeDisconnectRemote();
  GetVersionIdMap().erase(key_);
  Unpin();
}

void ServiceWorkerMain::MaybeDisconnectRemote() {
  if (remote_.is_bound() &&
      (version_destroyed_ ||
       (!service_worker_context_->IsLiveStartingServiceWorker(version_id_) &&
        !service_worker_context_->IsLiveRunningServiceWorker(version_id_)))) {
    remote_.reset();
  }
}

mojom::ElectronRenderer* ServiceWorkerMain::GetRendererApi() {
  if (!remote_.is_bound()) {
    if (!service_worker_context_->IsLiveRunningServiceWorker(version_id_)) {
      return nullptr;
    }

    service_worker_context_->GetRemoteAssociatedInterfaces(version_id_)
        .GetInterface(&remote_);
  }
  return remote_.get();
}

void ServiceWorkerMain::Send(v8::Isolate* isolate,
                             bool internal,
                             const std::string& channel,
                             v8::Local<v8::Value> args) {
  blink::CloneableMessage message;
  if (!gin::ConvertFromV8(isolate, args, &message)) {
    isolate->ThrowException(v8::Exception::Error(
        gin::StringToV8(isolate, "Failed to serialize arguments")));
    return;
  }

  auto* renderer_api_remote = GetRendererApi();
  if (!renderer_api_remote) {
    return;
  }

  renderer_api_remote->Message(internal, channel, std::move(message));
}

void ServiceWorkerMain::InvalidateVersionInfo() {
  if (version_info_ != nullptr) {
    version_info_.reset();
  }

  if (version_destroyed_)
    return;

  auto version_info = GetLiveVersionInfo(service_worker_context_, version_id_);
  if (version_info) {
    version_info_ =
        std::make_unique<content::ServiceWorkerVersionBaseInfo>(*version_info);
  } else {
    // When ServiceWorkerContextCore::RemoveLiveVersion is called, it posts a
    // task to notify that the service worker has stopped. At this point, the
    // live version will no longer exist.
    Destroy();
  }
}

void ServiceWorkerMain::OnRunningStatusChanged(
    blink::EmbeddedWorkerStatus running_status) {
  // Disconnect remote when content::ServiceWorkerHost has terminated.
  MaybeDisconnectRemote();

  InvalidateVersionInfo();

  // Redundant worker has been marked for deletion. Now that it's stopped, let's
  // destroy our wrapper.
  if (redundant_ && running_status == blink::EmbeddedWorkerStatus::kStopped) {
    Destroy();
  }
}

void ServiceWorkerMain::OnVersionRedundant() {
  // Redundant service workers have been either unregistered or replaced. A new
  // ServiceWorkerMain will need to be created.
  // Set internal state to mark it for deletion once it has fully stopped.
  redundant_ = true;
}

bool ServiceWorkerMain::IsDestroyed() const {
  return version_destroyed_;
}

const blink::StorageKey ServiceWorkerMain::GetStorageKey() {
  const GURL& scope = version_info_ ? version_info()->scope : GURL::EmptyGURL();
  return blink::StorageKey::CreateFirstParty(url::Origin::Create(scope));
}

gin_helper::Dictionary ServiceWorkerMain::StartExternalRequest(
    v8::Isolate* isolate,
    bool has_timeout) {
  auto details = gin_helper::Dictionary::CreateEmpty(isolate);

  if (version_destroyed_) {
    isolate->ThrowException(v8::Exception::TypeError(
        gin::StringToV8(isolate, "ServiceWorkerMain is destroyed")));
    return details;
  }

  auto request_uuid = base::Uuid::GenerateRandomV4();
  auto timeout_type =
      has_timeout
          ? content::ServiceWorkerExternalRequestTimeoutType::kDefault
          : content::ServiceWorkerExternalRequestTimeoutType::kDoesNotTimeout;

  content::ServiceWorkerExternalRequestResult start_result =
      service_worker_context_->StartingExternalRequest(
          version_id_, timeout_type, request_uuid);

  details.Set("id", request_uuid.AsLowercaseString());
  details.Set("ok",
              start_result == content::ServiceWorkerExternalRequestResult::kOk);

  return details;
}

void ServiceWorkerMain::FinishExternalRequest(v8::Isolate* isolate,
                                              std::string uuid) {
  base::Uuid request_uuid = base::Uuid::ParseLowercase(uuid);
  if (!request_uuid.is_valid()) {
    isolate->ThrowException(v8::Exception::TypeError(
        gin::StringToV8(isolate, "Invalid external request UUID")));
    return;
  }

  DCHECK(service_worker_context_);
  if (!service_worker_context_)
    return;

  content::ServiceWorkerExternalRequestResult result =
      service_worker_context_->FinishedExternalRequest(version_id_,
                                                       request_uuid);

  std::string error;
  switch (result) {
    case content::ServiceWorkerExternalRequestResult::kOk:
      break;
    case content::ServiceWorkerExternalRequestResult::kBadRequestId:
      error = "Unknown external request UUID";
      break;
    case content::ServiceWorkerExternalRequestResult::kWorkerNotRunning:
      error = "Service worker is no longer running";
      break;
    case content::ServiceWorkerExternalRequestResult::kWorkerNotFound:
      error = "Service worker was not found";
      break;
    case content::ServiceWorkerExternalRequestResult::kNullContext:
    default:
      error = "Service worker context is unavailable and may be shutting down";
      break;
  }

  if (!error.empty()) {
    isolate->ThrowException(
        v8::Exception::TypeError(gin::StringToV8(isolate, error)));
  }
}

size_t ServiceWorkerMain::CountExternalRequestsForTest() {
  if (version_destroyed_)
    return 0;
  auto& storage_key = GetStorageKey();
  return service_worker_context_->CountExternalRequestsForTest(storage_key);
}

int64_t ServiceWorkerMain::VersionID() const {
  return version_id_;
}

GURL ServiceWorkerMain::ScopeURL() const {
  if (version_destroyed_)
    return {};
  return version_info()->scope;
}

// static
gin::Handle<ServiceWorkerMain> ServiceWorkerMain::New(v8::Isolate* isolate) {
  return gin::Handle<ServiceWorkerMain>();
}

// static
gin::Handle<ServiceWorkerMain> ServiceWorkerMain::From(
    v8::Isolate* isolate,
    content::ServiceWorkerContext* sw_context,
    const content::StoragePartition* storage_partition,
    int64_t version_id) {
  ServiceWorkerKey service_worker_key(version_id, storage_partition);

  auto* service_worker = FromServiceWorkerKey(service_worker_key);
  if (service_worker)
    return gin::CreateHandle(isolate, service_worker);

  // Ensure ServiceWorkerVersion exists and is not redundant (pending deletion)
  auto* live_version = GetLiveVersion(sw_context, version_id);
  if (!live_version || live_version->is_redundant()) {
    return gin::Handle<ServiceWorkerMain>();
  }

  auto handle = gin::CreateHandle(
      isolate,
      new ServiceWorkerMain(sw_context, version_id, service_worker_key));

  // Prevent garbage collection of worker until it has been deleted internally.
  handle->Pin(isolate);

  return handle;
}

// static
void ServiceWorkerMain::FillObjectTemplate(
    v8::Isolate* isolate,
    v8::Local<v8::ObjectTemplate> templ) {
  gin_helper::ObjectTemplateBuilder(isolate, templ)
      .SetMethod("_send", &ServiceWorkerMain::Send)
      .SetMethod("isDestroyed", &ServiceWorkerMain::IsDestroyed)
      .SetMethod("_startExternalRequest",
                 &ServiceWorkerMain::StartExternalRequest)
      .SetMethod("_finishExternalRequest",
                 &ServiceWorkerMain::FinishExternalRequest)
      .SetMethod("_countExternalRequests",
                 &ServiceWorkerMain::CountExternalRequestsForTest)
      .SetProperty("versionId", &ServiceWorkerMain::VersionID)
      .SetProperty("scope", &ServiceWorkerMain::ScopeURL)
      .Build();
}

const char* ServiceWorkerMain::GetTypeName() {
  return GetClassName();
}

}  // namespace electron::api

namespace {

using electron::api::ServiceWorkerMain;

void Initialize(v8::Local<v8::Object> exports,
                v8::Local<v8::Value> unused,
                v8::Local<v8::Context> context,
                void* priv) {
  v8::Isolate* isolate = context->GetIsolate();
  gin_helper::Dictionary dict(isolate, exports);
  dict.Set("ServiceWorkerMain", ServiceWorkerMain::GetConstructor(context));
}

}  // namespace

NODE_LINKED_BINDING_CONTEXT_AWARE(electron_browser_service_worker_main,
                                  Initialize)
