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

#include "chrome/browser/policy/cbcm_invalidations_initializer.h"

#include "base/memory/raw_ptr.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/device_identity/device_oauth2_token_service.h"
#include "chrome/browser/device_identity/device_oauth2_token_service_factory.h"
#include "chrome/browser/net/system_network_context_manager.h"
#include "chrome/browser/policy/device_account_initializer.h"
#include "google_apis/gaia/gaia_constants.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"

namespace policy {

// A helper class to make the appropriate calls into the device account
// initializer and manage the ChromeBrowserCloudManagementRegistrar callback's
// lifetime.
class CBCMInvalidationsInitializer::MachineLevelDeviceAccountInitializerHelper
    : public DeviceAccountInitializer::Delegate {
 public:
  using Callback = base::OnceCallback<void(bool)>;

  // |policy_client| should be registered and outlive this object.
  MachineLevelDeviceAccountInitializerHelper(
      const std::string& service_account_email,
      policy::CloudPolicyClient* policy_client,
      Callback callback,
      scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory)
      : service_account_email_(service_account_email),
        policy_client_(policy_client),
        callback_(std::move(callback)) {
    CHECK(url_loader_factory);
    device_account_initializer_ = std::make_unique<DeviceAccountInitializer>(
        std::move(url_loader_factory), policy_client_, this);
    device_account_initializer_->FetchToken();
  }

  MachineLevelDeviceAccountInitializerHelper& operator=(
      MachineLevelDeviceAccountInitializerHelper&) = delete;
  MachineLevelDeviceAccountInitializerHelper(
      MachineLevelDeviceAccountInitializerHelper&) = delete;
  MachineLevelDeviceAccountInitializerHelper(
      MachineLevelDeviceAccountInitializerHelper&&) = delete;

  ~MachineLevelDeviceAccountInitializerHelper() override = default;

  // DeviceAccountInitializer::Delegate:
  void OnDeviceAccountTokenFetched(bool empty_token) override {
    if (empty_token) {
      // Not being able to obtain a token isn't a showstopper for machine
      // level policies: the browser will fallback to fetching policies on a
      // regular schedule and won't support remote commands. Getting a refresh
      // token will be reattempted on the next successful policy fetch.
      std::move(callback_).Run(false);
      return;
    }

    device_account_initializer_->StoreToken();
  }

  void OnDeviceAccountTokenStored() override {
    // When the token is stored, the account init procedure is complete and
    // it's now time to save the associated email address.
    DeviceOAuth2TokenServiceFactory::Get()->SetServiceAccountEmail(
        service_account_email_);
    std::move(callback_).Run(true);
  }

  void OnDeviceAccountTokenFetchError(
      std::optional<DeviceManagementStatus> /*dm_status*/) override {
    std::move(callback_).Run(false);
  }

  void OnDeviceAccountTokenStoreError() override {
    std::move(callback_).Run(false);
  }

  void OnDeviceAccountClientError(DeviceManagementStatus status) override {
    std::move(callback_).Run(false);
  }

  enterprise_management::DeviceServiceApiAccessRequest::DeviceType
  GetRobotAuthCodeDeviceType() override {
    return enterprise_management::DeviceServiceApiAccessRequest::CHROME_BROWSER;
  }

  std::set<std::string> GetRobotOAuthScopes() override {
    return {
        GaiaConstants::kGoogleUserInfoEmail,
        GaiaConstants::kFCMOAuthScope,
    };
  }

  std::string service_account_email_;
  raw_ptr<policy::CloudPolicyClient> policy_client_;
  std::unique_ptr<DeviceAccountInitializer> device_account_initializer_;
  Callback callback_;
};

CBCMInvalidationsInitializer::CBCMInvalidationsInitializer(Delegate* delegate)
    : delegate_(delegate) {}

CBCMInvalidationsInitializer::~CBCMInvalidationsInitializer() = default;

void CBCMInvalidationsInitializer::OnServiceAccountSet(
    CloudPolicyClient* client,
    const std::string& account_email) {
  // If there's no invalidations service active yet, now's the time to start it.
  // It will be notified when the service account for is ready to be used.
  if (!delegate_->IsInvalidationsServiceStarted())
    delegate_->StartInvalidations();

  // If there's no refresh token when a policy has a service account, or the
  // service account in the policy doesn't match the one in the token service,
  // the service account has to be initialized to the one in the policy.
  if (!DeviceOAuth2TokenServiceFactory::Get()->RefreshTokenIsAvailable() ||
      DeviceOAuth2TokenServiceFactory::Get()->GetRobotAccountId() !=
          CoreAccountId::FromRobotEmail(account_email)) {
    // Initialize the device service account and fetch auth codes to exchange
    // for a refresh token. Creating this object starts that process and the
    // callback will be called from it whether it succeeds or not.
    account_initializer_helper_ =
        std::make_unique<MachineLevelDeviceAccountInitializerHelper>(
            account_email, client,
            base::BindOnce(&CBCMInvalidationsInitializer::AccountInitCallback,
                           base::Unretained(this), account_email),
            delegate_->GetURLLoaderFactory()
                ? delegate_->GetURLLoaderFactory()
                : g_browser_process->system_network_context_manager()
                      ->GetSharedURLLoaderFactory());
  }
}

void CBCMInvalidationsInitializer::AccountInitCallback(
    const std::string& account_email,
    bool success) {
  account_initializer_helper_.reset();
  if (!success) {
    DVLOG(1)
        << "There was an error initializing the service account with email: "
        << account_email;
  }
}

}  // namespace policy
