// 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/base_search_provider.h"

#include <stddef.h>
#include <stdint.h>

#include <algorithm>
#include <memory>
#include <optional>
#include <string>
#include <vector>

#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/i18n/case_conversion.h"
#include "base/memory/scoped_refptr.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "components/omnibox/browser/actions/omnibox_action_in_suggest.h"
#include "components/omnibox/browser/aim_eligibility_service.h"
#include "components/omnibox/browser/autocomplete_provider_client.h"
#include "components/omnibox/browser/autocomplete_provider_listener.h"
#include "components/omnibox/browser/omnibox_field_trial.h"
#include "components/omnibox/browser/omnibox_prefs.h"
#include "components/omnibox/browser/page_classification_functions.h"
#include "components/omnibox/browser/remote_suggestions_service.h"
#include "components/omnibox/browser/search_scoring_signals_annotator.h"
#include "components/omnibox/browser/suggestion_answer.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/template_url.h"
#include "components/search_engines/template_url_service.h"
#include "components/variations/net/variations_http_headers.h"
#include "net/base/registry_controlled_domains/registry_controlled_domain.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "third_party/metrics_proto/omnibox_input_type.pb.h"
#include "third_party/omnibox_proto/navigational_intent.pb.h"
#include "third_party/omnibox_proto/rich_answer_template.pb.h"
#include "url/gurl.h"
#include "url/origin.h"

namespace {
constexpr bool is_android = !!BUILDFLAG(IS_ANDROID);
constexpr bool is_ios = !!BUILDFLAG(IS_IOS);

bool MatchTypeAndContentsAreEqual(const AutocompleteMatch& lhs,
                                  const AutocompleteMatch& rhs) {
  return lhs.contents == rhs.contents && lhs.type == rhs.type;
}

std::u16string GetMatchContentsForOnDeviceTailSuggestion(
    const std::u16string& input_text,
    const std::u16string& sanitized_suggestion) {
  std::u16string sanitized_input;

  base::TrimWhitespace(input_text, base::TRIM_TRAILING, &sanitized_input);
  sanitized_input = AutocompleteMatch::SanitizeString(sanitized_input);

  if (!base::StartsWith(sanitized_suggestion, sanitized_input,
                        base::CompareCase::SENSITIVE)) {
    return sanitized_suggestion;
  }

  // If there is no space inside the suggestion, show the entire suggestion in
  // UI. Otherwise replace the completed prefix of the suggestion with tail UI
  // symbols e.g. "...".
  // Examples (input/suggestion -> result):
  // 1. [googl]/[google] -> [google]
  // 2. [google]/[google map] -> [google map]
  // 3. [google ma]/[google map login] -> [...map login]
  // 4. [google map ]/[google map login] -> [...map login]
  size_t suggestion_last_space_index =
      sanitized_suggestion.find_last_of(base::kWhitespaceUTF16);
  size_t input_last_space_index =
      sanitized_input.find_last_of(base::kWhitespaceUTF16);
  if (suggestion_last_space_index == std::u16string::npos ||
      input_last_space_index == std::u16string::npos) {
    return sanitized_suggestion;
  }
  size_t start_index = input_last_space_index + 1;

  return sanitized_suggestion.substr(start_index);
}

}  // namespace

using OEP = metrics::OmniboxEventProto;

BaseSearchProvider::BaseSearchProvider(AutocompleteProvider::Type type,
                                       AutocompleteProviderClient* client)
    : AutocompleteProvider(type), client_(client) {}

// static
bool BaseSearchProvider::ShouldPrefetch(const AutocompleteMatch& match) {
  // TODO (manukh): `GetAdditionalInfoForDebugging()` shouldn't be used for
  //   non-debugging purposes.
  return match.GetAdditionalInfoForDebugging(kShouldPrefetchKey) == kTrue;
}

// static
bool BaseSearchProvider::ShouldPrerender(const AutocompleteMatch& match) {
  // TODO (manukh): `GetAdditionalInfoForDebugging()` shouldn't be used for
  //   non-debugging purposes.
  return match.GetAdditionalInfoForDebugging(kShouldPrerenderKey) == kTrue;
}

// static
AutocompleteMatch BaseSearchProvider::CreateSearchSuggestion(
    AutocompleteProvider* autocomplete_provider,
    const AutocompleteInput& input,
    const bool in_keyword_mode,
    const SearchSuggestionParser::SuggestResult& suggestion,
    const TemplateURL* template_url,
    const SearchTermsData& search_terms_data,
    int accepted_suggestion,
    bool append_extra_query_params_from_command_line) {
  AutocompleteMatch match(autocomplete_provider, suggestion.relevance(), false,
                          suggestion.type());

  if (!template_url)
    return match;
  match.keyword = template_url->keyword();
  bool is_google = search::TemplateURLIsGoogle(template_url, search_terms_data);
  // If SuggestTemplateInfo is available, use it. Otherwise, continue
  // populating information from EntityInfo.
  const auto& suggest_template_info = suggestion.suggest_template_info();
  if (suggest_template_info) {
    match.suggest_template = suggest_template_info;
  }
  if (is_google) {
    if (suggest_template_info) {
      if (suggest_template_info->has_image()) {
        match.image_dominant_color =
            suggest_template_info->image().dominant_color();
        match.image_url = GURL(suggest_template_info->image().url());
      }
    } else {
      match.image_dominant_color = suggestion.entity_info().dominant_color();
      match.image_url = GURL(suggestion.entity_info().image_url());
    }
    match.answer_template = suggestion.answer_template();
    match.answer_type = suggestion.answer_type();
  }
  match.entity_id = suggestion.entity_info().entity_id();
  match.website_uri = suggestion.entity_info().website_uri();
  match.contents = suggestion.match_contents();
  match.contents_class = suggestion.match_contents_class();
  match.suggestion_group_id = suggestion.suggestion_group_id();
  match.suggest_type = suggestion.suggest_type();
  for (const int subtype : suggestion.subtypes()) {
    match.subtypes.insert(SuggestSubtypeForNumber(subtype));
  }
  if (suggestion.type() == AutocompleteMatchType::SEARCH_SUGGEST_TAIL) {
    match.RecordAdditionalInfo(kACMatchPropertySuggestionText,
                               suggestion.suggestion());
    match.RecordAdditionalInfo(kACMatchPropertyContentsPrefix,
                               suggestion.match_contents_prefix());
    match.RecordAdditionalInfo(
        kACMatchPropertyContentsStartIndex,
        static_cast<int>(suggestion.suggestion().length() -
                         match.contents.length()));
  }

  if (!suggestion.annotation().empty()) {
    match.description = suggestion.annotation();
    // Descriptions should always have dimmed text.
    AutocompleteMatch::AddLastClassificationIfNecessary(
        &match.description_class, 0, ACMatchClassification::DIM);
  }

  const std::u16string input_text = input.IsZeroSuggest() ? u"" : input.text();
  const std::u16string input_lower = base::i18n::ToLower(input_text);
  // suggestion.match_contents() should have already been collapsed.
  match.allowed_to_be_default_match =
      (!in_keyword_mode || suggestion.from_keyword()) &&
      (base::CollapseWhitespace(input_lower, false) ==
       base::i18n::ToLower(suggestion.match_contents()));

  if (suggestion.from_keyword())
    match.from_keyword = true;

  // We only allow inlinable navsuggestions that were received before the
  // last keystroke because we don't want asynchronous inline autocompletions.
  if (!input.prevent_inline_autocomplete() &&
      !suggestion.received_after_last_keystroke() &&
      (!in_keyword_mode || suggestion.from_keyword()) &&
      !input.IsZeroSuggest() &&
      base::StartsWith(base::i18n::ToLower(suggestion.suggestion()),
                       input_lower, base::CompareCase::SENSITIVE)) {
    match.inline_autocompletion =
        suggestion.suggestion().substr(input_text.length());
    match.allowed_to_be_default_match = true;
  }

  const TemplateURLRef& search_url = template_url->url_ref();
  DCHECK(search_url.SupportsReplacement(search_terms_data));
  std::u16string query(suggestion.suggestion());
  std::u16string original_query(input_text);
  if (suggestion.type() == AutocompleteMatchType::CALCULATOR) {
    // Use query text, rather than the calculator answer suggestion, to search.
    query = original_query;
    original_query.clear();
  }
  match.fill_into_edit = GetFillIntoEdit(suggestion, template_url);
  match.search_terms_args =
      std::make_unique<TemplateURLRef::SearchTermsArgs>(query);
  match.search_terms_args->request_source = input.request_source();
  match.search_terms_args->original_query = original_query;
  match.search_terms_args->accepted_suggestion = accepted_suggestion;
  match.search_terms_args->page_classification =
      input.current_page_classification();
  if (suggest_template_info) {
    match.search_terms_args->additional_query_params =
        CreateQueryParamStringFromMap(
            suggest_template_info->default_search_parameters());
  } else {
    match.search_terms_args->additional_query_params =
        suggestion.entity_info().suggest_search_parameters();
  }
  match.search_terms_args->append_extra_query_params_from_command_line =
      append_extra_query_params_from_command_line;
  // Must be set for deduplication and navigation. AutocompleteController will
  // ultimately overwrite this with the searchbox stats before navigation.
  match.destination_url = GURL(search_url.ReplaceSearchTerms(
      *match.search_terms_args, search_terms_data));

  // Search results don't look like URLs.
  match.transition = suggestion.from_keyword() ? ui::PAGE_TRANSITION_KEYWORD
                                               : ui::PAGE_TRANSITION_GENERATED;

  // Attach Actions in Suggest to the newly created match on Android if Google
  // is the default search engine.
  if ((is_android || is_ios) && is_google) {
    if (suggest_template_info &&
        suggest_template_info->action_suggestions_size() > 0) {
      for (const omnibox::SuggestTemplateInfo_TemplateAction& action :
           suggest_template_info->action_suggestions()) {
        auto suggest_action = CreateActionInSuggest(
            action, search_url, *match.search_terms_args, search_terms_data);
        if (suggest_action) {
          match.actions.emplace_back(std::move(suggest_action));
        }
      }
    } else {
      // TODO(crbug.com/417745802): Remove once actions are migrated from
      // EntityInfo to SuggestTemplateInfo.
      for (const omnibox::ActionInfo& action_info :
           suggestion.entity_info().action_suggestions()) {
        omnibox::SuggestTemplateInfo::TemplateAction template_action;
        template_action.set_action_uri(action_info.action_uri());
        template_action.set_logs_action_type(action_info.logs_action_type());
        template_action.set_action_type(
            static_cast<omnibox::SuggestTemplateInfo_TemplateAction_ActionType>(
                action_info.action_type()));
        *template_action.mutable_search_parameters() =
            action_info.search_parameters();
        auto suggest_action =
            CreateActionInSuggest(template_action, search_url,
                                  *match.search_terms_args, search_terms_data);
        if (suggest_action) {
          match.actions.emplace_back(std::move(suggest_action));
        }
      }
    }
  }

  match.navigational_intent = suggestion.navigational_intent();

  return match;
}

scoped_refptr<OmniboxAction> BaseSearchProvider::CreateActionInSuggest(
    omnibox::SuggestTemplateInfo::TemplateAction template_action,
    const TemplateURLRef& search_url,
    const TemplateURLRef::SearchTermsArgs& original_search_terms_args,
    const SearchTermsData& search_terms_data) {
  std::optional<TemplateURLRef::SearchTermsArgs> action_search_terms_args;
  // If the Action's URL is empty, but the Action supplies additional search
  // parameters, compute new URL based on the base URL (that is specific to
  // the entire suggestion).
  if (template_action.action_uri().empty() &&
      !template_action.search_parameters().empty()) {
    action_search_terms_args = original_search_terms_args;
    action_search_terms_args->additional_query_params =
        CreateQueryParamStringFromMap(template_action.search_parameters());
  }

  return OmniboxActionInSuggest::Create(std::move(template_action),
                                        std::move(action_search_terms_args));
}

// static
std::string BaseSearchProvider::CreateQueryParamStringFromMap(
    const google::protobuf::Map<std::string, std::string>& query_param_map) {
  std::string query_params_string;
  for (const auto& param : query_param_map) {
    // Supply additional Query Parameters as instructed by the provider.
    if (!query_params_string.empty()) {
      query_params_string += '&';
    }
    query_params_string += param.first + "=" + param.second;
  }
  return query_params_string;
}

// static
AutocompleteMatch BaseSearchProvider::CreateShortcutSearchSuggestion(
    const std::u16string& suggestion,
    AutocompleteMatchType::Type type,
    const TemplateURL* template_url,
    const SearchTermsData& search_terms_data) {
  // These calls use a number of default values.  For instance, they assume the
  // caller knows what it's doing and we set this match to look as if it was
  // received/created synchronously.
  SearchSuggestionParser::SuggestResult suggest_result(
      suggestion, type, /*suggest_type=*/omnibox::TYPE_NATIVE_CHROME,
      /*subtypes=*/{}, /*from_keyword=*/false,
      /*navigational_intent=*/omnibox::NAV_INTENT_NONE,
      /*relevance=*/0, /*relevance_from_server=*/false,
      /*input_text=*/std::u16string());
  suggest_result.set_received_after_last_keystroke(false);
  return CreateSearchSuggestion(nullptr, AutocompleteInput(),
                                /*in_keyword_mode=*/false, suggest_result,
                                template_url, search_terms_data, 0, false);
}

// static
AutocompleteMatch BaseSearchProvider::CreateOnDeviceSearchSuggestion(
    AutocompleteProvider* autocomplete_provider,
    const AutocompleteInput& input,
    const std::u16string& suggestion,
    int relevance,
    const TemplateURL* template_url,
    const SearchTermsData& search_terms_data,
    int accepted_suggestion,
    bool is_tail_suggestion) {
  AutocompleteMatchType::Type match_type;
  omnibox::SuggestType suggest_type = omnibox::TYPE_NATIVE_CHROME;
  std::u16string match_contents, match_contents_prefix;

  if (is_tail_suggestion) {
    match_type = AutocompleteMatchType::SEARCH_SUGGEST_TAIL;
    suggest_type = omnibox::TYPE_TAIL;
    std::u16string sanitized_suggestion =
        AutocompleteMatch::SanitizeString(suggestion);
    match_contents = GetMatchContentsForOnDeviceTailSuggestion(
        input.text(), sanitized_suggestion);

    DCHECK_GE(sanitized_suggestion.size(), match_contents.size());
    match_contents_prefix = sanitized_suggestion.substr(
        0, sanitized_suggestion.size() - match_contents.size());
  } else {
    match_type = AutocompleteMatchType::SEARCH_SUGGEST;
    suggest_type = omnibox::TYPE_QUERY;
    match_contents = suggestion;
  }

  SearchSuggestionParser::SuggestResult suggest_result(
      suggestion, match_type, suggest_type,
      /*subtypes=*/{omnibox::SUBTYPE_SUGGEST_2G_LITE}, match_contents,
      match_contents_prefix,
      /*annotation=*/std::u16string(),
      /*entity_info=*/omnibox::EntityInfo(),
      /*deletion_url=*/"",
      /*from_keyword=*/false,
      /*navigational_intent=*/omnibox::NAV_INTENT_NONE, relevance,
      /*relevance_from_server=*/false,
      /*should_prefetch=*/false,
      /*should_prerender=*/false,
      base::CollapseWhitespace(input.text(), false));
  // On device providers are asynchronous.
  suggest_result.set_received_after_last_keystroke(true);
  return CreateSearchSuggestion(
      autocomplete_provider, input, /*in_keyword_mode=*/false, suggest_result,
      template_url, search_terms_data, accepted_suggestion,
      /*append_extra_query_params_from_command_line=*/true);
}

// static
bool BaseSearchProvider::PageURLIsEligibleForSuggestRequest(
    const GURL& page_url,
    metrics::OmniboxEventProto::PageClassification page_classification) {
  return page_url.is_valid() && page_url.SchemeIsHTTPOrHTTPS() &&
         !omnibox::IsNTPPage(page_classification);
}

// static
bool BaseSearchProvider::CanSendSuggestRequest(
    metrics::OmniboxEventProto::PageClassification page_classification,
    const TemplateURL* template_url,
    const AutocompleteProviderClient* client) {
  if (!template_url || template_url->suggestions_url().empty()) {
    return false;
  }

  // Setting SuggestUrl the same as SearchUrl is a typical misconfiguration.
  // It's not possible for a URL to both provide a search results page and
  // suggested queries response (at least they have different format).  Most
  // like the user set the search URL correctly; it would be obvious if they did
  // not. Thus, it's likely that the suggest URL is wrong.  Because it would not
  // give a valid query suggestion response, don't bother sending queries to it
  // (otherwise user will quickly hit rate-limit for search queries, that will
  // harm valid search queries as well).
  if (template_url->suggestions_url() == template_url->url()) {
    return false;
  }

  // Don't make a suggest request if in incognito mode; unless for the Lens
  // searchboxes.
  if (client->IsOffTheRecord() &&
      !omnibox::IsLensSearchbox(page_classification)) {
    return false;
  }

  // Don't make a suggest request if suggest is not enabled; unless for the Lens
  // searchboxes.
  if (!client->SearchSuggestEnabled() &&
      !omnibox::IsLensSearchbox(page_classification)) {
    return false;
  }

  return true;
}

// static
bool BaseSearchProvider::CanSendSecureSuggestRequest(
    metrics::OmniboxEventProto::PageClassification page_classification,
    const TemplateURL* template_url,
    const SearchTermsData& search_terms_data,
    const AutocompleteProviderClient* client) {
  if (!CanSendSuggestRequest(page_classification, template_url, client)) {
    return false;
  }

  // Make sure we are sending the suggest request through a cryptographically
  // secure channel to prevent exposing the current page URL or personalized
  // results without encryption.
  const GURL& suggest_url =
      template_url->GenerateSuggestionURL(search_terms_data);
  if (!suggest_url.is_valid() || !suggest_url.SchemeIsCryptographic()) {
    return false;
  }

  // Don't make a suggest request if Google is not the default search engine.
  // Note that currently only the pre-populated Google search provider supports
  // zero-prefix suggestions. If other pre-populated search engines decide to
  // support it, revise this test accordingly.
  if (!search::TemplateURLIsGoogle(template_url, search_terms_data)) {
    return false;
  }

  return true;
}

// static
bool BaseSearchProvider::CanSendSuggestRequestWithPageURL(
    const GURL& current_page_url,
    metrics::OmniboxEventProto::PageClassification page_classification,
    const TemplateURL* template_url,
    const SearchTermsData& search_terms_data,
    const AutocompleteProviderClient* client) {
  if (!CanSendSecureSuggestRequest(page_classification, template_url,
                                   search_terms_data, client)) {
    return false;
  }

  // Forbid sending the current page URL to the suggest endpoint if
  // URL data collection is off; unless the current page is the provider's
  // Search Results Page; or for the Lens searchboxes.
  if (!client->IsUrlDataCollectionActive() &&
      !template_url->IsSearchURL(current_page_url, search_terms_data) &&
      !omnibox::IsLensSearchbox(page_classification)) {
    return false;
  }

  return true;
}

void BaseSearchProvider::DeleteMatch(const AutocompleteMatch& match) {
  DCHECK(match.deletable);
  // TODO (manukh): `GetAdditionalInfoForDebugging()` shouldn't be used for
  //   non-debugging purposes.
  if (!match.GetAdditionalInfoForDebugging(BaseSearchProvider::kDeletionUrlKey)
           .empty()) {
    // Remote personalized suggestions in OTR contexts are not OK.
    DCHECK(!client_->IsOffTheRecord());
    deletion_loaders_.push_back(
        client()
            ->GetRemoteSuggestionsService(/*create_if_necessary=*/true)
            ->StartDeletionRequest(
                match.GetAdditionalInfoForDebugging(
                    BaseSearchProvider::kDeletionUrlKey),
                /*is_off_the_record=*/false,
                base::BindOnce(&BaseSearchProvider::OnDeletionComplete,
                               base::Unretained(this))));
  }

  const TemplateURL* template_url =
      match.GetTemplateURL(client_->GetTemplateURLService());
  // This may be nullptr if the template corresponding to the keyword has been
  // deleted or there is no keyword set.
  if (template_url != nullptr) {
    client_->DeleteMatchingURLsForKeywordFromHistory(template_url->id(),
                                                     match.contents);
  }

  // Immediately update the list of matches to show the match was deleted,
  // regardless of whether the server request actually succeeds.
  DeleteMatchFromMatches(match);
}

void BaseSearchProvider::AddProviderInfo(ProvidersInfo* provider_info) const {
  provider_info->push_back(metrics::OmniboxEventProto_ProviderInfo());
  metrics::OmniboxEventProto_ProviderInfo& new_entry = provider_info->back();
  new_entry.set_provider(AsOmniboxEventProviderType());
  new_entry.set_provider_done(done_);
}

// static
const char BaseSearchProvider::kRelevanceFromServerKey[] =
    "relevance_from_server";
const char BaseSearchProvider::kShouldPrefetchKey[] = "should_prefetch";
const char BaseSearchProvider::kShouldPrerenderKey[] = "should_prerender";
const char BaseSearchProvider::kDeletionUrlKey[] = "deletion_url";
const char BaseSearchProvider::kTrue[] = "true";
const char BaseSearchProvider::kFalse[] = "false";

BaseSearchProvider::~BaseSearchProvider() = default;

// static
std::u16string BaseSearchProvider::GetFillIntoEdit(
    const SearchSuggestionParser::SuggestResult& suggest_result,
    const TemplateURL* template_url) {
  std::u16string fill_into_edit;

  if (suggest_result.from_keyword())
    fill_into_edit.append(template_url->keyword() + u' ');

  fill_into_edit.append(suggest_result.suggestion());

  return fill_into_edit;
}

void BaseSearchProvider::SetDeletionURL(const std::string& deletion_url,
                                        AutocompleteMatch* match) {
  if (deletion_url.empty())
    return;

  TemplateURLService* template_url_service = client_->GetTemplateURLService();
  if (!template_url_service ||
      !template_url_service->GetDefaultSearchProvider())
    return;
  GURL url =
      template_url_service->GetDefaultSearchProvider()->GenerateSearchURL(
          template_url_service->search_terms_data());
  url = url.DeprecatedGetOriginAsURL().Resolve(deletion_url);
  if (url.is_valid()) {
    match->RecordAdditionalInfo(BaseSearchProvider::kDeletionUrlKey,
                                url.spec());
    match->deletable = true;
  }
}

void BaseSearchProvider::AddMatchToMap(
    const SearchSuggestionParser::SuggestResult& result,
    const AutocompleteInput& input,
    const TemplateURL* template_url,
    const SearchTermsData& search_terms_data,
    int accepted_suggestion,
    bool mark_as_deletable,
    bool in_keyword_mode,
    MatchMap* map) {
  AutocompleteMatch match = CreateSearchSuggestion(
      this, input, in_keyword_mode, result, template_url, search_terms_data,
      accepted_suggestion, ShouldAppendExtraParams(result));
  if (!match.destination_url.is_valid())
    return;
  if (match.IsSearchAimSuggestion() &&
      (!omnibox_feature_configs::AiMode::Get().allow_ai_mode_matches ||
       !client_->GetAimEligibilityService()->IsAimLocallyEligible())) {
    return;
  }
  match.RecordAdditionalInfo(kRelevanceFromServerKey,
                             result.relevance_from_server() ? kTrue : kFalse);
  match.RecordAdditionalInfo(kShouldPrefetchKey,
                             result.should_prefetch() ? kTrue : kFalse);
  match.RecordAdditionalInfo(kShouldPrerenderKey,
                             result.should_prerender() ? kTrue : kFalse);
  SetDeletionURL(result.deletion_url(), &match);
  if (mark_as_deletable)
    match.deletable = true;

  // Only set scoring signals for eligible matches.
  if (match.IsMlSignalLoggingEligible()) {
    // Initialize the ML scoring signals for this suggestion if needed.
    if (!match.scoring_signals) {
      match.scoring_signals = std::make_optional<ScoringSignals>();
    }

    if (result.relevance_from_server()) {
      match.scoring_signals->set_search_suggest_relevance(result.relevance());
    }
    SearchScoringSignalsAnnotator::UpdateMatchTypeScoringSignals(match,
                                                                 input.text());
  }

  // Try to add `match` to `map`.
  // NOTE: Keep this ToLower() call in sync with url_database.cc.
  MatchKey match_key(
      std::make_tuple(base::i18n::ToLower(result.suggestion()),
                      match.search_terms_args->additional_query_params));
  const std::pair<MatchMap::iterator, bool> i(
      map->insert(std::make_pair(match_key, match)));
  if (i.second) {
    auto& added_match = i.first->second;
    // If the newly added match has non-empty additional query params and
    // another match with the same search terms and a unique non-empty
    // additional query params is already present in the map, proactively set
    // `stripped_destination_url` to be the same as `destination_url`.
    // Otherwise, `stripped_destination_url` will later be set by
    // `AutocompleteResult::ComputeStrippedDestinationURL()` which strips away
    // the additional query params from `destination_url` leaving only the
    // search terms. That would result in these matches to be erroneously
    // deduped despite having unique additional query params.
    // Note that the match previously added to the map will continue to get the
    // typical `stripped_destination_url` allowing it to be deduped with the
    // plain-text matches (i.e., with no additional query params) as expected.
    const auto& added_match_query = std::get<0>(match_key);
    const auto& added_match_query_params = std::get<1>(match_key);
    if (!added_match_query_params.empty()) {
      for (const auto& entry : *map) {
        const auto& existing_match_query = std::get<0>(entry.first);
        const auto& existing_match_query_params = std::get<1>(entry.first);
        if (existing_match_query == added_match_query &&
            !existing_match_query_params.empty() &&
            existing_match_query_params != added_match_query_params) {
          added_match.stripped_destination_url = added_match.destination_url;
          break;
        }
      }
    }
  } else {
    auto& existing_match = i.first->second;
    // If a duplicate match is already in the map, replace it with `match` if it
    // is more relevant.
    // NOTE: We purposefully do a direct relevance comparison here instead of
    // using AutocompleteMatch::MoreRelevant(), so that we'll prefer "items
    // added first" rather than "items alphabetically first" when the scores
    // are equal. The only case this matters is when a user has results with
    // the same score that differ only by capitalization; because the history
    // system returns results sorted by recency, this means we'll pick the most
    // recent such result even if the precision of our relevance score is too
    // low to distinguish the two.
    if (match.relevance > existing_match.relevance) {
      match.duplicate_matches.insert(match.duplicate_matches.end(),
                                     existing_match.duplicate_matches.begin(),
                                     existing_match.duplicate_matches.end());
      existing_match.duplicate_matches.clear();
      match.duplicate_matches.push_back(existing_match);
      existing_match = std::move(match);
    } else {
      if (match.keyword == existing_match.keyword) {
        // Old and new matches are from the same search provider. It is okay to
        // record one match's prefetch/prerender data onto a different match
        // (for the same query string) for the following reasons:
        // 1. Because the suggest server only sends down a query string from
        // which we construct a URL, rather than sending a full URL, and because
        // we construct URLs from query strings in the same way every time, the
        // URLs for the two matches will be the same. Therefore, we won't end up
        // prefetching/prerendering something the server didn't intend.
        // 2. Presumably the server sets the prefetch/prerender bit on a match
        // it thinks is sufficiently relevant that the user is likely to choose
        // it. Surely setting the prefetch/prerender bit on a match of even
        // higher relevance won't violate this assumption.
        const bool should_prefetch =
            result.should_prefetch() || ShouldPrefetch(existing_match);
        existing_match.RecordAdditionalInfo(kShouldPrefetchKey,
                                            should_prefetch ? kTrue : kFalse);
        const bool should_prerender =
            result.should_prerender() || ShouldPrerender(existing_match);
        existing_match.RecordAdditionalInfo(kShouldPrerenderKey,
                                            should_prerender ? kTrue : kFalse);
      }
      existing_match.duplicate_matches.push_back(std::move(match));
    }

    // Copy over necessary fields from the lower-ranking duplicate. Note that
    // this requires the lower-ranking duplicate being added last. See the use
    // of push_back above:

    // This is to avoid losing the Answers in Suggest information.
    const auto& less_relevant_duplicate_match =
        existing_match.duplicate_matches.back();
    if (less_relevant_duplicate_match.answer_template &&
        !existing_match.answer_template) {
      existing_match.actions = less_relevant_duplicate_match.actions;
      existing_match.answer_template =
          less_relevant_duplicate_match.answer_template;
      existing_match.answer_type = less_relevant_duplicate_match.answer_type;
    }
    // This is to avoid having shopping categorical queries lose their images to
    // higher-relevance local history and verbatim matches. This works for the
    // shopping categorical queries because they only provide images at the
    // moment. That assumption may not hold in the future.
    // Ideally the entire `entity_info`, when available on a suggestion, should
    // be copied over. However `entity_info` is broken down to its constituents
    // in the constructor of SearchSuggestionParser::SuggestResult and used to
    // set individual fields on the AutocompleteMatch. This is in contrast to
    // Answers in Suggest which is kept on the match in its entirety. This is
    // partly because the entity name is used to set and classify the match
    // contents. Ideally `entity_info` should also be kept on the match in its
    // entirety so it can be carried over when deduplicating the matches here or
    // later in the Autocomplete process.
    // TODO(crbug.com/40276602): rework how `entity_info` is used in the match.
    if (!less_relevant_duplicate_match.image_url.is_empty() &&
        existing_match.image_url.is_empty()) {
      existing_match.image_url = less_relevant_duplicate_match.image_url;
    }
    // This is to avoid having shopping categorical queries lose their subtypes
    // to higher-relevance local history and verbatim matches. The subtypes are
    // sent to the backend in the ChromeSearchboxStats proto via the gs_lcrp=
    // param when the user selects a suggestion. The subtypes may be used to
    // identify what the user selected so they can be suggested the next time,
    // i.e., if the user selects a decorated suggestion - which is accompanied
    // by specific subtypes - we want to show a decorated suggestion next time.
    existing_match.subtypes.insert(
        less_relevant_duplicate_match.subtypes.begin(),
        less_relevant_duplicate_match.subtypes.end());
    // This is to avoid having `stripped_destination_url` being later set by
    // `AutocompleteResult::ComputeStrippedDestinationURL()` which strips away
    // the additional query params from `destination_url` leaving only the
    // search terms. That would result in these matches to be erroneously
    // deduped despite having unique additional query params.
    if (!less_relevant_duplicate_match.stripped_destination_url.is_empty() &&
        existing_match.stripped_destination_url.is_empty()) {
      existing_match.stripped_destination_url =
          less_relevant_duplicate_match.stripped_destination_url;
    }
  }
}

void BaseSearchProvider::DeleteMatchFromMatches(
    const AutocompleteMatch& match) {
  for (auto i(matches_.begin()); i != matches_.end(); ++i) {
    // Find the desired match to delete by checking the type and contents.
    // We can't check the destination URL, because the autocomplete controller
    // may have reformulated that. Not that while checking for matching
    // contents works for personalized suggestions, if more match types gain
    // deletion support, this algorithm may need to be re-examined.

    if (MatchTypeAndContentsAreEqual(match, *i)) {
      matches_.erase(i);
      break;
    }

    // Handle the case where the deleted match is only found within the
    // duplicate_matches sublist.
    std::vector<AutocompleteMatch>& duplicates = i->duplicate_matches;
    auto it =
        std::remove_if(duplicates.begin(), duplicates.end(),
                       [&match](const AutocompleteMatch& duplicate) {
                         return MatchTypeAndContentsAreEqual(match, duplicate);
                       });
    if (it != duplicates.end()) {
      duplicates.erase(it, duplicates.end());
      break;
    }
  }
}

void BaseSearchProvider::OnDeletionComplete(
    const network::SimpleURLLoader* source,
    const int response_code,
    std::optional<std::string> response_body) {
  RecordDeletionResult(response_code == 200);
  std::erase_if(
      deletion_loaders_,
      [source](const std::unique_ptr<network::SimpleURLLoader>& loader) {
        return loader.get() == source;
      });
}
