// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only

import { strictAssert } from './assert.std.ts';
import * as durations from './durations/index.std.ts';

const BACKOFF_FACTOR = 1.9;
const MAX_BACKOFF = 15 * durations.MINUTE;
const FIRST_BACKOFFS = [0, 190];
/**
 * For a given attempt, how long should we sleep (in milliseconds)?
 *
 * The attempt should be a positive integer, and it is 1-indexed. The first attempt is 1,
 * the second is 2, and so on.
 *
 * This is modified from [iOS's codebase][0].
 *
 * [0]: https://github.com/signalapp/Signal-iOS/blob/6069741602421744edfb59923d2fb3a66b1b23c1/SignalServiceKit/src/Util/OWSOperation.swift
 */

export type ExponentialBackoffOptionsType = {
  maxBackoffTime: number;
  multiplier: number;
  firstBackoffs: Array<number>;
};
export function exponentialBackoffSleepTime(
  attempt: number,
  options: ExponentialBackoffOptionsType = {
    maxBackoffTime: MAX_BACKOFF,
    multiplier: BACKOFF_FACTOR,
    firstBackoffs: FIRST_BACKOFFS,
  }
): number {
  const numHardcodedBackoffs = options.firstBackoffs.length;
  strictAssert(
    numHardcodedBackoffs > 0,
    'must include explicit first backoffs'
  );

  if (attempt - 1 < numHardcodedBackoffs) {
    // oxlint-disable-next-line typescript/no-non-null-assertion
    return options.firstBackoffs[attempt - 1]!;
  }

  const lastHardcodedBackoff = options.firstBackoffs.at(-1);
  strictAssert(
    lastHardcodedBackoff != null && lastHardcodedBackoff > 0,
    'lastHardcodedBackoff must be a positive number'
  );
  return Math.min(
    options.maxBackoffTime,
    (lastHardcodedBackoff / options.multiplier) *
      options.multiplier ** (attempt - numHardcodedBackoffs + 1)
  );
}

/**
 * If I want to retry for X milliseconds, how many attempts is that, roughly? For example,
 * 24 hours (86,400,000 milliseconds) is 111 attempts.
 *
 * `desiredDurationMs` should be at least 1.
 */
export function exponentialBackoffMaxAttempts(
  desiredDurationMs: number,
  options?: ExponentialBackoffOptionsType
): number {
  let attempts = 0;
  let total = 0;
  // There's probably some algebra we could do here instead of this loop, but this is
  //   fast even for giant numbers, and is typically called just once at startup.
  do {
    attempts += 1;
    total += exponentialBackoffSleepTime(attempts, options);
  } while (total < desiredDurationMs);
  return attempts;
}
