// 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/containers/flat_map.h"
#include "base/containers/map_util.h"
#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_info.h"     // nogncheck
#include "content/browser/service_worker/service_worker_version.h"  // nogncheck
#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/handle.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"

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*
auto& GetVersionIdMap() {
  using Map = base::flat_map<ServiceWorkerKey, ServiceWorkerMain*>;
  static base::NoDestructor<Map> instance;
  return *instance;
}

ServiceWorkerMain* FromServiceWorkerKey(const ServiceWorkerKey& key) {
  return base::FindPtrOrNull(GetVersionIdMap(), key);
}

// static
ServiceWorkerMain* ServiceWorkerMain::FromVersionID(
    std::string browser_context_id,
    content::StoragePartitionConfig storage_partition_config,
    int64_t version_id) {
  const ServiceWorkerKey key{std::move(browser_context_id),
                             std::move(storage_partition_config), version_id};
  return FromServiceWorkerKey(key);
}

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

ServiceWorkerMain::ServiceWorkerMain(content::ServiceWorkerContext* sw_context,
                                     int64_t version_id,
                                     ServiceWorkerKey key)
    : version_id_{version_id},
      key_{std::move(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;
}

GURL ServiceWorkerMain::ScriptURL() const {
  if (version_destroyed_)
    return {};
  return version_info()->script_url;
}

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

// static
gin_helper::Handle<ServiceWorkerMain> ServiceWorkerMain::From(
    v8::Isolate* isolate,
    content::ServiceWorkerContext* sw_context,
    std::string browser_context_id,
    content::StoragePartitionConfig storage_partition_config,
    int64_t version_id) {
  ServiceWorkerKey service_worker_key{std::move(browser_context_id),
                                      std::move(storage_partition_config),
                                      version_id};

  if (auto* service_worker = FromServiceWorkerKey(service_worker_key))
    return gin_helper::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_helper::Handle<ServiceWorkerMain>();
  }

  auto handle = gin_helper::CreateHandle(
      isolate, new ServiceWorkerMain{sw_context, version_id,
                                     std::move(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)
      .SetProperty("scriptURL", &ServiceWorkerMain::ScriptURL)
      .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* const isolate = electron::JavascriptEnvironment::GetIsolate();
  gin_helper::Dictionary dict{isolate, exports};
  dict.Set("ServiceWorkerMain",
           ServiceWorkerMain::GetConstructor(isolate, context));
}

}  // namespace

NODE_LINKED_BINDING_CONTEXT_AWARE(electron_browser_service_worker_main,
                                  Initialize)
