// Copyright 2021 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/ssl/https_first_mode_settings_tracker.h"

#include <string_view>

#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/json/values_util.h"
#include "base/metrics/histogram_functions.h"
#include "base/no_destructor.h"
#include "base/strings/string_number_conversions.h"
#include "base/task/thread_pool.h"
#include "base/time/default_clock.h"
#include "base/values.h"
#include "build/build_config.h"
#include "chrome/browser/content_settings/host_content_settings_map_factory.h"
#include "chrome/browser/metrics/chrome_metrics_service_accessor.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/safe_browsing/advanced_protection_status_manager.h"
#include "chrome/browser/safe_browsing/advanced_protection_status_manager_factory.h"
#include "chrome/browser/ssl/chrome_security_blocking_page_factory.h"
#include "chrome/browser/ssl/https_upgrades_interceptor.h"
#include "chrome/browser/ssl/https_upgrades_util.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/pref_names.h"
#include "components/prefs/pref_service.h"
#include "components/safe_browsing/core/common/features.h"
#include "components/safe_browsing/core/common/safe_browsing_prefs.h"
#include "components/security_interstitials/content/https_only_mode_blocking_page.h"
#include "components/security_interstitials/content/stateful_ssl_host_state_delegate.h"
#include "components/site_engagement/content/site_engagement_service.h"
#include "components/variations/synthetic_trials.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/storage_partition.h"
#include "net/base/url_util.h"
#include "third_party/blink/public/mojom/site_engagement/site_engagement.mojom.h"

#if !BUILDFLAG(IS_ANDROID)
#include "chrome/browser/safe_browsing/security_settings_bundle_toast_helper.h"
#endif

#if BUILDFLAG(IS_CHROMEOS)
#include "chrome/browser/ash/profiles/profile_helper.h"
#endif  // BUILDFLAG(IS_CHROMEOS)

// Minimum score of an HTTPS origin to enable HFM on its hostname.
const base::FeatureParam<int> kHttpsAddThreshold{
    &features::kHttpsFirstModeV2ForEngagedSites, "https-add-threshold", 80};

// Maximum score of an HTTP origin to enable HFM on its hostname.
const base::FeatureParam<int> kHttpsRemoveThreshold{
    &features::kHttpsFirstModeV2ForEngagedSites, "https-remove-threshold", 75};

// If HTTPS score goes below kHttpsRemoveThreshold or HTTP score goes above
// kHttpRemoveThreshold, disable HFM on this hostname.
const base::FeatureParam<int> kHttpAddThreshold{
    &features::kHttpsFirstModeV2ForEngagedSites, "http-add-threshold", 1};
const base::FeatureParam<int> kHttpRemoveThreshold{
    &features::kHttpsFirstModeV2ForEngagedSites, "http-remove-threshold", 5};

// Parameters for Typically Secure User heuristic:

// The rolling window size during which we check for HTTPS-Upgrades fallback
// entries. If the number of fallback entries is smaller than
// kMaxRecentFallbackEntryCount, we may automatically enable HTTPS-First Mode.
const base::TimeDelta kFallbackEntriesRollingWindowSize = base::Days(7);

// Maximum number of past HTTPS-Upgrade fallback events (i.e. would-be warnings)
// to auto-enable HFM, including the current fallback event that's being added
// to the events list.
const size_t kMaxRecentFallbackEntryCount = 2;

// Minimum age of the current browser profile to automatically enable HFM. This
// prevents auto-enabling HFM immediately upon first launch.
const base::TimeDelta kMinTypicallySecureProfileAge = base::Days(15);

// We should observe HTTPS-Upgrade and HFM navigations at least for this long
// before enabling HFM.
const base::TimeDelta kMinTypicallySecureObservationTime = base::Days(7);

// Minimum total score for a user to be considered typically secure. If the user
// doesn't have at least this much engagement score over all sites, they might
// not have used Chrome sufficiently for us to auto-enable HFM.
const base::FeatureParam<int> kMinTotalEngagementPointsForTypicallySecureUser{
    &features::kHttpsFirstModeV2ForTypicallySecureUsers,
    "min-total-site-engagement-score", 50};

// Rolling window size in days to count recent navigations. Navigations within
// this window will be counted to be used for the Typically Secure heuristic.
// Navigations older than this many days will be discarded from the count.
const base::FeatureParam<int> kNavigationCounterRollingWindowSizeInDays{
    &features::kHttpsFirstModeV2ForTypicallySecureUsers,
    "navigation-counts-rolling-window-size-in-days", 15};

// Minimum number of main frame navigations counted in this profile during a
// rolling window of kNavigationCounterDefaultRollingWindowSizeInDays for a user
// to be considered typically secure. If the user doesn't have at least this
// many navigations counted, they might not have used Chrome sufficiently for us
// to auto-enable HFM. A default value of 1500 is 100 navigations per day during
// the 15 day rolling window.
const base::FeatureParam<int> kMinRecentNavigationsForTypicallySecureUser{
    &features::kHttpsFirstModeV2ForTypicallySecureUsers,
    "min-recent-navigations", 1500};

// The key for the fallback events in the base preference.
constexpr char kFallbackEventsKey[] = "fallback_events";

// The key for the start timestamp in the base preference. This is the time
// when we started observing the profile with the Typically Secure User
// heuristic.
constexpr char kHeuristicStartTimestampKey[] = "heuristic_start_timestamp";

// The key in each fallback event for the fallback event timestamp. A fallback
// event is evicted from the list if this timestamp is older than
// kFallbackEntriesRollingWindowSize.
constexpr char kFallbackEventsPrefTimestampKey[] = "timestamp";

constexpr int kNavigationCounterDefaultSaveInterval = 10;

namespace {

using security_interstitials::https_only_mode::SiteEngagementHeuristicState;

const char kHttpsFirstModeServiceName[] = "HttpsFirstModeService";
const char kHttpsFirstModeSyntheticFieldTrialName[] =
    "HttpsFirstModeClientSetting";
const char kHttpsFirstModeSyntheticFieldTrialEnabledGroup[] = "Enabled";
const char kHttpsFirstModeSyntheticFieldTrialBalancedGroup[] = "Balanced";
const char kHttpsFirstModeSyntheticFieldTrialDisabledGroup[] = "Disabled";

// We don't need to protect this with a lock since it's only set while
// single-threaded in tests.
base::Clock* g_clock = nullptr;

base::Clock* GetClock() {
  return g_clock ? g_clock : base::DefaultClock::GetInstance();
}

// Returns the HTTP URL from `http_url` using the test port numbers, if any.
// TODO(crbug.com/40904694): Refactor and merge with UpgradeUrlToHttps().
GURL GetHttpUrlFromHttps(const GURL& https_url) {
  DCHECK(https_url.SchemeIsCryptographic());

  // Replace scheme with HTTP.
  GURL::Replacements upgrade_url;
  upgrade_url.SetSchemeStr(url::kHttpScheme);

  // For tests that use the EmbeddedTestServer, the server's port needs to be
  // specified as it can't use the default ports.
  int http_port_for_testing = HttpsUpgradesInterceptor::GetHttpPortForTesting();
  // `port_str` must be in scope for the call to ReplaceComponents() below.
  const std::string port_str = base::NumberToString(http_port_for_testing);
  if (http_port_for_testing) {
    // Only reached in testing, where the original URL will always have a
    // non-default port. One of the tests navigates to Google support pages, so
    // exclude that.
    // TODO(crbug.com/40904694): Remove this exception.
    if (https_url != GURL(security_interstitials::HttpsOnlyModeBlockingPage::
                              kLearnMoreLink)) {
      DCHECK(!https_url.GetPort().empty());
      upgrade_url.SetPortStr(port_str);
    }
  }

  return https_url.ReplaceComponents(upgrade_url);
}

std::unique_ptr<KeyedService> BuildService(content::BrowserContext* context) {
  Profile* profile = Profile::FromBrowserContext(context);
#if BUILDFLAG(IS_CHROMEOS)
  // Explicitly check for ChromeOS sign-in profiles (which would cause
  // double-counting of at-startup metrics for ChromeOS restarts) which are not
  // covered by the `IsRegularProfile()` check.
  if (ash::ProfileHelper::IsSigninProfile(profile)) {
    return nullptr;
  }
#endif  // BUILDFLAG(IS_CHROMEOS)
  return std::make_unique<HttpsFirstModeService>(profile, GetClock());
}

base::Time GetTimestamp(const base::DictValue& dict, const char* key) {
  const auto* timestamp_string = dict.Find(key);
  if (timestamp_string) {
    const auto timestamp = base::ValueToTime(timestamp_string);
    if (timestamp) {
      return *timestamp;
    }
  }
  return base::Time();
}

std::string GetSyntheticFieldTrialGroupName(HttpsFirstModeSetting setting) {
  switch (setting) {
    case HttpsFirstModeSetting::kEnabledFull:
      return kHttpsFirstModeSyntheticFieldTrialEnabledGroup;
    case HttpsFirstModeSetting::kEnabledBalanced:
      return kHttpsFirstModeSyntheticFieldTrialBalancedGroup;
    case HttpsFirstModeSetting::kDisabled:
      return kHttpsFirstModeSyntheticFieldTrialDisabledGroup;
    default:
      NOTREACHED();
  }
}

HttpsFirstModeStartupState GetStartupDetailedState(Profile* profile) {
  PrefService* prefs = profile->GetPrefs();

  if (base::FeatureList::IsEnabled(
          features::kHttpsFirstModeForAdvancedProtectionUsers)) {
    auto* ap_manager =
        safe_browsing::AdvancedProtectionStatusManagerFactory::GetForProfile(
            profile);
    if (ap_manager && ap_manager->IsUnderAdvancedProtection()) {
      return HttpsFirstModeStartupState::kEnabledFull;
    }
  }

  if (prefs->GetBoolean(prefs::kHttpsOnlyModeEnabled)) {
    return HttpsFirstModeStartupState::kEnabledFull;
  }

  if (IsBalancedModeEnabled(prefs)) {
    bool user_has_modified_settings =
        prefs->HasPrefPath(prefs::kHttpsOnlyModeEnabled) ||
        prefs->HasPrefPath(prefs::kHttpsFirstBalancedMode);
    if (!user_has_modified_settings) {
      if (base::FeatureList::IsEnabled(
              features::kHttpsFirstModeDefaultSettingPairsWithEsb) &&
          safe_browsing::IsEnhancedProtectionEnabled(*prefs)) {
        return HttpsFirstModeStartupState::kEnabledBalancedEsbPairing;
      }
      if (base::FeatureList::IsEnabled(
              features::kHttpsFirstBalancedModeAutoEnable)) {
        return HttpsFirstModeStartupState::kEnabledBalancedAutoEnable;
      }
    } else {
      if (prefs->GetBoolean(prefs::kHttpsOnlyModeAutoEnabled)) {
        return HttpsFirstModeStartupState::kEnabledBalancedTypicallySecure;
      }
      return HttpsFirstModeStartupState::kEnabledBalancedExplicit;
    }
  }

  return HttpsFirstModeStartupState::kDisabled;
}

}  // namespace

HttpsFirstModeService::HttpsFirstModeService(Profile* profile,
                                             base::Clock* clock)
    : profile_(profile), clock_(clock) {
  pref_change_registrar_.Init(profile_->GetPrefs());
  // Using base::Unretained() here is safe as the PrefChangeRegistrar is owned
  // by `this`.
  pref_change_registrar_.Add(
      prefs::kHttpsOnlyModeEnabled,
      base::BindRepeating(&HttpsFirstModeService::OnHttpsFirstModePrefChanged,
                          base::Unretained(this)));
  pref_change_registrar_.Add(
      prefs::kHttpsFirstBalancedMode,
      base::BindRepeating(&HttpsFirstModeService::OnHttpsFirstModePrefChanged,
                          base::Unretained(this)));
  pref_change_registrar_.Add(
      prefs::kSafeBrowsingEnhanced,
      base::BindRepeating(
          &HttpsFirstModeService::OnSafeBrowsingEnhancedPrefChanged,
          base::Unretained(this)));

  // Observe the settings bundle to trigger migration/toast dynamically
  // if the bundle changes during the session.
  pref_change_registrar_.Add(
      prefs::kSecuritySettingsBundle,
      base::BindRepeating(
          &HttpsFirstModeService::OnSecuritySettingsBundleChanged,
          base::Unretained(this)));

  // Make sure the pref state is logged and the synthetic field trial state is
  // created at startup (as the pref may never change over the session).
  HttpsFirstModeSetting setting = GetCurrentSetting();
  base::UmaHistogramEnumeration(
      "Security.HttpsFirstMode.SettingEnabledAtStartup2", setting);
  base::UmaHistogramEnumeration(
      "Security.HttpsFirstMode.SettingEnabledAtStartupDetailed",
      GetStartupDetailedState(profile_));
  ChromeMetricsServiceAccessor::RegisterSyntheticFieldTrial(
      kHttpsFirstModeSyntheticFieldTrialName,
      GetSyntheticFieldTrialGroupName(setting));

  // Restore navigation counts from the pref to be used in the Typically Secure
  // heuristic.
  navigation_counts_dict_ =
      profile_->GetPrefs()->GetDict(prefs::kHttpsUpgradeNavigations).Clone();
  navigation_counter_ = std::make_unique<DailyNavigationCounter>(
      &navigation_counts_dict_, clock_,
      kNavigationCounterRollingWindowSizeInDays.Get(),
      kNavigationCounterDefaultSaveInterval);

  content::GetUIThreadTaskRunner({base::TaskPriority::BEST_EFFORT})
      ->PostTask(FROM_HERE, base::BindOnce(&HttpsFirstModeService::AfterStartup,
                                           weak_factory_.GetWeakPtr()));
}

void HttpsFirstModeService::AfterStartup() {
  MigrateEnhancedBundleUsersAndMaybeShowToast();
  CheckUserIsTypicallySecureAndMaybeEnableHttpsFirstBalancedMode();
  MaybeEnableHttpsFirstModeForEngagedSites(base::OnceClosure());
}

void HttpsFirstModeService::MigrateEnhancedBundleUsersAndMaybeShowToast() {
  PrefService* prefs = profile_->GetPrefs();

  // If the Toast has already been shown or HFM features are not enabled, abort.
  if (prefs->GetBoolean(prefs::kHttpsFirstModeBundleToastQueued) ||
      !IsBalancedModeAvailable() ||
      !base::FeatureList::IsEnabled(
          safe_browsing::kBundledSecuritySettingsAskBeforeHttp)) {
    return;
  }

  // Check if this is an Enhanced Protection bundle user.
  auto bundle_setting = safe_browsing::GetSecurityBundleSetting(*prefs);
  if (bundle_setting !=
      safe_browsing::SecuritySettingsBundleSetting::ENHANCED) {
    return;
  }

  // Advanced Protection Program users are opted into the Enhanced bundle by
  // default but shouldn't have their secure connections settings modified or
  // show the toast since HFM is managed by AP.
  auto* advanced_protection_manager =
      safe_browsing::AdvancedProtectionStatusManagerFactory::GetForProfile(
          profile_);
  if (advanced_protection_manager &&
      advanced_protection_manager->IsUnderAdvancedProtection()) {
    return;
  }

  // If the user has explicitly modified secure connections settings in the
  // past, do not override their choice. Simply mark the Toast as shown and
  // abort.
  if (prefs->HasPrefPath(prefs::kHttpsOnlyModeEnabled) ||
      prefs->HasPrefPath(prefs::kHttpsFirstBalancedMode)) {
    prefs->SetBoolean(prefs::kHttpsFirstModeBundleToastQueued, true);
    return;
  }

  // Upgrade them to HFM Balanced Mode.
  keep_http_allowlist_on_next_pref_change_ = true;
  prefs->SetBoolean(prefs::kHttpsFirstBalancedMode, true);

  // Note: kHttpsFirstModeBundleToastQueued acts as the one-time UI migration
  // queue. It is marked true immediately on upgrade to prevent duplicate
  // migration evaluation on subsequent browser runs. The actual on-screen toast
  // is managed on startup by verifying
  // kSecuritySettingsBundleMigrationToastState is kPending.
  prefs->SetBoolean(prefs::kHttpsFirstModeBundleToastQueued, true);
  if (prefs->GetInteger(prefs::kSecuritySettingsBundleMigrationToastState) !=
      static_cast<int>(
          safe_browsing::SecuritySettingsBundleToastState::kShown)) {
    prefs->SetInteger(
        prefs::kSecuritySettingsBundleMigrationToastState,
        static_cast<int>(
            safe_browsing::SecuritySettingsBundleToastState::kPending));
  }

#if !BUILDFLAG(IS_ANDROID)
  // Dynamically trigger the toast on the active window immediately for the
  // current session.
  safe_browsing::SecuritySettingsBundleToastHelper::GetForProfile(profile_)
      ->TriggerIfNeeded();
#endif
}

void HttpsFirstModeService::
    CheckUserIsTypicallySecureAndMaybeEnableHttpsFirstBalancedMode() {
  if (MustDisableTypicallySecureUserHeuristic(profile_)) {
    return;
  }

  // If HFM or the auto-enable prefs were previously set, do not modify them.
  if (profile_->GetPrefs()->HasPrefPath(prefs::kHttpsOnlyModeEnabled) ||
      profile_->GetPrefs()->HasPrefPath(prefs::kHttpsFirstBalancedMode) ||
      profile_->GetPrefs()->HasPrefPath(prefs::kHttpsOnlyModeAutoEnabled)) {
    return;
  }
  if (!IsUserTypicallySecure()) {
    return;
  }
  // The prefs must be set in this order, as setting kHttpsFirstBalancedMode
  // will cause kHttpsFirstBalancedModeEnabledByTypicallySecureHeuristic to be
  // reset to false.
  // TODO(crbug.com/349860796): Consider having the typically-secure heuristic
  // turn on Balanced Mode instead.
  keep_http_allowlist_on_next_pref_change_ = true;
  profile_->GetPrefs()->SetBoolean(prefs::kHttpsFirstBalancedMode, true);
  profile_->GetPrefs()->SetBoolean(prefs::kHttpsOnlyModeAutoEnabled, true);
}

HttpsFirstModeService::~HttpsFirstModeService() = default;

void HttpsFirstModeService::OnHttpsFirstModePrefChanged() {
  HttpsFirstModeSetting setting = GetCurrentSetting();
  // Update synthetic field trial group registration.
  ChromeMetricsServiceAccessor::RegisterSyntheticFieldTrial(
      kHttpsFirstModeSyntheticFieldTrialName,
      GetSyntheticFieldTrialGroupName(setting));

  // Reset the HTTP allowlist and HTTPS enforcelist when the pref changes.
  // A user going from HTTPS-Upgrades to HTTPS-First Mode shouldn't inherit the
  // set of allowlisted sites (or vice versa).
  if (!keep_http_allowlist_on_next_pref_change_) {
    StatefulSSLHostStateDelegate* state =
        static_cast<StatefulSSLHostStateDelegate*>(
            profile_->GetSSLHostStateDelegate());
    if (state) {
      state->ClearHttpsOnlyModeAllowlist();
      state->ClearHttpsEnforcelist();
    }
  }
  keep_http_allowlist_on_next_pref_change_ = false;

  // Since the user modified the UI pref, explicitly disable any automatic
  // HTTPS-First Mode heuristic.
  profile_->GetPrefs()->SetBoolean(prefs::kHttpsOnlyModeAutoEnabled, false);
}

void HttpsFirstModeService::OnSafeBrowsingEnhancedPrefChanged() {
  HttpsFirstModeSetting setting = GetCurrentSetting();
  // Update synthetic field trial group registration.
  ChromeMetricsServiceAccessor::RegisterSyntheticFieldTrial(
      kHttpsFirstModeSyntheticFieldTrialName,
      GetSyntheticFieldTrialGroupName(setting));

  // Log implicit HFM state changes due to ESB pairing.
  if (base::FeatureList::IsEnabled(
          features::kHttpsFirstModeDefaultSettingPairsWithEsb)) {
    PrefService* prefs = profile_->GetPrefs();
    bool user_has_modified_settings =
        prefs->HasPrefPath(prefs::kHttpsOnlyModeEnabled) ||
        prefs->HasPrefPath(prefs::kHttpsFirstBalancedMode);
    if (!user_has_modified_settings) {
      bool esb_enabled = safe_browsing::IsEnhancedProtectionEnabled(*prefs);
      base::UmaHistogramEnumeration(
          "Security.HttpsFirstMode.SettingImplicitlyChanged",
          esb_enabled
              ? HttpsFirstModeImplicitStateChange::kBalancedEnabledByEsb
              : HttpsFirstModeImplicitStateChange::kBalancedDisabledByEsb);
    }
  }

  // Reset the HTTP allowlist and HTTPS enforcelist when the pref changes.
  if (!keep_http_allowlist_on_next_pref_change_) {
    StatefulSSLHostStateDelegate* state =
        static_cast<StatefulSSLHostStateDelegate*>(
            profile_->GetSSLHostStateDelegate());
    if (state) {
      state->ClearHttpsOnlyModeAllowlist();
      state->ClearHttpsEnforcelist();
    }
  }
  keep_http_allowlist_on_next_pref_change_ = false;
}

void HttpsFirstModeService::OnSecuritySettingsBundleChanged() {
  // Trigger bundle migration dynamically if the bundle transitioned
  // into Enhanced during the session (e.g., via sync).
  MigrateEnhancedBundleUsersAndMaybeShowToast();
}

bool HttpsFirstModeService::
    IsInterstitialEnabledByTypicallySecureUserHeuristic() const {
  return !MustDisableTypicallySecureUserHeuristic(profile_) &&
         profile_->GetPrefs()->GetBoolean(prefs::kHttpsOnlyModeAutoEnabled) &&
         profile_->GetPrefs()->GetBoolean(prefs::kHttpsFirstBalancedMode);
}

void HttpsFirstModeService::RecordHttpsUpgradeFallbackEvent() {
  UpdateFallbackEntries(/*add_new_entry=*/true);
}

bool HttpsFirstModeService::IsUserTypicallySecure() {
  return UpdateFallbackEntries(/*add_new_entry=*/false);
}

bool HttpsFirstModeService::UpdateFallbackEntries(bool add_new_entry) {
  if (!base::FeatureList::IsEnabled(
          features::kHttpsFirstModeV2ForTypicallySecureUsers) ||
      !IsBalancedModeAvailable()) {
    // Normally we'd use MustDisableTypicallySecureUserHeuristic() here, but
    // we want to record fallback entries even on enterprise devices. Otherwise,
    // if an enterprise managed device becomes unmanaged, the heuristic would
    // have zero fallback entries recorded. It would then try to enable the
    // interstitial because the user would appear typically secure.
    return false;
  }
  // Profile shouldn't be too new.
  if ((clock_->Now() - profile_->GetCreationTime()) <
      kMinTypicallySecureProfileAge) {
    return false;
  }
  base::Time now = clock_->Now();
  const base::DictValue& base_pref =
      profile_->GetPrefs()->GetDict(prefs::kHttpsUpgradeFallbacks);

  base::ListValue new_entries;
  const base::ListValue* fallback_events =
      base_pref.FindList(kFallbackEventsKey);
  base::Time latest_fallback_timestamp;
  if (fallback_events) {
    for (const auto& event : *fallback_events) {
      const base::DictValue* fallback_event = event.GetIfDict();
      if (!fallback_event) {
        continue;
      }
      auto* event_timestamp_string =
          fallback_event->Find(kFallbackEventsPrefTimestampKey);
      if (!event_timestamp_string) {
        continue;
      }
      auto event_timestamp = base::ValueToTime(event_timestamp_string);
      if (!event_timestamp.has_value()) {
        // Invalid entry, ignore.
        continue;
      }
      if (event_timestamp.value() > now) {
        // Invalid timestamp, ignore.
        continue;
      }
      if (event_timestamp.value() < now - kFallbackEntriesRollingWindowSize) {
        // Old event, ignore.
        continue;
      }
      new_entries.Append(fallback_event->Clone());
      if (event_timestamp.value() > latest_fallback_timestamp) {
        latest_fallback_timestamp = event_timestamp.value();
      }
    }
  }

  // Add the new fallback entry.
  if (add_new_entry) {
    base::DictValue new_event;
    new_event.Set(kFallbackEventsPrefTimestampKey, base::TimeToValue(now));
    new_entries.Append(std::move(new_event));
  }

  size_t recent_warning_count = new_entries.size();

  base::Time heuristic_start_timestamp =
      GetTimestamp(base_pref, kHeuristicStartTimestampKey);
  if (heuristic_start_timestamp.is_null()) {
    // This can happen in a new profile or if a previous version of Chrome
    // wrote the pref but didn't have this value.
    heuristic_start_timestamp = now;
  }

  auto* engagement_svc = site_engagement::SiteEngagementService::Get(profile_);
  bool enable_https_first_mode =
      ((now - heuristic_start_timestamp) >
       kMinTypicallySecureObservationTime) &&
      (recent_warning_count <= kMaxRecentFallbackEntryCount) &&
      (engagement_svc->GetTotalEngagementPoints() >=
       kMinTotalEngagementPointsForTypicallySecureUser.Get()) &&
      (now - latest_fallback_timestamp > base::Days(1)) &&
      (static_cast<int>(GetRecentNavigationCount()) >=
       kMinRecentNavigationsForTypicallySecureUser.Get());

  // Update the pref with the new fallback events.
  base::DictValue new_base_pref;
  new_base_pref.Set(kFallbackEventsKey, std::move(new_entries));
  new_base_pref.Set(kHeuristicStartTimestampKey,
                    base::TimeToValue(heuristic_start_timestamp));
  profile_->GetPrefs()->SetDict(prefs::kHttpsUpgradeFallbacks,
                                std::move(new_base_pref));
  return enable_https_first_mode;
}

void HttpsFirstModeService::MaybeEnableHttpsFirstModeForEngagedSites(
    base::OnceClosure done_callback) {
  // If HFM or the auto-enable prefs were previously set, do not modify HFM
  // status.
  if (MustDisableSiteEngagementHeuristic(profile_) ||
      profile_->GetPrefs()->HasPrefPath(prefs::kHttpsOnlyModeEnabled) ||
      profile_->GetPrefs()->HasPrefPath(prefs::kHttpsFirstBalancedMode) ||
      profile_->GetPrefs()->HasPrefPath(prefs::kHttpsOnlyModeAutoEnabled)) {
    if (!done_callback.is_null()) {
      std::move(done_callback).Run();
    }
    return;
  }
  // Ideal parameter order is kHttpsAddThreshold > kHttpsRemoveThreshold >
  // kHttpRemoveThreshold > kHttpAddThreshold.
  if (!(kHttpsAddThreshold.Get() > kHttpsRemoveThreshold.Get() &&
        kHttpsRemoveThreshold.Get() > kHttpRemoveThreshold.Get() &&
        kHttpRemoveThreshold.Get() > kHttpAddThreshold.Get())) {
    if (!done_callback.is_null()) {
      std::move(done_callback).Run();
    }
    return;
  }
  // Consider amending the SiteEngagementService API to take a callback so we
  // can exactly retrieve all the origins with score >= kHttpsAddThreshold.
  DCHECK_GE(kHttpsAddThreshold.Get(),
            site_engagement::SiteEngagementScore::GetHighEngagementBoundary());
  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE,
      {base::TaskPriority::USER_BLOCKING,
       base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN},
      base::BindOnce(
          &site_engagement::SiteEngagementService::GetAllDetailsInBackground,
          clock_->Now(),
          base::WrapRefCounted(
              HostContentSettingsMapFactory::GetForProfile(profile_)),
          site_engagement::SiteEngagementService::URLSets::HTTP,
          blink::mojom::EngagementLevel::HIGH),
      base::BindOnce(&HttpsFirstModeService::ProcessEngagedSitesList,
                     weak_factory_.GetWeakPtr(), std::move(done_callback)));
}

void HttpsFirstModeService::ProcessEngagedSitesList(
    base::OnceClosure done_callback,
    const std::vector<site_engagement::mojom::SiteEngagementDetails>& details) {
  DCHECK(IsBalancedModeAvailable());

  StatefulSSLHostStateDelegate* state =
      static_cast<StatefulSSLHostStateDelegate*>(
          profile_->GetSSLHostStateDelegate());
  // StatefulSSLHostStateDelegate can be null during tests. In that case, we
  // can't save the site setting.
  if (!state) {
    return;
  }
  auto* engagement_service =
      site_engagement::SiteEngagementService::Get(profile_);

  // If a non-unique hostname is in the enforcement list, it must have been
  // added by a previous version of Chrome, so remove it. Otherwise, ignore
  // non-unique hostnames.
  //
  // Complete Enforcement logic:
  // - Enforce on non-enforced (unique) hosts whose https score >=
  //   kHttpsAddThreshold and http score <= kHttpAddThreshold and have empty /
  //   default ports. We do this via tracking https origins in `details` which
  //   tracks highly engaged sites.
  // - Stop enforcing on enforced hosts whose https score <=
  //   kHttpsRemoveThreshold OR http score >= kHttpRemoveThreshold OR have non
  //   unique host names. We do this via the existing enforced sites in
  //   `GetHttpsEnforcedHosts`.

  content::StoragePartition* partition = profile_->GetDefaultStoragePartition();
  std::set<GURL> enabled_origins = state->GetHttpsEnforcedHosts(partition);

  // Enable highly engaged https origins.
  for (const site_engagement::mojom::SiteEngagementDetails& detail : details) {
    const GURL& origin = detail.origin;
    DCHECK(origin.SchemeIsHTTPOrHTTPS());
    DCHECK_GE(
        detail.total_score,
        site_engagement::SiteEngagementScore::GetHighEngagementBoundary());
    if (origin.SchemeIsCryptographic() && origin.port().empty() &&
        detail.total_score >= kHttpsAddThreshold.Get() &&
        engagement_service->GetScore(GetHttpUrlFromHttps(origin)) <=
            kHttpAddThreshold.Get() &&
        !enabled_origins.contains(origin) &&
        !net::IsHostnameNonUnique(origin.host())) {
      state->SetHttpsEnforcementForHost(origin.GetHost(), /*enforced=*/true,
                                        partition);
    }
  }

  // Disable low engaged origins that are already enabled.
  for (const GURL& origin : enabled_origins) {
    DCHECK(state->IsHttpsEnforcedForUrl(origin, partition));
    DCHECK(origin.SchemeIsCryptographic());
    DCHECK(origin.SchemeIsHTTPOrHTTPS());
    DCHECK(origin.port().empty());
    DCHECK(state->IsHttpsEnforcedForUrl(origin, partition));
    if (engagement_service->GetScore(origin) <= kHttpsRemoveThreshold.Get() ||
        engagement_service->GetScore(GetHttpUrlFromHttps(origin)) >=
            kHttpRemoveThreshold.Get() ||
        net::IsHostnameNonUnique(origin.host())) {
      state->SetHttpsEnforcementForHost(origin.GetHost(), /*enforced=*/false,
                                        partition);
    }
  }

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

HttpsFirstModeSetting HttpsFirstModeService::GetCurrentSetting() const {
  if (base::FeatureList::IsEnabled(
          features::kHttpsFirstModeForAdvancedProtectionUsers)) {
    auto* advanced_protection_manager =
        safe_browsing::AdvancedProtectionStatusManagerFactory::GetForProfile(
            profile_);
    if (advanced_protection_manager &&
        advanced_protection_manager->IsUnderAdvancedProtection()) {
      return HttpsFirstModeSetting::kEnabledFull;
    }
  }

  if (profile_->GetPrefs()->GetBoolean(prefs::kHttpsOnlyModeEnabled)) {
    return HttpsFirstModeSetting::kEnabledFull;
  }
  if (IsBalancedModeEnabled(profile_->GetPrefs())) {
    return HttpsFirstModeSetting::kEnabledBalanced;
  }
  return HttpsFirstModeSetting::kDisabled;
}

bool HttpsFirstModeService::UpdatePrefs(
    const HttpsFirstModeSetting& selection) {
  if (selection != HttpsFirstModeSetting::kDisabled &&
      selection != HttpsFirstModeSetting::kEnabledBalanced &&
      selection != HttpsFirstModeSetting::kEnabledFull) {
    return false;
  }

  if (!IsBalancedModeAvailable() &&
      selection == HttpsFirstModeSetting::kEnabledBalanced) {
    return false;
  }

  // Update both HTTPS-First Mode preferences to match the selection.
  //
  // Note that the HttpsFirstModeSetting::kEnabledBalanced is not available by
  // default. If the feature flag is disabled, then the kEnabledFull and
  // kDisabled settings will only be mapped to the kHttpsOnlyModeEnabled pref.
  //
  // Note: The Security.HttpsFirstMode.SettingChanged2 histogram is logged
  // here instead of in HttpsFirstModeService::OnHttpsFirstModePrefChanged()
  // because this will fire the pref observer _twice_, so logging the histogram
  // in the pref observer would cause double counting.
  if (IsBalancedModeAvailable()) {
    switch (selection) {
      case HttpsFirstModeSetting::kDisabled:
        base::UmaHistogramEnumeration("Security.HttpsFirstMode.SettingChanged2",
                                      HttpsFirstModeSetting::kDisabled);
        profile_->GetPrefs()->SetBoolean(prefs::kHttpsOnlyModeEnabled, false);
        profile_->GetPrefs()->SetBoolean(prefs::kHttpsFirstBalancedMode, false);
        break;
      case HttpsFirstModeSetting::kEnabledBalanced:
        base::UmaHistogramEnumeration("Security.HttpsFirstMode.SettingChanged2",
                                      HttpsFirstModeSetting::kEnabledBalanced);
        profile_->GetPrefs()->SetBoolean(prefs::kHttpsOnlyModeEnabled, false);
        profile_->GetPrefs()->SetBoolean(prefs::kHttpsFirstBalancedMode, true);
        break;
      case HttpsFirstModeSetting::kEnabledFull:
        base::UmaHistogramEnumeration("Security.HttpsFirstMode.SettingChanged2",
                                      HttpsFirstModeSetting::kEnabledFull);
        profile_->GetPrefs()->SetBoolean(prefs::kHttpsOnlyModeEnabled, true);
        profile_->GetPrefs()->SetBoolean(prefs::kHttpsFirstBalancedMode, false);
        break;
    }
  } else {
    // TODO(crbug.com/349860796): Remove old settings path once Balanced Mode
    // is launched.
    base::UmaHistogramEnumeration("Security.HttpsFirstMode.SettingChanged2",
                                  selection);
    profile_->GetPrefs()->SetBoolean(
        prefs::kHttpsOnlyModeEnabled,
        selection == HttpsFirstModeSetting::kEnabledFull);
  }
  return true;
}

void HttpsFirstModeService::IncrementRecentNavigationCount() {
  if (navigation_counter_->Increment()) {
    profile_->GetPrefs()->SetDict(prefs::kHttpsUpgradeNavigations,
                                  navigation_counts_dict_.Clone());
  }
}

size_t HttpsFirstModeService::GetRecentNavigationCount() const {
  return navigation_counter_->GetTotal();
}

void HttpsFirstModeService::SetClockForTesting(base::Clock* clock) {
  clock_ = clock;
}

size_t HttpsFirstModeService::GetFallbackEntryCountForTesting() const {
  const base::DictValue& base_pref =
      profile_->GetPrefs()->GetDict(prefs::kHttpsUpgradeFallbacks);
  const base::ListValue* fallback_events =
      base_pref.FindList(kFallbackEventsKey);
  return fallback_events ? fallback_events->size() : 0;
}

// static
HttpsFirstModeService* HttpsFirstModeServiceFactory::GetForProfile(
    Profile* profile) {
  return static_cast<HttpsFirstModeService*>(
      GetInstance()->GetServiceForBrowserContext(profile, /*create=*/true));
}

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

// static
BrowserContextKeyedServiceFactory::TestingFactory
HttpsFirstModeServiceFactory::GetDefaultFactoryForTesting() {
  return base::BindRepeating(&BuildService);
}

HttpsFirstModeServiceFactory::HttpsFirstModeServiceFactory()
    : ProfileKeyedServiceFactory(
          kHttpsFirstModeServiceName,
          // Don't create a service for non-regular profiles. This includes
          // Incognito (which uses the settings of the main profile) and Guest
          // Mode.
          ProfileSelections::Builder()
              .WithRegular(ProfileSelection::kOriginalOnly)
              // TODO(crbug.com/41488885): Check if this service is needed for
              // Ash Internals.
              .WithAshInternals(ProfileSelection::kOriginalOnly)
              .WithGuest(ProfileSelection::kOffTheRecordOnly)
              .Build()) {
  DependsOn(
      safe_browsing::AdvancedProtectionStatusManagerFactory::GetInstance());
}

HttpsFirstModeServiceFactory::~HttpsFirstModeServiceFactory() = default;

std::unique_ptr<KeyedService>
HttpsFirstModeServiceFactory::BuildServiceInstanceForBrowserContext(
    content::BrowserContext* context) const {
  return BuildService(context);
}

// static
base::Clock* HttpsFirstModeServiceFactory::SetClockForTesting(
    base::Clock* clock) {
  return std::exchange(g_clock, clock);
}
