// Copyright 2018 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_installer.h"

#include <utility>

#include "base/functional/bind.h"
#include "base/memory/ref_counted.h"
#include "base/memory/weak_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "content/browser/payments/payment_app_context_impl.h"
#include "content/browser/service_worker/service_worker_context_wrapper.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/console_message.h"
#include "content/public/browser/global_routing_id.h"
#include "content/public/browser/service_worker_context_observer.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/browser/web_contents.h"
#include "third_party/blink/public/common/storage_key/storage_key.h"
#include "third_party/blink/public/mojom/service_worker/service_worker_registration_options.mojom.h"
#include "url/gurl.h"
#include "url/origin.h"

namespace content {
namespace {

// Self deleting installer installs a web payment app and deletes itself.
class SelfDeleteInstaller
    : public ServiceWorkerContextObserver,
      public base::RefCountedThreadSafe<SelfDeleteInstaller> {
 public:
  SelfDeleteInstaller(const std::string& app_name,
                      const std::string& app_icon,
                      const GURL& sw_url,
                      const GURL& scope,
                      const std::string& method,
                      const SupportedDelegations& supported_delegations,
                      PaymentAppInstaller::InstallPaymentAppCallback callback)
      : app_name_(app_name),
        app_icon_(app_icon),
        sw_url_(sw_url),
        scope_(scope),
        method_(method),
        supported_delegations_(supported_delegations),
        callback_(std::move(callback)) {
    DCHECK_CURRENTLY_ON(BrowserThread::UI);
  }

  SelfDeleteInstaller(const SelfDeleteInstaller& other) = delete;
  SelfDeleteInstaller& operator=(const SelfDeleteInstaller& other) = delete;

  void Init(WebContents* web_contents,
            GlobalRenderFrameHostId requesting_frame_id,
            bool use_cache) {
    DCHECK_CURRENTLY_ON(BrowserThread::UI);

    AddRef();  // Balanced by Release() in FinishInstallation.

    // TODO: Switch to being a WebContentsObserver and listen for events to
    // terminate installation early?
    web_contents_ = web_contents->GetWeakPtr();
    requesting_frame_id_ = requesting_frame_id;

    use_cache_ = use_cache;

    BrowserContext* browser_context = web_contents->GetBrowserContext();
    service_worker_context_ =
        base::WrapRefCounted(static_cast<ServiceWorkerContextWrapper*>(
            browser_context->GetDefaultStoragePartition()
                ->GetServiceWorkerContext()));
    service_worker_context_->FindReadyRegistrationForScope(
        scope_,
        blink::StorageKey::CreateFirstParty(url::Origin::Create(scope_)),
        base::BindOnce(&SelfDeleteInstaller::OnFindReadyRegistrationForScope,
                       this));
  }

  void OnFindReadyRegistrationForScope(
      blink::ServiceWorkerStatusCode status,
      scoped_refptr<ServiceWorkerRegistration> registration) {
    DCHECK_CURRENTLY_ON(BrowserThread::UI);
    if (AbortInstallIfWebContentsOrBrowserContextIsGone())
      return;

    if (status == blink::ServiceWorkerStatusCode::kOk && registration) {
      DCHECK_EQ(scope_, registration->scope());
      // The service worker is already registered and activated (i.e., ready).
      // Use the existing service worker.
      registration_id_ = registration->id();
      SetPaymentAppIntoDatabase();
      return;
    }

    // If no service workers found, then register a new one.
    service_worker_context_->AddObserver(this);

    blink::mojom::ServiceWorkerRegistrationOptions option;
    option.scope = scope_;
    if (!use_cache_) {
      option.update_via_cache =
          blink::mojom::ServiceWorkerUpdateViaCache::kNone;
    }
    // TODO(crbug.com/40177656): Because this function can be called in a 3p
    // context we will need to generate a full StorageKey (origin + top-level
    // site) once StorageKey is expanded with the top-level site.
    service_worker_context_->RegisterServiceWorker(
        sw_url_,
        blink::StorageKey::CreateFirstParty(url::Origin::Create(option.scope)),
        option, requesting_frame_id_,
        base::BindOnce(&SelfDeleteInstaller::OnRegisterServiceWorkerResult,
                       this));
  }

  void OnRegistrationStored(int64_t registration_id,
                            const GURL& scope,
                            const ServiceWorkerRegistrationInformation&
                                service_worker_info) override {
    DCHECK_CURRENTLY_ON(BrowserThread::UI);
    if (AbortInstallIfWebContentsOrBrowserContextIsGone())
      return;

    if (scope.EqualsIgnoringRef(scope_))
      registration_id_ = registration_id;
  }

  void OnVersionActivated(int64_t version_id, const GURL& scope) override {
    DCHECK_CURRENTLY_ON(BrowserThread::UI);
    if (AbortInstallIfWebContentsOrBrowserContextIsGone())
      return;

    if (scope.EqualsIgnoringRef(scope_))
      SetPaymentAppIntoDatabase();
  }

  void OnErrorReported(int64_t version_id,
                       const GURL& scope,
                       const ErrorInfo& info) override {
    DCHECK_CURRENTLY_ON(BrowserThread::UI);
    if (AbortInstallIfWebContentsOrBrowserContextIsGone())
      return;

    if (scope.EqualsIgnoringRef(scope_))
      LOG(ERROR) << "The newly registered service worker has an error "
                 << info.error_message;
    FinishInstallation(false);
  }

  void OnRegisterServiceWorkerResult(blink::ServiceWorkerStatusCode status) {
    DCHECK_CURRENTLY_ON(BrowserThread::UI);
    if (AbortInstallIfWebContentsOrBrowserContextIsGone())
      return;

    if (status != blink::ServiceWorkerStatusCode::kOk) {
      LOG(ERROR) << "Failed to install the web payment app " << sw_url_.spec();
      FinishInstallation(false);
    }
  }

 private:
  friend class base::RefCountedThreadSafe<SelfDeleteInstaller>;

  ~SelfDeleteInstaller() override = default;

  // If web contents or browser context are gone, then aborts payment and
  // returns true. Should be called on UI thread.
  bool AbortInstallIfWebContentsOrBrowserContextIsGone() {
    DCHECK_CURRENTLY_ON(BrowserThread::UI);

    if (!web_contents_ || !web_contents_->GetBrowserContext()) {
      FinishInstallation(false);
      return true;
    }

    return false;
  }

  void SetPaymentAppIntoDatabase() {
    DCHECK_CURRENTLY_ON(BrowserThread::UI);
    DCHECK(web_contents_);
    DCHECK(web_contents_->GetBrowserContext());

    StoragePartitionImpl* partition = static_cast<StoragePartitionImpl*>(
        web_contents_->GetBrowserContext()->GetDefaultStoragePartition());
    scoped_refptr<PaymentAppContextImpl> payment_app_context =
        partition->GetPaymentAppContext();

    payment_app_context->payment_app_database()
        ->SetPaymentAppInfoForRegisteredServiceWorker(
            registration_id_, scope_.spec(), app_name_, app_icon_, method_,
            supported_delegations_,
            base::BindOnce(&SelfDeleteInstaller::OnSetPaymentAppInfo, this));
  }

  void OnSetPaymentAppInfo(payments::mojom::PaymentHandlerStatus status) {
    DCHECK_CURRENTLY_ON(BrowserThread::UI);

    FinishInstallation(status == payments::mojom::PaymentHandlerStatus::SUCCESS
                           ? true
                           : false);
  }

  void FinishInstallation(bool success) {
    DCHECK_CURRENTLY_ON(BrowserThread::UI);

    // Do nothing if this function has been called.
    if (callback_.is_null())
      return;

    base::UmaHistogramBoolean("PaymentRequest.PaymentHandlerInstallSuccess",
                              success);

    if (success) {
      std::move(callback_).Run(registration_id_);
    } else {
      std::move(callback_).Run(-1);
    }

    service_worker_context_->RemoveObserver(this);

    Release();  // Balanced by AddRef() in the constructor.
  }

  base::WeakPtr<WebContents> web_contents_;
  GlobalRenderFrameHostId requesting_frame_id_;
  std::string app_name_;
  std::string app_icon_;
  GURL sw_url_;
  GURL scope_;
  std::string method_;
  SupportedDelegations supported_delegations_;
  PaymentAppInstaller::InstallPaymentAppCallback callback_;
  bool use_cache_ = false;

  int64_t registration_id_ = -1;  // Take -1 as an invalid registration Id.
  scoped_refptr<ServiceWorkerContextWrapper> service_worker_context_;
};

}  // namespace.

// Static
void PaymentAppInstaller::Install(
    WebContents* web_contents,
    GlobalRenderFrameHostId requesting_frame_id,
    const std::string& app_name,
    const std::string& app_icon,
    const GURL& sw_url,
    const GURL& scope,
    bool use_cache,
    const std::string& method,
    const SupportedDelegations& supported_delegations,
    InstallPaymentAppCallback callback) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  auto installer = base::MakeRefCounted<SelfDeleteInstaller>(
      app_name, app_icon, sw_url, scope, method, supported_delegations,
      std::move(callback));
  installer->Init(web_contents, requesting_frame_id, use_cache);
}

}  // namespace content
