/*
 This file is part of GNU Taler
 (C) 2022 GNUnet e.V.
 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 
 */
/**
 * Imports.
 */
import {
  AbsoluteTime,
  AgeRestriction,
  AmountJson,
  Amounts,
  CancellationToken,
  CoinRefreshRequest,
  CoinStatus,
  Duration,
  ErrorInfoSummary,
  ExchangeEntryStatus,
  ExchangeListItem,
  ExchangeTosStatus,
  getErrorDetailFromException,
  j2s,
  Logger,
  NotificationType,
  OperationErrorInfo,
  RefreshReason,
  TalerErrorCode,
  TalerErrorDetail,
  TombstoneIdStr,
  TransactionIdStr,
  TransactionType,
  WalletNotification,
} from "@gnu-taler/taler-util";
import {
  WalletStoresV1,
  CoinRecord,
  ExchangeDetailsRecord,
  ExchangeRecord,
  BackupProviderRecord,
  DepositGroupRecord,
  PeerPullPaymentIncomingRecord,
  PeerPullPaymentInitiationRecord,
  PeerPushPaymentIncomingRecord,
  PeerPushPaymentInitiationRecord,
  PurchaseRecord,
  RecoupGroupRecord,
  RefreshGroupRecord,
  TipRecord,
  WithdrawalGroupRecord,
} from "../db.js";
import { makeErrorDetail, TalerError } from "@gnu-taler/taler-util";
import { InternalWalletState } from "../internal-wallet-state.js";
import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
import { GetReadOnlyAccess, GetReadWriteAccess } from "../util/query.js";
import { CryptoApiStoppedError } from "../crypto/workers/crypto-dispatcher.js";
import { PendingTaskType, TaskId } from "../pending-types.js";
import { assertUnreachable } from "../util/assertUnreachable.js";
import { constructTransactionIdentifier } from "./transactions.js";
const logger = new Logger("operations/common.ts");
export interface CoinsSpendInfo {
  coinPubs: string[];
  contributions: AmountJson[];
  refreshReason: RefreshReason;
  /**
   * Identifier for what the coin has been spent for.
   */
  allocationId: TransactionIdStr;
}
export async function makeCoinAvailable(
  ws: InternalWalletState,
  tx: GetReadWriteAccess<{
    coins: typeof WalletStoresV1.coins;
    coinAvailability: typeof WalletStoresV1.coinAvailability;
    denominations: typeof WalletStoresV1.denominations;
  }>,
  coinRecord: CoinRecord,
): Promise {
  checkLogicInvariant(coinRecord.status === CoinStatus.Fresh);
  const existingCoin = await tx.coins.get(coinRecord.coinPub);
  if (existingCoin) {
    return;
  }
  const denom = await tx.denominations.get([
    coinRecord.exchangeBaseUrl,
    coinRecord.denomPubHash,
  ]);
  checkDbInvariant(!!denom);
  const ageRestriction = coinRecord.maxAge;
  let car = await tx.coinAvailability.get([
    coinRecord.exchangeBaseUrl,
    coinRecord.denomPubHash,
    ageRestriction,
  ]);
  if (!car) {
    car = {
      maxAge: ageRestriction,
      amountFrac: denom.amountFrac,
      amountVal: denom.amountVal,
      currency: denom.currency,
      denomPubHash: denom.denomPubHash,
      exchangeBaseUrl: denom.exchangeBaseUrl,
      freshCoinCount: 0,
    };
  }
  car.freshCoinCount++;
  await tx.coins.put(coinRecord);
  await tx.coinAvailability.put(car);
}
export async function spendCoins(
  ws: InternalWalletState,
  tx: GetReadWriteAccess<{
    coins: typeof WalletStoresV1.coins;
    coinAvailability: typeof WalletStoresV1.coinAvailability;
    refreshGroups: typeof WalletStoresV1.refreshGroups;
    denominations: typeof WalletStoresV1.denominations;
  }>,
  csi: CoinsSpendInfo,
): Promise {
  if (csi.coinPubs.length != csi.contributions.length) {
    throw Error("assertion failed");
  }
  if (csi.coinPubs.length === 0) {
    return;
  }
  let refreshCoinPubs: CoinRefreshRequest[] = [];
  for (let i = 0; i < csi.coinPubs.length; i++) {
    const coin = await tx.coins.get(csi.coinPubs[i]);
    if (!coin) {
      throw Error("coin allocated for payment doesn't exist anymore");
    }
    const denom = await ws.getDenomInfo(
      ws,
      tx,
      coin.exchangeBaseUrl,
      coin.denomPubHash,
    );
    checkDbInvariant(!!denom);
    const coinAvailability = await tx.coinAvailability.get([
      coin.exchangeBaseUrl,
      coin.denomPubHash,
      coin.maxAge,
    ]);
    checkDbInvariant(!!coinAvailability);
    const contrib = csi.contributions[i];
    if (coin.status !== CoinStatus.Fresh) {
      const alloc = coin.spendAllocation;
      if (!alloc) {
        continue;
      }
      if (alloc.id !== csi.allocationId) {
        // FIXME: assign error code
        logger.info("conflicting coin allocation ID");
        logger.info(`old ID: ${alloc.id}, new ID: ${csi.allocationId}`);
        throw Error("conflicting coin allocation (id)");
      }
      if (0 !== Amounts.cmp(alloc.amount, contrib)) {
        // FIXME: assign error code
        throw Error("conflicting coin allocation (contrib)");
      }
      continue;
    }
    coin.status = CoinStatus.Dormant;
    coin.spendAllocation = {
      id: csi.allocationId,
      amount: Amounts.stringify(contrib),
    };
    const remaining = Amounts.sub(denom.value, contrib);
    if (remaining.saturated) {
      throw Error("not enough remaining balance on coin for payment");
    }
    refreshCoinPubs.push({
      amount: Amounts.stringify(remaining.amount),
      coinPub: coin.coinPub,
    });
    checkDbInvariant(!!coinAvailability);
    if (coinAvailability.freshCoinCount === 0) {
      throw Error(
        `invalid coin count ${coinAvailability.freshCoinCount} in DB`,
      );
    }
    coinAvailability.freshCoinCount--;
    await tx.coins.put(coin);
    await tx.coinAvailability.put(coinAvailability);
  }
  await ws.refreshOps.createRefreshGroup(
    ws,
    tx,
    Amounts.currencyOf(csi.contributions[0]),
    refreshCoinPubs,
    csi.refreshReason,
    {
      originatingTransactionId: csi.allocationId,
    },
  );
}
/**
 * Convert the task ID for a task that processes a transaction int
 * the ID for the transaction.
 */
function convertTaskToTransactionId(
  taskId: string,
): TransactionIdStr | undefined {
  const parsedTaskId = parseTaskIdentifier(taskId);
  switch (parsedTaskId.tag) {
    case PendingTaskType.PeerPullCredit:
      return constructTransactionIdentifier({
        tag: TransactionType.PeerPullCredit,
        pursePub: parsedTaskId.pursePub,
      });
    case PendingTaskType.PeerPullDebit:
      return constructTransactionIdentifier({
        tag: TransactionType.PeerPullDebit,
        peerPullPaymentIncomingId: parsedTaskId.peerPullPaymentIncomingId,
      });
    // FIXME: This doesn't distinguish internal-withdrawal.
    // Maybe we should have a different task type for that as well?
    // Or maybe transaction IDs should be valid task identifiers?
    case PendingTaskType.Withdraw:
      return constructTransactionIdentifier({
        tag: TransactionType.Withdrawal,
        withdrawalGroupId: parsedTaskId.withdrawalGroupId,
      });
    case PendingTaskType.PeerPushCredit:
      return constructTransactionIdentifier({
        tag: TransactionType.PeerPushCredit,
        peerPushPaymentIncomingId: parsedTaskId.peerPushPaymentIncomingId,
      });
    case PendingTaskType.Deposit:
      return constructTransactionIdentifier({
        tag: TransactionType.Deposit,
        depositGroupId: parsedTaskId.depositGroupId,
      });
    case PendingTaskType.Refresh:
      return constructTransactionIdentifier({
        tag: TransactionType.Refresh,
        refreshGroupId: parsedTaskId.refreshGroupId,
      });
    case PendingTaskType.TipPickup:
      return constructTransactionIdentifier({
        tag: TransactionType.Tip,
        walletTipId: parsedTaskId.walletTipId,
      });
    case PendingTaskType.PeerPushDebit:
      return constructTransactionIdentifier({
        tag: TransactionType.PeerPushDebit,
        pursePub: parsedTaskId.pursePub,
      });
    case PendingTaskType.Purchase:
      return constructTransactionIdentifier({
        tag: TransactionType.Payment,
        proposalId: parsedTaskId.proposalId,
      });
    default:
      return undefined;
  }
}
/**
 * For tasks that process a transaction,
 * generate a state transition notification.
 */
async function taskToTransactionNotification(
  ws: InternalWalletState,
  tx: GetReadOnlyAccess,
  pendingTaskId: string,
  e: TalerErrorDetail | undefined,
): Promise {
  const txId = convertTaskToTransactionId(pendingTaskId);
  if (!txId) {
    return undefined;
  }
  const txState = await ws.getTransactionState(ws, tx, txId);
  if (!txState) {
    return undefined;
  }
  const notif: WalletNotification = {
    type: NotificationType.TransactionStateTransition,
    transactionId: txId,
    oldTxState: txState,
    newTxState: txState,
  };
  if (e) {
    notif.errorInfo = {
      code: e.code as number,
      hint: e.hint,
    };
  }
  return notif;
}
async function storePendingTaskError(
  ws: InternalWalletState,
  pendingTaskId: string,
  e: TalerErrorDetail,
): Promise {
  logger.info(`storing pending task error for ${pendingTaskId}`);
  const maybeNotification = await ws.db.mktxAll().runReadWrite(async (tx) => {
    let retryRecord = await tx.operationRetries.get(pendingTaskId);
    if (!retryRecord) {
      retryRecord = {
        id: pendingTaskId,
        lastError: e,
        retryInfo: RetryInfo.reset(),
      };
    } else {
      retryRecord.lastError = e;
      retryRecord.retryInfo = RetryInfo.increment(retryRecord.retryInfo);
    }
    await tx.operationRetries.put(retryRecord);
    return taskToTransactionNotification(ws, tx, pendingTaskId, e);
  });
  if (maybeNotification) {
    ws.notify(maybeNotification);
  }
}
export async function resetPendingTaskTimeout(
  ws: InternalWalletState,
  pendingTaskId: string,
): Promise {
  const maybeNotification = await ws.db.mktxAll().runReadWrite(async (tx) => {
    let retryRecord = await tx.operationRetries.get(pendingTaskId);
    if (retryRecord) {
      // Note that we don't reset the lastError, it should still be visible
      // while the retry runs.
      retryRecord.retryInfo = RetryInfo.reset();
      await tx.operationRetries.put(retryRecord);
    }
    return taskToTransactionNotification(ws, tx, pendingTaskId, undefined);
  });
  if (maybeNotification) {
    ws.notify(maybeNotification);
  }
}
async function storePendingTaskPending(
  ws: InternalWalletState,
  pendingTaskId: string,
): Promise {
  const maybeNotification = await ws.db.mktxAll().runReadWrite(async (tx) => {
    let retryRecord = await tx.operationRetries.get(pendingTaskId);
    let hadError = false;
    if (!retryRecord) {
      retryRecord = {
        id: pendingTaskId,
        retryInfo: RetryInfo.reset(),
      };
    } else {
      if (retryRecord.lastError) {
        hadError = true;
      }
      delete retryRecord.lastError;
      retryRecord.retryInfo = RetryInfo.increment(retryRecord.retryInfo);
    }
    await tx.operationRetries.put(retryRecord);
    if (hadError) {
      return taskToTransactionNotification(ws, tx, pendingTaskId, undefined);
    } else {
      return undefined;
    }
  });
  if (maybeNotification) {
    ws.notify(maybeNotification);
  }
}
async function storePendingTaskFinished(
  ws: InternalWalletState,
  pendingTaskId: string,
): Promise {
  await ws.db
    .mktx((x) => [x.operationRetries])
    .runReadWrite(async (tx) => {
      await tx.operationRetries.delete(pendingTaskId);
    });
}
export async function runTaskWithErrorReporting(
  ws: InternalWalletState,
  opId: TaskId,
  f: () => Promise>,
): Promise> {
  let maybeError: TalerErrorDetail | undefined;
  try {
    const resp = await f();
    switch (resp.type) {
      case OperationAttemptResultType.Error:
        await storePendingTaskError(ws, opId, resp.errorDetail);
        return resp;
      case OperationAttemptResultType.Finished:
        await storePendingTaskFinished(ws, opId);
        return resp;
      case OperationAttemptResultType.Pending:
        await storePendingTaskPending(ws, opId);
        return resp;
      case OperationAttemptResultType.Longpoll:
        return resp;
    }
  } catch (e) {
    if (e instanceof CryptoApiStoppedError) {
      if (ws.stopped) {
        logger.warn("crypto API stopped during shutdown, ignoring error");
        return {
          type: OperationAttemptResultType.Error,
          errorDetail: makeErrorDetail(
            TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
            {},
            "Crypto API stopped during shutdown",
          ),
        };
      }
    }
    if (e instanceof TalerError) {
      logger.warn("operation processed resulted in error");
      logger.warn(`error was: ${j2s(e.errorDetail)}`);
      maybeError = e.errorDetail;
      await storePendingTaskError(ws, opId, maybeError!);
      return {
        type: OperationAttemptResultType.Error,
        errorDetail: e.errorDetail,
      };
    } else if (e instanceof Error) {
      // This is a bug, as we expect pending operations to always
      // do their own error handling and only throw WALLET_PENDING_OPERATION_FAILED
      // or return something.
      logger.error(`Uncaught exception: ${e.message}`);
      logger.error(`Stack: ${e.stack}`);
      maybeError = makeErrorDetail(
        TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
        {
          stack: e.stack,
        },
        `unexpected exception (message: ${e.message})`,
      );
      await storePendingTaskError(ws, opId, maybeError);
      return {
        type: OperationAttemptResultType.Error,
        errorDetail: maybeError,
      };
    } else {
      logger.error("Uncaught exception, value is not even an error.");
      maybeError = makeErrorDetail(
        TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
        {},
        `unexpected exception (not even an error)`,
      );
      await storePendingTaskError(ws, opId, maybeError);
      return {
        type: OperationAttemptResultType.Error,
        errorDetail: maybeError,
      };
    }
  }
}
export enum TombstoneTag {
  DeleteWithdrawalGroup = "delete-withdrawal-group",
  DeleteReserve = "delete-reserve",
  DeletePayment = "delete-payment",
  DeleteTip = "delete-tip",
  DeleteRefreshGroup = "delete-refresh-group",
  DeleteDepositGroup = "delete-deposit-group",
  DeleteRefund = "delete-refund",
  DeletePeerPullDebit = "delete-peer-pull-debit",
  DeletePeerPushDebit = "delete-peer-push-debit",
  DeletePeerPullCredit = "delete-peer-pull-credit",
  DeletePeerPushCredit = "delete-peer-push-credit",
}
export function getExchangeTosStatus(
  exchangeDetails: ExchangeDetailsRecord,
): ExchangeTosStatus {
  if (!exchangeDetails.tosAccepted) {
    return ExchangeTosStatus.New;
  }
  if (exchangeDetails.tosAccepted?.etag == exchangeDetails.tosCurrentEtag) {
    return ExchangeTosStatus.Accepted;
  }
  return ExchangeTosStatus.Changed;
}
export function makeExchangeListItem(
  r: ExchangeRecord,
  exchangeDetails: ExchangeDetailsRecord | undefined,
  lastError: TalerErrorDetail | undefined,
): ExchangeListItem {
  const lastUpdateErrorInfo: OperationErrorInfo | undefined = lastError
    ? {
        error: lastError,
      }
    : undefined;
  if (!exchangeDetails) {
    return {
      exchangeBaseUrl: r.baseUrl,
      currency: undefined,
      tosStatus: ExchangeTosStatus.Unknown,
      paytoUris: [],
      exchangeStatus: ExchangeEntryStatus.Unknown,
      permanent: r.permanent,
      ageRestrictionOptions: [],
      lastUpdateErrorInfo,
    };
  }
  let exchangeStatus;
  exchangeStatus = ExchangeEntryStatus.Ok;
  return {
    exchangeBaseUrl: r.baseUrl,
    currency: exchangeDetails.currency,
    tosStatus: getExchangeTosStatus(exchangeDetails),
    paytoUris: exchangeDetails.wireInfo.accounts.map((x) => x.payto_uri),
    exchangeStatus,
    permanent: r.permanent,
    ageRestrictionOptions: exchangeDetails.ageMask
      ? AgeRestriction.getAgeGroupsFromMask(exchangeDetails.ageMask)
      : [],
    lastUpdateErrorInfo,
  };
}
export interface LongpollResult {
  ready: boolean;
}
export function runLongpollAsync(
  ws: InternalWalletState,
  retryTag: string,
  reqFn: (ct: CancellationToken) => Promise,
): void {
  const asyncFn = async () => {
    if (ws.stopped) {
      logger.trace("not long-polling reserve, wallet already stopped");
      await storePendingTaskPending(ws, retryTag);
      return;
    }
    const cts = CancellationToken.create();
    let res: { ready: boolean } | undefined = undefined;
    try {
      ws.activeLongpoll[retryTag] = {
        cancel: () => {
          logger.trace("cancel of reserve longpoll requested");
          cts.cancel();
        },
      };
      res = await reqFn(cts.token);
    } catch (e) {
      await storePendingTaskError(ws, retryTag, getErrorDetailFromException(e));
      return;
    } finally {
      delete ws.activeLongpoll[retryTag];
    }
    if (!res.ready) {
      await storePendingTaskPending(ws, retryTag);
    }
    ws.workAvailable.trigger();
  };
  asyncFn();
}
export type ParsedTombstone =
  | {
      tag: TombstoneTag.DeleteWithdrawalGroup;
      withdrawalGroupId: string;
    }
  | { tag: TombstoneTag.DeleteRefund; refundGroupId: string }
  | { tag: TombstoneTag.DeleteReserve; reservePub: string }
  | { tag: TombstoneTag.DeleteRefreshGroup; refreshGroupId: string }
  | { tag: TombstoneTag.DeleteTip; walletTipId: string }
  | { tag: TombstoneTag.DeletePayment; proposalId: string };
export function constructTombstone(p: ParsedTombstone): TombstoneIdStr {
  switch (p.tag) {
    case TombstoneTag.DeleteWithdrawalGroup:
      return `tmb:${p.tag}:${p.withdrawalGroupId}` as TombstoneIdStr;
    case TombstoneTag.DeleteRefund:
      return `tmb:${p.tag}:${p.refundGroupId}` as TombstoneIdStr;
    case TombstoneTag.DeleteReserve:
      return `tmb:${p.tag}:${p.reservePub}` as TombstoneIdStr;
    case TombstoneTag.DeletePayment:
      return `tmb:${p.tag}:${p.proposalId}` as TombstoneIdStr;
    case TombstoneTag.DeleteRefreshGroup:
      return `tmb:${p.tag}:${p.refreshGroupId}` as TombstoneIdStr;
    case TombstoneTag.DeleteTip:
      return `tmb:${p.tag}:${p.walletTipId}` as TombstoneIdStr;
    default:
      assertUnreachable(p);
  }
}
/**
 * Uniform interface for a particular wallet transaction.
 */
export interface TransactionManager {
  get taskId(): TaskId;
  get transactionId(): TransactionIdStr;
  fail(): Promise;
  abort(): Promise;
  suspend(): Promise;
  resume(): Promise;
  process(): Promise;
}
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 {
  const task = x.split(":");
  if (task.length < 2) {
    throw Error("task id should have al least 2 parts separated by ':'");
  }
  const [type, ...rest] = task;
  switch (type) {
    case PendingTaskType.Backup:
      return { tag: type, backupProviderBaseUrl: rest[0] };
    case PendingTaskType.Deposit:
      return { tag: type, depositGroupId: rest[0] };
    case PendingTaskType.ExchangeCheckRefresh:
      return { tag: type, exchangeBaseUrl: rest[0] };
    case PendingTaskType.ExchangeUpdate:
      return { tag: type, exchangeBaseUrl: rest[0] };
    case PendingTaskType.PeerPullCredit:
      return { tag: type, pursePub: rest[0] };
    case PendingTaskType.PeerPullDebit:
      return { tag: type, peerPullPaymentIncomingId: rest[0] };
    case PendingTaskType.PeerPushCredit:
      return { tag: type, peerPushPaymentIncomingId: rest[0] };
    case PendingTaskType.PeerPushDebit:
      return { tag: type, pursePub: rest[0] };
    case PendingTaskType.Purchase:
      return { tag: type, proposalId: rest[0] };
    case PendingTaskType.Recoup:
      return { tag: type, recoupGroupId: rest[0] };
    case PendingTaskType.Refresh:
      return { tag: type, refreshGroupId: rest[0] };
    case PendingTaskType.TipPickup:
      return { tag: type, walletTipId: rest[0] };
    case PendingTaskType.Withdraw:
      return { tag: type, withdrawalGroupId: rest[0] };
    default:
      throw Error("invalid task identifier");
  }
}
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;
  }
}
/**
 * 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})`);
  }
}