//
// Copyright (C) 2017 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

#include "update_engine/update_manager/next_update_check_policy_impl.h"

#include <algorithm>

#include "update_engine/common/utils.h"

using base::Time;
using base::TimeDelta;
using std::max;
using std::string;

namespace chromeos_update_manager {

NextUpdateCheckTimePolicyImpl::NextUpdateCheckTimePolicyImpl(
    const NextUpdateCheckPolicyConstants& constants)
    : policy_constants_(constants) {}

EvalStatus NextUpdateCheckTimePolicyImpl::UpdateCheckAllowed(
    EvaluationContext* ec,
    State* state,
    string* error,
    UpdateCheckParams* result) const {
  // Ensure that periodic update checks are timed properly.
  Time next_update_check;

  if (NextUpdateCheckTime(
          ec, state, error, &next_update_check, policy_constants_) !=
      EvalStatus::kSucceeded) {
    return EvalStatus::kFailed;
  }
  if (!ec->IsWallclockTimeGreaterThan(next_update_check)) {
    LOG(INFO) << "Periodic check interval not satisfied, blocking until "
              << chromeos_update_engine::utils::ToString(next_update_check);
    return EvalStatus::kAskMeAgainLater;
  }

  return EvalStatus::kContinue;
}

EvalStatus NextUpdateCheckTimePolicyImpl::NextUpdateCheckTime(
    EvaluationContext* ec,
    State* state,
    string* error,
    Time* next_update_check,
    const NextUpdateCheckPolicyConstants& constants) {
  UpdaterProvider* const updater_provider = state->updater_provider();

  // Don't check for updates too often. We limit the update checks to once every
  // some interval. The interval is kTimeoutInitialInterval the first time and
  // kTimeoutPeriodicInterval for the subsequent update checks. If the update
  // check fails, we increase the interval between the update checks
  // exponentially until kTimeoutMaxBackoffInterval. Finally, to avoid having
  // many chromebooks running update checks at the exact same time, we add some
  // fuzz to the interval.
  const Time* updater_started_time =
      ec->GetValue(updater_provider->var_updater_started_time());
  POLICY_CHECK_VALUE_AND_FAIL(updater_started_time, error);

  const Time* last_checked_time =
      ec->GetValue(updater_provider->var_last_checked_time());

  const auto* seed = ec->GetValue(state->random_provider()->var_seed());
  POLICY_CHECK_VALUE_AND_FAIL(seed, error);

  PRNG prng(*seed);

  // If this is the first attempt, compute and return an initial value.
  if (last_checked_time == nullptr ||
      *last_checked_time < *updater_started_time) {
    *next_update_check = *updater_started_time +
                         FuzzedInterval(&prng,
                                        constants.timeout_initial_interval,
                                        constants.timeout_regular_fuzz);
    return EvalStatus::kSucceeded;
  }

  // Check whether the server is enforcing a poll interval; if not, this value
  // will be zero.
  const unsigned int* server_dictated_poll_interval =
      ec->GetValue(updater_provider->var_server_dictated_poll_interval());
  POLICY_CHECK_VALUE_AND_FAIL(server_dictated_poll_interval, error);

  int interval = *server_dictated_poll_interval;
  int fuzz = 0;

  // If no poll interval was dictated by server compute a back-off period,
  // starting from a predetermined base periodic interval and increasing
  // exponentially by the number of consecutive failed attempts.
  if (interval == 0) {
    const unsigned int* consecutive_failed_update_checks =
        ec->GetValue(updater_provider->var_consecutive_failed_update_checks());
    POLICY_CHECK_VALUE_AND_FAIL(consecutive_failed_update_checks, error);

    interval = constants.timeout_periodic_interval;
    unsigned int num_failures = *consecutive_failed_update_checks;
    while (interval < constants.timeout_max_backoff_interval && num_failures) {
      interval *= 2;
      num_failures--;
    }
  }

  // We cannot back off longer than the predetermined maximum interval.
  if (interval > constants.timeout_max_backoff_interval)
    interval = constants.timeout_max_backoff_interval;

  // We cannot back off shorter than the predetermined periodic interval. Also,
  // in this case set the fuzz to a predetermined regular value.
  if (interval <= constants.timeout_periodic_interval) {
    interval = constants.timeout_periodic_interval;
    fuzz = constants.timeout_regular_fuzz;
  }

  // If not otherwise determined, defer to a fuzz of +/-(interval / 2).
  if (fuzz == 0)
    fuzz = interval;

  *next_update_check =
      *last_checked_time + FuzzedInterval(&prng, interval, fuzz);
  return EvalStatus::kSucceeded;
}

TimeDelta NextUpdateCheckTimePolicyImpl::FuzzedInterval(PRNG* prng,
                                                        int interval,
                                                        int fuzz) {
  DCHECK_GE(interval, 0);
  DCHECK_GE(fuzz, 0);
  int half_fuzz = fuzz / 2;
  // This guarantees the output interval is non negative.
  int interval_min = max(interval - half_fuzz, 0);
  int interval_max = interval + half_fuzz;
  return TimeDelta::FromSeconds(prng->RandMinMax(interval_min, interval_max));
}

}  // namespace chromeos_update_manager