/*
 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 <http://www.gnu.org/licenses/>
 */

import {
  AbsoluteTime,
  AcceptPeerPushPaymentResponse,
  Amounts,
  ConfirmPeerPushCreditRequest,
  ContractTermsUtil,
  ExchangePurseMergeRequest,
  ExchangeWalletKycStatus,
  HttpStatusCode,
  Logger,
  NotificationType,
  PeerContractTerms,
  PreparePeerPushCreditRequest,
  PreparePeerPushCreditResponse,
  TalerPreciseTimestamp,
  Transaction,
  TransactionAction,
  TransactionIdStr,
  TransactionMajorState,
  TransactionMinorState,
  TransactionState,
  TransactionType,
  WalletAccountMergeFlags,
  WalletKycUuid,
  assertUnreachable,
  checkDbInvariant,
  codecForAny,
  codecForExchangeGetContractResponse,
  codecForPeerContractTerms,
  codecForWalletKycUuid,
  decodeCrock,
  eddsaGetPublic,
  encodeCrock,
  getRandomBytes,
  j2s,
  parsePayPushUri,
  talerPaytoFromExchangeReserve,
} from "@gnu-taler/taler-util";
import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http";
import {
  PendingTaskType,
  TaskIdStr,
  TaskIdentifiers,
  TaskRunResult,
  TombstoneTag,
  TransactionContext,
  TransitionResult,
  TransitionResultType,
  constructTaskIdentifier,
  genericWaitForStateVal,
  requireExchangeTosAcceptedOrThrow,
} from "./common.js";
import {
  KycPendingInfo,
  OperationRetryRecord,
  PeerPushCreditStatus,
  PeerPushPaymentIncomingRecord,
  WalletDbAllStoresReadOnlyTransaction,
  WalletDbReadWriteTransaction,
  WalletDbStoresArr,
  WithdrawalGroupRecord,
  WithdrawalGroupStatus,
  WithdrawalRecordType,
  timestampPreciseFromDb,
  timestampPreciseToDb,
} from "./db.js";
import {
  BalanceThresholdCheckResult,
  checkIncomingAmountLegalUnderKycBalanceThreshold,
  fetchFreshExchange,
  getScopeForAllExchanges,
  handleStartExchangeWalletKyc,
} from "./exchanges.js";
import {
  codecForExchangePurseStatus,
  getMergeReserveInfo,
} from "./pay-peer-common.js";
import {
  TransitionInfo,
  constructTransactionIdentifier,
  isUnsuccessfulTransaction,
  notifyTransition,
  parseTransactionIdentifier,
} from "./transactions.js";
import { WalletExecutionContext } from "./wallet.js";
import {
  PerformCreateWithdrawalGroupResult,
  getExchangeWithdrawalInfo,
  internalPerformCreateWithdrawalGroup,
  internalPrepareCreateWithdrawalGroup,
  waitWithdrawalFinal,
} from "./withdraw.js";

const logger = new Logger("pay-peer-push-credit.ts");

export class PeerPushCreditTransactionContext implements TransactionContext {
  readonly transactionId: TransactionIdStr;
  readonly taskId: TaskIdStr;

  constructor(
    public wex: WalletExecutionContext,
    public peerPushCreditId: string,
  ) {
    this.transactionId = constructTransactionIdentifier({
      tag: TransactionType.PeerPushCredit,
      peerPushCreditId,
    });
    this.taskId = constructTaskIdentifier({
      tag: PendingTaskType.PeerPushCredit,
      peerPushCreditId,
    });
  }

  /**
   * Transition a peer-push-credit transaction.
   * Extra object stores may be accessed during the transition.
   */
  async transition<StoreNameArray extends WalletDbStoresArr = []>(
    opts: { extraStores?: StoreNameArray; transactionLabel?: string },
    f: (
      rec: PeerPushPaymentIncomingRecord | undefined,
      tx: WalletDbReadWriteTransaction<
        [
          "peerPushCredit",
          "transactionsMeta",
          "operationRetries",
          "exchanges",
          "exchangeDetails",
          ...StoreNameArray,
        ]
      >,
    ) => Promise<TransitionResult<PeerPushPaymentIncomingRecord>>,
  ): Promise<TransitionInfo | undefined> {
    const baseStores = [
      "peerPushCredit" as const,
      "transactionsMeta" as const,
      "operationRetries" as const,
      "exchanges" as const,
      "exchangeDetails" as const,
    ];
    const stores = opts.extraStores
      ? [...baseStores, ...opts.extraStores]
      : baseStores;

    let errorThrown: Error | undefined;
    const transitionInfo = await this.wex.db.runReadWriteTx(
      { storeNames: stores },
      async (tx) => {
        const rec = await tx.peerPushCredit.get(this.peerPushCreditId);
        let oldTxState: TransactionState;
        if (rec) {
          oldTxState = computePeerPushCreditTransactionState(rec);
        } else {
          oldTxState = {
            major: TransactionMajorState.None,
          };
        }
        let res: TransitionResult<PeerPushPaymentIncomingRecord> | undefined;
        try {
          res = await f(rec, tx);
        } catch (error) {
          if (error instanceof Error) {
            errorThrown = error;
          }
          return undefined;
        }

        switch (res.type) {
          case TransitionResultType.Transition: {
            await tx.peerPushCredit.put(res.rec);
            await this.updateTransactionMeta(tx);
            const newTxState = computePeerPushCreditTransactionState(res.rec);
            return {
              oldTxState,
              newTxState,
            };
          }
          case TransitionResultType.Delete:
            await tx.peerPushCredit.delete(this.peerPushCreditId);
            await this.updateTransactionMeta(tx);
            return {
              oldTxState,
              newTxState: {
                major: TransactionMajorState.None,
              },
            };
          default:
            return undefined;
        }
      },
    );
    if (errorThrown) {
      throw errorThrown;
    }
    notifyTransition(this.wex, this.transactionId, transitionInfo);
    return transitionInfo;
  }

  async updateTransactionMeta(
    tx: WalletDbReadWriteTransaction<["peerPushCredit", "transactionsMeta"]>,
  ): Promise<void> {
    const ppdRec = await tx.peerPushCredit.get(this.peerPushCreditId);
    if (!ppdRec) {
      await tx.transactionsMeta.delete(this.transactionId);
      return;
    }
    await tx.transactionsMeta.put({
      transactionId: this.transactionId,
      status: ppdRec.status,
      timestamp: ppdRec.timestamp,
      currency: Amounts.currencyOf(ppdRec.estimatedAmountEffective),
      exchanges: [ppdRec.exchangeBaseUrl],
    });
  }

  /**
   * Get the full transaction details for the transaction.
   *
   * Returns undefined if the transaction is in a state where we do not have a
   * transaction item (e.g. if it was deleted).
   */
  async lookupFullTransaction(
    tx: WalletDbAllStoresReadOnlyTransaction,
  ): Promise<Transaction | undefined> {
    const pushInc = await tx.peerPushCredit.get(this.peerPushCreditId);
    if (!pushInc) {
      return undefined;
    }

    let wg: WithdrawalGroupRecord | undefined = undefined;
    let wgRetryRecord: OperationRetryRecord | undefined = undefined;
    if (pushInc.withdrawalGroupId) {
      wg = await tx.withdrawalGroups.get(pushInc.withdrawalGroupId);
      if (wg) {
        const withdrawalOpId = TaskIdentifiers.forWithdrawal(wg);
        wgRetryRecord = await tx.operationRetries.get(withdrawalOpId);
      }
    }
    const pushIncOpId = TaskIdentifiers.forPeerPushCredit(pushInc);
    const pushRetryRecord = await tx.operationRetries.get(pushIncOpId);

    const ct = await tx.contractTerms.get(pushInc.contractTermsHash);

    if (!ct) {
      throw Error("contract terms for P2P payment not found");
    }

    const peerContractTerms = ct.contractTermsRaw;

    if (wg) {
      if (wg.wgInfo.withdrawalType !== WithdrawalRecordType.PeerPushCredit) {
        throw Error("invalid withdrawal group type for push payment credit");
      }
      checkDbInvariant(wg.instructedAmount !== undefined, "wg uninitialized");
      checkDbInvariant(wg.denomsSel !== undefined, "wg uninitialized");
      checkDbInvariant(wg.exchangeBaseUrl !== undefined, "wg uninitialized");

      const txState = computePeerPushCreditTransactionState(pushInc);
      return {
        type: TransactionType.PeerPushCredit,
        txState,
        scopes: await getScopeForAllExchanges(tx, [pushInc.exchangeBaseUrl]),
        txActions: computePeerPushCreditTransactionActions(pushInc),
        amountEffective: isUnsuccessfulTransaction(txState)
          ? Amounts.stringify(Amounts.zeroOfAmount(wg.instructedAmount))
          : Amounts.stringify(wg.denomsSel.totalCoinValue),
        amountRaw: Amounts.stringify(wg.instructedAmount),
        exchangeBaseUrl: wg.exchangeBaseUrl,
        info: {
          expiration: peerContractTerms.purse_expiration,
          summary: peerContractTerms.summary,
        },
        timestamp: timestampPreciseFromDb(wg.timestampStart),
        transactionId: constructTransactionIdentifier({
          tag: TransactionType.PeerPushCredit,
          peerPushCreditId: pushInc.peerPushCreditId,
        }),
        kycUrl: wg.kycUrl,
        kycPaytoHash: wg.kycPending?.paytoHash,
        ...(wgRetryRecord?.lastError ? { error: wgRetryRecord.lastError } : {}),
      };
    }

    const txState = computePeerPushCreditTransactionState(pushInc);
    return {
      type: TransactionType.PeerPushCredit,
      txState,
      scopes: await getScopeForAllExchanges(tx, [pushInc.exchangeBaseUrl]),
      txActions: computePeerPushCreditTransactionActions(pushInc),
      amountEffective: isUnsuccessfulTransaction(txState)
        ? Amounts.stringify(Amounts.zeroOfAmount(peerContractTerms.amount))
        : // FIXME: This is wrong, needs to consider fees!
          Amounts.stringify(peerContractTerms.amount),
      amountRaw: Amounts.stringify(peerContractTerms.amount),
      exchangeBaseUrl: pushInc.exchangeBaseUrl,
      info: {
        expiration: peerContractTerms.purse_expiration,
        summary: peerContractTerms.summary,
      },
      kycUrl: pushInc.kycUrl,
      kycPaytoHash: pushInc.kycInfo?.paytoHash,
      timestamp: timestampPreciseFromDb(pushInc.timestamp),
      transactionId: constructTransactionIdentifier({
        tag: TransactionType.PeerPushCredit,
        peerPushCreditId: pushInc.peerPushCreditId,
      }),
      ...(pushRetryRecord?.lastError
        ? { error: pushRetryRecord.lastError }
        : {}),
    };
  }

  async deleteTransaction(): Promise<void> {
    const { wex, peerPushCreditId } = this;
    await wex.db.runReadWriteTx(
      {
        storeNames: [
          "withdrawalGroups",
          "peerPushCredit",
          "tombstones",
          "transactionsMeta",
        ],
      },
      async (tx) => {
        const pushInc = await tx.peerPushCredit.get(peerPushCreditId);
        if (!pushInc) {
          return;
        }
        if (pushInc.withdrawalGroupId) {
          const withdrawalGroupId = pushInc.withdrawalGroupId;
          const withdrawalGroupRecord =
            await tx.withdrawalGroups.get(withdrawalGroupId);
          if (withdrawalGroupRecord) {
            await tx.withdrawalGroups.delete(withdrawalGroupId);
            await tx.tombstones.put({
              id: TombstoneTag.DeleteWithdrawalGroup + ":" + withdrawalGroupId,
            });
          }
        }
        await tx.peerPushCredit.delete(peerPushCreditId);
        await this.updateTransactionMeta(tx);
        await tx.tombstones.put({
          id: TombstoneTag.DeletePeerPushCredit + ":" + peerPushCreditId,
        });
      },
    );
    return;
  }

  async suspendTransaction(): Promise<void> {
    await this.transition({}, async (pushCreditRec) => {
      if (!pushCreditRec) {
        return TransitionResult.stay();
      }
      let newStatus: PeerPushCreditStatus | undefined = undefined;
      switch (pushCreditRec.status) {
        case PeerPushCreditStatus.DialogProposed:
        case PeerPushCreditStatus.Done:
        case PeerPushCreditStatus.SuspendedMerge:
        case PeerPushCreditStatus.SuspendedMergeKycRequired:
        case PeerPushCreditStatus.SuspendedWithdrawing:
        case PeerPushCreditStatus.SuspendedBalanceKycRequired:
        case PeerPushCreditStatus.SuspendedBalanceKycInit:
        case PeerPushCreditStatus.Failed:
        case PeerPushCreditStatus.Aborted:
          break;
        case PeerPushCreditStatus.PendingBalanceKycRequired:
          newStatus = PeerPushCreditStatus.SuspendedBalanceKycRequired;
          break;
        case PeerPushCreditStatus.PendingBalanceKycInit:
          newStatus = PeerPushCreditStatus.SuspendedBalanceKycInit;
          break;
        case PeerPushCreditStatus.PendingMergeKycRequired:
          newStatus = PeerPushCreditStatus.SuspendedMergeKycRequired;
          break;
        case PeerPushCreditStatus.PendingMerge:
          newStatus = PeerPushCreditStatus.SuspendedMerge;
          break;
        case PeerPushCreditStatus.PendingWithdrawing:
          // FIXME: Suspend internal withdrawal transaction!
          newStatus = PeerPushCreditStatus.SuspendedWithdrawing;
          break;
        default:
          assertUnreachable(pushCreditRec.status);
      }
      if (newStatus != null) {
        pushCreditRec.status = newStatus;
        return TransitionResult.transition(pushCreditRec);
      } else {
        return TransitionResult.stay();
      }
    });
  }

  async abortTransaction(): Promise<void> {
    const { wex, peerPushCreditId, taskId: retryTag, transactionId } = this;
    const transitionInfo = await wex.db.runReadWriteTx(
      { storeNames: ["peerPushCredit", "transactionsMeta"] },
      async (tx) => {
        const pushCreditRec = await tx.peerPushCredit.get(peerPushCreditId);
        if (!pushCreditRec) {
          logger.warn(`peer push credit ${peerPushCreditId} not found`);
          return;
        }
        let newStatus: PeerPushCreditStatus | undefined = undefined;
        switch (pushCreditRec.status) {
          case PeerPushCreditStatus.Failed:
          case PeerPushCreditStatus.Aborted:
          case PeerPushCreditStatus.Done:
            break;
          case PeerPushCreditStatus.SuspendedMerge:
          case PeerPushCreditStatus.DialogProposed:
          case PeerPushCreditStatus.SuspendedMergeKycRequired:
          case PeerPushCreditStatus.SuspendedWithdrawing:
          case PeerPushCreditStatus.PendingBalanceKycRequired:
          case PeerPushCreditStatus.SuspendedBalanceKycRequired:
          case PeerPushCreditStatus.PendingWithdrawing:
          case PeerPushCreditStatus.PendingMergeKycRequired:
          case PeerPushCreditStatus.PendingMerge:
          case PeerPushCreditStatus.PendingBalanceKycInit:
          case PeerPushCreditStatus.SuspendedBalanceKycInit:
            newStatus = PeerPushCreditStatus.Aborted;
            break;
          default:
            assertUnreachable(pushCreditRec.status);
        }
        if (newStatus != null) {
          const oldTxState =
            computePeerPushCreditTransactionState(pushCreditRec);
          pushCreditRec.status = newStatus;
          const newTxState =
            computePeerPushCreditTransactionState(pushCreditRec);
          await tx.peerPushCredit.put(pushCreditRec);
          await this.updateTransactionMeta(tx);
          return {
            oldTxState,
            newTxState,
          };
        }
        return undefined;
      },
    );
    notifyTransition(wex, transactionId, transitionInfo);
    wex.taskScheduler.startShepherdTask(retryTag);
  }

  async resumeTransaction(): Promise<void> {
    const { wex, peerPushCreditId, taskId: retryTag, transactionId } = this;
    const transitionInfo = await wex.db.runReadWriteTx(
      { storeNames: ["peerPushCredit", "transactionsMeta"] },
      async (tx) => {
        const pushCreditRec = await tx.peerPushCredit.get(peerPushCreditId);
        if (!pushCreditRec) {
          logger.warn(`peer push credit ${peerPushCreditId} not found`);
          return;
        }
        let newStatus: PeerPushCreditStatus | undefined = undefined;
        switch (pushCreditRec.status) {
          case PeerPushCreditStatus.DialogProposed:
          case PeerPushCreditStatus.PendingMergeKycRequired:
          case PeerPushCreditStatus.PendingMerge:
          case PeerPushCreditStatus.PendingWithdrawing:
          case PeerPushCreditStatus.PendingBalanceKycRequired:
          case PeerPushCreditStatus.PendingBalanceKycInit:
          case PeerPushCreditStatus.Done:
          case PeerPushCreditStatus.Aborted:
          case PeerPushCreditStatus.Failed:
            break;
          case PeerPushCreditStatus.SuspendedMerge:
            newStatus = PeerPushCreditStatus.PendingMerge;
            break;
          case PeerPushCreditStatus.SuspendedMergeKycRequired:
            newStatus = PeerPushCreditStatus.PendingMergeKycRequired;
            break;
          case PeerPushCreditStatus.SuspendedWithdrawing:
            // FIXME: resume underlying "internal-withdrawal" transaction.
            newStatus = PeerPushCreditStatus.PendingWithdrawing;
            break;
          case PeerPushCreditStatus.SuspendedBalanceKycRequired:
            newStatus = PeerPushCreditStatus.PendingBalanceKycRequired;
            break;
          case PeerPushCreditStatus.SuspendedBalanceKycInit:
            newStatus = PeerPushCreditStatus.PendingBalanceKycInit;
            break;
          default:
            assertUnreachable(pushCreditRec.status);
        }
        if (newStatus != null) {
          const oldTxState =
            computePeerPushCreditTransactionState(pushCreditRec);
          pushCreditRec.status = newStatus;
          const newTxState =
            computePeerPushCreditTransactionState(pushCreditRec);
          await tx.peerPushCredit.put(pushCreditRec);
          await this.updateTransactionMeta(tx);
          return {
            oldTxState,
            newTxState,
          };
        }
        return undefined;
      },
    );
    notifyTransition(wex, transactionId, transitionInfo);
    wex.taskScheduler.startShepherdTask(retryTag);
  }

  async failTransaction(): Promise<void> {
    const { wex, peerPushCreditId, taskId: retryTag, transactionId } = this;
    const transitionInfo = await wex.db.runReadWriteTx(
      { storeNames: ["peerPushCredit", "transactionsMeta"] },
      async (tx) => {
        const pushCreditRec = await tx.peerPushCredit.get(peerPushCreditId);
        if (!pushCreditRec) {
          logger.warn(`peer push credit ${peerPushCreditId} not found`);
          return;
        }
        let newStatus: PeerPushCreditStatus | undefined = undefined;
        switch (pushCreditRec.status) {
          case PeerPushCreditStatus.Done:
          case PeerPushCreditStatus.Aborted:
          case PeerPushCreditStatus.Failed:
            // Already in a final state.
            return;
          case PeerPushCreditStatus.DialogProposed:
          case PeerPushCreditStatus.PendingMergeKycRequired:
          case PeerPushCreditStatus.PendingMerge:
          case PeerPushCreditStatus.PendingWithdrawing:
          case PeerPushCreditStatus.SuspendedMerge:
          case PeerPushCreditStatus.SuspendedMergeKycRequired:
          case PeerPushCreditStatus.SuspendedWithdrawing:
          case PeerPushCreditStatus.PendingBalanceKycRequired:
          case PeerPushCreditStatus.SuspendedBalanceKycRequired:
          case PeerPushCreditStatus.PendingBalanceKycInit:
          case PeerPushCreditStatus.SuspendedBalanceKycInit:
            newStatus = PeerPushCreditStatus.Failed;
            break;
          default:
            assertUnreachable(pushCreditRec.status);
        }
        if (newStatus != null) {
          const oldTxState =
            computePeerPushCreditTransactionState(pushCreditRec);
          pushCreditRec.status = newStatus;
          const newTxState =
            computePeerPushCreditTransactionState(pushCreditRec);
          await tx.peerPushCredit.put(pushCreditRec);
          await this.updateTransactionMeta(tx);
          return {
            oldTxState,
            newTxState,
          };
        }
        return undefined;
      },
    );
    wex.taskScheduler.stopShepherdTask(retryTag);
    notifyTransition(wex, transactionId, transitionInfo);
    wex.taskScheduler.startShepherdTask(retryTag);
  }
}

export async function preparePeerPushCredit(
  wex: WalletExecutionContext,
  req: PreparePeerPushCreditRequest,
): Promise<PreparePeerPushCreditResponse> {
  const uri = parsePayPushUri(req.talerUri);

  if (!uri) {
    throw Error("got invalid taler://pay-push URI");
  }

  const existing = await wex.db.runReadOnlyTx(
    { storeNames: ["contractTerms", "peerPushCredit"] },
    async (tx) => {
      const existingPushInc =
        await tx.peerPushCredit.indexes.byExchangeAndContractPriv.get([
          uri.exchangeBaseUrl,
          uri.contractPriv,
        ]);
      if (!existingPushInc) {
        return;
      }
      const existingContractTermsRec = await tx.contractTerms.get(
        existingPushInc.contractTermsHash,
      );
      if (!existingContractTermsRec) {
        throw Error(
          "contract terms for peer push payment credit not found in database",
        );
      }
      const existingContractTerms = codecForPeerContractTerms().decode(
        existingContractTermsRec.contractTermsRaw,
      );
      return { existingPushInc, existingContractTerms };
    },
  );

  if (existing) {
    return {
      amount: existing.existingContractTerms.amount,
      amountEffective: existing.existingPushInc.estimatedAmountEffective,
      amountRaw: existing.existingContractTerms.amount,
      contractTerms: existing.existingContractTerms,
      peerPushCreditId: existing.existingPushInc.peerPushCreditId,
      transactionId: constructTransactionIdentifier({
        tag: TransactionType.PeerPushCredit,
        peerPushCreditId: existing.existingPushInc.peerPushCreditId,
      }),
      exchangeBaseUrl: existing.existingPushInc.exchangeBaseUrl,
    };
  }

  const exchangeBaseUrl = uri.exchangeBaseUrl;

  const contractPriv = uri.contractPriv;
  const contractPub = encodeCrock(eddsaGetPublic(decodeCrock(contractPriv)));

  const getContractUrl = new URL(`contracts/${contractPub}`, exchangeBaseUrl);

  const contractHttpResp = await wex.http.fetch(getContractUrl.href);

  const contractResp = await readSuccessResponseJsonOrThrow(
    contractHttpResp,
    codecForExchangeGetContractResponse(),
  );

  const pursePub = contractResp.purse_pub;

  const dec = await wex.cryptoApi.decryptContractForMerge({
    ciphertext: contractResp.econtract,
    contractPriv: contractPriv,
    pursePub: pursePub,
  });

  const getPurseUrl = new URL(`purses/${pursePub}/deposit`, exchangeBaseUrl);

  const purseHttpResp = await wex.http.fetch(getPurseUrl.href);

  const contractTerms = codecForPeerContractTerms().decode(dec.contractTerms);

  const purseStatus = await readSuccessResponseJsonOrThrow(
    purseHttpResp,
    codecForExchangePurseStatus(),
  );

  logger.info(
    `peer push credit, purse balance ${purseStatus.balance}, contract amount ${contractTerms.amount}`,
  );

  const peerPushCreditId = encodeCrock(getRandomBytes(32));

  const contractTermsHash = ContractTermsUtil.hashContractTerms(
    dec.contractTerms,
  );

  const withdrawalGroupId = encodeCrock(getRandomBytes(32));

  const wi = await getExchangeWithdrawalInfo(
    wex,
    exchangeBaseUrl,
    Amounts.parseOrThrow(purseStatus.balance),
    undefined,
  );

  if (wi.selectedDenoms.selectedDenoms.length === 0) {
    throw Error(
      `unable to prepare push credit from ${exchangeBaseUrl}, can't select denominations for instructed amount (${purseStatus.balance}`,
    );
  }

  const ctx = new PeerPushCreditTransactionContext(wex, withdrawalGroupId);

  const transitionInfo = await wex.db.runReadWriteTx(
    { storeNames: ["contractTerms", "peerPushCredit", "transactionsMeta"] },
    async (tx) => {
      const rec: PeerPushPaymentIncomingRecord = {
        peerPushCreditId,
        contractPriv: contractPriv,
        exchangeBaseUrl: exchangeBaseUrl,
        mergePriv: dec.mergePriv,
        pursePub: pursePub,
        timestamp: timestampPreciseToDb(TalerPreciseTimestamp.now()),
        contractTermsHash,
        status: PeerPushCreditStatus.DialogProposed,
        withdrawalGroupId,
        currency: Amounts.currencyOf(purseStatus.balance),
        estimatedAmountEffective: Amounts.stringify(
          wi.withdrawalAmountEffective,
        ),
      };
      await tx.peerPushCredit.add(rec);
      await ctx.updateTransactionMeta(tx);
      await tx.contractTerms.put({
        h: contractTermsHash,
        contractTermsRaw: dec.contractTerms,
      });

      const newTxState = computePeerPushCreditTransactionState(rec);

      return {
        oldTxState: {
          major: TransactionMajorState.None,
        },
        newTxState,
      } satisfies TransitionInfo;
    },
  );

  const transactionId = constructTransactionIdentifier({
    tag: TransactionType.PeerPushCredit,
    peerPushCreditId,
  });

  notifyTransition(wex, transactionId, transitionInfo);

  wex.ws.notify({
    type: NotificationType.BalanceChange,
    hintTransactionId: transactionId,
  });

  return {
    amount: purseStatus.balance,
    amountEffective: wi.withdrawalAmountEffective,
    amountRaw: purseStatus.balance,
    contractTerms: dec.contractTerms,
    peerPushCreditId,
    transactionId,
    exchangeBaseUrl,
  };
}

async function longpollKycStatus(
  wex: WalletExecutionContext,
  peerPushCreditId: string,
  exchangeUrl: string,
  kycInfo: KycPendingInfo,
): Promise<TaskRunResult> {
  const ctx = new PeerPushCreditTransactionContext(wex, peerPushCreditId);

  // FIXME: What if this changes? Should be part of the p2p record
  const mergeReserveInfo = await getMergeReserveInfo(wex, {
    exchangeBaseUrl: exchangeUrl,
  });

  const sigResp = await wex.cryptoApi.signWalletKycAuth({
    accountPriv: mergeReserveInfo.reservePriv,
    accountPub: mergeReserveInfo.reservePub,
  });

  const url = new URL(`kyc-check/${kycInfo.requirementRow}`, exchangeUrl);
  logger.info(`kyc url ${url.href}`);
  const kycStatusRes = await wex.ws.runLongpollQueueing(
    wex,
    url.hostname,
    async (timeoutMs) => {
      url.searchParams.set("timeout_ms", `${timeoutMs}`);
      return await wex.http.fetch(url.href, {
        method: "GET",
        headers: {
          ["Account-Owner-Signature"]: sigResp.sig,
        },
        cancellationToken: wex.cancellationToken,
      });
    },
  );

  if (
    kycStatusRes.status === HttpStatusCode.Ok ||
    kycStatusRes.status === HttpStatusCode.NoContent
  ) {
    const transitionInfo = await wex.db.runReadWriteTx(
      { storeNames: ["peerPushCredit", "transactionsMeta"] },
      async (tx) => {
        const peerInc = await tx.peerPushCredit.get(peerPushCreditId);
        if (!peerInc) {
          return;
        }
        if (peerInc.status !== PeerPushCreditStatus.PendingMergeKycRequired) {
          return;
        }
        const oldTxState = computePeerPushCreditTransactionState(peerInc);
        peerInc.status = PeerPushCreditStatus.PendingMerge;
        const newTxState = computePeerPushCreditTransactionState(peerInc);
        await tx.peerPushCredit.put(peerInc);
        await ctx.updateTransactionMeta(tx);
        return { oldTxState, newTxState };
      },
    );
    notifyTransition(wex, ctx.transactionId, transitionInfo);
    return TaskRunResult.progress();
  } else if (kycStatusRes.status === HttpStatusCode.Accepted) {
    // Access token / URL stays the same, just long-poll again.
    return TaskRunResult.longpollReturnedPending();
  } else {
    throw Error(`unexpected response from kyc-check (${kycStatusRes.status})`);
  }
}

async function processPeerPushCreditKycRequired(
  wex: WalletExecutionContext,
  peerInc: PeerPushPaymentIncomingRecord,
  kycPending: WalletKycUuid,
): Promise<TaskRunResult> {
  const transactionId = constructTransactionIdentifier({
    tag: TransactionType.PeerPushCredit,
    peerPushCreditId: peerInc.peerPushCreditId,
  });
  const ctx = new PeerPushCreditTransactionContext(
    wex,
    peerInc.peerPushCreditId,
  );
  const { peerPushCreditId } = peerInc;

  // FIXME: What if this changes? Should be part of the p2p record
  const mergeReserveInfo = await getMergeReserveInfo(wex, {
    exchangeBaseUrl: peerInc.exchangeBaseUrl,
  });

  const sigResp = await wex.cryptoApi.signWalletKycAuth({
    accountPriv: mergeReserveInfo.reservePriv,
    accountPub: mergeReserveInfo.reservePub,
  });

  const url = new URL(
    `kyc-check/${kycPending.requirement_row}`,
    peerInc.exchangeBaseUrl,
  );

  logger.info(`kyc url ${url.href}`);
  const kycStatusRes = await wex.http.fetch(url.href, {
    method: "GET",
    headers: {
      ["Account-Owner-Signature"]: sigResp.sig,
    },
    cancellationToken: wex.cancellationToken,
  });

  logger.info(`kyc result status ${kycStatusRes.status}`);

  if (
    kycStatusRes.status === HttpStatusCode.Ok ||
    kycStatusRes.status === HttpStatusCode.NoContent
  ) {
    logger.warn("kyc requested, but already fulfilled");
    return TaskRunResult.finished();
  } else if (kycStatusRes.status === HttpStatusCode.Accepted) {
    const kycStatus = await kycStatusRes.json();
    logger.info(`kyc status: ${j2s(kycStatus)}`);
    const { transitionInfo, result } = await wex.db.runReadWriteTx(
      { storeNames: ["peerPushCredit", "transactionsMeta"] },
      async (tx) => {
        const peerInc = await tx.peerPushCredit.get(peerPushCreditId);
        if (!peerInc) {
          return {
            transitionInfo: undefined,
            result: TaskRunResult.finished(),
          };
        }
        const oldTxState = computePeerPushCreditTransactionState(peerInc);
        peerInc.kycInfo = {
          paytoHash: kycPending.h_payto,
          requirementRow: kycPending.requirement_row,
        };
        peerInc.kycUrl = kycStatus.kyc_url;
        peerInc.status = PeerPushCreditStatus.PendingMergeKycRequired;
        const newTxState = computePeerPushCreditTransactionState(peerInc);
        await tx.peerPushCredit.put(peerInc);
        await ctx.updateTransactionMeta(tx);
        return {
          transitionInfo: { oldTxState, newTxState },
          result: TaskRunResult.progress(),
        };
      },
    );
    notifyTransition(wex, transactionId, transitionInfo);
    return result;
  } else {
    throw Error(`unexpected response from kyc-check (${kycStatusRes.status})`);
  }
}

async function handlePendingMerge(
  wex: WalletExecutionContext,
  peerInc: PeerPushPaymentIncomingRecord,
  contractTerms: PeerContractTerms,
): Promise<TaskRunResult> {
  const { peerPushCreditId } = peerInc;
  const ctx = new PeerPushCreditTransactionContext(wex, peerPushCreditId);

  const kycCheckRes = await checkIncomingAmountLegalUnderKycBalanceThreshold(
    wex,
    peerInc.exchangeBaseUrl,
    peerInc.estimatedAmountEffective,
  );

  if (kycCheckRes.result === "violation") {
    // Do this before we transition so that the exchange is already in the right state.
    await handleStartExchangeWalletKyc(wex, {
      amount: kycCheckRes.nextThreshold,
      exchangeBaseUrl: peerInc.exchangeBaseUrl,
    });
    await ctx.transition({}, async (rec) => {
      if (!rec) {
        return TransitionResult.stay();
      }
      if (rec.status !== PeerPushCreditStatus.PendingMerge) {
        return TransitionResult.stay();
      }
      rec.status = PeerPushCreditStatus.PendingBalanceKycInit;
      return TransitionResult.transition(rec);
    });
    return TaskRunResult.progress();
  }

  const amount = Amounts.parseOrThrow(contractTerms.amount);

  // FIXME: What if this changes? Should be part of the p2p record
  const mergeReserveInfo = await getMergeReserveInfo(wex, {
    exchangeBaseUrl: peerInc.exchangeBaseUrl,
  });

  const timestamp = timestampPreciseFromDb(peerInc.timestamp);

  const mergeTimestamp = AbsoluteTime.toProtocolTimestamp(
    AbsoluteTime.fromPreciseTimestamp(timestamp),
  );

  const reservePayto = talerPaytoFromExchangeReserve(
    peerInc.exchangeBaseUrl,
    mergeReserveInfo.reservePub,
  );

  const sigRes = await wex.cryptoApi.signPurseMerge({
    contractTermsHash: ContractTermsUtil.hashContractTerms(contractTerms),
    flags: WalletAccountMergeFlags.MergeFullyPaidPurse,
    mergePriv: peerInc.mergePriv,
    mergeTimestamp: mergeTimestamp,
    purseAmount: Amounts.stringify(amount),
    purseExpiration: contractTerms.purse_expiration,
    purseFee: Amounts.stringify(Amounts.zeroOfCurrency(amount.currency)),
    pursePub: peerInc.pursePub,
    reservePayto,
    reservePriv: mergeReserveInfo.reservePriv,
  });

  const mergePurseUrl = new URL(
    `purses/${peerInc.pursePub}/merge`,
    peerInc.exchangeBaseUrl,
  );

  const mergeReq: ExchangePurseMergeRequest = {
    payto_uri: reservePayto,
    merge_timestamp: mergeTimestamp,
    merge_sig: sigRes.mergeSig,
    reserve_sig: sigRes.accountSig,
  };

  const mergeHttpResp = await wex.http.fetch(mergePurseUrl.href, {
    method: "POST",
    body: mergeReq,
  });

  if (mergeHttpResp.status === HttpStatusCode.UnavailableForLegalReasons) {
    const respJson = await mergeHttpResp.json();
    const kycPending = codecForWalletKycUuid().decode(respJson);
    logger.info(`kyc uuid response: ${j2s(kycPending)}`);
    return processPeerPushCreditKycRequired(wex, peerInc, kycPending);
  }

  logger.trace(`merge request: ${j2s(mergeReq)}`);
  const res = await readSuccessResponseJsonOrThrow(
    mergeHttpResp,
    codecForAny(),
  );
  logger.trace(`merge response: ${j2s(res)}`);

  const withdrawalGroupPrep = await internalPrepareCreateWithdrawalGroup(wex, {
    amount,
    wgInfo: {
      withdrawalType: WithdrawalRecordType.PeerPushCredit,
    },
    forcedWithdrawalGroupId: peerInc.withdrawalGroupId,
    exchangeBaseUrl: peerInc.exchangeBaseUrl,
    reserveStatus: WithdrawalGroupStatus.PendingQueryingStatus,
    reserveKeyPair: {
      priv: mergeReserveInfo.reservePriv,
      pub: mergeReserveInfo.reservePub,
    },
  });

  const txRes = await wex.db.runReadWriteTx(
    {
      storeNames: [
        "contractTerms",
        "peerPushCredit",
        "withdrawalGroups",
        "reserves",
        "exchanges",
        "exchangeDetails",
        "transactionsMeta",
      ],
    },
    async (tx) => {
      const peerInc = await tx.peerPushCredit.get(peerPushCreditId);
      if (!peerInc) {
        return undefined;
      }
      const oldTxState = computePeerPushCreditTransactionState(peerInc);
      let wgCreateRes: PerformCreateWithdrawalGroupResult | undefined =
        undefined;
      switch (peerInc.status) {
        case PeerPushCreditStatus.PendingMerge:
        case PeerPushCreditStatus.PendingMergeKycRequired: {
          peerInc.status = PeerPushCreditStatus.PendingWithdrawing;
          wgCreateRes = await internalPerformCreateWithdrawalGroup(
            wex,
            tx,
            withdrawalGroupPrep,
          );
          peerInc.withdrawalGroupId =
            wgCreateRes.withdrawalGroup.withdrawalGroupId;
          break;
        }
      }
      await tx.peerPushCredit.put(peerInc);
      await ctx.updateTransactionMeta(tx);
      const newTxState = computePeerPushCreditTransactionState(peerInc);
      return {
        peerPushCreditTransition: { oldTxState, newTxState },
        wgCreateRes,
      };
    },
  );
  // Transaction was committed, now we can emit notifications.
  if (txRes?.wgCreateRes?.exchangeNotif) {
    wex.ws.notify(txRes.wgCreateRes.exchangeNotif);
  }
  notifyTransition(
    wex,
    withdrawalGroupPrep.transactionId,
    txRes?.wgCreateRes?.transitionInfo,
  );
  notifyTransition(wex, ctx.transactionId, txRes?.peerPushCreditTransition);

  return TaskRunResult.backoff();
}

async function handlePendingWithdrawing(
  wex: WalletExecutionContext,
  peerInc: PeerPushPaymentIncomingRecord,
): Promise<TaskRunResult> {
  if (!peerInc.withdrawalGroupId) {
    throw Error("invalid db state (withdrawing, but no withdrawal group ID");
  }
  await waitWithdrawalFinal(wex, peerInc.withdrawalGroupId);
  const ctx = new PeerPushCreditTransactionContext(
    wex,
    peerInc.peerPushCreditId,
  );
  const wgId = peerInc.withdrawalGroupId;
  let finished: boolean = false;
  const transitionInfo = await wex.db.runReadWriteTx(
    { storeNames: ["peerPushCredit", "withdrawalGroups", "transactionsMeta"] },
    async (tx) => {
      const ppi = await tx.peerPushCredit.get(peerInc.peerPushCreditId);
      if (!ppi) {
        finished = true;
        return;
      }
      if (ppi.status !== PeerPushCreditStatus.PendingWithdrawing) {
        finished = true;
        return;
      }
      const oldTxState = computePeerPushCreditTransactionState(ppi);
      const wg = await tx.withdrawalGroups.get(wgId);
      if (!wg) {
        // FIXME: Fail the operation instead?
        return undefined;
      }
      switch (wg.status) {
        case WithdrawalGroupStatus.Done:
          finished = true;
          ppi.status = PeerPushCreditStatus.Done;
          break;
        // FIXME: Also handle other final states!
      }
      await tx.peerPushCredit.put(ppi);
      await ctx.updateTransactionMeta(tx);
      const newTxState = computePeerPushCreditTransactionState(ppi);
      return {
        oldTxState,
        newTxState,
      };
    },
  );
  notifyTransition(wex, ctx.transactionId, transitionInfo);
  if (finished) {
    return TaskRunResult.finished();
  } else {
    // FIXME: Return indicator that we depend on the other operation!
    return TaskRunResult.backoff();
  }
}

export async function processPeerPushCredit(
  wex: WalletExecutionContext,
  peerPushCreditId: string,
): Promise<TaskRunResult> {
  if (!wex.ws.networkAvailable) {
    return TaskRunResult.networkRequired();
  }
  const ctx = new PeerPushCreditTransactionContext(wex, peerPushCreditId);

  let peerInc: PeerPushPaymentIncomingRecord | undefined;
  let contractTerms: PeerContractTerms | undefined;
  await wex.db.runReadWriteTx(
    { storeNames: ["contractTerms", "peerPushCredit", "transactionsMeta"] },
    async (tx) => {
      peerInc = await tx.peerPushCredit.get(peerPushCreditId);
      if (!peerInc) {
        return;
      }
      const ctRec = await tx.contractTerms.get(peerInc.contractTermsHash);
      if (ctRec) {
        contractTerms = ctRec.contractTermsRaw;
      }
      await tx.peerPushCredit.put(peerInc);
      await ctx.updateTransactionMeta(tx);
    },
  );

  if (!peerInc) {
    throw Error(
      `can't accept unknown incoming p2p push payment (${peerPushCreditId})`,
    );
  }

  logger.info(
    `processing peerPushCredit in state ${peerInc.status.toString(16)}`,
  );

  checkDbInvariant(
    !!contractTerms,
    `not contract terms for peer push ${peerPushCreditId}`,
  );

  switch (peerInc.status) {
    case PeerPushCreditStatus.PendingMergeKycRequired: {
      if (!peerInc.kycInfo) {
        throw Error("invalid state, kycInfo required");
      }
      return await longpollKycStatus(
        wex,
        peerPushCreditId,
        peerInc.exchangeBaseUrl,
        peerInc.kycInfo,
      );
    }
    case PeerPushCreditStatus.PendingMerge: {
      return handlePendingMerge(wex, peerInc, contractTerms);
    }
    case PeerPushCreditStatus.PendingWithdrawing: {
      return handlePendingWithdrawing(wex, peerInc);
    }
    case PeerPushCreditStatus.PendingBalanceKycInit:
    case PeerPushCreditStatus.PendingBalanceKycRequired: {
      return await processPeerPushCreditBalanceKyc(ctx, peerInc);
    }
    default:
      return TaskRunResult.finished();
  }
}

async function processPeerPushCreditBalanceKyc(
  ctx: PeerPushCreditTransactionContext,
  peerInc: PeerPushPaymentIncomingRecord,
): Promise<TaskRunResult> {
  const exchangeBaseUrl = peerInc.exchangeBaseUrl;
  const amount = peerInc.estimatedAmountEffective;

  const ret = await genericWaitForStateVal(ctx.wex, {
    async checkState(): Promise<BalanceThresholdCheckResult | undefined> {
      const checkRes = await checkIncomingAmountLegalUnderKycBalanceThreshold(
        ctx.wex,
        exchangeBaseUrl,
        amount,
      );
      logger.info(
        `balance check result for ${exchangeBaseUrl} +${amount}: ${j2s(
          checkRes,
        )}`,
      );
      if (checkRes.result === "ok") {
        return checkRes;
      }
      if (
        peerInc.status === PeerPushCreditStatus.PendingBalanceKycInit &&
        checkRes.walletKycStatus === ExchangeWalletKycStatus.Legi
      ) {
        return checkRes;
      }
      await handleStartExchangeWalletKyc(ctx.wex, {
        amount: checkRes.nextThreshold,
        exchangeBaseUrl,
      });
      return undefined;
    },
    filterNotification(notif) {
      return (
        (notif.type === NotificationType.ExchangeStateTransition &&
          notif.exchangeBaseUrl === exchangeBaseUrl) ||
        notif.type === NotificationType.BalanceChange
      );
    },
  });

  if (ret.result === "ok") {
    await ctx.transition({}, async (rec) => {
      if (!rec) {
        return TransitionResult.stay();
      }
      if (rec.status !== PeerPushCreditStatus.PendingBalanceKycRequired) {
        return TransitionResult.stay();
      }
      rec.status = PeerPushCreditStatus.PendingMerge;
      return TransitionResult.transition(rec);
    });
    return TaskRunResult.progress();
  } else if (
    peerInc.status === PeerPushCreditStatus.PendingBalanceKycInit &&
    ret.walletKycStatus === ExchangeWalletKycStatus.Legi
  ) {
    await ctx.transition({}, async (rec) => {
      if (!rec) {
        return TransitionResult.stay();
      }
      if (rec.status !== PeerPushCreditStatus.PendingBalanceKycInit) {
        return TransitionResult.stay();
      }
      rec.status = PeerPushCreditStatus.PendingBalanceKycRequired;
      delete rec.kycInfo;
      rec.kycAccessToken = ret.walletKycAccessToken;
      // FIXME: #9109 this should not be constructed here, it should be an opaque URL from exchange response
      rec.kycUrl = new URL(
        `kyc-spa/${ret.walletKycAccessToken}`,
        exchangeBaseUrl,
      ).href;
      return TransitionResult.transition(rec);
    });
    return TaskRunResult.progress();
  } else {
    throw Error("not reached");
  }
}

export async function confirmPeerPushCredit(
  wex: WalletExecutionContext,
  req: ConfirmPeerPushCreditRequest,
): Promise<AcceptPeerPushPaymentResponse> {
  const parsedTx = parseTransactionIdentifier(req.transactionId);
  if (!parsedTx) {
    throw Error("invalid transaction ID");
  }
  if (parsedTx.tag !== TransactionType.PeerPushCredit) {
    throw Error("invalid transaction ID type");
  }
  const ctx = new PeerPushCreditTransactionContext(
    wex,
    parsedTx.peerPushCreditId,
  );

  logger.trace(`confirming peer-push-credit ${ctx.peerPushCreditId}`);

  const peerInc = await wex.db.runReadWriteTx(
    { storeNames: ["contractTerms", "peerPushCredit", "transactionsMeta"] },
    async (tx) => {
      const rec = await tx.peerPushCredit.get(ctx.peerPushCreditId);
      if (!rec) {
        return;
      }
      if (rec.status === PeerPushCreditStatus.DialogProposed) {
        rec.status = PeerPushCreditStatus.PendingMerge;
      }
      await tx.peerPushCredit.put(rec);
      await ctx.updateTransactionMeta(tx);
      return rec;
    },
  );

  if (!peerInc) {
    throw Error(
      `can't accept unknown incoming p2p push payment (${req.transactionId})`,
    );
  }

  const exchange = await fetchFreshExchange(wex, peerInc.exchangeBaseUrl);
  requireExchangeTosAcceptedOrThrow(exchange);

  wex.taskScheduler.startShepherdTask(ctx.taskId);

  return {
    transactionId: ctx.transactionId,
  };
}

export function computePeerPushCreditTransactionState(
  pushCreditRecord: PeerPushPaymentIncomingRecord,
): TransactionState {
  switch (pushCreditRecord.status) {
    case PeerPushCreditStatus.DialogProposed:
      return {
        major: TransactionMajorState.Dialog,
        minor: TransactionMinorState.Proposed,
      };
    case PeerPushCreditStatus.PendingMerge:
      return {
        major: TransactionMajorState.Pending,
        minor: TransactionMinorState.Merge,
      };
    case PeerPushCreditStatus.Done:
      return {
        major: TransactionMajorState.Done,
      };
    case PeerPushCreditStatus.PendingMergeKycRequired:
      return {
        major: TransactionMajorState.Pending,
        minor: TransactionMinorState.MergeKycRequired,
      };
    case PeerPushCreditStatus.PendingWithdrawing:
      return {
        major: TransactionMajorState.Pending,
        minor: TransactionMinorState.Withdraw,
      };
    case PeerPushCreditStatus.SuspendedMerge:
      return {
        major: TransactionMajorState.Suspended,
        minor: TransactionMinorState.Merge,
      };
    case PeerPushCreditStatus.SuspendedMergeKycRequired:
      return {
        major: TransactionMajorState.Suspended,
        minor: TransactionMinorState.MergeKycRequired,
      };
    case PeerPushCreditStatus.SuspendedWithdrawing:
      return {
        major: TransactionMajorState.Suspended,
        minor: TransactionMinorState.Withdraw,
      };
    case PeerPushCreditStatus.Aborted:
      return {
        major: TransactionMajorState.Aborted,
      };
    case PeerPushCreditStatus.Failed:
      return {
        major: TransactionMajorState.Failed,
      };
    case PeerPushCreditStatus.PendingBalanceKycRequired:
      return {
        major: TransactionMajorState.Pending,
        minor: TransactionMinorState.BalanceKycRequired,
      };
    case PeerPushCreditStatus.SuspendedBalanceKycRequired:
      return {
        major: TransactionMajorState.Suspended,
        minor: TransactionMinorState.BalanceKycRequired,
      };
    case PeerPushCreditStatus.PendingBalanceKycInit:
      return {
        major: TransactionMajorState.Pending,
        minor: TransactionMinorState.BalanceKycInit,
      };
    case PeerPushCreditStatus.SuspendedBalanceKycInit:
      return {
        major: TransactionMajorState.Suspended,
        minor: TransactionMinorState.BalanceKycInit,
      };
    default:
      assertUnreachable(pushCreditRecord.status);
  }
}

export function computePeerPushCreditTransactionActions(
  pushCreditRecord: PeerPushPaymentIncomingRecord,
): TransactionAction[] {
  switch (pushCreditRecord.status) {
    case PeerPushCreditStatus.DialogProposed:
      return [TransactionAction.Retry, TransactionAction.Delete];
    case PeerPushCreditStatus.PendingMerge:
      return [
        TransactionAction.Retry,
        TransactionAction.Abort,
        TransactionAction.Suspend,
      ];
    case PeerPushCreditStatus.Done:
      return [TransactionAction.Delete];
    case PeerPushCreditStatus.PendingMergeKycRequired:
      return [
        TransactionAction.Retry,
        TransactionAction.Abort,
        TransactionAction.Suspend,
      ];
    case PeerPushCreditStatus.PendingWithdrawing:
      return [
        TransactionAction.Retry,
        TransactionAction.Suspend,
        TransactionAction.Fail,
      ];
    case PeerPushCreditStatus.SuspendedMerge:
      return [TransactionAction.Resume, TransactionAction.Abort];
    case PeerPushCreditStatus.SuspendedMergeKycRequired:
      return [TransactionAction.Resume, TransactionAction.Abort];
    case PeerPushCreditStatus.SuspendedWithdrawing:
      return [TransactionAction.Resume, TransactionAction.Fail];
    case PeerPushCreditStatus.PendingBalanceKycRequired:
      return [TransactionAction.Suspend, TransactionAction.Abort];
    case PeerPushCreditStatus.SuspendedBalanceKycRequired:
      return [TransactionAction.Resume, TransactionAction.Abort];
    case PeerPushCreditStatus.PendingBalanceKycInit:
      return [TransactionAction.Suspend, TransactionAction.Abort];
    case PeerPushCreditStatus.SuspendedBalanceKycInit:
      return [TransactionAction.Resume, TransactionAction.Abort];
    case PeerPushCreditStatus.Aborted:
      return [TransactionAction.Delete];
    case PeerPushCreditStatus.Failed:
      return [TransactionAction.Delete];
    default:
      assertUnreachable(pushCreditRecord.status);
  }
}
