// Copyright 2015 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/signin/internal/identity_manager/account_fetcher_service.h"

#include <string>
#include <utility>
#include <vector>

#include "base/command_line.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/metrics/field_trial.h"
#include "base/metrics/histogram_functions.h"
#include "base/notreached.h"
#include "base/trace_event/trace_event.h"
#include "base/values.h"
#include "build/build_config.h"
#include "components/image_fetcher/core/image_decoder.h"
#include "components/image_fetcher/core/image_fetcher_impl.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/signin/internal/identity_manager/account_capabilities_fetcher.h"
#include "components/signin/internal/identity_manager/account_fetcher_factory.h"
#include "components/signin/internal/identity_manager/account_info_fetcher.h"
#include "components/signin/internal/identity_manager/account_info_util.h"
#include "components/signin/internal/identity_manager/account_tracker_service.h"
#include "components/signin/internal/identity_manager/profile_oauth2_token_service.h"
#include "components/signin/public/base/avatar_icon_util.h"
#include "components/signin/public/base/signin_client.h"
#include "components/signin/public/base/signin_switches.h"
#include "components/signin/public/identity_manager/account_capabilities.h"
#include "components/signin/public/identity_manager/account_info.h"
#include "net/http/http_status_code.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"

#if BUILDFLAG(IS_CHROMEOS)
#include "ash/constants/ash_features.h"
#endif
namespace {

const base::TimeDelta kRefreshFromTokenServiceDelay = base::Hours(24);

}  // namespace

const char kImageFetcherUmaClient[] = "AccountFetcherService";

// This pref used to be in the AccountTrackerService, hence its string value.
const char AccountFetcherService::kLastUpdatePref[] =
    "account_tracker_service_last_update";

// AccountFetcherService implementation
AccountFetcherService::AccountFetcherService() = default;

AccountFetcherService::~AccountFetcherService() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
}

// static
void AccountFetcherService::RegisterPrefs(PrefRegistrySimple* user_prefs) {
  user_prefs->RegisterTimePref(AccountFetcherService::kLastUpdatePref,
                               base::Time());
}

void AccountFetcherService::Initialize(
    SigninClient* signin_client,
    ProfileOAuth2TokenService* token_service,
    AccountTrackerService* account_tracker_service,
    std::unique_ptr<image_fetcher::ImageDecoder> image_decoder,
    std::unique_ptr<AccountFetcherFactory> account_fetcher_factory) {
  DCHECK(signin_client);
  DCHECK(!signin_client_);
  signin_client_ = signin_client;
  DCHECK(account_tracker_service);
  DCHECK(!account_tracker_service_);
  account_tracker_service_ = account_tracker_service;
  DCHECK(token_service);
  DCHECK(!token_service_);
  token_service_ = token_service;
  token_service_observation_.Observe(token_service_);

  DCHECK(image_decoder);
  DCHECK(!image_decoder_);
  image_decoder_ = std::move(image_decoder);
  DCHECK(!account_fetcher_factory_);
  DCHECK(account_fetcher_factory);
  account_fetcher_factory_ = std::move(account_fetcher_factory);

  // Tokens may have already been loaded and we will not receive a
  // notification-on-registration for |token_service_->AddObserver(this)| few
  // lines above.
  if (token_service_->AreAllCredentialsLoaded()) {
    OnRefreshTokensLoaded();
  }
}

bool AccountFetcherService::IsAllUserInfoFetched() const {
  return user_info_requests_.empty();
}

bool AccountFetcherService::AreAllAccountCapabilitiesFetched() const {
  return account_capabilities_requests_.empty();
}

void AccountFetcherService::OnNetworkInitialized() {
  DCHECK(!network_initialized_);
  DCHECK(!network_fetches_enabled_);
  network_initialized_ = true;
  MaybeEnableNetworkFetches();
}

void AccountFetcherService::EnableNetworkFetchesForTest() {
  if (!network_initialized_) {
    OnNetworkInitialized();
  }

  if (!refresh_tokens_loaded_) {
    OnRefreshTokensLoaded();
  }
}

void AccountFetcherService::EnableAccountRemovalForTest() {
  enable_account_removal_for_test_ = true;
}

AccountFetcherFactory*
AccountFetcherService::GetAccountFetcherFactoryForTest() {
  return account_fetcher_factory_.get();
}

void AccountFetcherService::RefreshAllAccountInfo(bool only_fetch_if_invalid) {
  for (const auto& account : token_service_->GetAccounts()) {
    RefreshAccountInfo(account, only_fetch_if_invalid);
  }
}

// Child account status is refreshed through invalidations which are only
// available for the primary account. Finding the primary account requires a
// dependency on PrimaryAccountManager which we get around by only allowing a
// single account. This is possible since we only support a single account to be
// a child anyway.
#if BUILDFLAG(IS_ANDROID)
void AccountFetcherService::RefreshAccountInfoIfStale(
    const CoreAccountId& account_id) {
  DCHECK(network_fetches_enabled_);
  RefreshAccountInfo(account_id, /*only_fetch_if_invalid=*/true);
}
#endif

void AccountFetcherService::MaybeEnableNetworkFetches() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (!network_initialized_ || !refresh_tokens_loaded_) {
    return;
  }

  if (!network_fetches_enabled_) {
    network_fetches_enabled_ = true;
    CHECK(!repeating_timer_);
    CHECK(!persistent_repeating_timer_);
    if (base::FeatureList::IsEnabled(switches::kFetchAccountInfoOnRestart)) {
      // Schedule a fetch kRefreshFromTokenServiceDelay from now.
      repeating_timer_ = std::make_unique<base::RepeatingTimer>();
      repeating_timer_->Start(
          FROM_HERE, kRefreshFromTokenServiceDelay,
          base::BindRepeating(&AccountFetcherService::RefreshAllAccountInfo,
                              base::Unretained(this),
                              /*only_fetch_if_invalid=*/false));
    } else {
      // Schedule a fetch kRefreshFromTokenServiceDelay from the last fetch
      // time.
      persistent_repeating_timer_ =
          std::make_unique<signin::PersistentRepeatingTimer>(
              signin_client_->GetPrefs(),
              AccountFetcherService::kLastUpdatePref,
              kRefreshFromTokenServiceDelay,
              base::BindRepeating(&AccountFetcherService::RefreshAllAccountInfo,
                                  base::Unretained(this),
                                  /*only_fetch_if_invalid=*/false));
      persistent_repeating_timer_->Start();
    }
  }

  // If kFetchAccountInfoOnRestart is enabled, fetch account info
  // unconditionally. Otherwise, only fetch if the account info is invalid.
  bool only_fetch_if_invalid =
      !base::FeatureList::IsEnabled(switches::kFetchAccountInfoOnRestart);
  RefreshAllAccountInfo(only_fetch_if_invalid);
}

// Starts fetching user information. This is called periodically to refresh.
void AccountFetcherService::StartFetchingUserInfo(
    const CoreAccountId& account_id) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK(network_fetches_enabled_);

  if (!user_info_requests_.contains(account_id)) {
    DVLOG(1) << "StartFetching " << account_id;
    user_info_fetch_start_times_[account_id] = base::TimeTicks::Now();
    auto [it, inserted] = user_info_requests_.emplace(
        account_id,
        account_fetcher_factory_->CreateAccountInfoFetcher(
            account_id,
            base::BindOnce(&AccountFetcherService::OnUserInfoFetchCompleted,
                           base::Unretained(this), account_id)));
    CHECK(inserted);
    it->second->Start();
  }
}

void AccountFetcherService::DestroyFetchers(const CoreAccountId& account_id) {
  user_info_requests_.erase(account_id);
  account_capabilities_requests_.erase(account_id);
}

void AccountFetcherService::PrepareForFetchingAccountCapabilities() {
  account_fetcher_factory_->PrepareForFetchingAccountCapabilities();
}

void AccountFetcherService::StartFetchingAccountCapabilities(
    const CoreAccountInfo& core_account_info) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK(network_fetches_enabled_);

  std::unique_ptr<AccountCapabilitiesFetcher>& request =
      account_capabilities_requests_[core_account_info.account_id];
  if (!request) {
    AccountInfo account_info =
        account_tracker_service_->GetAccountInfo(core_account_info.account_id);

    request = account_fetcher_factory_->CreateAccountCapabilitiesFetcher(
        core_account_info,
        account_info.GetAccountCapabilities().AreAnyCapabilitiesKnown()
            ? AccountCapabilitiesFetcher::FetchPriority::kBackground
            : AccountCapabilitiesFetcher::FetchPriority::kForeground,
        base::BindRepeating(
            &AccountFetcherService::OnSomeAccountCapabilitiesFetched,
            base::Unretained(this)),
        base::BindOnce(
            &AccountFetcherService::OnAccountCapabilitiesFetchComplete,
            base::Unretained(this)));
    request->Start();
  }
}

void AccountFetcherService::RefreshAccountInfo(const CoreAccountId& account_id,
                                               bool only_fetch_if_invalid) {
  DCHECK(network_fetches_enabled_);

  // TODO(crbug.com/40283608): It seems quite suspect account tracker needs to
  // start tracking the account when refreshing the account info. Understand why
  // this is needed and ideally remove this call (it may have been added just
  // for tests).
  base::UmaHistogramBoolean(
      "Signin.AccountTracker.RefreshAccountInfo.IsAlreadyTrackingAccount",
      account_tracker_service_->IsTrackingAccount(account_id));
  account_tracker_service_->StartTrackingAccount(account_id);

  AccountInfo info = account_tracker_service_->GetAccountInfo(account_id);

  if (!only_fetch_if_invalid ||
      !info.GetAccountCapabilities().AreAllCapabilitiesKnown()) {
    StartFetchingAccountCapabilities(info);
  }

  // |only_fetch_if_invalid| is false when the service is due for a timed
  // update.
  if (!only_fetch_if_invalid || !info.IsValid()) {
    // Fetching the user info will also fetch the account image.
    StartFetchingUserInfo(account_id);
    return;
  }

  // User info is already valid and does not need to be downloaded again.
  // Fetch the account image in case it was not fetched previously.
  //
  // Note: |FetchAccountImage()| does not fetch the account image if the
  // account image was already downloaded. So it is fine to call this method
  // even when |only_fetch_if_invalid| is true.
  FetchAccountImage(account_id);
}

void AccountFetcherService::OnUserInfoFetchCompleted(
    const CoreAccountId& account_id,
    std::optional<AccountInfo> fetched_account_info) {
  if (!fetched_account_info) {
    LOG(WARNING) << "Failed to get UserInfo for " << account_id;
    user_info_fetch_start_times_.erase(account_id);
    // |account_id| is owned by the request. Cannot be used after this line.
    user_info_requests_.erase(account_id);
    return;
  }

  account_tracker_service_->SetAccountInfoFromUserInfo(account_id,
                                                       *fetched_account_info);
  auto it = user_info_fetch_start_times_.find(account_id);
  if (it != user_info_fetch_start_times_.end()) {
    base::UmaHistogramMediumTimes(
        "Signin.AccountFetcher.AccountUserInfoFetchTime",
        base::TimeTicks::Now() - it->second);
    user_info_fetch_start_times_.erase(it);
  }
  FetchAccountImage(account_id);
  user_info_requests_.erase(account_id);
}

image_fetcher::ImageFetcherImpl*
AccountFetcherService::GetOrCreateImageFetcher() {
  // Lazy initialization of |image_fetcher_| because the request context might
  // not be available yet when |Initialize| is called.
  if (!image_fetcher_) {
    image_fetcher_ = std::make_unique<image_fetcher::ImageFetcherImpl>(
        std::move(image_decoder_), signin_client_->GetURLLoaderFactory());
  }
  return image_fetcher_.get();
}

void AccountFetcherService::FetchAccountImage(const CoreAccountId& account_id) {
  DCHECK(signin_client_);
  AccountInfo account_info =
      account_tracker_service_->GetAccountInfo(account_id);
  if (!account_info.GetAvatarUrl().has_value()) {
    return;
  }

  GURL picture_url(*account_info.GetAvatarUrl());
  if (!picture_url.is_valid()) {
    DVLOG(1) << "Invalid avatar picture URL: \"" << *account_info.GetAvatarUrl()
             << "\"";
    return;
  }
  GURL image_url_with_size(signin::GetAvatarImageURLWithOptions(
      picture_url, signin::kAccountInfoImageSize, true /* no_silhouette */));

  if (image_url_with_size.spec() ==
      account_info.GetLastDownloadedAvatarUrlWithSize()) {
    return;
  }

  net::NetworkTrafficAnnotationTag traffic_annotation =
      net::DefineNetworkTrafficAnnotation("accounts_image_fetcher", R"(
        semantics {
          sender: "Image fetcher for GAIA accounts"
          description:
            "To use a GAIA web account to log into Chrome in the user menu, the"
            "account images of the signed-in GAIA accounts are displayed."
          trigger: "At startup."
          data: "Account picture URL of signed-in GAIA accounts."
          destination: GOOGLE_OWNED_SERVICE
        }
        policy {
          cookies_allowed: YES
          cookies_store: "user"
          setting: "This feature cannot be disabled by settings, "
                   "however, it will only be requested if the user "
                   "has signed into the web."
          policy_exception_justification:
            "Not implemented, considered not useful as no content is being "
            "uploaded or saved; this request merely downloads the web account"
            "profile image."
        })");

  auto callback = base::BindOnce(&AccountFetcherService::OnImageFetched,
                                 base::Unretained(this), account_id,
                                 image_url_with_size.spec());
  image_fetcher::ImageFetcherParams params(traffic_annotation,
                                           kImageFetcherUmaClient);
  GetOrCreateImageFetcher()->FetchImage(image_url_with_size,
                                        std::move(callback), std::move(params));
}

void AccountFetcherService::OnSomeAccountCapabilitiesFetched(
    const CoreAccountId& account_id,
    const AccountCapabilities& account_capabilities) {
  account_tracker_service_->SetAccountCapabilities(account_id,
                                                   account_capabilities);
}

void AccountFetcherService::OnAccountCapabilitiesFetchComplete(
    const CoreAccountId& account_id) {
  // |account_id| is owned by the request. Cannot be used after this line.
  account_capabilities_requests_.erase(account_id);
}

void AccountFetcherService::OnRefreshTokenAvailable(
    const CoreAccountId& account_id) {
  TRACE_EVENT1("AccountFetcherService",
               "AccountFetcherService::OnRefreshTokenAvailable", "account_id",
               account_id.ToString());
  DVLOG(1) << "AVAILABLE " << account_id;

  // The SigninClient needs a "final init" in order to perform some actions
  // (such as fetching the signin token "handle" in order to look for password
  // changes) once everything is initialized and the refresh token is present.
  signin_client_->DoFinalInit();

  if (!network_fetches_enabled_) {
    return;
  }
  RefreshAccountInfo(account_id, /*only_fetch_if_invalid=*/true);
}

void AccountFetcherService::OnRefreshTokenRevoked(
    const CoreAccountId& account_id) {
  TRACE_EVENT1("AccountFetcherService",
               "AccountFetcherService::OnRefreshTokenRevoked", "account_id",
               account_id.ToString());
  DVLOG(1) << "REVOKED " << account_id;

  // Short-circuit out if network fetches are not enabled.
  if (!network_fetches_enabled_) {
    if (enable_account_removal_for_test_) {
      account_tracker_service_->StopTrackingAccount(account_id);
    }
    return;
  }

  DestroyFetchers(account_id);
  account_tracker_service_->StopTrackingAccount(account_id);
}

void AccountFetcherService::OnRefreshTokensLoaded() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  refresh_tokens_loaded_ = true;
  MaybeEnableNetworkFetches();
}

void AccountFetcherService::OnImageFetched(
    const CoreAccountId& account_id,
    const std::string& image_url_with_size,
    const gfx::Image& image,
    const image_fetcher::RequestMetadata& metadata) {
  if (metadata.http_response_code != net::HTTP_OK) {
    DCHECK(image.IsEmpty());
    return;
  }
  account_tracker_service_->SetAccountImage(account_id, image_url_with_size,
                                            image);
}
