// Copyright 2020 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/android_payment_app_factory.h"

#include <map>
#include <memory>
#include <set>
#include <string>
#include <utility>
#include <vector>

#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/stl_util.h"
#include "base/supports_user_data.h"
#include "components/payments/content/android_app_communication.h"
#include "components/payments/content/android_payment_app.h"
#include "components/payments/content/content_payment_request_delegate.h"
#include "components/payments/content/payment_request_spec.h"
#include "components/payments/core/android_app_description.h"
#include "components/payments/core/android_app_description_tools.h"
#include "components/payments/core/method_strings.h"
#include "components/payments/core/native_error_strings.h"
#include "components/payments/core/payment_request_data_util.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/document_user_data.h"
#include "content/public/browser/web_contents.h"

namespace payments {
namespace {

class AppFinder : public base::SupportsUserData::Data {
 public:
  static base::WeakPtr<AppFinder> CreateAndSetOwnedBy(
      base::SupportsUserData* owner) {
    DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
    DCHECK(owner);
    auto owned = std::make_unique<AppFinder>(owner);
    auto weak_ptr = owned->weak_ptr_factory_.GetWeakPtr();
    const void* key = owned.get();
    owner->SetUserData(key, std::move(owned));
    return weak_ptr;
  }

  explicit AppFinder(base::SupportsUserData* owner) : owner_(owner) {}
  ~AppFinder() override = default;

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

  void FindApps(base::WeakPtr<AndroidAppCommunication> communication,
                base::WeakPtr<PaymentAppFactory::Delegate> delegate) {
    DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
    DCHECK_EQ(nullptr, delegate_.get());
    DCHECK_NE(nullptr, delegate.get());
    DCHECK_EQ(0U, number_of_pending_is_ready_to_pay_queries_);
    DCHECK_EQ(nullptr, communication_.get());
    DCHECK_NE(nullptr, communication.get());
    DCHECK(delegate->GetSpec());
    DCHECK(delegate->GetSpec()->details().id.has_value());

    delegate_ = delegate;
    communication_ = communication;

    std::set<std::string> twa_payment_method_names = {
        methods::kGooglePlayBilling,
    };
    if (base::STLSetIntersection<std::set<std::string>>(
            delegate_->GetSpec()->payment_method_identifiers_set(),
            twa_payment_method_names)
            .empty()) {
      OnDoneCreatingPaymentApps();
      return;
    }

    delegate_->GetTwaPackageName(base::BindOnce(
        &AppFinder::OnGetTwaPackageName, weak_ptr_factory_.GetWeakPtr()));
  }

  void OnGetTwaPackageName(const std::string& twa_package_name) {
    if (twa_package_name.empty()) {
      OnDoneCreatingPaymentApps();
      return;
    }

    communication_->GetAppDescriptions(
        twa_package_name, base::BindOnce(&AppFinder::OnGetAppDescriptions,
                                         weak_ptr_factory_.GetWeakPtr()));
  }

 private:
  // Check that our required dependencies are still valid, i.e. that the page
  // isn't currently being torn down.
  bool PageIsValid() {
    return communication_ && delegate_ && delegate_->GetSpec() &&
           delegate_->GetInitiatorRenderFrameHost();
  }

  void OnGetAppDescriptions(
      const std::optional<std::string>& error_message,
      std::vector<std::unique_ptr<AndroidAppDescription>> app_descriptions) {
    DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
    // The browser could be shutting down.
    if (!PageIsValid()) {
      return;
    }

    if (error_message.has_value()) {
      delegate_->OnPaymentAppCreationError(error_message.value());
      OnDoneCreatingPaymentApps();
      return;
    }

    std::vector<std::unique_ptr<AndroidAppDescription>> single_activity_apps;
    for (size_t i = 0; i < app_descriptions.size(); ++i) {
      auto app = std::move(app_descriptions[i]);
      if (app->service_names.size() > 1U) {
        delegate_->OnPaymentAppCreationError(errors::kMoreThanOneService);
        continue;
      }

      // Move each activity in the given |app| to its own AndroidAppDescription
      // in |single_activity_apps|, so the code can treat each PAY intent as its
      // own payment app. This allows Android apps to implement PAY intent in
      // multiple activities with different names and icons for different use
      // cases.
      SplitPotentiallyMultipleActivities(std::move(app), &single_activity_apps);
    }

    number_of_pending_is_ready_to_pay_queries_ = single_activity_apps.size();
    if (number_of_pending_is_ready_to_pay_queries_ == 0U) {
      OnDoneCreatingPaymentApps();
      return;
    }

    for (size_t i = 0; i < single_activity_apps.size(); ++i) {
      std::unique_ptr<AndroidAppDescription> single_activity_app =
          std::move(single_activity_apps[i]);

      const std::string& default_method =
          single_activity_app->activities.front()->default_payment_method;
      DCHECK_EQ(methods::kGooglePlayBilling, default_method);

      std::set<std::string> supported_payment_methods = {default_method};
      std::set<std::string> payment_method_names =
          base::STLSetIntersection<std::set<std::string>>(
              delegate_->GetSpec()->payment_method_identifiers_set(),
              supported_payment_methods);

      std::unique_ptr<std::map<std::string, std::set<std::string>>>
          stringified_method_data = data_util::FilterStringifiedMethodData(
              delegate_->GetSpec()->stringified_method_data(),
              supported_payment_methods);

      // TODO(crbug.com/40106647): Download the web app manifest for
      // |default_payment_method_name| to verify Android app signature.

      // Skip querying IS_READY_TO_PAY service when Chrome is off-the-record or
      // when the app does not implement the IS_READY_TO_PAY service.
      if (delegate_->IsOffTheRecord() ||
          single_activity_app->service_names.empty()) {
        OnIsReadyToPay(std::move(single_activity_app), payment_method_names,
                       std::move(stringified_method_data),
                       /*error_message=*/std::nullopt,
                       /*is_ready_to_pay=*/true);
        continue;
      }

      const std::string package = single_activity_app->package;
      const std::string service_name =
          single_activity_app->service_names.front();
      std::map<std::string, std::set<std::string>>
          stringified_method_data_copy = *stringified_method_data;
      communication_->IsReadyToPay(
          package, service_name, stringified_method_data_copy,
          delegate_->GetTopOrigin(), delegate_->GetFrameOrigin(),
          delegate_->GetSpec()->details().id.value(),
          base::BindOnce(&AppFinder::OnIsReadyToPay,
                         weak_ptr_factory_.GetWeakPtr(),
                         std::move(single_activity_app), payment_method_names,
                         std::move(stringified_method_data)));
    }
  }

  void OnIsReadyToPay(
      std::unique_ptr<AndroidAppDescription> app_description,
      const std::set<std::string>& payment_method_names,
      std::unique_ptr<std::map<std::string, std::set<std::string>>>
          stringified_method_data,
      const std::optional<std::string>& error_message,
      bool is_ready_to_pay) {
    DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
    DCHECK_LT(0U, number_of_pending_is_ready_to_pay_queries_);

    // The browser could be shutting down.
    if (!PageIsValid()) {
      OnDoneCreatingPaymentApps();
      return;
    }

    if (error_message.has_value()) {
      delegate_->OnPaymentAppCreationError(error_message.value());
    } else if (is_ready_to_pay) {
      delegate_->OnPaymentAppCreated(std::make_unique<AndroidPaymentApp>(
          payment_method_names, std::move(stringified_method_data),
          delegate_->GetTopOrigin(), delegate_->GetFrameOrigin(),
          delegate_->GetSpec()->details().id.value(),
          std::move(app_description), communication_,
          delegate_->GetInitiatorRenderFrameHost()->GetGlobalId()));
    }

    if (--number_of_pending_is_ready_to_pay_queries_ == 0)
      OnDoneCreatingPaymentApps();
  }

  void OnDoneCreatingPaymentApps() {
    if (delegate_)
      delegate_->OnDoneCreatingPaymentApps();

    owner_->RemoveUserData(this);
  }

  raw_ptr<base::SupportsUserData> owner_;
  base::WeakPtr<PaymentAppFactory::Delegate> delegate_;
  size_t number_of_pending_is_ready_to_pay_queries_ = 0;
  base::WeakPtr<AndroidAppCommunication> communication_;

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

}  // namespace

AndroidPaymentAppFactory::AndroidPaymentAppFactory(
    base::WeakPtr<AndroidAppCommunication> communication)
    : PaymentAppFactory(PaymentApp::Type::NATIVE_MOBILE_APP),
      communication_(communication) {
  DCHECK(communication_);
}

AndroidPaymentAppFactory::~AndroidPaymentAppFactory() = default;

void AndroidPaymentAppFactory::Create(base::WeakPtr<Delegate> delegate) {
  content::WebContents* web_contents = delegate->GetWebContents();
  if (web_contents) {
    auto app_finder = AppFinder::CreateAndSetOwnedBy(web_contents);
    app_finder->FindApps(communication_, delegate);
  }
}

}  // namespace payments
