// Copyright 2022 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/enterprise/idle/action.h"

#include <algorithm>
#include <cstring>
#include <utility>

#include "base/callback_list.h"
#include "base/check_is_test.h"
#include "base/containers/flat_map.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/scoped_observation.h"
#include "base/task/single_thread_task_runner.h"
#include "chrome/browser/browsing_data/chrome_browsing_data_remover_constants.h"
#include "chrome/browser/enterprise/idle/action_runner.h"
#include "chrome/browser/enterprise/idle/idle_service_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "components/enterprise/idle/idle_pref_names.h"
#include "components/enterprise/idle/metrics.h"
#include "components/prefs/pref_service.h"
#include "content/public/browser/browsing_data_remover.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/web_contents.h"

#if BUILDFLAG(IS_ANDROID)
#include "chrome/browser/ui/android/tab_model/tab_model.h"
#include "chrome/browser/ui/android/tab_model/tab_model_list.h"
#else
#include "chrome/browser/enterprise/idle/dialog_manager.h"
#include "chrome/browser/lifetime/application_lifetime_desktop.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"  // nogncheck crbug.com/40147906
#include "chrome/browser/ui/browser_window/public/browser_window_interface_iterator.h"
#include "chrome/browser/ui/idle_bubble.h"
#include "chrome/browser/ui/profiles/profile_picker.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#endif  // BUILDFLAG(IS_ANDROID)

namespace enterprise_idle {

namespace {

#if !BUILDFLAG(IS_ANDROID)
bool ProfileHasBrowsers(const Profile* profile) {
  DCHECK(profile);
  profile = profile->GetOriginalProfile();
  bool has_browsers = false;
  ForEachCurrentBrowserWindowInterfaceOrderedByActivation(
      [profile,
       &has_browsers](BrowserWindowInterface* browser_window_interface) {
        if (browser_window_interface->GetProfile()->GetOriginalProfile() ==
            profile) {
          has_browsers = true;
          return false;
        }
        return true;
      });
  return has_browsers;
}

// Wrapper Action for DialogManager. Shows a 30s warning dialog, shared across
// profiles.
class ShowDialogAction : public Action {
 public:
  explicit ShowDialogAction(base::flat_set<ActionType> action_types)
      : Action(static_cast<int>(ActionType::kShowDialog)),
        action_types_(std::move(action_types)) {}

  void Run(Profile* profile, Continuation continuation) override {
    base::TimeDelta timeout =
        IdleServiceFactory::GetForBrowserContext(profile)->GetTimeout();
    continuation_ = std::move(continuation);
    // Action object's lifetime extends until it calls `continuation_`, so
    // passing `this` as a raw pointer is safe.
    base::CallbackListSubscription subscription =
        DialogManager::GetInstance()->MaybeShowDialog(
            profile, timeout, action_types_,
            base::BindOnce(&ShowDialogAction::OnDialogFinished,
                           base::Unretained(this)));
    if (subscription) {
      // If there is no dialog to show, MaybeShowDialog() resolves immediately
      // and we destroy this object via OnCloseFinished(). This if guards
      // against a use-after-free.
      subscription_ = std::move(subscription);
    }
  }

  bool ShouldNotifyUserOfPendingDestructiveAction(Profile* profile) override {
    NOTREACHED();  // Should only be called in ActionFactory::Build().
  }

 private:
  void OnDialogFinished(bool expired) {
    std::move(continuation_).Run(/*success=*/expired);
  }

  base::flat_set<ActionType> action_types_;
  Action::Continuation continuation_;
  base::CallbackListSubscription subscription_;
};

// Action that closes all browsers for a Profile.
class CloseBrowsersAction : public Action {
 public:
  CloseBrowsersAction()
      : Action(static_cast<int>(ActionType::kCloseBrowsers)) {}

  // Action:
  void Run(Profile* profile, Continuation continuation) override {
    if (!ProfileHasBrowsers(profile)) {
      // No browsers for this profile, so it'd be a no-op. Finish immediately.
      std::move(continuation).Run(/*success=*/true);
      return;
    }

    continuation_ = std::move(continuation);
    // TODO(crbug.com/40222234): Get customer feedback on whether
    // skip_beforeunload should be true or false.
    chrome::CloseAllBrowsersWithProfile(
        profile,
        /*skip_beforeunload=*/true,
        base::BindRepeating(&CloseBrowsersAction::OnCloseSuccess,
                            base::Unretained(this)),
        base::BindRepeating(&CloseBrowsersAction::OnCloseAborted,
                            base::Unretained(this)));
  }

  bool ShouldNotifyUserOfPendingDestructiveAction(Profile* profile) override {
    // If there are no browsers, closing is a no-op. No need for a dialog then.
    return profile && ProfileHasBrowsers(profile);
  }

 private:
  void OnCloseSuccess(const base::FilePath& profile_dir) {
    metrics::RecordActionsSuccess(
        metrics::IdleTimeoutActionType::kCloseBrowsers, true);
    std::move(continuation_).Run(/*success=*/true);
  }

  void OnCloseAborted(const base::FilePath& profile_dir) {
    metrics::RecordActionsSuccess(
        metrics::IdleTimeoutActionType::kCloseBrowsers, false);
    std::move(continuation_).Run(/*success=*/false);
  }
  Action::Continuation continuation_;
  base::CallbackListSubscription subscription_;
};

// Action that shows the Profile Picker.
class ShowProfilePickerAction : public Action {
 public:
  ShowProfilePickerAction()
      : Action(static_cast<int>(ActionType::kShowProfilePicker)) {}

  // Action:
  void Run(Profile* profile, Continuation continuation) override {
    ProfilePicker::Show(ProfilePicker::Params::FromEntryPoint(
        ProfilePicker::EntryPoint::kProfileIdle));
    metrics::RecordActionsSuccess(
        metrics::IdleTimeoutActionType::kShowProfilePicker, true);
    std::move(continuation).Run(true);
  }

  bool ShouldNotifyUserOfPendingDestructiveAction(Profile* profile) override {
    return false;
  }
};
#endif  // !BUILDFLAG(IS_ANDROID)

// Action that clears one or more types of data via BrowsingDataRemover.
// Multiple data types may be grouped into a single ClearBrowsingDataAction
// object.
//
// TODO(crbug.com/40840688): Call ChromeBrowsingDataLifetimeManager, instead of
// BrowsingDataRemover directly? Especially if we add a keepalive, or use
// kClearBrowsingDataOnExitDeletionPending...
class ClearBrowsingDataAction : public Action,
                                content::BrowsingDataRemover::Observer {
 public:
  explicit ClearBrowsingDataAction(
      base::flat_set<ActionType> action_types,
      content::BrowsingDataRemover* browsing_data_remover_for_testing)
      : Action(static_cast<int>(ActionType::kClearBrowsingHistory)),
        action_types_(std::move(action_types)),
        browsing_data_remover_for_testing_(browsing_data_remover_for_testing) {}

  ~ClearBrowsingDataAction() override = default;

  // Action:
  void Run(Profile* profile, Continuation continuation) override {
    auto* remover = profile->GetBrowsingDataRemover();
    if (browsing_data_remover_for_testing_) {
      remover = browsing_data_remover_for_testing_.get();
    }
    observation_.Observe(remover);
    continuation_ = std::move(continuation);
    // Action object's lifetime extends until it calls `continuation_`, so
    // passing `this` as a raw pointer is safe.
    deletion_start_time_ = base::TimeTicks::Now();
    remover->RemoveAndReply(base::Time(), base::Time::Max(), GetRemoveMask(),
                            GetOriginTypeMask(), this);
    // TODO(crbug.com/40840688): Add a pair of keepalives?
  }

  bool ShouldNotifyUserOfPendingDestructiveAction(Profile* profile) override {
#if BUILDFLAG(IS_ANDROID)
    return true;
#else
    return profile && ProfileHasBrowsers(profile);
#endif
  }

  // content::BrowsingDataRemoverObserver::Observer:
  void OnBrowsingDataRemoverDone(uint64_t failed_data_types) override {
    bool success = failed_data_types == 0;
    observation_.Reset();
    metrics::RecordActionsSuccess(
        metrics::IdleTimeoutActionType::kClearBrowsingData, success);
    metrics::RecordIdleTimeoutActionTimeTaken(
        metrics::IdleTimeoutActionType::kClearBrowsingData,
        base::TimeTicks::Now() - deletion_start_time_);
    std::move(continuation_).Run(success);
  }

 private:
  uint64_t GetRemoveMask() const {
    using content::BrowsingDataRemover;
    static const std::pair<ActionType, uint64_t> entries[] = {
#if !BUILDFLAG(IS_ANDROID)
      {ActionType::kClearDownloadHistory,
       BrowsingDataRemover::DATA_TYPE_DOWNLOADS},
      {ActionType::kClearHostedAppData,
       chrome_browsing_data_remover::DATA_TYPE_SITE_DATA},
#endif  // !BUILDFLAG(IS_ANDROID)
      {ActionType::kClearBrowsingHistory,
       chrome_browsing_data_remover::DATA_TYPE_HISTORY},
      {ActionType::kClearCookiesAndOtherSiteData,
       chrome_browsing_data_remover::DATA_TYPE_SITE_DATA},
      {ActionType::kClearCachedImagesAndFiles,
       BrowsingDataRemover::DATA_TYPE_CACHE},
      {ActionType::kClearPasswordSignin,
       chrome_browsing_data_remover::DATA_TYPE_PASSWORDS},
      {ActionType::kClearAutofill,
       chrome_browsing_data_remover::DATA_TYPE_FORM_DATA},
      {ActionType::kClearSiteSettings,
       chrome_browsing_data_remover::DATA_TYPE_CONTENT_SETTINGS}
    };
    uint64_t result = 0;
    for (const auto& [action_type, mask] : entries) {
      if (action_types_.contains(action_type)) {
        result |= mask;
      }
    }
    return result;
  }

  uint64_t GetOriginTypeMask() const {
    using content::BrowsingDataRemover;
    uint64_t result = 0;
    if (action_types_.contains(ActionType::kClearCookiesAndOtherSiteData)) {
      result |= BrowsingDataRemover::ORIGIN_TYPE_UNPROTECTED_WEB;
    }
#if !BUILDFLAG(IS_ANDROID)
    if (action_types_.contains(ActionType::kClearHostedAppData)) {
      result |= BrowsingDataRemover::ORIGIN_TYPE_PROTECTED_WEB;
    }
#endif  // !BUILDFLAG(IS_ANDROID)
    return result;
  }

  base::TimeTicks deletion_start_time_;
  base::flat_set<ActionType> action_types_;
  raw_ptr<content::BrowsingDataRemover, DanglingUntriaged>
      browsing_data_remover_for_testing_;
  base::ScopedObservation<content::BrowsingDataRemover,
                          content::BrowsingDataRemover::Observer>
      observation_{this};
  Continuation continuation_;
};

class ReloadPagesAction : public Action {
 public:
  ReloadPagesAction() : Action(static_cast<int>(ActionType::kReloadPages)) {}

  void Run(Profile* profile, Continuation continuation) override {
#if BUILDFLAG(IS_ANDROID)
    // This covers regular tabs, PWAs, and CCTs.
    for (TabModel* model : TabModelList::models()) {
      if (model->GetProfile() != profile) {
        continue;  // Deliberately ignore incognito.
      }
      for (int i = 0; i < model->GetTabCount(); i++) {
        model->GetWebContentsAt(i)->GetController().Reload(
            content::ReloadType::NORMAL,
            /*check_for_repost=*/true);
      }
    }
#else
    // This covers regular tabs and PWAs.
    ForEachCurrentBrowserWindowInterfaceOrderedByActivation(
        [profile](BrowserWindowInterface* browser_window_interface) {
          const TabStripModel* model =
              browser_window_interface->GetTabStripModel();
          if (model->profile() != profile) {
            return true;
          }
          for (int i = 0; i < model->count(); i++) {
            model->GetWebContentsAt(i)->GetController().Reload(
                content::ReloadType::NORMAL,
                /*check_for_repost=*/true);
          }
          return true;
        });
#endif  // BUILDFLAG(IS_ANDROID)
    metrics::RecordActionsSuccess(metrics::IdleTimeoutActionType::kReloadPages,
                                  true);
    std::move(continuation).Run(/*success=*/true);
  }

  bool ShouldNotifyUserOfPendingDestructiveAction(Profile* profile) override {
#if BUILDFLAG(IS_ANDROID)
    return true;
#else
    return profile && ProfileHasBrowsers(profile);
#endif
  }
};

#if !BUILDFLAG(IS_ANDROID)
// Shows a bubble anchored to the 3-dot menu after other actions are finished.
class ShowBubbleAction : public Action {
 public:
  explicit ShowBubbleAction(base::flat_set<ActionType> action_types)
      : Action(static_cast<int>(ActionType::kShowBubble)),
        action_types_(std::move(action_types)) {}

  void Run(Profile* profile, Continuation continuation) override {
    BrowserWindowInterface* const bwi =
        GetLastActiveBrowserWindowInterfaceWithAnyProfile();
    profile->GetPrefs()->SetBoolean(prefs::kIdleTimeoutShowBubbleOnStartup,
                                    true);
    if (bwi && bwi->IsActive() && bwi->GetProfile() == profile &&
        !action_types_.contains(ActionType::kCloseBrowsers)) {
      // A browser for this profile has focus. Show the bubble there.
      ShowIdleBubble(
          bwi, IdleServiceFactory::GetForBrowserContext(profile)->GetTimeout(),
          ActionsToActionSet(action_types_),
          base::BindOnce(&ShowBubbleAction::OnClose, bwi->GetWeakPtr()));
    } else {
      // No active browser for this profile. Show the bubble when a browser
      // gains focus, or on next startup. Let IdleService::BrowserObserver do
      // it.
    }
    std::move(continuation).Run(true);
  }

  bool ShouldNotifyUserOfPendingDestructiveAction(Profile* profile) override {
    return false;
  }

 private:
  static void OnClose(base::WeakPtr<BrowserWindowInterface> bwi) {
    if (!bwi) {
      return;
    }
    bwi->GetProfile()->GetPrefs()->SetBoolean(
        prefs::kIdleTimeoutShowBubbleOnStartup, false);
  }

  base::flat_set<ActionType> action_types_;
};
#endif  // !BUILDFLAG(IS_ANDROID)

}  // namespace

Action::Action(int priority) : priority_(priority) {}

Action::~Action() = default;

bool ActionFactory::CompareActionsByPriority::operator()(
    const std::unique_ptr<Action>& a,
    const std::unique_ptr<Action>& b) const {
  return a->priority() > b->priority();
}

// static
ActionFactory* ActionFactory::GetInstance() {
  static base::NoDestructor<ActionFactory> instance;
  return instance.get();
}

ActionFactory::ActionQueue ActionFactory::Build(
    Profile* profile,
    const std::vector<ActionType>& action_types) {
  std::vector<std::unique_ptr<Action>> actions;

  base::flat_set<ActionType> clear_actions;
  for (auto action_type : action_types) {
    switch (action_type) {
#if !BUILDFLAG(IS_ANDROID)
      case ActionType::kCloseBrowsers:
        actions.push_back(std::make_unique<CloseBrowsersAction>());
        break;
      case ActionType::kShowProfilePicker:
        actions.push_back(std::make_unique<ShowProfilePickerAction>());
        break;
#endif  // !BUILDFLAG(IS_ANDROID)

      // "clear_*" actions are all grouped into a single Action object. Collect
      // them in a flat_set<>, and create the shared object once we have the
      // entire collection.
#if !BUILDFLAG(IS_ANDROID)
      case ActionType::kClearDownloadHistory:
      case ActionType::kClearHostedAppData:
#endif  // !BUILDFLAG(IS_ANDROID)
      case ActionType::kClearBrowsingHistory:
      case ActionType::kClearCookiesAndOtherSiteData:
      case ActionType::kClearCachedImagesAndFiles:
      case ActionType::kClearPasswordSignin:
      case ActionType::kClearAutofill:
      case ActionType::kClearSiteSettings:
        clear_actions.insert(action_type);
        break;

      case ActionType::kReloadPages:
        actions.push_back(std::make_unique<ReloadPagesAction>());
        break;

      default:
        // TODO(crbug.com/40222234): Perform validation in the `PolicyHandler`.
        NOTREACHED();
    }
  }

  if (!clear_actions.empty()) {
    // Merge "clear_*" actions into a single Action.
    actions.push_back(std::make_unique<ClearBrowsingDataAction>(
        std::move(clear_actions), browsing_data_remover_for_testing_));
  }

#if !BUILDFLAG(IS_ANDROID)
  bool needs_dialog = std::ranges::any_of(actions, [profile](const auto& a) {
    return a->ShouldNotifyUserOfPendingDestructiveAction(profile);
  });
  if (needs_dialog) {
    actions.push_back(std::make_unique<ShowDialogAction>(action_types));
    actions.push_back(std::make_unique<ShowBubbleAction>(action_types));
  }
#endif  // !BUILDFLAG(IS_ANDROID)

  return ActionQueue(ActionQueue::value_compare(), std::move(actions));
}

ActionFactory::ActionFactory() = default;
ActionFactory::~ActionFactory() = default;

void ActionFactory::SetBrowsingDataRemoverForTesting(
    content::BrowsingDataRemover* remover) {
  CHECK_IS_TEST();
  browsing_data_remover_for_testing_ = remover;
}

#if !BUILDFLAG(IS_ANDROID)
IdleDialog::ActionSet ActionsToActionSet(
    const base::flat_set<ActionType>& action_types) {
  IdleDialog::ActionSet action_set = {.close = false, .clear = false};
  for (ActionType action_type : action_types) {
    switch (action_type) {
      case ActionType::kCloseBrowsers:
        action_set.close = true;
        break;

      case ActionType::kShowDialog:
      case ActionType::kShowProfilePicker:
      case ActionType::kShowBubble:
        break;

      case ActionType::kClearBrowsingHistory:
      case ActionType::kClearDownloadHistory:
      case ActionType::kClearCookiesAndOtherSiteData:
      case ActionType::kClearCachedImagesAndFiles:
      case ActionType::kClearPasswordSignin:
      case ActionType::kClearAutofill:
      case ActionType::kClearSiteSettings:
      case ActionType::kClearHostedAppData:
      case ActionType::kReloadPages:
        action_set.clear = true;
        break;
    }
  }
  return action_set;
}
#endif  // !BUILDFLAG(IS_ANDROID)

}  // namespace enterprise_idle
