// Copyright 2024 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/profiles/batch_upload/batch_upload_service.h"

#include <algorithm>
#include <array>
#include <map>

#include "base/check_deref.h"
#include "base/containers/to_vector.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/notreached.h"
#include "base/strings/to_string.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/profiles/batch_upload/batch_upload_delegate.h"
#include "chrome/browser/signin/identity_manager_factory.h"
#include "chrome/browser/signin/signin_promo_util.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/frame/toolbar_button_provider.h"
#include "chrome/browser/ui/views/toolbar/avatar_toolbar_button_interface.h"
#include "chrome/grit/generated_resources.h"
#include "components/signin/public/base/signin_buildflags.h"
#include "components/signin/public/identity_manager/identity_manager.h"
#include "components/sync/base/data_type.h"
#include "components/sync/service/local_data_description.h"
#include "components/sync/service/sync_service.h"
#include "ui/base/l10n/l10n_util.h"

namespace {

#if BUILDFLAG(ENABLE_DICE_SUPPORT)
// Duration of displaying the saving to account text in the avatar button.
constexpr base::TimeDelta kBatchUploadAvatarButtonOverrideTextDuration =
    base::Seconds(3);
#endif  // BUILDFLAG(ENABLE_DICE_SUPPORT)

// This list contains all the data types that are available for the Batch Upload
// dialog. Data types should not be repeated and the list is ordered based on
// the priority of showing in the dialog.
// An exception to the order of this list may happen to the first item, when the
// entry point used to open the batch upload dialog is tied to a data type. This
// data type will be shown first for consistency with the action from the entry
// point. E.g. Bookmarks promo card entry point will force
// `syncer::DataType::BOOKMARKS` to be the first data type shown (and not
// repeated afterwards).
const std::array<syncer::DataType, 6> kBatchUploadAvailableTypesOrder{
    // clang-format off
    syncer::DataType::PASSWORDS,
    syncer::DataType::BOOKMARKS,
    syncer::DataType::READING_LIST,
    syncer::DataType::CONTACT_INFO,
    syncer::DataType::EXTENSIONS,
    syncer::DataType::THEMES,
    // clang-format on
};

// Determines the primary type based on the entry point. Some entry point may be
// generic and not tied to any data type - std::nullopt is returned in this
// case.
std::optional<syncer::DataType> PrimaryTypeFromEntryPoint(
    BatchUploadService::EntryPoint entry_point) {
  switch (entry_point) {
    case BatchUploadService::EntryPoint::kPasswordManagerSettings:
    case BatchUploadService::EntryPoint::kPasswordPromoCard:
      return syncer::PASSWORDS;
    case BatchUploadService::EntryPoint::kBookmarksManagerPromoCard:
    case BatchUploadService::EntryPoint::
        kProfileMenuPrimaryButtonWithBookmarksAction:
    case BatchUploadService::EntryPoint::
        kProfileMenuPrimaryButtonWithBookmarksActionFromAvatarPromo:
      return syncer::BOOKMARKS;
    case BatchUploadService::EntryPoint::kAccountSettingsPage:
    case BatchUploadService::EntryPoint::kProfileMenuRowButtonAction:
    case BatchUploadService::EntryPoint::kProfileMenuPrimaryButtonAction:
    case BatchUploadService::EntryPoint::
        kProfileMenuPrimaryButtonWithWindows10DepreciationAction:
    case BatchUploadService::EntryPoint::
        kProfileMenuPrimaryButtonActionFromAvatarPromo:
    case BatchUploadService::EntryPoint::
        kProfileMenuPrimaryButtonWithWindows10DepreciationActionFromAvatarPromo:
      return std::nullopt;
  }
}

// Return the primary local data description after performing additional checks.
syncer::LocalDataDescription GetPrimaryTypeLocalDataDescription(
    const std::map<syncer::DataType, syncer::LocalDataDescription>&
        local_data_descriptions_map,
    syncer::DataType primary_type) {
  CHECK(std::ranges::contains(kBatchUploadAvailableTypesOrder, primary_type));
  CHECK(local_data_descriptions_map.contains(primary_type));
  const syncer::LocalDataDescription& primary_local_data_description =
      local_data_descriptions_map.at(primary_type);
  CHECK(!primary_local_data_description.local_data_models.empty())
      << "Primary data type should have local data since the entry point "
         "is available.";
  CHECK_EQ(primary_type, primary_local_data_description.type)
      << "Non empty data description's data type and the keyed mapping "
         "value should always match.";
  return primary_local_data_description;
}

// Returns the list of data descriptions in `local_data_descriptions_map`
// following the expected displayed order in the dialog.
// `entry_point` is used to determine the primary (first) type, if any, to be
// displayed, overriding the expected order for the first type only. Primary
// type should have local data - since the entry point was available.
// Generic entry points have no primary type associated with it, the regular
// order will be displayed.
// Other data descriptions with no local data will be filtered out.
std::vector<syncer::LocalDataDescription>
GetOrderedListOfNonEmptyDataDescriptions(
    std::map<syncer::DataType, syncer::LocalDataDescription>
        local_data_descriptions_map,
    BatchUploadService::EntryPoint entry_point) {
  std::vector<syncer::LocalDataDescription> local_data_description_list;

  std::optional<syncer::DataType> primary_type =
      PrimaryTypeFromEntryPoint(entry_point);
  if (primary_type.has_value()) {
    // Treat the primary type first to make sure it is the first type for
    // display, overriding the expected order given in
    // `kBatchUploadAvailableTypesOrder`.
    local_data_description_list.push_back(GetPrimaryTypeLocalDataDescription(
        local_data_descriptions_map, *primary_type));
    local_data_descriptions_map.erase(*primary_type);
  }

  // Reorder the result from the `local_data_descriptions_map` based on the
  // available types order.
  for (syncer::DataType type : kBatchUploadAvailableTypesOrder) {
    if (!local_data_descriptions_map.contains(type)) {
      continue;
    }

    syncer::LocalDataDescription& local_data_description =
        local_data_descriptions_map.at(type);
    if (!local_data_description.local_data_models.empty()) {
      CHECK_EQ(type, local_data_description.type)
          << "Non empty data description's data type and the keyed mapping "
             "value should always match.";
      local_data_description_list.push_back(std::move(local_data_description));
    }
    local_data_descriptions_map.erase(type);
  }

  // All initial inputs should be processed.
  CHECK(local_data_descriptions_map.empty());
  return local_data_description_list;
}

// Whether there exist a current local data item of any type.
bool HasLocalDataToShow(
    const std::map<syncer::DataType, syncer::LocalDataDescription>&
        local_data_descriptions) {
  // As long as a data type has at least a single item to show, the dialog can
  // be shown.
  return std::ranges::any_of(
      local_data_descriptions, [](const auto& local_data_description) {
        return !local_data_description.second.local_data_models.empty();
      });
}

#if BUILDFLAG(ENABLE_DICE_SUPPORT)
void RecordBatchUploadTriggeredMetrics(
    BatchUploadService::EntryPoint entry_point,
    const GaiaId& gaia_id,
    PrefService& prefs) {
  signin::ProfileMenuAvatarButtonPromoInfo::Type promo_type;
  switch (entry_point) {
    case BatchUploadService::EntryPoint::kPasswordManagerSettings:
    case BatchUploadService::EntryPoint::kPasswordPromoCard:
    case BatchUploadService::EntryPoint::kBookmarksManagerPromoCard:
    case BatchUploadService::EntryPoint::kProfileMenuRowButtonAction:
    case BatchUploadService::EntryPoint::kProfileMenuPrimaryButtonAction:
    case BatchUploadService::EntryPoint::
        kProfileMenuPrimaryButtonWithBookmarksAction:
    case BatchUploadService::EntryPoint::
        kProfileMenuPrimaryButtonWithWindows10DepreciationAction:
    case BatchUploadService::EntryPoint::kAccountSettingsPage:
      // Those entry points are not recording any metric related to the
      // AvatarButton.
      return;
    case BatchUploadService::EntryPoint::
        kProfileMenuPrimaryButtonActionFromAvatarPromo:
      promo_type =
          signin::ProfileMenuAvatarButtonPromoInfo::Type::kBatchUploadPromo;
      break;
    case BatchUploadService::EntryPoint::
        kProfileMenuPrimaryButtonWithBookmarksActionFromAvatarPromo:
      promo_type = signin::ProfileMenuAvatarButtonPromoInfo::Type::
          kBatchUploadBookmarksPromo;
      break;
    case BatchUploadService::EntryPoint::
        kProfileMenuPrimaryButtonWithWindows10DepreciationActionFromAvatarPromo:
      promo_type = signin::ProfileMenuAvatarButtonPromoInfo::Type::
          kBatchUploadWindows10DepreciationPromo;
      break;
  }

  signin::RecordAvatarButtonPromoAcceptedAtPromoShownCount(promo_type, gaia_id,
                                                           prefs);
}
#endif  // BUILDFLAG(ENABLE_DICE_SUPPORT)

}  // namespace

BatchUploadService::BatchUploadService(
    signin::IdentityManager* identity_manager,
    syncer::SyncService* sync_service,
    PrefService* pref_service,
    std::unique_ptr<BatchUploadDelegate> delegate)
    : identity_manager_(CHECK_DEREF(identity_manager)),
      sync_service_(CHECK_DEREF(sync_service)),
      prefs_(CHECK_DEREF(pref_service)),
      delegate_(std::move(delegate)) {}

BatchUploadService::~BatchUploadService() = default;

void BatchUploadService::OpenBatchUpload(
    BrowserWindowInterface* browser,
    EntryPoint entry_point,
    base::OnceCallback<void(bool)> dialog_shown_callback,
    base::OnceCallback<void()> dialog_closed_callback) {
  if (!IsUserEligibleToOpenDialog()) {
    std::move(dialog_shown_callback).Run(false);
    std::move(dialog_closed_callback).Run();
    return;
  }

  // Do not allow to have more than one dialog shown at a time.
  if (IsDialogOpened()) {
    // TODO(b/361330952): give focus to the browser that is showing the dialog
    // currently.
    std::move(dialog_shown_callback).Run(false);
    std::move(dialog_closed_callback).Run();
    return;
  }

  // Create the state of the dialog that may be shown, in preparation for
  // showing the dialog once all the local data descriptions are in
  // `OnGetLocalDataDescriptionsReady()`. Allows to make sure that while getting
  // the local data descriptions, no other dialog opening is triggered.
  state_.dialog_state_ = std::make_unique<ResettableState::DialogState>();
  state_.dialog_state_->browser_ = browser;
  state_.dialog_state_->entry_point_ = entry_point;
  state_.dialog_state_->dialog_shown_callback_ =
      std::move(dialog_shown_callback);
  state_.dialog_state_->dialog_closed_callback_ =
      std::move(dialog_closed_callback);

  GetLocalDataDescriptionsForAvailableTypes(
      base::BindOnce(&BatchUploadService::OnGetLocalDataDescriptionsReady,
                     base::Unretained(this)));
}

void BatchUploadService::GetLocalDataDescriptionsForAvailableTypes(
    base::OnceCallback<
        void(std::map<syncer::DataType, syncer::LocalDataDescription>)>
        result_callback) {
  syncer::DataTypeSet data_types;
  // Iterate over all available enums.
  for (syncer::DataType type : kBatchUploadAvailableTypesOrder) {
    data_types.Put(type);
  }

  sync_service_->GetLocalDataDescriptions(data_types,
                                          std::move(result_callback));
}

void BatchUploadService::OnGetLocalDataDescriptionsReady(
    std::map<syncer::DataType, syncer::LocalDataDescription> local_data_map) {
  if (local_data_map.empty() || !HasLocalDataToShow(local_data_map)) {
    std::move(state_.dialog_state_->dialog_shown_callback_).Run(false);
    ResetDialogState();
    return;
  }

  std::move(state_.dialog_state_->dialog_shown_callback_).Run(true);
  delegate_->ShowBatchUploadDialog(
      state_.dialog_state_->browser_,
      GetOrderedListOfNonEmptyDataDescriptions(
          std::move(local_data_map), state_.dialog_state_->entry_point_),
      state_.dialog_state_->entry_point_,
      /*complete_callback=*/
      base::BindOnce(&BatchUploadService::OnBatchUploadDialogResult,
                     base::Unretained(this)));
  // The dialog may be reset at this point: `state_.dialog_state_` may be null.
}

void BatchUploadService::OnBatchUploadDialogResult(
    const std::map<syncer::DataType,
                   std::vector<syncer::LocalDataItemModel::DataId>>&
        item_ids_to_move) {
  CHECK(state_.dialog_state_);

  // Batch Upload was cancelled.
  if (item_ids_to_move.empty()) {
    std::move(state_.dialog_state_->dialog_closed_callback_).Run();
    ResetDialogState();
    return;
  }

  sync_service_->TriggerLocalDataMigrationForItems(item_ids_to_move);

#if BUILDFLAG(ENABLE_DICE_SUPPORT)
  // `browser` may be null in tests.
  BrowserWindowInterface* browser = state_.dialog_state_->browser_.get();
  if (browser) {
    state_.saving_browser_state_ =
        std::make_unique<ResettableState::SavingBrowserState>();
    TriggerAvatarButtonSavingDataText(browser);
  }
#endif  // BUILDFLAG(ENABLE_DICE_SUPPORT)

  // The callback has to be called after `TriggerLocalDataMigrationForItems()`
  // so that it reacts to the state after the migration.
  std::move(state_.dialog_state_->dialog_closed_callback_).Run();

#if BUILDFLAG(ENABLE_DICE_SUPPORT)
  GaiaId primary_gaia =
      identity_manager_->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin)
          .gaia;
  RecordBatchUploadTriggeredMetrics(state_.dialog_state_->entry_point_,
                                    primary_gaia, prefs_.get());
#endif  // BUILDFLAG(ENABLE_DICE_SUPPORT)
  ResetDialogState();
}

bool BatchUploadService::IsUserEligibleToOpenDialog() const {
  AccountInfo primary_account = identity_manager_->FindExtendedAccountInfo(
      identity_manager_->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin));
  // If not signed in, the user should not have access to the dialog.
  if (primary_account.IsEmpty() ||
      identity_manager_->HasPrimaryAccount(signin::ConsentLevel::kSync)) {
    return false;
  }

  // If is in Sign in pending, the user should not have access to the dialog.
  if (identity_manager_->HasAccountWithRefreshTokenInPersistentErrorState(
          primary_account.GetAccountId())) {
    return false;
  }

  return true;
}

#if BUILDFLAG(ENABLE_DICE_SUPPORT)
void BatchUploadService::TriggerAvatarButtonSavingDataText(
    BrowserWindowInterface* browser) {
  CHECK(browser);
  CHECK(state_.saving_browser_state_);
  // Show the text.
  state_.saving_browser_state_->avatar_override_clear_callback_ =
      BrowserView::GetBrowserViewForBrowser(browser)
          ->toolbar_button_provider()
          ->GetAvatarToolbarButtonInterface()
          ->SetExplicitButtonState(
              l10n_util::GetStringUTF16(
                  IDS_BATCH_UPLOAD_AVATAR_BUTTON_SAVING_TO_ACCOUNT),
              /*accessibility_label=*/std::nullopt,
              /*explicit_action=*/std::nullopt,
              /*should_announce=*/true);

  // Prepare the timer to stop the overridden text from showing.
  state_.saving_browser_state_->avatar_override_timer_.Start(
      FROM_HERE, kBatchUploadAvatarButtonOverrideTextDuration,
      base::BindOnce(&BatchUploadService::OnAvatarOverrideTextTimeout,
                     // Unretained is fine here since the timer is a field
                     // member and will not fire if destroyed.
                     base::Unretained(this)));
}
#endif  // BUILDFLAG(ENABLE_DICE_SUPPORT)

#if BUILDFLAG(ENABLE_DICE_SUPPORT)
void BatchUploadService::OnAvatarOverrideTextTimeout() {
  CHECK(state_.saving_browser_state_ &&
        state_.saving_browser_state_->avatar_override_clear_callback_);
  state_.saving_browser_state_->avatar_override_clear_callback_.RunAndReset();
  state_.saving_browser_state_.reset();
}
#endif  // BUILDFLAG(ENABLE_DICE_SUPPORT)

bool BatchUploadService::IsDialogOpened() const {
  return state_.dialog_state_ != nullptr;
}

void BatchUploadService::ResetDialogState() {
  state_.dialog_state_.reset();
}

BatchUploadService::ResettableState::ResettableState() = default;
BatchUploadService::ResettableState::~ResettableState() = default;

BatchUploadService::ResettableState::DialogState::DialogState() = default;
BatchUploadService::ResettableState::DialogState::~DialogState() = default;

// static
std::vector<syncer::DataType> BatchUploadService::AvailableTypesOrder() {
  // Transforming to vector to avoid changing every definition on updates.
  return base::ToVector(kBatchUploadAvailableTypesOrder);
}
