// Copyright 2023 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/feed/core/v2/view_demotion.h"

#include <algorithm>
#include <map>
#include <ostream>
#include <tuple>
#include <vector>

#include "base/check_op.h"
#include "base/containers/flat_set.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/metrics/histogram_functions.h"
#include "base/time/time.h"
#include "components/feed/core/proto/v2/store.pb.h"
#include "components/feed/core/v2/config.h"
#include "components/feed/core/v2/feed_store.h"
#include "components/feed/core/v2/feed_stream.h"
#include "components/feed/core/v2/feedstore_util.h"
#include "components/feed/feed_feature_list.h"

namespace feed {
namespace {
base::TimeDelta kMaxViewAge = base::Hours(72);

bool IsEnabled(FeedStream& feed_stream) {
  return base::FeatureList::IsEnabled(kFeedSignedOutViewDemotion) &&
         !feed_stream.IsSignedIn();
}
}  // namespace

namespace internal {
DocViewDigest CreateDigest(std::vector<feedstore::DocView> all_views) {
  const base::Time now = base::Time::Now();
  const base::Time too_old = now - kMaxViewAge;
  DocViewDigest digest;

  // Remove all views that are too old.
  std::erase_if(all_views, [&](const feedstore::DocView& view) {
    base::Time view_time =
        feedstore::FromTimestampMillis(view.view_time_millis());
    if (view_time > now || view_time < too_old) {
      digest.old_doc_views.push_back(view);
      return true;
    }
    return false;
  });

  // Note that all_views will be sorted by the datastore key, which is prefixed
  // with the docid, and therefore we can assume that all views for a given
  // docid are adjacent in this list. We take advantage of this here.
  for (const feedstore::DocView& view : all_views) {
    if (digest.doc_view_counts.empty() ||
        digest.doc_view_counts.back().docid != view.docid()) {
      digest.doc_view_counts.push_back(DocViewCount{view.docid(), 1});
    } else {
      ++digest.doc_view_counts.back().view_count;
    }
  }

  // If there are too many recent entries, remove docids which have been viewed
  // least recently.
  if (digest.doc_view_counts.size() > GetFeedConfig().max_docviews_to_send) {
    // Get the most recent view time for each doc, sort by time and select the
    // docids to remove.
    size_t remove_count =
        digest.doc_view_counts.size() - GetFeedConfig().max_docviews_to_send;

    base::UmaHistogramCounts100(
        "ContentSuggestions.Feed.DroppedDocumentViewCount", remove_count);

    std::vector<std::pair<int64_t, uint64_t>> latest_view_to_docid;
    for (const feedstore::DocView& view : all_views) {
      if (latest_view_to_docid.empty() ||
          latest_view_to_docid.back().second != view.docid()) {
        latest_view_to_docid.emplace_back(view.view_time_millis(),
                                          view.docid());
      } else {
        auto& view_time = latest_view_to_docid.back().first;
        view_time = std::max(view_time, view.view_time_millis());
      }
    }

    std::ranges::sort(latest_view_to_docid);

    std::vector<uint64_t> docids_to_remove;
    docids_to_remove.reserve(remove_count);
    for (auto iter = latest_view_to_docid.begin();
         iter != latest_view_to_docid.begin() + remove_count; ++iter) {
      docids_to_remove.push_back(iter->second);
    }

    // Finally, remove the old docids, and add to old_doc_views.
    base::flat_set<uint64_t> docids_to_remove_set(std::move(docids_to_remove));
    std::erase_if(digest.doc_view_counts, [&](const DocViewCount& view_count) {
      return docids_to_remove_set.contains(view_count.docid);
    });
    for (const feedstore::DocView& view : all_views) {
      if (docids_to_remove_set.contains(view.docid())) {
        digest.old_doc_views.push_back(view);
      }
    }
    DCHECK_EQ(digest.doc_view_counts.size(),
              GetFeedConfig().max_docviews_to_send);
  }

  base::UmaHistogramCounts1000(
      "ContentSuggestions.Feed.DocumentViewSendCount1000",
      digest.doc_view_counts.size());
  return digest;
}
}  // namespace internal

bool DocViewCount::operator==(const DocViewCount& rhs) const {
  return std::tie(docid, view_count) == std::tie(rhs.docid, rhs.view_count);
}

void ReadDocViewDigestIfEnabled(
    FeedStream& feed_stream,
    base::OnceCallback<void(DocViewDigest)> callback) {
  if (!IsEnabled(feed_stream)) {
    std::move(callback).Run({});
    return;
  }
  auto callback_wrapper = [](base::OnceCallback<void(DocViewDigest)> callback,
                             std::vector<feedstore::DocView> all_views) {
    std::move(callback).Run(internal::CreateDigest(std::move(all_views)));
  };
  feed_stream.GetStore().ReadDocViews(
      base::BindOnce(callback_wrapper, std::move(callback)));
}

void RemoveOldDocViews(const DocViewDigest& doc_view_digest,
                       FeedStore& feed_store) {
  feed_store.RemoveDocViews(doc_view_digest.old_doc_views);
}

void WriteDocViewIfEnabled(FeedStream& feed_stream, uint64_t docid) {
  if (!IsEnabled(feed_stream)) {
    return;
  }
  feed_stream.GetStore().WriteDocView(feedstore::CreateDocView(docid));
}

DocViewDigest::DocViewDigest() = default;
DocViewDigest::~DocViewDigest() = default;
DocViewDigest::DocViewDigest(const DocViewDigest&) = default;
DocViewDigest::DocViewDigest(DocViewDigest&&) = default;
DocViewDigest& DocViewDigest::operator=(const DocViewDigest&) = default;
DocViewDigest& DocViewDigest::operator=(DocViewDigest&&) = default;

std::ostream& operator<<(std::ostream& os, const DocViewCount& doc_view_count) {
  return os << "DocViewCount{docid: " << doc_view_count.docid
            << " count: " << doc_view_count.view_count << " }";
}

}  // namespace feed
