// Copyright 2025 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/tab_group_provider.h"

#include <algorithm>

#include "base/i18n/case_conversion.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/trace_event/trace_event.h"
#include "components/omnibox/common/omnibox_features.h"
#include "components/saved_tab_groups/public/saved_tab_group.h"
#include "components/saved_tab_groups/public/tab_group_sync_service.h"
#if BUILDFLAG(IS_ANDROID)
#include "components/browser_ui/util/android/url_constants.h"
#endif
#include "components/omnibox/browser/autocomplete_enums.h"
#include "components/omnibox/browser/autocomplete_input.h"
#include "components/omnibox/browser/autocomplete_match.h"
#include "components/omnibox/browser/autocomplete_match_classification.h"
#include "components/omnibox/browser/autocomplete_provider_client.h"
#include "components/omnibox/browser/in_memory_url_index_types.h"
#include "components/omnibox/browser/keyword_provider.h"
#include "components/omnibox/browser/match_compare.h"
#include "components/omnibox/browser/omnibox_field_trial.h"
#include "components/omnibox/browser/page_classification_functions.h"
#include "components/omnibox/browser/scoring_functor.h"
#include "components/omnibox/browser/tab_matcher.h"
#include "components/query_parser/query_parser.h"
#include "components/saved_tab_groups/public/saved_tab_group_tab.h"
#include "components/search_engines/template_url.h"
#include "components/strings/grit/components_strings.h"
#include "components/url_formatter/url_formatter.h"
#include "content/public/common/url_constants.h"
#include "third_party/omnibox_proto/groups.pb.h"
#include "ui/base/l10n/l10n_util.h"
#include "url/gurl.h"

namespace {
std::pair<int, std::u16string> Score(
    const AutocompleteInput& input,
    const query_parser::QueryNodeVector& input_query_nodes,
    const tab_groups::SavedTabGroup& group) {
  TRACE_EVENT("omnibox", "TabGroupProvider::Score");

  std::u16string matching_url;
  // TODO(crbug.com/435705148): Remove this check when unnamed tab groups are
  // supported as they may be surfaced through a matching URL with no title.
  if (group.title().empty()) {
    return {0, matching_url};
  }

  // Extract query words from the title.
  const std::u16string lower_title = base::i18n::ToLower(group.title());
  query_parser::QueryWordVector title_words;
  query_parser::QueryParser::ExtractQueryWords(lower_title, &title_words);

  // Every input term must be included in either (or both) the title or URL.
  query_parser::Snippet::MatchPositions title_matches;
  query_parser::Snippet::MatchPositions url_matches;
  if (!std::ranges::all_of(input_query_nodes, [&](const auto& query_node) {
        // Search for a matching URL and early return once one is found but do
        // not consider chrome prefixed tabs during matching. The URL provided
        // by saved_tab will include user queries for search result pages.
        bool has_url_match;
        for (auto& saved_tab : group.saved_tabs()) {
#if BUILDFLAG(IS_ANDROID)
          if (saved_tab.url().SchemeIs(browser_ui::kChromeUINativeScheme) ||
              saved_tab.url().SchemeIs(content::kChromeUIScheme)) {
            continue;
          }
#endif

          const std::u16string lower_url =
              base::i18n::ToLower(url_formatter::FormatUrl(
                  saved_tab.url(),
                  AutocompleteMatch::GetFormatTypes(
                      /*preserve_scheme=*/false,
                      /*preserve_subdomain=*/false),
                  base::UnescapeRule::SPACES, nullptr, nullptr, nullptr));

          query_parser::QueryWordVector url_words;
          query_parser::QueryParser::ExtractQueryWords(lower_url, &url_words);

          has_url_match = query_node->HasMatchIn(url_words, &url_matches);
          if (has_url_match) {
            matching_url = lower_url;
            break;
          }
        }
        // Using local vars so to not short circuit adding URL matches when
        // title matches are found.
        const bool has_title_match =
            query_node->HasMatchIn(title_words, &title_matches);
        return has_title_match || has_url_match;
      })) {
    return {0, matching_url};
  }

  // Max score is based on these suggestions sharing a group with open tab
  // matches.
  const int kMaxScore = 1000;
  const double title_factor =
      for_each(title_matches.begin(), title_matches.end(),
               ScoringFunctor(lower_title.length()))
          .ScoringFactor();
  const double url_factor = for_each(url_matches.begin(), url_matches.end(),
                                     ScoringFunctor(matching_url.length()))
                                .ScoringFactor();
  const double normalized_factors =
      std::min((title_factor + url_factor) / (lower_title.length() + 10), 1.0);

  return {normalized_factors * kMaxScore, matching_url};
}

}  // namespace

TabGroupProvider::TabGroupProvider(AutocompleteProviderClient* client)
    : AutocompleteProvider(AutocompleteProvider::TYPE_OPEN_TAB),
      client_(client) {}

TabGroupProvider::~TabGroupProvider() = default;

// TODO(crbug.com/412433887): Make the TabGroupProvider async.
void TabGroupProvider::Start(const AutocompleteInput& input,
                             bool minimal_changes) {
  Stop(AutocompleteStopReason::kClobbered);
  if (!omnibox::IsAndroidHubOrTabSearch(input.current_page_classification()) ||
      client_->IsOffTheRecord()) {
    return;
  }

  TRACE_EVENT("omnibox", "TabGroupProvider::Start");
  // Remove the keyword from input if we're in keyword mode for a starter pack
  // engine.
  const auto& [adjusted_input, template_url] =
      AdjustInputForStarterPackKeyword(input, client_->GetTemplateURLService());

  // Preprocess the query into query nodes.
  const auto adjusted_input_text = std::u16string(
      base::TrimWhitespace(base::i18n::ToLower(adjusted_input.text()),
                           base::TrimPositions::TRIM_ALL));
  query_parser::QueryNodeVector input_query_nodes;
  query_parser::QueryParser::ParseQueryNodes(
      adjusted_input_text,
      query_parser::MatchingAlgorithm::ALWAYS_PREFIX_SEARCH,
      &input_query_nodes);

  for (auto& group : client_->GetTabGroupSyncService()->GetAllGroups()) {
    const auto& [score, matching_url] = Score(input, input_query_nodes, group);
    if (score > 0) {
      matches_.push_back(
          CreateTabGroupMatch(input, group, score, matching_url));
    }
  }
}

AutocompleteMatch TabGroupProvider::CreateTabGroupMatch(
    const AutocompleteInput& input,
    const tab_groups::SavedTabGroup& group,
    int score,
    const std::u16string& matching_url) {
  AutocompleteMatch match(this, score, /*deletable=*/false,
                          AutocompleteMatchType::TAB_GROUP);
  match.contents = group.title();
  auto contents_terms = FindTermMatches(input.text(), match.contents);
  match.contents_class = ClassifyTermMatches(
      contents_terms, match.contents.size(), ACMatchClassification::MATCH,
      ACMatchClassification::NONE);
  match.matching_tab_group_uuid = group.saved_guid();
  match.suggestion_group_id = omnibox::GROUP_MOBILE_OPEN_TABS;
  match.image_dominant_color =
      base::NumberToString(static_cast<int>(group.color()));

  std::u16string url_list;
  for (auto& saved_tab : group.saved_tabs()) {
#if BUILDFLAG(IS_ANDROID)
    // Skip showing chrome-prefixed tabs.
    if (saved_tab.url().SchemeIs(browser_ui::kChromeUINativeScheme) ||
        saved_tab.url().SchemeIs(content::kChromeUIScheme)) {
      continue;
    }
#endif

    const std::u16string url = url_formatter::FormatUrl(
        saved_tab.url(),
        AutocompleteMatch::GetFormatTypes(/*preserve_scheme=*/false,
                                          /*preserve_subdomain=*/false),
        base::UnescapeRule::SPACES, nullptr, nullptr, nullptr);

    // Place the matching URL at the front of the list.
    if (!matching_url.empty() && url == matching_url) {
      url_list.insert(0, u", ");
      url_list.insert(0, url);
    } else {
      url_list.append(url);
      url_list.append(u", ");
    }
  }

  // Remove the last comma and space from the string.
  if (!url_list.empty()) {
    url_list.erase(url_list.size() - 2);
  }

  match.description = url_list;
  auto description_terms = FindTermMatches(input.text(), match.description);
  match.description_class = ClassifyTermMatches(
      description_terms, match.description.size(),
      ACMatchClassification::MATCH | ACMatchClassification::URL,
      ACMatchClassification::URL);
  match.has_tab_match = true;

  return match;
}
