// Copyright 2015 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_engine.h"

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

#include "base/barrier_callback.h"
#include "base/check_op.h"
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/strings/strcat.h"
#include "base/task/sequenced_task_runner.h"
#include "base/uuid.h"
#include "base/values.h"
#include "base/version.h"
#include "components/prefs/pref_service.h"
#include "components/update_client/component.h"
#include "components/update_client/configurator.h"
#include "components/update_client/crx_cache.h"
#include "components/update_client/crx_update_item.h"
#include "components/update_client/persisted_data.h"
#include "components/update_client/protocol_parser.h"
#include "components/update_client/update_checker.h"
#include "components/update_client/update_client.h"
#include "components/update_client/update_client_errors.h"
#include "components/update_client/utils.h"
#include "third_party/abseil-cpp/absl/container/flat_hash_map.h"

namespace update_client {

namespace {

base::DictValue MakeEvent(UpdateClient::PingParams ping_params,
                          const base::Version& previous_version) {
  base::DictValue event;
  event.Set("eventtype", ping_params.event_type);
  event.Set("eventresult", ping_params.result);
  if (ping_params.error_code) {
    event.Set("errorcode", ping_params.error_code);
  }
  if (ping_params.extra_code1) {
    event.Set("extracode1", ping_params.extra_code1);
  }
  if (!ping_params.app_command_id.empty()) {
    event.Set("appcommandid", ping_params.app_command_id);
  }
  event.Set("previousversion", previous_version.GetString());
  return event;
}

}  // namespace

UpdateContext::UpdateContext(
    scoped_refptr<Configurator> config,
    bool is_foreground,
    bool is_install,
    const std::vector<std::string>& ids,
    UpdateClient::CrxStateChangeCallback crx_state_change_callback,
    UpdateEngine::Callback callback,
    PersistedData* persisted_data,
    bool is_update_check_only,
    base::RepeatingCallback<int64_t(const base::FilePath&)> get_available_space)
    : config(config),
      is_foreground(is_foreground),
      is_install(is_install),
      ids(ids),
      crx_state_change_callback(crx_state_change_callback),
      callback(std::move(callback)),
      session_id(base::StrCat(
          {"{", base::Uuid::GenerateRandomV4().AsLowercaseString(), "}"})),
      persisted_data(persisted_data),
      is_update_check_only(is_update_check_only),
      get_available_space(get_available_space) {
  for (const auto& id : ids) {
    components.insert(
        std::make_pair(id, std::make_unique<Component>(*this, id)));
  }
}

UpdateContext::~UpdateContext() = default;

UpdateEngine::UpdateEngine(
    scoped_refptr<Configurator> config,
    UpdateChecker::Factory update_checker_factory,
    scoped_refptr<PingManager> ping_manager,
    const UpdateClient::CrxStateChangeCallback& notify_observers_callback)
    : config_(config),
      update_checker_factory_(update_checker_factory),
      ping_manager_(ping_manager),
      notify_observers_callback_(notify_observers_callback) {}

UpdateEngine::~UpdateEngine() = default;

base::RepeatingClosure UpdateEngine::Update(
    bool is_foreground,
    bool is_install,
    const std::vector<std::string>& ids,
    UpdateClient::CrxDataCallback crx_data_callback,
    UpdateClient::CrxStateChangeCallback crx_state_change_callback,
    Callback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  return InvokeOperation(is_foreground, /*is_update_check_only=*/false,
                         is_install, ids, std::move(crx_data_callback),
                         std::move(crx_state_change_callback),
                         std::move(callback));
}

void UpdateEngine::CheckForUpdate(
    bool is_foreground,
    const std::string& id,
    UpdateClient::CrxDataCallback crx_data_callback,
    UpdateClient::CrxStateChangeCallback crx_state_change_callback,
    Callback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  InvokeOperation(is_foreground, /*is_update_check_only=*/true,
                  /*is_install=*/false, {id}, std::move(crx_data_callback),
                  std::move(crx_state_change_callback), std::move(callback));
}

base::RepeatingClosure UpdateEngine::InvokeOperation(
    bool is_foreground,
    bool is_update_check_only,
    bool is_install,
    const std::vector<std::string>& ids,
    UpdateClient::CrxDataCallback crx_data_callback,
    UpdateClient::CrxStateChangeCallback crx_state_change_callback,
    Callback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (ids.empty()) {
    base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE,
        base::BindOnce(std::move(callback), Error::INVALID_ARGUMENT));
    return base::DoNothing();
  }

  if (IsThrottled(is_foreground)) {
    base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, base::BindOnce(std::move(callback), Error::RETRY_LATER));
    return base::DoNothing();
  }

  scoped_refptr<UpdateContext> update_context =
      base::MakeRefCounted<UpdateContext>(
          config_, is_foreground, is_install, ids,
          crx_state_change_callback
              ? base::BindRepeating(
                    [](UpdateClient::CrxStateChangeCallback a,
                       UpdateClient::CrxStateChangeCallback b,
                       const CrxUpdateItem& item) {
                      a.Run(item);
                      b.Run(item);
                    },
                    crx_state_change_callback, notify_observers_callback_)
              : notify_observers_callback_,
          std::move(callback), config_->GetPersistedData(),
          is_update_check_only);
  CHECK(!update_context->session_id.empty());

  const auto [unused, inserted] = update_contexts_.insert(
      std::make_pair(update_context->session_id, update_context));
  CHECK(inserted);

  // Calls out to get the corresponding CrxComponent data for the components.
  std::move(crx_data_callback)
      .Run(ids,
           base::BindOnce(&UpdateEngine::StartOperation, this, update_context));
  return is_update_check_only
             ? base::DoNothing()
             : base::BindRepeating(
                   [](scoped_refptr<UpdateContext> context) {
                     context->is_cancelled = true;
                     for (const auto& entry : context->components) {
                       entry.second->Cancel();
                     }
                   },
                   update_context);
}

void UpdateEngine::StartOperation(
    scoped_refptr<UpdateContext> update_context,
    const std::vector<std::optional<CrxComponent>>& crx_components) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (crx_components.size() != update_context->ids.size()) {
    base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, base::BindOnce(std::move(update_context->callback),
                                  Error::BAD_CRX_DATA_CALLBACK));
    return;
  }

  for (size_t i = 0; i != update_context->ids.size(); ++i) {
    const auto& id = update_context->ids[i];

    CHECK_EQ(update_context->components[id]->state(), ComponentState::kNew);

    const auto crx_component = crx_components[i];
    if (crx_component && (id == GetCrxComponentID(*crx_component))) {
      // This component can be checked for updates.
      auto& component = update_context->components[id];
      component->set_crx_component(*crx_component);
      component->set_previous_version(component->crx_component()->version);
      component->set_previous_fp(component->crx_component()->fingerprint);
      update_context->components_to_check_for_updates.push_back(id);
    } else {
      // |CrxDataCallback| did not return a CrxComponent instance for this
      // component, which most likely, has been uninstalled. This component
      // is going to be transitioned to an error state when the its |Handle|
      // method is called later on by the engine.
      update_context->component_queue.push(id);
    }
  }
  base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE,
      base::BindOnce(update_context->components_to_check_for_updates.empty()
                         ? &UpdateEngine::HandleComponent
                         : &UpdateEngine::DoUpdateCheck,
                     this, update_context));
}

void UpdateEngine::DoUpdateCheck(scoped_refptr<UpdateContext> update_context) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  CHECK(update_context);

  // Make the components transition from |kNew| to |kChecking| state.
  for (const auto& id : update_context->components_to_check_for_updates) {
    update_context->components[id]->Handle(base::DoNothing());
  }

  update_context->update_checker = update_checker_factory_.Run(config_);

  update_context->update_checker->CheckForUpdates(
      update_context, config_->ExtraRequestParams(),
      base::BindOnce(&UpdateEngine::UpdateCheckResultsAvailable, this,
                     update_context));
}

void UpdateEngine::UpdateCheckResultsAvailable(
    scoped_refptr<UpdateContext> update_context,
    std::optional<ProtocolParser::Results> results,
    ErrorCategory error_category,
    int error,
    int retry_after_sec) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  CHECK(update_context);

  update_context->retry_after_sec = retry_after_sec;

  // Only positive values for throttle_sec are effective. 0 means that no
  // throttling occurs and it resets the throttle.
  // Negative values are not trusted and are ignored.
  static constexpr int kMaxRetryAfterSec = 24 * 60 * 60;  // 24 hours.
  const int throttle_sec =
      std::min(update_context->retry_after_sec, kMaxRetryAfterSec);
  if (throttle_sec >= 0) {
    config_->GetPersistedData()->SetThrottleUpdatesUntil(
        throttle_sec ? base::Time::Now() + base::Seconds(throttle_sec)
                     : base::Time());
  }

  update_context->update_check_error = error;

  auto complete = base::BarrierCallback<bool>(
      update_context->components_to_check_for_updates.size(),
      base::BindOnce([](const std::vector<bool>&) {})
          .Then(base::BindOnce(&UpdateEngine::UpdateCheckComplete, this,
                               update_context)));
  if (error) {
    CHECK(!results);
    for (const auto& id : update_context->components_to_check_for_updates) {
      auto it = update_context->components.find(id);
      CHECK(it != update_context->components.end());
      auto& component = it->second;
      component->SetUpdateCheckResult(std::nullopt, ErrorCategory::kUpdateCheck,
                                      error, complete);
    }
    return;
  }

  CHECK(results);
  CHECK_EQ(0, error);

  absl::flat_hash_map<std::string, ProtocolParser::App> id_to_result;
  id_to_result.reserve(results->apps.size());
  for (const auto& result : results->apps) {
    id_to_result[result.app_id] = result;
  }

  for (const auto& id : update_context->components_to_check_for_updates) {
    auto components_it = update_context->components.find(id);
    CHECK(components_it != update_context->components.end());
    auto& component = components_it->second;
    const auto& it = id_to_result.find(id);
    if (it != id_to_result.end()) {
      const auto& result = it->second;
      const auto& [category, protocol_error] = [](const std::string& status) {
        // "ok" and "noupdate" are non-error cases.
        if (status == "ok" || status == "noupdate") {
          return std::make_pair(ErrorCategory::kNone, ProtocolError::NONE);
        }
        // Some app status literals can be folded down as an updatecheck status.
        if (status == "error-unknownApplication") {
          return std::make_pair(ErrorCategory::kUpdateCheck,
                                ProtocolError::UNKNOWN_APPLICATION);
        }
        if (status == "restricted") {
          return std::make_pair(ErrorCategory::kUpdateCheck,
                                ProtocolError::RESTRICTED_APPLICATION);
        }
        if (status == "error-invalidAppId") {
          return std::make_pair(ErrorCategory::kUpdateCheck,
                                ProtocolError::INVALID_APPID);
        }
        if (status == "error-osnotsupported") {
          return std::make_pair(ErrorCategory::kUpdateCheck,
                                ProtocolError::OS_NOT_SUPPORTED);
        }
        if (status == "error-hwnotsupported") {
          return std::make_pair(ErrorCategory::kUpdateCheck,
                                ProtocolError::HW_NOT_SUPPORTED);
        }
        if (status == "error-hash") {
          return std::make_pair(ErrorCategory::kUpdateCheck,
                                ProtocolError::NO_HASH);
        }
        if (status == "error-unsupportedprotocol") {
          return std::make_pair(ErrorCategory::kUpdateCheck,
                                ProtocolError::UNSUPPORTED_PROTOCOL);
        }
        if (status == "error-internal") {
          return std::make_pair(ErrorCategory::kUpdateCheck,
                                ProtocolError::INTERNAL);
        }
        if (status == "error-inexpressible") {
          return std::make_pair(ErrorCategory::kUpdateCheck,
                                ProtocolError::INEXPRESSIBLE);
        }
        // Otherwise, this is an unknown status.
        return std::make_pair(ErrorCategory::kUpdateCheck,
                              ProtocolError::UNKNOWN_ERROR);
      }(result.status);
      component->SetUpdateCheckResult(
          result, category, static_cast<int>(protocol_error), complete);
    } else {
      component->SetUpdateCheckResult(
          std::nullopt, ErrorCategory::kUpdateCheck,
          static_cast<int>(ProtocolError::UPDATE_RESPONSE_NOT_FOUND), complete);
    }
  }
}

void UpdateEngine::UpdateCheckComplete(
    scoped_refptr<UpdateContext> update_context) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  CHECK(update_context);

  for (const auto& id : update_context->components_to_check_for_updates) {
    update_context->component_queue.push(id);

    // Handle the |kChecking| state and transition the component to the
    // next state, depending on the update check results.
    auto components_it = update_context->components.find(id);
    CHECK(components_it != update_context->components.end());
    auto& component = components_it->second;
    CHECK_EQ(component->state(), ComponentState::kChecking);
    component->Handle(base::DoNothing());
  }

  base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE,
      base::BindOnce(&UpdateEngine::HandleComponent, this, update_context));
}

void UpdateEngine::HandleComponent(
    scoped_refptr<UpdateContext> update_context) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  CHECK(update_context);

  auto& queue = update_context->component_queue;

  if (queue.empty()) {
    const Error error = update_context->update_check_error
                            ? Error::UPDATE_CHECK_ERROR
                            : Error::NONE;

    base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, base::BindOnce(&UpdateEngine::UpdateComplete, this,
                                  update_context, error));
    return;
  }

  const auto& id = queue.front();
  auto components_it = update_context->components.find(id);
  CHECK(components_it != update_context->components.end());
  auto& component = components_it->second;
  CHECK(component);

  auto& next_update_delay = update_context->next_update_delay;
  if (!next_update_delay.is_zero() && component->IsUpdateAvailable()) {
    base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
        FROM_HERE,
        base::BindOnce(&UpdateEngine::HandleComponent, this, update_context),
        next_update_delay);
    next_update_delay = base::TimeDelta();
    return;
  }

  component->Handle(base::BindOnce(&UpdateEngine::HandleComponentComplete, this,
                                   update_context));
}

void UpdateEngine::HandleComponentComplete(
    scoped_refptr<UpdateContext> update_context) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  CHECK(update_context);

  auto& queue = update_context->component_queue;
  CHECK(!queue.empty());

  const auto& id = queue.front();
  auto components_it = update_context->components.find(id);
  CHECK(components_it != update_context->components.end());
  auto& component = components_it->second;
  CHECK(component);

  base::OnceClosure callback =
      base::BindOnce(&UpdateEngine::HandleComponent, this, update_context);
  if (component->IsHandled()) {
    update_context->next_update_delay = component->GetUpdateDuration();
    queue.pop();
    if (!component->events().empty()) {
      CHECK(component->crx_component());
      ping_manager_->SendPing(component->session_id(),
                              *component->crx_component(),
                              component->GetEvents(), std::move(callback));
      return;
    }
  }

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

void UpdateEngine::UpdateComplete(scoped_refptr<UpdateContext> update_context,
                                  Error error) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  CHECK(update_context);

  const auto num_erased = update_contexts_.erase(update_context->session_id);
  CHECK_EQ(1u, num_erased);

  base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE, base::BindOnce(std::move(update_context->callback), error));
}

bool UpdateEngine::GetUpdateState(const std::string& id,
                                  CrxUpdateItem* update_item) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  for (const auto& context : update_contexts_) {
    const auto& components = context.second->components;
    const auto it = components.find(id);
    if (it != components.end()) {
      *update_item = it->second->GetCrxUpdateItem();
      return true;
    }
  }
  return false;
}

bool UpdateEngine::IsThrottled(bool is_foreground) const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  base::Time throttle_updates_until =
      config_->GetPersistedData()->GetThrottleUpdatesUntil();

  if (is_foreground || throttle_updates_until.is_null()) {
    return false;
  }

  const auto now(base::Time::Now());

  // Throttle the calls in the interval (t - 1 day, t) to limit the effect of
  // unset clocks or clock drift.
  return throttle_updates_until - base::Days(1) < now &&
         now < throttle_updates_until;
}

void UpdateEngine::SendPing(const CrxComponent& crx_component,
                            UpdateClient::PingParams ping_params,
                            Callback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  std::vector<base::DictValue> events;
  events.push_back(MakeEvent(ping_params, crx_component.version));
  ping_manager_->SendPing(
      base::StrCat(
          {"{", base::Uuid::GenerateRandomV4().AsLowercaseString(), "}"}),
      crx_component, std::move(events),
      base::BindOnce(std::move(callback), Error::NONE));
}

}  // namespace update_client
