// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "content/browser/payments/payment_app_provider_impl.h"

#include <map>
#include <sstream>
#include <string>
#include <string_view>
#include <vector>

#include "base/base64.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/ref_counted_memory.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/to_string.h"
#include "base/token.h"
#include "content/browser/devtools/devtools_background_services_context_impl.h"
#include "content/browser/payments/payment_app_context_impl.h"
#include "content/browser/payments/payment_app_installer.h"
#include "content/browser/payments/payment_handler_web_contents_observer.h"
#include "content/browser/service_worker/service_worker_context_wrapper.h"
#include "content/browser/service_worker/service_worker_metrics.h"
#include "content/browser/service_worker/service_worker_version.h"
#include "content/browser/storage_partition_impl.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/devtools_background_services_context.h"
#include "content/public/browser/global_routing_id.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_features.h"
#include "mojo/public/cpp/bindings/message.h"
#include "mojo/public/mojom/base/time.mojom.h"
#include "third_party/blink/public/common/service_worker/service_worker_status_code.h"
#include "third_party/blink/public/common/storage_key/storage_key.h"
#include "third_party/blink/public/mojom/service_worker/service_worker_container_type.mojom.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/gfx/image/image.h"

namespace content {
namespace {

using payments::mojom::CanMakePaymentEventDataPtr;
using payments::mojom::PaymentDetailsModifierPtr;
using payments::mojom::PaymentEventResponseType;
using payments::mojom::PaymentHandlerStatus;
using payments::mojom::PaymentMethodDataPtr;
using payments::mojom::PaymentRequestEventDataPtr;

void AddMethodDataToMap(const std::vector<PaymentMethodDataPtr>& method_data,
                        std::map<std::string, std::string>* out) {
  for (size_t i = 0; i < method_data.size(); ++i) {
    std::string counter =
        method_data.size() == 1 ? "" : " #" + base::NumberToString(i);
    out->emplace("Method Name" + counter, method_data[i]->supported_method);
    out->emplace("Method Data" + counter, method_data[i]->stringified_data);
  }
}

void AddModifiersToMap(const std::vector<PaymentDetailsModifierPtr>& modifiers,
                       std::map<std::string, std::string>* out) {
  for (size_t i = 0; i < modifiers.size(); ++i) {
    std::string prefix =
        "Modifier" +
        (modifiers.size() == 1 ? "" : " #" + base::NumberToString(i));
    out->emplace(prefix + " Method Name",
                 modifiers[i]->method_data->supported_method);
    out->emplace(prefix + " Method Data",
                 modifiers[i]->method_data->stringified_data);
    if (!modifiers[i]->total)
      continue;
    out->emplace(prefix + " Total Currency",
                 modifiers[i]->total->amount->currency);
    out->emplace(prefix + " Total Value", modifiers[i]->total->amount->value);
  }
}

std::string EncodeIcon(const SkBitmap& app_icon) {
  if (app_icon.empty())
    return "";

  gfx::Image decoded_image = gfx::Image::CreateFrom1xBitmap(app_icon);
  scoped_refptr<base::RefCountedMemory> raw_data = decoded_image.As1xPNGBytes();
  return base::Base64Encode(*raw_data);
}

void CheckRegistrationSuccess(base::OnceCallback<void(bool success)> callback,
                              int64_t registration_id) {
  std::move(callback).Run(/*success=*/registration_id >= 0);
}

}  // namespace

// static
PaymentAppProvider* PaymentAppProvider::GetOrCreateForWebContents(
    WebContents* payment_request_web_contents) {
  return WebContentsUserData<PaymentAppProviderImpl>::GetOrCreateForWebContents(
      payment_request_web_contents);
}

void PaymentAppProviderImpl::InvokePaymentApp(
    int64_t registration_id,
    const url::Origin& sw_origin,
    PaymentRequestEventDataPtr event_data,
    InvokePaymentAppCallback callback) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  DCHECK(payment_request_web_contents_);

  if (DevToolsBackgroundServicesContextImpl* dev_tools =
          GetDevTools(sw_origin)) {
    std::map<std::string, std::string> data = {
        {"Merchant Top Origin", event_data->top_origin.spec()},
        {"Merchant Payment Request Origin",
         event_data->payment_request_origin.spec()},
        {"Total Currency", event_data->total->currency},
        {"Total Value", event_data->total->value},
        {"Instrument Key", event_data->instrument_key},
    };
    AddMethodDataToMap(event_data->method_data, &data);
    AddModifiersToMap(event_data->modifiers, &data);
    dev_tools->LogBackgroundServiceEvent(
        registration_id, blink::StorageKey::CreateFirstParty(sw_origin),
        DevToolsBackgroundService::kPaymentHandler, "Payment request",
        /*instance_id=*/event_data->payment_request_id, data);
  }

  StoragePartitionImpl* partition = static_cast<StoragePartitionImpl*>(
      payment_request_web_contents_->GetBrowserContext()
          ->GetDefaultStoragePartition());
  scoped_refptr<ServiceWorkerContextWrapper> service_worker_context =
      partition->GetServiceWorkerContext();

  event_dispatcher_->InvokePayment(registration_id, sw_origin,
                                   std::move(service_worker_context),
                                   std::move(event_data), std::move(callback));
}

void PaymentAppProviderImpl::InstallAndInvokePaymentApp(
    GlobalRenderFrameHostId requesting_frame_id,
    PaymentRequestEventDataPtr event_data,
    const std::string& app_name,
    const SkBitmap& app_icon,
    const GURL& sw_js_url,
    const GURL& sw_scope,
    bool sw_use_cache,
    const std::string& method,
    const SupportedDelegations& supported_delegations,
    RegistrationIdCallback registration_id_callback,
    InvokePaymentAppCallback callback) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  DCHECK(payment_request_web_contents_);

  if (!sw_js_url.is_valid() || !sw_scope.is_valid() || method.empty()) {
    GetUIThreadTaskRunner({})->PostTask(
        FROM_HERE,
        base::BindOnce(
            std::move(callback),
            PaymentAppProviderUtil::CreateBlankPaymentHandlerResponse(
                PaymentEventResponseType::PAYMENT_HANDLER_INSTALL_FAILED)));
    return;
  }

  url::Origin sw_origin = url::Origin::Create(sw_scope);
  if (DevToolsBackgroundServicesContextImpl* dev_tools =
          GetDevTools(sw_origin)) {
    std::map<std::string, std::string> data = {
        {"Merchant Top Origin", event_data->top_origin.spec()},
        {"Merchant Payment Request Origin",
         event_data->payment_request_origin.spec()},
        {"Method Name", method},
        {"Payment Handler Name", app_name},
        {"Service Worker JavaScript File URL", sw_js_url.spec()},
        {"Service Worker Scope", sw_scope.spec()},
        {"Service Worker Uses Cache", base::ToString(sw_use_cache)},
    };
    dev_tools->LogBackgroundServiceEvent(
        /*service_worker_registration_id=*/-1,
        blink::StorageKey::CreateFirstParty(sw_origin),
        DevToolsBackgroundService::kPaymentHandler, "Install payment handler",
        /*instance_id=*/event_data->payment_request_id, data);
  }

  PaymentAppInstaller::Install(
      payment_request_web_contents_, requesting_frame_id, app_name,
      EncodeIcon(app_icon), sw_js_url, sw_scope, sw_use_cache, method,
      supported_delegations,
      base::BindOnce(&PaymentAppProviderImpl::OnInstallPaymentApp,
                     weak_ptr_factory_.GetWeakPtr(), sw_origin,
                     std::move(event_data), std::move(registration_id_callback),
                     std::move(callback)));
}

void PaymentAppProviderImpl::UpdatePaymentAppMetadata(
    int64_t registration_id,
    const std::string& instrument_key,
    const std::string& name,
    const std::string& string_encoded_icon,
    const std::string& method_name,
    const SupportedDelegations& supported_delegations,
    PaymentAppProvider::UpdatePaymentAppMetadataCallback callback) {
  StoragePartitionImpl* partition = static_cast<StoragePartitionImpl*>(
      payment_request_web_contents_->GetBrowserContext()
          ->GetDefaultStoragePartition());
  scoped_refptr<PaymentAppContextImpl> payment_app_context =
      partition->GetPaymentAppContext();

  payment_app_context->payment_app_database()
      ->SetPaymentAppInfoForRegisteredServiceWorker(
          registration_id, instrument_key, name, string_encoded_icon,
          method_name, supported_delegations, std::move(callback));
}

void PaymentAppProviderImpl::CanMakePayment(
    int64_t registration_id,
    const url::Origin& sw_origin,
    const std::string& payment_request_id,
    CanMakePaymentEventDataPtr event_data,
    CanMakePaymentCallback callback) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  DCHECK(payment_request_web_contents_);

  if (DevToolsBackgroundServicesContextImpl* dev_tools =
          GetDevTools(sw_origin)) {
    std::map<std::string, std::string> data = {
        {"Merchant Top Origin", event_data->top_origin.spec()},
        {"Merchant Payment Request Origin",
         event_data->payment_request_origin.spec()}};
    AddMethodDataToMap(event_data->method_data, &data);
    AddModifiersToMap(event_data->modifiers, &data);
    dev_tools->LogBackgroundServiceEvent(
        registration_id, blink::StorageKey::CreateFirstParty(sw_origin),
        DevToolsBackgroundService::kPaymentHandler, "Can make payment",
        /*instance_id=*/payment_request_id, data);
  }

  StoragePartitionImpl* partition = static_cast<StoragePartitionImpl*>(
      payment_request_web_contents_->GetBrowserContext()
          ->GetDefaultStoragePartition());
  scoped_refptr<ServiceWorkerContextWrapper> service_worker_context =
      partition->GetServiceWorkerContext();

  event_dispatcher_->CanMakePayment(registration_id, sw_origin,
                                    payment_request_id,
                                    std::move(service_worker_context),
                                    std::move(event_data), std::move(callback));
}

void PaymentAppProviderImpl::AbortPayment(int64_t registration_id,
                                          const url::Origin& sw_origin,
                                          const std::string& payment_request_id,
                                          AbortCallback callback) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  DCHECK(payment_request_web_contents_);

  if (DevToolsBackgroundServicesContextImpl* dev_tools =
          GetDevTools(sw_origin)) {
    dev_tools->LogBackgroundServiceEvent(
        registration_id, blink::StorageKey::CreateFirstParty(sw_origin),
        DevToolsBackgroundService::kPaymentHandler, "Abort payment",
        /*instance_id=*/payment_request_id, {});
  }

  StoragePartitionImpl* partition = static_cast<StoragePartitionImpl*>(
      payment_request_web_contents_->GetBrowserContext()
          ->GetDefaultStoragePartition());
  scoped_refptr<ServiceWorkerContextWrapper> service_worker_context =
      partition->GetServiceWorkerContext();

  event_dispatcher_->AbortPayment(
      registration_id, sw_origin, payment_request_id,
      std::move(service_worker_context), std::move(callback));
}

void PaymentAppProviderImpl::SetOpenedWindow(
    WebContents* payment_handler_web_contents) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  DCHECK(payment_handler_web_contents);

  CloseOpenedWindow();
  DCHECK(!payment_handler_window_);

  payment_handler_window_ = payment_handler_web_contents->GetWeakPtr();

  payment_handler_disconnected_for_test_ = false;
  payment_handler_web_contents_observer_ =
      std::make_unique<PaymentHandlerWebContentsObserver>(
          payment_handler_web_contents,
          base::BindOnce(&PaymentAppProviderImpl::OnPaymentHandlerDisconnected,
                         weak_ptr_factory_.GetWeakPtr()));
}

void PaymentAppProviderImpl::CloseOpenedWindow() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  payment_handler_web_contents_observer_.reset();

  if (payment_handler_window_)
    payment_handler_window_->Close();

  payment_handler_window_.reset();
}

void PaymentAppProviderImpl::OnClosingOpenedWindow(
    PaymentEventResponseType reason) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  event_dispatcher_->OnClosingOpenedWindow(reason);
}

void PaymentAppProviderImpl::InstallPaymentAppForTesting(
    const SkBitmap& app_icon,
    const GURL& service_worker_javascript_file_url,
    const GURL& service_worker_scope,
    const std::string& payment_method_identifier,
    GlobalRenderFrameHostId requesting_frame_id,
    base::OnceCallback<void(bool success)> callback) {
  CHECK(service_worker_javascript_file_url.is_valid());
  CHECK(service_worker_scope.is_valid());
  CHECK(!payment_method_identifier.empty());

  PaymentAppInstaller::Install(
      payment_request_web_contents_, requesting_frame_id,
      /*app_name=*/"Test App Name", EncodeIcon(app_icon),
      service_worker_javascript_file_url, service_worker_scope,
      /*use_cache=*/false, payment_method_identifier,
      content::SupportedDelegations(),
      base::BindOnce(&CheckRegistrationSuccess, std::move(callback)));
}

void PaymentAppProviderImpl::SetRegistrationId(int64_t registration_id) {
  registration_id_ = registration_id;
}

void PaymentAppProviderImpl::OnPaymentHandlerDisconnected() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  payment_handler_disconnected_for_test_ = true;
  payment_handler_web_contents_observer_.reset();
  StoragePartitionImpl* partition = static_cast<StoragePartitionImpl*>(
      payment_request_web_contents_->GetBrowserContext()
          ->GetDefaultStoragePartition());
  ServiceWorkerContextWrapper* service_worker_context =
      partition->GetServiceWorkerContext();
  service_worker_context->FindReadyRegistrationForIdOnly(
      registration_id_,
      base::BindOnce([](blink::ServiceWorkerStatusCode find_status,
                        scoped_refptr<ServiceWorkerRegistration> registration) {
        if (registration && registration->active_version()) {
          registration->active_version()->OnPaymentHandlerDisconnect();
        }
      }));
}

DevToolsBackgroundServicesContextImpl* PaymentAppProviderImpl::GetDevTools(
    const url::Origin& sw_origin) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  DCHECK(payment_request_web_contents_);
  auto* storage_partition =
      payment_request_web_contents_->GetBrowserContext()
          ->GetStoragePartitionForUrl(sw_origin.GetURL(),
                                      /*can_create=*/true);
  if (!storage_partition)
    return nullptr;

  DevToolsBackgroundServicesContextImpl* dev_tools =
      static_cast<DevToolsBackgroundServicesContextImpl*>(
          storage_partition->GetDevToolsBackgroundServicesContext());
  return dev_tools && dev_tools->IsRecording(
                          DevToolsBackgroundService::kPaymentHandler)
             ? dev_tools
             : nullptr;
}

void PaymentAppProviderImpl::StartServiceWorkerForDispatch(
    int64_t registration_id,
    PaymentEventDispatcher::ServiceWorkerStartCallback callback) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  StoragePartitionImpl* partition = static_cast<StoragePartitionImpl*>(
      payment_request_web_contents_->GetBrowserContext()
          ->GetDefaultStoragePartition());
  scoped_refptr<ServiceWorkerContextWrapper> service_worker_context =
      partition->GetServiceWorkerContext();

  event_dispatcher_->FindRegistration(std::move(service_worker_context),
                                      registration_id, std::move(callback));
}

void PaymentAppProviderImpl::OnInstallPaymentApp(
    const url::Origin& sw_origin,
    PaymentRequestEventDataPtr event_data,
    RegistrationIdCallback registration_id_callback,
    InvokePaymentAppCallback callback,
    int64_t registration_id) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  DCHECK(payment_request_web_contents_);

  if (DevToolsBackgroundServicesContextImpl* dev_tools =
          GetDevTools(sw_origin)) {
    std::map<std::string, std::string> data = {
        {"Payment Handler Install Success",
         base::ToString(registration_id >= 0)},
    };
    dev_tools->LogBackgroundServiceEvent(
        registration_id, blink::StorageKey::CreateFirstParty(sw_origin),
        DevToolsBackgroundService::kPaymentHandler,
        "Install payment handler result",
        /*instance_id=*/event_data->payment_request_id, data);
  }

  if (registration_id >= 0) {
    std::move(registration_id_callback).Run(registration_id);
    InvokePaymentApp(registration_id, sw_origin, std::move(event_data),
                     std::move(callback));
  } else {
    std::move(callback).Run(
        PaymentAppProviderUtil::CreateBlankPaymentHandlerResponse(
            PaymentEventResponseType::PAYMENT_HANDLER_INSTALL_FAILED));
  }
}

PaymentAppProviderImpl::PaymentAppProviderImpl(
    WebContents* payment_request_web_contents)
    : WebContentsUserData<PaymentAppProviderImpl>(
          *payment_request_web_contents),
      payment_request_web_contents_(payment_request_web_contents),
      event_dispatcher_(std::make_unique<PaymentEventDispatcher>()) {
  event_dispatcher_->set_payment_app_provider(weak_ptr_factory_.GetWeakPtr());
}

PaymentAppProviderImpl::~PaymentAppProviderImpl() = default;

WEB_CONTENTS_USER_DATA_KEY_IMPL(PaymentAppProviderImpl);
}  // namespace content
