// Copyright 2018 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/navigation_predictor/navigation_predictor.h"

#include <algorithm>
#include <memory>
#include <optional>

#include "base/base_switches.h"
#include "base/check_op.h"
#include "base/command_line.h"
#include "base/hash/hash.h"
#include "base/metrics/field_trial_params.h"
#include "base/rand_util.h"
#include "base/system/sys_info.h"
#include "base/time/default_tick_clock.h"
#include "chrome/browser/navigation_predictor/navigation_predictor_keyed_service.h"
#include "chrome/browser/navigation_predictor/navigation_predictor_keyed_service_factory.h"
#include "chrome/browser/navigation_predictor/preloading_model_keyed_service.h"
#include "chrome/browser/navigation_predictor/preloading_model_keyed_service_factory.h"
#include "chrome/browser/preloading/preloading_prefs.h"
#include "chrome/browser/profiles/profile.h"
#include "components/no_state_prefetch/browser/no_state_prefetch_manager.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/preloading_data.h"
#include "content/public/browser/site_instance.h"
#include "content/public/browser/web_contents.h"
#include "mojo/public/cpp/bindings/message.h"
#include "mojo/public/cpp/bindings/self_owned_receiver.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 "third_party/blink/public/common/features.h"
#include "url/gurl.h"
#include "url/url_canon.h"

namespace {

// The maximum number of clicks to track in a single navigation.
constexpr size_t kMaxClicksTracked = 10;

bool IsPrerendering(content::RenderFrameHost& render_frame_host) {
  return render_frame_host.GetLifecycleState() ==
         content::RenderFrameHost::LifecycleState::kPrerendering;
}

NavigationPredictor::FontSizeBucket GetFontSizeFromPx(uint32_t font_size_px) {
  if (font_size_px < 10) {
    return NavigationPredictor::kLessThanTen;
  } else if (font_size_px < 18) {
    return NavigationPredictor::kTenToSeventeen;
  } else {
    return NavigationPredictor::kEighteenOrGreater;
  }
}

bool IsBoldFont(uint32_t font_weight) {
  return font_weight > 500;
}

struct PathLengthDepthAndHash {
  // `path_length` caps at 100.
  uint8_t path_length;
  // `path_depth` caps at 5.
  uint8_t path_depth;
  // 10-bucket hash.
  uint8_t hash_bucket;
};

PathLengthDepthAndHash GetUrlPathLengthDepthAndHash(const GURL& target_url) {
  std::string_view path = target_url.path();
  int64_t path_length = path.length();
  path_length = ukm::GetLinearBucketMin(path_length, 10);
  // Truncate at 100 characters.
  path_length = std::min(path_length, static_cast<int64_t>(100));

  int num_slashes = std::ranges::count(path, '/');
  // Truncate at 5.
  int path_depth = std::min(num_slashes, 5);

  // 10-bucket hash of the URL's path.
  uint32_t hash = base::PersistentHash(path);
  uint8_t hash_bucket = hash % 10;

  return {static_cast<uint8_t>(path_length), static_cast<uint8_t>(path_depth),
          hash_bucket};
}

// Returns the minimum of the bucket that |value| belongs in, used for
// |ratio_distance_root_top|.
int GetLinearBucketForLinkLocation(int value) {
  return ukm::GetLinearBucketMin(static_cast<int64_t>(value), 10);
}

// Returns the minimum of the bucket that |value| belongs in, used for
// |ratio_area|.
int GetLinearBucketForRatioArea(int value) {
  return ukm::GetLinearBucketMin(static_cast<int64_t>(value), 5);
}

base::TimeDelta MLModelExecutionTimerStartDelay() {
  return base::Milliseconds(
      blink::features::kPreloadingModelTimerStartDelay.Get());
}

base::TimeDelta MLModelExecutionTimerInterval() {
  return base::Milliseconds(
      blink::features::kPreloadingModelTimerInterval.Get());
}

base::TimeDelta MLModelMaxHoverTime() {
  return blink::features::kPreloadingModelMaxHoverTime.Get();
}

void RecordMetricsForModelTraining(
    const PreloadingModelKeyedService::Inputs& inputs,
    ukm::SourceId ukm_source,
    std::optional<double> sampling_likelihood,
    bool is_accurate) {
  constexpr double kBucketSpacing = 1.3;

  const int sampling_likelihood_per_million =
      static_cast<int>(1'000'000 * sampling_likelihood.value_or(1.0));
  const int sampling_amount_bucket = ukm::GetExponentialBucketMin(
      1'000'000 - sampling_likelihood_per_million, kBucketSpacing);

  ukm::builders::Preloading_NavigationPredictorModelTrainingData builder(
      ukm_source);

  builder.SetSamplingAmount(sampling_amount_bucket);
  builder.SetIsAccurate(is_accurate);
  builder.SetContainsImage(inputs.contains_image);
  // Font size is already bucketed. See `FontSizeBucket`.
  builder.SetFontSize(inputs.font_size);
  builder.SetHasTextSibling(inputs.has_text_sibling);
  builder.SetIsBold(inputs.is_bold);
  builder.SetIsInIframe(inputs.is_in_iframe);
  builder.SetIsURLIncrementedByOne(inputs.is_url_incremented_by_one);
  builder.SetNavigationStartToLinkLoggedMs(ukm::GetExponentialBucketMin(
      inputs.navigation_start_to_link_logged.InMilliseconds(), kBucketSpacing));
  builder.SetPathDepth(inputs.path_depth);
  // Path length is already bucketed.
  DCHECK_EQ(
      inputs.path_length,
      ukm::GetLinearBucketMin(static_cast<int64_t>(inputs.path_length), 10));
  builder.SetPathLength(inputs.path_length);
  builder.SetPercentClickableArea(
      GetLinearBucketForRatioArea(inputs.percent_clickable_area));
  builder.SetPercentVerticalDistance(
      GetLinearBucketForLinkLocation(inputs.percent_vertical_distance));
  builder.SetSameHost(inputs.is_same_host);
  builder.SetHoverDwellTimeMs(ukm::GetExponentialBucketMin(
      inputs.hover_dwell_time.InMilliseconds(), kBucketSpacing));
  builder.SetPointerHoveringOverCount(ukm::GetExponentialBucketMin(
      inputs.pointer_hovering_over_count, kBucketSpacing));

  builder.Record(ukm::UkmRecorder::Get());
}

bool MaySendTraffic() {
  // TODO(b/290223353): Due to concerns about the amount of traffic this feature
  // would create on desktop, we'll just enable for a random sample of clients.
  // We should scale up the percentage of enabled clients.
  // Note that NavigationPredictor has functionality, unrelated to sending
  // requests, which continues to run regardless of this parameter.
  static const bool may_send_traffic = [] {
    // Use a fixed state for benchmarking.
    if (base::CommandLine::ForCurrentProcess()->HasSwitch(
            ::switches::kEnableBenchmarking)) {
#if BUILDFLAG(IS_ANDROID)
      return true;
#else
      return false;
#endif
    }

    int enabled_percent =
        blink::features::kPredictorTrafficClientEnabledPercent.Get();

    // This isn't user facing, so we'll just re-roll for each session.
    return base::RandIntInclusive(0, 99) < enabled_percent;
  }();

  return may_send_traffic;
}

}  // namespace

NavigationPredictor::AnchorElementData::AnchorElementData(
    blink::mojom::AnchorElementMetricsPtr metrics,
    base::TimeTicks first_report_timestamp)
    : ratio_distance_root_top(metrics->ratio_distance_root_top),
      ratio_area(static_cast<uint8_t>(metrics->ratio_area * 100)),
      is_in_iframe(metrics->is_in_iframe),
      contains_image(metrics->contains_image),
      is_same_host(metrics->is_same_host),
      is_url_incremented_by_one(metrics->is_url_incremented_by_one),
      has_text_sibling(metrics->has_text_sibling),
      is_bold_font(IsBoldFont(metrics->font_weight)),
      font_size(GetFontSizeFromPx(metrics->font_size_px)),
      target_url(metrics->target_url),
      first_report_timestamp(first_report_timestamp) {}

NavigationPredictor::AnchorElementData::~AnchorElementData() = default;

NavigationPredictor::NavigationPredictor(
    content::RenderFrameHost& render_frame_host,
    mojo::PendingReceiver<AnchorElementMetricsHost> receiver)
    : content::DocumentService<blink::mojom::AnchorElementMetricsHost>(
          render_frame_host,
          std::move(receiver)),
      clock_(base::DefaultTickClock::GetInstance()) {
  DETACH_FROM_SEQUENCE(sequence_checker_);
  // When using content::Page::IsPrimary, bfcache can cause returning a false in
  // the back/forward navigation. So, DCHECK only checks if current page is
  // prerendering until deciding how to handle bfcache navigations. See also
  // https://crbug.com/40193806.
  DCHECK(!IsPrerendering(render_frame_host));

  navigation_start_ = NowTicks();
  ukm_recorder_ = ukm::UkmRecorder::Get();
  ukm_source_id_ = render_frame_host.GetMainFrame()->GetPageUkmSourceId();
}

NavigationPredictor::~NavigationPredictor() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
}

void NavigationPredictor::Create(
    content::RenderFrameHost* render_frame_host,
    mojo::PendingReceiver<blink::mojom::AnchorElementMetricsHost> receiver) {
  CHECK(render_frame_host);
  CHECK(!IsPrerendering(*render_frame_host));

  if (!base::FeatureList::IsEnabled(blink::features::kNavigationPredictor)) {
    return;
  }

  // Only valid for the main frame.
  if (render_frame_host->GetParentOrOuterDocument()) {
    return;
  }

  content::WebContents* web_contents =
      content::WebContents::FromRenderFrameHost(render_frame_host);
  if (!web_contents) {
    return;
  }

  DCHECK(web_contents->GetBrowserContext());
  if (web_contents->GetBrowserContext()->IsOffTheRecord()) {
    return;
  }

  // The object is bound to the lifetime of the |render_frame_host| and the mojo
  // connection. See DocumentService for details.
  new NavigationPredictor(*render_frame_host, std::move(receiver));
}

NavigationPredictorMetricsDocumentData&
NavigationPredictor::GetNavigationPredictorMetricsDocumentData() const {
  // Create the `NavigationPredictorMetricsDocumentData` object for this
  // document if it doesn't already exist.
  NavigationPredictorMetricsDocumentData* data =
      NavigationPredictorMetricsDocumentData::GetOrCreateForCurrentDocument(
          &render_frame_host());
  DCHECK(data);
  return *data;
}

void NavigationPredictor::ReportNewAnchorElements(
    std::vector<blink::mojom::AnchorElementMetricsPtr> elements,
    const std::vector<uint32_t>& removed_elements) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK(base::FeatureList::IsEnabled(blink::features::kNavigationPredictor));
  DCHECK(!IsPrerendering(render_frame_host()));

  // Create the AnchorsData object for this WebContents if it doesn't already
  // exist. Note that NavigationPredictor only runs on the main frame, but get
  // reports for links from all same-process iframes.
  NavigationPredictorMetricsDocumentData::AnchorsData& data =
      GetNavigationPredictorMetricsDocumentData().GetAnchorsData();
  const GURL document_url =
      render_frame_host().GetLastCommittedURL().GetWithoutRef();
  if (!document_url.is_valid()) {
    return;
  }
  std::vector<GURL> new_predictions;
  const base::TimeTicks now = NowTicks();
  for (auto& element : elements) {
    AnchorId anchor_id(element->anchor_id);
    if (anchors_.find(anchor_id) != anchors_.end()) {
      continue;
    }

    auto [id_it, id_inserted] = tracked_anchor_id_to_index_.insert(
        {anchor_id, tracked_anchor_id_to_index_.size()});

    // We may have seen this anchor before, but it was removed from the page, so
    // we stopped tracking it. We'll start tracking it again, but not treat it
    // as a new anchor.
    if (id_inserted) {
      data.number_of_anchors_++;
      if (element->contains_image) {
        data.number_of_anchors_contains_image_++;
      }
      if (element->is_url_incremented_by_one) {
        data.number_of_anchors_url_incremented_++;
      }
      if (element->is_in_iframe) {
        data.number_of_anchors_in_iframe_++;
      }
      if (element->is_same_host) {
        data.number_of_anchors_same_host_++;
      }
      data.viewport_height_ = element->viewport_size.height();
      data.viewport_width_ = element->viewport_size.width();
      data.total_clickable_space_ += element->ratio_area * 100;
      data.link_locations_.push_back(
          element->ratio_distance_top_to_visible_top);

      // Collect the target URL if it is new, without ref (# fragment).
      GURL target_url = element->target_url.GetWithoutRef();
      if (target_url != document_url) {
        auto [url_it, url_inserted] =
            predicted_urls_.insert(base::FastHash(target_url.spec()));
        if (url_inserted) {
          new_predictions.push_back(std::move(target_url));
        }
      }
    }

    anchors_.emplace(std::piecewise_construct, std::forward_as_tuple(anchor_id),
                     std::forward_as_tuple(std::move(element), now));
  }

  for (uint32_t removed_element : removed_elements) {
    AnchorId anchor_id(removed_element);
    // Stop tracking removed elements to conserve memory. We leave an entry in
    // `tracked_anchor_id_to_index_` to detect if a removed element is re-added
    // to the page.
    anchors_.erase(anchor_id);
  }

  if (!new_predictions.empty() && MaySendTraffic()) {
    NavigationPredictorKeyedService* service =
        NavigationPredictorKeyedServiceFactory::GetForProfile(
            Profile::FromBrowserContext(
                render_frame_host().GetBrowserContext()));
    DCHECK(service);
    content::WebContents* web_contents =
        content::WebContents::FromRenderFrameHost(&render_frame_host());

    service->OnPredictionUpdated(
        web_contents, document_url,
        NavigationPredictorKeyedService::PredictionSource::
            kAnchorElementsParsedFromWebPage,
        new_predictions);
  }
}

void NavigationPredictor::OnPreloadingHeuristicsModelDone(
    GURL url,
    PreloadingModelKeyedService::Result result) {
  if (!result.has_value()) {
    return;
  }
  render_frame_host().OnPreloadingHeuristicsModelDone(url, result.value());
}

void NavigationPredictor::ProcessPointerEventUsingMLModel(
    blink::mojom::AnchorElementPointerEventForMLModelPtr pointer_event) {
  // Find anchor elements data.
  AnchorId anchor_id(pointer_event->anchor_id);
  auto it = anchors_.find(anchor_id);
  if (it == anchors_.end()) {
    return;
  }

  AnchorElementData& anchor = it->second;
  switch (pointer_event->user_interaction_event_type) {
    case blink::mojom::AnchorElementUserInteractionEventForMLModelType::
        kPointerOut: {
      anchor.pointer_over_timestamp.reset();
      ml_model_candidate_.reset();
      break;
    }
    case blink::mojom::AnchorElementUserInteractionEventForMLModelType::
        kPointerOver: {
      // Currently we only process mouse based events.
      if (!pointer_event->is_mouse) {
        return;
      }
      // Ignore anchors pointing to the same document.
      if (IsTargetURLTheSameAsDocument(anchor)) {
        return;
      }

      anchor.pointer_over_timestamp = NowTicks();
      anchor.pointer_hovering_over_count++;
      ml_model_candidate_ = anchor_id;
      if (!ml_model_execution_timer_.IsRunning()) {
        ml_model_execution_timer_.Start(
            FROM_HERE, MLModelExecutionTimerStartDelay(),
            base::BindOnce(&NavigationPredictor::OnMLModelExecutionTimerFired,
                           base::Unretained(this)));
      }
      break;
    }
    default:
      break;
  }
}

void NavigationPredictor::OnMLModelExecutionTimerFired() {
  // Check whether preloading is enabled or not.
  Profile* profile =
      Profile::FromBrowserContext(render_frame_host().GetBrowserContext());
  if (prefetch::IsSomePreloadingEnabled(*profile->GetPrefs()) !=
      content::PreloadingEligibility::kEligible) {
    return;
  }

  // Execute the model.
  PreloadingModelKeyedService* model_service =
      PreloadingModelKeyedServiceFactory::GetForProfile(profile);
  if (!model_service) {
    return;
  }

  if (!ml_model_candidate_.has_value()) {
    return;
  }
  auto it = anchors_.find(ml_model_candidate_.value());
  if (it == anchors_.end()) {
    return;
  }

  AnchorElementData& anchor = it->second;

  PreloadingModelKeyedService::Inputs inputs;
  inputs.contains_image = anchor.contains_image;
  inputs.font_size = anchor.font_size;
  inputs.has_text_sibling = anchor.has_text_sibling;
  inputs.is_bold = anchor.is_bold_font;
  inputs.is_in_iframe = anchor.is_in_iframe;
  inputs.is_url_incremented_by_one = anchor.is_url_incremented_by_one;
  inputs.navigation_start_to_link_logged =
      anchor.first_report_timestamp - navigation_start_;
  auto path_info = GetUrlPathLengthDepthAndHash(anchor.target_url);
  inputs.path_length = path_info.path_length;
  inputs.path_depth = path_info.path_depth;
  inputs.percent_clickable_area = anchor.ratio_area;
  inputs.percent_vertical_distance =
      static_cast<int>(anchor.ratio_distance_root_top * 100);

  inputs.is_same_host = anchor.is_same_host;
  auto to_timedelta = [this](std::optional<base::TimeTicks> ts) {
    return ts.has_value() ? NowTicks() - ts.value() : base::TimeDelta();
  };
  // TODO(329691634): Using the real viewport entry time for
  // `entered_viewport_to_left_viewport` produces low quality results.
  // We could remove it from the model, if we can't get this to be useful.
  inputs.entered_viewport_to_left_viewport = base::TimeDelta();
  inputs.hover_dwell_time = to_timedelta(anchor.pointer_over_timestamp);
  inputs.pointer_hovering_over_count = anchor.pointer_hovering_over_count;
  if (model_score_callback_) {
    std::move(model_score_callback_).Run(inputs);
  }

  content::PreloadingData* preloading_data =
      content::PreloadingData::GetOrCreateForWebContents(
          content::WebContents::FromRenderFrameHost(&render_frame_host()));
  preloading_data->OnPreloadingHeuristicsModelInput(
      anchor.target_url,
      base::BindOnce(&RecordMetricsForModelTraining, inputs,
                     render_frame_host().GetPageUkmSourceId()));
  model_service->Score(
      &scoring_model_task_tracker_, inputs,
      base::BindOnce(&NavigationPredictor::OnPreloadingHeuristicsModelDone,
                     weak_ptr_factory_.GetWeakPtr(), anchor.target_url));

  if (inputs.hover_dwell_time < MLModelMaxHoverTime() &&
      !ml_model_execution_timer_.IsRunning()) {
    ml_model_execution_timer_.Start(
        FROM_HERE, MLModelExecutionTimerInterval(),
        base::BindOnce(&NavigationPredictor::OnMLModelExecutionTimerFired,
                       base::Unretained(this)));
  }
}

void NavigationPredictor::SetModelScoreCallbackForTesting(
    ModelScoreCallbackForTesting callback) {
  model_score_callback_ = std::move(callback);
}

// static
bool NavigationPredictor::disable_renderer_metric_sending_delay_for_testing_ =
    false;

// static
void NavigationPredictor::DisableRendererMetricSendingDelayForTesting() {
  disable_renderer_metric_sending_delay_for_testing_ = true;
}

void NavigationPredictor::ShouldSkipUpdateDelays(
    ShouldSkipUpdateDelaysCallback callback) {
  std::move(callback).Run(disable_renderer_metric_sending_delay_for_testing_);
}

void NavigationPredictor::ReportAnchorElementClick(
    blink::mojom::AnchorElementClickPtr click) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK(base::FeatureList::IsEnabled(blink::features::kNavigationPredictor));
  DCHECK(!IsPrerendering(render_frame_host()));

  navigation_start_to_click_ = click->navigation_start_to_click;

  clicked_count_++;
  if (clicked_count_ > kMaxClicksTracked) {
    return;
  }

  if (!ukm_recorder_) {
    return;
  }

  auto& navigation_predictor_metrics_data =
      GetNavigationPredictorMetricsDocumentData();
  // An anchor index of -1 indicates that we are not going to log details about
  // the anchor that was clicked.
  int anchor_index = -1;
  AnchorId anchor_id(click->anchor_id);
  auto index_it = tracked_anchor_id_to_index_.find(anchor_id);
  if (index_it != tracked_anchor_id_to_index_.end()) {
    anchor_index = index_it->second;

    // Record PreloadOnHover.HoverTakenMs and PreloadOnHover.PointerDownTakenMs
    // to UKM. We should make sure that we only process the `sampled` anchor
    // elements here, as `AnchorElementMetricsSender` reports all new anchor
    // elements to `NavigationPredictor`, but only reports user interactions
    // events for the  `sampled` anchors. Otherwise, we will end up creating
    // empty `UserInteractionsData` UKM records.
    auto& user_interactions =
        navigation_predictor_metrics_data.GetUserInteractionsData();
    auto user_interaction_it = user_interactions.find(index_it->second);
    if (user_interaction_it != user_interactions.end()) {
      auto& user_interaction = user_interaction_it->second;

      // navigation_start_to_click_ is set to click->navigation_start_to_click
      // and should always have a value.
      CHECK(navigation_start_to_click_.has_value());
      if (user_interaction.last_navigation_start_to_pointer_over.has_value() ||
          user_interaction.last_navigation_start_to_last_pointer_down
              .has_value()) {
        NavigationPredictorMetricsDocumentData::PreloadOnHoverData
            preload_on_hover;
        preload_on_hover.taken = true;
        if (user_interaction.last_navigation_start_to_pointer_over
                .has_value()) {
          // `hover_dwell_time` measures the time delta from the last mouse over
          // event to the last mouse click event.
          preload_on_hover.hover_dwell_time =
              navigation_start_to_click_.value() -
              user_interaction.last_navigation_start_to_pointer_over.value();
        }
        if (user_interaction.last_navigation_start_to_last_pointer_down
                .has_value()) {
          // `pointer_down_duration` measures the time delta from the last mouse
          // down event to the last mouse click event.
          preload_on_hover.pointer_down_duration =
              navigation_start_to_click_.value() -
              user_interaction.last_navigation_start_to_last_pointer_down
                  .value();
          user_interaction.last_navigation_start_to_last_pointer_down.reset();
        }
        navigation_predictor_metrics_data.AddPreloadOnHoverData(
            std::move(preload_on_hover));
      }
    }
  }

  NavigationPredictorMetricsDocumentData::PageLinkClickData page_link_click;
  page_link_click.anchor_element_index_ = anchor_index;
  auto it = anchors_.find(anchor_id);
  if (it != anchors_.end()) {
    page_link_click.href_unchanged_ =
        (it->second.target_url == click->target_url);
  }
  navigation_start_to_click_ = click->navigation_start_to_click;
  // navigation_start_to_click_ is set to click->navigation_start_to_click and
  // should always have a value.
  CHECK(navigation_start_to_click_.has_value());

  navigation_predictor_metrics_data.SetNavigationStartToClick(
      navigation_start_to_click_.value());

  page_link_click.navigation_start_to_link_clicked_ =
      navigation_start_to_click_.value();
  navigation_predictor_metrics_data.AddPageLinkClickData(
      std::move(page_link_click));
}

void NavigationPredictor::ReportAnchorElementsLeftViewport(
    std::vector<blink::mojom::AnchorElementLeftViewportPtr> elements) {
  auto& user_interactions =
      GetNavigationPredictorMetricsDocumentData().GetUserInteractionsData();
  for (const auto& element : elements) {
    auto index_it =
        tracked_anchor_id_to_index_.find(AnchorId(element->anchor_id));
    if (index_it == tracked_anchor_id_to_index_.end()) {
      continue;
    }
    auto& user_interaction = user_interactions[index_it->second];
    user_interaction.is_in_viewport = false;
    user_interaction.last_navigation_start_to_entered_viewport.reset();
    user_interaction.max_time_in_viewport = std::max(
        user_interaction.max_time_in_viewport.value_or(base::TimeDelta()),
        element->time_in_viewport);
    user_interaction.percent_vertical_position.reset();
    user_interaction.percent_distance_from_pointer_down.reset();
  }
}

void NavigationPredictor::ReportAnchorElementsPositionUpdate(
    std::vector<blink::mojom::AnchorElementPositionUpdatePtr> elements) {
  if (!base::FeatureList::IsEnabled(
          blink::features::kNavigationPredictorNewViewportFeatures)) {
    ReportBadMessageAndDeleteThis(
        "ReportAnchorElementsPositionUpdate should only be called with "
        "kNavigationPredictorNewViewportFeatures enabled.");
    return;
  }

  auto& user_interactions =
      GetNavigationPredictorMetricsDocumentData().GetUserInteractionsData();
  for (const auto& element : elements) {
    auto index_it =
        tracked_anchor_id_to_index_.find(AnchorId(element->anchor_id));
    if (index_it == tracked_anchor_id_to_index_.end()) {
      continue;
    }
    auto& user_interaction = user_interactions[index_it->second];
    user_interaction.percent_vertical_position =
        base::saturated_cast<int>(element->vertical_position_ratio * 100);
    if (element->distance_from_pointer_down_ratio.has_value()) {
      user_interaction.percent_distance_from_pointer_down =
          base::saturated_cast<int>(
              element->distance_from_pointer_down_ratio.value() * 100);
    }
  }
}

void NavigationPredictor::ReportAnchorElementPointerDataOnHoverTimerFired(
    blink::mojom::AnchorElementPointerDataOnHoverTimerFiredPtr msg) {
  if (!msg->pointer_data || !msg->pointer_data->is_mouse_pointer) {
    return;
  }

  auto& user_interactions =
      GetNavigationPredictorMetricsDocumentData().GetUserInteractionsData();
  auto index_it = tracked_anchor_id_to_index_.find(AnchorId(msg->anchor_id));
  if (index_it == tracked_anchor_id_to_index_.end()) {
    return;
  }

  auto& user_interaction = user_interactions[index_it->second];
  user_interaction.mouse_velocity = msg->pointer_data->mouse_velocity;
  user_interaction.mouse_acceleration = msg->pointer_data->mouse_acceleration;
}

void NavigationPredictor::ReportAnchorElementPointerOver(
    blink::mojom::AnchorElementPointerOverPtr pointer_over_event) {
  auto& user_interactions =
      GetNavigationPredictorMetricsDocumentData().GetUserInteractionsData();
  auto index_it =
      tracked_anchor_id_to_index_.find(AnchorId(pointer_over_event->anchor_id));
  if (index_it == tracked_anchor_id_to_index_.end()) {
    return;
  }

  auto& user_interaction = user_interactions[index_it->second];
  if (!user_interaction.is_hovered) {
    user_interaction.pointer_hovering_over_count++;
  }
  user_interaction.is_hovered = true;
  user_interaction.last_navigation_start_to_pointer_over =
      pointer_over_event->navigation_start_to_pointer_over;
}

void NavigationPredictor::ReportAnchorElementPointerOut(
    blink::mojom::AnchorElementPointerOutPtr hover_event) {
  auto& navigation_predictor_metrics_data =
      GetNavigationPredictorMetricsDocumentData();
  auto& user_interactions =
      navigation_predictor_metrics_data.GetUserInteractionsData();
  auto index_it =
      tracked_anchor_id_to_index_.find(AnchorId(hover_event->anchor_id));
  if (index_it == tracked_anchor_id_to_index_.end()) {
    return;
  }

  auto& user_interaction = user_interactions[index_it->second];
  // Record PreloadOnHover.HoverNotTakenMs and
  // PreloadOnHover.MouseDownNotTakenMs to UKM.
  NavigationPredictorMetricsDocumentData::PreloadOnHoverData preload_on_hover;
  preload_on_hover.taken = false;
  preload_on_hover.hover_dwell_time = hover_event->hover_dwell_time;
  if (user_interaction.last_navigation_start_to_last_pointer_down.has_value() &&
      user_interaction.last_navigation_start_to_pointer_over.has_value()) {
    preload_on_hover.pointer_down_duration =
        user_interaction.last_navigation_start_to_pointer_over.value() +
        hover_event->hover_dwell_time -
        user_interaction.last_navigation_start_to_last_pointer_down.value();
    user_interaction.last_navigation_start_to_last_pointer_down.reset();
  }
  navigation_predictor_metrics_data.AddPreloadOnHoverData(
      std::move(preload_on_hover));

  // Update user interactions.
  user_interaction.is_hovered = false;
  user_interaction.last_navigation_start_to_pointer_over.reset();
  user_interaction.max_hover_dwell_time = std::max(
      hover_event->hover_dwell_time,
      user_interaction.max_hover_dwell_time.value_or(base::TimeDelta()));
}

void NavigationPredictor::ReportAnchorElementPointerDown(
    blink::mojom::AnchorElementPointerDownPtr pointer_down_event) {
  auto index_it =
      tracked_anchor_id_to_index_.find(AnchorId(pointer_down_event->anchor_id));
  if (index_it == tracked_anchor_id_to_index_.end()) {
    return;
  }

  auto& user_interactions =
      GetNavigationPredictorMetricsDocumentData().GetUserInteractionsData();
  auto& user_interaction = user_interactions[index_it->second];
  user_interaction.last_navigation_start_to_last_pointer_down =
      pointer_down_event->navigation_start_to_pointer_down;
}

void NavigationPredictor::ReportAnchorElementsEnteredViewport(
    std::vector<blink::mojom::AnchorElementEnteredViewportPtr> elements) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK(base::FeatureList::IsEnabled(blink::features::kNavigationPredictor));
  DCHECK(!IsPrerendering(render_frame_host()));

  if (elements.empty()) {
    return;
  }
  auto& navigation_predictor_metrics_data =
      GetNavigationPredictorMetricsDocumentData();
  auto& user_interactions =
      navigation_predictor_metrics_data.GetUserInteractionsData();
  for (const auto& element : elements) {
    AnchorId anchor_id(element->anchor_id);
    auto index_it = tracked_anchor_id_to_index_.find(anchor_id);
    if (index_it == tracked_anchor_id_to_index_.end()) {
      // We're not tracking this element, no need to generate a
      // NavigationPredictorAnchorElementMetrics record.
      continue;
    }
    auto& user_interaction = user_interactions[index_it->second];
    if (!user_interaction.is_in_viewport) {
      user_interaction.entered_viewport_count++;
    }
    user_interaction.is_in_viewport = true;
    user_interaction.last_navigation_start_to_entered_viewport =
        element->navigation_start_to_entered_viewport;

    auto anchor_it = anchors_.find(anchor_id);
    if (anchor_it == anchors_.end()) {
      // We don't know about this anchor, likely because at its first paint,
      // AnchorElementMetricsSender didn't send it to NavigationPredictor.
      // Reasons could be that the link had non-HTTP scheme, the anchor had
      // zero width/height, etc.
      continue;
    }
    const AnchorElementData& anchor = anchor_it->second;
    // Collect the target URL if it is new, without ref (# fragment).
    if (IsTargetURLTheSameAsDocument(anchor)) {
      // Ignore anchors pointing to the same document.
      continue;
    }

    if (!ukm_recorder_) {
      continue;
    }

    NavigationPredictorMetricsDocumentData::AnchorElementMetricsData metrics;

    metrics.is_in_iframe_ = anchor.is_in_iframe;
    metrics.is_url_incremented_by_one_ = anchor.is_url_incremented_by_one;
    metrics.contains_image_ = anchor.contains_image;
    metrics.is_same_host_ = anchor.is_same_host;
    metrics.has_text_sibling_ = anchor.has_text_sibling;
    metrics.is_bold_ = anchor.is_bold_font;
    metrics.navigation_start_to_link_logged =
        element->navigation_start_to_entered_viewport;

    metrics.font_size_bucket_ = anchor.font_size;
    auto path_info = GetUrlPathLengthDepthAndHash(anchor.target_url);
    metrics.path_length_ = path_info.path_length;
    metrics.path_depth_ = path_info.path_depth;
    metrics.bucketed_path_hash_ = path_info.hash_bucket;

    int percent_ratio_area = anchor.ratio_area;
    metrics.percent_clickable_area_ =
        GetLinearBucketForRatioArea(percent_ratio_area);

    int percent_ratio_distance_root_top =
        static_cast<int>(anchor.ratio_distance_root_top * 100);
    metrics.percent_vertical_distance_ =
        GetLinearBucketForLinkLocation(percent_ratio_distance_root_top);

    navigation_predictor_metrics_data.AddAnchorElementMetricsData(
        index_it->second, std::move(metrics));
  }
}

bool NavigationPredictor::IsTargetURLTheSameAsDocument(
    const AnchorElementData& anchor) {
  return render_frame_host().GetLastCommittedURL().EqualsIgnoringRef(
      anchor.target_url);
}
