// 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 "chrome/browser/glic/suggestions/contextual_cueing_service.h"

#include <cmath>
#include <variant>

#include "base/check.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/task/single_thread_task_runner.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/glic/browser_ui/glic_nudge_controller.h"
#include "chrome/browser/glic/glic_pref_names.h"
#include "chrome/browser/glic/suggestions/contextual_cueing_enums.h"
#include "chrome/browser/glic/suggestions/contextual_cueing_features.h"
#include "chrome/browser/glic/suggestions/contextual_cueing_page_data.h"
#include "chrome/browser/glic/suggestions/contextual_cueing_prefs.h"
#include "chrome/browser/glic/suggestions/zero_state_suggestions_page_data.h"
#include "chrome/browser/glic/suggestions/zero_state_suggestions_request.h"
#include "chrome/browser/optimization_guide/optimization_guide_keyed_service.h"
#include "chrome/browser/predictors/loading_predictor.h"
#include "chrome/common/buildflags.h"
#include "chrome/common/chrome_features.h"
#include "components/optimization_guide/core/model_execution/model_execution_manager.h"
#include "components/optimization_guide/core/optimization_guide_util.h"
#include "components/optimization_guide/proto/hints.pb.h"
#include "components/prefs/pref_service.h"
#include "components/search_engines/template_url.h"
#include "components/search_engines/template_url_service.h"
#include "components/signin/public/identity_manager/identity_manager.h"
#include "content/public/browser/web_contents.h"
#include "net/base/network_anonymization_key.h"
#include "services/metrics/public/cpp/metrics_utils.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "services/metrics/public/cpp/ukm_recorder.h"
#include "services/network/public/cpp/constants.h"
#include "url/gurl.h"

namespace glic {
namespace {

void LogNudgeInteractionHistogram(NudgeInteraction interaction,
                                  bool is_dynamic) {
  base::UmaHistogramEnumeration("ContextualCueing.NudgeInteraction",
                                interaction);
  std::string cue_type = is_dynamic ? "Dynamic" : "Static";
  base::UmaHistogramEnumeration("ContextualCueing.NudgeInteraction." + cue_type,
                                interaction);
}

void LogNudgeInteractionUKM(ukm::SourceId source_id,
                            NudgeInteraction interaction,
                            bool is_dynamic,
                            base::TimeTicks document_available_time,
                            base::TimeTicks nudge_shown_time) {
  auto* ukm_recorder = ukm::UkmRecorder::Get();
  ukm::builders::ContextualCueing_NudgeInteraction(source_id)
      .SetNudgeInteraction(static_cast<int64_t>(interaction))
      .SetNudgeIsDynamic(is_dynamic)
      .SetNudgeShownDuration(ukm::GetExponentialBucketMinForUserTiming(
          (base::TimeTicks::Now() - nudge_shown_time).InMilliseconds()))
      .SetNudgeLatencyAfterPageLoad(
          (nudge_shown_time - document_available_time).InMilliseconds())
      .Record(ukm_recorder->Get());
}

bool IsGlicTabContextEnabled(PrefService* pref_service) {
  if (base::FeatureList::IsEnabled(features::kGlicDefaultTabContextSetting)) {
    return true;
  }
  return pref_service->GetBoolean(glic::prefs::kGlicTabContextEnabled);
}

void OnSuggestionsReceived(bool is_fre,
                           base::TimeTicks fetch_begin_time,
                           GlicSuggestionsCallback callback,
                           std::vector<std::string> suggestions) {
  base::TimeDelta suggestion_latency =
      base::TimeTicks::Now() - fetch_begin_time;
  std::string result_type =
      suggestions.empty() ? "EmptySuggestions" : "ValidSuggestions";
  std::string engagement_type = is_fre ? "FRE" : "Reengagement";
  // Continue logging the original histogram.
  base::UmaHistogramTimes(
      "ContextualCueing.GlicSuggestions.SuggestionsFetchLatency." + result_type,
      suggestion_latency);
  // Add another split by engagement type.
  base::UmaHistogramTimes(
      "ContextualCueing.GlicSuggestions.SuggestionsFetchLatency." +
          result_type + "." + engagement_type,
      suggestion_latency);

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

base::ListValue ConvertSupportedToolsToPrefValue(
    const std::vector<std::string>& supported_tools) {
  base::ListValue pref_tools;
  for (const auto& tool : supported_tools) {
    pref_tools.Append(tool);
  }
  return pref_tools;
}

std::vector<std::string> GetSupportedToolsFromPref(
    const base::ListValue& pref_value) {
  std::vector<std::string> supported_tools;
  for (const base::Value& value : pref_value) {
    supported_tools.push_back(value.GetString());
  }
  return supported_tools;
}

// Populates the tools to be sent in the request for zero state suggestions.
// Will cache tools from request if present. Otherwise, gets the tools cached
// from pref.
void PopulateSupportedToolsForRequest(
    const std::optional<std::vector<std::string>>& tools_from_request,
    PrefService* pref_service,
    optimization_guide::proto::ZeroStateSuggestionsRequest* out_request) {
  std::vector<std::string> req_supported_tools;
  if (tools_from_request) {
    req_supported_tools = *tools_from_request;
    pref_service->SetList(
        contextual_cueing::prefs::kZeroStateSuggestionsSupportedTools,
        ConvertSupportedToolsToPrefValue(*tools_from_request));
  } else {
    req_supported_tools = GetSupportedToolsFromPref(pref_service->GetList(
        contextual_cueing::prefs::kZeroStateSuggestionsSupportedTools));
  }
  *out_request->mutable_supported_tools() = {req_supported_tools.begin(),
                                             req_supported_tools.end()};
}

}  // namespace

ContextualCueingService::ContextualCueingService(
    page_content_annotations::PageContentExtractionService*
        page_content_extraction_service,
    OptimizationGuideKeyedService* optimization_guide_keyed_service,
    predictors::LoadingPredictor* loading_predictor,
    signin::IdentityManager* identity_manager,
    PrefService* pref_service,
    TemplateURLService* template_url_service)
    : recent_nudge_tracker_(kNudgeCapCount.Get(), kNudgeCapTime.Get()),
      recent_visited_origins_(kVisitedDomainsLimit.Get()),
      page_content_extraction_service_(page_content_extraction_service),
      optimization_guide_keyed_service_(optimization_guide_keyed_service),
      loading_predictor_(loading_predictor),
      pref_service_(pref_service),
      template_url_service_(template_url_service),
      identity_manager_(identity_manager),
      mes_url_(optimization_guide::GetModelExecutionServiceURL()) {
  if (optimization_guide_keyed_service_ && IsZeroStateSuggestionsEnabled()) {
    optimization_guide_keyed_service_->RegisterOptimizationTypes(
        {optimization_guide::proto::GLIC_ZERO_STATE_SUGGESTIONS});
  }

  if (kEnablePageContentExtraction.Get() && page_content_extraction_service_) {
    page_content_extraction_service_->AddObserver(this);
  }
}

ContextualCueingService::~ContextualCueingService() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (kEnablePageContentExtraction.Get() && page_content_extraction_service_) {
    page_content_extraction_service_->RemoveObserver(this);
  }
}

void ContextualCueingService::ReportPageLoad() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (remaining_quiet_loads_) {
    remaining_quiet_loads_--;
  }
}

void ContextualCueingService::CueingNudgeShown(const GURL& url) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  recent_nudge_tracker_.CueingNudgeShown();
  shown_backoff_end_time_ =
      base::TimeTicks::Now() + kMinTimeBetweenNudges.Get();

  if (kMinPageCountBetweenNudges.Get()) {
    // Let the cue logic be performed the next page after quiet count pages.
    remaining_quiet_loads_ = kMinPageCountBetweenNudges.Get() + 1;
  }

  auto origin = url::Origin::Create(url);
  auto iter = recent_visited_origins_.Get(origin);
  if (iter == recent_visited_origins_.end()) {
    iter = recent_visited_origins_.Put(
        origin,
        ::contextual_cueing::NudgeCapTracker(kNudgeCapCountPerDomain.Get(),
                                             kNudgeCapTimePerDomain.Get()));
  }
  iter->second.CueingNudgeShown();
}

void ContextualCueingService::CueingNudgeDismissed() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  base::TimeDelta backoff_duration =
      kBackoffTime.Get() * pow(kBackoffMultiplierBase.Get(), dismiss_count_);

  dismiss_backoff_end_time_ = base::TimeTicks::Now() + backoff_duration;
  ++dismiss_count_;
}

void ContextualCueingService::CueingNudgeClicked() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  dismiss_count_ = 0;
}

NudgeDecision ContextualCueingService::CanShowNudge(const GURL& url) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (remaining_quiet_loads_ > 0) {
    return NudgeDecision::kNotEnoughPageLoadsSinceLastNudge;
  }
  if (shown_backoff_end_time_ &&
      base::TimeTicks::Now() < shown_backoff_end_time_) {
    return NudgeDecision::kNotEnoughTimeSinceLastNudgeShown;
  }
  if (IsNudgeBlockedByBackoffRule()) {
    return NudgeDecision::kNotEnoughTimeSinceLastNudgeDismissed;
  }
  if (!recent_nudge_tracker_.CanShowNudge()) {
    return NudgeDecision::kTooManyNudgesShownToTheUser;
  }
  auto iter = recent_visited_origins_.Peek(url::Origin::Create(url));
  if (iter != recent_visited_origins_.end() && !iter->second.CanShowNudge()) {
    return NudgeDecision::kTooManyNudgesShownToTheUserForDomain;
  }
  return NudgeDecision::kSuccess;
}

bool ContextualCueingService::IsNudgeBlockedByBackoffRule() const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  return dismiss_backoff_end_time_ &&
         (base::TimeTicks::Now() < dismiss_backoff_end_time_);
}

bool ContextualCueingService::IsPageTypeEligibleForContextualSuggestions(
    GURL url) const {
  // Non-HTTP/HTTPS pages are not eligible.
  if (!url.SchemeIsHTTPOrHTTPS()) {
    return false;
  }

  // Search results pages are not eligible.
  if (!kAllowContextualSuggestionsForSearchResultsPages.Get() &&
      (template_url_service_ &&
       template_url_service_->ExtractSearchMetadata(url))) {
    return false;
  }

  return true;
}

void ContextualCueingService::OnNudgeActivity(
    base::WeakPtr<content::WebContents> web_contents,
    base::TimeTicks document_available_time,
    bool is_dynamic,
    glic::GlicNudgeActivity activity) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  std::optional<base::TimeTicks> nudge_time =
      recent_nudge_tracker_.GetMostRecentNudgeTime();
  NudgeInteraction interaction;
  bool log_ukm = false;
  switch (activity) {
    case glic::GlicNudgeActivity::kNudgeShown:
      interaction = NudgeInteraction::kShown;
      CueingNudgeShown(web_contents ? web_contents->GetLastCommittedURL()
                                    : GURL());
      break;
    case glic::GlicNudgeActivity::kNudgeClicked:
      CueingNudgeClicked();
      interaction = NudgeInteraction::kClicked;
      log_ukm = true;
      break;
    case glic::GlicNudgeActivity::kNudgeDismissed:
      interaction = NudgeInteraction::kDismissed;
      CueingNudgeDismissed();
      log_ukm = true;
      break;
    case glic::GlicNudgeActivity::kNudgeNotShownWebContents:
      interaction = NudgeInteraction::kNudgeNotShownWebContents;
      break;
    case glic::GlicNudgeActivity::kNudgeNotShownWindowCallToActionUI:
      interaction = NudgeInteraction::kNudgeNotShownWindowCallToActionUI;
      break;
    case glic::GlicNudgeActivity::kNudgeIgnoredActiveTabChanged:
      interaction = NudgeInteraction::kIgnoredTabChange;
      // The ActiveTabChanged activity is called very aggresivly and there may
      // not be an actively shown nudge. We should only log this as an action if
      // there is a shown nudge is dismissed
      if (!nudge_time) {
        return;
      }
      log_ukm = true;
      break;
    case glic::GlicNudgeActivity::kNudgeIgnoredNavigation:
      interaction = NudgeInteraction::kIgnoredNavigation;
      log_ukm = true;
      break;
    case glic::GlicNudgeActivity::kNudgeIgnoredOpenedContextualTasksSidePanel:
      interaction = NudgeInteraction::kIgnoredOpenedContextualTasksSidePanel;
      log_ukm = true;
      break;
    case glic::GlicNudgeActivity::kNudgeIgnoredOmniboxContextMenuInteraction:
      interaction = NudgeInteraction::kIgnoredOmniboxContextMenuInteraction;
      log_ukm = true;
      break;
  }
  LogNudgeInteractionHistogram(interaction, is_dynamic);
  // As this function is called multiple times per nudge only some of the
  // activities result in a UKM call.
  if (log_ukm && web_contents) {
    CHECK(nudge_time);
    LogNudgeInteractionUKM(
        web_contents->GetPrimaryMainFrame()->GetPageUkmSourceId(), interaction,
        is_dynamic, document_available_time, *nudge_time);
  }
}

void ContextualCueingService::PrepareToFetchContextualGlicZeroStateSuggestions(
    content::WebContents* web_contents) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (!IsZeroStateSuggestionsEnabled()) {
    return;
  }

  if (!optimization_guide_keyed_service_) {
    return;
  }

  if (!IsPageTypeEligibleForContextualSuggestions(
          web_contents->GetLastCommittedURL())) {
    return;
  }

  if (!IsGlicTabContextEnabled(pref_service_)) {
    return;
  }

  // This call preflights grabbing the page content.
  ZeroStateSuggestionsPageData::CreateForPage(web_contents->GetPrimaryPage());

  if (loading_predictor_) {
    net::NetworkAnonymizationKey anonymization_key =
        net::NetworkAnonymizationKey::CreateSameSite(
            net::SchemefulSite(mes_url_));
    loading_predictor_->PreconnectURLIfAllowed(
        mes_url_, /*allow_credentials=*/true, anonymization_key,
        network::GetNoOpNetworkRestrictionsId());
  }
}

std::unique_ptr<ZeroStateSuggestionsRequest>
ContextualCueingService::MakeZeroStateSuggestionsRequest(
    const std::vector<raw_ptr<content::WebContents>>& web_contents_list,
    bool is_fre,
    std::optional<std::vector<std::string>> supported_tools,
    const content::WebContents* focused_tab) {
  // Construct base request proto.
  optimization_guide::proto::ZeroStateSuggestionsRequest request_proto;
  request_proto.set_is_fre(is_fre);
  if (g_browser_process) {
    request_proto.set_locale(g_browser_process->GetApplicationLocale());
  }
  request_proto.set_chrome_platform(optimization_guide::GetChromePlatform());
  PopulateSupportedToolsForRequest(supported_tools, pref_service_,
                                   &request_proto);
  // Instantiate the one-of to indicate the request type.
  if (focused_tab && web_contents_list.size() == 1) {
    request_proto.mutable_page_context();
  } else {
    request_proto.mutable_page_context_list();
  }

  return std::make_unique<ZeroStateSuggestionsRequest>(
      optimization_guide_keyed_service_, identity_manager_, request_proto,
      web_contents_list, focused_tab);
}

void ContextualCueingService::
    GetContextualGlicZeroStateSuggestionsForFocusedTab(
        content::WebContents* web_contents,
        bool is_fre,
        std::optional<std::vector<std::string>> supported_tools,
        GlicSuggestionsCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (!IsZeroStateSuggestionsEnabled()) {
    std::move(callback).Run({});
    return;
  }

  if (!optimization_guide_keyed_service_) {
    std::move(callback).Run({});
    return;
  }

  bool page_type_eligible = IsPageTypeEligibleForContextualSuggestions(
      web_contents->GetLastCommittedURL());
  base::UmaHistogramBoolean(
      "ContextualCueing.GlicSuggestions.FocusedTabEligibleForSuggestions",
      page_type_eligible);
  if (!page_type_eligible) {
    std::move(callback).Run({});
    return;
  }

  if (!IsGlicTabContextEnabled(pref_service_)) {
    std::move(callback).Run({});
    return;
  }

  // Add callback to new request or existing one if already have one for
  // the page associated with `web_contents`.
  auto* zss_data = ZeroStateSuggestionsPageData::GetOrCreateForPage(
      web_contents->GetPrimaryPage());
  auto* zss_request_ptr = zss_data->focused_tab_request();
  if (!zss_request_ptr) {
    auto zss_request = MakeZeroStateSuggestionsRequest(
        {web_contents}, is_fre, supported_tools, web_contents);
    zss_request_ptr = zss_request.get();
    zss_data->set_focused_tab_request(std::move(zss_request));
  }
  zss_request_ptr->AddCallback(base::BindOnce(&OnSuggestionsReceived, is_fre,
                                              base::TimeTicks::Now(),
                                              std::move(callback)));
}

std::optional<std::vector<raw_ptr<content::WebContents>>>
ContextualCueingService::GetOutstandingPinnedTabsContents() {
  if (!pinned_tabs_zero_state_suggestions_request_) {
    return std::nullopt;
  }
  return pinned_tabs_zero_state_suggestions_request_->GetRequestedTabs();
}

bool ContextualCueingService::
    GetContextualGlicZeroStateSuggestionsForPinnedTabs(
        std::vector<raw_ptr<content::WebContents>> pinned_web_contents,
        bool is_fre,
        std::optional<std::vector<std::string>> supported_tools,
        const content::WebContents* focused_tab,
        GlicSuggestionsCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (!IsZeroStateSuggestionsEnabled()) {
    std::move(callback).Run({});
    return false;
  }

  if (!optimization_guide_keyed_service_) {
    std::move(callback).Run({});
    return false;
  }

  // Remove all ineligible pages from list.
  std::erase_if(pinned_web_contents, [&](content::WebContents* web_contents) {
    return !IsPageTypeEligibleForContextualSuggestions(
        web_contents->GetLastCommittedURL());
  });
  base::UmaHistogramBoolean(
      "ContextualCueing.GlicSuggestions.PinnedTabsEligibleForSuggestions",
      !pinned_web_contents.empty());
  if (pinned_web_contents.empty()) {
    std::move(callback).Run({});
    return false;
  }

  // Initiate request for suggestions for pinned tabs.
  pinned_tabs_zero_state_suggestions_request_ = MakeZeroStateSuggestionsRequest(
      pinned_web_contents, is_fre, supported_tools, focused_tab);
  pinned_tabs_zero_state_suggestions_request_->AddCallback(base::BindOnce(
      &ContextualCueingService::OnPinnedTabsSuggestionsReceived,
      weak_ptr_factory_.GetWeakPtr(), is_fre, base::TimeTicks::Now(),
      pinned_tabs_zero_state_suggestions_request_->AsWeakPtr(),
      std::move(callback)));
  return true;
}

void ContextualCueingService::OnPinnedTabsSuggestionsReceived(
    bool is_fre,
    base::TimeTicks fetch_begin_time,
    base::WeakPtr<ZeroStateSuggestionsRequest> pinned_tabs_request,
    GlicSuggestionsCallback callback,
    std::vector<std::string> suggestions) {
  OnSuggestionsReceived(is_fre, fetch_begin_time, std::move(callback),
                        std::move(suggestions));

  // Only destroy the outstanding pinned tabs request if it is the same.
  if (pinned_tabs_request &&
      pinned_tabs_request.get() ==
          pinned_tabs_zero_state_suggestions_request_.get()) {
    base::SequencedTaskRunner::GetCurrentDefault()->PostNonNestableTask(
        FROM_HERE,
        base::BindOnce(&ZeroStateSuggestionsRequest::Destroy,
                       std::move(pinned_tabs_zero_state_suggestions_request_)));
  }
}

void ContextualCueingService::OnPageContentExtracted(
    content::Page& page,
    page_content_annotations::PageContent page_content) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  page_content_annotations::RefCountedAnnotatedPageContentPtr
      annotated_page_content_ptr =
          page_content_annotations::GetAnnotatedPageContentPtrFromPageContent(
              page_content);
  if (!annotated_page_content_ptr) {
    return;
  }

  auto* cueing_page_data = ContextualCueingPageData::GetForPage(page);
  if (!cueing_page_data) {
    return;
  }
  cueing_page_data->OnPageContentExtracted(annotated_page_content_ptr->data);
}

}  // namespace glic
