// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "components/omnibox/browser/omnibox_field_trial.h"

#include <algorithm>
#include <cmath>
#include <functional>
#include <string>

#include "base/command_line.h"
#include "base/feature_list.h"
#include "base/metrics/field_trial.h"
#include "base/metrics/field_trial_params.h"
#include "base/no_destructor.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/system/sys_info.h"
#include "base/time/time.h"
#include "base/trace_event/memory_usage_estimator.h"
#include "build/build_config.h"
#include "components/history/core/browser/url_database.h"
#include "components/omnibox/browser/aim_eligibility_service.h"
#include "components/omnibox/browser/autocomplete_provider.h"
#include "components/omnibox/browser/autocomplete_provider_client.h"
#include "components/omnibox/browser/page_classification_functions.h"
#include "components/omnibox/browser/url_index_private_data.h"
#include "components/omnibox/common/omnibox_feature_configs.h"
#include "components/omnibox/common/omnibox_features.h"
#include "components/search/search.h"
#include "components/search_engines/ai_mode_button_service.h"
#include "components/variations/active_field_trials.h"
#include "components/variations/hashing.h"
#include "components/variations/variations_associated_data.h"
#include "third_party/metrics_proto/omnibox_event.pb.h"
#include "ui/base/device_form_factor.h"
#include "ui/base/ui_base_features.h"

using metrics::OmniboxEventProto;

namespace {

typedef std::map<std::string, std::string> VariationParams;
typedef HUPScoringParams::ScoreBuckets ScoreBuckets;

void InitializeBucketsFromString(const std::string& bucket_string,
                                 ScoreBuckets* score_buckets) {
  // Clear the buckets.
  score_buckets->buckets().clear();
  base::StringPairs kv_pairs;
  if (base::SplitStringIntoKeyValuePairs(bucket_string, ':', ',', &kv_pairs)) {
    for (base::StringPairs::const_iterator it = kv_pairs.begin();
         it != kv_pairs.end(); ++it) {
      ScoreBuckets::CountMaxRelevance bucket;
      base::StringToDouble(it->first, &bucket.first);
      base::StringToInt(it->second, &bucket.second);
      score_buckets->buckets().push_back(bucket);
    }
    std::sort(score_buckets->buckets().begin(), score_buckets->buckets().end(),
              std::greater<ScoreBuckets::CountMaxRelevance>());
  }
}

void InitializeScoreBuckets(const VariationParams& params,
                            const char* relevance_cap_param,
                            const char* half_life_param,
                            const char* score_buckets_param,
                            const char* use_decay_factor_param,
                            ScoreBuckets* score_buckets) {
  auto it = params.find(relevance_cap_param);
  if (it != params.end()) {
    int relevance_cap;
    if (base::StringToInt(it->second, &relevance_cap)) {
      score_buckets->set_relevance_cap(relevance_cap);
    }
  }

  it = params.find(use_decay_factor_param);
  if (it != params.end()) {
    int use_decay_factor;
    if (base::StringToInt(it->second, &use_decay_factor)) {
      score_buckets->set_use_decay_factor(use_decay_factor != 0);
    }
  }

  it = params.find(half_life_param);
  if (it != params.end()) {
    int half_life_days;
    if (base::StringToInt(it->second, &half_life_days)) {
      score_buckets->set_half_life_days(half_life_days);
    }
  }

  it = params.find(score_buckets_param);
  if (it != params.end()) {
    // The value of the score bucket is a comma-separated list of
    // {DecayedCount/DecayedFactor + ":" + MaxRelevance}.
    InitializeBucketsFromString(it->second, score_buckets);
  }
}

OmniboxFieldTrial::MLConfig& GetMLConfigInternal() {
  static base::NoDestructor<OmniboxFieldTrial::MLConfig> s_config;
  return *s_config;
}

bool IsKoreanLocale(const std::string& locale) {
  return locale == "ko" || locale == "ko-KR";
}

#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS)
bool IsEnglishLocale(const std::string& locale) {
  return base::StartsWith(locale, "en", base::CompareCase::SENSITIVE);
}
#endif  // !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS)

}  // namespace

HUPScoringParams::ScoreBuckets::ScoreBuckets()
    : relevance_cap_(-1), half_life_days_(-1), use_decay_factor_(false) {}

HUPScoringParams::ScoreBuckets::ScoreBuckets(const ScoreBuckets& other) =
    default;

HUPScoringParams::ScoreBuckets::~ScoreBuckets() = default;

size_t HUPScoringParams::ScoreBuckets::EstimateMemoryUsage() const {
  return base::trace_event::EstimateMemoryUsage(buckets_);
}

double HUPScoringParams::ScoreBuckets::HalfLifeTimeDecay(
    const base::TimeDelta& elapsed_time) const {
  double time_ms;
  if ((half_life_days_ <= 0) ||
      ((time_ms = elapsed_time.InMillisecondsF()) <= 0)) {
    return 1.0;
  }

  const double half_life_intervals =
      time_ms / base::Days(half_life_days_).InMillisecondsF();
  return pow(2.0, -half_life_intervals);
}

size_t HUPScoringParams::EstimateMemoryUsage() const {
  size_t res = 0;

  res += base::trace_event::EstimateMemoryUsage(typed_count_buckets);
  res += base::trace_event::EstimateMemoryUsage(visited_count_buckets);

  return res;
}

int OmniboxFieldTrial::GetDisabledProviderTypes() {
  const std::string& types_string = base::GetFieldTrialParamValue(
      kBundledExperimentFieldTrialName, kDisableProvidersRule);
  int types = 0;
  if (types_string.empty() || !base::StringToInt(types_string, &types)) {
    return 0;
  }
  return types;
}

void OmniboxFieldTrial::GetActiveSuggestFieldTrialHashes(
    std::vector<uint32_t>* field_trial_hashes) {
  field_trial_hashes->clear();
  if (base::FieldTrialList::TrialExists(kBundledExperimentFieldTrialName)) {
    field_trial_hashes->push_back(
        variations::HashName(kBundledExperimentFieldTrialName));
  }
}

size_t OmniboxFieldTrial::GetProviderMaxMatches(
    AutocompleteProvider::Type provider) {
  size_t default_max_matches_per_provider = 3;

  std::string param_value;
  if (OmniboxFieldTrial::IsMlUrlScoringEnabled()) {
    param_value =
        OmniboxFieldTrial::GetMLConfig().ml_url_scoring_max_matches_by_provider;
  } else {
    param_value = base::GetFieldTrialParamValueByFeature(
        omnibox::kUIExperimentMaxAutocompleteMatches,
        OmniboxFieldTrial::kUIMaxAutocompleteMatchesByProviderParam);
  }

  // If the experiment param specifies a max results for |provider|, return the
  // specified limit.
  // E.g., if param_value = '3:2' and provider = 3, return 2.
  // Otherwise, if the experiment param specifies a default value for
  // unspecified providers, return the default value.
  // E.g., if param_value = '3:3,*:4' and provider = 1, return 4,
  // Otherwise, return |default_max_matches_per_provider|.
  base::StringPairs kv_pairs;
  if (base::SplitStringIntoKeyValuePairs(param_value, ':', ',', &kv_pairs)) {
    for (const auto& kv_pair : kv_pairs) {
      int k;
      base::StringToInt(kv_pair.first, &k);
      size_t v;
      base::StringToSizeT(kv_pair.second, &v);

      if (kv_pair.first == "*") {
        default_max_matches_per_provider = v;
      } else if (k == provider) {
        return v;
      }
    }
  }

  return default_max_matches_per_provider;
}

void OmniboxFieldTrial::GetDefaultHUPScoringParams(
    HUPScoringParams* scoring_params) {
  ScoreBuckets* type_score_buckets = &scoring_params->typed_count_buckets;
  type_score_buckets->set_half_life_days(30);
  type_score_buckets->set_use_decay_factor(false);
  // Default typed count buckets based on decayed typed count. The
  // values here are based on the results of field trials to determine what
  // maximized overall result quality.
  const std::string& typed_count_score_buckets_str =
      "1.0:1413,0.97:1390,0.93:1360,0.85:1340,0.72:1320,0.50:1250,0.0:1203";
  InitializeBucketsFromString(typed_count_score_buckets_str,
                              type_score_buckets);

  ScoreBuckets* visit_score_buckets = &scoring_params->visited_count_buckets;
  visit_score_buckets->set_half_life_days(30);
  visit_score_buckets->set_use_decay_factor(false);
  // Buckets based on visit count.  Like the typed count buckets above, the
  // values here were chosen based on field trials.  Note that when a URL hasn't
  // been visited in the last 30 days, we clamp its score to 100, which
  // basically demotes it below any other results in the dropdown.
  const std::string& visit_count_score_buckets_str = "4.0:790,0.5:590,0.0:100";
  InitializeBucketsFromString(visit_count_score_buckets_str,
                              visit_score_buckets);
}

void OmniboxFieldTrial::GetExperimentalHUPScoringParams(
    HUPScoringParams* scoring_params) {
  VariationParams params;
  if (!base::GetFieldTrialParams(kBundledExperimentFieldTrialName, &params)) {
    return;
  }

  InitializeScoreBuckets(params, kHUPNewScoringTypedCountRelevanceCapParam,
                         kHUPNewScoringTypedCountHalfLifeTimeParam,
                         kHUPNewScoringTypedCountScoreBucketsParam,
                         kHUPNewScoringTypedCountUseDecayFactorParam,
                         &scoring_params->typed_count_buckets);
  InitializeScoreBuckets(params, kHUPNewScoringVisitedCountRelevanceCapParam,
                         kHUPNewScoringVisitedCountHalfLifeTimeParam,
                         kHUPNewScoringVisitedCountScoreBucketsParam,
                         kHUPNewScoringVisitedCountUseDecayFactorParam,
                         &scoring_params->visited_count_buckets);
}

float OmniboxFieldTrial::HQPBookmarkValue() {
  std::string bookmark_value_str = base::GetFieldTrialParamValue(
      kBundledExperimentFieldTrialName, kHQPBookmarkValueRule);
  if (bookmark_value_str.empty()) {
    return 10;
  }
  // This is a best-effort conversion; we trust the hand-crafted parameters
  // downloaded from the server to be perfect.  There's no need for handle
  // errors smartly.
  double bookmark_value;
  base::StringToDouble(bookmark_value_str, &bookmark_value);
  return bookmark_value;
}

bool OmniboxFieldTrial::HQPAllowMatchInTLDValue() {
  return base::GetFieldTrialParamValue(kBundledExperimentFieldTrialName,
                                       kHQPAllowMatchInTLDRule) == "true";
}

bool OmniboxFieldTrial::HQPAllowMatchInSchemeValue() {
  return base::GetFieldTrialParamValue(kBundledExperimentFieldTrialName,
                                       kHQPAllowMatchInSchemeRule) == "true";
}

void OmniboxFieldTrial::GetSuggestPollingStrategy(bool* from_last_keystroke,
                                                  int* polling_delay_ms) {
  *from_last_keystroke =
      base::GetFieldTrialParamValue(
          kBundledExperimentFieldTrialName,
          kMeasureSuggestPollingDelayFromLastKeystrokeRule) == "true";

  const std::string& polling_delay_string = base::GetFieldTrialParamValue(
      kBundledExperimentFieldTrialName, kSuggestPollingDelayMsRule);
  if (polling_delay_string.empty() ||
      !base::StringToInt(polling_delay_string, polling_delay_ms) ||
      (*polling_delay_ms <= 0)) {
    *polling_delay_ms = kDefaultMinimumTimeBetweenSuggestQueriesMs;
  }
}

std::string OmniboxFieldTrial::HQPExperimentalScoringBuckets() {
  return base::GetFieldTrialParamValue(kBundledExperimentFieldTrialName,
                                       kHQPExperimentalScoringBucketsParam);
}

float OmniboxFieldTrial::HQPExperimentalTopicalityThreshold() {
  std::string topicality_threshold_str = base::GetFieldTrialParamValue(
      kBundledExperimentFieldTrialName,
      kHQPExperimentalScoringTopicalityThresholdParam);

  double topicality_threshold;
  if (topicality_threshold_str.empty() ||
      !base::StringToDouble(topicality_threshold_str, &topicality_threshold)) {
    return 0.5f;
  }

  return static_cast<float>(topicality_threshold);
}

int OmniboxFieldTrial::MaxNumHQPUrlsIndexedAtStartup() {
#if BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_IOS)
  // Limits on Android and iOS are chosen based on experiment results. See
  // crbug.com/715852#c18 and crbug.com/1141539#c31.
  constexpr int kDefaultOnLowEndDevices = 100;
  constexpr int kDefaultOnNonLowEndDevices = 400;
#else
  // Use 20,000 entries as a safety cap for users with spammed history,
  // such as users who were stuck in a redirect loop with autogenerated URLs.
  // This limit will only affect 0.01% of Windows users. crbug.com/750845.
  constexpr int kDefaultOnLowEndDevices = 20000;
  constexpr int kDefaultOnNonLowEndDevices = 20000;
#endif

  const bool is_low_end =
      base::SysInfo::IsLowEndDeviceOrPartialLowEndModeEnabled();
  const int default_value =
      is_low_end ? kDefaultOnLowEndDevices : kDefaultOnNonLowEndDevices;

  // Allow the startup cap to be overridden via the bundled omnibox field trial,
  // honoring the kMaxNumHQPUrlsIndexedAtStartupOn{Low,NonLow}EndDevicesParam
  // parameters (previously declared but never read). Falls back to the
  // constants above when no valid, positive value is configured, so default
  // behavior is unchanged. Mirrors the HQPMaxVisitsToScore() pattern below.
  const char* param_name =
      is_low_end ? kMaxNumHQPUrlsIndexedAtStartupOnLowEndDevicesParam
                 : kMaxNumHQPUrlsIndexedAtStartupOnNonLowEndDevicesParam;
  const std::string param_value = base::GetFieldTrialParamValue(
      kBundledExperimentFieldTrialName, param_name);
  int parsed_value = 0;
  if (!param_value.empty() && base::StringToInt(param_value, &parsed_value) &&
      parsed_value > 0) {
    return parsed_value;
  }
  return default_value;
}

size_t OmniboxFieldTrial::HQPMaxVisitsToScore() {
  std::string max_visits_str = base::GetFieldTrialParamValue(
      kBundledExperimentFieldTrialName, kHQPMaxVisitsToScoreRule);
  constexpr size_t kDefaultMaxVisitsToScore = 10;
  static_assert(
      URLIndexPrivateData::kMaxVisitsToStoreInCache >= kDefaultMaxVisitsToScore,
      "HQP should store at least as many visits as it expects to score");
  if (max_visits_str.empty()) {
    return kDefaultMaxVisitsToScore;
  }
  // This is a best-effort conversion; we trust the hand-crafted parameters
  // downloaded from the server to be perfect.  There's no need for handle
  // errors smartly.
  size_t max_visits_value;
  base::StringToSizeT(max_visits_str, &max_visits_value);
  return max_visits_value;
}

float OmniboxFieldTrial::HQPTypedValue() {
  std::string typed_value_str = base::GetFieldTrialParamValue(
      kBundledExperimentFieldTrialName, kHQPTypedValueRule);
  if (typed_value_str.empty()) {
    return 1.5;
  }
  // This is a best-effort conversion; we trust the hand-crafted parameters
  // downloaded from the server to be perfect.  There's no need for handle
  // errors smartly.
  double typed_value;
  base::StringToDouble(typed_value_str, &typed_value);
  return typed_value;
}

OmniboxFieldTrial::NumMatchesScores OmniboxFieldTrial::HQPNumMatchesScores() {
  std::string str = base::GetFieldTrialParamValue(
      kBundledExperimentFieldTrialName, kHQPNumMatchesScoresRule);
  static constexpr char kDefaultNumMatchesScores[] = "1:3,2:2.5,3:2,4:1.5";
  if (str.empty()) {
    str = kDefaultNumMatchesScores;
  }
  // The parameter is a comma-separated list of (number, value) pairs such as
  // listed above.
  // This is a best-effort conversion; we trust the hand-crafted parameters
  // downloaded from the server to be perfect.  There's no need to handle
  // errors smartly.
  base::StringPairs kv_pairs;
  if (!base::SplitStringIntoKeyValuePairs(str, ':', ',', &kv_pairs)) {
    return NumMatchesScores{};
  }
  NumMatchesScores num_matches_scores(kv_pairs.size());
  for (size_t i = 0; i < kv_pairs.size(); ++i) {
    base::StringToSizeT(kv_pairs[i].first, &num_matches_scores[i].first);
    // The input must be sorted by number of matches.
    DCHECK((i == 0) ||
           (num_matches_scores[i].first > num_matches_scores[i - 1].first));
    base::StringToDouble(kv_pairs[i].second, &num_matches_scores[i].second);
  }
  return num_matches_scores;
}

size_t OmniboxFieldTrial::HQPNumTitleWordsToAllow() {
  // The value of the rule is a string that encodes an integer (actually
  // size_t) containing the number of words.
  size_t num_title_words;
  if (!base::StringToSizeT(
          base::GetFieldTrialParamValue(kBundledExperimentFieldTrialName,
                                        kHQPNumTitleWordsRule),
          &num_title_words)) {
    return 20;
  }
  return num_title_words;
}

bool OmniboxFieldTrial::HQPAlsoDoHUPLikeScoring() {
  return base::GetFieldTrialParamValue(kBundledExperimentFieldTrialName,
                                       kHQPAlsoDoHUPLikeScoringRule) == "true";
}

bool OmniboxFieldTrial::HUPSearchDatabase() {
  const std::string& value = base::GetFieldTrialParamValue(
      kBundledExperimentFieldTrialName, kHUPSearchDatabaseRule);
  return value.empty() || (value == "true");
}

bool OmniboxFieldTrial::IsOnDeviceHeadSuggestEnabledForIncognito() {
  return base::FeatureList::IsEnabled(omnibox::kOnDeviceHeadProviderIncognito);
}

bool OmniboxFieldTrial::IsOnDeviceHeadSuggestEnabledForNonIncognito() {
  return base::FeatureList::IsEnabled(
      omnibox::kOnDeviceHeadProviderNonIncognito);
}

bool OmniboxFieldTrial::IsOnDeviceHeadSuggestEnabledForAnyMode() {
  return IsOnDeviceHeadSuggestEnabledForIncognito() ||
         IsOnDeviceHeadSuggestEnabledForNonIncognito();
}

bool OmniboxFieldTrial::IsOnDeviceTailSuggestEnabled(
    const std::string& locale) {
  // Tail model will only be enabled when head provider is also enabled.
  if (!IsOnDeviceHeadSuggestEnabledForAnyMode()) {
    return false;
  }

// On Desktop the model is only launched for English locales.
#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS)
  if (IsEnglishLocale(locale)) {
    return base::FeatureList::IsEnabled(
        omnibox::kOnDeviceTailEnableEnglishModel);
  }
#endif  // !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS)

  return base::FeatureList::IsEnabled(omnibox::kOnDeviceTailModel);
}

bool OmniboxFieldTrial::ShouldEncodeLeadingSpaceForOnDeviceTailSuggest() {
  return base::GetFieldTrialParamByFeatureAsBool(omnibox::kOnDeviceTailModel,
                                                 "ShouldEncodeLeadingSpace",
                                                 /*default_value=*/false);
}

bool OmniboxFieldTrial::IsOnDeviceHeadSuggestEnabledForLocale(
    const std::string& locale) {
  if (IsKoreanLocale(locale)) {
    return false;
  }
  return IsOnDeviceHeadSuggestEnabledForAnyMode();
}

std::string OmniboxFieldTrial::OnDeviceHeadModelLocaleConstraint(
    bool is_incognito) {
  const base::Feature* feature =
      is_incognito ? &omnibox::kOnDeviceHeadProviderIncognito
                   : &omnibox::kOnDeviceHeadProviderNonIncognito;
  std::string constraint = base::GetFieldTrialParamValueByFeature(
      *feature, kOnDeviceHeadModelLocaleConstraint);
  if (constraint.empty()) {
    constraint = "500000";
  }
  return constraint;
}

const char OmniboxFieldTrial::kBundledExperimentFieldTrialName[] =
    "OmniboxBundledExperimentV1";
const char OmniboxFieldTrial::kDisableProvidersRule[] = "DisableProviders";
const char OmniboxFieldTrial::kHQPBookmarkValueRule[] = "HQPBookmarkValue";
const char OmniboxFieldTrial::kHQPTypedValueRule[] = "HQPTypedValue";
const char OmniboxFieldTrial::kHQPAllowMatchInTLDRule[] = "HQPAllowMatchInTLD";
const char OmniboxFieldTrial::kHQPAllowMatchInSchemeRule[] =
    "HQPAllowMatchInScheme";
const char
    OmniboxFieldTrial::kMeasureSuggestPollingDelayFromLastKeystrokeRule[] =
        "MeasureSuggestPollingDelayFromLastKeystroke";
const char OmniboxFieldTrial::kSuggestPollingDelayMsRule[] =
    "SuggestPollingDelayMs";
const char OmniboxFieldTrial::kHQPMaxVisitsToScoreRule[] =
    "HQPMaxVisitsToScoreRule";
const char OmniboxFieldTrial::kHQPNumMatchesScoresRule[] =
    "HQPNumMatchesScores";
const char OmniboxFieldTrial::kHQPNumTitleWordsRule[] = "HQPNumTitleWords";
const char OmniboxFieldTrial::kHQPAlsoDoHUPLikeScoringRule[] =
    "HQPAlsoDoHUPLikeScoring";
const char OmniboxFieldTrial::kHUPSearchDatabaseRule[] = "HUPSearchDatabase";
const char OmniboxFieldTrial::kHUPNewScoringTypedCountRelevanceCapParam[] =
    "TypedCountRelevanceCap";
const char OmniboxFieldTrial::kHUPNewScoringTypedCountHalfLifeTimeParam[] =
    "TypedCountHalfLifeTime";
const char OmniboxFieldTrial::kHUPNewScoringTypedCountScoreBucketsParam[] =
    "TypedCountScoreBuckets";
const char OmniboxFieldTrial::kHUPNewScoringTypedCountUseDecayFactorParam[] =
    "TypedCountUseDecayFactor";
const char OmniboxFieldTrial::kHUPNewScoringVisitedCountRelevanceCapParam[] =
    "VisitedCountRelevanceCap";
const char OmniboxFieldTrial::kHUPNewScoringVisitedCountHalfLifeTimeParam[] =
    "VisitedCountHalfLifeTime";
const char OmniboxFieldTrial::kHUPNewScoringVisitedCountScoreBucketsParam[] =
    "VisitedCountScoreBuckets";
const char OmniboxFieldTrial::kHUPNewScoringVisitedCountUseDecayFactorParam[] =
    "VisitedCountUseDecayFactor";

const char OmniboxFieldTrial::kHQPExperimentalScoringBucketsParam[] =
    "HQPExperimentalScoringBuckets";
const char
    OmniboxFieldTrial::kHQPExperimentalScoringTopicalityThresholdParam[] =
        "HQPExperimentalScoringTopicalityThreshold";

const char
    OmniboxFieldTrial::kMaxNumHQPUrlsIndexedAtStartupOnLowEndDevicesParam[] =
        "MaxNumHQPUrlsIndexedAtStartupOnLowEndDevices";
const char
    OmniboxFieldTrial::kMaxNumHQPUrlsIndexedAtStartupOnNonLowEndDevicesParam[] =
        "MaxNumHQPUrlsIndexedAtStartupOnNonLowEndDevices";

const char OmniboxFieldTrial::kUIMaxAutocompleteMatchesByProviderParam[] =
    "UIMaxAutocompleteMatchesByProvider";
const char OmniboxFieldTrial::kUIMaxAutocompleteMatchesParam[] =
    "UIMaxAutocompleteMatches";
const char OmniboxFieldTrial::kDynamicMaxAutocompleteUrlCutoffParam[] =
    "OmniboxDynamicMaxAutocompleteUrlCutoff";
const char OmniboxFieldTrial::kDynamicMaxAutocompleteIncreasedLimitParam[] =
    "OmniboxDynamicMaxAutocompleteIncreasedLimit";
const char OmniboxFieldTrial::kSuppressPsuggestBackfillWithMIAParam[] =
    "SuppressPsuggestBackfillWithMIA";

const char OmniboxFieldTrial::kOnDeviceHeadModelLocaleConstraint[] =
    "ForceModelLocaleConstraint";

int OmniboxFieldTrial::kDefaultMinimumTimeBetweenSuggestQueriesMs = 100;

namespace OmniboxFieldTrial {

// Local history zero-prefix (aka zero-suggest) and prefix suggestions:

// Whether to ignore all ZPS prefetch responses received from the Suggest
// service when the user is on a Google SRP. This can be used, for example,
// during experimentation to measure the performance impact of only the
// request/response portion of ZPS prefetching (i.e. without updating the
// user-visible list of suggestions in the Omnibox).
const base::FeatureParam<bool> kZeroSuggestPrefetchingOnSRPCounterfactual(
    &omnibox::kZeroSuggestPrefetchingOnSRP,
    "ZeroSuggestPrefetchingOnSRPCounterfactual",
    false);

// The debouncing delay (in milliseconds) to use when throttling ZPS prefetch
// requests.
const base::FeatureParam<int> kZeroSuggestPrefetchDebounceDelay(
    &omnibox::kZeroSuggestPrefetchDebouncing,
    "ZeroSuggestPrefetchDebounceDelay",
    300);

// Whether to calculate debouncing delay relative to the latest successful run
// (instead of the latest run request).
const base::FeatureParam<bool> kZeroSuggestPrefetchDebounceFromLastRun(
    &omnibox::kZeroSuggestPrefetchDebouncing,
    "ZeroSuggestPrefetchDebounceFromLastRun",
    true);

bool IsZeroSuggestPrefetchingEnabledInContext(
    metrics::OmniboxEventProto::PageClassification page_classification) {
  switch (page_classification) {
    case metrics::OmniboxEventProto::NTP_ZPS_PREFETCH:
    case metrics::OmniboxEventProto::NTP_ACTION_CHIPS:
      return true;
    case metrics::OmniboxEventProto::SRP_ZPS_PREFETCH:
      return base::FeatureList::IsEnabled(
          omnibox::kZeroSuggestPrefetchingOnSRP);
    case metrics::OmniboxEventProto::OTHER_ZPS_PREFETCH:
      return base::FeatureList::IsEnabled(
          omnibox::kZeroSuggestPrefetchingOnWeb);
    default:
      return false;
  }
}

bool IsOnFocusZeroSuggestEnabledInContext(
    metrics::OmniboxEventProto::PageClassification page_classification) {
  static bool enabled =
      base::FeatureList::IsEnabled(omnibox::kFocusTriggersWebAndSRPZeroSuggest);

  switch (page_classification) {
    case metrics::OmniboxEventProto::
        SEARCH_RESULT_PAGE_NO_SEARCH_TERM_REPLACEMENT:
    case metrics::OmniboxEventProto::OTHER:
      return enabled;
    default:
      return false;
  }
}

bool IsHideSuggestionGroupHeadersEnabledInContext(
    metrics::OmniboxEventProto::PageClassification page_classification) {
  static bool enabled =
      base::FeatureList::IsEnabled(omnibox::kHideSuggestionGroupHeaders);

  switch (page_classification) {
    case metrics::OmniboxEventProto::
        SEARCH_RESULT_PAGE_NO_SEARCH_TERM_REPLACEMENT:
    case metrics::OmniboxEventProto::OTHER:
      return enabled;
    default:
      return false;
  }
}

bool IsAimOmniboxEntrypointEnabled(
    const AimEligibilityService* aim_eligibility_service,
    const AiModeButtonService* ai_mode_button_service,
    const TemplateURLService* template_url_service) {
  // `aim_eligibility_service` can be null in tests.
  if (!aim_eligibility_service) {
    return false;
  }

  // Entrypoint can't be shown if it can't be configured.
  if (!ai_mode_button_service || !ai_mode_button_service->GetCurrentConfig()) {
    return false;
  }

  // If the DSE is Google, the entrypoint should respect Google server
  // eligibility regardless of the 3p feature state.
  if (search::DefaultSearchProviderIsGoogle(template_url_service)) {
    return aim_eligibility_service->IsAimEligible();
  }

  // If DSE is not Google, then entrypoint should ignore Google server
  // eligibility. Instead, it requires the 3p flag and local checks excluding
  // DSE.
  return base::FeatureList::IsEnabled(omnibox::kAim3pEntrypoint) &&
         aim_eligibility_service->IsAimAllowedByFeatureAndPolicy() &&
         aim_eligibility_service->IsAimAllowedByThirdPartyPolicy();
}

bool IsAimStarterPackEnabled(
    const AimEligibilityService* aim_eligibility_service) {
  // AI starter pack should be available if any AI omnibox feature is available.
  return AimEligibilityService::GenericKillSwitchFeatureCheck(
             aim_eligibility_service,
             omnibox_feature_configs::Toolbelt::kOmniboxToolbelt) ||
         AimEligibilityService::GenericKillSwitchFeatureCheck(
             aim_eligibility_service, omnibox::kAiModeStartPack);
}

// Rich autocompletion.

bool IsRichAutocompletionEnabled() {
  return base::FeatureList::IsEnabled(omnibox::kRichAutocompletion);
}

const base::FeatureParam<size_t> kRichAutocompletionAutocompleteTitlesMinChar(
    &omnibox::kRichAutocompletion,
    "RichAutocompletionAutocompleteTitlesMinChar",
    3);

const base::FeatureParam<size_t>
    kRichAutocompletionAutocompleteShortcutTextMinChar(
        &omnibox::kRichAutocompletion,
        "RichAutocompletionAutocompleteShortcutTextMinChar",
        3);

// ---------------------------------------------------------
// ML Relevance Scoring ->

// If true, enables scoring signal annotators for logging Omnibox scoring
// signals to OmniboxEventProto.
const base::FeatureParam<bool> kEnableScoringSignalsAnnotatorsForLogging(
    &omnibox::kLogUrlScoringSignals,
    "enable_scoring_signals_annotators",
    false);

// If true, enables scoring signal annotators for ML scoring.
const base::FeatureParam<bool> kEnableScoringSignalsAnnotatorsForMlScoring(
    &omnibox::kMlUrlScoring,
    "enable_scoring_signals_annotators_for_ml_scoring",
    true);

// If true, runs the ML scoring model but does not assign new relevance scores
// to the URL suggestions and does not rerank them.
const base::FeatureParam<bool> kMlUrlScoringCounterfactual(
    &omnibox::kMlUrlScoring,
    "MlUrlScoringCounterfactual",
    false);

// If true, increases the number of candidates the URL autocomplete providers
// pass to the controller beyond `provider_max_matches`.
const base::FeatureParam<bool> kMlUrlScoringUnlimitedNumCandidates(
    &omnibox::kMlUrlScoring,
    "MlUrlScoringUnlimitedNumCandidates",
    false);

const base::FeatureParam<std::string> kMlUrlScoringMaxMatchesByProvider(
    &omnibox::kMlUrlScoring,
    "MlUrlScoringMaxMatchesByProvider",
    "");

MLConfig::MLConfig() {
  log_url_scoring_signals =
      base::FeatureList::IsEnabled(omnibox::kLogUrlScoringSignals);
  enable_history_scoring_signals_annotator_for_searches =
      base::FeatureList::IsEnabled(
          omnibox::kEnableHistoryScoringSignalsAnnotatorForSearches);
  enable_scoring_signals_annotators =
      kEnableScoringSignalsAnnotatorsForLogging.Get() ||
      kEnableScoringSignalsAnnotatorsForMlScoring.Get();
  shortcut_document_signals =
      base::FeatureParam<bool>(&omnibox::kLogUrlScoringSignals,
                               "MlUrlScoringShortcutDocumentSignals", false)
          .Get() ||
      base::FeatureParam<bool>(&omnibox::kMlUrlScoring,
                               "MlUrlScoringShortcutDocumentSignals", true)
          .Get();

  ml_url_scoring = base::FeatureList::IsEnabled(omnibox::kMlUrlScoring);
  ml_url_scoring_counterfactual = kMlUrlScoringCounterfactual.Get();
  ml_url_scoring_unlimited_num_candidates =
      kMlUrlScoringUnlimitedNumCandidates.Get();
  ml_url_scoring_max_matches_by_provider =
      kMlUrlScoringMaxMatchesByProvider.Get();

  // `kMlUrlSearchBlending` parameters.
  mapped_search_blending =
      base::FeatureParam<bool>(&omnibox::kMlUrlSearchBlending,
                               "MlUrlSearchBlending_MappedSearchBlending",
                               mapped_search_blending)
          .Get();
  mapped_search_blending_min =
      base::FeatureParam<int>(&omnibox::kMlUrlSearchBlending,
                              "MlUrlSearchBlending_MappedSearchBlendingMin",
                              mapped_search_blending_min)
          .Get();
  mapped_search_blending_max =
      base::FeatureParam<int>(&omnibox::kMlUrlSearchBlending,
                              "MlUrlSearchBlending_MappedSearchBlendingMax",
                              mapped_search_blending_max)
          .Get();
  mapped_search_blending_grouping_threshold =
      base::FeatureParam<int>(
          &omnibox::kMlUrlSearchBlending,
          "MlUrlSearchBlending_MappedSearchBlendingGroupingThreshold",
          mapped_search_blending_grouping_threshold)
          .Get();

  // `kMlUrlPiecewiseMappedSearchBlending` parameters.
  piecewise_mapped_search_blending =
      base::FeatureParam<bool>(&omnibox::kMlUrlPiecewiseMappedSearchBlending,
                               "MlUrlPiecewiseMappedSearchBlending",
                               piecewise_mapped_search_blending)
          .Get();
  piecewise_mapped_search_blending_grouping_threshold =
      base::FeatureParam<int>(
          &omnibox::kMlUrlPiecewiseMappedSearchBlending,
          "MlUrlPiecewiseMappedSearchBlending_GroupingThreshold",
          piecewise_mapped_search_blending_grouping_threshold)
          .Get();
  piecewise_mapped_search_blending_break_points =
      base::FeatureParam<std::string>(
          &omnibox::kMlUrlPiecewiseMappedSearchBlending,
          "MlUrlPiecewiseMappedSearchBlending_BreakPoints", "")
          .Get();
  piecewise_mapped_search_blending_break_points_verbatim_url =
      base::FeatureParam<std::string>(
          &omnibox::kMlUrlPiecewiseMappedSearchBlending,
          "MlUrlPiecewiseMappedSearchBlending_BreakPoints_VerbatimUrl",
          piecewise_mapped_search_blending_break_points.c_str())
          .Get();
  piecewise_mapped_search_blending_break_points_search =
      base::FeatureParam<std::string>(
          &omnibox::kMlUrlPiecewiseMappedSearchBlending,
          "MlUrlPiecewiseMappedSearchBlending_BreakPoints_Search", "0,0;1,1400")
          .Get();
  piecewise_mapped_search_blending_relevance_bias =
      base::FeatureParam<int>(
          &omnibox::kMlUrlPiecewiseMappedSearchBlending,
          "MlUrlPiecewiseMappedSearchBlending_RelevanceBias",
          piecewise_mapped_search_blending_relevance_bias)
          .Get();

  url_scoring_model = base::FeatureList::IsEnabled(omnibox::kUrlScoringModel);

  ml_url_score_caching =
      base::FeatureList::IsEnabled(omnibox::kMlUrlScoreCaching);
  max_ml_score_cache_size =
      base::FeatureParam<int>(&omnibox::kMlUrlScoreCaching,
                              "MlUrlScoreCaching_MaxMlScoreCacheSize",
                              max_ml_score_cache_size)
          .Get();

  enable_ml_scoring_for_searches =
      base::FeatureParam<bool>(&omnibox::kMlUrlScoring,
                               "MlUrlScoring_EnableMlScoringForSearches", false)
          .Get();
  enable_ml_scoring_for_verbatim_urls =
      base::FeatureParam<bool>(&omnibox::kMlUrlScoring,
                               "MlUrlScoring_EnableMlScoringForVerbatimUrls",
                               false)
          .Get();
}

MLConfig::~MLConfig() = default;

MLConfig::MLConfig(const MLConfig&) = default;

MLConfig& MLConfig::operator=(const MLConfig& other) = default;

ScopedMLConfigForTesting::ScopedMLConfigForTesting()
    : original_config_(std::make_unique<MLConfig>(GetMLConfig())) {
  GetMLConfigInternal() = {};
}

ScopedMLConfigForTesting::~ScopedMLConfigForTesting() {
  GetMLConfigInternal() = *original_config_;
}

MLConfig& ScopedMLConfigForTesting::GetMLConfig() {
  return GetMLConfigInternal();
}

const MLConfig& GetMLConfig() {
  return GetMLConfigInternal();
}

bool IsReportingUrlScoringSignalsEnabled() {
  return GetMLConfig().log_url_scoring_signals;
}

bool IsPopulatingUrlScoringSignalsEnabled() {
  return IsReportingUrlScoringSignalsEnabled() || IsMlUrlScoringEnabled();
}

bool AreScoringSignalsAnnotatorsEnabled() {
  return GetMLConfig().enable_scoring_signals_annotators;
}
bool IsMlUrlScoringEnabled() {
  return IsUrlScoringModelEnabled() && GetMLConfig().ml_url_scoring;
}
bool IsMlUrlScoringCounterfactual() {
  return IsMlUrlScoringEnabled() && GetMLConfig().ml_url_scoring_counterfactual;
}
bool IsMlUrlScoringUnlimitedNumCandidatesEnabled() {
  return IsMlUrlScoringEnabled() &&
         GetMLConfig().ml_url_scoring_unlimited_num_candidates;
}
bool IsUrlScoringModelEnabled() {
  return GetMLConfig().url_scoring_model;
}
bool IsMlUrlScoreCachingEnabled() {
  return GetMLConfig().ml_url_score_caching;
}

std::vector<std::pair<double, int>> GetPiecewiseMappingBreakPoints(
    PiecewiseMappingVariant mapping_variant) {
  std::vector<std::pair<double, int>> break_points;

  std::string param_value;
  switch (mapping_variant) {
    case PiecewiseMappingVariant::kRegular:
      param_value = OmniboxFieldTrial::GetMLConfig()
                        .piecewise_mapped_search_blending_break_points;
      break;
    case PiecewiseMappingVariant::kVerbatimUrl:
      param_value =
          OmniboxFieldTrial::GetMLConfig()
              .piecewise_mapped_search_blending_break_points_verbatim_url;
      break;
    case PiecewiseMappingVariant::kSearch:
      param_value = OmniboxFieldTrial::GetMLConfig()
                        .piecewise_mapped_search_blending_break_points_search;
      break;
    default:
      NOTREACHED();
  }

  base::StringPairs pairs;
  if (base::SplitStringIntoKeyValuePairs(param_value, ',', ';', &pairs)) {
    for (const auto& p : pairs) {
      double ml_score;
      base::StringToDouble(p.first, &ml_score);
      int relevance;
      base::StringToInt(p.second, &relevance);

      break_points.push_back(std::make_pair(ml_score, relevance));
    }
  }

  return break_points;
}

// <- ML Relevance Scoring
// ---------------------------------------------------------
// Touch Down Trigger For Prefetch ->
const base::FeatureParam<int>
    kTouchDownTriggerForPrefetchMaxPrefetchesPerOmniboxSession(
        &omnibox::kOmniboxTouchDownTriggerForPrefetch,
        "max_prefetches_per_omnibox_session",
        5);
// <- Touch Down Trigger For Prefetch
// ---------------------------------------------------------
// Site Search Starter Pack ->
const base::FeatureParam<std::string> kGeminiUrlOverride(
    &omnibox::kStarterPackExpansion,
    "StarterPackGeminiUrlOverride",
    "https://gemini.google.com/prompt?"
    "utm_source=chrome_omnibox&utm_medium=owned&utm_campaign=gemini_shortcut");

bool IsStarterPackExpansionEnabled() {
  return base::FeatureList::IsEnabled(omnibox::kStarterPackExpansion);
}

bool IsStarterPackIPHEnabled() {
  return base::FeatureList::IsEnabled(omnibox::kStarterPackIPH);
}

// <- Site Search Starter Pack
}  // namespace OmniboxFieldTrial
