// 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 "chrome/browser/ui/webui/signin/profile_picker_handler.h"

#include <algorithm>
#include <variant>
#include <vector>

#include "base/check.h"
#include "base/check_deref.h"
#include "base/check_op.h"
#include "base/debug/dump_without_crashing.h"
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/json/values_util.h"
#include "base/metrics/histogram_functions.h"
#include "base/notreached.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/trace_event/trace_event.h"
#include "base/values.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/new_tab_page/chrome_colors/chrome_colors_service.h"
#include "chrome/browser/new_tab_page/chrome_colors/generated_colors_info.h"
#include "chrome/browser/profiles/keep_alive/profile_keep_alive_types.h"
#include "chrome/browser/profiles/keep_alive/scoped_profile_keep_alive.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_attributes_entry.h"
#include "chrome/browser/profiles/profile_attributes_storage.h"
#include "chrome/browser/profiles/profile_avatar_icon_util.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/profiles/profile_statistics.h"
#include "chrome/browser/profiles/profile_statistics_factory.h"
#include "chrome/browser/profiles/profile_window.h"
#include "chrome/browser/profiles/profiles_state.h"
#include "chrome/browser/signin/signin_util.h"
#include "chrome/browser/sync/sync_service_factory.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/color/chrome_color_id.h"
#include "chrome/browser/ui/profiles/profile_colors_util.h"
#include "chrome/browser/ui/profiles/profile_picker.h"
#include "chrome/browser/ui/profiles/profile_view_utils.h"
#include "chrome/browser/ui/ui_features.h"
#include "chrome/browser/ui/views/profiles/profile_management_types.h"
#include "chrome/browser/ui/webui/profile_helper.h"
#include "chrome/browser/ui/webui/signin/login_ui_service.h"
#include "chrome/browser/ui/webui/signin/login_ui_service_factory.h"
#include "chrome/browser/ui/webui/signin/signin_error_ui.h"
#include "chrome/browser/ui/webui/signin/signin_ui_error.h"
#include "chrome/browser/ui/webui/theme_source.h"
#include "chrome/common/pref_names.h"
#include "chrome/common/themes/autogenerated_theme_util.h"
#include "chrome/common/url_constants.h"
#include "chrome/common/webui_url_constants.h"
#include "chrome/grit/branded_strings.h"
#include "chrome/grit/generated_resources.h"
#include "components/prefs/pref_service.h"
#include "components/signin/public/base/signin_metrics.h"
#include "components/signin/public/base/signin_switches.h"
#include "components/signin/public/identity_manager/account_info.h"
#include "components/signin/public/identity_manager/tribool.h"
#include "components/startup_metric_utils/browser/startup_metric_utils.h"
#include "components/sync/base/features.h"
#include "content/public/browser/url_data_source.h"
#include "content/public/browser/web_ui.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/mojom/themes.mojom.h"
#include "ui/base/ui_base_features.h"
#include "ui/base/webui/web_ui_util.h"
#include "ui/color/color_id.h"
#include "ui/color/color_provider.h"
#include "ui/gfx/color_utils.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gfx/image/image_skia_rep.h"
#include "ui/webui/webui_util.h"

namespace {
const size_t kProfileCardAvatarSize = 74;
const size_t kProfileCreationAvatarSize = 100;

constexpr int kDefaultThemeColorId = -1;
constexpr int kManuallyPickedColorId = 0;

std::optional<SkColor> GetChromeColorColorById(int color_id) {
  for (chrome_colors::ColorInfo color_info :
       chrome_colors::kGeneratedColorsInfo) {
    if (color_id == color_info.id) {
      return color_info.color;
    }
  }

  return std::nullopt;
}

void RecordAskOnStartupChanged(bool value) {
  base::UmaHistogramBoolean("ProfilePicker.AskOnStartupChanged", value);
}

base::DictValue GetAutogeneratedProfileThemeInfoValue(
    int color_id,
    std::optional<SkColor> color,
    const ui::ColorProvider& color_provider,
    SkColor frame_color,
    SkColor active_tab_color,
    SkColor frame_text_color,
    float scale_factor) {
  base::DictValue dict;
  dict.Set("colorId", color_id);
  if (color.has_value()) {
    dict.Set("color", static_cast<int>(*color));
  }
  dict.Set("themeFrameColor", color_utils::SkColorToRgbaString(frame_color));
  dict.Set("themeShapeColor",
           color_utils::SkColorToRgbaString(active_tab_color));
  dict.Set("themeFrameTextColor",
           color_utils::SkColorToRgbaString(frame_text_color));
  DefaultAvatarColors avatar_colors =
      GetDefaultAvatarColors(color_provider, frame_color);
  gfx::Image icon = profiles::GetPlaceholderAvatarIconWithColors(
      /*fill_color=*/avatar_colors.fill_color,
      /*stroke_color=*/avatar_colors.stroke_color,
      kProfileCreationAvatarSize * scale_factor);
  dict.Set("themeGenericAvatar", webui::GetBitmapDataUrl(icon.AsBitmap()));
  return dict;
}

base::DictValue CreateDefaultProfileThemeInfo(
    const ui::ColorProvider& color_provider,
    float scale_factor) {
  SkColor frame_color = color_provider.GetColor(ui::kColorFrameActive);
  SkColor active_tab_color = color_provider.GetColor(kColorToolbar);
  SkColor frame_text_color =
      color_provider.GetColor(kColorTabForegroundInactiveFrameActive);
  return GetAutogeneratedProfileThemeInfoValue(
      kDefaultThemeColorId, std::nullopt, color_provider, frame_color,
      active_tab_color, frame_text_color, scale_factor);
}

base::DictValue CreateAutogeneratedProfileThemeInfo(
    int color_id,
    SkColor color,
    const ui::ColorProvider& color_provider,
    float scale_factor) {
  auto theme_colors = GetAutogeneratedThemeColors(color);
  SkColor frame_color = theme_colors.frame_color;
  SkColor active_tab_color = theme_colors.active_tab_color;
  SkColor frame_text_color = theme_colors.frame_text_color;
  return GetAutogeneratedProfileThemeInfoValue(color_id, color, color_provider,
                                               frame_color, active_tab_color,
                                               frame_text_color, scale_factor);
}

std::pair<std::string, bool> GetAvatarIconUrlAndAvatarRingStatus(
    const ProfileAttributesEntry* entry,
    int avatar_icon_size_dip,
    const ui::ColorProvider* color_provider,
    float scale) {
  int avatar_icon_size = avatar_icon_size_dip * scale;
  std::string icon_url;
  bool has_gradient_ring = false;
  if (base::FeatureList::IsEnabled(switches::kEnableAiSubscriptionAvatarRing) &&
      entry->GetAiSubscriptionTier() > 0 && color_provider) {
    // Note: For linear gradient ring to appear, the corresponding profile
    // needs to have been loaded at least once (with
    // kEnableAiSubscriptionAvatarRing), so that the corresponding profile
    // attribute entry is written. Updates to the AI subscription tier trigger
    // ProfileAttributesStorage::Observer::OnProfileAiSubscriptionTierUpdated
    // which refreshes the picker UI list.
    has_gradient_ring = true;

    gfx::ImageSkia avatar_skia = gfx::ImageSkia::CreateFromBitmap(
        entry->GetAvatarIcon(avatar_icon_size).AsBitmap(), scale);
    ui::ImageModel avatar_model = ui::ImageModel::FromImageSkia(avatar_skia);

    avatar_skia = AddLinearGradientRingToAvatar(
        avatar_model, *color_provider, avatar_icon_size_dip, kAvatarRingGapDip,
        kAvatarRingThicknessDip);

    SkBitmap bitmap = avatar_skia.GetRepresentation(scale).GetBitmap();
    return {webui::GetBitmapDataUrl(bitmap), has_gradient_ring};
  }
  gfx::Image icon =
      profiles::GetSizedAvatarIcon(entry->GetAvatarIcon(avatar_icon_size),
                                   avatar_icon_size, avatar_icon_size);
  return {webui::GetBitmapDataUrl(icon.AsBitmap()), has_gradient_ring};
}

// ProfileState is the dictionary used to map a `ProfileAttributesEntry` in JS.
// This directly maps to `ProfileState` in
// `chrome/browser/resources/signin/profile_picker/manage_profiles_browser_proxy.ts`.
base::DictValue CreateProfileState(const ProfileAttributesEntry* entry,
                                   int avatar_icon_size_dip,
                                   const ui::ColorProvider* color_provider,
                                   float scale) {
  base::DictValue profile_entry;
  profile_entry.Set("profilePath", base::FilePathToValue(entry->GetPath()));
  profile_entry.Set("localProfileName", entry->GetLocalProfileName());
  profile_entry.Set("hasEnterpriseLabel",
                    !entry->GetEnterpriseProfileLabel().empty());
  profile_entry.Set("isSyncing",
                    entry->GetSigninState() ==
                        SigninState::kSignedInWithConsentedPrimaryAccount);
  profile_entry.Set("needsSignin", entry->IsSigninRequired());
  // GAIA full name/user name can be empty, if the profile is not signed in to
  // chrome.
  profile_entry.Set("gaiaName", entry->GetGAIAName());
  profile_entry.Set("userName", entry->GetUserName());

  const auto local_profile_name = entry->GetLocalProfileName();
  std::u16string profileCardButtonLabel = l10n_util::GetStringFUTF16(
      IDS_PROFILE_PICKER_PROFILE_CARD_LABEL, local_profile_name);
  if (entry->GetIsManaged() == signin::Tribool::kTrue) {
    profile_entry.Set("avatarBadge", "cr:domain");
#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN)
  } else if (entry->IsSupervised()) {
    profileCardButtonLabel = l10n_util::GetStringFUTF16(
        IDS_PROFILE_PICKER_PROFILE_CARD_LABEL_SUPERVISED, local_profile_name);
    profile_entry.Set("avatarBadge", "cr:family-link");
#endif  // BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN)
  } else {
    profile_entry.Set("avatarBadge", "");
  }
  auto [icon_url, has_gradient_ring] = GetAvatarIconUrlAndAvatarRingStatus(
      entry, avatar_icon_size_dip, color_provider, scale);
  if (has_gradient_ring && !profileCardButtonLabel.empty()) {
    profileCardButtonLabel = l10n_util::GetStringFUTF16(
        IDS_PROFILE_AVATAR_NAME_WITH_AI_MEMBERSHIP, profileCardButtonLabel);
  }
  profile_entry.Set("profileCardButtonLabel", profileCardButtonLabel);
  profile_entry.Set("hasAvatarRing", has_gradient_ring);
  profile_entry.Set("avatarIcon", icon_url);
  return profile_entry;
}

// Opens a URL form the Chrome support Help Center based on the intended action
// from the user.
// Either Sign in to Chrome if there are no eligible profiles.
// Or add a new Profile if the existing profiles do not match the user's need.
void OpenLearnMoreURL(bool is_profile_list_empty,
                      BrowserWindowInterface* browser) {
  // Browser may be closing if the Profile was locked after being loaded for
  // example.
  if (!browser || browser->IsDeleteScheduled()) {
    return;
  }

  browser->OpenURL(
      content::OpenURLParams(
          GURL(is_profile_list_empty
                   ? chrome::kSigninOnDesktopLearnMoreURL
                   : chrome::kAddNewProfileOnDesktopLearnMoreURL),
          content::Referrer(), WindowOpenDisposition::NEW_FOREGROUND_TAB,
          ui::PAGE_TRANSITION_LINK, false),
      /*navigation_handle_callback=*/{});
}

}  // namespace

ProfilePickerHandler::ProfilePickerHandler(bool is_glic_version)
    : is_glic_version_(is_glic_version) {}

ProfilePickerHandler::~ProfilePickerHandler() {
  OnJavascriptDisallowed();
}

void ProfilePickerHandler::EnableStartupMetrics() {
  DCHECK(creation_time_on_startup_.is_null());
  content::WebContents* contents = web_ui()->GetWebContents();
  if (contents->GetVisibility() == content::Visibility::VISIBLE) {
    // Only record paint event if the window is visible.
    creation_time_on_startup_ = base::TimeTicks::Now();
    Observe(web_ui()->GetWebContents());
  }
}

void ProfilePickerHandler::RegisterMessages() {
  web_ui()->RegisterMessageCallback(
      "mainViewInitialize",
      base::BindRepeating(&ProfilePickerHandler::HandleMainViewInitialize,
                          base::Unretained(this)));
  web_ui()->RegisterMessageCallback(
      "launchSelectedProfile",
      base::BindRepeating(&ProfilePickerHandler::HandleLaunchSelectedProfile,
                          base::Unretained(this), /*open_settings=*/false));
  web_ui()->RegisterMessageCallback(
      "openManageProfileSettingsSubPage",
      base::BindRepeating(&ProfilePickerHandler::HandleLaunchSelectedProfile,
                          base::Unretained(this), /*open_settings=*/true));
  web_ui()->RegisterMessageCallback(
      "launchGuestProfile",
      base::BindRepeating(&ProfilePickerHandler::HandleLaunchGuestProfile,
                          base::Unretained(this)));
  web_ui()->RegisterMessageCallback(
      "askOnStartupChanged",
      base::BindRepeating(&ProfilePickerHandler::HandleAskOnStartupChanged,
                          base::Unretained(this)));
  web_ui()->RegisterMessageCallback(
      "getNewProfileSuggestedThemeInfo",
      base::BindRepeating(
          &ProfilePickerHandler::HandleGetNewProfileSuggestedThemeInfo,
          base::Unretained(this)));
  web_ui()->RegisterMessageCallback(
      "getProfileThemeInfo",
      base::BindRepeating(&ProfilePickerHandler::HandleGetProfileThemeInfo,
                          base::Unretained(this)));
  web_ui()->RegisterMessageCallback(
      "removeProfile",
      base::BindRepeating(&ProfilePickerHandler::HandleRemoveProfile,
                          base::Unretained(this)));
  web_ui()->RegisterMessageCallback(
      "getProfileStatistics",
      base::BindRepeating(&ProfilePickerHandler::HandleGetProfileStatistics,
                          base::Unretained(this)));
  web_ui()->RegisterMessageCallback(
      "closeProfileStatistics",
      base::BindRepeating(&ProfilePickerHandler::HandleCloseProfileStatistics,
                          base::Unretained(this)));
  web_ui()->RegisterMessageCallback(
      "selectNewAccount",
      base::BindRepeating(&ProfilePickerHandler::HandleSelectNewAccount,
                          base::Unretained(this)));
  // TODO(crbug.com/40144179): Consider renaming this message to
  // 'createLocalProfile' as this is only used for local profiles.
  web_ui()->RegisterMessageCallback(
      "getAvailableIcons",
      base::BindRepeating(&ProfilePickerHandler::HandleGetAvailableIcons,
                          base::Unretained(this)));
  web_ui()->RegisterMessageCallback(
      "continueWithoutAccount",
      base::BindRepeating(&ProfilePickerHandler::HandleContinueWithoutAccount,
                          base::Unretained(this)));
  web_ui()->RegisterMessageCallback(
      "getProfileState",
      base::BindRepeating(&ProfilePickerHandler::HandleGetProfileState,
                          base::Unretained(this)));
  web_ui()->RegisterMessageCallback(
      "confirmProfileSwitch",
      base::BindRepeating(&ProfilePickerHandler::HandleConfirmProfileSwitch,
                          base::Unretained(this)));
  web_ui()->RegisterMessageCallback(
      "cancelProfileSwitch",
      base::BindRepeating(&ProfilePickerHandler::HandleCancelProfileSwitch,
                          base::Unretained(this)));
  web_ui()->RegisterMessageCallback(
      "setProfileName",
      base::BindRepeating(&ProfilePickerHandler::HandleSetProfileName,
                          base::Unretained(this)));
  web_ui()->RegisterMessageCallback(
      "recordSignInPromoImpression",
      base::BindRepeating(
          &ProfilePickerHandler::HandleRecordSignInPromoImpression,
          base::Unretained(this)));
  web_ui()->RegisterMessageCallback(
      "updateProfileOrder",
      base::BindRepeating(&ProfilePickerHandler::HandleUpdateProfileOrder,
                          base::Unretained(this)));
  web_ui()->RegisterMessageCallback(
      "onLearnMoreClicked",
      base::BindRepeating(&ProfilePickerHandler::HandleOnLearnMoreClicked,
                          base::Unretained(this)));
  Profile* profile = Profile::FromWebUI(web_ui());
  content::URLDataSource::Add(profile, std::make_unique<ThemeSource>(profile));
}

void ProfilePickerHandler::OnJavascriptAllowed() {
  ProfileManager* profile_manager = g_browser_process->profile_manager();
  profile_attributes_storage_observation_.Observe(
      &profile_manager->GetProfileAttributesStorage());
}
void ProfilePickerHandler::OnJavascriptDisallowed() {
  profile_attributes_storage_observation_.Reset();
  weak_factory_.InvalidateWeakPtrs();
}

void ProfilePickerHandler::HandleMainViewInitialize(
    const base::ListValue& args) {
  AllowJavascript();
  PushProfilesList();
}

void ProfilePickerHandler::HandleLaunchSelectedProfile(
    bool open_settings,
    const base::ListValue& args) {
  TRACE_EVENT1("browser", "ProfilePickerHandler::HandleLaunchSelectedProfile",
               "args", args.DebugString());
  if (args.empty()) {
    return;
  }
  const base::Value& profile_path_value = args[0];

  std::optional<base::FilePath> profile_path =
      base::ValueToFilePath(profile_path_value);
  if (!profile_path) {
    return;
  }

  ProfileAttributesEntry& entry =
      CHECK_DEREF(g_browser_process->profile_manager()
                      ->GetProfileAttributesStorage()
                      .GetProfileAttributesWithPath(*profile_path));

  // If a browser window cannot be opened for profile, show an error message or
  // attempt to unlock the profile in the Profile Picker.
  if (entry.IsSigninRequired()) {
    TryLaunchLockedProfile(entry);
    return;
  }

  bool should_record_startup_metrics = !creation_time_on_startup_.is_null();
  ProfilePicker::PickProfile(
      *profile_path,
      ProfilePicker::ProfilePickingArgs{
          .open_settings = open_settings,
          .should_record_startup_metrics = should_record_startup_metrics},
      base::BindOnce(&ProfilePickerHandler::OnResetPickerButtons,
                     weak_factory_.GetWeakPtr()));
}

void ProfilePickerHandler::TryLaunchLockedProfile(
    ProfileAttributesEntry& entry) {
  CHECK(signin_util::IsForceSigninEnabled());
  CHECK(entry.IsSigninRequired());

  // Only pre-exisitng locked profiles can be reused by reauthing. We consider a
  // profile as pre-existing if it has been active previously and signed into,
  // and if there is a RestrictSigninToPattern policy, the account would also
  // need to match the policy filter.

  // Reauth attempt.
  if ((syncer::IsReplaceSyncPromosWithSignInPromosEnabled() &&
       entry.GetSigninState() != SigninState::kNotSignedIn) ||
      entry.CanBeManaged()) {
    // Glic version cannot run the reauth steps, show a dialog instead that
    // will redirect the user to the regular version of the picker.
    if (is_glic_version_) {
      DisplaySigninErrorDialog(
          /*profile_path=*/base::FilePath(),
          ForceSigninUIError::ReauthNotSupportedByGlicFlow());
      OnResetPickerButtons(false);
      return;
    }

    g_browser_process->profile_manager()->LoadProfileByPath(
        entry.GetPath(), /*incognito=*/false,
        base::BindOnce(&ProfilePickerHandler::OnProfileLoadedForSwitchToReauth,
                       weak_factory_.GetWeakPtr()));
    return;
  }

  // Default profile, not yet active, fresh sign in is allowed as the profile
  // was not used yet. Default profile is the automatically created profile,
  // initial profile or after deleting the last profile for example.
  if (entry.GetActiveTime().is_null()) {
    // Triggers a fresh sign in via profile picker without existing email
    // address.
    std::vector<StepSwitchFinishedCallback> callbacks;
    callbacks.emplace_back(
        base::BindOnce(&ProfilePickerHandler::OnLoadSigninFinished,
                       weak_factory_.GetWeakPtr()));
    callbacks.emplace_back(
        base::BindOnce(&ProfilePickerHandler::OnResetPickerButtons,
                       weak_factory_.GetWeakPtr()));
    ProfilePicker::SwitchToSignIn(
        entry.GetPath(),
        CombineCallbacks<StepSwitchFinishedCallback, bool>(std::move(callbacks))
            .value());
    return;
  }

  // Remaining active profiles: those cannot be reauthed or signed in to.

  // Do not allow users to sign in to a pre-existing locked profile, as this may
  // force unexpected profile data merge.
  DisplaySigninErrorDialog(
      /*profile_path=*/base::FilePath(),
      ForceSigninUIError::ReauthNotAllowed());
  OnResetPickerButtons(false);
}

void ProfilePickerHandler::OnProfileLoadedForSwitchToReauth(Profile* profile) {
  if (!profile) {
    return;
  }
  ProfilePicker::SwitchToReauth(
      profile,
      base::BindOnce(&ProfilePickerHandler::OnResetPickerButtons,
                     weak_factory_.GetWeakPtr()),
      base::BindOnce(&ProfilePickerHandler::DisplayForceSigninErrorDialog,
                     weak_factory_.GetWeakPtr(), profile->GetPath()));
}

void ProfilePickerHandler::DisplaySigninErrorDialog(
    const base::FilePath& profile_path,
    const std::variant<ForceSigninUIError, SigninUIError>& error) {
  AllowJavascript();

  if (std::holds_alternative<ForceSigninUIError>(error)) {
    CHECK(signin_util::IsForceSigninEnabled());
    const ForceSigninUIError& force_signin_error =
        std::get<ForceSigninUIError>(error);
    DisplayForceSigninErrorDialog(profile_path, force_signin_error);
    return;
  }

  const SigninUIError& generic_signin_error = std::get<SigninUIError>(error);
  CHECK(!generic_signin_error.message().empty());
  FireWebUIListener(
      "display-signin-error-dialog",
      base::Value(SigninErrorUI::GetTitle(generic_signin_error.email())),
      base::Value(generic_signin_error.message()),
      base::Value(std::u16string()));
}

void ProfilePickerHandler::DisplayForceSigninErrorDialog(
    const base::FilePath& profile_path,
    const ForceSigninUIError& error) {
  AllowJavascript();
  const auto& [title, body] = error.GetErrorTexts();
  FireWebUIListener("display-signin-error-dialog", base::Value(title),
                    base::Value(body),
                    base::Value(profile_path.AsUTF16Unsafe()));
}

void ProfilePickerHandler::HandleLaunchGuestProfile(
    const base::ListValue& args) {
  // TODO(crbug.com/40123459): Add check |IsGuestModeEnabled| once policy
  // checking has been added to the UI.
  ProfilePicker::PickProfile(
      ProfileManager::GetGuestProfilePath(),
      ProfilePicker::ProfilePickingArgs{.open_settings = false,
                                        .should_record_startup_metrics = false},
      base::BindOnce(&ProfilePickerHandler::OnResetPickerButtons,
                     weak_factory_.GetWeakPtr()));
}

void ProfilePickerHandler::HandleAskOnStartupChanged(
    const base::ListValue& list) {
  if (list.empty() || !list[0].is_bool()) {
    return;
  }
  const bool show_on_startup = list[0].GetBool();

  PrefService* prefs = g_browser_process->local_state();
  prefs->SetBoolean(prefs::kBrowserShowProfilePickerOnStartup, show_on_startup);
  RecordAskOnStartupChanged(show_on_startup);
}

void ProfilePickerHandler::HandleGetNewProfileSuggestedThemeInfo(
    const base::ListValue& args) {
  AllowJavascript();
  CHECK_EQ(1U, args.size());
  const base::Value& callback_id = args[0];

  chrome_colors::ColorInfo color_info = GenerateNewProfileColor();
  base::DictValue dict = CreateAutogeneratedProfileThemeInfo(
      color_info.id, color_info.color,
      web_ui()->GetWebContents()->GetColorProvider(),
      web_ui()->GetDeviceScaleFactor());
  ResolveJavascriptCallback(callback_id, dict);
}

void ProfilePickerHandler::HandleGetProfileThemeInfo(
    const base::ListValue& args) {
  AllowJavascript();
  CHECK_EQ(2U, args.size());
  const base::Value& callback_id = args[0];
  const base::DictValue& user_theme_choice = args[1].GetDict();
  int color_id = user_theme_choice.FindInt("colorId").value();
  std::optional<SkColor> color = user_theme_choice.FindDouble("color");
  base::DictValue dict;
  switch (color_id) {
    case kDefaultThemeColorId:
      dict = CreateDefaultProfileThemeInfo(
          web_ui()->GetWebContents()->GetColorProvider(),
          web_ui()->GetDeviceScaleFactor());
      break;
    case kManuallyPickedColorId:
      dict = CreateAutogeneratedProfileThemeInfo(
          color_id, *color, web_ui()->GetWebContents()->GetColorProvider(),
          web_ui()->GetDeviceScaleFactor());
      break;
    default:
      dict = CreateAutogeneratedProfileThemeInfo(
          color_id, *GetChromeColorColorById(color_id),
          web_ui()->GetWebContents()->GetColorProvider(),
          web_ui()->GetDeviceScaleFactor());
      break;
  }
  ResolveJavascriptCallback(callback_id, dict);
}

void ProfilePickerHandler::HandleGetAvailableIcons(
    const base::ListValue& args) {
  AllowJavascript();
  CHECK_EQ(1U, args.size());
  const base::Value& callback_id = args[0];
  ResolveJavascriptCallback(callback_id,
                            profiles::GetCustomProfileAvatarIconsAndLabels());
}

void ProfilePickerHandler::HandleContinueWithoutAccount(
    const base::ListValue& args) {
  CHECK_EQ(1U, args.size());

  // profileColor is undefined for the default theme.
  std::optional<SkColor> profile_color;
  if (args[0].is_int()) {
    profile_color = args[0].GetInt();
  }

  RecordProfilePickerAction(ProfilePickerAction::kLaunchNewProfile);
  ProfileMetrics::LogProfileAddNewUser(
      ProfileMetrics::ADD_NEW_PROFILE_PICKER_LOCAL);
  ProfilePicker::SwitchToSignedOutPostIdentityFlow(profile_color);
}

void ProfilePickerHandler::HandleGetProfileState(const base::ListValue& args) {
  AllowJavascript();
  CHECK_EQ(2U, args.size());
  const base::Value& callback_id = args[0];
  const base::Value& profile_path_value = args[1];

  std::optional<base::FilePath> profile_path =
      base::ValueToFilePath(profile_path_value);
  CHECK(profile_path.has_value());

  ProfileAttributesEntry* entry =
      g_browser_process->profile_manager()
          ->GetProfileAttributesStorage()
          .GetProfileAttributesWithPath(profile_path.value());
  CHECK(entry);
  float scale = web_ui()->GetDeviceScaleFactor();
  const ui::ColorProvider* color_provider =
      web_ui()->GetWebContents()
          ? &web_ui()->GetWebContents()->GetColorProvider()
          : nullptr;
  base::DictValue dict =
      CreateProfileState(entry, kProfileCardAvatarSize, color_provider, scale);
  ResolveJavascriptCallback(callback_id, dict);
}

void ProfilePickerHandler::HandleConfirmProfileSwitch(
    const base::ListValue& args) {
  if (args.empty()) {
    return;
  }
  const base::Value& profile_path_value = args[0];

  std::optional<base::FilePath> profile_path =
      base::ValueToFilePath(profile_path_value);
  if (!profile_path) {
    return;
  }

  // TODO(crbug.com/40751337): remove the profile used for the sign-in
  // flow.
  ProfilePicker::PickProfile(
      *profile_path,
      ProfilePicker::ProfilePickingArgs{.open_settings = false,
                                        .should_record_startup_metrics = false},
      base::BindOnce(&ProfilePickerHandler::OnResetPickerButtons,
                     weak_factory_.GetWeakPtr()));
}

void ProfilePickerHandler::HandleCancelProfileSwitch(
    const base::ListValue& args) {
  ProfilePicker::CancelSignInFlow();
}

void ProfilePickerHandler::HandleRecordSignInPromoImpression(
    const base::ListValue& /*args*/) {
  signin_metrics::RecordSigninImpressionUserActionForAccessPoint(
      signin_metrics::AccessPoint::kUserManager);
  signin_metrics::LogSignInOffered(
      signin_metrics::AccessPoint::kUserManager,
      signin_metrics::PromoAction::
          PROMO_ACTION_NEW_ACCOUNT_NO_EXISTING_ACCOUNT);
}

void ProfilePickerHandler::HandleSetProfileName(const base::ListValue& args) {
  CHECK_EQ(2U, args.size());
  const base::Value& profile_path_value = args[0];
  std::optional<base::FilePath> profile_path =
      base::ValueToFilePath(profile_path_value);

  if (!profile_path) {
    NOTREACHED();
  }
  std::u16string profile_name = base::UTF8ToUTF16(args[1].GetString());
  base::TrimWhitespace(profile_name, base::TRIM_ALL, &profile_name);
  CHECK(!profile_name.empty());
  ProfileAttributesEntry* entry =
      g_browser_process->profile_manager()
          ->GetProfileAttributesStorage()
          .GetProfileAttributesWithPath(profile_path.value());
  CHECK(entry);
  entry->SetLocalProfileName(profile_name, /*is_default_name=*/false);
}

void ProfilePickerHandler::HandleRemoveProfile(const base::ListValue& args) {
  CHECK_EQ(1U, args.size());
  const base::Value& profile_path_value = args[0];
  std::optional<base::FilePath> profile_path =
      base::ValueToFilePath(profile_path_value);

  if (!profile_path) {
    NOTREACHED();
  }

  RecordProfilePickerAction(ProfilePickerAction::kDeleteProfile);
  DCHECK(profile_statistics_keep_alive_);

  // Deleting the profile may delete `this` (see See
  // https://crbug.com/40934491), if the profile picker was shown in a tab. Keep
  // the `ScopedProfileKeepAlive` until the end of the function, to avoid the
  // profile being unloaded and reloaded.
  std::unique_ptr<ScopedProfileKeepAlive> profile_statistics_keep_alive =
      std::move(profile_statistics_keep_alive_);
  webui::DeleteProfileAtPath(*profile_path,
                             ProfileMetrics::DELETE_PROFILE_USER_MANAGER);
  // Do not use `this` after this point, it may be deleted.
}

void ProfilePickerHandler::HandleUpdateProfileOrder(
    const base::ListValue& args) {
  CHECK_EQ(2U, args.size());
  CHECK(args[0].is_int());
  CHECK(args[1].is_int());

  int from_index = args[0].GetInt();
  int to_index = args[1].GetInt();
  CHECK(from_index >= 0 && to_index >= 0);

  g_browser_process->profile_manager()
      ->GetProfileAttributesStorage()
      .UpdateProfilesOrderPref(from_index, to_index);
}

void ProfilePickerHandler::HandleOnLearnMoreClicked(
    const base::ListValue& args) {
  CHECK(is_glic_version_);
  CHECK_EQ(0U, args.size());

  bool is_profile_list_empty = GetProfilesAttributesForDisplay().empty();
  // Loads the last used profile and open/uses a browser to show the help page.
  profiles::SwitchToProfile(
      g_browser_process->profile_manager()->GetLastUsedProfileDir(),
      /*always_create=*/false,
      base::BindOnce(&OpenLearnMoreURL, is_profile_list_empty));
}

void ProfilePickerHandler::HandleCloseProfileStatistics(
    const base::ListValue& args) {
  CHECK_EQ(0U, args.size());
  DCHECK(profile_statistics_keep_alive_);
  profile_statistics_keep_alive_.reset();
}

void ProfilePickerHandler::HandleGetProfileStatistics(
    const base::ListValue& args) {
  AllowJavascript();
  CHECK_EQ(1U, args.size());
  const base::Value& profile_path_value = args[0];
  std::optional<base::FilePath> profile_path =
      base::ValueToFilePath(profile_path_value);
  if (!profile_path) {
    return;
  }

  Profile* profile =
      g_browser_process->profile_manager()->GetProfileByPath(*profile_path);

  if (profile) {
    GatherProfileStatistics(profile);
  } else {
    g_browser_process->profile_manager()->LoadProfileByPath(
        *profile_path, false,
        base::BindOnce(&ProfilePickerHandler::GatherProfileStatistics,
                       weak_factory_.GetWeakPtr()));
  }
}

void ProfilePickerHandler::GatherProfileStatistics(Profile* profile) {
  if (!profile) {
    return;
  }

  profile_statistics_keep_alive_ = std::make_unique<ScopedProfileKeepAlive>(
      profile, ProfileKeepAliveOrigin::kProfileStatistics);

  ProfileStatisticsFactory::GetForProfile(profile)->GatherStatistics(
      base::BindRepeating(&ProfilePickerHandler::OnProfileStatisticsReceived,
                          weak_factory_.GetWeakPtr(), profile->GetPath()));
}

void ProfilePickerHandler::OnProfileStatisticsReceived(
    const base::FilePath& profile_path,
    profiles::ProfileCategoryStats result) {
  base::DictValue dict;
  dict.Set("profilePath", base::FilePathToValue(profile_path));
  base::DictValue stats;
  // Categories are defined in |kProfileStatisticsCategories|
  // {"BrowsingHistory", "Passwords", "Bookmarks", "Autofill"}.
  for (const auto& item : result) {
    stats.Set(item.category, item.count);
  }
  dict.Set("statistics", std::move(stats));
  FireWebUIListener("profile-statistics-received", dict);
}

void ProfilePickerHandler::HandleSelectNewAccount(const base::ListValue& args) {
  AllowJavascript();
  CHECK_EQ(1U, args.size());
  std::optional<SkColor> profile_color = args[0].GetIfInt();
  if (signin_util::IsForceSigninEnabled()) {
    // Force sign-in policy uses a separate flow that doesn't initialize the
    // profile color. Generate a new profile color here.
    profile_color = GenerateNewProfileColor().color;
  }
  std::vector<StepSwitchFinishedCallback> callbacks;
  callbacks.emplace_back(base::BindOnce(
      &ProfilePickerHandler::OnLoadSigninFinished, weak_factory_.GetWeakPtr()));
  callbacks.emplace_back(base::BindOnce(
      &ProfilePickerHandler::OnResetPickerButtons, weak_factory_.GetWeakPtr()));
  ProfilePicker::SwitchToSignIn(
      profile_color,
      CombineCallbacks<StepSwitchFinishedCallback, bool>(std::move(callbacks))
          .value());
}

void ProfilePickerHandler::OnLoadSigninFinished(bool success) {
  AllowJavascript();
  FireWebUIListener("load-signin-finished", base::Value(success));
}

void ProfilePickerHandler::OnResetPickerButtons(bool success) {
  AllowJavascript();
  FireWebUIListener("reset-picker-buttons", base::Value(success));
}

void ProfilePickerHandler::PushProfilesList() {
  DCHECK(IsJavascriptAllowed());
  FireWebUIListener("profiles-list-changed", GetProfilesList());
}

void ProfilePickerHandler::SetProfilesOrder(
    const std::vector<ProfileAttributesEntry*>& entries) {
  profiles_order_.clear();
  size_t index = 0;
  for (const ProfileAttributesEntry* entry : entries) {
    profiles_order_[entry->GetPath()] = index++;
  }
}

std::vector<ProfileAttributesEntry*>
ProfilePickerHandler::GetProfilesAttributesForDisplay() {
  std::vector<ProfileAttributesEntry*> ordered_entries =
      g_browser_process->profile_manager()
          ->GetProfileAttributesStorage()
          .GetAllProfilesAttributesSortedByLocalProfileNameWithCheck();
  std::erase_if(ordered_entries, [](const ProfileAttributesEntry* entry) {
    return entry->IsOmitted();
  });

  // In Glic version, only allow profile entries that are eligible. This may
  // cause the returned profile list to be empty, and will display different
  // strings in the Ui.
  if (is_glic_version_) {
    std::erase_if(ordered_entries, [](const ProfileAttributesEntry* entry) {
      return !entry->IsGlicEligible();
    });
  }

  size_t number_of_profiles = ordered_entries.size();

  if (profiles_order_.size() != number_of_profiles) {
    // Should only happen the first time the function is called.
    // Profile creation and deletion are handled at
    // 'OnProfileAdded', 'OnProfileWasRemoved'.
    DCHECK(!profiles_order_.size());
    SetProfilesOrder(ordered_entries);
    return ordered_entries;
  }

  // Vector of nullptr entries.
  std::vector<ProfileAttributesEntry*> entries(number_of_profiles);
  for (ProfileAttributesEntry* entry : ordered_entries) {
    const auto it = profiles_order_.find(entry->GetPath());
    DCHECK(it != profiles_order_.end());
    size_t index = it->second;
    DCHECK_LT(index, number_of_profiles);
    DCHECK(!entries[index]);
    entries[index] = entry;
  }

  return entries;
}

base::ListValue ProfilePickerHandler::GetProfilesList() {
  base::ListValue profiles_list;

  std::vector<ProfileAttributesEntry*> entries =
      GetProfilesAttributesForDisplay();
  float scale = web_ui()->GetDeviceScaleFactor();
  const ui::ColorProvider* color_provider =
      web_ui()->GetWebContents()
          ? &web_ui()->GetWebContents()->GetColorProvider()
          : nullptr;
  for (const ProfileAttributesEntry* entry : entries) {
    profiles_list.Append(CreateProfileState(entry, kProfileCardAvatarSize,
                                            color_provider, scale));
  }
  return profiles_list;
}

void ProfilePickerHandler::AddProfileToListAndPushUpdates(
    const base::FilePath& profile_path) {
  size_t number_of_profiles = profiles_order_.size();
  auto it_and_whether_inserted =
      profiles_order_.insert({profile_path, number_of_profiles});
  // We shouldn't add the same profile to the list more than once. Use
  // `insert()` to not corrput the map in case this happens.
  // https://crbug.com/40759222
  DCHECK(it_and_whether_inserted.second);

  MaybeUpdateGuestMode();
  PushProfilesList();
}

void ProfilePickerHandler::RemoveProfileFromListAndPushUpdates(
    const base::FilePath& profile_path) {
  auto remove_it = profiles_order_.find(profile_path);
  // Guest and omitted profiles aren't added to the list.
  // It's possible that a profile gets marked as guest or as omitted after it
  // had been added to the list. In that case, the profile gets removed from the
  // list once in `OnProfileIsOmittedChanged()` but not the second time when
  // `OnProfileWasRemoved()` is called.
  if (remove_it == profiles_order_.end()) {
    return;
  }

  size_t index = remove_it->second;
  profiles_order_.erase(remove_it);
  for (auto& it : profiles_order_) {
    if (it.second > index) {
      --it.second;
    }
  }
  MaybeUpdateGuestMode();
  FireWebUIListener("profile-removed", base::FilePathToValue(profile_path));
}

void ProfilePickerHandler::OnProfileAdded(const base::FilePath& profile_path) {
  ProfileAttributesEntry* entry =
      g_browser_process->profile_manager()
          ->GetProfileAttributesStorage()
          .GetProfileAttributesWithPath(profile_path);
  CHECK(entry);
  if (entry->IsOmitted()) {
    return;
  }
  if (is_glic_version_ && !entry->IsGlicEligible()) {
    return;
  }

  AddProfileToListAndPushUpdates(profile_path);
}

void ProfilePickerHandler::OnProfileWasRemoved(
    const base::FilePath& profile_path,
    const std::u16string& profile_name) {
  DCHECK(IsJavascriptAllowed());
  RemoveProfileFromListAndPushUpdates(profile_path);
}

void ProfilePickerHandler::OnProfileIsOmittedChanged(
    const base::FilePath& profile_path) {
  ProfileAttributesEntry* entry =
      g_browser_process->profile_manager()
          ->GetProfileAttributesStorage()
          .GetProfileAttributesWithPath(profile_path);
  CHECK(entry);
  if (entry->IsOmitted() || (is_glic_version_ && !entry->IsGlicEligible())) {
    RemoveProfileFromListAndPushUpdates(profile_path);
  } else {
    AddProfileToListAndPushUpdates(profile_path);
  }
}

void ProfilePickerHandler::OnProfileAvatarChanged(
    const base::FilePath& profile_path) {
  PushProfilesList();
}

void ProfilePickerHandler::OnProfileHighResAvatarLoaded(
    const base::FilePath& profile_path) {
  PushProfilesList();
}

void ProfilePickerHandler::OnProfileNameChanged(
    const base::FilePath& profile_path,
    const std::u16string& old_profile_name) {
  PushProfilesList();
}

void ProfilePickerHandler::OnProfileIsManagedChanged(
    const base::FilePath& profile_path) {
  PushProfilesList();
}

void ProfilePickerHandler::OnProfileSupervisedUserIdChanged(
    const base::FilePath& profile_path) {
  MaybeUpdateGuestMode();
  PushProfilesList();
}

void ProfilePickerHandler::OnProfileIsGlicEligibleChanged(
    const base::FilePath& profile_path) {
  if (!is_glic_version_) {
    return;
  }

  ProfileAttributesEntry* entry =
      g_browser_process->profile_manager()
          ->GetProfileAttributesStorage()
          .GetProfileAttributesWithPath(profile_path);
  CHECK(entry);
  if (entry->IsOmitted()) {
    return;
  }

  if (entry->IsGlicEligible()) {
    AddProfileToListAndPushUpdates(profile_path);
  } else {
    RemoveProfileFromListAndPushUpdates(profile_path);
  }
}

void ProfilePickerHandler::OnProfileAiSubscriptionTierUpdated(
    const base::FilePath& profile_path,
    int tier) {
  PushProfilesList();
}

void ProfilePickerHandler::DidFirstVisuallyNonEmptyPaint() {
  DCHECK(!creation_time_on_startup_.is_null());
  auto now = base::TimeTicks::Now();
  base::UmaHistogramTimes("ProfilePicker.StartupTime.FirstPaint",
                          now - creation_time_on_startup_);
  startup_metric_utils::GetBrowser().RecordExternalStartupMetric(
      "ProfilePicker.StartupTime.FirstPaint.FromApplicationStart", now,
      /*set_non_browser_ui_displayed=*/true);
  // Stop observing so that the histogram is only recorded once.
  Observe(nullptr);
}

void ProfilePickerHandler::OnVisibilityChanged(content::Visibility visibility) {
  // If the profile picker is hidden, the first paint will be delayed until the
  // picker is visible again. Stop monitoring the first paint to avoid polluting
  // the metrics.
  if (visibility != content::Visibility::VISIBLE) {
    Observe(nullptr);
  }
}

void ProfilePickerHandler::MaybeUpdateGuestMode() {
  CHECK(IsJavascriptAllowed());
  FireWebUIListener("guest-mode-availability-updated",
                    base::Value(profiles::IsGuestModeEnabled()));
}

void RecordProfilePickerAction(ProfilePickerAction action) {
  base::UmaHistogramEnumeration("ProfilePicker.UserAction", action);
}
