/*
 This file is part of GNU Taler
 (C) 2022-2023 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 
 */
import {
  Amounts,
  CheckPeerPushDebitRequest,
  CheckPeerPushDebitResponse,
  CoinRefreshRequest,
  ContractTermsUtil,
  HttpStatusCode,
  InitiatePeerPushDebitRequest,
  InitiatePeerPushDebitResponse,
  Logger,
  NotificationType,
  RefreshReason,
  TalerError,
  TalerErrorCode,
  TalerPreciseTimestamp,
  TalerProtocolTimestamp,
  TalerProtocolViolationError,
  TalerUriAction,
  TransactionAction,
  TransactionMajorState,
  TransactionMinorState,
  TransactionState,
  TransactionType,
  decodeCrock,
  encodeCrock,
  getRandomBytes,
  hash,
  j2s,
  stringifyTalerUri,
} from "@gnu-taler/taler-util";
import {
  HttpResponse,
  readSuccessResponseJsonOrThrow,
  readTalerErrorResponse,
} from "@gnu-taler/taler-util/http";
import { EncryptContractRequest } from "../crypto/cryptoTypes.js";
import {
  PeerPushPaymentInitiationRecord,
  PeerPushPaymentInitiationStatus,
  RefreshOperationStatus,
  createRefreshGroup,
} from "../index.js";
import { InternalWalletState } from "../internal-wallet-state.js";
import { PendingTaskType } from "../pending-types.js";
import { assertUnreachable } from "../util/assertUnreachable.js";
import { checkLogicInvariant } from "../util/invariants.js";
import {
  TaskRunResult,
  TaskRunResultType,
  constructTaskIdentifier,
  runLongpollAsync,
  spendCoins,
} from "./common.js";
import {
  PeerCoinRepair,
  codecForExchangePurseStatus,
  getTotalPeerPaymentCost,
  queryCoinInfosForSelection,
  selectPeerCoins,
} from "./pay-peer-common.js";
import {
  constructTransactionIdentifier,
  notifyTransition,
  stopLongpolling,
} from "./transactions.js";
const logger = new Logger("pay-peer-push-debit.ts");
export async function checkPeerPushDebit(
  ws: InternalWalletState,
  req: CheckPeerPushDebitRequest,
): Promise {
  const instructedAmount = Amounts.parseOrThrow(req.amount);
  const coinSelRes = await selectPeerCoins(ws, { instructedAmount });
  if (coinSelRes.type === "failure") {
    throw TalerError.fromDetail(
      TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE,
      {
        insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails,
      },
    );
  }
  const totalAmount = await getTotalPeerPaymentCost(
    ws,
    coinSelRes.result.coins,
  );
  return {
    amountEffective: Amounts.stringify(totalAmount),
    amountRaw: req.amount,
  };
}
async function handlePurseCreationConflict(
  ws: InternalWalletState,
  peerPushInitiation: PeerPushPaymentInitiationRecord,
  resp: HttpResponse,
): Promise {
  const pursePub = peerPushInitiation.pursePub;
  const errResp = await readTalerErrorResponse(resp);
  if (errResp.code !== TalerErrorCode.EXCHANGE_GENERIC_INSUFFICIENT_FUNDS) {
    await failPeerPushDebitTransaction(ws, pursePub);
    return TaskRunResult.finished();
  }
  // FIXME: Properly parse!
  const brokenCoinPub = (errResp as any).coin_pub;
  logger.trace(`excluded broken coin pub=${brokenCoinPub}`);
  if (!brokenCoinPub) {
    // FIXME: Details!
    throw new TalerProtocolViolationError();
  }
  const instructedAmount = Amounts.parseOrThrow(peerPushInitiation.amount);
  const sel = peerPushInitiation.coinSel;
  const repair: PeerCoinRepair = {
    coinPubs: [],
    contribs: [],
    exchangeBaseUrl: peerPushInitiation.exchangeBaseUrl,
  };
  for (let i = 0; i < sel.coinPubs.length; i++) {
    if (sel.coinPubs[i] != brokenCoinPub) {
      repair.coinPubs.push(sel.coinPubs[i]);
      repair.contribs.push(Amounts.parseOrThrow(sel.contributions[i]));
    }
  }
  const coinSelRes = await selectPeerCoins(ws, { instructedAmount, repair });
  if (coinSelRes.type == "failure") {
    // FIXME: Details!
    throw Error(
      "insufficient balance to re-select coins to repair double spending",
    );
  }
  await ws.db
    .mktx((x) => [x.peerPushPaymentInitiations])
    .runReadWrite(async (tx) => {
      const myPpi = await tx.peerPushPaymentInitiations.get(
        peerPushInitiation.pursePub,
      );
      if (!myPpi) {
        return;
      }
      switch (myPpi.status) {
        case PeerPushPaymentInitiationStatus.PendingCreatePurse:
        case PeerPushPaymentInitiationStatus.SuspendedCreatePurse: {
          const sel = coinSelRes.result;
          myPpi.coinSel = {
            coinPubs: sel.coins.map((x) => x.coinPub),
            contributions: sel.coins.map((x) => x.contribution),
          };
          break;
        }
        default:
          return;
      }
      await tx.peerPushPaymentInitiations.put(myPpi);
    });
  return TaskRunResult.finished();
}
async function processPeerPushDebitCreateReserve(
  ws: InternalWalletState,
  peerPushInitiation: PeerPushPaymentInitiationRecord,
): Promise {
  logger.info("processing peer-push-debit pending(create-reserve)");
  const pursePub = peerPushInitiation.pursePub;
  const purseExpiration = peerPushInitiation.purseExpiration;
  const hContractTerms = peerPushInitiation.contractTermsHash;
  const purseSigResp = await ws.cryptoApi.signPurseCreation({
    hContractTerms,
    mergePub: peerPushInitiation.mergePub,
    minAge: 0,
    purseAmount: peerPushInitiation.amount,
    purseExpiration,
    pursePriv: peerPushInitiation.pursePriv,
  });
  const coins = await queryCoinInfosForSelection(
    ws,
    peerPushInitiation.coinSel,
  );
  const depositSigsResp = await ws.cryptoApi.signPurseDeposits({
    exchangeBaseUrl: peerPushInitiation.exchangeBaseUrl,
    pursePub: peerPushInitiation.pursePub,
    coins,
  });
  const encryptContractRequest: EncryptContractRequest = {
    contractTerms: peerPushInitiation.contractTerms,
    mergePriv: peerPushInitiation.mergePriv,
    pursePriv: peerPushInitiation.pursePriv,
    pursePub: peerPushInitiation.pursePub,
    contractPriv: peerPushInitiation.contractPriv,
    contractPub: peerPushInitiation.contractPub,
    nonce: peerPushInitiation.contractEncNonce,
  };
  logger.info(`encrypt contract request: ${j2s(encryptContractRequest)}`);
  const econtractResp = await ws.cryptoApi.encryptContractForMerge(
    encryptContractRequest,
  );
  const econtractHash = encodeCrock(
    hash(decodeCrock(econtractResp.econtract.econtract)),
  );
  logger.info(`econtract hash: ${econtractHash}`);
  const createPurseUrl = new URL(
    `purses/${peerPushInitiation.pursePub}/create`,
    peerPushInitiation.exchangeBaseUrl,
  );
  const reqBody = {
    amount: peerPushInitiation.amount,
    merge_pub: peerPushInitiation.mergePub,
    purse_sig: purseSigResp.sig,
    h_contract_terms: hContractTerms,
    purse_expiration: purseExpiration,
    deposits: depositSigsResp.deposits,
    min_age: 0,
    econtract: econtractResp.econtract,
  };
  logger.info(`request body: ${j2s(reqBody)}`);
  const httpResp = await ws.http.fetch(createPurseUrl.href, {
    method: "POST",
    body: reqBody,
  });
  {
    const resp = await httpResp.json();
    logger.info(`resp: ${j2s(resp)}`);
  }
  switch (httpResp.status) {
    case HttpStatusCode.Ok:
      break;
    case HttpStatusCode.Forbidden: {
      // FIXME: Store this error!
      await failPeerPushDebitTransaction(ws, pursePub);
      return TaskRunResult.finished();
    }
    case HttpStatusCode.Conflict: {
      // Handle double-spending
      return handlePurseCreationConflict(ws, peerPushInitiation, httpResp);
    }
    default: {
      const errResp = await readTalerErrorResponse(httpResp);
      return {
        type: TaskRunResultType.Error,
        errorDetail: errResp,
      };
    }
  }
  if (httpResp.status !== HttpStatusCode.Ok) {
    // FIXME: do proper error reporting
    throw Error("got error response from exchange");
  }
  await transitionPeerPushDebitTransaction(ws, pursePub, {
    stFrom: PeerPushPaymentInitiationStatus.PendingCreatePurse,
    stTo: PeerPushPaymentInitiationStatus.PendingReady,
  });
  return TaskRunResult.finished();
}
async function processPeerPushDebitAbortingDeletePurse(
  ws: InternalWalletState,
  peerPushInitiation: PeerPushPaymentInitiationRecord,
): Promise {
  const { pursePub, pursePriv } = peerPushInitiation;
  const transactionId = constructTransactionIdentifier({
    tag: TransactionType.PeerPushDebit,
    pursePub,
  });
  const sigResp = await ws.cryptoApi.signDeletePurse({
    pursePriv,
  });
  const purseUrl = new URL(
    `purses/${pursePub}`,
    peerPushInitiation.exchangeBaseUrl,
  );
  const resp = await ws.http.fetch(purseUrl.href, {
    method: "DELETE",
    headers: {
      "taler-purse-signature": sigResp.sig,
    },
  });
  logger.info(`deleted purse with response status ${resp.status}`);
  const transitionInfo = await ws.db
    .mktx((x) => [
      x.peerPushPaymentInitiations,
      x.refreshGroups,
      x.denominations,
      x.coinAvailability,
      x.coins,
    ])
    .runReadWrite(async (tx) => {
      const ppiRec = await tx.peerPushPaymentInitiations.get(pursePub);
      if (!ppiRec) {
        return undefined;
      }
      if (
        ppiRec.status !== PeerPushPaymentInitiationStatus.AbortingDeletePurse
      ) {
        return undefined;
      }
      const currency = Amounts.currencyOf(ppiRec.amount);
      const oldTxState = computePeerPushDebitTransactionState(ppiRec);
      const coinPubs: CoinRefreshRequest[] = [];
      for (let i = 0; i < ppiRec.coinSel.coinPubs.length; i++) {
        coinPubs.push({
          amount: ppiRec.coinSel.contributions[i],
          coinPub: ppiRec.coinSel.coinPubs[i],
        });
      }
      const refresh = await createRefreshGroup(
        ws,
        tx,
        currency,
        coinPubs,
        RefreshReason.AbortPeerPushDebit,
      );
      ppiRec.status = PeerPushPaymentInitiationStatus.AbortingRefresh;
      ppiRec.abortRefreshGroupId = refresh.refreshGroupId;
      await tx.peerPushPaymentInitiations.put(ppiRec);
      const newTxState = computePeerPushDebitTransactionState(ppiRec);
      return {
        oldTxState,
        newTxState,
      };
    });
  notifyTransition(ws, transactionId, transitionInfo);
  return TaskRunResult.pending();
}
interface SimpleTransition {
  stFrom: PeerPushPaymentInitiationStatus;
  stTo: PeerPushPaymentInitiationStatus;
}
async function transitionPeerPushDebitTransaction(
  ws: InternalWalletState,
  pursePub: string,
  transitionSpec: SimpleTransition,
): Promise {
  const transactionId = constructTransactionIdentifier({
    tag: TransactionType.PeerPushDebit,
    pursePub,
  });
  const transitionInfo = await ws.db
    .mktx((x) => [x.peerPushPaymentInitiations])
    .runReadWrite(async (tx) => {
      const ppiRec = await tx.peerPushPaymentInitiations.get(pursePub);
      if (!ppiRec) {
        return undefined;
      }
      if (ppiRec.status !== transitionSpec.stFrom) {
        return undefined;
      }
      const oldTxState = computePeerPushDebitTransactionState(ppiRec);
      ppiRec.status = transitionSpec.stTo;
      await tx.peerPushPaymentInitiations.put(ppiRec);
      const newTxState = computePeerPushDebitTransactionState(ppiRec);
      return {
        oldTxState,
        newTxState,
      };
    });
  notifyTransition(ws, transactionId, transitionInfo);
}
async function processPeerPushDebitAbortingRefresh(
  ws: InternalWalletState,
  peerPushInitiation: PeerPushPaymentInitiationRecord,
): Promise {
  const pursePub = peerPushInitiation.pursePub;
  const abortRefreshGroupId = peerPushInitiation.abortRefreshGroupId;
  checkLogicInvariant(!!abortRefreshGroupId);
  const transactionId = constructTransactionIdentifier({
    tag: TransactionType.PeerPushDebit,
    pursePub: peerPushInitiation.pursePub,
  });
  const transitionInfo = await ws.db
    .mktx((x) => [x.refreshGroups, x.peerPushPaymentInitiations])
    .runReadWrite(async (tx) => {
      const refreshGroup = await tx.refreshGroups.get(abortRefreshGroupId);
      let newOpState: PeerPushPaymentInitiationStatus | undefined;
      if (!refreshGroup) {
        // Maybe it got manually deleted? Means that we should
        // just go into failed.
        logger.warn("no aborting refresh group found for deposit group");
        newOpState = PeerPushPaymentInitiationStatus.Failed;
      } else {
        if (refreshGroup.operationStatus === RefreshOperationStatus.Finished) {
          newOpState = PeerPushPaymentInitiationStatus.Aborted;
        } else if (
          refreshGroup.operationStatus === RefreshOperationStatus.Failed
        ) {
          newOpState = PeerPushPaymentInitiationStatus.Failed;
        }
      }
      if (newOpState) {
        const newDg = await tx.peerPushPaymentInitiations.get(pursePub);
        if (!newDg) {
          return;
        }
        const oldTxState = computePeerPushDebitTransactionState(newDg);
        newDg.status = newOpState;
        const newTxState = computePeerPushDebitTransactionState(newDg);
        await tx.peerPushPaymentInitiations.put(newDg);
        return { oldTxState, newTxState };
      }
      return undefined;
    });
  notifyTransition(ws, transactionId, transitionInfo);
  // FIXME: Shouldn't this be finished in some cases?!
  return TaskRunResult.pending();
}
/**
 * Process the "pending(ready)" state of a peer-push-debit transaction.
 */
async function processPeerPushDebitReady(
  ws: InternalWalletState,
  peerPushInitiation: PeerPushPaymentInitiationRecord,
): Promise {
  logger.info("processing peer-push-debit pending(ready)");
  const pursePub = peerPushInitiation.pursePub;
  const retryTag = constructTaskIdentifier({
    tag: PendingTaskType.PeerPushDebit,
    pursePub,
  });
  runLongpollAsync(ws, retryTag, async (ct) => {
    const mergeUrl = new URL(
      `purses/${pursePub}/merge`,
      peerPushInitiation.exchangeBaseUrl,
    );
    mergeUrl.searchParams.set("timeout_ms", "30000");
    logger.info(`long-polling on purse status at ${mergeUrl.href}`);
    const resp = await ws.http.fetch(mergeUrl.href, {
      // timeout: getReserveRequestTimeout(withdrawalGroup),
      cancellationToken: ct,
    });
    if (resp.status === HttpStatusCode.Ok) {
      const purseStatus = await readSuccessResponseJsonOrThrow(
        resp,
        codecForExchangePurseStatus(),
      );
      const mergeTimestamp = purseStatus.merge_timestamp;
      logger.info(`got purse status ${j2s(purseStatus)}`);
      if (!mergeTimestamp || TalerProtocolTimestamp.isNever(mergeTimestamp)) {
        return { ready: false };
      } else {
        await transitionPeerPushDebitTransaction(
          ws,
          peerPushInitiation.pursePub,
          {
            stFrom: PeerPushPaymentInitiationStatus.PendingReady,
            stTo: PeerPushPaymentInitiationStatus.Done,
          },
        );
        return {
          ready: true,
        };
      }
    } else if (resp.status === HttpStatusCode.Gone) {
      await transitionPeerPushDebitTransaction(
        ws,
        peerPushInitiation.pursePub,
        {
          stFrom: PeerPushPaymentInitiationStatus.PendingReady,
          stTo: PeerPushPaymentInitiationStatus.Expired,
        },
      );
      return {
        ready: true,
      };
    } else {
      logger.warn(`unexpected HTTP status for purse: ${resp.status}`);
      return {
        ready: false,
      };
    }
  });
  logger.trace(
    "returning early from peer-push-debit for long-polling in background",
  );
  return {
    type: TaskRunResultType.Longpoll,
  };
}
export async function processPeerPushDebit(
  ws: InternalWalletState,
  pursePub: string,
): Promise {
  const peerPushInitiation = await ws.db
    .mktx((x) => [x.peerPushPaymentInitiations])
    .runReadOnly(async (tx) => {
      return tx.peerPushPaymentInitiations.get(pursePub);
    });
  if (!peerPushInitiation) {
    throw Error("peer push payment not found");
  }
  const retryTag = constructTaskIdentifier({
    tag: PendingTaskType.PeerPushDebit,
    pursePub,
  });
  // We're already running!
  if (ws.activeLongpoll[retryTag]) {
    logger.info("peer-push-debit task already in long-polling, returning!");
    return {
      type: TaskRunResultType.Longpoll,
    };
  }
  switch (peerPushInitiation.status) {
    case PeerPushPaymentInitiationStatus.PendingCreatePurse:
      return processPeerPushDebitCreateReserve(ws, peerPushInitiation);
    case PeerPushPaymentInitiationStatus.PendingReady:
      return processPeerPushDebitReady(ws, peerPushInitiation);
    case PeerPushPaymentInitiationStatus.AbortingDeletePurse:
      return processPeerPushDebitAbortingDeletePurse(ws, peerPushInitiation);
    case PeerPushPaymentInitiationStatus.AbortingRefresh:
      return processPeerPushDebitAbortingRefresh(ws, peerPushInitiation);
    default: {
      const txState = computePeerPushDebitTransactionState(peerPushInitiation);
      logger.warn(
        `not processing peer-push-debit transaction in state ${j2s(txState)}`,
      );
    }
  }
  return TaskRunResult.finished();
}
/**
 * Initiate sending a peer-to-peer push payment.
 */
export async function initiatePeerPushDebit(
  ws: InternalWalletState,
  req: InitiatePeerPushDebitRequest,
): Promise {
  const instructedAmount = Amounts.parseOrThrow(
    req.partialContractTerms.amount,
  );
  const purseExpiration = req.partialContractTerms.purse_expiration;
  const contractTerms = req.partialContractTerms;
  const pursePair = await ws.cryptoApi.createEddsaKeypair({});
  const mergePair = await ws.cryptoApi.createEddsaKeypair({});
  const hContractTerms = ContractTermsUtil.hashContractTerms(contractTerms);
  const contractKeyPair = await ws.cryptoApi.createEddsaKeypair({});
  const coinSelRes = await selectPeerCoins(ws, { instructedAmount });
  if (coinSelRes.type !== "success") {
    throw TalerError.fromDetail(
      TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE,
      {
        insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails,
      },
    );
  }
  const sel = coinSelRes.result;
  logger.info(`selected p2p coins (push): ${j2s(coinSelRes)}`);
  const totalAmount = await getTotalPeerPaymentCost(
    ws,
    coinSelRes.result.coins,
  );
  const pursePub = pursePair.pub;
  const transactionId = constructTaskIdentifier({
    tag: PendingTaskType.PeerPushDebit,
    pursePub,
  });
  const contractEncNonce = encodeCrock(getRandomBytes(24));
  const transitionInfo = await ws.db
    .mktx((x) => [
      x.exchanges,
      x.contractTerms,
      x.coins,
      x.coinAvailability,
      x.denominations,
      x.refreshGroups,
      x.peerPushPaymentInitiations,
    ])
    .runReadWrite(async (tx) => {
      // FIXME: Instead of directly doing a spendCoin here,
      // we might want to mark the coins as used and spend them
      // after we've been able to create the purse.
      await spendCoins(ws, tx, {
        // allocationId: `txn:peer-push-debit:${pursePair.pub}`,
        allocationId: constructTransactionIdentifier({
          tag: TransactionType.PeerPushDebit,
          pursePub: pursePair.pub,
        }),
        coinPubs: sel.coins.map((x) => x.coinPub),
        contributions: sel.coins.map((x) =>
          Amounts.parseOrThrow(x.contribution),
        ),
        refreshReason: RefreshReason.PayPeerPush,
      });
      const ppi: PeerPushPaymentInitiationRecord = {
        amount: Amounts.stringify(instructedAmount),
        contractPriv: contractKeyPair.priv,
        contractPub: contractKeyPair.pub,
        contractTermsHash: hContractTerms,
        exchangeBaseUrl: sel.exchangeBaseUrl,
        mergePriv: mergePair.priv,
        mergePub: mergePair.pub,
        purseExpiration: purseExpiration,
        pursePriv: pursePair.priv,
        pursePub: pursePair.pub,
        timestampCreated: TalerPreciseTimestamp.now(),
        status: PeerPushPaymentInitiationStatus.PendingCreatePurse,
        contractTerms: contractTerms,
        contractEncNonce,
        coinSel: {
          coinPubs: sel.coins.map((x) => x.coinPub),
          contributions: sel.coins.map((x) => x.contribution),
        },
        totalCost: Amounts.stringify(totalAmount),
      };
      await tx.peerPushPaymentInitiations.add(ppi);
      await tx.contractTerms.put({
        h: hContractTerms,
        contractTermsRaw: contractTerms,
      });
      const newTxState = computePeerPushDebitTransactionState(ppi);
      return {
        oldTxState: { major: TransactionMajorState.None },
        newTxState,
      };
    });
  notifyTransition(ws, transactionId, transitionInfo);
  ws.notify({ type: NotificationType.BalanceChange });
  return {
    contractPriv: contractKeyPair.priv,
    mergePriv: mergePair.priv,
    pursePub: pursePair.pub,
    exchangeBaseUrl: coinSelRes.result.exchangeBaseUrl,
    talerUri: stringifyTalerUri({
      type: TalerUriAction.PayPush,
      exchangeBaseUrl: coinSelRes.result.exchangeBaseUrl,
      contractPriv: contractKeyPair.priv,
    }),
    transactionId: constructTransactionIdentifier({
      tag: TransactionType.PeerPushDebit,
      pursePub: pursePair.pub,
    }),
  };
}
export function computePeerPushDebitTransactionActions(
  ppiRecord: PeerPushPaymentInitiationRecord,
): TransactionAction[] {
  switch (ppiRecord.status) {
    case PeerPushPaymentInitiationStatus.PendingCreatePurse:
      return [TransactionAction.Abort, TransactionAction.Suspend];
    case PeerPushPaymentInitiationStatus.PendingReady:
      return [TransactionAction.Abort, TransactionAction.Suspend];
    case PeerPushPaymentInitiationStatus.Aborted:
      return [TransactionAction.Delete];
    case PeerPushPaymentInitiationStatus.AbortingDeletePurse:
      return [TransactionAction.Suspend, TransactionAction.Fail];
    case PeerPushPaymentInitiationStatus.AbortingRefresh:
      return [TransactionAction.Suspend, TransactionAction.Fail];
    case PeerPushPaymentInitiationStatus.SuspendedAbortingDeletePurse:
      return [TransactionAction.Resume, TransactionAction.Fail];
    case PeerPushPaymentInitiationStatus.SuspendedAbortingRefresh:
      return [TransactionAction.Resume, TransactionAction.Fail];
    case PeerPushPaymentInitiationStatus.SuspendedCreatePurse:
      return [TransactionAction.Resume, TransactionAction.Abort];
    case PeerPushPaymentInitiationStatus.SuspendedReady:
      return [TransactionAction.Suspend, TransactionAction.Abort];
    case PeerPushPaymentInitiationStatus.Done:
      return [TransactionAction.Delete];
    case PeerPushPaymentInitiationStatus.Expired:
      return [TransactionAction.Delete];
    case PeerPushPaymentInitiationStatus.Failed:
      return [TransactionAction.Delete];
  }
}
export async function abortPeerPushDebitTransaction(
  ws: InternalWalletState,
  pursePub: string,
) {
  const taskId = constructTaskIdentifier({
    tag: PendingTaskType.PeerPushDebit,
    pursePub,
  });
  const transactionId = constructTransactionIdentifier({
    tag: TransactionType.PeerPushDebit,
    pursePub,
  });
  stopLongpolling(ws, taskId);
  const transitionInfo = await ws.db
    .mktx((x) => [x.peerPushPaymentInitiations])
    .runReadWrite(async (tx) => {
      const pushDebitRec = await tx.peerPushPaymentInitiations.get(pursePub);
      if (!pushDebitRec) {
        logger.warn(`peer push debit ${pursePub} not found`);
        return;
      }
      let newStatus: PeerPushPaymentInitiationStatus | undefined = undefined;
      switch (pushDebitRec.status) {
        case PeerPushPaymentInitiationStatus.PendingReady:
        case PeerPushPaymentInitiationStatus.SuspendedReady:
          newStatus = PeerPushPaymentInitiationStatus.AbortingDeletePurse;
          break;
        case PeerPushPaymentInitiationStatus.SuspendedCreatePurse:
        case PeerPushPaymentInitiationStatus.PendingCreatePurse:
          // Network request might already be in-flight!
          newStatus = PeerPushPaymentInitiationStatus.AbortingDeletePurse;
          break;
        case PeerPushPaymentInitiationStatus.SuspendedAbortingRefresh:
        case PeerPushPaymentInitiationStatus.SuspendedAbortingDeletePurse:
        case PeerPushPaymentInitiationStatus.AbortingRefresh:
        case PeerPushPaymentInitiationStatus.Done:
        case PeerPushPaymentInitiationStatus.AbortingDeletePurse:
        case PeerPushPaymentInitiationStatus.Aborted:
        case PeerPushPaymentInitiationStatus.Expired:
        case PeerPushPaymentInitiationStatus.Failed:
          // Do nothing
          break;
        default:
          assertUnreachable(pushDebitRec.status);
      }
      if (newStatus != null) {
        const oldTxState = computePeerPushDebitTransactionState(pushDebitRec);
        pushDebitRec.status = newStatus;
        const newTxState = computePeerPushDebitTransactionState(pushDebitRec);
        await tx.peerPushPaymentInitiations.put(pushDebitRec);
        return {
          oldTxState,
          newTxState,
        };
      }
      return undefined;
    });
  notifyTransition(ws, transactionId, transitionInfo);
}
export async function failPeerPushDebitTransaction(
  ws: InternalWalletState,
  pursePub: string,
) {
  const taskId = constructTaskIdentifier({
    tag: PendingTaskType.PeerPushDebit,
    pursePub,
  });
  const transactionId = constructTransactionIdentifier({
    tag: TransactionType.PeerPushDebit,
    pursePub,
  });
  stopLongpolling(ws, taskId);
  const transitionInfo = await ws.db
    .mktx((x) => [x.peerPushPaymentInitiations])
    .runReadWrite(async (tx) => {
      const pushDebitRec = await tx.peerPushPaymentInitiations.get(pursePub);
      if (!pushDebitRec) {
        logger.warn(`peer push debit ${pursePub} not found`);
        return;
      }
      let newStatus: PeerPushPaymentInitiationStatus | undefined = undefined;
      switch (pushDebitRec.status) {
        case PeerPushPaymentInitiationStatus.AbortingRefresh:
        case PeerPushPaymentInitiationStatus.SuspendedAbortingRefresh:
          // FIXME: What to do about the refresh group?
          newStatus = PeerPushPaymentInitiationStatus.Failed;
          break;
        case PeerPushPaymentInitiationStatus.AbortingDeletePurse:
        case PeerPushPaymentInitiationStatus.SuspendedAbortingDeletePurse:
        case PeerPushPaymentInitiationStatus.PendingReady:
        case PeerPushPaymentInitiationStatus.SuspendedReady:
        case PeerPushPaymentInitiationStatus.SuspendedCreatePurse:
        case PeerPushPaymentInitiationStatus.PendingCreatePurse:
          newStatus = PeerPushPaymentInitiationStatus.Failed;
          break;
        case PeerPushPaymentInitiationStatus.Done:
        case PeerPushPaymentInitiationStatus.Aborted:
        case PeerPushPaymentInitiationStatus.Failed:
        case PeerPushPaymentInitiationStatus.Expired:
          // Do nothing
          break;
        default:
          assertUnreachable(pushDebitRec.status);
      }
      if (newStatus != null) {
        const oldTxState = computePeerPushDebitTransactionState(pushDebitRec);
        pushDebitRec.status = newStatus;
        const newTxState = computePeerPushDebitTransactionState(pushDebitRec);
        await tx.peerPushPaymentInitiations.put(pushDebitRec);
        return {
          oldTxState,
          newTxState,
        };
      }
      return undefined;
    });
  notifyTransition(ws, transactionId, transitionInfo);
}
export async function suspendPeerPushDebitTransaction(
  ws: InternalWalletState,
  pursePub: string,
) {
  const taskId = constructTaskIdentifier({
    tag: PendingTaskType.PeerPushDebit,
    pursePub,
  });
  const transactionId = constructTransactionIdentifier({
    tag: TransactionType.PeerPushDebit,
    pursePub,
  });
  stopLongpolling(ws, taskId);
  const transitionInfo = await ws.db
    .mktx((x) => [x.peerPushPaymentInitiations])
    .runReadWrite(async (tx) => {
      const pushDebitRec = await tx.peerPushPaymentInitiations.get(pursePub);
      if (!pushDebitRec) {
        logger.warn(`peer push debit ${pursePub} not found`);
        return;
      }
      let newStatus: PeerPushPaymentInitiationStatus | undefined = undefined;
      switch (pushDebitRec.status) {
        case PeerPushPaymentInitiationStatus.PendingCreatePurse:
          newStatus = PeerPushPaymentInitiationStatus.SuspendedCreatePurse;
          break;
        case PeerPushPaymentInitiationStatus.AbortingRefresh:
          newStatus = PeerPushPaymentInitiationStatus.SuspendedAbortingRefresh;
          break;
        case PeerPushPaymentInitiationStatus.AbortingDeletePurse:
          newStatus =
            PeerPushPaymentInitiationStatus.SuspendedAbortingDeletePurse;
          break;
        case PeerPushPaymentInitiationStatus.PendingReady:
          newStatus = PeerPushPaymentInitiationStatus.SuspendedReady;
          break;
        case PeerPushPaymentInitiationStatus.SuspendedAbortingDeletePurse:
        case PeerPushPaymentInitiationStatus.SuspendedAbortingRefresh:
        case PeerPushPaymentInitiationStatus.SuspendedReady:
        case PeerPushPaymentInitiationStatus.SuspendedCreatePurse:
        case PeerPushPaymentInitiationStatus.Done:
        case PeerPushPaymentInitiationStatus.Aborted:
        case PeerPushPaymentInitiationStatus.Failed:
        case PeerPushPaymentInitiationStatus.Expired:
          // Do nothing
          break;
        default:
          assertUnreachable(pushDebitRec.status);
      }
      if (newStatus != null) {
        const oldTxState = computePeerPushDebitTransactionState(pushDebitRec);
        pushDebitRec.status = newStatus;
        const newTxState = computePeerPushDebitTransactionState(pushDebitRec);
        await tx.peerPushPaymentInitiations.put(pushDebitRec);
        return {
          oldTxState,
          newTxState,
        };
      }
      return undefined;
    });
  notifyTransition(ws, transactionId, transitionInfo);
}
export async function resumePeerPushDebitTransaction(
  ws: InternalWalletState,
  pursePub: string,
) {
  const taskId = constructTaskIdentifier({
    tag: PendingTaskType.PeerPushDebit,
    pursePub,
  });
  const transactionId = constructTransactionIdentifier({
    tag: TransactionType.PeerPushDebit,
    pursePub,
  });
  stopLongpolling(ws, taskId);
  const transitionInfo = await ws.db
    .mktx((x) => [x.peerPushPaymentInitiations])
    .runReadWrite(async (tx) => {
      const pushDebitRec = await tx.peerPushPaymentInitiations.get(pursePub);
      if (!pushDebitRec) {
        logger.warn(`peer push debit ${pursePub} not found`);
        return;
      }
      let newStatus: PeerPushPaymentInitiationStatus | undefined = undefined;
      switch (pushDebitRec.status) {
        case PeerPushPaymentInitiationStatus.SuspendedAbortingDeletePurse:
          newStatus = PeerPushPaymentInitiationStatus.AbortingDeletePurse;
          break;
        case PeerPushPaymentInitiationStatus.SuspendedAbortingRefresh:
          newStatus = PeerPushPaymentInitiationStatus.AbortingRefresh;
          break;
        case PeerPushPaymentInitiationStatus.SuspendedReady:
          newStatus = PeerPushPaymentInitiationStatus.PendingReady;
          break;
        case PeerPushPaymentInitiationStatus.SuspendedCreatePurse:
          newStatus = PeerPushPaymentInitiationStatus.PendingCreatePurse;
          break;
        case PeerPushPaymentInitiationStatus.PendingCreatePurse:
        case PeerPushPaymentInitiationStatus.AbortingRefresh:
        case PeerPushPaymentInitiationStatus.AbortingDeletePurse:
        case PeerPushPaymentInitiationStatus.PendingReady:
        case PeerPushPaymentInitiationStatus.Done:
        case PeerPushPaymentInitiationStatus.Aborted:
        case PeerPushPaymentInitiationStatus.Failed:
        case PeerPushPaymentInitiationStatus.Expired:
          // Do nothing
          break;
        default:
          assertUnreachable(pushDebitRec.status);
      }
      if (newStatus != null) {
        const oldTxState = computePeerPushDebitTransactionState(pushDebitRec);
        pushDebitRec.status = newStatus;
        const newTxState = computePeerPushDebitTransactionState(pushDebitRec);
        await tx.peerPushPaymentInitiations.put(pushDebitRec);
        return {
          oldTxState,
          newTxState,
        };
      }
      return undefined;
    });
  ws.workAvailable.trigger();
  notifyTransition(ws, transactionId, transitionInfo);
}
export function computePeerPushDebitTransactionState(
  ppiRecord: PeerPushPaymentInitiationRecord,
): TransactionState {
  switch (ppiRecord.status) {
    case PeerPushPaymentInitiationStatus.PendingCreatePurse:
      return {
        major: TransactionMajorState.Pending,
        minor: TransactionMinorState.CreatePurse,
      };
    case PeerPushPaymentInitiationStatus.PendingReady:
      return {
        major: TransactionMajorState.Pending,
        minor: TransactionMinorState.Ready,
      };
    case PeerPushPaymentInitiationStatus.Aborted:
      return {
        major: TransactionMajorState.Aborted,
      };
    case PeerPushPaymentInitiationStatus.AbortingDeletePurse:
      return {
        major: TransactionMajorState.Aborting,
        minor: TransactionMinorState.DeletePurse,
      };
    case PeerPushPaymentInitiationStatus.AbortingRefresh:
      return {
        major: TransactionMajorState.Aborting,
        minor: TransactionMinorState.Refresh,
      };
    case PeerPushPaymentInitiationStatus.SuspendedAbortingDeletePurse:
      return {
        major: TransactionMajorState.SuspendedAborting,
        minor: TransactionMinorState.DeletePurse,
      };
    case PeerPushPaymentInitiationStatus.SuspendedAbortingRefresh:
      return {
        major: TransactionMajorState.SuspendedAborting,
        minor: TransactionMinorState.Refresh,
      };
    case PeerPushPaymentInitiationStatus.SuspendedCreatePurse:
      return {
        major: TransactionMajorState.Suspended,
        minor: TransactionMinorState.CreatePurse,
      };
    case PeerPushPaymentInitiationStatus.SuspendedReady:
      return {
        major: TransactionMajorState.Suspended,
        minor: TransactionMinorState.Ready,
      };
    case PeerPushPaymentInitiationStatus.Done:
      return {
        major: TransactionMajorState.Done,
      };
    case PeerPushPaymentInitiationStatus.Failed:
      return {
        major: TransactionMajorState.Failed,
      };
    case PeerPushPaymentInitiationStatus.Expired:
      return {
        major: TransactionMajorState.Expired,
      };
  }
}