/*
 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 {
  AcceptPeerPullPaymentResponse,
  Amounts,
  CoinRefreshRequest,
  ConfirmPeerPullDebitRequest,
  ContractTermsUtil,
  ExchangePurseDeposits,
  HttpStatusCode,
  Logger,
  NotificationType,
  PeerContractTerms,
  PreparePeerPullDebitRequest,
  PreparePeerPullDebitResponse,
  RefreshReason,
  TalerError,
  TalerErrorCode,
  TalerPreciseTimestamp,
  TalerProtocolViolationError,
  TransactionAction,
  TransactionMajorState,
  TransactionMinorState,
  TransactionState,
  TransactionType,
  codecForAny,
  codecForExchangeGetContractResponse,
  codecForPeerContractTerms,
  decodeCrock,
  eddsaGetPublic,
  encodeCrock,
  getRandomBytes,
  j2s,
  parsePayPullUri,
} from "@gnu-taler/taler-util";
import {
  HttpResponse,
  readSuccessResponseJsonOrThrow,
  readTalerErrorResponse,
} from "@gnu-taler/taler-util/http";
import {
  InternalWalletState,
  PeerPullDebitRecordStatus,
  PeerPullPaymentIncomingRecord,
  PendingTaskType,
  RefreshOperationStatus,
  createRefreshGroup,
  timestampPreciseToDb,
} from "../index.js";
import { assertUnreachable } from "../util/assertUnreachable.js";
import { checkLogicInvariant } from "../util/invariants.js";
import {
  TaskRunResult,
  TaskRunResultType,
  constructTaskIdentifier,
  spendCoins,
} from "./common.js";
import {
  codecForExchangePurseStatus,
  getTotalPeerPaymentCost,
  queryCoinInfosForSelection,
} from "./pay-peer-common.js";
import {
  constructTransactionIdentifier,
  notifyTransition,
  parseTransactionIdentifier,
  stopLongpolling,
} from "./transactions.js";
import { PeerCoinRepair, selectPeerCoins } from "../util/coinSelection.js";
const logger = new Logger("pay-peer-pull-debit.ts");
async function handlePurseCreationConflict(
  ws: InternalWalletState,
  peerPullInc: PeerPullPaymentIncomingRecord,
  resp: HttpResponse,
): Promise {
  const pursePub = peerPullInc.pursePub;
  const errResp = await readTalerErrorResponse(resp);
  if (errResp.code !== TalerErrorCode.EXCHANGE_GENERIC_INSUFFICIENT_FUNDS) {
    await failPeerPullDebitTransaction(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(peerPullInc.amount);
  const sel = peerPullInc.coinSel;
  if (!sel) {
    throw Error("invalid state (coin selection expected)");
  }
  const repair: PeerCoinRepair = {
    coinPubs: [],
    contribs: [],
    exchangeBaseUrl: peerPullInc.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",
    );
  }
  const totalAmount = await getTotalPeerPaymentCost(
    ws,
    coinSelRes.result.coins,
  );
  await ws.db
    .mktx((x) => [x.peerPullDebit])
    .runReadWrite(async (tx) => {
      const myPpi = await tx.peerPullDebit.get(peerPullInc.peerPullDebitId);
      if (!myPpi) {
        return;
      }
      switch (myPpi.status) {
        case PeerPullDebitRecordStatus.PendingDeposit:
        case PeerPullDebitRecordStatus.SuspendedDeposit: {
          const sel = coinSelRes.result;
          myPpi.coinSel = {
            coinPubs: sel.coins.map((x) => x.coinPub),
            contributions: sel.coins.map((x) => x.contribution),
            totalCost: Amounts.stringify(totalAmount),
          };
          break;
        }
        default:
          return;
      }
      await tx.peerPullDebit.put(myPpi);
    });
  return TaskRunResult.finished();
}
async function processPeerPullDebitPendingDeposit(
  ws: InternalWalletState,
  peerPullInc: PeerPullPaymentIncomingRecord,
): Promise {
  const peerPullDebitId = peerPullInc.peerPullDebitId;
  const pursePub = peerPullInc.pursePub;
  const coinSel = peerPullInc.coinSel;
  if (!coinSel) {
    throw Error("invalid state, no coins selected");
  }
  const coins = await queryCoinInfosForSelection(ws, coinSel);
  const depositSigsResp = await ws.cryptoApi.signPurseDeposits({
    exchangeBaseUrl: peerPullInc.exchangeBaseUrl,
    pursePub: peerPullInc.pursePub,
    coins,
  });
  const purseDepositUrl = new URL(
    `purses/${pursePub}/deposit`,
    peerPullInc.exchangeBaseUrl,
  );
  const depositPayload: ExchangePurseDeposits = {
    deposits: depositSigsResp.deposits,
  };
  if (logger.shouldLogTrace()) {
    logger.trace(`purse deposit payload: ${j2s(depositPayload)}`);
  }
  const transactionId = constructTransactionIdentifier({
    tag: TransactionType.PeerPullDebit,
    peerPullDebitId,
  });
  const httpResp = await ws.http.fetch(purseDepositUrl.href, {
    method: "POST",
    body: depositPayload,
  });
  switch (httpResp.status) {
    case HttpStatusCode.Ok: {
      const resp = await readSuccessResponseJsonOrThrow(
        httpResp,
        codecForAny(),
      );
      logger.trace(`purse deposit response: ${j2s(resp)}`);
      const transitionInfo = await ws.db
        .mktx((x) => [x.peerPullDebit])
        .runReadWrite(async (tx) => {
          const pi = await tx.peerPullDebit.get(peerPullDebitId);
          if (!pi) {
            throw Error("peer pull payment not found anymore");
          }
          if (pi.status !== PeerPullDebitRecordStatus.PendingDeposit) {
            return;
          }
          const oldTxState = computePeerPullDebitTransactionState(pi);
          pi.status = PeerPullDebitRecordStatus.Done;
          const newTxState = computePeerPullDebitTransactionState(pi);
          await tx.peerPullDebit.put(pi);
          return { oldTxState, newTxState };
        });
      notifyTransition(ws, transactionId, transitionInfo);
      break;
    }
    case HttpStatusCode.Gone: {
      const transitionInfo = await ws.db
        .mktx((x) => [
          x.peerPullDebit,
          x.refreshGroups,
          x.denominations,
          x.coinAvailability,
          x.coins,
        ])
        .runReadWrite(async (tx) => {
          const pi = await tx.peerPullDebit.get(peerPullDebitId);
          if (!pi) {
            throw Error("peer pull payment not found anymore");
          }
          if (pi.status !== PeerPullDebitRecordStatus.PendingDeposit) {
            return;
          }
          const oldTxState = computePeerPullDebitTransactionState(pi);
          const currency = Amounts.currencyOf(pi.totalCostEstimated);
          const coinPubs: CoinRefreshRequest[] = [];
          if (!pi.coinSel) {
            throw Error("invalid db state");
          }
          for (let i = 0; i < pi.coinSel.coinPubs.length; i++) {
            coinPubs.push({
              amount: pi.coinSel.contributions[i],
              coinPub: pi.coinSel.coinPubs[i],
            });
          }
          const refresh = await createRefreshGroup(
            ws,
            tx,
            currency,
            coinPubs,
            RefreshReason.AbortPeerPushDebit,
          );
          pi.status = PeerPullDebitRecordStatus.AbortingRefresh;
          pi.abortRefreshGroupId = refresh.refreshGroupId;
          const newTxState = computePeerPullDebitTransactionState(pi);
          await tx.peerPullDebit.put(pi);
          return { oldTxState, newTxState };
        });
      notifyTransition(ws, transactionId, transitionInfo);
      break;
    }
    case HttpStatusCode.Conflict: {
      return handlePurseCreationConflict(ws, peerPullInc, httpResp);
    }
    default: {
      const errResp = await readTalerErrorResponse(httpResp);
      return {
        type: TaskRunResultType.Error,
        errorDetail: errResp,
      };
    }
  }
  return TaskRunResult.finished();
}
async function processPeerPullDebitAbortingRefresh(
  ws: InternalWalletState,
  peerPullInc: PeerPullPaymentIncomingRecord,
): Promise {
  const peerPullDebitId = peerPullInc.peerPullDebitId;
  const abortRefreshGroupId = peerPullInc.abortRefreshGroupId;
  checkLogicInvariant(!!abortRefreshGroupId);
  const transactionId = constructTransactionIdentifier({
    tag: TransactionType.PeerPullDebit,
    peerPullDebitId,
  });
  const transitionInfo = await ws.db
    .mktx((x) => [x.refreshGroups, x.peerPullDebit])
    .runReadWrite(async (tx) => {
      const refreshGroup = await tx.refreshGroups.get(abortRefreshGroupId);
      let newOpState: PeerPullDebitRecordStatus | 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 = PeerPullDebitRecordStatus.Failed;
      } else {
        if (refreshGroup.operationStatus === RefreshOperationStatus.Finished) {
          newOpState = PeerPullDebitRecordStatus.Aborted;
        } else if (
          refreshGroup.operationStatus === RefreshOperationStatus.Failed
        ) {
          newOpState = PeerPullDebitRecordStatus.Failed;
        }
      }
      if (newOpState) {
        const newDg = await tx.peerPullDebit.get(peerPullDebitId);
        if (!newDg) {
          return;
        }
        const oldTxState = computePeerPullDebitTransactionState(newDg);
        newDg.status = newOpState;
        const newTxState = computePeerPullDebitTransactionState(newDg);
        await tx.peerPullDebit.put(newDg);
        return { oldTxState, newTxState };
      }
      return undefined;
    });
  notifyTransition(ws, transactionId, transitionInfo);
  // FIXME: Shouldn't this be finished in some cases?!
  return TaskRunResult.pending();
}
export async function processPeerPullDebit(
  ws: InternalWalletState,
  peerPullDebitId: string,
): Promise {
  const peerPullInc = await ws.db
    .mktx((x) => [x.peerPullDebit])
    .runReadOnly(async (tx) => {
      return tx.peerPullDebit.get(peerPullDebitId);
    });
  if (!peerPullInc) {
    throw Error("peer pull debit not found");
  }
  switch (peerPullInc.status) {
    case PeerPullDebitRecordStatus.PendingDeposit:
      return await processPeerPullDebitPendingDeposit(ws, peerPullInc);
    case PeerPullDebitRecordStatus.AbortingRefresh:
      return await processPeerPullDebitAbortingRefresh(ws, peerPullInc);
  }
  return TaskRunResult.finished();
}
export async function confirmPeerPullDebit(
  ws: InternalWalletState,
  req: ConfirmPeerPullDebitRequest,
): Promise {
  let peerPullDebitId: string;
  if (req.transactionId) {
    const parsedTx = parseTransactionIdentifier(req.transactionId);
    if (!parsedTx || parsedTx.tag !== TransactionType.PeerPullDebit) {
      throw Error("invalid peer-pull-debit transaction identifier");
    }
    peerPullDebitId = parsedTx.peerPullDebitId;
  } else if (req.peerPullDebitId) {
    peerPullDebitId = req.peerPullDebitId;
  } else {
    throw Error("invalid request, transactionId or peerPullDebitId required");
  }
  const peerPullInc = await ws.db
    .mktx((x) => [x.peerPullDebit])
    .runReadOnly(async (tx) => {
      return tx.peerPullDebit.get(peerPullDebitId);
    });
  if (!peerPullInc) {
    throw Error(
      `can't accept unknown incoming p2p pull payment (${req.peerPullDebitId})`,
    );
  }
  const instructedAmount = Amounts.parseOrThrow(peerPullInc.amount);
  const coinSelRes = await selectPeerCoins(ws, { instructedAmount });
  logger.info(`selected p2p coins (pull): ${j2s(coinSelRes)}`);
  if (coinSelRes.type !== "success") {
    throw TalerError.fromDetail(
      TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE,
      {
        insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails,
      },
    );
  }
  const sel = coinSelRes.result;
  const totalAmount = await getTotalPeerPaymentCost(
    ws,
    coinSelRes.result.coins,
  );
  const ppi = await ws.db
    .mktx((x) => [
      x.exchanges,
      x.coins,
      x.denominations,
      x.refreshGroups,
      x.peerPullDebit,
      x.coinAvailability,
    ])
    .runReadWrite(async (tx) => {
      await spendCoins(ws, tx, {
        // allocationId: `txn:peer-pull-debit:${req.peerPullDebitId}`,
        allocationId: constructTransactionIdentifier({
          tag: TransactionType.PeerPullDebit,
          peerPullDebitId,
        }),
        coinPubs: sel.coins.map((x) => x.coinPub),
        contributions: sel.coins.map((x) =>
          Amounts.parseOrThrow(x.contribution),
        ),
        refreshReason: RefreshReason.PayPeerPull,
      });
      const pi = await tx.peerPullDebit.get(peerPullDebitId);
      if (!pi) {
        throw Error();
      }
      if (pi.status === PeerPullDebitRecordStatus.DialogProposed) {
        pi.status = PeerPullDebitRecordStatus.PendingDeposit;
        pi.coinSel = {
          coinPubs: sel.coins.map((x) => x.coinPub),
          contributions: sel.coins.map((x) => x.contribution),
          totalCost: Amounts.stringify(totalAmount),
        };
      }
      await tx.peerPullDebit.put(pi);
      return pi;
    });
  ws.notify({ type: NotificationType.BalanceChange });
  const transactionId = constructTransactionIdentifier({
    tag: TransactionType.PeerPullDebit,
    peerPullDebitId,
  });
  return {
    transactionId,
  };
}
/**
 * Look up information about an incoming peer pull payment.
 * Store the results in the wallet DB.
 */
export async function preparePeerPullDebit(
  ws: InternalWalletState,
  req: PreparePeerPullDebitRequest,
): Promise {
  const uri = parsePayPullUri(req.talerUri);
  if (!uri) {
    throw Error("got invalid taler://pay-pull URI");
  }
  const existing = await ws.db
    .mktx((x) => [x.peerPullDebit, x.contractTerms])
    .runReadOnly(async (tx) => {
      const peerPullDebitRecord =
        await tx.peerPullDebit.indexes.byExchangeAndContractPriv.get([
          uri.exchangeBaseUrl,
          uri.contractPriv,
        ]);
      if (!peerPullDebitRecord) {
        return;
      }
      const contractTerms = await tx.contractTerms.get(
        peerPullDebitRecord.contractTermsHash,
      );
      if (!contractTerms) {
        return;
      }
      return { peerPullDebitRecord, contractTerms };
    });
  if (existing) {
    return {
      amount: existing.peerPullDebitRecord.amount,
      amountRaw: existing.peerPullDebitRecord.amount,
      amountEffective: existing.peerPullDebitRecord.totalCostEstimated,
      contractTerms: existing.contractTerms.contractTermsRaw,
      peerPullDebitId: existing.peerPullDebitRecord.peerPullDebitId,
      transactionId: constructTransactionIdentifier({
        tag: TransactionType.PeerPullDebit,
        peerPullDebitId: existing.peerPullDebitRecord.peerPullDebitId,
      }),
    };
  }
  const exchangeBaseUrl = uri.exchangeBaseUrl;
  const contractPriv = uri.contractPriv;
  const contractPub = encodeCrock(eddsaGetPublic(decodeCrock(contractPriv)));
  const getContractUrl = new URL(`contracts/${contractPub}`, exchangeBaseUrl);
  const contractHttpResp = await ws.http.fetch(getContractUrl.href);
  const contractResp = await readSuccessResponseJsonOrThrow(
    contractHttpResp,
    codecForExchangeGetContractResponse(),
  );
  const pursePub = contractResp.purse_pub;
  const dec = await ws.cryptoApi.decryptContractForDeposit({
    ciphertext: contractResp.econtract,
    contractPriv: contractPriv,
    pursePub: pursePub,
  });
  const getPurseUrl = new URL(`purses/${pursePub}/merge`, exchangeBaseUrl);
  const purseHttpResp = await ws.http.fetch(getPurseUrl.href);
  const purseStatus = await readSuccessResponseJsonOrThrow(
    purseHttpResp,
    codecForExchangePurseStatus(),
  );
  const peerPullDebitId = encodeCrock(getRandomBytes(32));
  let contractTerms: PeerContractTerms;
  if (dec.contractTerms) {
    contractTerms = codecForPeerContractTerms().decode(dec.contractTerms);
    // FIXME: Check that the purseStatus balance matches contract terms amount
  } else {
    // FIXME: In this case, where do we get the purse expiration from?!
    // https://bugs.gnunet.org/view.php?id=7706
    throw Error("pull payments without contract terms not supported yet");
  }
  const contractTermsHash = ContractTermsUtil.hashContractTerms(contractTerms);
  // FIXME: Why don't we compute the totalCost here?!
  const instructedAmount = Amounts.parseOrThrow(contractTerms.amount);
  const coinSelRes = await selectPeerCoins(ws, { instructedAmount });
  logger.info(`selected p2p coins (pull): ${j2s(coinSelRes)}`);
  if (coinSelRes.type !== "success") {
    throw TalerError.fromDetail(
      TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE,
      {
        insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails,
      },
    );
  }
  const totalAmount = await getTotalPeerPaymentCost(
    ws,
    coinSelRes.result.coins,
  );
  await ws.db
    .mktx((x) => [x.peerPullDebit, x.contractTerms])
    .runReadWrite(async (tx) => {
      await tx.contractTerms.put({
        h: contractTermsHash,
        contractTermsRaw: contractTerms,
      }),
        await tx.peerPullDebit.add({
          peerPullDebitId,
          contractPriv: contractPriv,
          exchangeBaseUrl: exchangeBaseUrl,
          pursePub: pursePub,
          timestampCreated: timestampPreciseToDb(TalerPreciseTimestamp.now()),
          contractTermsHash,
          amount: contractTerms.amount,
          status: PeerPullDebitRecordStatus.DialogProposed,
          totalCostEstimated: Amounts.stringify(totalAmount),
        });
    });
  return {
    amount: contractTerms.amount,
    amountEffective: Amounts.stringify(totalAmount),
    amountRaw: contractTerms.amount,
    contractTerms: contractTerms,
    peerPullDebitId,
    transactionId: constructTransactionIdentifier({
      tag: TransactionType.PeerPullDebit,
      peerPullDebitId: peerPullDebitId,
    }),
  };
}
export async function suspendPeerPullDebitTransaction(
  ws: InternalWalletState,
  peerPullDebitId: string,
) {
  const taskId = constructTaskIdentifier({
    tag: PendingTaskType.PeerPullDebit,
    peerPullDebitId,
  });
  const transactionId = constructTransactionIdentifier({
    tag: TransactionType.PeerPullDebit,
    peerPullDebitId,
  });
  stopLongpolling(ws, taskId);
  const transitionInfo = await ws.db
    .mktx((x) => [x.peerPullDebit])
    .runReadWrite(async (tx) => {
      const pullDebitRec = await tx.peerPullDebit.get(peerPullDebitId);
      if (!pullDebitRec) {
        logger.warn(`peer pull debit ${peerPullDebitId} not found`);
        return;
      }
      let newStatus: PeerPullDebitRecordStatus | undefined = undefined;
      switch (pullDebitRec.status) {
        case PeerPullDebitRecordStatus.DialogProposed:
          break;
        case PeerPullDebitRecordStatus.Done:
          break;
        case PeerPullDebitRecordStatus.PendingDeposit:
          newStatus = PeerPullDebitRecordStatus.SuspendedDeposit;
          break;
        case PeerPullDebitRecordStatus.SuspendedDeposit:
          break;
        case PeerPullDebitRecordStatus.Aborted:
          break;
        case PeerPullDebitRecordStatus.AbortingRefresh:
          newStatus = PeerPullDebitRecordStatus.SuspendedAbortingRefresh;
          break;
        case PeerPullDebitRecordStatus.Failed:
          break;
        case PeerPullDebitRecordStatus.SuspendedAbortingRefresh:
          break;
        default:
          assertUnreachable(pullDebitRec.status);
      }
      if (newStatus != null) {
        const oldTxState = computePeerPullDebitTransactionState(pullDebitRec);
        pullDebitRec.status = newStatus;
        const newTxState = computePeerPullDebitTransactionState(pullDebitRec);
        await tx.peerPullDebit.put(pullDebitRec);
        return {
          oldTxState,
          newTxState,
        };
      }
      return undefined;
    });
  notifyTransition(ws, transactionId, transitionInfo);
}
export async function abortPeerPullDebitTransaction(
  ws: InternalWalletState,
  peerPullDebitId: string,
) {
  const taskId = constructTaskIdentifier({
    tag: PendingTaskType.PeerPullDebit,
    peerPullDebitId,
  });
  const transactionId = constructTransactionIdentifier({
    tag: TransactionType.PeerPullDebit,
    peerPullDebitId,
  });
  stopLongpolling(ws, taskId);
  const transitionInfo = await ws.db
    .mktx((x) => [x.peerPullDebit])
    .runReadWrite(async (tx) => {
      const pullDebitRec = await tx.peerPullDebit.get(peerPullDebitId);
      if (!pullDebitRec) {
        logger.warn(`peer pull debit ${peerPullDebitId} not found`);
        return;
      }
      let newStatus: PeerPullDebitRecordStatus | undefined = undefined;
      switch (pullDebitRec.status) {
        case PeerPullDebitRecordStatus.DialogProposed:
          newStatus = PeerPullDebitRecordStatus.Aborted;
          break;
        case PeerPullDebitRecordStatus.Done:
          break;
        case PeerPullDebitRecordStatus.PendingDeposit:
          newStatus = PeerPullDebitRecordStatus.AbortingRefresh;
          break;
        case PeerPullDebitRecordStatus.SuspendedDeposit:
          break;
        case PeerPullDebitRecordStatus.Aborted:
          break;
        case PeerPullDebitRecordStatus.AbortingRefresh:
          break;
        case PeerPullDebitRecordStatus.Failed:
          break;
        case PeerPullDebitRecordStatus.SuspendedAbortingRefresh:
          break;
        default:
          assertUnreachable(pullDebitRec.status);
      }
      if (newStatus != null) {
        const oldTxState = computePeerPullDebitTransactionState(pullDebitRec);
        pullDebitRec.status = newStatus;
        const newTxState = computePeerPullDebitTransactionState(pullDebitRec);
        await tx.peerPullDebit.put(pullDebitRec);
        return {
          oldTxState,
          newTxState,
        };
      }
      return undefined;
    });
  notifyTransition(ws, transactionId, transitionInfo);
}
export async function failPeerPullDebitTransaction(
  ws: InternalWalletState,
  peerPullDebitId: string,
) {
  const taskId = constructTaskIdentifier({
    tag: PendingTaskType.PeerPullDebit,
    peerPullDebitId,
  });
  const transactionId = constructTransactionIdentifier({
    tag: TransactionType.PeerPullDebit,
    peerPullDebitId,
  });
  stopLongpolling(ws, taskId);
  const transitionInfo = await ws.db
    .mktx((x) => [x.peerPullDebit])
    .runReadWrite(async (tx) => {
      const pullDebitRec = await tx.peerPullDebit.get(peerPullDebitId);
      if (!pullDebitRec) {
        logger.warn(`peer pull debit ${peerPullDebitId} not found`);
        return;
      }
      let newStatus: PeerPullDebitRecordStatus | undefined = undefined;
      switch (pullDebitRec.status) {
        case PeerPullDebitRecordStatus.DialogProposed:
          newStatus = PeerPullDebitRecordStatus.Aborted;
          break;
        case PeerPullDebitRecordStatus.Done:
          break;
        case PeerPullDebitRecordStatus.PendingDeposit:
          break;
        case PeerPullDebitRecordStatus.SuspendedDeposit:
          break;
        case PeerPullDebitRecordStatus.Aborted:
          break;
        case PeerPullDebitRecordStatus.Failed:
          break;
        case PeerPullDebitRecordStatus.SuspendedAbortingRefresh:
        case PeerPullDebitRecordStatus.AbortingRefresh:
          // FIXME: abort underlying refresh!
          newStatus = PeerPullDebitRecordStatus.Failed;
          break;
        default:
          assertUnreachable(pullDebitRec.status);
      }
      if (newStatus != null) {
        const oldTxState = computePeerPullDebitTransactionState(pullDebitRec);
        pullDebitRec.status = newStatus;
        const newTxState = computePeerPullDebitTransactionState(pullDebitRec);
        await tx.peerPullDebit.put(pullDebitRec);
        return {
          oldTxState,
          newTxState,
        };
      }
      return undefined;
    });
  notifyTransition(ws, transactionId, transitionInfo);
}
export async function resumePeerPullDebitTransaction(
  ws: InternalWalletState,
  peerPullDebitId: string,
) {
  const taskId = constructTaskIdentifier({
    tag: PendingTaskType.PeerPullDebit,
    peerPullDebitId,
  });
  const transactionId = constructTransactionIdentifier({
    tag: TransactionType.PeerPullDebit,
    peerPullDebitId,
  });
  stopLongpolling(ws, taskId);
  const transitionInfo = await ws.db
    .mktx((x) => [x.peerPullDebit])
    .runReadWrite(async (tx) => {
      const pullDebitRec = await tx.peerPullDebit.get(peerPullDebitId);
      if (!pullDebitRec) {
        logger.warn(`peer pull debit ${peerPullDebitId} not found`);
        return;
      }
      let newStatus: PeerPullDebitRecordStatus | undefined = undefined;
      switch (pullDebitRec.status) {
        case PeerPullDebitRecordStatus.DialogProposed:
        case PeerPullDebitRecordStatus.Done:
        case PeerPullDebitRecordStatus.PendingDeposit:
          break;
        case PeerPullDebitRecordStatus.SuspendedDeposit:
          newStatus = PeerPullDebitRecordStatus.PendingDeposit;
          break;
        case PeerPullDebitRecordStatus.Aborted:
          break;
        case PeerPullDebitRecordStatus.AbortingRefresh:
          break;
        case PeerPullDebitRecordStatus.Failed:
          break;
        case PeerPullDebitRecordStatus.SuspendedAbortingRefresh:
          newStatus = PeerPullDebitRecordStatus.AbortingRefresh;
          break;
        default:
          assertUnreachable(pullDebitRec.status);
      }
      if (newStatus != null) {
        const oldTxState = computePeerPullDebitTransactionState(pullDebitRec);
        pullDebitRec.status = newStatus;
        const newTxState = computePeerPullDebitTransactionState(pullDebitRec);
        await tx.peerPullDebit.put(pullDebitRec);
        return {
          oldTxState,
          newTxState,
        };
      }
      return undefined;
    });
  ws.workAvailable.trigger();
  notifyTransition(ws, transactionId, transitionInfo);
}
export function computePeerPullDebitTransactionState(
  pullDebitRecord: PeerPullPaymentIncomingRecord,
): TransactionState {
  switch (pullDebitRecord.status) {
    case PeerPullDebitRecordStatus.DialogProposed:
      return {
        major: TransactionMajorState.Dialog,
        minor: TransactionMinorState.Proposed,
      };
    case PeerPullDebitRecordStatus.PendingDeposit:
      return {
        major: TransactionMajorState.Pending,
        minor: TransactionMinorState.Deposit,
      };
    case PeerPullDebitRecordStatus.Done:
      return {
        major: TransactionMajorState.Done,
      };
    case PeerPullDebitRecordStatus.SuspendedDeposit:
      return {
        major: TransactionMajorState.Suspended,
        minor: TransactionMinorState.Deposit,
      };
    case PeerPullDebitRecordStatus.Aborted:
      return {
        major: TransactionMajorState.Aborted,
      };
    case PeerPullDebitRecordStatus.AbortingRefresh:
      return {
        major: TransactionMajorState.Aborting,
        minor: TransactionMinorState.Refresh,
      };
    case PeerPullDebitRecordStatus.Failed:
      return {
        major: TransactionMajorState.Failed,
      };
    case PeerPullDebitRecordStatus.SuspendedAbortingRefresh:
      return {
        major: TransactionMajorState.SuspendedAborting,
        minor: TransactionMinorState.Refresh,
      };
  }
}
export function computePeerPullDebitTransactionActions(
  pullDebitRecord: PeerPullPaymentIncomingRecord,
): TransactionAction[] {
  switch (pullDebitRecord.status) {
    case PeerPullDebitRecordStatus.DialogProposed:
      return [];
    case PeerPullDebitRecordStatus.PendingDeposit:
      return [TransactionAction.Abort, TransactionAction.Suspend];
    case PeerPullDebitRecordStatus.Done:
      return [TransactionAction.Delete];
    case PeerPullDebitRecordStatus.SuspendedDeposit:
      return [TransactionAction.Resume, TransactionAction.Abort];
    case PeerPullDebitRecordStatus.Aborted:
      return [TransactionAction.Delete];
    case PeerPullDebitRecordStatus.AbortingRefresh:
      return [TransactionAction.Fail, TransactionAction.Suspend];
    case PeerPullDebitRecordStatus.Failed:
      return [TransactionAction.Delete];
    case PeerPullDebitRecordStatus.SuspendedAbortingRefresh:
      return [TransactionAction.Resume, TransactionAction.Fail];
  }
}