// Copyright 2014 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/update_checker.h"

#include <stddef.h>

#include <algorithm>
#include <functional>
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include <vector>

#include "base/check_op.h"
#include "base/containers/flat_map.h"
#include "base/containers/span.h"
#include "base/containers/to_vector.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/memory/ref_counted.h"
#include "base/memory/weak_ptr.h"
#include "base/sequence_checker.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/thread_pool.h"
#include "build/build_config.h"
#include "components/update_client/activity_data_service.h"
#include "components/update_client/cancellation.h"
#include "components/update_client/component.h"
#include "components/update_client/configurator.h"
#include "components/update_client/persisted_data.h"
#include "components/update_client/protocol_definition.h"
#include "components/update_client/protocol_handler.h"
#include "components/update_client/protocol_serializer.h"
#include "components/update_client/request_sender.h"
#include "components/update_client/task_traits.h"
#include "components/update_client/update_client.h"
#include "components/update_client/update_engine.h"
#include "components/update_client/utils.h"
#include "url/gurl.h"

namespace update_client {
namespace {

class UpdateCheckerImpl : public UpdateChecker {
 public:
  explicit UpdateCheckerImpl(scoped_refptr<Configurator> config);
  UpdateCheckerImpl(const UpdateCheckerImpl&) = delete;
  UpdateCheckerImpl& operator=(const UpdateCheckerImpl&) = delete;
  ~UpdateCheckerImpl() override;

  // Overrides for UpdateChecker.
  void CheckForUpdates(
      scoped_refptr<UpdateContext> context,
      const base::flat_map<std::string, std::string>& additional_attributes,
      UpdateCheckCallback update_check_callback) override;

 private:
  static UpdaterStateAttributes ReadUpdaterStateAttributes(
      UpdaterStateProvider update_state_provider,
      bool is_machine);

  void CheckForUpdatesHelper(
      scoped_refptr<UpdateContext> context,
      const std::vector<GURL>& urls,
      const base::flat_map<std::string, std::string>& additional_attributes,
      const std::multimap<std::string, std::string>& cache_contents,
      const UpdaterStateAttributes& updater_state_attributes,
      const std::set<std::string>& active_ids);

  void OnRequestSenderComplete(scoped_refptr<UpdateContext> context,
                               std::optional<base::OnceClosure> fallback,
                               int error,
                               const std::string& response,
                               int retry_after_sec);

  void UpdateCheckSucceeded(scoped_refptr<UpdateContext> context,
                            const ProtocolParser::Results& results,
                            int retry_after_sec);

  void UpdateCheckFailed(ErrorCategory error_category,
                         int error,
                         int retry_after_sec);

  SEQUENCE_CHECKER(sequence_checker_);
  const scoped_refptr<Configurator> config_;
  UpdateCheckCallback update_check_callback_;
  scoped_refptr<Cancellation> cancellation_ =
      base::MakeRefCounted<Cancellation>();
  base::WeakPtrFactory<UpdateCheckerImpl> weak_factory_{this};
};

UpdateCheckerImpl::UpdateCheckerImpl(scoped_refptr<Configurator> config)
    : config_(config) {}

UpdateCheckerImpl::~UpdateCheckerImpl() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  cancellation_->Cancel();
}

void UpdateCheckerImpl::CheckForUpdates(
    scoped_refptr<UpdateContext> context,
    const base::flat_map<std::string, std::string>& additional_attributes,
    UpdateCheckCallback update_check_callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  update_check_callback_ = std::move(update_check_callback);

  auto check_for_updates_invoker = base::BindOnce(
      &UpdateCheckerImpl::CheckForUpdatesHelper, weak_factory_.GetWeakPtr(),
      context, config_->UpdateUrl(), additional_attributes);

  context->config->GetCrxCache()->ListHashesByAppId(base::BindOnce(
      [](base::OnceCallback<void(const std::multimap<std::string, std::string>&,
                                 const UpdaterStateAttributes&,
                                 const std::set<std::string>&)>
             check_for_updates_invoker,
         scoped_refptr<Configurator> config, std::vector<std::string> ids,
         const std::multimap<std::string, std::string>& cache_contents) {
        base::ThreadPool::PostTaskAndReplyWithResult(
            FROM_HERE, kTaskTraits,
            base::BindOnce(&UpdateCheckerImpl::ReadUpdaterStateAttributes,
                           config->GetUpdaterStateProvider(),
                           !config->IsPerUserInstall()),
            base::BindOnce(
                [](base::OnceCallback<void(const UpdaterStateAttributes&,
                                           const std::set<std::string>&)>
                       check_for_updates_invoker,
                   scoped_refptr<Configurator> config,
                   std::vector<std::string> ids,
                   const UpdaterStateAttributes& updater_state_attributes) {
                  config->GetPersistedData()->GetActiveBits(
                      ids, base::BindOnce(std::move(check_for_updates_invoker),
                                          updater_state_attributes));
                },
                base::BindOnce(std::move(check_for_updates_invoker),
                               cache_contents),
                config, ids));
      },
      std::move(check_for_updates_invoker), config_,
      context->components_to_check_for_updates));
}

// This function runs on the blocking pool task runner.
UpdaterStateAttributes UpdateCheckerImpl::ReadUpdaterStateAttributes(
    UpdaterStateProvider update_state_provider,
    bool is_machine) {
#if BUILDFLAG(IS_WIN)
  // On Windows, the Chrome and the updater install modes are matched by design.
  return update_state_provider.Run(is_machine);
#elif BUILDFLAG(IS_MAC)
  return update_state_provider.Run(false);
#else
  return {};
#endif  // BUILDFLAG(IS_WIN)
}

void UpdateCheckerImpl::CheckForUpdatesHelper(
    scoped_refptr<UpdateContext> context,
    const std::vector<GURL>& urls,
    const base::flat_map<std::string, std::string>& additional_attributes,
    const std::multimap<std::string, std::string>& cache_contents,
    const UpdaterStateAttributes& updater_state_attributes,
    const std::set<std::string>& active_ids) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (urls.empty()) {
    UpdateCheckFailed(ErrorCategory::kUpdateCheck,
                      static_cast<int>(ProtocolError::MISSING_URLS), 0);
    return;
  }
  GURL url = urls.front();

  // Components in this update check are either all foreground, or all
  // background since this member is inherited from the component's update
  // context. Pick the state of the first component to use in the update check.
  CHECK(!context->components.empty());
  const bool is_foreground =
      context->components.cbegin()->second->is_foreground();
  CHECK(std::ranges::all_of(
      context->components,
      [is_foreground](IdToComponentPtrMap::const_reference& elem) {
        return is_foreground == elem.second->is_foreground();
      }));

  PersistedData* metadata = config_->GetPersistedData();

  std::vector<std::string> sent_ids;

  std::vector<protocol_request::App> apps;
  for (const auto& app_id : context->components_to_check_for_updates) {
    auto it = context->components.find(app_id);
    CHECK(it != context->components.end());
    const auto& component = it->second;
    CHECK_EQ(component->id(), app_id);
    const auto& crx_component = component->crx_component();
    CHECK(crx_component);

    if (crx_component->requires_network_encryption &&
        !url.SchemeIsCryptographic()) {
      continue;
    }

    sent_ids.push_back(app_id);

    std::string install_source;
    if (!crx_component->install_source.empty()) {
      install_source = crx_component->install_source;
    } else if (component->is_foreground()) {
      install_source = "ondemand";
    }

    std::vector<std::string> cached_hashes;
    auto [first, last] = cache_contents.equal_range(app_id);
    for (auto i = first; i != last; ++i) {
      cached_hashes.push_back(i->second);
    }

    apps.push_back(MakeProtocolApp(
        app_id, crx_component->version, crx_component->ap, crx_component->brand,
        active_ids.contains(app_id) ? metadata->GetInstallId(app_id) : "",
        crx_component->lang.empty() ? config_->GetLang() : crx_component->lang,
        metadata->GetInstallDate(app_id), install_source,
        crx_component->install_location, crx_component->installer_attributes,
        metadata->GetCohort(app_id), metadata->GetCohortHint(app_id),
        metadata->GetCohortName(app_id), crx_component->channel,
        crx_component->disabled_reasons, cached_hashes,
        MakeProtocolUpdateCheck(
            !crx_component->updates_enabled ||
                (!crx_component->allow_updates_on_metered_connection &&
                 config_->IsConnectionMetered()),
            crx_component->target_version_prefix,
            crx_component->rollback_allowed,
            crx_component->same_version_update_allowed),
        [](const std::string& install_data_index)
            -> std::vector<protocol_request::Data> {
          if (install_data_index.empty()) {
            return {};
          } else {
            return {{"install", install_data_index, ""}};
          }
        }(crx_component->install_data_index),
        MakeProtocolPing(app_id, config_->GetPersistedData(),
                         active_ids.contains(app_id)),
        std::nullopt));
  }

  if (sent_ids.empty()) {
    // No apps could be checked over this URL.
    UpdateCheckFailed(ErrorCategory::kUpdateCheck,
                      static_cast<int>(ProtocolError::MISSING_URLS), 0);
    return;
  }

  const auto request = MakeProtocolRequest(
      !config_->IsPerUserInstall(), context->session_id, config_->GetProdId(),
      config_->GetBrowserVersion().GetString(), config_->GetChannel(),
      config_->GetOSLongName(), config_->GetDownloadPreference(),
      config_->IsMachineExternallyManaged(), additional_attributes,
      updater_state_attributes, std::move(apps));

  cancellation_->OnCancel(
      base::MakeRefCounted<RequestSender>(config_->GetNetworkFetcherFactory())
          ->Send({url},
                 BuildUpdateCheckExtraRequestHeaders(
                     config_->GetProdId(), config_->GetBrowserVersion(),
                     sent_ids, is_foreground),
                 config_->GetProtocolHandlerFactory()
                     ->CreateSerializer()
                     ->Serialize(request),
                 config_->EnabledCupSigning(),
                 base::BindOnce(
                     &UpdateCheckerImpl::OnRequestSenderComplete,
                     weak_factory_.GetWeakPtr(), context,
                     urls.size() > 1
                         ? std::optional<base::OnceClosure>(base::BindOnce(
                               &UpdateCheckerImpl::CheckForUpdatesHelper,
                               weak_factory_.GetWeakPtr(), context,
                               base::ToVector(base::span(urls).subspan(1u)),
                               additional_attributes, cache_contents,
                               updater_state_attributes, active_ids))
                         : std::nullopt)));
}

void UpdateCheckerImpl::OnRequestSenderComplete(
    scoped_refptr<UpdateContext> context,
    std::optional<base::OnceClosure> fallback,
    int error,
    const std::string& response,
    int retry_after_sec) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  cancellation_->Clear();

  if (error) {
    VLOG(1) << "RequestSender failed " << error;
    if (fallback && retry_after_sec <= 0) {
      std::move(*fallback).Run();
    } else {
      UpdateCheckFailed(ErrorCategory::kUpdateCheck, error, retry_after_sec);
    }
    return;
  }

  auto parser = config_->GetProtocolHandlerFactory()->CreateParser();
  if (!parser->Parse(response)) {
    VLOG(1) << "Parse failed " << parser->errors();
    UpdateCheckFailed(ErrorCategory::kUpdateCheck,
                      static_cast<int>(ProtocolError::PARSE_FAILED),
                      retry_after_sec);
    return;
  }

  CHECK_EQ(0, error);
  UpdateCheckSucceeded(context, parser->results(), retry_after_sec);
}

void UpdateCheckerImpl::UpdateCheckSucceeded(
    scoped_refptr<UpdateContext> context,
    const ProtocolParser::Results& results,
    int retry_after_sec) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  PersistedData* metadata = config_->GetPersistedData();
  const int daynum = results.daystart_elapsed_days;
  for (const auto& result : results.apps) {
    if (result.cohort) {
      metadata->SetCohort(result.app_id, *result.cohort);
    }
    if (result.cohort_name) {
      metadata->SetCohortName(result.app_id, *result.cohort_name);
    }
    if (result.cohort_hint) {
      metadata->SetCohortHint(result.app_id, *result.cohort_hint);
    }
  }

  base::OnceClosure reply =
      base::BindOnce(std::move(update_check_callback_),
                     std::make_optional<ProtocolParser::Results>(results),
                     ErrorCategory::kNone, 0, retry_after_sec);

  if (daynum != ProtocolParser::kNoDaystart) {
    metadata->SetDateLastData(context->components_to_check_for_updates, daynum,
                              std::move(reply));
    return;
  }
  metadata->SetLastUpdateCheckError({});

  base::SequencedTaskRunner::GetCurrentDefault()->PostTask(FROM_HERE,
                                                           std::move(reply));
}

void UpdateCheckerImpl::UpdateCheckFailed(ErrorCategory error_category,
                                          int error,
                                          int retry_after_sec) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  CHECK_NE(0, error);
  config_->GetPersistedData()->SetLastUpdateCheckError(
      {.category = error_category, .code = error});

  base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE, base::BindOnce(std::move(update_check_callback_), std::nullopt,
                                error_category, error, retry_after_sec));
}

}  // namespace

std::unique_ptr<UpdateChecker> UpdateChecker::Create(
    scoped_refptr<Configurator> config) {
  return std::make_unique<UpdateCheckerImpl>(config);
}

}  // namespace update_client
