// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifndef CHROME_BROWSER_ASH_APP_MODE_RETRY_RUNNER_H_
#define CHROME_BROWSER_ASH_APP_MODE_RETRY_RUNNER_H_

#include <memory>
#include <optional>
#include <variant>

#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/memory/ptr_util.h"
#include "base/memory/weak_ptr.h"
#include "base/time/time.h"
#include "chrome/browser/ash/app_mode/cancellable_job.h"

namespace ash {

template <typename Result>
using RetryResultCallback = base::OnceCallback<void(Result result)>;
template <typename Result>
using RetryJob = base::RepeatingCallback<std::unique_ptr<CancellableJob>(
    RetryResultCallback<Result> on_result)>;
template <typename Result>
using VoidRetryJob =
    base::RepeatingCallback<void(RetryResultCallback<Result> on_result)>;
template <typename Result>
using RetryPredicate = base::RepeatingCallback<bool(const Result& result)>;

// Runs the given `job` up to `n` times.
//
// `job` will be run `n` times or until `should_retry` returns false, whichever
// happens first.
//
// `should_retry` will be invoked with the result whenever `job` finishes and
// there are attempts remaining. The result of `should_retry` determines if we
// must retry the job, or if we're done and we can pass the result to `on_done`.
//
// `on_done` will be called with the result of the last `job`.
//
// Attempts at `job` are delayed with exponential backoff after failure.
//
// Destroying the returned `std::unique_ptr` cancels this task. In that case
// `on_done` will not be called.
template <typename Result>
[[nodiscard]] std::unique_ptr<CancellableJob> RunUpToNTimes(
    int n,
    VoidRetryJob<Result> job,
    RetryPredicate<Result> should_retry,
    RetryResultCallback<Result> on_done);

// Same as above but accepts jobs that return instances of `CancellableJob`.
//
// Each `CancellableJob` instance generated by attempts at `job` are eventually
// destroyed after they return. This guarantees resources allocated by the
// execution of `job` are not kept around indefinitely after the `job` ends.
template <typename Result>
[[nodiscard]] std::unique_ptr<CancellableJob> RunUpToNTimes(
    int n,
    RetryJob<Result> job,
    RetryPredicate<Result> should_retry,
    RetryResultCallback<Result> on_done);

// Returns a predicate suitable for `RunUpToNTimes()` jobs that return an
// `std::optional` and should retry when the result is `std::nullopt`.
template <typename T>
base::RepeatingCallback<bool(const std::optional<T>&)> RetryIfNullopt();

namespace internal {

// Computes the delay for `attempt_count` with exponential backoff.
base::TimeDelta DelayForAttempt(int attempt_count);

// Runs `task` after the given `delay` in the current sequence.
void PostDelayedTask(base::OnceClosure task, base::TimeDelta delay);

// Helper to retry tasks that can fail.
template <typename Result, typename HandleType = std::monostate>
class RetryRunner : public CancellableJob {
 public:
  using Job = base::RepeatingCallback<std::unique_ptr<HandleType>(
      RetryResultCallback<Result> on_result)>;

  [[nodiscard]] static std::unique_ptr<CancellableJob> Run(
      int max_attempts,
      Job job,
      RetryPredicate<Result> should_retry,
      RetryResultCallback<Result> on_done) {
    auto handle = base::WrapUnique(new RetryRunner<Result, HandleType>(
        max_attempts, job, should_retry, std::move(on_done)));
    handle->RunAndRetryOnFailure();
    return handle;
  }

  RetryRunner(const RetryRunner&) = delete;
  RetryRunner& operator=(const RetryRunner&) = delete;
  ~RetryRunner() override = default;

 private:
  RetryRunner(int max_attempts,
              Job job,
              RetryPredicate<Result> should_retry,
              RetryResultCallback<Result> on_done)
      : max_attempts_(max_attempts),
        job_(job),
        should_retry_(should_retry),
        on_done_(std::move(on_done)) {}

  void RunAndRetryOnFailure(int attempt_count = 1) {
    current_run_ = job_.Run(base::BindOnce(
        [](base::WeakPtr<RetryRunner> self, int attempt_count, Result result) {
          if (!self) {
            return;
          }

          self->current_run_.reset();
          if (attempt_count >= self->max_attempts_ ||
              !self->should_retry_.Run(result)) {
            return std::move(self->on_done_).Run(std::move(result));
          }

          internal::PostDelayedTask(
              base::BindOnce(&RetryRunner::RunAndRetryOnFailure, self,
                             attempt_count + 1),
              internal::DelayForAttempt(attempt_count));
        },
        weak_ptr_factory_.GetWeakPtr(), attempt_count));
    if (on_done_.is_null()) {
      // `job_` already called `on_done_` synchronously, reset `current_run_`.
      current_run_.reset();
    }
  }

  int max_attempts_;
  Job job_;
  RetryPredicate<Result> should_retry_;
  RetryResultCallback<Result> on_done_;
  std::unique_ptr<HandleType> current_run_;
  base::WeakPtrFactory<RetryRunner> weak_ptr_factory_{this};
};

}  // namespace internal

template <typename Result>
[[nodiscard]] std::unique_ptr<CancellableJob> RunUpToNTimes(
    int n,
    VoidRetryJob<Result> job,
    RetryPredicate<Result> should_retry,
    RetryResultCallback<Result> on_done) {
  return internal::RetryRunner<Result>::Run(
      /*max_attempts=*/n, /*job=*/
      base::BindRepeating(
          [](VoidRetryJob<Result> job,
             RetryResultCallback<Result> result_callback) {
            job.Run(std::move(result_callback));
            return std::make_unique<std::monostate>();
          },
          job),
      should_retry, std::move(on_done));
}

template <typename Result>
[[nodiscard]] std::unique_ptr<CancellableJob> RunUpToNTimes(
    int n,
    RetryJob<Result> job,
    RetryPredicate<Result> should_retry,
    RetryResultCallback<Result> on_done) {
  return internal::RetryRunner<Result, CancellableJob>::Run(
      /*max_attempts=*/n, job, should_retry, std::move(on_done));
}

template <typename T>
base::RepeatingCallback<bool(const std::optional<T>&)> RetryIfNullopt() {
  return base::BindRepeating(
      [](const std::optional<T>& optional) { return !optional.has_value(); });
}

}  // namespace ash

#endif  // CHROME_BROWSER_ASH_APP_MODE_RETRY_RUNNER_H_
