// Copyright 2015 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/history/browsing_history_handler.h"

#include <stddef.h>

#include <optional>
#include <set>

#include "base/check_deref.h"
#include "base/check_op.h"
#include "base/containers/flat_map.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/i18n/rtl.h"
#include "base/i18n/time_formatting.h"
#include "base/metrics/histogram_functions.h"
#include "base/notreached.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/default_clock.h"
#include "base/time/time.h"
#include "chrome/browser/bookmarks/bookmark_model_factory.h"
#include "chrome/browser/content_settings/host_content_settings_map_factory.h"
#include "chrome/browser/critical_actions/critical_action_factory.h"
#include "chrome/browser/critical_actions/critical_action_ui_utils.h"
#include "chrome/browser/favicon/large_icon_service_factory.h"
#include "chrome/browser/history/history_service_factory.h"
#include "chrome/browser/history/history_utils.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/signin/account_preview_data_service_factory.h"
#include "chrome/browser/signin/chrome_signin_pref_names.h"
#include "chrome/browser/signin/identity_manager_factory.h"
#include "chrome/browser/signin/signin_ui_util.h"
#include "chrome/browser/supervised_user/supervised_user_url_filtering_service_factory.h"
#include "chrome/browser/sync/device_info_sync_service_factory.h"
#include "chrome/browser/sync/sync_service_factory.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/browser_window/public/global_browser_collection.h"
#include "chrome/browser/ui/chrome_pages.h"
#include "chrome/browser/ui/hats/hats_service.h"
#include "chrome/browser/ui/hats/hats_service_factory.h"
#include "chrome/browser/ui/hats/survey_config.h"
#include "chrome/browser/ui/profiles/profile_view_utils.h"
#include "chrome/browser/ui/url_identity.h"
#include "chrome/browser/ui/webui/favicon_source.h"
#include "chrome/browser/ui/webui/signin/signin_utils.h"
#include "chrome/browser/ui/webui/top_chrome/top_chrome_web_ui_controller.h"
#include "chrome/common/buildflags.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/pref_names.h"
#include "components/bookmarks/browser/bookmark_model.h"
#include "components/bookmarks/browser/bookmark_utils.h"
#include "components/critical_actions/core/browser/critical_action_service.h"
#include "components/critical_actions/core/browser/critical_action_types.h"
#include "components/critical_actions/core/browser/features.h"
#include "components/favicon/core/fallback_url_util.h"
#include "components/favicon/core/large_icon_service.h"
#include "components/favicon_base/favicon_url_parser.h"
#include "components/history/core/browser/features.h"
#include "components/history_clusters/core/config.h"
#include "components/history_clusters/core/features.h"
#include "components/history_clusters/core/history_clusters_prefs.h"
#include "components/history_embeddings/core/history_embeddings_features.h"
#include "components/keyed_service/core/service_access_type.h"
#include "components/prefs/pref_service.h"
#include "components/query_parser/snippet.h"
#include "components/signin/public/base/signin_pref_names.h"
#include "components/signin/public/base/signin_prefs.h"
#include "components/signin/public/identity_manager/account_info.h"
#include "components/signin/public/identity_manager/identity_manager.h"
#include "components/strings/grit/components_strings.h"
#include "components/supervised_user/core/browser/supervised_user_url_filtering_service.h"
#include "components/supervised_user/core/browser/supervised_user_utils.h"
#include "components/sync/protocol/sync_enums.pb.h"
#include "components/sync/service/sync_service.h"
#include "components/sync_device_info/device_info.h"
#include "components/sync_device_info/device_info_sync_service.h"
#include "components/sync_device_info/device_info_tracker.h"
#include "content/public/browser/url_data_source.h"
#include "content/public/browser/web_ui.h"
#include "third_party/abseil-cpp/absl/container/flat_hash_map.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/l10n/time_format.h"
#include "ui/webui/resources/cr_components/history/history.mojom.h"

using bookmarks::BookmarkModel;
using history::BrowsingHistoryService;
using history::HistoryService;
using history::WebHistoryService;

namespace {

#if !BUILDFLAG(IS_CHROMEOS)
constexpr int kHistorySyncPromoShownThreshold = 5;
constexpr base::TimeDelta kHistorySyncPromoCooldown = base::Days(7);

history::mojom::AccountInfoPtr CreateAccountInfoDataMojo(
    const AccountInfo& info) {
  auto account_info_mojo = history::mojom::AccountInfo::New();
  account_info_mojo->name = std::string(info.GetFullName().value_or(""));
  account_info_mojo->email = std::string(info.GetEmail());
  account_info_mojo->account_image_src =
      GURL(signin::GetAccountPictureUrl(info));
  return account_info_mojo;
}
#endif

// Identifiers for the type of device from which a history entry originated.
static const char kDeviceTypeLaptop[] = "laptop";
static const char kDeviceTypePhone[] = "phone";
static const char kDeviceTypeTablet[] = "tablet";

// Gets the name and type of a device for the given sync client ID.
// |name| and |type| are out parameters.
void GetDeviceNameAndType(const syncer::DeviceInfoTracker* tracker,
                          const std::string& client_id,
                          std::string* name,
                          std::string* type) {
  // DeviceInfoTracker must be syncing in order for remote history entries to
  // be available.
  DCHECK(tracker);
  DCHECK(tracker->IsSyncing());

  const syncer::DeviceInfo* device_info = tracker->GetDeviceInfo(client_id);
  if (device_info) {
    *name = device_info->client_name();
    switch (device_info->form_factor()) {
      case syncer::DeviceInfo::FormFactor::kPhone:
        *type = kDeviceTypePhone;
        break;
      case syncer::DeviceInfo::FormFactor::kTablet:
        *type = kDeviceTypeTablet;
        break;
      // return the laptop icon as default.
      case syncer::DeviceInfo::FormFactor::kUnknown:
        [[fallthrough]];
      case syncer::DeviceInfo::FormFactor::kAutomotive:
        [[fallthrough]];
      case syncer::DeviceInfo::FormFactor::kWearable:
        [[fallthrough]];
      case syncer::DeviceInfo::FormFactor::kTv:
        [[fallthrough]];
      case syncer::DeviceInfo::FormFactor::kDesktop:
        *type = kDeviceTypeLaptop;
    }
    return;
  }

  *name = l10n_util::GetStringUTF8(IDS_HISTORY_UNKNOWN_DEVICE);
  *type = kDeviceTypeLaptop;
}

// Formats `entry`'s URL and title and adds them to `result`.
std::pair<std::string, std::string> SetHistoryEntryUrlAndTitle(
    const BrowsingHistoryService::HistoryEntry& entry) {
  bool using_url_as_the_title = false;
  std::u16string title_to_set(entry.title);
  if (entry.title.empty()) {
    using_url_as_the_title = true;
    title_to_set = base::UTF8ToUTF16(entry.url.spec());
  }

  // Since the title can contain BiDi text, we need to mark the text as either
  // RTL or LTR, depending on the characters in the string. If we use the URL
  // as the title, we mark the title as LTR since URLs are always treated as
  // left to right strings.
  if (base::i18n::IsRTL()) {
    if (using_url_as_the_title) {
      base::i18n::WrapStringWithLTRFormatting(&title_to_set);
    } else {
      base::i18n::AdjustStringForLocaleDirection(&title_to_set);
    }
  }

  // Number of chars to truncate titles when making them "short".
  static const size_t kShortTitleLength = 300;
  if (title_to_set.size() > kShortTitleLength) {
    title_to_set.resize(kShortTitleLength);
  }

  return std::make_tuple(entry.url.spec(), base::UTF16ToUTF8(title_to_set));
}

// Helper function to check if entry is present in local database (local-side
// history).
bool IsUrlInLocalDatabase(const BrowsingHistoryService::HistoryEntry& entry) {
  switch (entry.entry_type) {
    case BrowsingHistoryService::HistoryEntry::EntryType::EMPTY_ENTRY:
    case BrowsingHistoryService::HistoryEntry::EntryType::REMOTE_ENTRY:
      return false;
    case BrowsingHistoryService::HistoryEntry::EntryType::LOCAL_ENTRY:
    case BrowsingHistoryService::HistoryEntry::EntryType::COMBINED_ENTRY:
      return true;
  }
  NOTREACHED();
}

// Helper function to check if entry is present in user remote data (server-side
// history).
bool IsEntryInRemoteUserData(
    const BrowsingHistoryService::HistoryEntry& entry) {
  switch (entry.entry_type) {
    case BrowsingHistoryService::HistoryEntry::EntryType::EMPTY_ENTRY:
    case BrowsingHistoryService::HistoryEntry::EntryType::LOCAL_ENTRY:
      return false;
    case BrowsingHistoryService::HistoryEntry::EntryType::REMOTE_ENTRY:
    case BrowsingHistoryService::HistoryEntry::EntryType::COMBINED_ENTRY:
      return true;
  }
  NOTREACHED();
}

// Expected URL types for `UrlIdentity::CreateFromUrl()`.
constexpr UrlIdentity::TypeSet allowed_types = {
    UrlIdentity::Type::kDefault, UrlIdentity::Type::kFile,
    UrlIdentity::Type::kIsolatedWebApp, UrlIdentity::Type::kChromeExtension};
constexpr UrlIdentity::FormatOptions url_identity_options{
    .default_options = {UrlIdentity::DefaultFormatOptions::
                            kOmitSchemePathAndTrivialSubdomains}};

history::mojom::FilteringBehavior FilteringBehaviorToMojom(
    supervised_user::FilteringBehavior filtering_behavior) {
  switch (filtering_behavior) {
    case supervised_user::FilteringBehavior::kAllow:
      return history::mojom::FilteringBehavior::kAllow;
    case supervised_user::FilteringBehavior::kBlock:
      return history::mojom::FilteringBehavior::kBlock;
    case supervised_user::FilteringBehavior::kInvalid:
      return history::mojom::FilteringBehavior::kInvalid;
    default:
      return history::mojom::FilteringBehavior::kUnknown;
  }
}

// Converts `entry` to a history::mojom::QueryResult to be owned by the caller.
history::mojom::HistoryEntryPtr HistoryEntryToMojom(
    const BrowsingHistoryService::HistoryEntry& entry,
    BookmarkModel* bookmark_model,
    Profile& profile,
    const syncer::DeviceInfoTracker* tracker,
    base::Clock* clock) {
  auto result_mojom = history::mojom::HistoryEntry::New();
  base::DictValue dictionary;
  auto url_and_title = SetHistoryEntryUrlAndTitle(entry);
  result_mojom->url = url_and_title.first;
  result_mojom->title = url_and_title.second;

  // UrlIdentity holds a user-identifiable string for a URL. We will display
  // this string to the user.
  std::u16string domain =
      UrlIdentity::CreateFromUrl(&profile, entry.url, allowed_types,
                                 url_identity_options)
          .name;

  // When the domain is empty, use the scheme instead. This allows for a
  // sensible treatment of e.g. file: URLs when group by domain is on.
  if (domain.empty()) {
    domain = base::UTF8ToUTF16(entry.url.GetScheme() + ":");
  }

  // The items which are to be written into result are also described in
  // chrome/browser/resources/history/history.js in @typedef for
  // HistoryEntry. Please update it whenever you add or remove
  // any keys in result.
  result_mojom->domain = base::UTF16ToUTF8(domain);

  result_mojom->fallback_favicon_text =
      base::UTF16ToUTF8(favicon::GetFallbackIconText(entry.url));

  result_mojom->time = entry.time.InMillisecondsFSinceUnixEpoch();

  // Pass the timestamps in a map.
  base::flat_map<std::string, std::vector<double>> all_timestamps;
  for (const auto& [url, timestamps] : entry.all_timestamps) {
    std::vector<double> timestamps_for_url;
    // Add all timestamps for this URL.
    for (const base::Time& timestamp : timestamps) {
      timestamps_for_url.push_back(timestamp.InMillisecondsFSinceUnixEpoch());
    }
    all_timestamps[url.spec()] = std::move(timestamps_for_url);
  }
  result_mojom->all_timestamps = std::move(all_timestamps);

  // Always pass the short date since it is needed both in the search and in
  // the monthly view.
  result_mojom->date_short =
      base::UTF16ToUTF8(base::TimeFormatShortDate(entry.time));

  std::u16string snippet_string;
  std::u16string date_relative_day;
  std::u16string date_time_of_day;
  bool is_blocked_visit = false;

  // Only pass in the strings we need (search results need a shortdate
  // and snippet, browse results need day and time information). Makes sure that
  // values of result are never undefined
  if (entry.is_search_result) {
    snippet_string = entry.snippet;
  } else {
    std::u16string date_str =
        ui::TimeFormat::RelativeDate(entry.time, clock->Now().LocalMidnight());
    if (date_str.empty()) {
      date_str = base::TimeFormatFriendlyDate(entry.time);
    } else {
      date_str = l10n_util::GetStringFUTF16(
          IDS_HISTORY_DATE_WITH_RELATIVE_TIME, date_str,
          base::TimeFormatFriendlyDate(entry.time));
    }
    date_relative_day = date_str;
    date_time_of_day = base::TimeFormatTimeOfDay(entry.time);
  }

  std::string device_name;
  std::string device_type;
  if (!entry.client_id.empty()) {
    GetDeviceNameAndType(tracker, entry.client_id, &device_name, &device_type);
  }

  result_mojom->device_name = device_name;
  result_mojom->device_type = device_type;

  supervised_user::FilteringBehavior filtering_behavior;
  if (profile.IsChild()) {
    filtering_behavior =
        supervised_user::SupervisedUserUrlFilteringServiceFactory::
            GetForProfile(&profile)
                ->GetFilteringBehavior(entry.url.GetWithEmptyPath())
                .behavior;
    is_blocked_visit = entry.blocked_visit;
    result_mojom->host_filtering_behavior =
        FilteringBehaviorToMojom(filtering_behavior);
  } else {
    result_mojom->host_filtering_behavior =
        history::mojom::FilteringBehavior::kUnknown;
  }

  result_mojom->date_time_of_day = base::UTF16ToUTF8(date_time_of_day);
  result_mojom->date_relative_day = base::UTF16ToUTF8(date_relative_day);
  result_mojom->snippet = base::UTF16ToUTF8(snippet_string);
  result_mojom->starred = bookmark_model->IsBookmarked(entry.url);
  result_mojom->blocked_visit = is_blocked_visit;
  result_mojom->is_url_in_remote_user_data = IsEntryInRemoteUserData(entry);
  result_mojom->remote_icon_url_for_uma = entry.remote_icon_url_for_uma.spec();
  result_mojom->is_actor_visit = entry.is_actor_visit;

  // Additional debugging fields shown only if the debug feature is enabled.
  if (history_clusters::GetConfig().user_visible_debug) {
    auto debug_mojom = history::mojom::DebugInfo::New();
    debug_mojom->is_url_in_local_database = IsUrlInLocalDatabase(entry);
    debug_mojom->visit_count = entry.visit_count;
    debug_mojom->typed_count = entry.typed_count;
    result_mojom->debug = std::move(debug_mojom);
  }

  // Initialize critical_actions array.
  result_mojom->critical_actions =
      std::vector<history::mojom::CriticalActionPtr>();

  return result_mojom;
}

history::mojom::CriticalActionPtr CriticalActionToMojom(
    const critical_actions::CriticalActionEntry& action) {
  auto action_mojom = history::mojom::CriticalAction::New();
  action_mojom->id = action.critical_action_id;
  action_mojom->linkout_url =
      critical_actions::GetCriticalActionLinkoutUrl(action);
  action_mojom->label = action.GetLabel();
  action_mojom->tooltip = action.GetTooltip();
  action_mojom->action_type =
      static_cast<history::mojom::CriticalActionType>(action.action_type);
  return action_mojom;
}

struct ActionGroupKey {
  std::string_view conversation_id;
  std::string_view actor_task_id;
  int64_t visit_id;
  critical_actions::ActionType action_type;
  auto operator<=>(const ActionGroupKey&) const = default;
};

std::vector<critical_actions::CriticalActionEntry> DeduplicateCriticalActions(
    const std::vector<critical_actions::CriticalActionEntry>& raw_actions,
    base::TimeDelta time_tolerance) {
  if (raw_actions.empty()) {
    return {};
  }

  base::flat_map<ActionGroupKey, base::Time> group_latest_times;
  std::vector<critical_actions::CriticalActionEntry> deduped_results;
  deduped_results.reserve(raw_actions.size());

  for (const auto& action : raw_actions) {
    ActionGroupKey key{.conversation_id = action.conversation_id,
                       .actor_task_id = action.actor_task_id,
                       .visit_id = action.visit_id,
                       .action_type = action.action_type};

    auto it = group_latest_times.find(key);

    // The database returns actions sorted by timestamp DESC (latest first).
    // A subsequent action falls into the tolerance window of the previous
    // action if its timestamp is greater than or equal to the
    // (last_accepted_timestamp - time_tolerance).
    if (it != group_latest_times.end() &&
        action.timestamp >= (it->second - time_tolerance)) {
      continue;  // It's a duplicate, skip it.
    }

    group_latest_times[key] = action.timestamp;
    deduped_results.push_back(action);
  }

  return deduped_results;
}

}  // namespace

BrowsingHistoryHandler::BrowsingHistoryHandler(
    mojo::PendingReceiver<history::mojom::PageHandler> pending_page_handler,
    Profile* profile,
    content::WebContents* web_contents)
    : profile_(profile),
      web_contents_(web_contents),
      page_handler_(this, std::move(pending_page_handler)),
      identity_manager_(
          CHECK_DEREF(IdentityManagerFactory::GetForProfile(profile))),
      clock_(base::DefaultClock::GetInstance()),
      browsing_history_service_(nullptr) {}

BrowsingHistoryHandler::~BrowsingHistoryHandler() = default;

void BrowsingHistoryHandler::SetSidePanelUIEmbedder(
    base::WeakPtr<TopChromeWebUIController::Embedder> side_panel_embedder) {
  side_panel_embedder_ = side_panel_embedder;
}

void BrowsingHistoryHandler::SetPage(
    mojo::PendingRemote<history::mojom::Page> pending_page) {
  page_.Bind(std::move(pending_page));
  // TODO(mfacey@): Explore whether deferred_callbacks_ can be removed.
  for (auto& deferred_callback : deferred_callbacks_) {
    std::move(deferred_callback).Run();
  }
  deferred_callbacks_.clear();

  HatsService* hats_service =
      HatsServiceFactory::GetForProfile(profile_,
                                        /* create_if_necessary = */ true);
  if (!hats_service) {
    return;
  }

  // Experiment group HaTS survey should trigger if the HaTS experiment group
  // feature is enabled and any of the history improvement features is enabled.
  // Check the HaTS feature last, so that clients are only counted as active if
  // one of the other feature is enabled.
  if ((history::IsBrowsingHistoryActorIntegrationM3Enabled() ||
       base::FeatureList::IsEnabled(
           history::kBrowsingHistorySimilarVisitsGrouping)) &&
      base::FeatureList::IsEnabled(
          features::kHappinessTrackingSurveysForDesktopHistoryPageExperiment)) {
    hats_service->LaunchDelayedSurveyForWebContents(
        kHatsSurveyTriggerHistoryPageExperiment, web_contents_,
        features::kHappinessTrackingSurveysForDesktopHistoryPageExperimentTime
            .Get()
            .InMilliseconds());
  }

  // Control group HaTS survey should trigger if the HaTS control group feature
  // is enabled and none of the history improvement features is enabled.
  // Check the HaTS feature last, so that clients are only counted as active if
  // all of the other features are disabled.
  if (!history::IsBrowsingHistoryActorIntegrationM3Enabled() &&
      !base::FeatureList::IsEnabled(
          history::kBrowsingHistorySimilarVisitsGrouping) &&
      base::FeatureList::IsEnabled(
          features::kHappinessTrackingSurveysForDesktopHistoryPageControl)) {
    hats_service->LaunchDelayedSurveyForWebContents(
        kHatsSurveyTriggerHistoryPageControl, web_contents_,
        features::kHappinessTrackingSurveysForDesktopHistoryPageControlTime
            .Get()
            .InMilliseconds());
  }
}

void BrowsingHistoryHandler::ShowSidePanelUI() {
  if (side_panel_embedder_) {
    side_panel_embedder_->ShowUI();
  }
}

void BrowsingHistoryHandler::StartQueryHistory() {
  HistoryService* local_history = HistoryServiceFactory::GetForProfile(
      profile_, ServiceAccessType::EXPLICIT_ACCESS);
  syncer::SyncService* sync_service =
      SyncServiceFactory::GetForProfile(profile_);
  browsing_history_service_ = std::make_unique<BrowsingHistoryService>(
      this, local_history, sync_service);

  // 150 = RESULTS_PER_PAGE from chrome/browser/resources/history/constants.js
  SendHistoryQuery(150, std::string(), std::nullopt, true, true);
}

void BrowsingHistoryHandler::QueryHistory(const std::string& query,
                                          int max_count,
                                          std::optional<double> begin_timestamp,
                                          bool include_user_visits,
                                          bool include_actor_visits,
                                          QueryHistoryCallback callback) {
  if (!browsing_history_service_) {
    // Page was refreshed, so need to call StartQueryHistory here
    StartQueryHistory();
  }

  // Reset the query history continuation callback. Since it is repopulated in
  // OnQueryComplete(), it cannot be reset earlier, as the early return above
  // prevents the QueryHistory() call to the browsing history service.
  query_history_continuation_.Reset();

  // Cancel the previous query if it is still in flight.
  if (query_history_callback_) {
    std::move(query_history_callback_).Run(history::mojom::QueryResult::New());
  }

  query_history_callback_ = std::move(callback);

  SendHistoryQuery(max_count, query, begin_timestamp, include_user_visits,
                   include_actor_visits);
}

void BrowsingHistoryHandler::SendHistoryQuery(
    int max_count,
    const std::string& query,
    std::optional<double> begin_timestamp,
    bool include_user_visits,
    bool include_actor_visits) {
  query_timer_ = base::ElapsedTimer();
  history::QueryOptions options;
  options.max_count = max_count;
  options.policy_for_404_visits = history::VisitQuery404sPolicy::kExclude404s;
  options.duplicate_policy = history::QueryOptions::REMOVE_DUPLICATES_PER_DAY;
  options.include_actor_visits = include_actor_visits;
  options.include_user_visits = include_user_visits;
  std::string query_without_prefix = query;

  const std::string kHostPrefix = "host:";
  if (query.rfind(kHostPrefix, 0) == 0) {
    options.host_only = true;
    query_without_prefix = query.substr(kHostPrefix.length());
  }

  if (begin_timestamp.has_value()) {
    options.begin_time =
        base::Time::FromMillisecondsSinceUnixEpoch(begin_timestamp.value());
  }

  browsing_history_service_->QueryHistory(
      base::UTF8ToUTF16(query_without_prefix), options);
}

void BrowsingHistoryHandler::QueryHistoryContinuation(
    QueryHistoryContinuationCallback callback) {
  // Cancel the previous query if it is still in flight.
  if (query_history_callback_) {
    std::move(query_history_callback_).Run(history::mojom::QueryResult::New());
  }
  query_history_callback_ = std::move(callback);

  if (!query_history_continuation_.is_null()) {
    std::move(query_history_continuation_).Run();
  }
}

void BrowsingHistoryHandler::RemoveVisits(
    const std::vector<history::mojom::RemovalItemPtr> items,
    RemoveVisitsCallback callback) {
  remove_visits_callbacks_.push(std::move(callback));

  std::vector<BrowsingHistoryService::HistoryEntry> items_to_remove;
  items_to_remove.reserve(items.size());
  for (const auto& item : items) {
    const std::string url = item->url;
    const std::vector<double> timestamps = item->timestamps;
    if (url.empty()) {
      NOTREACHED() << "Unable to extract arguments";
    }

    DCHECK_GT(timestamps.size(), 0U);
    BrowsingHistoryService::HistoryEntry entry;
    entry.url = GURL(url);

    for (const auto& timestamp : timestamps) {
      base::Time visit_time =
          base::Time::FromMillisecondsSinceUnixEpoch(timestamp);
      entry.all_timestamps[entry.url].insert(visit_time);
    }

    items_to_remove.push_back(entry);
  }

  browsing_history_service_->RemoveVisits(items_to_remove);
}

void BrowsingHistoryHandler::OpenClearBrowsingDataDialog() {
  // TODO(beng): This is an improper direct dependency on Browser. Route this
  // through some sort of delegate.
  BrowserWindowInterface* browser =
      GlobalBrowserCollection::GetInstance()->FindBrowserWithTab(web_contents_);
  chrome::ShowClearBrowsingDataDialog(browser);
}

void BrowsingHistoryHandler::TurnOnHistorySync() {
#if !BUILDFLAG(IS_CHROMEOS)
  BrowserWindowInterface* browser =
      GlobalBrowserCollection::GetInstance()->FindBrowserWithTab(web_contents_);
  if (browser) {
    signin_ui_util::SignInAndEnableHistorySync(
        browser, profile_, signin_metrics::AccessPoint::kRecentTabs);
  }
#else
  // This is not expected to be called on ChromeOS as the screen that uses this
  // function is never shown for ChromeOS (using <if expr="not is_chromeos">).
  NOTREACHED();
#endif
}

#if !BUILDFLAG(IS_CHROMEOS)
void BrowsingHistoryHandler::ShouldShowHistoryPageHistorySyncPromo(
    ShouldShowHistoryPageHistorySyncPromoCallback callback) {
  const int promo_shown_count = GetHistoryPageHistorySyncPromoShownCount();

  // If the promo has been shown more than the threshold, the promo should not
  // be shown.
  if (promo_shown_count >= kHistorySyncPromoShownThreshold) {
    std::move(callback).Run(false);
    return;
  }

  const bool shown_after_dismissal =
      IsHistoryPageHistorySyncPromoShownAfterDismissal();
  // If the promo was dismissed and shown once after dismissal, the promo should
  // not be shown anymore.
  if (shown_after_dismissal) {
    std::move(callback).Run(false);
    return;
  }
  const base::Time last_dismissed_timestamp =
      GetHistoryPageHistorySyncPromoLastDismissedTimestamp();
  const bool was_dismissed = !last_dismissed_timestamp.is_null();
  // If the promo was dismissed and the cooldown has not passed, the promo
  // should not be shown.
  if (was_dismissed &&
      clock_->Now() < last_dismissed_timestamp + kHistorySyncPromoCooldown) {
    std::move(callback).Run(false);
    return;
  }

  std::move(callback).Run(true);
}

void BrowsingHistoryHandler::RecordHistoryPageHistorySyncPromoDismissed() {
  SetHistoryPageHistorySyncPromoLastDismissedTimestamp(clock_->Now());
}

void BrowsingHistoryHandler::IncrementHistoryPageHistorySyncPromoShownCount() {
  IncrementHistoryPageHistorySyncPromoShownCountPref();

  const base::Time last_dismissed_timestamp =
      GetHistoryPageHistorySyncPromoLastDismissedTimestamp();
  const bool was_dismissed = !last_dismissed_timestamp.is_null();
  if (was_dismissed) {
    SetHistoryPageHistorySyncPromoShownAfterDismissal();
  }
}

int BrowsingHistoryHandler::GetHistoryPageHistorySyncPromoShownCount() const {
  const AccountInfo account = signin_ui_util::GetSingleAccountForPromos(
      &identity_manager_.get(),
      AccountPreviewDataServiceFactory::GetForProfile(profile_));
  if (account.GetGaiaId().empty()) {
    return profile_->GetPrefs()->GetInteger(
        prefs::kHistoryPageHistorySyncPromoShownCountPerProfile);
  }

  return SigninPrefs(*profile_->GetPrefs())
      .GetHistoryPageHistorySyncPromoShownCount(account.GetGaiaId());
}

base::Time
BrowsingHistoryHandler::GetHistoryPageHistorySyncPromoLastDismissedTimestamp()
    const {
  const AccountInfo account = signin_ui_util::GetSingleAccountForPromos(
      &identity_manager_.get(),
      AccountPreviewDataServiceFactory::GetForProfile(profile_));
  if (account.GetGaiaId().empty()) {
    return profile_->GetPrefs()->GetTime(
        prefs::kHistoryPageHistorySyncPromoLastDismissedTimestampPerProfile);
  }

  return SigninPrefs(*profile_->GetPrefs())
      .GetHistoryPageHistorySyncPromoLastDismissedTimestamp(account.GetGaiaId())
      .value_or(base::Time());
}

bool BrowsingHistoryHandler::IsHistoryPageHistorySyncPromoShownAfterDismissal()
    const {
  const AccountInfo account = signin_ui_util::GetSingleAccountForPromos(
      &identity_manager_.get(),
      AccountPreviewDataServiceFactory::GetForProfile(profile_));
  if (account.GetGaiaId().empty()) {
    return profile_->GetPrefs()->GetBoolean(
        prefs::kHistoryPageHistorySyncPromoShownAfterDismissalPerProfile);
  }

  return SigninPrefs(*profile_->GetPrefs())
      .GetHistoryPageHistorySyncPromoShownAfterDismissal(account.GetGaiaId());
}

void BrowsingHistoryHandler::
    SetHistoryPageHistorySyncPromoLastDismissedTimestamp(base::Time time) {
  const AccountInfo account = signin_ui_util::GetSingleAccountForPromos(
      &identity_manager_.get(),
      AccountPreviewDataServiceFactory::GetForProfile(profile_));
  if (account.GetGaiaId().empty()) {
    profile_->GetPrefs()->SetTime(
        prefs::kHistoryPageHistorySyncPromoLastDismissedTimestampPerProfile,
        time);
  } else {
    SigninPrefs(*profile_->GetPrefs())
        .SetHistoryPageHistorySyncPromoLastDismissedTimestamp(
            account.GetGaiaId(), time);
  }
}

void BrowsingHistoryHandler::
    IncrementHistoryPageHistorySyncPromoShownCountPref() {
  const AccountInfo account = signin_ui_util::GetSingleAccountForPromos(
      &identity_manager_.get(),
      AccountPreviewDataServiceFactory::GetForProfile(profile_));
  if (account.GetGaiaId().empty()) {
    const int promo_shown_count = profile_->GetPrefs()->GetInteger(
        prefs::kHistoryPageHistorySyncPromoShownCountPerProfile);
    profile_->GetPrefs()->SetInteger(
        prefs::kHistoryPageHistorySyncPromoShownCountPerProfile,
        promo_shown_count + 1);
  } else {
    SigninPrefs(*profile_->GetPrefs())
        .IncrementHistoryPageHistorySyncPromoShownCount(account.GetGaiaId());
  }
}

void BrowsingHistoryHandler::
    SetHistoryPageHistorySyncPromoShownAfterDismissal() {
  const AccountInfo account = signin_ui_util::GetSingleAccountForPromos(
      &identity_manager_.get(),
      AccountPreviewDataServiceFactory::GetForProfile(profile_));
  if (account.GetGaiaId().empty()) {
    profile_->GetPrefs()->SetBoolean(
        prefs::kHistoryPageHistorySyncPromoShownAfterDismissalPerProfile, true);
  } else {
    SigninPrefs(*profile_->GetPrefs())
        .SetHistoryPageHistorySyncPromoShownAfterDismissal(account.GetGaiaId());
  }
}
#endif

void BrowsingHistoryHandler::RemoveBookmark(const std::string& url) {
  BookmarkModel* model = BookmarkModelFactory::GetForBrowserContext(profile_);
  bookmarks::RemoveAllBookmarks(model, GURL(url), FROM_HERE);
}
//
void BrowsingHistoryHandler::SetLastSelectedTab(const int last_tab) {
  profile_->GetPrefs()->SetInteger(history_clusters::prefs::kLastSelectedTab,
                                   last_tab);
}

void BrowsingHistoryHandler::OnQueryComplete(
    const std::vector<BrowsingHistoryService::HistoryEntry>& results,
    const BrowsingHistoryService::QueryResultsInfo& query_results_info,
    base::OnceClosure continuation_closure) {
  query_history_continuation_ = std::move(continuation_closure);
  CHECK(profile_);

  if (base::FeatureList::IsEnabled(
          critical_actions::features::kCriticalActionHistory)) {
    std::vector<int64_t> actor_visit_ids;
    for (const auto& entry : results) {
      if (entry.is_actor_visit) {
        actor_visit_ids.insert(actor_visit_ids.end(),
                               entry.all_visit_ids.begin(),
                               entry.all_visit_ids.end());
      }
    }

    if (!actor_visit_ids.empty()) {
      critical_actions::CriticalActionService* critical_action_service =
          critical_actions::CriticalActionFactory::GetForProfile(profile_);
      if (critical_action_service) {
        critical_actions::CriticalActionQueryOptions options;
        options.visit_ids = std::move(actor_visit_ids);
        critical_action_service->GetCriticalActions(
            options,
            base::BindOnce(&BrowsingHistoryHandler::CriticalActionsFetched,
                           weak_factory_.GetWeakPtr(), results,
                           query_results_info, base::ElapsedTimer()));
        return;
      }
    }
  }

  HandleQueryResults(results, query_results_info, {});
}

void BrowsingHistoryHandler::CriticalActionsFetched(
    const std::vector<BrowsingHistoryService::HistoryEntry>& results,
    const BrowsingHistoryService::QueryResultsInfo& query_results_info,
    base::ElapsedTimer critical_actions_timer,
    std::vector<critical_actions::CriticalActionEntry> critical_actions) {
  base::UmaHistogramTimes("HistoryPage.CriticalActionsQueryTime",
                          critical_actions_timer.Elapsed());
  HandleQueryResults(results, query_results_info, std::move(critical_actions));
}

void BrowsingHistoryHandler::HandleQueryResults(
    const std::vector<BrowsingHistoryService::HistoryEntry>& results,
    const BrowsingHistoryService::QueryResultsInfo& query_results_info,
    std::vector<critical_actions::CriticalActionEntry> critical_actions) {
  if (query_timer_.has_value()) {
    const bool has_actor_visits =
        std::any_of(results.begin(), results.end(),
                    [](const auto& entry) { return entry.is_actor_visit; });
    if (has_actor_visits &&
        base::FeatureList::IsEnabled(
            critical_actions::features::kCriticalActionHistory)) {
      base::UmaHistogramTimes(
          "HistoryPage.QueryHistoryTotalTime.WithCriticalActions",
          query_timer_->Elapsed());
    } else {
      base::UmaHistogramTimes(
          "HistoryPage.QueryHistoryTotalTime.WithoutCriticalActions",
          query_timer_->Elapsed());
    }
    query_timer_.reset();
  }

  BookmarkModel* bookmark_model =
      BookmarkModelFactory::GetForBrowserContext(profile_);

  const syncer::DeviceInfoTracker* tracker =
      DeviceInfoSyncServiceFactory::GetForProfile(profile_)
          ->GetDeviceInfoTracker();

  DCHECK(tracker);

  absl::flat_hash_map<history::VisitID,
                      std::vector<history::mojom::CriticalActionPtr>>
      actions_by_visit_id;

  // Deduplicate actions belonging to the same task and visit.
  // 5 seconds is chosen as a safe heuristic upper bound to accommodate
  // potential latency delays between the Actor and Chrome side logs
  // of the same event, while being small enough to avoid merging separate
  // events.
  std::vector<critical_actions::CriticalActionEntry> processed_actions =
      DeduplicateCriticalActions(critical_actions, base::Seconds(5));

  for (const auto& action : processed_actions) {
    if (action.visit_id == history::kInvalidVisitID ||
        action.action_type == critical_actions::ActionType::kUnknown) {
      continue;
    }
    actions_by_visit_id[action.visit_id].push_back(
        CriticalActionToMojom(action));
  }

  std::vector<history::mojom::HistoryEntryPtr> results_mojom;
  for (const BrowsingHistoryService::HistoryEntry& entry : results) {
    history::mojom::HistoryEntryPtr entry_mojom =
        HistoryEntryToMojom(entry, bookmark_model, *profile_, tracker, clock_);

    if (entry.is_actor_visit &&
        base::FeatureList::IsEnabled(
            critical_actions::features::kCriticalActionHistory)) {
      for (history::VisitID visit_id : entry.all_visit_ids) {
        auto it = actions_by_visit_id.find(visit_id);
        if (it != actions_by_visit_id.end()) {
          for (auto& action : it->second) {
            entry_mojom->critical_actions.push_back(std::move(action));
          }
        }
      }
      base::UmaHistogramCounts100("HistoryPage.CriticalActionsPerVisitCount",
                                  entry_mojom->critical_actions.size());
    }

    results_mojom.push_back(std::move(entry_mojom));
  }

  auto results_info = history::mojom::HistoryQuery::New();
  // The items which are to be written into results_info_ are also
  // described in ui/webui/resources/cr_components/history/history.mojom.
  results_info->term = base::UTF16ToUTF8(query_results_info.search_text);
  results_info->finished = query_results_info.reached_beginning;

  auto final_results = history::mojom::QueryResult::New();
  final_results->info = std::move(results_info);
  final_results->value = std::move(results_mojom);

  if (query_history_callback_) {
    std::move(query_history_callback_).Run(std::move(final_results));
  }
}

void BrowsingHistoryHandler::OnRemoveVisitsComplete() {
  CHECK(!remove_visits_callbacks_.empty());
  std::move(remove_visits_callbacks_.front()).Run();
  remove_visits_callbacks_.pop();
}

void BrowsingHistoryHandler::OnRemoveVisitsFailed() {
  CHECK(!remove_visits_callbacks_.empty());
  std::move(remove_visits_callbacks_.front()).Run();
  remove_visits_callbacks_.pop();
}

void BrowsingHistoryHandler::HistoryDeleted() {
  if (page_) {
    page_->OnHistoryDeleted();
  } else {
    deferred_callbacks_.push_back(base::BindOnce(
        &BrowsingHistoryHandler::HistoryDeleted, weak_factory_.GetWeakPtr()));
  }
}

void BrowsingHistoryHandler::HasOtherFormsOfBrowsingHistory(
    bool has_other_forms,
    bool has_synced_results) {
  if (page_) {
    page_->OnHasOtherFormsChanged(has_other_forms);
  } else {
    deferred_callbacks_.push_back(base::BindOnce(
        &BrowsingHistoryHandler::HasOtherFormsOfBrowsingHistory,
        weak_factory_.GetWeakPtr(), has_other_forms, has_synced_results));
  }
}

Profile* BrowsingHistoryHandler::GetProfile() {
  return profile_;
}

void BrowsingHistoryHandler::RequestAccountInfo(
    RequestAccountInfoCallback callback) {
#if !BUILDFLAG(IS_CHROMEOS)
  AccountInfo account_info = signin_ui_util::GetSingleAccountForPromos(
      &identity_manager_.get(),
      AccountPreviewDataServiceFactory::GetForProfile(profile_));
  std::move(callback).Run(CreateAccountInfoDataMojo(account_info));

  if (!identity_manager_observation_.IsObserving()) {
    identity_manager_observation_.Observe(&identity_manager_.get());
  }
#else
  // This is not expected to be called on ChromeOS as the screen that uses this
  // function is never shown for ChromeOS (using <if expr="not is_chromeos">).
  NOTREACHED();
#endif
}

void BrowsingHistoryHandler::OnExtendedAccountInfoUpdated(
    const AccountInfo& info) {
#if !BUILDFLAG(IS_CHROMEOS)
  AccountInfo account_to_display = signin_ui_util::GetSingleAccountForPromos(
      &identity_manager_.get(),
      AccountPreviewDataServiceFactory::GetForProfile(profile_));

  if (info.IsEmpty() || !info.IsValid() ||
      info.GetAccountId() != account_to_display.GetAccountId()) {
    return;
  }
  page_->SendAccountInfo(CreateAccountInfoDataMojo(info));
#else
  // This is not expected to be called on ChromeOS as the screen that uses this
  // function is never shown for ChromeOS (using <if expr="not is_chromeos">).
  NOTREACHED();
#endif
}
