// 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 "components/payments/content/service_worker_payment_app_finder.h"

#include <algorithm>
#include <vector>

#include "base/base64.h"
#include "base/check.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/ref_counted_memory.h"
#include "base/memory/scoped_refptr.h"
#include "base/stl_util.h"
#include "base/supports_user_data.h"
#include "base/task/single_thread_task_runner.h"
#include "components/payments/content/developer_console_logger.h"
#include "components/payments/content/installable_payment_app_crawler.h"
#include "components/payments/content/manifest_verifier.h"
#include "components/payments/content/payment_manifest_downloader.h"
#include "components/payments/content/utility/payment_manifest_parser.h"
#include "components/payments/content/web_payments_web_data_service.h"
#include "components/payments/core/features.h"
#include "components/payments/core/method_strings.h"
#include "components/payments/core/url_util.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/payment_app_provider.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/browser/stored_payment_app.h"
#include "content/public/browser/weak_document_ptr.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_features.h"
#include "ui/gfx/image/image.h"
#include "url/url_canon.h"

namespace payments {
namespace {

// Returns true if |app| supports at least one of the |requests|.
bool AppSupportsAtLeastOneRequestedMethodData(
    const content::StoredPaymentApp& app,
    const std::vector<mojom::PaymentMethodDataPtr>& requests) {
  for (const auto& enabled_method : app.enabled_methods) {
    for (const auto& request : requests) {
      if (enabled_method == request->supported_method) {
        return true;
      }
    }
  }
  return false;
}

void RemovePortNumbersFromScopesForTest(
    content::InstalledPaymentAppsFinder::PaymentApps* apps) {
  GURL::Replacements replacements;
  replacements.ClearPort();
  for (auto& app : *apps) {
    app.second->scope = app.second->scope.ReplaceComponents(replacements);
  }
}

class SelfDeletingServiceWorkerPaymentAppFinder
    : public base::SupportsUserData::Data {
 public:
  static base::WeakPtr<SelfDeletingServiceWorkerPaymentAppFinder>
  CreateAndSetOwnedBy(content::WebContents* owner) {
    auto owned =
        std::make_unique<SelfDeletingServiceWorkerPaymentAppFinder>(owner);
    auto* pointer = owned.get();
    owner->SetUserData(pointer, std::move(owned));
    return pointer->weak_ptr_factory_.GetWeakPtr();
  }

  explicit SelfDeletingServiceWorkerPaymentAppFinder(
      content::WebContents* owner)
      : owner_(owner) {}

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

  ~SelfDeletingServiceWorkerPaymentAppFinder() override = default;

  // After |callback| has fired, the factory refreshes its own cache in the
  // background. Once the cache has been refreshed, the factory invokes the
  // |finished_using_resources_callback|. At this point, it's safe to delete
  // this factory. Don't destroy the factory and don't call this method again
  // until |finished_using_resources_callback| has run.
  void GetAllPaymentApps(
      const url::Origin& merchant_origin,
      content::RenderFrameHost* initiator_render_frame_host,
      std::unique_ptr<PaymentManifestDownloader> downloader,
      scoped_refptr<WebPaymentsWebDataService> cache,
      const std::vector<mojom::PaymentMethodDataPtr>& requested_method_data,
      ServiceWorkerPaymentAppFinder::GetAllPaymentAppsCallback callback,
      base::OnceClosure finished_using_resources_callback) {
    DCHECK(!verifier_);
    DCHECK(initiator_render_frame_host);
    DCHECK(initiator_render_frame_host->IsActive());

    downloader_ = std::move(downloader);

    content::WebContents* web_contents =
        content::WebContents::FromRenderFrameHost(initiator_render_frame_host);
    parser_ = std::make_unique<PaymentManifestParser>(
        std::make_unique<DeveloperConsoleLogger>(web_contents));
    cache_ = cache;
    verifier_ = std::make_unique<ManifestVerifier>(
        merchant_origin, web_contents, downloader_.get(), parser_.get(),
        cache_.get());
    if (base::FeatureList::IsEnabled(
            features::kWebPaymentsJustInTimePaymentApp)) {
      crawler_ = std::make_unique<InstallablePaymentAppCrawler>(
          merchant_origin, initiator_render_frame_host, downloader_.get(),
          parser_.get(), cache_.get());
      if (ignore_port_in_origin_comparison_for_testing_)
        crawler_->IgnorePortInOriginComparisonForTesting();
    }

    // Method data cannot be copied and is passed in as a const-ref, which
    // cannot be moved, so make a manual copy for using below.
    for (const auto& method_data : requested_method_data) {
      requested_method_data_.emplace_back(method_data.Clone());
    }
    callback_ = std::move(callback);
    finished_using_resources_callback_ =
        std::move(finished_using_resources_callback);

    content::InstalledPaymentAppsFinder::GetInstance(
        initiator_render_frame_host->GetBrowserContext())
        ->GetAllPaymentApps(base::BindOnce(
            &SelfDeletingServiceWorkerPaymentAppFinder::OnGotAllPaymentApps,
            weak_ptr_factory_.GetWeakPtr()));
  }

  void IgnorePortInOriginComparisonForTesting() {
    ignore_port_in_origin_comparison_for_testing_ = true;
  }

 private:
  // base::SupportsUserData::Data implementation.
  std::unique_ptr<Data> Clone() override {
    return nullptr;  // Cloning is not supported.
  }

  static void RemoveUnrequestedMethods(
      const std::vector<mojom::PaymentMethodDataPtr>& requested_method_data,
      content::InstalledPaymentAppsFinder::PaymentApps* apps) {
    std::set<std::string> requested_methods;
    for (const auto& requested_method_datum : requested_method_data) {
      requested_methods.insert(requested_method_datum->supported_method);
    }
    for (auto& app : *apps) {
      std::sort(app.second->enabled_methods.begin(),
                app.second->enabled_methods.end());
      app.second->enabled_methods =
          base::STLSetIntersection<std::vector<std::string>>(
              app.second->enabled_methods, requested_methods);
    }
  }

  void OnGotAllPaymentApps(
      content::InstalledPaymentAppsFinder::PaymentApps apps) {
    if (ignore_port_in_origin_comparison_for_testing_)
      RemovePortNumbersFromScopesForTest(&apps);

    RemoveUnrequestedMethods(requested_method_data_, &apps);
    ServiceWorkerPaymentAppFinder::RemoveAppsWithoutMatchingMethodData(
        requested_method_data_, &apps);
    if (apps.empty()) {
      OnPaymentAppsVerified(std::move(apps), first_error_message_);
      OnPaymentAppsVerifierFinishedUsingResources();
      return;
    }

    // The |verifier_| will invoke |OnPaymentAppsVerified| with the list of all
    // valid payment apps. This list may be empty, if none of the apps were
    // found to be valid.
    is_payment_verifier_finished_using_resources_ = false;
    verifier_->Verify(
        std::move(apps),
        base::BindOnce(
            &SelfDeletingServiceWorkerPaymentAppFinder::OnPaymentAppsVerified,
            weak_ptr_factory_.GetWeakPtr()),
        base::BindOnce(&SelfDeletingServiceWorkerPaymentAppFinder::
                           OnPaymentAppsVerifierFinishedUsingResources,
                       weak_ptr_factory_.GetWeakPtr()));
  }

  void OnPaymentAppsVerified(
      content::InstalledPaymentAppsFinder::PaymentApps apps,
      const std::string& error_message) {
    if (first_error_message_.empty())
      first_error_message_ = error_message;

    installed_apps_ = std::move(apps);

    // Crawl for installable web payment apps if no web payment apps were
    // already installed.
    //
    // If `crawler_` is null, that indicates that JIT install has been disabled
    // entirely, and so we should be doing no crawling.
    if (installed_apps_.empty() && crawler_ != nullptr) {
      is_payment_app_crawler_finished_using_resources_ = false;
      crawler_->Start(
          requested_method_data_,
          /*method_manifest_urls_for_metadata_refresh=*/std::set<GURL>(),
          base::BindOnce(&SelfDeletingServiceWorkerPaymentAppFinder::
                             OnPaymentAppsCrawledForInstallation,
                         weak_ptr_factory_.GetWeakPtr()),
          base::BindOnce(&SelfDeletingServiceWorkerPaymentAppFinder::
                             OnPaymentAppsCrawlerFinishedUsingResources,
                         weak_ptr_factory_.GetWeakPtr()));
      return;
    }

    // To ensure that payment apps are able to respond to web-app manifest
    // changes (such as their icon changing), we refetch the web-app manifest
    // for already-installed apps. This process can be slow, so we don't block
    // the creation of payment apps on it - the app will be updated in the
    // background and changes will take effect on any subsequent Payment Request
    // launch.
    std::set<GURL> method_manifest_urls_for_icon_refresh;
    for (auto& app : installed_apps_) {
      for (const auto& method : app.second->enabled_methods) {
        // Only payment methods with manifests are eligible for refetching the
        // icon of their installed payment apps.
        GURL method_manifest_url = GURL(method);
        if (!UrlUtil::IsValidUrlBasedPaymentMethodIdentifier(
                method_manifest_url)) {
          continue;
        }
        method_manifest_urls_for_icon_refresh.insert(method_manifest_url);
      }
    }

    if (!method_manifest_urls_for_icon_refresh.empty() && crawler_ != nullptr) {
      DCHECK(!installed_apps_.empty());
      is_payment_app_crawler_finished_using_resources_ = false;
      crawler_->Start(
          requested_method_data_,
          std::move(method_manifest_urls_for_icon_refresh),
          base::BindOnce(&SelfDeletingServiceWorkerPaymentAppFinder::
                             OnPaymentAppsCrawledForUpdatedMetadata,
                         weak_ptr_factory_.GetWeakPtr()),
          base::BindOnce(&SelfDeletingServiceWorkerPaymentAppFinder::
                             OnPaymentAppsCrawlerFinishedUsingResources,
                         weak_ptr_factory_.GetWeakPtr()));

      // Deliberately copy installed_apps_, as it is still needed in
      // |OnPaymentAppsCrawledForUpdatedMetadata|.
      content::InstalledPaymentAppsFinder::PaymentApps installed_apps_copy;
      for (const auto& app : installed_apps_) {
        installed_apps_copy[app.first] =
            std::make_unique<content::StoredPaymentApp>(*app.second);
      }
      std::move(callback_).Run(
          std::move(installed_apps_copy),
          ServiceWorkerPaymentAppFinder::InstallablePaymentApps(),
          first_error_message_);
    } else {
      // Release crawler_ since it will not be used from now on.
      crawler_.reset();

      std::move(callback_).Run(
          std::move(installed_apps_),
          ServiceWorkerPaymentAppFinder::InstallablePaymentApps(),
          first_error_message_);
    }
  }

  void OnPaymentAppsCrawledForInstallation(
      std::map<GURL, std::unique_ptr<WebAppInstallationInfo>> apps_info,
      std::map<GURL, std::unique_ptr<RefetchedMetadata>> refetched_app_metadata,
      const std::string& error_message) {
    // This path should only be hit when there were no apps already installed;
    // see |OnPaymentAppsVerified|.
    CHECK(installed_apps_.empty());

    // Payment app metadata should not be refetched when crawling for
    // installable apps.
    CHECK(refetched_app_metadata.empty());

    if (first_error_message_.empty())
      first_error_message_ = error_message;

    std::move(callback_).Run(content::InstalledPaymentAppsFinder::PaymentApps(),
                             std::move(apps_info), first_error_message_);
  }

  void OnPaymentAppsCrawledForUpdatedMetadata(
      std::map<GURL, std::unique_ptr<WebAppInstallationInfo>> apps_info,
      std::map<GURL, std::unique_ptr<RefetchedMetadata>> refetched_app_metadata,
      // We deliberately ignore the error message, as this method is an optional
      // asynchronous update - if it failed, it is ok to fail silently.
      const std::string& ignored_error_message) {
    // This crawl should only have been triggered for refetched metadata, and in
    // that mode the crawler should not suggest installable apps to us.
    DCHECK(apps_info.empty());

    // TODO(crbug.com/40259220): Consider optimizing either these database
    // writes or the entire re-crawling process to avoid fetching/saving icons
    // when nothing has changed in the manifest.
    for (auto& refetched_metadata : refetched_app_metadata) {
      GURL web_app_manifest_url = refetched_metadata.first;
      RefetchedMetadata* data = refetched_metadata.second.get();
      for (auto& app : installed_apps_) {
        // It is possible (unlikely) to have multiple apps with same origins.
        // The proper validation is to store web_app_manifest_url in
        // StoredPaymentApp and confirm that it is the same as the
        // web_app_manifest_url from which metadata is fetched.
        if (crawler_->IsSameOriginWith(GURL(app.second->scope),
                                       web_app_manifest_url)) {
          UpdatePaymentAppMetadata(app.second, data->icon, data->method_name,
                                   data->supported_delegations);
          break;
        }
      }
    }
  }

  void UpdatePaymentAppMetadata(
      const std::unique_ptr<content::StoredPaymentApp>& app,
      const std::unique_ptr<SkBitmap>& icon,
      const std::string& method_name,
      const content::SupportedDelegations& supported_delegations) {
    number_of_app_metadata_to_update_++;

    // If fetching a new icon failed (e.g., due to CSP policy on the given
    // page), make sure we don't override the old icon with nothing.
    bool fetched_new_icon = icon && !icon->empty();
    gfx::Image decoded_image =
        gfx::Image::CreateFrom1xBitmap(fetched_new_icon ? *icon : *app->icon);
    scoped_refptr<base::RefCountedMemory> raw_data =
        decoded_image.As1xPNGBytes();
    std::string string_encoded_icon = base::Base64Encode(*raw_data);

    content::PaymentAppProvider::GetOrCreateForWebContents(owner_)
        ->UpdatePaymentAppMetadata(
            app->registration_id, app->scope.spec(), app->name,
            string_encoded_icon, method_name, supported_delegations,
            base::BindOnce(&SelfDeletingServiceWorkerPaymentAppFinder::
                               OnUpdatePaymentAppMetadata,
                           weak_ptr_factory_.GetWeakPtr()));
  }

  void OnUpdatePaymentAppMetadata(
      payments::mojom::PaymentHandlerStatus status) {
    DCHECK(number_of_app_metadata_to_update_ > 0);
    number_of_app_metadata_to_update_--;
    if (number_of_app_metadata_to_update_ == 0) {
      FinishUsingResourcesIfReady();
    }
  }

  void OnPaymentAppsCrawlerFinishedUsingResources() {
    crawler_.reset();

    is_payment_app_crawler_finished_using_resources_ = true;
    FinishUsingResourcesIfReady();
  }

  void OnPaymentAppsVerifierFinishedUsingResources() {
    verifier_.reset();

    is_payment_verifier_finished_using_resources_ = true;
    FinishUsingResourcesIfReady();
  }

  void FinishUsingResourcesIfReady() {
    if (is_payment_verifier_finished_using_resources_ &&
        is_payment_app_crawler_finished_using_resources_ &&
        !finished_using_resources_callback_.is_null() &&
        number_of_app_metadata_to_update_ == 0) {
      downloader_.reset();
      parser_.reset();
      std::move(finished_using_resources_callback_).Run();

      base::SingleThreadTaskRunner::GetCurrentDefault()->PostNonNestableTask(
          FROM_HERE,
          base::BindOnce(&SelfDeletingServiceWorkerPaymentAppFinder::DeleteSelf,
                         weak_ptr_factory_.GetWeakPtr()));
    }
  }

  void DeleteSelf() { owner_->RemoveUserData(this); }

  // |owner_| owns this SelfDeletingServiceWorkerPaymentAppFinder, so it is
  // always valid.
  raw_ptr<content::WebContents> owner_;

  std::unique_ptr<PaymentManifestDownloader> downloader_;
  std::unique_ptr<PaymentManifestParser> parser_;
  scoped_refptr<WebPaymentsWebDataService> cache_;
  std::vector<mojom::PaymentMethodDataPtr> requested_method_data_;
  ServiceWorkerPaymentAppFinder::GetAllPaymentAppsCallback callback_;
  base::OnceClosure finished_using_resources_callback_;
  std::string first_error_message_;

  std::unique_ptr<ManifestVerifier> verifier_;
  bool is_payment_verifier_finished_using_resources_ = true;

  std::unique_ptr<InstallablePaymentAppCrawler> crawler_;
  bool is_payment_app_crawler_finished_using_resources_ = true;

  bool ignore_port_in_origin_comparison_for_testing_ = false;

  content::InstalledPaymentAppsFinder::PaymentApps installed_apps_;

  size_t number_of_app_metadata_to_update_ = 0;

  base::WeakPtrFactory<SelfDeletingServiceWorkerPaymentAppFinder>
      weak_ptr_factory_{this};
};

}  // namespace

ServiceWorkerPaymentAppFinder::~ServiceWorkerPaymentAppFinder() = default;

void ServiceWorkerPaymentAppFinder::GetAllPaymentApps(
    const url::Origin& merchant_origin,
    scoped_refptr<WebPaymentsWebDataService> cache,
    std::vector<mojom::PaymentMethodDataPtr> requested_method_data,
    base::WeakPtr<CSPChecker> csp_checker,
    GetAllPaymentAppsCallback callback,
    base::OnceClosure finished_writing_cache_callback_for_testing) {
  DCHECK(!requested_method_data.empty());

  if (!render_frame_host().IsActive())
    return;

  // Do not look up payment handlers for ignored payment methods.
  std::erase_if(
      requested_method_data,
      [&](const mojom::PaymentMethodDataPtr& method_data) {
        return ignored_methods_.contains(method_data->supported_method);
      });
  if (requested_method_data.empty()) {
    std::move(callback).Run(
        content::InstalledPaymentAppsFinder::PaymentApps(),
        std::map<GURL, std::unique_ptr<WebAppInstallationInfo>>(),
        /*error_message=*/"");
    return;
  }

  auto* web_contents =
      content::WebContents::FromRenderFrameHost(&render_frame_host());
  auto self_delete_factory =
      SelfDeletingServiceWorkerPaymentAppFinder::CreateAndSetOwnedBy(
          web_contents);

  std::unique_ptr<PaymentManifestDownloader> downloader;
  if (test_downloader_ != nullptr) {
    test_downloader_->SetCSPCheckerForTesting(csp_checker);  // IN-TEST
    downloader = std::move(test_downloader_);
    self_delete_factory->IgnorePortInOriginComparisonForTesting();
  } else {
    mojo::Remote<network::mojom::URLLoaderFactory> url_loader_factory;
    render_frame_host().CreateNetworkServiceDefaultFactory(
        url_loader_factory.BindNewPipeAndPassReceiver());
    downloader = std::make_unique<payments::PaymentManifestDownloader>(
        std::make_unique<DeveloperConsoleLogger>(web_contents), csp_checker,
        render_frame_host()
            .GetBrowserContext()
            ->GetDefaultStoragePartition()
            ->GetURLLoaderFactoryForBrowserProcess(),
        std::move(url_loader_factory),
        render_frame_host().GetWeakDocumentPtr());
  }

  self_delete_factory->GetAllPaymentApps(
      merchant_origin, &render_frame_host(), std::move(downloader), cache,
      requested_method_data, std::move(callback),
      std::move(finished_writing_cache_callback_for_testing));
}

// static
void ServiceWorkerPaymentAppFinder::RemoveAppsWithoutMatchingMethodData(
    const std::vector<mojom::PaymentMethodDataPtr>& requested_method_data,
    content::InstalledPaymentAppsFinder::PaymentApps* apps) {
  for (auto it = apps->begin(); it != apps->end();) {
    if (AppSupportsAtLeastOneRequestedMethodData(*it->second,
                                                 requested_method_data)) {
      ++it;
    } else {
      it = apps->erase(it);
    }
  }
}

ServiceWorkerPaymentAppFinder::ServiceWorkerPaymentAppFinder(
    content::RenderFrameHost* rfh)
    : content::DocumentUserData<ServiceWorkerPaymentAppFinder>(rfh),
      ignored_methods_({methods::kGooglePlayBilling}),
      test_downloader_(nullptr) {}

DOCUMENT_USER_DATA_KEY_IMPL(ServiceWorkerPaymentAppFinder);

}  // namespace payments
