/*
 This file is part of GNU Taler
 (C) 2020 Taler Systems S.A.
 GNU Taler is free software; you can redistribute it and/or modify it under the
 terms of the GNU General Public License as published by the Free Software
 Foundation; either version 3, or (at your option) any later version.
 GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
 You should have received a copy of the GNU General Public License along with
 GNU Taler; see the file COPYING.  If not, see 
 */
/**
 * Helpers for dealing with retry timeouts.
 */
/**
 * Imports.
 */
import {
  AbsoluteTime,
  Duration,
  Logger,
  TalerErrorDetail,
} from "@gnu-taler/taler-util";
import {
  BackupProviderRecord,
  DepositGroupRecord,
  ExchangeRecord,
  PeerPullPaymentIncomingRecord,
  PeerPullPaymentInitiationRecord,
  PeerPushPaymentIncomingRecord,
  PeerPushPaymentInitiationRecord,
  PurchaseRecord,
  RecoupGroupRecord,
  RefreshGroupRecord,
  TipRecord,
  WalletStoresV1,
  WithdrawalGroupRecord,
} from "../db.js";
import { TalerError } from "@gnu-taler/taler-util";
import { InternalWalletState } from "../internal-wallet-state.js";
import { PendingTaskType, TaskId } from "../pending-types.js";
import { GetReadWriteAccess } from "./query.js";
import { assertUnreachable } from "./assertUnreachable.js";
const logger = new Logger("util/retries.ts");
export enum OperationAttemptResultType {
  Finished = "finished",
  Pending = "pending",
  Error = "error",
  Longpoll = "longpoll",
}
export type OperationAttemptResult =
  | OperationAttemptFinishedResult
  | OperationAttemptErrorResult
  | OperationAttemptLongpollResult
  | OperationAttemptPendingResult;
export namespace OperationAttemptResult {
  export function finishedEmpty(): OperationAttemptResult {
    return {
      type: OperationAttemptResultType.Finished,
      result: undefined,
    };
  }
  export function pendingEmpty(): OperationAttemptResult {
    return {
      type: OperationAttemptResultType.Pending,
      result: undefined,
    };
  }
  export function longpoll(): OperationAttemptResult {
    return {
      type: OperationAttemptResultType.Longpoll,
    };
  }
}
export interface OperationAttemptFinishedResult {
  type: OperationAttemptResultType.Finished;
  result: T;
}
export interface OperationAttemptPendingResult {
  type: OperationAttemptResultType.Pending;
  result: T;
}
export interface OperationAttemptErrorResult {
  type: OperationAttemptResultType.Error;
  errorDetail: TalerErrorDetail;
}
export interface OperationAttemptLongpollResult {
  type: OperationAttemptResultType.Longpoll;
}
export interface RetryInfo {
  firstTry: AbsoluteTime;
  nextRetry: AbsoluteTime;
  retryCounter: number;
}
export interface RetryPolicy {
  readonly backoffDelta: Duration;
  readonly backoffBase: number;
  readonly maxTimeout: Duration;
}
const defaultRetryPolicy: RetryPolicy = {
  backoffBase: 1.5,
  backoffDelta: Duration.fromSpec({ seconds: 1 }),
  maxTimeout: Duration.fromSpec({ minutes: 2 }),
};
function updateTimeout(
  r: RetryInfo,
  p: RetryPolicy = defaultRetryPolicy,
): void {
  const now = AbsoluteTime.now();
  if (now.t_ms === "never") {
    throw Error("assertion failed");
  }
  if (p.backoffDelta.d_ms === "forever") {
    r.nextRetry = AbsoluteTime.never();
    return;
  }
  const nextIncrement =
    p.backoffDelta.d_ms * Math.pow(p.backoffBase, r.retryCounter);
  const t =
    now.t_ms +
    (p.maxTimeout.d_ms === "forever"
      ? nextIncrement
      : Math.min(p.maxTimeout.d_ms, nextIncrement));
  r.nextRetry = AbsoluteTime.fromMilliseconds(t);
}
export namespace RetryInfo {
  export function getDuration(
    r: RetryInfo | undefined,
    p: RetryPolicy = defaultRetryPolicy,
  ): Duration {
    if (!r) {
      // If we don't have any retry info, run immediately.
      return { d_ms: 0 };
    }
    if (p.backoffDelta.d_ms === "forever") {
      return { d_ms: "forever" };
    }
    const t = p.backoffDelta.d_ms * Math.pow(p.backoffBase, r.retryCounter);
    return {
      d_ms:
        p.maxTimeout.d_ms === "forever" ? t : Math.min(p.maxTimeout.d_ms, t),
    };
  }
  export function reset(p: RetryPolicy = defaultRetryPolicy): RetryInfo {
    const now = AbsoluteTime.now();
    const info = {
      firstTry: now,
      nextRetry: now,
      retryCounter: 0,
    };
    updateTimeout(info, p);
    return info;
  }
  export function increment(
    r: RetryInfo | undefined,
    p: RetryPolicy = defaultRetryPolicy,
  ): RetryInfo {
    if (!r) {
      return reset(p);
    }
    const r2 = { ...r };
    r2.retryCounter++;
    updateTimeout(r2, p);
    return r2;
  }
}
/**
 * Parsed representation of task identifiers.
 */
export type ParsedTaskIdentifier =
  | {
      tag: PendingTaskType.Withdraw;
      withdrawalGroupId: string;
    }
  | { tag: PendingTaskType.ExchangeUpdate; exchangeBaseUrl: string }
  | { tag: PendingTaskType.Backup; backupProviderBaseUrl: string }
  | { tag: PendingTaskType.Deposit; depositGroupId: string }
  | { tag: PendingTaskType.ExchangeCheckRefresh; exchangeBaseUrl: string }
  | { tag: PendingTaskType.ExchangeUpdate; exchangeBaseUrl: string }
  | { tag: PendingTaskType.PeerPullDebit; peerPullPaymentIncomingId: string }
  | { tag: PendingTaskType.PeerPullCredit; pursePub: string }
  | { tag: PendingTaskType.PeerPushCredit; peerPushPaymentIncomingId: string }
  | { tag: PendingTaskType.PeerPushDebit; pursePub: string }
  | { tag: PendingTaskType.Purchase; proposalId: string }
  | { tag: PendingTaskType.Recoup; recoupGroupId: string }
  | { tag: PendingTaskType.TipPickup; walletTipId: string }
  | { tag: PendingTaskType.Refresh; refreshGroupId: string };
export function parseTaskIdentifier(x: string): ParsedTaskIdentifier {
  throw Error("not yet implemented");
}
export function constructTaskIdentifier(p: ParsedTaskIdentifier): TaskId {
  switch (p.tag) {
    case PendingTaskType.Backup:
      return `${p.tag}:${p.backupProviderBaseUrl}` as TaskId;
    case PendingTaskType.Deposit:
      return `${p.tag}:${p.depositGroupId}` as TaskId;
    case PendingTaskType.ExchangeCheckRefresh:
      return `${p.tag}:${p.exchangeBaseUrl}` as TaskId;
    case PendingTaskType.ExchangeUpdate:
      return `${p.tag}:${p.exchangeBaseUrl}` as TaskId;
    case PendingTaskType.PeerPullDebit:
      return `${p.tag}:${p.peerPullPaymentIncomingId}` as TaskId;
    case PendingTaskType.PeerPushCredit:
      return `${p.tag}:${p.peerPushPaymentIncomingId}` as TaskId;
    case PendingTaskType.PeerPullCredit:
      return `${p.tag}:${p.pursePub}` as TaskId;
    case PendingTaskType.PeerPushDebit:
      return `${p.tag}:${p.pursePub}` as TaskId;
    case PendingTaskType.Purchase:
      return `${p.tag}:${p.proposalId}` as TaskId;
    case PendingTaskType.Recoup:
      return `${p.tag}:${p.recoupGroupId}` as TaskId;
    case PendingTaskType.Refresh:
      return `${p.tag}:${p.refreshGroupId}` as TaskId;
    case PendingTaskType.TipPickup:
      return `${p.tag}:${p.walletTipId}` as TaskId;
    case PendingTaskType.Withdraw:
      return `${p.tag}:${p.withdrawalGroupId}` as TaskId;
    default:
      assertUnreachable(p);
  }
}
export namespace TaskIdentifiers {
  export function forWithdrawal(wg: WithdrawalGroupRecord): TaskId {
    return `${PendingTaskType.Withdraw}:${wg.withdrawalGroupId}` as TaskId;
  }
  export function forExchangeUpdate(exch: ExchangeRecord): TaskId {
    return `${PendingTaskType.ExchangeUpdate}:${exch.baseUrl}` as TaskId;
  }
  export function forExchangeUpdateFromUrl(exchBaseUrl: string): TaskId {
    return `${PendingTaskType.ExchangeUpdate}:${exchBaseUrl}` as TaskId;
  }
  export function forExchangeCheckRefresh(exch: ExchangeRecord): TaskId {
    return `${PendingTaskType.ExchangeCheckRefresh}:${exch.baseUrl}` as TaskId;
  }
  export function forTipPickup(tipRecord: TipRecord): TaskId {
    return `${PendingTaskType.TipPickup}:${tipRecord.walletTipId}` as TaskId;
  }
  export function forRefresh(refreshGroupRecord: RefreshGroupRecord): TaskId {
    return `${PendingTaskType.Refresh}:${refreshGroupRecord.refreshGroupId}` as TaskId;
  }
  export function forPay(purchaseRecord: PurchaseRecord): TaskId {
    return `${PendingTaskType.Purchase}:${purchaseRecord.proposalId}` as TaskId;
  }
  export function forRecoup(recoupRecord: RecoupGroupRecord): TaskId {
    return `${PendingTaskType.Recoup}:${recoupRecord.recoupGroupId}` as TaskId;
  }
  export function forDeposit(depositRecord: DepositGroupRecord): TaskId {
    return `${PendingTaskType.Deposit}:${depositRecord.depositGroupId}` as TaskId;
  }
  export function forBackup(backupRecord: BackupProviderRecord): TaskId {
    return `${PendingTaskType.Backup}:${backupRecord.baseUrl}` as TaskId;
  }
  export function forPeerPushPaymentInitiation(
    ppi: PeerPushPaymentInitiationRecord,
  ): TaskId {
    return `${PendingTaskType.PeerPushDebit}:${ppi.pursePub}` as TaskId;
  }
  export function forPeerPullPaymentInitiation(
    ppi: PeerPullPaymentInitiationRecord,
  ): TaskId {
    return `${PendingTaskType.PeerPullCredit}:${ppi.pursePub}` as TaskId;
  }
  export function forPeerPullPaymentDebit(
    ppi: PeerPullPaymentIncomingRecord,
  ): TaskId {
    return `${PendingTaskType.PeerPullDebit}:${ppi.peerPullPaymentIncomingId}` as TaskId;
  }
  export function forPeerPushCredit(
    ppi: PeerPushPaymentIncomingRecord,
  ): TaskId {
    return `${PendingTaskType.PeerPushCredit}:${ppi.peerPushPaymentIncomingId}` as TaskId;
  }
}
export async function scheduleRetryInTx(
  ws: InternalWalletState,
  tx: GetReadWriteAccess<{
    operationRetries: typeof WalletStoresV1.operationRetries;
  }>,
  opId: string,
  errorDetail?: TalerErrorDetail,
): Promise {
  let retryRecord = await tx.operationRetries.get(opId);
  if (!retryRecord) {
    retryRecord = {
      id: opId,
      retryInfo: RetryInfo.reset(),
    };
    if (errorDetail) {
      retryRecord.lastError = errorDetail;
    }
  } else {
    retryRecord.retryInfo = RetryInfo.increment(retryRecord.retryInfo);
    if (errorDetail) {
      retryRecord.lastError = errorDetail;
    } else {
      delete retryRecord.lastError;
    }
  }
  await tx.operationRetries.put(retryRecord);
}
export async function scheduleRetry(
  ws: InternalWalletState,
  opId: string,
  errorDetail?: TalerErrorDetail,
): Promise {
  return await ws.db
    .mktx((x) => [x.operationRetries])
    .runReadWrite(async (tx) => {
      tx.operationRetries;
      scheduleRetryInTx(ws, tx, opId, errorDetail);
    });
}
/**
 * Run an operation handler, expect a success result and extract the success value.
 */
export async function unwrapOperationHandlerResultOrThrow(
  res: OperationAttemptResult,
): Promise {
  switch (res.type) {
    case OperationAttemptResultType.Finished:
      return res.result;
    case OperationAttemptResultType.Error:
      throw TalerError.fromUncheckedDetail(res.errorDetail);
    default:
      throw Error(`unexpected operation result (${res.type})`);
  }
}