// Copyright 2024 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/update_client/op_download.h"

#include <cstdint>
#include <string>
#include <utility>
#include <vector>

#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/location.h"
#include "base/memory/scoped_refptr.h"
#include "base/task/sequenced_task_runner.h"
#include "base/types/expected.h"
#include "base/values.h"
#include "build/build_config.h"
#include "components/update_client/cancellation.h"
#include "components/update_client/configurator.h"
#include "components/update_client/crx_downloader.h"
#include "components/update_client/crx_downloader_factory.h"
#include "components/update_client/protocol_definition.h"
#include "components/update_client/update_client_errors.h"
#include "components/update_client/update_client_metrics.h"
#include "url/gurl.h"

namespace update_client {

namespace {

#if BUILDFLAG(IS_MAC)
// The minimum size of a download to attempt it at background priority.
constexpr int64_t kBackgroundDownloadSizeThreshold = 10'000'000; /*10 MB*/
#else
constexpr int64_t kBackgroundDownloadSizeThreshold = 0;
#endif

bool CanDoBackgroundDownload(bool is_foreground,
                             bool background_downloads_enabled,
                             int64_t size) {
  return !is_foreground && background_downloads_enabled &&
         size >= kBackgroundDownloadSizeThreshold;
}

// Returns a string literal corresponding to the value of the downloader |d|.
const char* DownloaderToString(CrxDownloader::DownloadMetrics::Downloader d) {
  switch (d) {
    case CrxDownloader::DownloadMetrics::kUrlFetcher:
      return "direct";
    case CrxDownloader::DownloadMetrics::kBits:
      return "bits";
    case CrxDownloader::DownloadMetrics::kBackgroundMac:
      return "nsurlsession_background";
    default:
      return "unknown";
  }
}

base::DictValue MakeEvent(const CrxDownloader::DownloadMetrics& dm) {
  base::DictValue event;
  event.Set("eventtype", protocol_request::kEventDownload);
  event.Set("eventresult", static_cast<int>(dm.error == 0));
  event.Set("downloader", DownloaderToString(dm.downloader));
  if (dm.error) {
    event.Set("errorcode", dm.error);
  }
  if (dm.extra_code1) {
    event.Set("extracode1", dm.extra_code1);
  }
  event.Set("url", dm.url.spec());

  // -1 means that the  byte counts are not known.
  if (dm.total_bytes >= 0 &&
      dm.total_bytes < protocol_request::kProtocolMaxInt) {
    event.Set("total", static_cast<double>(dm.total_bytes));
  }
  if (dm.downloaded_bytes >= 0 &&
      dm.downloaded_bytes < protocol_request::kProtocolMaxInt) {
    event.Set("downloaded", static_cast<double>(dm.downloaded_bytes));
  }
  if (dm.download_time_ms >= 0 &&
      dm.download_time_ms < protocol_request::kProtocolMaxInt) {
    event.Set("download_time_ms", static_cast<double>(dm.download_time_ms));
  }
  return event;
}

void DownloadComplete(
    const std::string& id,
    scoped_refptr<CrxDownloader> crx_downloader,
    scoped_refptr<Cancellation> cancellation,
    base::RepeatingCallback<void(base::DictValue)> event_adder,
    base::OnceCallback<void(base::expected<base::FilePath, CategorizedError>)>
        callback,
    const CrxDownloader::Result& download_result) {
  cancellation->Clear();

  for (const auto& metric : crx_downloader->download_metrics()) {
    event_adder.Run(MakeEvent(metric));
    if (metric.error == 0) {
      metrics::RecordCRXDownloadTime(
          base::Milliseconds(metric.download_time_ms), id);
    }
  }

  if (cancellation->IsCancelled()) {
    base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE,
        base::BindOnce(
            std::move(callback),
            base::unexpected<CategorizedError>(
                {.category = ErrorCategory::kService,
                 .code = std::to_underlying(ServiceError::CANCELLED)})));
    return;
  }

  if (download_result.error) {
    CHECK(download_result.response.empty());
    base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, base::BindOnce(std::move(callback),
                                  base::unexpected<CategorizedError>(
                                      {.category = ErrorCategory::kDownload,
                                       .code = download_result.error,
                                       .extra = download_result.extra_code1})));
    return;
  }
  base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE, base::BindOnce(std::move(callback), download_result.response));
}

}  // namespace

base::OnceClosure DownloadOperation(
    scoped_refptr<Configurator> config,
    const std::string& id,
    bool is_foreground,
    const std::vector<GURL>& urls,
    int64_t size,
    const std::string& hash,
    base::RepeatingCallback<void(base::DictValue)> event_adder,
    base::RepeatingCallback<void(ComponentState)> state_tracker,
    CrxDownloader::ProgressCallback progress_callback,
    const base::FilePath& file,
    base::OnceCallback<void(base::expected<base::FilePath, CategorizedError>)>
        callback) {
  state_tracker.Run(ComponentState::kDownloading);
  auto cancellation = base::MakeRefCounted<Cancellation>();
  progress_callback.Run(-1, -1);
  scoped_refptr<CrxDownloader> crx_downloader =
      config->GetCrxDownloaderFactory()->MakeCrxDownloader(
          config->GetProdId(),
          CanDoBackgroundDownload(is_foreground,
                                  config->EnabledBackgroundDownloader(), size));
  crx_downloader->set_progress_callback(base::BindRepeating(
      [](CrxDownloader::ProgressCallback progress_callback, int64_t file_size,
         int64_t downloaded_bytes, int64_t /*content_length*/) {
        progress_callback.Run(downloaded_bytes, file_size);
      },
      progress_callback, size));
  cancellation->OnCancel(crx_downloader->StartDownload(
      urls, hash,
      base::BindOnce(&DownloadComplete, id, crx_downloader, cancellation,
                     event_adder, std::move(callback))));
  return base::BindOnce(&Cancellation::Cancel, cancellation);
}

}  // namespace update_client
