// Copyright 2020 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/reporting/client/report_queue_impl.h"

#include <memory>
#include <optional>
#include <queue>
#include <string>
#include <utility>

#include "base/check.h"
#include "base/check_op.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/memory/ptr_util.h"
#include "base/memory/scoped_refptr.h"
#include "base/memory/weak_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/notreached.h"
#include "base/sequence_checker.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/task/bind_post_task.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/time/time.h"
#include "base/types/expected.h"
#include "components/reporting/client/report_queue_configuration.h"
#include "components/reporting/proto/synced/metric_data.pb.h"
#include "components/reporting/proto/synced/record.pb.h"
#include "components/reporting/proto/synced/record_constants.pb.h"
#include "components/reporting/storage/storage_module_interface.h"
#include "components/reporting/util/reporting_errors.h"
#include "components/reporting/util/status.h"
#include "components/reporting/util/statusor.h"

namespace reporting {
namespace {

// UTC time of 2122-01-01T00:00:00Z since Unix epoch 1970-01-01T00:00:00Z in
// microseconds.
constexpr int64_t kTime2122 = 4'796'668'800'000'000;

// Returns true if record is allowed to go to `destination`. Returns false
// otherwise.
static bool RecordMayGoToDestination(const std::string& record_data,
                                     Destination destination) {
  // All records sent to destination *_METRIC must be MetricData
  // protos due to the way the server is implemented.
  if (destination == Destination::EVENT_METRIC ||
      destination == Destination::TELEMETRY_METRIC ||
      destination == Destination::INFO_METRIC) {
    MetricData metric_data;
    const bool is_metric_data =
        metric_data.ParseFromString(record_data) &&
        (metric_data.has_event_data() || metric_data.has_telemetry_data() ||
         metric_data.has_info_data());
    LOG_IF(ERROR, !is_metric_data)
        << "Only MetricData records may be enqueued with destinations: "
           "EVENT_METRIC, TELEMETRY_METRIC, or INFO_METRIC";
    return is_metric_data;
  }
  return true;
}

StatusOr<Record> ProduceRecord(std::string dm_token,
                               Destination destination,
                               int64_t reserved_space,
                               std::optional<SourceInfo> source_info,
                               ReportQueue::RecordProducer record_producer) {
  // Generate record data.
  auto record_result = std::move(record_producer).Run();
  if (!record_result.has_value()) {
    return base::unexpected(record_result.error());
  }

  CHECK(RecordMayGoToDestination(record_result.value(), destination));

  // Augment data.
  Record record;
  *record.mutable_data() = std::move(record_result.value());
  record.set_destination(destination);
  if (reserved_space > 0L) {
    record.set_reserved_space(reserved_space);
  }

  // Additional record augmentation for keeping local record copy.
  // Note: that must be done before calling `storage->AddRecord` below,
  // because later the handler might call it with no need to set this flag.
  switch (destination) {
    case LOG_UPLOAD:
      // It would be better to base the decision on `upload_settings` presence
      // in the event, but that would require protobuf reflecion, that is not
      // included in Chromium build. So instead we just use `destination`.
      record.set_needs_local_unencrypted_copy(true);
      break;
    default:  // Do nothing.
      break;
  }

  // |record| with no DM token is assumed to be associated with device DM
  // token.
  if (!dm_token.empty()) {
    *record.mutable_dm_token() = std::move(dm_token);
  }

  // Augment source info if available.
  if (source_info.has_value()) {
    *record.mutable_source_info() = std::move(source_info.value());
  }

  // Calculate timestamp in microseconds - to match Spanner expectations.
  const int64_t time_since_epoch_us =
      base::Time::Now().InMillisecondsSinceUnixEpoch() *
      base::Time::kMicrosecondsPerMillisecond;
  if (time_since_epoch_us > kTime2122) {
    // Unusual timestamp. Reject the record even though the record is good
    // otherwise, because we can't obtain a reasonable timestamp. We have this
    // code block here because server very occasionally detects very large
    // timestamps. The reason could come from occasional irregular system
    // time. Filtering out irregular timestamps here should address the
    // problem without leaving timestamp-related bugs in the ERP undiscovered
    // (should there be any).
    base::UmaHistogramBoolean("Browser.ERP.UnusualEnqueueTimestamp", true);
    return base::unexpected(Status(
        error::FAILED_PRECONDITION,
        base::StrCat(
            {"Abnormal system timestamp obtained. Microseconds since epoch: ",
             base::NumberToString(time_since_epoch_us)})));
  }
  record.set_timestamp_us(time_since_epoch_us);
  return std::move(record);
}

// Calls |record_producer|, checks the result and in case of success, forwards
// it to the storage. In production code should be invoked asynchronously, on
// a thread pool (no synchronization expected).
void AddRecordToStorage(scoped_refptr<StorageModuleInterface> storage,
                        Priority priority,
                        WrappedRateLimiter::AsyncAcquireCb acquire_cb,
                        std::string dm_token,
                        Destination destination,
                        int64_t reserved_space,
                        std::optional<SourceInfo> source_info,
                        ReportQueue::RecordProducer record_producer,
                        StorageModuleInterface::EnqueueCallback callback) {
  auto record_result =
      ProduceRecord(dm_token, destination, reserved_space, source_info,
                    std::move(record_producer));

  if (!record_result.has_value()) {
    std::move(callback).Run(record_result.error());
    return;
  }

  const auto record_size = record_result.value().ByteSizeLong();

  // Prepare `Storage::AddRecord` as a callback.
  auto add_record_cb =
      base::BindOnce(&StorageModuleInterface::AddRecord, storage, priority,
                     std::move(record_result.value()));

  // Rate-limit event, if required.
  if (!acquire_cb) {
    // No rate limiter, just add resulting Record to the storage.
    std::move(add_record_cb).Run(std::move(callback));
    return;
  }

  // Add Record only if rate limiter approves.
  acquire_cb.Run(
      record_size,
      base::BindOnce(
          [](size_t record_size,
             base::OnceCallback<void(StorageModuleInterface::EnqueueCallback
                                         callback)> add_record_cb,
             StorageModuleInterface::EnqueueCallback callback, bool acquired) {
            if (!acquired) {
              std::move(callback).Run(
                  Status(error::OUT_OF_RANGE,
                         base::StrCat({"Event size ",
                                       base::NumberToString(record_size),
                                       " rejected by rate limiter"})));
              return;
            }
            // Add resulting Record to the storage.
            std::move(add_record_cb).Run(std::move(callback));
          },
          record_size, std::move(add_record_cb), std::move(callback)));
}
}  // namespace

void ReportQueueImpl::Create(
    std::unique_ptr<ReportQueueConfiguration> config,
    scoped_refptr<StorageModuleInterface> storage,
    base::OnceCallback<void(StatusOr<std::unique_ptr<ReportQueue>>)> cb) {
  std::move(cb).Run(base::WrapUnique<ReportQueueImpl>(
      new ReportQueueImpl(std::move(config), storage)));
}

ReportQueueImpl::ReportQueueImpl(
    std::unique_ptr<ReportQueueConfiguration> config,
    scoped_refptr<StorageModuleInterface> storage)
    : config_(std::move(config)), storage_(storage) {
  CHECK(config_);
}

ReportQueueImpl::~ReportQueueImpl() = default;

void ReportQueueImpl::AddProducedRecord(RecordProducer record_producer,
                                        Priority priority,
                                        EnqueueCallback callback) const {
  const Status status = config_->CheckPolicy();
  if (!status.ok()) {
    std::move(callback).Run(status);
    return;
  }

  if (priority == Priority::UNDEFINED_PRIORITY) {
    std::move(callback).Run(
        Status(error::INVALID_ARGUMENT, "Priority must be defined"));
    return;
  }

  // Execute |record_producer| on arbitrary thread, analyze the result and send
  // it to the Storage, returning with the callback.
  base::ThreadPool::PostTask(
      FROM_HERE, {base::TaskPriority::BEST_EFFORT},
      base::BindOnce(&AddRecordToStorage, storage_, priority,
                     config_->is_event_allowed_cb(), config_->dm_token(),
                     config_->destination(), config_->reserved_space(),
                     config_->source_info(), std::move(record_producer),
                     // EnqueueCallback must be run on the current thread, we
                     // need to bind to make sure it's posted to correct thread
                     // from the ThreadPool.
                     base::BindPostTaskToCurrentDefault(std::move(callback))));
}

void ReportQueueImpl::Flush(Priority priority, FlushCallback callback) {
  storage_->Flush(priority, std::move(callback));
}

base::OnceCallback<void(StatusOr<std::unique_ptr<ReportQueue>>)>
ReportQueueImpl::PrepareToAttachActualQueue() const {
  NOTREACHED();
}

Destination ReportQueueImpl::GetDestination() const {
  return config_->destination();
}

// Implementation of SpeculativeReportQueueImpl::PendingRecordProducer

SpeculativeReportQueueImpl::PendingRecordProducer::PendingRecordProducer(
    RecordProducer producer,
    EnqueueCallback callback,
    Priority priority)
    : record_producer(std::move(producer)),
      record_callback(std::move(callback)),
      record_priority(priority) {}

SpeculativeReportQueueImpl::PendingRecordProducer::PendingRecordProducer(
    PendingRecordProducer&& other)
    : record_producer(std::move(other.record_producer)),
      record_callback(std::move(other.record_callback)),
      record_priority(other.record_priority) {}

SpeculativeReportQueueImpl::PendingRecordProducer::~PendingRecordProducer() =
    default;

SpeculativeReportQueueImpl::PendingRecordProducer&
SpeculativeReportQueueImpl::PendingRecordProducer::operator=(
    PendingRecordProducer&& other) {
  record_producer = std::move(other.record_producer);
  record_callback = std::move(other.record_callback);
  record_priority = other.record_priority;
  return *this;
}

// static
std::unique_ptr<SpeculativeReportQueueImpl, base::OnTaskRunnerDeleter>
SpeculativeReportQueueImpl::Create(
    const SpeculativeConfigSettings& config_settings) {
  scoped_refptr<base::SequencedTaskRunner> sequenced_task_runner =
      base::ThreadPool::CreateSequencedTaskRunner(
          {base::TaskPriority::BEST_EFFORT, base::MayBlock()});
  return std::unique_ptr<SpeculativeReportQueueImpl, base::OnTaskRunnerDeleter>(
      new SpeculativeReportQueueImpl(config_settings, sequenced_task_runner),
      base::OnTaskRunnerDeleter(sequenced_task_runner));
}

SpeculativeReportQueueImpl::SpeculativeReportQueueImpl(
    const SpeculativeConfigSettings& config_settings,
    scoped_refptr<base::SequencedTaskRunner> sequenced_task_runner)
    : sequenced_task_runner_(sequenced_task_runner),
      config_settings_(config_settings) {
  DETACH_FROM_SEQUENCE(sequence_checker_);
}

SpeculativeReportQueueImpl::~SpeculativeReportQueueImpl() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  PurgePendingProducers(
      Status(error::DATA_LOSS, "The queue is being destructed"));
}

void SpeculativeReportQueueImpl::Flush(Priority priority,
                                       FlushCallback callback) {
  sequenced_task_runner_->PostTask(
      FROM_HERE,
      base::BindOnce(
          [](Priority priority, FlushCallback callback,
             base::WeakPtr<SpeculativeReportQueueImpl> self) {
            if (!self) {
              std::move(callback).Run(
                  Status(error::UNAVAILABLE, "Queue has been destructed"));
              base::UmaHistogramEnumeration(
                  reporting::kUmaUnavailableErrorReason,
                  UnavailableErrorReason::REPORT_QUEUE_DESTRUCTED,
                  UnavailableErrorReason::MAX_VALUE);
              return;
            }
            DCHECK_CALLED_ON_VALID_SEQUENCE(self->sequence_checker_);
            if (!self->actual_report_queue_.has_value()) {
              std::move(callback).Run(Status(error::FAILED_PRECONDITION,
                                             "ReportQueue is not ready yet."));
              return;
            }
            const std::unique_ptr<ReportQueue>& report_queue =
                self->actual_report_queue_.value();
            report_queue->Flush(priority, std::move(callback));
          },
          priority, std::move(callback), weak_ptr_factory_.GetWeakPtr()));
}

void SpeculativeReportQueueImpl::AddProducedRecord(
    RecordProducer record_producer,
    Priority priority,
    EnqueueCallback callback) const {
  // Invoke producer on a thread pool, then enqueue record on sequenced task
  // runner.
  sequenced_task_runner_->PostTask(
      FROM_HERE,
      base::BindOnce(&SpeculativeReportQueueImpl::MaybeEnqueueRecordProducer,
                     weak_ptr_factory_.GetWeakPtr(), priority,
                     base::BindPostTaskToCurrentDefault(std::move(callback)),
                     std::move(record_producer)));
}

void SpeculativeReportQueueImpl::MaybeEnqueueRecordProducer(
    Priority priority,
    EnqueueCallback callback,
    RecordProducer record_producer) const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (!actual_report_queue_.has_value()) {
    // Queue is not ready yet, store the record in the memory queue.
    pending_record_producers_.emplace(std::move(record_producer),
                                      std::move(callback), priority);
    return;
  }
  // Queue is ready. If memory queue is empty, just forward the record.
  if (pending_record_producers_.empty()) {
    const std::unique_ptr<ReportQueue>& report_queue =
        actual_report_queue_.value();
    report_queue->AddProducedRecord(std::move(record_producer), priority,
                                    std::move(callback));
    return;
  }
  // If memory queue is not empty, attach the new record at the
  // end and initiate enqueuing of everything from there.
  pending_record_producers_.emplace(std::move(record_producer),
                                    std::move(callback), priority);
  EnqueuePendingRecordProducers();
}

void SpeculativeReportQueueImpl::EnqueuePendingRecordProducers() const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  CHECK(actual_report_queue_.has_value());
  if (pending_record_producers_.empty()) {
    return;
  }
  const std::unique_ptr<ReportQueue>& report_queue =
      actual_report_queue_.value();
  auto head = std::move(pending_record_producers_.front());
  pending_record_producers_.pop();
  if (pending_record_producers_.empty()) {
    // Last of the pending records.
    report_queue->AddProducedRecord(std::move(head.record_producer),
                                    head.record_priority,
                                    std::move(head.record_callback));
    return;
  }
  report_queue->AddProducedRecord(
      std::move(head.record_producer), head.record_priority,
      base::BindPostTask(
          sequenced_task_runner_,
          base::BindOnce(
              [](base::WeakPtr<const SpeculativeReportQueueImpl> self,
                 EnqueueCallback callback, Status status) {
                if (!status.ok()) {
                  std::move(callback).Run(status);
                  return;
                }
                if (!self) {
                  std::move(callback).Run(
                      Status(error::UNAVAILABLE, "Queue has been destructed"));
                  base::UmaHistogramEnumeration(
                      reporting::kUmaUnavailableErrorReason,
                      UnavailableErrorReason::REPORT_QUEUE_DESTRUCTED,
                      UnavailableErrorReason::MAX_VALUE);
                  return;
                }
                std::move(callback).Run(status);
                self->EnqueuePendingRecordProducers();
              },
              weak_ptr_factory_.GetWeakPtr(),
              std::move(head.record_callback))));
}

base::OnceCallback<void(StatusOr<std::unique_ptr<ReportQueue>>)>
SpeculativeReportQueueImpl::PrepareToAttachActualQueue() const {
  return base::BindPostTask(
      sequenced_task_runner_,
      base::BindOnce(&SpeculativeReportQueueImpl::AttachActualQueue,
                     weak_ptr_factory_.GetMutableWeakPtr()));
}

Destination SpeculativeReportQueueImpl::GetDestination() const {
  return config_settings_.destination;
}

void SpeculativeReportQueueImpl::AttachActualQueue(
    StatusOr<std::unique_ptr<ReportQueue>> status_or_actual_queue) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (actual_report_queue_.has_value()) {
    // Already attached, do nothing.
    return;
  }
  if (!status_or_actual_queue.has_value()) {
    // Failed to create actual queue.
    // Flush all pending records with this status.
    PurgePendingProducers(status_or_actual_queue.error());
    return;
  }
  // Actual report queue succeeded, store it (never to change later).
  CHECK_EQ(config_settings_.destination,
           status_or_actual_queue.value()->GetDestination());
  actual_report_queue_ = std::move(status_or_actual_queue.value());
  EnqueuePendingRecordProducers();
}

void SpeculativeReportQueueImpl::PurgePendingProducers(Status status) const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  while (!pending_record_producers_.empty()) {
    auto head = std::move(pending_record_producers_.front());
    pending_record_producers_.pop();
    base::UmaHistogramEnumeration(
        reporting::kUmaDataLossErrorReason,
        DataLossErrorReason::
            SPECULATIVE_REPORT_QUEUE_DESTRUCTED_BEFORE_RECORDS_ENQUEUED,
        DataLossErrorReason::MAX_VALUE);
    std::move(head.record_callback).Run(status);
  }
}
}  // namespace reporting
