// Copyright 2012 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/extensions/api/identity/identity_api.h"

#include <stddef.h>

#include <algorithm>
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include <vector>

#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/lazy_instance.h"
#include "base/strings/string_number_conversions.h"
#include "base/trace_event/trace_event.h"
#include "base/values.h"
#include "build/build_config.h"
#include "chrome/browser/app_mode/app_mode_utils.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/signin/account_consistency_mode_manager.h"
#include "chrome/browser/signin/chrome_signin_client_factory.h"
#include "chrome/browser/signin/identity_manager_factory.h"
#include "chrome/browser/ui/browser_window/public/browser_window_features.h"
#include "chrome/common/extensions/api/identity.h"
#include "chrome/common/url_constants.h"
#include "components/signin/public/base/consent_level.h"
#include "components/signin/public/identity_manager/primary_account_change_event.h"
#include "extensions/browser/extension_function_dispatcher.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/buildflags/buildflags.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_l10n_util.h"
#include "extensions/common/manifest_handlers/oauth2_manifest_handler.h"
#include "extensions/common/permissions/permission_set.h"
#include "extensions/common/permissions/permissions_data.h"
#include "google_apis/gaia/gaia_id.h"
#include "google_apis/gaia/gaia_urls.h"
#include "url/gurl.h"

#if BUILDFLAG(ENABLE_DICE_SUPPORT)
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/scoped_tabbed_browser_displayer.h"
#include "chrome/browser/ui/signin/signin_view_controller.h"  // nogncheck crbug.com/423799622
#endif

static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE));

using signin::ConsentLevel;
using signin::PrimaryAccountChangeEvent;

namespace extensions {

namespace {
const char kIdentityGaiaIdPref[] = "identity_gaia_id";
}

IdentityAPI::IdentityAPI(content::BrowserContext* context)
    : IdentityAPI(Profile::FromBrowserContext(context),
                  IdentityManagerFactory::GetForProfile(
                      Profile::FromBrowserContext(context)),
                  ExtensionPrefs::Get(context),
                  EventRouter::Get(context)) {}

IdentityAPI::~IdentityAPI() = default;

IdentityMintRequestQueue* IdentityAPI::mint_queue() { return &mint_queue_; }

IdentityTokenCache* IdentityAPI::token_cache() {
  return &token_cache_;
}

void IdentityAPI::SetGaiaIdForExtension(const std::string& extension_id,
                                        const GaiaId& gaia_id) {
  DCHECK(!gaia_id.empty());
  extension_prefs_->UpdateExtensionPref(extension_id, kIdentityGaiaIdPref,
                                        base::Value(gaia_id.ToString()));
}

std::optional<GaiaId> IdentityAPI::GetGaiaIdForExtension(
    const std::string& extension_id) {
  std::string gaia_id;
  if (!extension_prefs_->ReadPrefAsString(extension_id, kIdentityGaiaIdPref,
                                          &gaia_id)) {
    return std::nullopt;
  }
  return GaiaId(gaia_id);
}

void IdentityAPI::EraseGaiaIdForExtension(const std::string& extension_id) {
  extension_prefs_->UpdateExtensionPref(extension_id, kIdentityGaiaIdPref,
                                        std::nullopt);
}

void IdentityAPI::EraseStaleGaiaIdsForAllExtensions() {
  // Refresh tokens haven't been loaded yet. Wait for OnRefreshTokensLoaded() to
  // fire.
  if (!identity_manager_->AreRefreshTokensLoaded()) {
    return;
  }
  auto accounts = GetAccountsWithRefreshTokensForExtensions();
  for (const ExtensionId& extension_id : extension_prefs_->GetExtensions()) {
    std::optional<GaiaId> gaia_id = GetGaiaIdForExtension(extension_id);
    if (!gaia_id) {
      continue;
    }
    if (!std::ranges::contains(accounts, *gaia_id, &CoreAccountInfo::gaia)) {
      EraseGaiaIdForExtension(extension_id);
    }
  }
}

base::ScopedClosureRunner IdentityAPI::StartTrackingWebAuthFlow(
    const extensions::ExtensionId& extension_id) {
  if (active_web_auth_flows_.insert(extension_id).second) {
    return base::ScopedClosureRunner(
        base::BindOnce(&IdentityAPI::StopTrackingWebAuthFlow,
                       weak_ptr_factory_.GetWeakPtr(), extension_id));
  }
  return base::ScopedClosureRunner();
}

void IdentityAPI::StopTrackingWebAuthFlow(
    const extensions::ExtensionId& extension_id) {
  active_web_auth_flows_.erase(extension_id);
}

void IdentityAPI::Shutdown() {
  on_shutdown_callback_list_.Notify();
  identity_manager_->RemoveObserver(this);
}

static base::LazyInstance<BrowserContextKeyedAPIFactory<IdentityAPI>>::
    DestructorAtExit g_identity_api_factory = LAZY_INSTANCE_INITIALIZER;

// static
BrowserContextKeyedAPIFactory<IdentityAPI>* IdentityAPI::GetFactoryInstance() {
  return g_identity_api_factory.Pointer();
}

base::CallbackListSubscription IdentityAPI::RegisterOnShutdownCallback(
    base::OnceClosure cb) {
  return on_shutdown_callback_list_.Add(std::move(cb));
}

bool IdentityAPI::AreExtensionsRestrictedToPrimaryAccount() {
  return !AccountConsistencyModeManager::IsDiceEnabledForProfile(profile_) &&
         !AccountConsistencyModeManager::IsMirrorEnabledForProfile(profile_);
}

bool IdentityAPI::HasAccessToChromeAccounts() const {
  return identity_manager_->HasPrimaryAccount(ConsentLevel::kSignin);
}

std::vector<CoreAccountInfo>
IdentityAPI::GetAccountsWithRefreshTokensForExtensions() {
  if (!HasAccessToChromeAccounts()) {
    return {};
  }
  return identity_manager_->GetAccountsWithRefreshTokens();
}

#if BUILDFLAG(ENABLE_DICE_SUPPORT)
void IdentityAPI::MaybeShowChromeSigninDialog(
    const std::u16string& extension_name_for_display,
    base::OnceClosure on_complete) {
  if (HasAccessToChromeAccounts() ||
      identity_manager_->GetAccountsWithRefreshTokens().empty()) {
    DVLOG(1) << "The user is not signed in on the web!";
    std::move(on_complete).Run();
    return;
  }

  if (is_chrome_signin_dialog_open_) {
    DVLOG(1) << "Chrome sign in dialog is already open, extensions are not "
                "allowed to trigger more than one";
    on_chrome_signin_dialog_completed_.push_back(std::move(on_complete));
    return;
  }

  // Used in unittests to avoid creating a browser.
  if (skip_ui_for_testing_callback_) {
    HandleSkipUIForTesting(std::move(on_complete));  // IN-TEST
    return;
  }

  chrome::ScopedTabbedBrowserDisplayer displayer(profile_);
  BrowserWindowInterface* browser = displayer.browser_window_interface();
  if (!browser) {
    DVLOG(1) << "Could not create a browser to show Extensions Chrome Sign in "
                "dialog.";
    std::move(on_complete).Run();
    return;
  }
  on_chrome_signin_dialog_completed_.push_back(std::move(on_complete));
  is_chrome_signin_dialog_open_ = true;
  browser->GetFeatures()
      .signin_view_controller()
      ->MaybeShowChromeSigninDialogForExtensions(
          extension_name_for_display,
          base::BindOnce(&IdentityAPI::OnChromeSigninDialogDestroyed,
                         weak_ptr_factory_.GetWeakPtr()));
}

void IdentityAPI::OnChromeSigninDialogDestroyed() {
  is_chrome_signin_dialog_open_ = false;
  std::vector<base::OnceClosure> callbacks;
  std::swap(on_chrome_signin_dialog_completed_, callbacks);
  for (auto& callback : callbacks) {
    std::move(callback).Run();
  }
}

void IdentityAPI::HandleSkipUIForTesting(base::OnceClosure on_complete) {
  is_chrome_signin_dialog_open_ = true;
  on_chrome_signin_dialog_completed_.push_back(std::move(on_complete));
  // Allow tests to complete the flow.
  std::move(skip_ui_for_testing_callback_)
      .Run(base::BindOnce(&IdentityAPI::OnChromeSigninDialogDestroyed,
                          weak_ptr_factory_.GetWeakPtr()));
  return;
}
#endif

IdentityAPI::IdentityAPI(Profile* profile,
                         signin::IdentityManager* identity_manager,
                         ExtensionPrefs* extension_prefs,
                         EventRouter* event_router)
    : profile_(profile),
      identity_manager_(identity_manager),
      extension_prefs_(extension_prefs),
      event_router_(event_router) {
  identity_manager_->AddObserver(this);
  EraseStaleGaiaIdsForAllExtensions();
}

void IdentityAPI::OnPrimaryAccountChanged(
    const PrimaryAccountChangeEvent& event_details) {
  switch (event_details.GetEventTypeFor(ConsentLevel::kSignin)) {
    case signin::PrimaryAccountChangeEvent::Type::kNone:
      break;
    case signin::PrimaryAccountChangeEvent::Type::kSet:
      for (auto& account : GetAccountsWithRefreshTokensForExtensions()) {
        OnRefreshTokenUpdatedForAccount(account);
      }
      break;
    case signin::PrimaryAccountChangeEvent::Type::kCleared:
      EraseStaleGaiaIdsForAllExtensions();
      base::flat_set<GaiaId> tracked_accounts;
      std::swap(tracked_accounts, accounts_known_to_extensions_);
      for (auto& account_id : tracked_accounts) {
        FireOnAccountSignInChanged(account_id, /*is_signed_in=*/false);
      }
      break;
  }
}

void IdentityAPI::OnRefreshTokensLoaded() {
  EraseStaleGaiaIdsForAllExtensions();
}

void IdentityAPI::OnRefreshTokenUpdatedForAccount(
    const CoreAccountInfo& account_info) {
  // Refresh tokens are sometimes made available in contexts where
  // AccountTrackerService is not tracking the account in question. Bail out in
  // these cases.
  if (!HasAccessToChromeAccounts() || account_info.gaia.empty()) {
    return;
  }
  accounts_known_to_extensions_.insert(account_info.gaia);
  FireOnAccountSignInChanged(account_info.gaia, true);
}

void IdentityAPI::OnExtendedAccountInfoRemoved(
    const AccountInfo& account_info) {
  DCHECK(!account_info.gaia.empty());
  EraseStaleGaiaIdsForAllExtensions();

  auto it = accounts_known_to_extensions_.find(account_info.gaia);
  if (it == accounts_known_to_extensions_.end()) {
    // Account unknown to Extensions.
    return;
  }
  accounts_known_to_extensions_.erase(it);
  FireOnAccountSignInChanged(account_info.gaia, false);
}

void IdentityAPI::FireOnAccountSignInChanged(const GaiaId& gaia_id,
                                             bool is_signed_in) {
  CHECK(!gaia_id.empty());
  api::identity::AccountInfo api_account_info;
  api_account_info.id = gaia_id.ToString();

  auto args =
      api::identity::OnSignInChanged::Create(api_account_info, is_signed_in);
  std::unique_ptr<Event> event(new Event(
      events::IDENTITY_ON_SIGN_IN_CHANGED,
      api::identity::OnSignInChanged::kEventName, std::move(args), profile_));

  if (on_signin_changed_callback_for_testing_) {
    on_signin_changed_callback_for_testing_.Run(event.get());
  }

  event_router_->BroadcastEvent(std::move(event));
}

template <>
void BrowserContextKeyedAPIFactory<IdentityAPI>::DeclareFactoryDependencies() {
  DependsOn(ExtensionsBrowserClient::Get()->GetExtensionSystemFactory());

  DependsOn(ChromeSigninClientFactory::GetInstance());
  DependsOn(IdentityManagerFactory::GetInstance());
}

}  // namespace extensions
