// 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 "chrome/browser/policy/device_account_initializer.h"

#include <utility>

#include "base/base64.h"
#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/single_thread_task_runner.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 "components/policy/core/common/cloud/dm_auth.h"
#include "google_apis/gaia/gaia_auth_util.h"
#include "google_apis/gaia/gaia_urls.h"
#include "net/http/http_status_code.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"

namespace em = enterprise_management;

namespace policy {

DeviceAccountInitializer::DeviceAccountInitializer(
    scoped_refptr<network::SharedURLLoaderFactory> shared_url_loader_factory,
    CloudPolicyClient* client,
    Delegate* delegate)
    : shared_url_loader_factory_(std::move(shared_url_loader_factory)),
      client_(client),
      delegate_(delegate),
      handling_request_(false) {
  CHECK(shared_url_loader_factory_);

  client_->AddObserver(this);
}

DeviceAccountInitializer::~DeviceAccountInitializer() {
  Stop();
  client_->RemoveObserver(this);
}

void DeviceAccountInitializer::FetchToken() {
  CHECK(client_->is_registered());
  handling_request_ = true;
  client_->FetchRobotAuthCodes(
      DMAuth::FromDMToken(client_->dm_token()),
      delegate_->GetRobotAuthCodeDeviceType(), delegate_->GetRobotOAuthScopes(),
      base::BindOnce(&DeviceAccountInitializer::OnRobotAuthCodesFetched,
                     weak_ptr_factory_.GetWeakPtr()));
}

void DeviceAccountInitializer::OnRobotAuthCodesFetched(
    DeviceManagementStatus status,
    const std::string& auth_code) {
  if (status != DM_STATUS_SUCCESS) {
    handling_request_ = false;
    delegate_->OnDeviceAccountTokenFetchError(status);
    return;
  }
  if (auth_code.empty()) {
    // If the server doesn't provide an auth code, skip the robot auth setup.
    // This allows clients running against the test server to transparently skip
    // robot auth.
    handling_request_ = false;
    delegate_->OnDeviceAccountTokenFetched(true);
    return;
  }

  gaia::OAuthClientInfo client_info;
  client_info.client_id = GaiaUrls::GetInstance()->oauth2_chrome_client_id();
  client_info.client_secret =
      GaiaUrls::GetInstance()->oauth2_chrome_client_secret();
  client_info.redirect_uri = "oob";

  // Use the system request context to avoid sending user cookies.
  gaia_oauth_client_ =
      std::make_unique<gaia::GaiaOAuthClient>(shared_url_loader_factory_);
  gaia_oauth_client_->GetTokensFromAuthCode(client_info, auth_code,
                                            0 /* max_retries */, this);
}

// GaiaOAuthClient::Delegate callback for OAuth2 refresh token fetched.
void DeviceAccountInitializer::OnGetTokensResponse(
    const std::string& refresh_token,
    const std::string& access_token,
    int expires_in_seconds) {
  robot_refresh_token_ = refresh_token;
  handling_request_ = false;
  delegate_->OnDeviceAccountTokenFetched(false);
}

// GaiaOAuthClient::Delegate
void DeviceAccountInitializer::OnRefreshTokenResponse(
    const std::string& access_token,
    int expires_in_seconds) {
  // We never use the code that should trigger this callback.
  handling_request_ = false;
  LOG(FATAL) << "Unexpected callback invoked.";
}

// GaiaOAuthClient::Delegate OAuth2 error when fetching refresh token request.
void DeviceAccountInitializer::OnOAuthError() {
  // OnOAuthError is only called if the request is bad (malformed) or the
  // response is bad (empty access token returned).
  LOG(ERROR) << "OAuth protocol error while fetching API refresh token.";
  handling_request_ = false;
  delegate_->OnDeviceAccountTokenFetchError(/*dm_status=*/std::nullopt);
}

// GaiaOAuthClient::Delegate network error when fetching refresh token.
void DeviceAccountInitializer::OnNetworkError(int response_code) {
  LOG(ERROR) << "Network error while fetching API refresh token: "
             << response_code;
  handling_request_ = false;
  delegate_->OnDeviceAccountTokenFetchError(/*dm_status=*/std::nullopt);
}

void DeviceAccountInitializer::StoreToken() {
  handling_request_ = true;
  DeviceOAuth2TokenServiceFactory::Get()->SetAndSaveRefreshToken(
      robot_refresh_token_,
      base::BindOnce(&DeviceAccountInitializer::HandleStoreRobotAuthTokenResult,
                     weak_ptr_factory_.GetWeakPtr()));
}

void DeviceAccountInitializer::HandleStoreRobotAuthTokenResult(bool result) {
  handling_request_ = false;
  if (!result) {
    LOG(ERROR) << "Failed to store API refresh token.";
    delegate_->OnDeviceAccountTokenStoreError();
    return;
  }
  delegate_->OnDeviceAccountTokenStored();
}

void DeviceAccountInitializer::Stop() {
  handling_request_ = false;
  weak_ptr_factory_.InvalidateWeakPtrs();
}

void DeviceAccountInitializer::OnClientError(CloudPolicyClient* client) {
  if (!handling_request_)
    return;
  DCHECK_EQ(client_, client);
  handling_request_ = false;
  delegate_->OnDeviceAccountClientError(client->last_dm_status());
}

}  // namespace policy
