// 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/ui/tabs/tab_group_deletion_dialog_controller.h"

#include <string>

#include "base/check_deref.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/i18n/message_formatter.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/signin/identity_manager_factory.h"
#include "chrome/browser/tab_group_sync/tab_group_sync_service_factory.h"
#include "chrome/browser/ui/browser_element_identifiers.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/dialogs/browser_dialogs.h"
#include "chrome/browser/ui/tabs/saved_tab_groups/saved_tab_group_pref_names.h"
#include "chrome/browser/ui/tabs/saved_tab_groups/saved_tab_group_utils.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/tabs/tab_strip_model_observer.h"
#include "chrome/common/pref_names.h"
#include "chrome/grit/generated_resources.h"
#include "components/prefs/pref_service.h"
#include "components/saved_tab_groups/public/types.h"
#include "components/signin/public/base/consent_level.h"
#include "components/signin/public/identity_manager/identity_manager.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/models/dialog_model.h"
#include "ui/views/widget/widget.h"

namespace tab_groups {

namespace {
// The text that shows on the checkbox.
constexpr int kDontAskId = IDS_TAB_GROUP_DELETION_DIALOG_DONT_ASK;

// Body text for all delete actions.
constexpr int kDeleteBodySyncedId =
    IDS_TAB_GROUP_DELETION_DIALOG_BODY_SYNCED_DELETE;
constexpr int kDeleteBodyNotSyncedId =
    IDS_TAB_GROUP_DELETION_DIALOG_BODY_NOT_SYNCED_DELETE;

// For deletion, the text that shows on the dialog.
constexpr int kDeleteTitleId = IDS_TAB_GROUP_DELETION_DIALOG_TITLE_DELETE;
constexpr int kDeleteOkTextId = IDS_TAB_GROUP_DELETION_DIALOG_OK_TEXT_DELETE;

// For ungrouping, the text that shows on the dialog.
constexpr int kUngroupTitleId = IDS_TAB_GROUP_DELETION_DIALOG_TITLE_UNGROUP;
constexpr int kUngroupBodySyncedId =
    IDS_TAB_GROUP_DELETION_DIALOG_BODY_SYNCED_UNGROUP;
constexpr int kUngroupBodyNotSyncedId =
    IDS_TAB_GROUP_DELETION_DIALOG_BODY_NOT_SYNCED_UNGROUP;
constexpr int kUngroupOkTextId = IDS_TAB_GROUP_DELETION_DIALOG_OK_TEXT_UNGROUP;

// For closing the last tab, the text that shows on the dialog.
constexpr int kCloseTabAndDeleteTitleId =
    IDS_TAB_GROUP_DELETION_DIALOG_TITLE_CLOSE_TAB_AND_DELETE;

// For removing the last tab, the text that shows on the dialog.
constexpr int kRemoveTabAndDeleteTitleId =
    IDS_TAB_GROUP_DELETION_DIALOG_TITLE_REMOVE_TAB_AND_DELETE;

struct DialogText {
  const std::u16string title;
  const std::u16string body;
  const std::u16string ok_text;
  const std::optional<std::u16string> cancel_text;
};

// Returns the list of strings that are needed for a given dialog type.
DialogText GetDialogText(
    Profile* profile,
    const DeletionDialogController::DialogMetadata& dialog_metadata) {
  tab_groups::TabGroupSyncService* tab_group_service =
      tab_groups::TabGroupSyncServiceFactory::GetForProfile(profile);

  const bool is_sync_enabled =
      tab_group_service &&
      tab_groups::SavedTabGroupUtils::AreSavedTabGroupsSyncedForProfile(
          profile);

  std::u16string email =
      l10n_util::GetStringUTF16(IDS_TAB_GROUP_DELETION_DIALOG_MISSING_EMAIL);
  signin::IdentityManager* identity_manager =
      IdentityManagerFactory::GetForProfile(profile);
  if (profile && identity_manager) {
    email = base::UTF8ToUTF16(
        identity_manager->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin)
            .email);
  }

  const int closing_group_count = dialog_metadata.closing_group_count;
  const int closing_multiple_groups = closing_group_count > 1;
  const int plural_type_count =
      dialog_metadata.closing_multiple_tabs + closing_multiple_groups;

  switch (dialog_metadata.type) {
    case DeletionDialogController::DialogType::DeleteSingle: {
      return DialogText{
          base::i18n::MessageFormatter::FormatWithNumberedArgs(
              l10n_util::GetStringUTF16(kDeleteTitleId), closing_group_count),
          base::i18n::MessageFormatter::FormatWithNumberedArgs(
              is_sync_enabled
                  ? l10n_util::GetStringFUTF16(kDeleteBodySyncedId, email)
                  : l10n_util::GetStringUTF16(kDeleteBodyNotSyncedId),
              closing_group_count),
          base::i18n::MessageFormatter::FormatWithNumberedArgs(
              l10n_util::GetStringUTF16(kDeleteOkTextId), closing_group_count)};
    }
    case DeletionDialogController::DialogType::UngroupSingle: {
      return DialogText{
          base::i18n::MessageFormatter::FormatWithNumberedArgs(
              l10n_util::GetStringUTF16(kUngroupTitleId), closing_group_count),
          base::i18n::MessageFormatter::FormatWithNumberedArgs(
              is_sync_enabled
                  ? l10n_util::GetStringFUTF16(kUngroupBodySyncedId, email)
                  : l10n_util::GetStringUTF16(kUngroupBodyNotSyncedId),
              closing_group_count),
          l10n_util::GetStringUTF16(kUngroupOkTextId)};
    }
    case DeletionDialogController::DialogType::RemoveTabAndDelete: {
      return DialogText{
          base::i18n::MessageFormatter::FormatWithNumberedArgs(
              l10n_util::GetStringUTF16(kRemoveTabAndDeleteTitleId),
              plural_type_count),
          base::i18n::MessageFormatter::FormatWithNumberedArgs(
              is_sync_enabled
                  ? l10n_util::GetStringFUTF16(kDeleteBodySyncedId, email)
                  : l10n_util::GetStringUTF16(kDeleteBodyNotSyncedId),
              closing_group_count),
          base::i18n::MessageFormatter::FormatWithNumberedArgs(
              l10n_util::GetStringUTF16(kDeleteOkTextId), closing_group_count)};
    }
    case DeletionDialogController::DialogType::CloseTabAndDelete: {
      return DialogText{
          base::i18n::MessageFormatter::FormatWithNumberedArgs(
              l10n_util::GetStringUTF16(kCloseTabAndDeleteTitleId),
              plural_type_count),
          base::i18n::MessageFormatter::FormatWithNumberedArgs(
              is_sync_enabled
                  ? l10n_util::GetStringFUTF16(kDeleteBodySyncedId, email)
                  : l10n_util::GetStringUTF16(kDeleteBodyNotSyncedId),
              closing_group_count),
          base::i18n::MessageFormatter::FormatWithNumberedArgs(
              l10n_util::GetStringUTF16(kDeleteOkTextId), closing_group_count)};
    }
    case DeletionDialogController::DialogType::DeleteSingleShared: {
      const bool title_is_empty =
          !dialog_metadata.title_of_closing_group.has_value() ||
          dialog_metadata.title_of_closing_group->empty();
      std::u16string body_text =
          title_is_empty
              ? l10n_util::GetStringUTF16(
                    IDS_DATA_SHARING_OWNER_DELETE_DIALOG_BODY_NO_GROUP_TITLE)
              : l10n_util::GetStringFUTF16(
                    IDS_DATA_SHARING_OWNER_DELETE_DIALOG_BODY,
                    dialog_metadata.title_of_closing_group.value());
      return DialogText{
          l10n_util::GetStringUTF16(IDS_DATA_SHARING_OWNER_DELETE_DIALOG_TITLE),
          std::move(body_text),
          l10n_util::GetStringUTF16(
              IDS_DATA_SHARING_OWNER_DELETE_DIALOG_CONFIRM)};
    }
    case DeletionDialogController::DialogType::CloseTabAndKeepOrLeaveGroup: {
      return DialogText{
          l10n_util::GetPluralStringFUTF16(
              IDS_DATA_SHARING_DELETE_LAST_TAB_TITLE,
              dialog_metadata.closing_group_count),
          l10n_util::GetPluralStringFUTF16(
              IDS_DATA_SHARING_MEMBER_DELETE_LAST_TAB_BODY,
              dialog_metadata.closing_group_count),
          l10n_util::GetPluralStringFUTF16(
              IDS_DATA_SHARING_DELETE_LAST_TAB_CONFIRM,
              dialog_metadata.closing_group_count),
          l10n_util::GetPluralStringFUTF16(
              IDS_DATA_SHARING_MEMBER_DELETE_LAST_TAB_CANCEL,
              dialog_metadata.closing_group_count),
      };
    }
    case DeletionDialogController::DialogType::CloseTabAndKeepOrDeleteGroup: {
      return DialogText{
          l10n_util::GetPluralStringFUTF16(
              IDS_DATA_SHARING_DELETE_LAST_TAB_TITLE,
              dialog_metadata.closing_group_count),
          l10n_util::GetPluralStringFUTF16(
              IDS_DATA_SHARING_OWNER_DELETE_LAST_TAB_BODY,
              dialog_metadata.closing_group_count),
          l10n_util::GetPluralStringFUTF16(
              IDS_DATA_SHARING_DELETE_LAST_TAB_CONFIRM,
              dialog_metadata.closing_group_count),
          l10n_util::GetPluralStringFUTF16(
              IDS_DATA_SHARING_OWNER_DELETE_LAST_TAB_CANCEL,
              dialog_metadata.closing_group_count),
      };
    }
    case DeletionDialogController::DialogType::LeaveGroup: {
      const bool title_is_empty =
          !dialog_metadata.title_of_closing_group.has_value() ||
          dialog_metadata.title_of_closing_group->empty();
      std::u16string body_text =
          title_is_empty
              ? l10n_util::GetStringUTF16(
                    IDS_DATA_SHARING_LEAVE_DIALOG_BODY_NO_GROUP_TITLE)
              : l10n_util::GetStringFUTF16(
                    IDS_DATA_SHARING_LEAVE_DIALOG_BODY,
                    dialog_metadata.title_of_closing_group.value());

      return DialogText{
          l10n_util::GetStringUTF16(IDS_DATA_SHARING_LEAVE_DIALOG_TITLE),
          std::move(body_text),
          l10n_util::GetStringUTF16(IDS_DATA_SHARING_LEAVE_DIALOG_CONFIRM)};
    }
  }
}

bool IsDialogSkippable(DeletionDialogController::DialogType type) {
  switch (type) {
    // Saved tab group dialogs are skippable.
    case DeletionDialogController::DialogType::DeleteSingle:
    case DeletionDialogController::DialogType::UngroupSingle:
    case DeletionDialogController::DialogType::RemoveTabAndDelete:
    case DeletionDialogController::DialogType::CloseTabAndDelete: {
      return true;
    }
    // Shared tab group dialogs aren't skippable.
    case DeletionDialogController::DialogType::DeleteSingleShared:
    case DeletionDialogController::DialogType::CloseTabAndKeepOrLeaveGroup:
    case DeletionDialogController::DialogType::CloseTabAndKeepOrDeleteGroup:
    case DeletionDialogController::DialogType::LeaveGroup: {
      return false;
    }
  }
}

// Returns the the value from the settings pref for a given dialog type.
bool IsDialogSkippedByUserSettings(Profile* profile,
                                   DeletionDialogController::DialogType type) {
  if (!profile) {
    return false;
  }
  PrefService* pref_service = profile->GetPrefs();
  switch (type) {
    case DeletionDialogController::DialogType::DeleteSingle: {
      return pref_service->GetBoolean(
          saved_tab_groups::prefs::kTabGroupsDeletionSkipDialogOnDelete);
    }
    case DeletionDialogController::DialogType::UngroupSingle: {
      return pref_service->GetBoolean(
          saved_tab_groups::prefs::kTabGroupsDeletionSkipDialogOnUngroup);
    }
    case DeletionDialogController::DialogType::RemoveTabAndDelete: {
      return pref_service->GetBoolean(
          saved_tab_groups::prefs::kTabGroupsDeletionSkipDialogOnRemoveTab);
    }
    case DeletionDialogController::DialogType::CloseTabAndDelete: {
      return pref_service->GetBoolean(
          saved_tab_groups::prefs::kTabGroupsDeletionSkipDialogOnCloseTab);
    }
    // Shared tab groups dialogs aren't skippable.
    case DeletionDialogController::DialogType::DeleteSingleShared:
    case DeletionDialogController::DialogType::CloseTabAndKeepOrLeaveGroup:
    case DeletionDialogController::DialogType::CloseTabAndKeepOrDeleteGroup:
    case DeletionDialogController::DialogType::LeaveGroup: {
      return false;
    }
  }
}

void SetSkipDialogForType(Profile* profile,
                          DeletionDialogController::DialogType type,
                          bool new_value) {
  if (!profile) {
    return;
  }

  PrefService* pref_service = profile->GetPrefs();
  switch (type) {
    case DeletionDialogController::DialogType::DeleteSingle: {
      return pref_service->SetBoolean(
          saved_tab_groups::prefs::kTabGroupsDeletionSkipDialogOnDelete,
          new_value);
    }
    case DeletionDialogController::DialogType::UngroupSingle: {
      return pref_service->SetBoolean(
          saved_tab_groups::prefs::kTabGroupsDeletionSkipDialogOnUngroup,
          new_value);
    }
    case DeletionDialogController::DialogType::RemoveTabAndDelete: {
      return pref_service->SetBoolean(
          saved_tab_groups::prefs::kTabGroupsDeletionSkipDialogOnRemoveTab,
          new_value);
    }
    case DeletionDialogController::DialogType::CloseTabAndDelete: {
      return pref_service->SetBoolean(
          saved_tab_groups::prefs::kTabGroupsDeletionSkipDialogOnCloseTab,
          new_value);
    }
    // Shared tab group dialogs aren't skippable.
    case DeletionDialogController::DialogType::DeleteSingleShared:
    case DeletionDialogController::DialogType::CloseTabAndKeepOrLeaveGroup:
    case DeletionDialogController::DialogType::CloseTabAndKeepOrDeleteGroup:
    case DeletionDialogController::DialogType::LeaveGroup: {
      // We should never try to set the skip pref for these dialog types.
      NOTREACHED();
    }
  }
}

// Keep type dialogs don't let the user cancel their action; instead, they
// choose whether the group should stick around or go away.
bool IsDialogKeepType(DeletionDialogController::DialogType type) {
  return type == DeletionDialogController::DialogType::
                     CloseTabAndKeepOrDeleteGroup ||
         type ==
             DeletionDialogController::DialogType::CloseTabAndKeepOrLeaveGroup;
}

}  // anonymous namespace

// DialogMetadata
DeletionDialogController::DialogMetadata::DialogMetadata(
    DialogType type,
    int closing_group_count,
    bool closing_multiple_tabs)
    : type(type),
      closing_group_count(closing_group_count),
      closing_multiple_tabs(closing_multiple_tabs) {}

DeletionDialogController::DialogMetadata::DialogMetadata(DialogType type)
    : type(type) {}

DeletionDialogController::DialogMetadata::~DialogMetadata() = default;

// DialogState
DeletionDialogController::DialogState::DialogState(
    DialogType type_,
    ui::DialogModel* dialog_model_,
    base::OnceCallback<void(DeletionDialogTiming)> callback_,
    std::optional<base::OnceCallback<void()>> keep_groups_)
    : type(type_),
      dialog_model(dialog_model_),
      callback(std::move(callback_)),
      keep_groups(std::move(keep_groups_)) {}

DeletionDialogController::DialogState::~DialogState() = default;

DEFINE_USER_DATA(DeletionDialogController);

// static
DeletionDialogController* DeletionDialogController::From(
    BrowserWindowInterface* browser) {
  return Get(browser->GetUnownedUserDataHost());
}

DeletionDialogController::DeletionDialogController(
    BrowserWindowInterface* browser,
    Profile* profile,
    TabStripModel* tab_strip_model)
    : scoped_unowned_user_data_(browser->GetUnownedUserDataHost(), *this),
      profile_(CHECK_DEREF(profile)),
      tab_strip_model_(CHECK_DEREF(tab_strip_model)) {
  show_dialog_model_fn_ =
      base::BindRepeating(&DeletionDialogController::CreateDialogFromBrowser,
                          weak_ptr_factory_.GetWeakPtr(), browser);
  tab_strip_model_->AddObserver(this);
}

DeletionDialogController::DeletionDialogController(
    BrowserWindowInterface* browser,
    Profile* profile,
    TabStripModel* tab_strip_model,
    ShowDialogModelCallback show_dialog_model_fn)
    : scoped_unowned_user_data_(browser->GetUnownedUserDataHost(), *this),
      profile_(CHECK_DEREF(profile)),
      show_dialog_model_fn_(show_dialog_model_fn),
      tab_strip_model_(CHECK_DEREF(tab_strip_model)) {}

DeletionDialogController::~DeletionDialogController() = default;

bool DeletionDialogController::CanShowDialog() const {
  return !IsShowingDialog();
}

bool DeletionDialogController::IsShowingDialog() const {
  return !!state_;
}

void DeletionDialogController::CreateDialogFromBrowser(
    BrowserWindowInterface* browser,
    std::unique_ptr<ui::DialogModel> dialog_model) {
  // Showing a modal dialog can spin a nested run loop on macOS, during which
  // the owning browser and controller may be torn down. Guard the post-show
  // write with a weak pointer to avoid a use-after-free write if `this` is
  // destroyed during ShowBrowserModal.
  auto weak_ptr = weak_ptr_factory_.GetWeakPtr();
  views::Widget* widget =
      chrome::ShowBrowserModal(browser, std::move(dialog_model));
  if (weak_ptr) {
    weak_ptr->widget_ = widget;
  }
}

bool DeletionDialogController::MaybeShowDialog(
    const DialogMetadata& metadata,
    base::OnceCallback<void(DeletionDialogTiming)> callback,
    std::optional<base::OnceCallback<void()>> keep_groups) {
  if (IsDialogKeepType(metadata.type)) {
    CHECK(keep_groups.has_value());
  } else {
    CHECK(!keep_groups.has_value());
  }

  if (!CanShowDialog()) {
    return false;
  }

  if (IsDialogSkippedByUserSettings(GetProfile(), metadata.type)) {
    std::move(callback).Run(DeletionDialogTiming::Synchronous);
    return false;
  }

  std::unique_ptr<ui::DialogModel> dialog_model = BuildDialogModel(metadata);

  state_.emplace(metadata.type, dialog_model.get(), std::move(callback),
                 std::move(keep_groups));

  show_dialog_model_fn_.Run(std::move(dialog_model));
  return true;
}

void DeletionDialogController::OnTabStripModelChanged(
    TabStripModel* tab_strip_model,
    const TabStripModelChange& change,
    const TabStripSelectionChange& selection) {
  if (widget_) {
    widget_->Close();
  }
}

void DeletionDialogController::SetPrefsPreventShowingDialogForTesting(
    bool should_prevent_dialog) {
  auto* prefs = profile_->GetPrefs();
  prefs->SetBoolean(
      saved_tab_groups::prefs::kTabGroupsDeletionSkipDialogOnDelete,
      should_prevent_dialog);
  prefs->SetBoolean(
      saved_tab_groups::prefs::kTabGroupsDeletionSkipDialogOnUngroup,
      should_prevent_dialog);
  prefs->SetBoolean(
      saved_tab_groups::prefs::kTabGroupsDeletionSkipDialogOnRemoveTab,
      should_prevent_dialog);
  prefs->SetBoolean(
      saved_tab_groups::prefs::kTabGroupsDeletionSkipDialogOnCloseTab,
      should_prevent_dialog);
}

void DeletionDialogController::OnDialogOk() {
  if (state_->dialog_model &&
      state_->dialog_model->HasField(kDeletionDialogDontAskCheckboxId) &&
      state_->dialog_model
          ->GetCheckboxByUniqueId(kDeletionDialogDontAskCheckboxId)
          ->is_checked()) {
    SetSkipDialogForType(GetProfile(), state_->type, true);
  }
  if (IsDialogKeepType(state_->type)) {
    std::move(state_->keep_groups.value()).Run();
  }
  std::move(state_->callback).Run(DeletionDialogTiming::Asynchronous);
  state_.reset();
}

void DeletionDialogController::OnDialogCancel() {
  if (IsDialogKeepType(state_->type)) {
    std::move(state_->callback).Run(DeletionDialogTiming::Asynchronous);
  }
  state_.reset();
}

void DeletionDialogController::OnCloseAction() {
  state_.reset();
}

void DeletionDialogController::OnDialogDestroying() {
  widget_ = nullptr;
  state_.reset();
}

std::unique_ptr<ui::DialogModel> DeletionDialogController::BuildDialogModel(
    const DialogMetadata& metadata) {
  DialogText strings = GetDialogText(GetProfile(), metadata);

  ui::DialogModel::Button::Params cancel_button_params;
  cancel_button_params.SetEnabled(true).SetId(kDeletionDialogCancelButtonId);
  if (strings.cancel_text.has_value()) {
    cancel_button_params.SetLabel(strings.cancel_text.value());
  }

  ui::DialogModel::Builder dialog_builder = ui::DialogModel::Builder();
  dialog_builder.SetTitle(strings.title)
      .AddParagraph(ui::DialogModelLabel(strings.body))
      .AddCancelButton(base::BindOnce(&DeletionDialogController::OnDialogCancel,
                                      weak_ptr_factory_.GetWeakPtr()),
                       cancel_button_params)
      .AddOkButton(base::BindOnce(&DeletionDialogController::OnDialogOk,
                                  weak_ptr_factory_.GetWeakPtr()),
                   ui::DialogModel::Button::Params()
                       .SetLabel(strings.ok_text)
                       .SetEnabled(true)
                       .SetId(kDeletionDialogOkButtonId))
      .SetCloseActionCallback(
          base::BindOnce(&DeletionDialogController::OnCloseAction,
                         weak_ptr_factory_.GetWeakPtr()))
      .SetDialogDestroyingCallback(
          base::BindOnce(&DeletionDialogController::OnDialogDestroying,
                         weak_ptr_factory_.GetWeakPtr()));
  if (IsDialogSkippable(metadata.type)) {
    dialog_builder.AddCheckbox(
        kDeletionDialogDontAskCheckboxId,
        ui::DialogModelLabel(l10n_util::GetStringUTF16(kDontAskId)));
  }
  return dialog_builder.Build();
}

Profile* DeletionDialogController::GetProfile() {
  return &profile_.get();
}

}  // namespace tab_groups
