// Copyright 2021 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/policy/messaging_layer/upload/upload_provider.h"

#include <list>
#include <memory>
#include <utility>

#include "base/containers/flat_map.h"
#include "base/memory/ref_counted_delete_on_sequence.h"
#include "base/memory/weak_ptr.h"
#include "base/strings/strcat.h"
#include "base/task/bind_post_task.h"
#include "base/task/sequenced_task_runner.h"
#include "base/thread_annotations.h"
#include "build/build_config.h"
#include "chrome/browser/policy/messaging_layer/upload/configuration_file_controller.h"
#include "chrome/browser/policy/messaging_layer/upload/upload_client.h"
#include "chrome/browser/policy/messaging_layer/util/upload_declarations.h"
#include "components/reporting/proto/synced/configuration_file.pb.h"
#include "components/reporting/proto/synced/record.pb.h"
#include "components/reporting/proto/synced/status.pb.h"
#include "components/reporting/util/backoff_settings.h"
#include "components/reporting/util/status.h"
#include "components/reporting/util/statusor.h"
#include "net/base/backoff_entry.h"

namespace reporting {

namespace {
std::unique_ptr<ConfigurationFileController> CreateConfigurationFileController(
    UploadClient::UpdateConfigInMissiveCallback update_config_in_missive_cb) {
#if BUILDFLAG(IS_CHROMEOS)
  return std::make_unique<ConfigurationFileController>(
      update_config_in_missive_cb);
#else   // !BUILDFLAG(IS_CHROMEOS)
  return nullptr;
#endif  // BUILDFLAG(IS_CHROMEOS)
}
}  // namespace

// EncryptedReportingUploadProvider refcounted helper class.
class EncryptedReportingUploadProvider::UploadHelper
    : public base::RefCountedDeleteOnSequence<UploadHelper> {
 public:
  UploadHelper(ReportSuccessfulUploadCallback report_successful_upload_cb,
               EncryptionKeyAttachedCallback encryption_key_attached_cb,
               UpdateConfigInMissiveCallback update_config_in_missive_cb,
               UploadClientBuilderCb upload_client_builder_cb,
               scoped_refptr<base::SequencedTaskRunner> sequenced_task_runner);
  UploadHelper(const UploadHelper& other) = delete;
  UploadHelper& operator=(const UploadHelper& other) = delete;

  // Requests new cloud policy client (can be invoked on any thread)
  void PostNewUploadClientRequest();

  // Uploads encrypted records (can be invoked on any thread).
  void EnqueueUpload(bool need_encryption_key,
                     std::vector<EncryptedRecord> records,
                     ScopedReservation scoped_reservation,
                     UploadEnqueuedCallback enqueued_cb) const;

 private:
  friend class base::RefCountedDeleteOnSequence<UploadHelper>;
  friend class base::DeleteHelper<UploadHelper>;

  // Refcounted object can only have private or protected destructor.
  ~UploadHelper();

  // Stages of upload client creation, scheduled on a sequenced task runner.
  void TryNewUploadClientRequest();
  void OnUploadClientResult(
      StatusOr<std::unique_ptr<UploadClient>> client_result);

  // Helpers to send the configuration file gotten from the response
  // to the `ConfigurationFileController`.
  void UpdateConfigFile(ConfigFile file);

  // Uploads encrypted records on sequenced task runner (and thus capable of
  // detecting whether upload client is ready or not). If not ready,
  // it will wait and then upload.
  void EnqueueUploadInternal(bool need_encryption_key,
                             std::vector<EncryptedRecord> records,
                             ScopedReservation scoped_reservation,
                             UploadEnqueuedCallback enqueued_cb);

  // Sequence task runner and checker used during
  // `PostNewUploadClientRequest` processing.
  // It is also used to protect `upload_client_`.
  const scoped_refptr<base::SequencedTaskRunner> sequenced_task_runner_;
  SEQUENCE_CHECKER(sequenced_task_checker_);

  // Callbacks for successful upload, key delivery and configuration file
  // attached.
  const ReportSuccessfulUploadCallback report_successful_upload_cb_;
  const EncryptionKeyAttachedCallback encryption_key_attached_cb_;
  ConfigFileAttachedCallback config_file_attached_cb_;

  // Callback for upload client creation.
  UploadClientBuilderCb upload_client_builder_cb_;

  // Tracking of asynchronous stages.
  std::atomic<bool> upload_client_request_in_progress_{false};
  const std::unique_ptr<::net::BackoffEntry> backoff_entry_;

  // Stored data from upload requests before upload client is ready.
  // Note that vectors of records submitted for upload are mapped by
  // generation id (which is the same for all records being uploaded at once
  // since they originate from the same priority queue). As a result, if
  // the caller attempts to upload the same records multiple times (e.g.
  // because it did not yet get a confirmation from server), we will only
  // hold to one set of records.
  // |stored_reservations_| reflect amount of memory assigned to the respective
  // element in |stored_records_| (if it is assigned, otherwise it is absent
  // from the map). unique_ptr is used, to enable moving/erasing the map
  // elements. Guarded by sequenced_task_runner_.
  base::flat_map</*generation_id*/ int64_t, std::vector<EncryptedRecord>>
      stored_records_ GUARDED_BY_CONTEXT(sequenced_task_checker_);
  base::flat_map</*generation_id*/ int64_t, std::unique_ptr<ScopedReservation>>
      stored_reservations_ GUARDED_BY_CONTEXT(sequenced_task_checker_);
  bool stored_need_encryption_key_ GUARDED_BY_CONTEXT(sequenced_task_checker_){
      false};
  int stored_config_file_version_ GUARDED_BY_CONTEXT(sequenced_task_checker_){
      0};

  // Upload client (protected by sequenced task runner). Once set, is used
  // repeatedly.
  std::unique_ptr<UploadClient> upload_client_
      GUARDED_BY_CONTEXT(sequenced_task_checker_);

  // Configuration file controller (protected by sequenced task runner). Once
  // set, is used repeatedly.
  std::unique_ptr<ConfigurationFileController> config_controller_
      GUARDED_BY_CONTEXT(sequenced_task_checker_);

  // Keep this last so that all weak pointers will be invalidated at the
  // beginning of destruction.
  base::WeakPtrFactory<UploadHelper> weak_ptr_factory_{this};
};

EncryptedReportingUploadProvider::UploadHelper::UploadHelper(
    ReportSuccessfulUploadCallback report_successful_upload_cb,
    EncryptionKeyAttachedCallback encryption_key_attached_cb,
    UpdateConfigInMissiveCallback update_config_in_missive_cb,
    UploadClientBuilderCb upload_client_builder_cb,
    scoped_refptr<base::SequencedTaskRunner> sequenced_task_runner)
    : base::RefCountedDeleteOnSequence<UploadHelper>(sequenced_task_runner),
      sequenced_task_runner_(std::move(sequenced_task_runner)),
      report_successful_upload_cb_(report_successful_upload_cb),
      encryption_key_attached_cb_(encryption_key_attached_cb),
      upload_client_builder_cb_(std::move(upload_client_builder_cb)),
      backoff_entry_(GetBackoffEntry()),
      config_controller_(CreateConfigurationFileController(
          std::move(update_config_in_missive_cb))) {
  config_file_attached_cb_ =
      base::BindPostTask(sequenced_task_runner_,
                         base::BindRepeating(&UploadHelper::UpdateConfigFile,
                                             weak_ptr_factory_.GetWeakPtr()));
  DETACH_FROM_SEQUENCE(sequenced_task_checker_);
}

EncryptedReportingUploadProvider::UploadHelper::~UploadHelper() {
  // Weak pointer factory must be destructed on the sequence.
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequenced_task_checker_);
}

void EncryptedReportingUploadProvider::UploadHelper::
    PostNewUploadClientRequest() {
  sequenced_task_runner_->PostTask(
      FROM_HERE, base::BindOnce(&UploadHelper::TryNewUploadClientRequest,
                                weak_ptr_factory_.GetWeakPtr()));
}

void EncryptedReportingUploadProvider::UploadHelper::
    TryNewUploadClientRequest() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequenced_task_checker_);
  if (upload_client_ != nullptr) {
    return;
  }
  if (upload_client_request_in_progress_) {
    return;
  }
  upload_client_request_in_progress_ = true;

  sequenced_task_runner_->PostDelayedTask(
      FROM_HERE,
      base::BindOnce(
          [](base::WeakPtr<UploadHelper> self) {
            if (!self) {
              return;  // Provider expired
            }
            std::move(self->upload_client_builder_cb_)
                .Run(base::BindPostTask(
                    self->sequenced_task_runner_,
                    base::BindRepeating(&UploadHelper::OnUploadClientResult,
                                        self)));
          },
          weak_ptr_factory_.GetWeakPtr()),
      backoff_entry_->GetTimeUntilRelease());

  // Increase backoff_entry_ for next request.
  backoff_entry_->InformOfRequest(/*succeeded=*/false);
}

void EncryptedReportingUploadProvider::UploadHelper::OnUploadClientResult(
    StatusOr<std::unique_ptr<UploadClient>> client_result) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequenced_task_checker_);
  if (!client_result.has_value()) {
    upload_client_request_in_progress_ = false;
    PostNewUploadClientRequest();
    return;
  }

  // Record upload client to be used.
  upload_client_ = std::move(client_result.value());
  backoff_entry_->InformOfRequest(/*succeeded=*/true);
  upload_client_request_in_progress_ = false;

  // Upload client is ready, upload all previously stored requests (if any).
  while (!stored_records_.empty() || stored_need_encryption_key_) {
    std::vector<EncryptedRecord> records;
    ScopedReservation scoped_reservation;
    if (!stored_records_.empty()) {
      records = std::move(stored_records_.begin()->second);
      auto it = stored_reservations_.find(stored_records_.begin()->first);
      if (it != stored_reservations_.end()) {
        scoped_reservation.HandOver(*it->second);
        CHECK(!it->second->reserved());
        stored_reservations_.erase(it);
      }
      stored_records_.erase(stored_records_.begin());
    }
    const bool need_encryption_key =
        std::exchange(stored_need_encryption_key_, false);
    upload_client_->EnqueueUpload(
        need_encryption_key, stored_config_file_version_, std::move(records),
        std::move(scoped_reservation),
        base::BindOnce([](StatusOr<std::list<int64_t>> cached_records_seq_ids) {
          LOG_IF(ERROR, !cached_records_seq_ids.has_value())
              << "Upload not enqueued, error="
              << cached_records_seq_ids.error();
        }),
        report_successful_upload_cb_, encryption_key_attached_cb_,
        config_file_attached_cb_);
  }
}

void EncryptedReportingUploadProvider::UploadHelper::UpdateConfigFile(
    ConfigFile file) {
  // This function is only called in ChromeOS so no nullptr check is needed.
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequenced_task_checker_);
  stored_config_file_version_ =
      config_controller_->HandleConfigurationFile(std::move(file));
}

void EncryptedReportingUploadProvider::UploadHelper::EnqueueUpload(
    bool need_encryption_key,
    std::vector<EncryptedRecord> records,
    ScopedReservation scoped_reservation,
    UploadEnqueuedCallback enqueued_cb) const {
  sequenced_task_runner_->PostTask(
      FROM_HERE,
      base::BindOnce(
          &UploadHelper::EnqueueUploadInternal,
          weak_ptr_factory_.GetMutableWeakPtr(), need_encryption_key,
          std::move(records), std::move(scoped_reservation),
          Scoped<StatusOr<std::list<int64_t>>>(
              std::move(enqueued_cb),
              base::unexpected(Status(error::UNAVAILABLE,
                                      "UploadHelper has been destructed")))));
}

// static
void EncryptedReportingUploadProvider::UploadHelper::EnqueueUploadInternal(
    bool need_encryption_key,
    std::vector<EncryptedRecord> records,
    ScopedReservation scoped_reservation,
    UploadEnqueuedCallback enqueued_cb) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequenced_task_checker_);
  if (upload_client_ == nullptr) {
    stored_need_encryption_key_ |= need_encryption_key;
    int64_t generation_id = 0;
    if (!records.empty() && records.begin()->has_sequence_information() &&
        records.begin()->sequence_information().has_generation_id()) {
      generation_id = records.begin()->sequence_information().generation_id();
    }
    std::list<int64_t> cached_records_seq_ids;
    for (const auto& record : records) {
      cached_records_seq_ids.push_back(
          record.sequence_information().sequencing_id());
    }
    stored_records_.emplace(generation_id, std::move(records));
    stored_reservations_.emplace(
        generation_id,
        std::make_unique<ScopedReservation>(std::move(scoped_reservation)));
    // Report success even though the upload has not been executed.
    // All stored records are listed as cached, because we don't need to re-send
    // them (although actual cache will be created later).
    // And actual upload success is reported through two permanent repeating
    // callbacks, once the client is created and upload is executed.
    std::move(enqueued_cb).Run(std::move(cached_records_seq_ids));
    return;
  }
  upload_client_->EnqueueUpload(
      need_encryption_key, stored_config_file_version_, std::move(records),
      std::move(scoped_reservation), std::move(enqueued_cb),
      report_successful_upload_cb_, encryption_key_attached_cb_,
      config_file_attached_cb_);
}

// EncryptedReportingUploadProvider implementation.

EncryptedReportingUploadProvider::EncryptedReportingUploadProvider(
    ReportSuccessfulUploadCallback report_successful_upload_cb,
    EncryptionKeyAttachedCallback encryption_key_attached_cb,
    UpdateConfigInMissiveCallback update_config_in_missive_cb,
    UploadClientBuilderCb upload_client_builder_cb)
    : helper_(base::MakeRefCounted<UploadHelper>(
          report_successful_upload_cb,
          encryption_key_attached_cb,
          update_config_in_missive_cb,
          std::move(upload_client_builder_cb),
          base::SequencedTaskRunner::GetCurrentDefault())) {
  helper_->PostNewUploadClientRequest();
}

EncryptedReportingUploadProvider::~EncryptedReportingUploadProvider() = default;

void EncryptedReportingUploadProvider::RequestUploadEncryptedRecords(
    bool need_encryption_key,
    std::vector<EncryptedRecord> records,
    ScopedReservation scoped_reservation,
    UploadEnqueuedCallback enqueued_cb) {
  CHECK(helper_);
  helper_->EnqueueUpload(need_encryption_key, std::move(records),
                         std::move(scoped_reservation), std::move(enqueued_cb));
}

base::WeakPtr<EncryptedReportingUploadProvider>
EncryptedReportingUploadProvider::GetWeakPtr() {
  return weak_ptr_factory_.GetWeakPtr();
}

// static
EncryptedReportingUploadProvider::UploadClientBuilderCb
EncryptedReportingUploadProvider::GetUploadClientBuilder() {
  return base::BindOnce(&UploadClient::Create);
}
}  // namespace reporting
