/*
 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 {
  AcceptPeerPushPaymentResponse,
  Amounts,
  ConfirmPeerPushCreditRequest,
  ContractTermsUtil,
  ExchangePurseMergeRequest,
  HttpStatusCode,
  Logger,
  NotificationType,
  PeerContractTerms,
  PreparePeerPushCreditRequest,
  PreparePeerPushCreditResponse,
  TalerErrorCode,
  TalerPreciseTimestamp,
  TalerProtocolTimestamp,
  TransactionAction,
  TransactionMajorState,
  TransactionMinorState,
  TransactionState,
  TransactionType,
  WalletAccountMergeFlags,
  WalletKycUuid,
  codecForAny,
  codecForExchangeGetContractResponse,
  codecForPeerContractTerms,
  codecForWalletKycUuid,
  decodeCrock,
  eddsaGetPublic,
  encodeCrock,
  getRandomBytes,
  j2s,
  makeErrorDetail,
  parsePayPushUri,
  talerPaytoFromExchangeReserve,
} from "@gnu-taler/taler-util";
import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http";
import {
  InternalWalletState,
  KycPendingInfo,
  KycUserType,
  PeerPushPaymentIncomingRecord,
  PeerPushPaymentIncomingStatus,
  PendingTaskType,
  WithdrawalGroupStatus,
  WithdrawalRecordType,
} from "../index.js";
import { assertUnreachable } from "../util/assertUnreachable.js";
import { checkDbInvariant } from "../util/invariants.js";
import {
  TaskRunResult,
  TaskRunResultType,
  constructTaskIdentifier,
  runLongpollAsync,
} from "./common.js";
import { updateExchangeFromUrl } from "./exchanges.js";
import {
  codecForExchangePurseStatus,
  getMergeReserveInfo,
} from "./pay-peer-common.js";
import {
  TransitionInfo,
  constructTransactionIdentifier,
  notifyTransition,
  parseTransactionIdentifier,
  stopLongpolling,
} from "./transactions.js";
import {
  getExchangeWithdrawalInfo,
  internalPerformCreateWithdrawalGroup,
  internalPrepareCreateWithdrawalGroup,
} from "./withdraw.js";
const logger = new Logger("pay-peer-push-credit.ts");
export async function preparePeerPushCredit(
  ws: InternalWalletState,
  req: PreparePeerPushCreditRequest,
): Promise {
  const uri = parsePayPushUri(req.talerUri);
  if (!uri) {
    throw Error("got invalid taler://pay-push URI");
  }
  const existing = await ws.db
    .mktx((x) => [x.contractTerms, x.peerPushPaymentIncoming])
    .runReadOnly(async (tx) => {
      const existingPushInc =
        await tx.peerPushPaymentIncoming.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,
      peerPushPaymentIncomingId:
        existing.existingPushInc.peerPushPaymentIncomingId,
      transactionId: constructTransactionIdentifier({
        tag: TransactionType.PeerPushCredit,
        peerPushPaymentIncomingId:
          existing.existingPushInc.peerPushPaymentIncomingId,
      }),
    };
  }
  const exchangeBaseUrl = uri.exchangeBaseUrl;
  await updateExchangeFromUrl(ws, 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.decryptContractForMerge({
    ciphertext: contractResp.econtract,
    contractPriv: contractPriv,
    pursePub: pursePub,
  });
  const getPurseUrl = new URL(`purses/${pursePub}/deposit`, exchangeBaseUrl);
  const purseHttpResp = await ws.http.fetch(getPurseUrl.href);
  const purseStatus = await readSuccessResponseJsonOrThrow(
    purseHttpResp,
    codecForExchangePurseStatus(),
  );
  const peerPushPaymentIncomingId = encodeCrock(getRandomBytes(32));
  const contractTermsHash = ContractTermsUtil.hashContractTerms(
    dec.contractTerms,
  );
  const withdrawalGroupId = encodeCrock(getRandomBytes(32));
  const wi = await getExchangeWithdrawalInfo(
    ws,
    exchangeBaseUrl,
    Amounts.parseOrThrow(purseStatus.balance),
    undefined,
  );
  await ws.db
    .mktx((x) => [x.contractTerms, x.peerPushPaymentIncoming])
    .runReadWrite(async (tx) => {
      await tx.peerPushPaymentIncoming.add({
        peerPushPaymentIncomingId,
        contractPriv: contractPriv,
        exchangeBaseUrl: exchangeBaseUrl,
        mergePriv: dec.mergePriv,
        pursePub: pursePub,
        timestamp: TalerPreciseTimestamp.now(),
        contractTermsHash,
        status: PeerPushPaymentIncomingStatus.DialogProposed,
        withdrawalGroupId,
        currency: Amounts.currencyOf(purseStatus.balance),
        estimatedAmountEffective: Amounts.stringify(
          wi.withdrawalAmountEffective,
        ),
      });
      await tx.contractTerms.put({
        h: contractTermsHash,
        contractTermsRaw: dec.contractTerms,
      });
    });
  ws.notify({ type: NotificationType.BalanceChange });
  return {
    amount: purseStatus.balance,
    amountEffective: wi.withdrawalAmountEffective,
    amountRaw: purseStatus.balance,
    contractTerms: dec.contractTerms,
    peerPushPaymentIncomingId,
    transactionId: constructTransactionIdentifier({
      tag: TransactionType.PeerPushCredit,
      peerPushPaymentIncomingId,
    }),
  };
}
async function longpollKycStatus(
  ws: InternalWalletState,
  peerPushPaymentIncomingId: string,
  exchangeUrl: string,
  kycInfo: KycPendingInfo,
  userType: KycUserType,
): Promise {
  const transactionId = constructTransactionIdentifier({
    tag: TransactionType.PeerPushCredit,
    peerPushPaymentIncomingId,
  });
  const retryTag = constructTaskIdentifier({
    tag: PendingTaskType.PeerPushCredit,
    peerPushPaymentIncomingId,
  });
  runLongpollAsync(ws, retryTag, async (ct) => {
    const url = new URL(
      `kyc-check/${kycInfo.requirementRow}/${kycInfo.paytoHash}/${userType}`,
      exchangeUrl,
    );
    url.searchParams.set("timeout_ms", "10000");
    logger.info(`kyc url ${url.href}`);
    const kycStatusRes = await ws.http.fetch(url.href, {
      method: "GET",
      cancellationToken: ct,
    });
    if (
      kycStatusRes.status === HttpStatusCode.Ok ||
      //FIXME: NoContent is not expected https://docs.taler.net/core/api-exchange.html#post--purses-$PURSE_PUB-merge
      // remove after the exchange is fixed or clarified
      kycStatusRes.status === HttpStatusCode.NoContent
    ) {
      const transitionInfo = await ws.db
        .mktx((x) => [x.peerPushPaymentIncoming])
        .runReadWrite(async (tx) => {
          const peerInc = await tx.peerPushPaymentIncoming.get(
            peerPushPaymentIncomingId,
          );
          if (!peerInc) {
            return;
          }
          if (
            peerInc.status !==
            PeerPushPaymentIncomingStatus.PendingMergeKycRequired
          ) {
            return;
          }
          const oldTxState = computePeerPushCreditTransactionState(peerInc);
          peerInc.status = PeerPushPaymentIncomingStatus.PendingMerge;
          const newTxState = computePeerPushCreditTransactionState(peerInc);
          await tx.peerPushPaymentIncoming.put(peerInc);
          return { oldTxState, newTxState };
        });
      notifyTransition(ws, transactionId, transitionInfo);
      return { ready: true };
    } else if (kycStatusRes.status === HttpStatusCode.Accepted) {
      // FIXME: Do we have to update the URL here?
      return { ready: false };
    } else {
      throw Error(
        `unexpected response from kyc-check (${kycStatusRes.status})`,
      );
    }
  });
  return {
    type: TaskRunResultType.Longpoll,
  };
}
async function processPeerPushCreditKycRequired(
  ws: InternalWalletState,
  peerInc: PeerPushPaymentIncomingRecord,
  kycPending: WalletKycUuid,
): Promise {
  const transactionId = constructTransactionIdentifier({
    tag: TransactionType.PeerPushCredit,
    peerPushPaymentIncomingId: peerInc.peerPushPaymentIncomingId,
  });
  const { peerPushPaymentIncomingId } = peerInc;
  const userType = "individual";
  const url = new URL(
    `kyc-check/${kycPending.requirement_row}/${kycPending.h_payto}/${userType}`,
    peerInc.exchangeBaseUrl,
  );
  logger.info(`kyc url ${url.href}`);
  const kycStatusRes = await ws.http.fetch(url.href, {
    method: "GET",
  });
  if (
    kycStatusRes.status === HttpStatusCode.Ok ||
    //FIXME: NoContent is not expected https://docs.taler.net/core/api-exchange.html#post--purses-$PURSE_PUB-merge
    // remove after the exchange is fixed or clarified
    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 ws.db
      .mktx((x) => [x.peerPushPaymentIncoming])
      .runReadWrite(async (tx) => {
        const peerInc = await tx.peerPushPaymentIncoming.get(
          peerPushPaymentIncomingId,
        );
        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 = PeerPushPaymentIncomingStatus.PendingMergeKycRequired;
        const newTxState = computePeerPushCreditTransactionState(peerInc);
        await tx.peerPushPaymentIncoming.put(peerInc);
        // We'll remove this eventually!  New clients should rely on the
        // kycUrl field of the transaction, not the error code.
        const res: TaskRunResult = {
          type: TaskRunResultType.Error,
          errorDetail: makeErrorDetail(
            TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED,
            {
              kycUrl: kycStatus.kyc_url,
            },
          ),
        };
        return {
          transitionInfo: { oldTxState, newTxState },
          result: res,
        };
      });
    notifyTransition(ws, transactionId, transitionInfo);
    return result;
  } else {
    throw Error(`unexpected response from kyc-check (${kycStatusRes.status})`);
  }
}
async function handlePendingMerge(
  ws: InternalWalletState,
  peerInc: PeerPushPaymentIncomingRecord,
  contractTerms: PeerContractTerms,
): Promise {
  const { peerPushPaymentIncomingId } = peerInc;
  const transactionId = constructTransactionIdentifier({
    tag: TransactionType.PeerPushCredit,
    peerPushPaymentIncomingId,
  });
  const amount = Amounts.parseOrThrow(contractTerms.amount);
  const mergeReserveInfo = await getMergeReserveInfo(ws, {
    exchangeBaseUrl: peerInc.exchangeBaseUrl,
  });
  const mergeTimestamp = TalerProtocolTimestamp.now();
  const reservePayto = talerPaytoFromExchangeReserve(
    peerInc.exchangeBaseUrl,
    mergeReserveInfo.reservePub,
  );
  const sigRes = await ws.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 ws.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(ws, peerInc, kycPending);
  }
  logger.trace(`merge request: ${j2s(mergeReq)}`);
  const res = await readSuccessResponseJsonOrThrow(
    mergeHttpResp,
    codecForAny(),
  );
  logger.trace(`merge response: ${j2s(res)}`);
  const withdrawalGroupPrep = await internalPrepareCreateWithdrawalGroup(ws, {
    amount,
    wgInfo: {
      withdrawalType: WithdrawalRecordType.PeerPushCredit,
      contractTerms,
    },
    forcedWithdrawalGroupId: peerInc.withdrawalGroupId,
    exchangeBaseUrl: peerInc.exchangeBaseUrl,
    reserveStatus: WithdrawalGroupStatus.PendingQueryingStatus,
    reserveKeyPair: {
      priv: mergeReserveInfo.reservePriv,
      pub: mergeReserveInfo.reservePub,
    },
  });
  const txRes = await ws.db
    .mktx((x) => [
      x.contractTerms,
      x.peerPushPaymentIncoming,
      x.withdrawalGroups,
      x.reserves,
      x.exchanges,
      x.exchangeDetails,
    ])
    .runReadWrite(async (tx) => {
      const peerInc = await tx.peerPushPaymentIncoming.get(
        peerPushPaymentIncomingId,
      );
      if (!peerInc) {
        return undefined;
      }
      let withdrawalTransition: TransitionInfo | undefined;
      const oldTxState = computePeerPushCreditTransactionState(peerInc);
      switch (peerInc.status) {
        case PeerPushPaymentIncomingStatus.PendingMerge:
        case PeerPushPaymentIncomingStatus.PendingMergeKycRequired: {
          peerInc.status = PeerPushPaymentIncomingStatus.PendingWithdrawing;
          const wgRes = await internalPerformCreateWithdrawalGroup(
            ws,
            tx,
            withdrawalGroupPrep,
          );
          peerInc.withdrawalGroupId = wgRes.withdrawalGroup.withdrawalGroupId;
          break;
        }
      }
      await tx.peerPushPaymentIncoming.put(peerInc);
      const newTxState = computePeerPushCreditTransactionState(peerInc);
      return {
        peerPushCreditTransition: { oldTxState, newTxState },
        withdrawalTransition,
      };
    });
  notifyTransition(
    ws,
    withdrawalGroupPrep.transactionId,
    txRes?.withdrawalTransition,
  );
  notifyTransition(ws, transactionId, txRes?.peerPushCreditTransition);
  return TaskRunResult.finished();
}
async function handlePendingWithdrawing(
  ws: InternalWalletState,
  peerInc: PeerPushPaymentIncomingRecord,
): Promise {
  if (!peerInc.withdrawalGroupId) {
    throw Error("invalid db state (withdrawing, but no withdrawal group ID");
  }
  const transactionId = constructTransactionIdentifier({
    tag: TransactionType.PeerPushCredit,
    peerPushPaymentIncomingId: peerInc.peerPushPaymentIncomingId,
  });
  const wgId = peerInc.withdrawalGroupId;
  let finished: boolean = false;
  const transitionInfo = await ws.db
    .mktx((x) => [x.peerPushPaymentIncoming, x.withdrawalGroups])
    .runReadWrite(async (tx) => {
      const ppi = await tx.peerPushPaymentIncoming.get(
        peerInc.peerPushPaymentIncomingId,
      );
      if (!ppi) {
        finished = true;
        return;
      }
      if (ppi.status !== PeerPushPaymentIncomingStatus.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.Finished:
          finished = true;
          ppi.status = PeerPushPaymentIncomingStatus.Done;
          break;
        // FIXME: Also handle other final states!
      }
      await tx.peerPushPaymentIncoming.put(ppi);
      const newTxState = computePeerPushCreditTransactionState(ppi);
      return {
        oldTxState,
        newTxState,
      };
    });
  notifyTransition(ws, transactionId, transitionInfo);
  if (finished) {
    return TaskRunResult.finished();
  } else {
    // FIXME: Return indicator that we depend on the other operation!
    return TaskRunResult.pending();
  }
}
export async function processPeerPushCredit(
  ws: InternalWalletState,
  peerPushPaymentIncomingId: string,
): Promise {
  let peerInc: PeerPushPaymentIncomingRecord | undefined;
  let contractTerms: PeerContractTerms | undefined;
  await ws.db
    .mktx((x) => [x.contractTerms, x.peerPushPaymentIncoming])
    .runReadWrite(async (tx) => {
      peerInc = await tx.peerPushPaymentIncoming.get(peerPushPaymentIncomingId);
      if (!peerInc) {
        return;
      }
      const ctRec = await tx.contractTerms.get(peerInc.contractTermsHash);
      if (ctRec) {
        contractTerms = ctRec.contractTermsRaw;
      }
      await tx.peerPushPaymentIncoming.put(peerInc);
    });
  checkDbInvariant(!!contractTerms);
  if (!peerInc) {
    throw Error(
      `can't accept unknown incoming p2p push payment (${peerPushPaymentIncomingId})`,
    );
  }
  switch (peerInc.status) {
    case PeerPushPaymentIncomingStatus.PendingMergeKycRequired: {
      if (!peerInc.kycInfo) {
        throw Error("invalid state, kycInfo required");
      }
      return await longpollKycStatus(
        ws,
        peerPushPaymentIncomingId,
        peerInc.exchangeBaseUrl,
        peerInc.kycInfo,
        "individual",
      );
    }
    case PeerPushPaymentIncomingStatus.PendingMerge:
      return handlePendingMerge(ws, peerInc, contractTerms);
    case PeerPushPaymentIncomingStatus.PendingWithdrawing:
      return handlePendingWithdrawing(ws, peerInc);
    default:
      return TaskRunResult.finished();
  }
}
export async function confirmPeerPushCredit(
  ws: InternalWalletState,
  req: ConfirmPeerPushCreditRequest,
): Promise {
  let peerInc: PeerPushPaymentIncomingRecord | undefined;
  let peerPushPaymentIncomingId: string;
  if (req.peerPushPaymentIncomingId) {
    peerPushPaymentIncomingId = req.peerPushPaymentIncomingId;
  } else if (req.transactionId) {
    const parsedTx = parseTransactionIdentifier(req.transactionId);
    if (!parsedTx) {
      throw Error("invalid transaction ID");
    }
    if (parsedTx.tag !== TransactionType.PeerPushCredit) {
      throw Error("invalid transaction ID type");
    }
    peerPushPaymentIncomingId = parsedTx.peerPushPaymentIncomingId;
  } else {
    throw Error(
      "no transaction ID (or deprecated peerPushPaymentIncomingId) provided",
    );
  }
  await ws.db
    .mktx((x) => [x.contractTerms, x.peerPushPaymentIncoming])
    .runReadWrite(async (tx) => {
      peerInc = await tx.peerPushPaymentIncoming.get(peerPushPaymentIncomingId);
      if (!peerInc) {
        return;
      }
      if (peerInc.status === PeerPushPaymentIncomingStatus.DialogProposed) {
        peerInc.status = PeerPushPaymentIncomingStatus.PendingMerge;
      }
      await tx.peerPushPaymentIncoming.put(peerInc);
    });
  if (!peerInc) {
    throw Error(
      `can't accept unknown incoming p2p push payment (${req.peerPushPaymentIncomingId})`,
    );
  }
  ws.workAvailable.trigger();
  const transactionId = constructTransactionIdentifier({
    tag: TransactionType.PeerPushCredit,
    peerPushPaymentIncomingId,
  });
  return {
    transactionId,
  };
}
export async function suspendPeerPushCreditTransaction(
  ws: InternalWalletState,
  peerPushPaymentIncomingId: string,
) {
  const taskId = constructTaskIdentifier({
    tag: PendingTaskType.PeerPushCredit,
    peerPushPaymentIncomingId,
  });
  const transactionId = constructTransactionIdentifier({
    tag: TransactionType.PeerPushCredit,
    peerPushPaymentIncomingId,
  });
  stopLongpolling(ws, taskId);
  const transitionInfo = await ws.db
    .mktx((x) => [x.peerPushPaymentIncoming])
    .runReadWrite(async (tx) => {
      const pushCreditRec = await tx.peerPushPaymentIncoming.get(
        peerPushPaymentIncomingId,
      );
      if (!pushCreditRec) {
        logger.warn(`peer push credit ${peerPushPaymentIncomingId} not found`);
        return;
      }
      let newStatus: PeerPushPaymentIncomingStatus | undefined = undefined;
      switch (pushCreditRec.status) {
        case PeerPushPaymentIncomingStatus.DialogProposed:
        case PeerPushPaymentIncomingStatus.Done:
        case PeerPushPaymentIncomingStatus.SuspendedMerge:
        case PeerPushPaymentIncomingStatus.SuspendedMergeKycRequired:
        case PeerPushPaymentIncomingStatus.SuspendedWithdrawing:
          break;
        case PeerPushPaymentIncomingStatus.PendingMergeKycRequired:
          newStatus = PeerPushPaymentIncomingStatus.SuspendedMergeKycRequired;
          break;
        case PeerPushPaymentIncomingStatus.PendingMerge:
          newStatus = PeerPushPaymentIncomingStatus.SuspendedMerge;
          break;
        case PeerPushPaymentIncomingStatus.PendingWithdrawing:
          // FIXME: Suspend internal withdrawal transaction!
          newStatus = PeerPushPaymentIncomingStatus.SuspendedWithdrawing;
          break;
        case PeerPushPaymentIncomingStatus.Aborted:
          break;
        case PeerPushPaymentIncomingStatus.Failed:
          break;
        default:
          assertUnreachable(pushCreditRec.status);
      }
      if (newStatus != null) {
        const oldTxState = computePeerPushCreditTransactionState(pushCreditRec);
        pushCreditRec.status = newStatus;
        const newTxState = computePeerPushCreditTransactionState(pushCreditRec);
        await tx.peerPushPaymentIncoming.put(pushCreditRec);
        return {
          oldTxState,
          newTxState,
        };
      }
      return undefined;
    });
  notifyTransition(ws, transactionId, transitionInfo);
}
export async function abortPeerPushCreditTransaction(
  ws: InternalWalletState,
  peerPushPaymentIncomingId: string,
) {
  const taskId = constructTaskIdentifier({
    tag: PendingTaskType.PeerPushCredit,
    peerPushPaymentIncomingId,
  });
  const transactionId = constructTransactionIdentifier({
    tag: TransactionType.PeerPushCredit,
    peerPushPaymentIncomingId,
  });
  stopLongpolling(ws, taskId);
  const transitionInfo = await ws.db
    .mktx((x) => [x.peerPushPaymentIncoming])
    .runReadWrite(async (tx) => {
      const pushCreditRec = await tx.peerPushPaymentIncoming.get(
        peerPushPaymentIncomingId,
      );
      if (!pushCreditRec) {
        logger.warn(`peer push credit ${peerPushPaymentIncomingId} not found`);
        return;
      }
      let newStatus: PeerPushPaymentIncomingStatus | undefined = undefined;
      switch (pushCreditRec.status) {
        case PeerPushPaymentIncomingStatus.DialogProposed:
          newStatus = PeerPushPaymentIncomingStatus.Aborted;
          break;
        case PeerPushPaymentIncomingStatus.Done:
          break;
        case PeerPushPaymentIncomingStatus.SuspendedMerge:
        case PeerPushPaymentIncomingStatus.SuspendedMergeKycRequired:
        case PeerPushPaymentIncomingStatus.SuspendedWithdrawing:
          newStatus = PeerPushPaymentIncomingStatus.Aborted;
          break;
        case PeerPushPaymentIncomingStatus.PendingMergeKycRequired:
          newStatus = PeerPushPaymentIncomingStatus.Aborted;
          break;
        case PeerPushPaymentIncomingStatus.PendingMerge:
          newStatus = PeerPushPaymentIncomingStatus.Aborted;
          break;
        case PeerPushPaymentIncomingStatus.PendingWithdrawing:
          newStatus = PeerPushPaymentIncomingStatus.Aborted;
          break;
        case PeerPushPaymentIncomingStatus.Aborted:
          break;
        case PeerPushPaymentIncomingStatus.Failed:
          break;
        default:
          assertUnreachable(pushCreditRec.status);
      }
      if (newStatus != null) {
        const oldTxState = computePeerPushCreditTransactionState(pushCreditRec);
        pushCreditRec.status = newStatus;
        const newTxState = computePeerPushCreditTransactionState(pushCreditRec);
        await tx.peerPushPaymentIncoming.put(pushCreditRec);
        return {
          oldTxState,
          newTxState,
        };
      }
      return undefined;
    });
  notifyTransition(ws, transactionId, transitionInfo);
}
export async function failPeerPushCreditTransaction(
  ws: InternalWalletState,
  peerPushPaymentIncomingId: string,
) {
  // We don't have any "aborting" states!
  throw Error("can't run cancel-aborting on peer-push-credit transaction");
}
export async function resumePeerPushCreditTransaction(
  ws: InternalWalletState,
  peerPushPaymentIncomingId: string,
) {
  const taskId = constructTaskIdentifier({
    tag: PendingTaskType.PeerPushCredit,
    peerPushPaymentIncomingId,
  });
  const transactionId = constructTransactionIdentifier({
    tag: TransactionType.PeerPushCredit,
    peerPushPaymentIncomingId,
  });
  stopLongpolling(ws, taskId);
  const transitionInfo = await ws.db
    .mktx((x) => [x.peerPushPaymentIncoming])
    .runReadWrite(async (tx) => {
      const pushCreditRec = await tx.peerPushPaymentIncoming.get(
        peerPushPaymentIncomingId,
      );
      if (!pushCreditRec) {
        logger.warn(`peer push credit ${peerPushPaymentIncomingId} not found`);
        return;
      }
      let newStatus: PeerPushPaymentIncomingStatus | undefined = undefined;
      switch (pushCreditRec.status) {
        case PeerPushPaymentIncomingStatus.DialogProposed:
        case PeerPushPaymentIncomingStatus.Done:
        case PeerPushPaymentIncomingStatus.PendingMergeKycRequired:
        case PeerPushPaymentIncomingStatus.PendingMerge:
        case PeerPushPaymentIncomingStatus.PendingWithdrawing:
        case PeerPushPaymentIncomingStatus.SuspendedMerge:
          newStatus = PeerPushPaymentIncomingStatus.PendingMerge;
          break;
        case PeerPushPaymentIncomingStatus.SuspendedMergeKycRequired:
          newStatus = PeerPushPaymentIncomingStatus.PendingMergeKycRequired;
          break;
        case PeerPushPaymentIncomingStatus.SuspendedWithdrawing:
          // FIXME: resume underlying "internal-withdrawal" transaction.
          newStatus = PeerPushPaymentIncomingStatus.PendingWithdrawing;
          break;
        case PeerPushPaymentIncomingStatus.Aborted:
          break;
        case PeerPushPaymentIncomingStatus.Failed:
          break;
        default:
          assertUnreachable(pushCreditRec.status);
      }
      if (newStatus != null) {
        const oldTxState = computePeerPushCreditTransactionState(pushCreditRec);
        pushCreditRec.status = newStatus;
        const newTxState = computePeerPushCreditTransactionState(pushCreditRec);
        await tx.peerPushPaymentIncoming.put(pushCreditRec);
        return {
          oldTxState,
          newTxState,
        };
      }
      return undefined;
    });
  ws.workAvailable.trigger();
  notifyTransition(ws, transactionId, transitionInfo);
}
export function computePeerPushCreditTransactionState(
  pushCreditRecord: PeerPushPaymentIncomingRecord,
): TransactionState {
  switch (pushCreditRecord.status) {
    case PeerPushPaymentIncomingStatus.DialogProposed:
      return {
        major: TransactionMajorState.Dialog,
        minor: TransactionMinorState.Proposed,
      };
    case PeerPushPaymentIncomingStatus.PendingMerge:
      return {
        major: TransactionMajorState.Pending,
        minor: TransactionMinorState.Merge,
      };
    case PeerPushPaymentIncomingStatus.Done:
      return {
        major: TransactionMajorState.Done,
      };
    case PeerPushPaymentIncomingStatus.PendingMergeKycRequired:
      return {
        major: TransactionMajorState.Pending,
        minor: TransactionMinorState.KycRequired,
      };
    case PeerPushPaymentIncomingStatus.PendingWithdrawing:
      return {
        major: TransactionMajorState.Pending,
        minor: TransactionMinorState.Withdraw,
      };
    case PeerPushPaymentIncomingStatus.SuspendedMerge:
      return {
        major: TransactionMajorState.Suspended,
        minor: TransactionMinorState.Merge,
      };
    case PeerPushPaymentIncomingStatus.SuspendedMergeKycRequired:
      return {
        major: TransactionMajorState.Suspended,
        minor: TransactionMinorState.MergeKycRequired,
      };
    case PeerPushPaymentIncomingStatus.SuspendedWithdrawing:
      return {
        major: TransactionMajorState.Suspended,
        minor: TransactionMinorState.Withdraw,
      };
    case PeerPushPaymentIncomingStatus.Aborted:
      return {
        major: TransactionMajorState.Aborted,
      };
    case PeerPushPaymentIncomingStatus.Failed:
      return {
        major: TransactionMajorState.Failed,
      };
    default:
      assertUnreachable(pushCreditRecord.status);
  }
}
export function computePeerPushCreditTransactionActions(
  pushCreditRecord: PeerPushPaymentIncomingRecord,
): TransactionAction[] {
  switch (pushCreditRecord.status) {
    case PeerPushPaymentIncomingStatus.DialogProposed:
      return [TransactionAction.Delete];
    case PeerPushPaymentIncomingStatus.PendingMerge:
      return [TransactionAction.Abort, TransactionAction.Suspend];
    case PeerPushPaymentIncomingStatus.Done:
      return [TransactionAction.Delete];
    case PeerPushPaymentIncomingStatus.PendingMergeKycRequired:
      return [TransactionAction.Abort, TransactionAction.Suspend];
    case PeerPushPaymentIncomingStatus.PendingWithdrawing:
      return [TransactionAction.Suspend, TransactionAction.Fail];
    case PeerPushPaymentIncomingStatus.SuspendedMerge:
      return [TransactionAction.Resume, TransactionAction.Abort];
    case PeerPushPaymentIncomingStatus.SuspendedMergeKycRequired:
      return [TransactionAction.Resume, TransactionAction.Abort];
    case PeerPushPaymentIncomingStatus.SuspendedWithdrawing:
      return [TransactionAction.Resume, TransactionAction.Fail];
    case PeerPushPaymentIncomingStatus.Aborted:
      return [TransactionAction.Delete];
    case PeerPushPaymentIncomingStatus.Failed:
      return [TransactionAction.Delete];
    default:
      assertUnreachable(pushCreditRecord.status);
  }
}