/*
 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 {
  AbsoluteTime,
  Amounts,
  CancellationToken,
  CheckPeerPullCreditRequest,
  CheckPeerPullCreditResponse,
  ContractTermsUtil,
  ExchangeReservePurseRequest,
  HttpStatusCode,
  InitiatePeerPullCreditRequest,
  InitiatePeerPullCreditResponse,
  Logger,
  NotificationType,
  PeerContractTerms,
  TalerErrorCode,
  TalerPreciseTimestamp,
  TalerProtocolTimestamp,
  TalerUriAction,
  TransactionAction,
  TransactionMajorState,
  TransactionMinorState,
  TransactionState,
  TransactionType,
  WalletAccountMergeFlags,
  WalletKycUuid,
  codecForAny,
  codecForWalletKycUuid,
  encodeCrock,
  getRandomBytes,
  j2s,
  makeErrorDetail,
  stringifyTalerUri,
  talerPaytoFromExchangeReserve,
} from "@gnu-taler/taler-util";
import {
  readSuccessResponseJsonOrErrorCode,
  readSuccessResponseJsonOrThrow,
  throwUnexpectedRequestError,
} from "@gnu-taler/taler-util/http";
import {
  KycPendingInfo,
  KycUserType,
  PeerPullCreditRecord,
  PeerPullPaymentCreditStatus,
  WithdrawalGroupStatus,
  WithdrawalRecordType,
  updateExchangeFromUrl,
} from "../index.js";
import { InternalWalletState } from "../internal-wallet-state.js";
import { PendingTaskType } from "../pending-types.js";
import { assertUnreachable } from "../util/assertUnreachable.js";
import { checkDbInvariant } from "../util/invariants.js";
import {
  LongpollResult,
  TaskRunResult,
  TaskRunResultType,
  constructTaskIdentifier,
  runLongpollAsync,
} from "./common.js";
import {
  codecForExchangePurseStatus,
  getMergeReserveInfo,
} from "./pay-peer-common.js";
import {
  constructTransactionIdentifier,
  notifyTransition,
  stopLongpolling,
} from "./transactions.js";
import {
  getExchangeWithdrawalInfo,
  internalCreateWithdrawalGroup,
} from "./withdraw.js";
const logger = new Logger("pay-peer-pull-credit.ts");
async function queryPurseForPeerPullCredit(
  ws: InternalWalletState,
  pullIni: PeerPullCreditRecord,
  cancellationToken: CancellationToken,
): Promise {
  const purseDepositUrl = new URL(
    `purses/${pullIni.pursePub}/deposit`,
    pullIni.exchangeBaseUrl,
  );
  purseDepositUrl.searchParams.set("timeout_ms", "30000");
  logger.info(`querying purse status via ${purseDepositUrl.href}`);
  const resp = await ws.http.fetch(purseDepositUrl.href, {
    timeout: { d_ms: 60000 },
    cancellationToken,
  });
  logger.info(`purse status code: HTTP ${resp.status}`);
  const result = await readSuccessResponseJsonOrErrorCode(
    resp,
    codecForExchangePurseStatus(),
  );
  if (result.isError) {
    logger.info(`got purse status error, EC=${result.talerErrorResponse.code}`);
    if (resp.status === 404) {
      return { ready: false };
    } else {
      throwUnexpectedRequestError(resp, result.talerErrorResponse);
    }
  }
  logger.trace(`purse status: ${j2s(result.response)}`);
  const depositTimestamp = result.response.deposit_timestamp;
  if (!depositTimestamp || TalerProtocolTimestamp.isNever(depositTimestamp)) {
    logger.info("purse not ready yet (no deposit)");
    return { ready: false };
  }
  const reserve = await ws.db
    .mktx((x) => [x.reserves])
    .runReadOnly(async (tx) => {
      return await tx.reserves.get(pullIni.mergeReserveRowId);
    });
  if (!reserve) {
    throw Error("reserve for peer pull credit not found in wallet DB");
  }
  await internalCreateWithdrawalGroup(ws, {
    amount: Amounts.parseOrThrow(pullIni.amount),
    wgInfo: {
      withdrawalType: WithdrawalRecordType.PeerPullCredit,
      contractPriv: pullIni.contractPriv,
    },
    forcedWithdrawalGroupId: pullIni.withdrawalGroupId,
    exchangeBaseUrl: pullIni.exchangeBaseUrl,
    reserveStatus: WithdrawalGroupStatus.PendingQueryingStatus,
    reserveKeyPair: {
      priv: reserve.reservePriv,
      pub: reserve.reservePub,
    },
  });
  const transactionId = constructTransactionIdentifier({
    tag: TransactionType.PeerPullCredit,
    pursePub: pullIni.pursePub,
  });
  const transitionInfo = await ws.db
    .mktx((x) => [x.peerPullCredit])
    .runReadWrite(async (tx) => {
      const finPi = await tx.peerPullCredit.get(pullIni.pursePub);
      if (!finPi) {
        logger.warn("peerPullCredit not found anymore");
        return;
      }
      const oldTxState = computePeerPullCreditTransactionState(finPi);
      if (finPi.status === PeerPullPaymentCreditStatus.PendingReady) {
        finPi.status = PeerPullPaymentCreditStatus.PendingWithdrawing;
      }
      await tx.peerPullCredit.put(finPi);
      const newTxState = computePeerPullCreditTransactionState(finPi);
      return { oldTxState, newTxState };
    });
  notifyTransition(ws, transactionId, transitionInfo);
  return {
    ready: true,
  };
}
async function longpollKycStatus(
  ws: InternalWalletState,
  pursePub: string,
  exchangeUrl: string,
  kycInfo: KycPendingInfo,
  userType: KycUserType,
): Promise {
  const transactionId = constructTransactionIdentifier({
    tag: TransactionType.PeerPullCredit,
    pursePub,
  });
  const retryTag = constructTaskIdentifier({
    tag: PendingTaskType.PeerPullCredit,
    pursePub,
  });
  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.peerPullCredit])
        .runReadWrite(async (tx) => {
          const peerIni = await tx.peerPullCredit.get(pursePub);
          if (!peerIni) {
            return;
          }
          if (
            peerIni.status !==
            PeerPullPaymentCreditStatus.PendingMergeKycRequired
          ) {
            return;
          }
          const oldTxState = computePeerPullCreditTransactionState(peerIni);
          peerIni.status = PeerPullPaymentCreditStatus.PendingCreatePurse;
          const newTxState = computePeerPullCreditTransactionState(peerIni);
          await tx.peerPullCredit.put(peerIni);
          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 processPeerPullCreditAbortingDeletePurse(
  ws: InternalWalletState,
  peerPullIni: PeerPullCreditRecord,
): Promise {
  const { pursePub, pursePriv } = peerPullIni;
  const transactionId = constructTransactionIdentifier({
    tag: TransactionType.PeerPushDebit,
    pursePub,
  });
  const sigResp = await ws.cryptoApi.signDeletePurse({
    pursePriv,
  });
  const purseUrl = new URL(`purses/${pursePub}`, peerPullIni.exchangeBaseUrl);
  const resp = await ws.http.fetch(purseUrl.href, {
    method: "DELETE",
    headers: {
      "taler-purse-signature": sigResp.sig,
    },
  });
  logger.info(`deleted purse with response status ${resp.status}`);
  const transitionInfo = await ws.db
    .mktx((x) => [
      x.peerPullCredit,
      x.refreshGroups,
      x.denominations,
      x.coinAvailability,
      x.coins,
    ])
    .runReadWrite(async (tx) => {
      const ppiRec = await tx.peerPullCredit.get(pursePub);
      if (!ppiRec) {
        return undefined;
      }
      if (ppiRec.status !== PeerPullPaymentCreditStatus.AbortingDeletePurse) {
        return undefined;
      }
      const oldTxState = computePeerPullCreditTransactionState(ppiRec);
      ppiRec.status = PeerPullPaymentCreditStatus.Aborted;
      const newTxState = computePeerPullCreditTransactionState(ppiRec);
      return {
        oldTxState,
        newTxState,
      };
    });
  notifyTransition(ws, transactionId, transitionInfo);
  return TaskRunResult.pending();
}
async function handlePeerPullCreditWithdrawing(
  ws: InternalWalletState,
  pullIni: PeerPullCreditRecord,
): Promise {
  if (!pullIni.withdrawalGroupId) {
    throw Error("invalid db state (withdrawing, but no withdrawal group ID");
  }
  const transactionId = constructTransactionIdentifier({
    tag: TransactionType.PeerPullCredit,
    pursePub: pullIni.pursePub,
  });
  const wgId = pullIni.withdrawalGroupId;
  let finished: boolean = false;
  const transitionInfo = await ws.db
    .mktx((x) => [x.peerPullCredit, x.withdrawalGroups])
    .runReadWrite(async (tx) => {
      const ppi = await tx.peerPullCredit.get(pullIni.pursePub);
      if (!ppi) {
        finished = true;
        return;
      }
      if (ppi.status !== PeerPullPaymentCreditStatus.PendingWithdrawing) {
        finished = true;
        return;
      }
      const oldTxState = computePeerPullCreditTransactionState(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 = PeerPullPaymentCreditStatus.Done;
          break;
        // FIXME: Also handle other final states!
      }
      await tx.peerPullCredit.put(ppi);
      const newTxState = computePeerPullCreditTransactionState(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();
  }
}
async function handlePeerPullCreditCreatePurse(
  ws: InternalWalletState,
  pullIni: PeerPullCreditRecord,
): Promise {
  const purseFee = Amounts.stringify(Amounts.zeroOfAmount(pullIni.amount));
  const pursePub = pullIni.pursePub;
  const mergeReserve = await ws.db
    .mktx((x) => [x.reserves])
    .runReadOnly(async (tx) => {
      return tx.reserves.get(pullIni.mergeReserveRowId);
    });
  if (!mergeReserve) {
    throw Error("merge reserve for peer pull payment not found in database");
  }
  const contractTermsRecord = await ws.db
    .mktx((x) => [x.contractTerms])
    .runReadOnly(async (tx) => {
      return tx.contractTerms.get(pullIni.contractTermsHash);
    });
  if (!contractTermsRecord) {
    throw Error("contract terms for peer pull payment not found in database");
  }
  const contractTerms: PeerContractTerms = contractTermsRecord.contractTermsRaw;
  const reservePayto = talerPaytoFromExchangeReserve(
    pullIni.exchangeBaseUrl,
    mergeReserve.reservePub,
  );
  const econtractResp = await ws.cryptoApi.encryptContractForDeposit({
    contractPriv: pullIni.contractPriv,
    contractPub: pullIni.contractPub,
    contractTerms: contractTermsRecord,
    pursePriv: pullIni.pursePriv,
    pursePub: pullIni.pursePub,
    nonce: pullIni.contractEncNonce,
  });
  const purseExpiration = contractTerms.purse_expiration;
  const sigRes = await ws.cryptoApi.signReservePurseCreate({
    contractTermsHash: pullIni.contractTermsHash,
    flags: WalletAccountMergeFlags.CreateWithPurseFee,
    mergePriv: pullIni.mergePriv,
    mergeTimestamp: TalerPreciseTimestamp.round(pullIni.mergeTimestamp),
    purseAmount: pullIni.amount,
    purseExpiration: purseExpiration,
    purseFee: purseFee,
    pursePriv: pullIni.pursePriv,
    pursePub: pullIni.pursePub,
    reservePayto,
    reservePriv: mergeReserve.reservePriv,
  });
  const reservePurseReqBody: ExchangeReservePurseRequest = {
    merge_sig: sigRes.mergeSig,
    merge_timestamp: TalerPreciseTimestamp.round(pullIni.mergeTimestamp),
    h_contract_terms: pullIni.contractTermsHash,
    merge_pub: pullIni.mergePub,
    min_age: 0,
    purse_expiration: purseExpiration,
    purse_fee: purseFee,
    purse_pub: pullIni.pursePub,
    purse_sig: sigRes.purseSig,
    purse_value: pullIni.amount,
    reserve_sig: sigRes.accountSig,
    econtract: econtractResp.econtract,
  };
  logger.info(`reserve purse request: ${j2s(reservePurseReqBody)}`);
  const reservePurseMergeUrl = new URL(
    `reserves/${mergeReserve.reservePub}/purse`,
    pullIni.exchangeBaseUrl,
  );
  const httpResp = await ws.http.fetch(reservePurseMergeUrl.href, {
    method: "POST",
    body: reservePurseReqBody,
  });
  if (httpResp.status === HttpStatusCode.UnavailableForLegalReasons) {
    const respJson = await httpResp.json();
    const kycPending = codecForWalletKycUuid().decode(respJson);
    logger.info(`kyc uuid response: ${j2s(kycPending)}`);
    return processPeerPullCreditKycRequired(ws, pullIni, kycPending);
  }
  const resp = await readSuccessResponseJsonOrThrow(httpResp, codecForAny());
  logger.info(`reserve merge response: ${j2s(resp)}`);
  const transactionId = constructTransactionIdentifier({
    tag: TransactionType.PeerPullCredit,
    pursePub: pullIni.pursePub,
  });
  const transitionInfo = await ws.db
    .mktx((x) => [x.peerPullCredit])
    .runReadWrite(async (tx) => {
      const pi2 = await tx.peerPullCredit.get(pursePub);
      if (!pi2) {
        return;
      }
      const oldTxState = computePeerPullCreditTransactionState(pi2);
      pi2.status = PeerPullPaymentCreditStatus.PendingReady;
      await tx.peerPullCredit.put(pi2);
      const newTxState = computePeerPullCreditTransactionState(pi2);
      return { oldTxState, newTxState };
    });
  notifyTransition(ws, transactionId, transitionInfo);
  return TaskRunResult.finished();
}
export async function processPeerPullCredit(
  ws: InternalWalletState,
  pursePub: string,
): Promise {
  const pullIni = await ws.db
    .mktx((x) => [x.peerPullCredit])
    .runReadOnly(async (tx) => {
      return tx.peerPullCredit.get(pursePub);
    });
  if (!pullIni) {
    throw Error("peer pull payment initiation not found in database");
  }
  const retryTag = constructTaskIdentifier({
    tag: PendingTaskType.PeerPullCredit,
    pursePub,
  });
  // We're already running!
  if (ws.activeLongpoll[retryTag]) {
    logger.info("peer-pull-credit already in long-polling, returning!");
    return {
      type: TaskRunResultType.Longpoll,
    };
  }
  logger.trace(`processing ${retryTag}, status=${pullIni.status}`);
  switch (pullIni.status) {
    case PeerPullPaymentCreditStatus.Done: {
      return TaskRunResult.finished();
    }
    case PeerPullPaymentCreditStatus.PendingReady:
      runLongpollAsync(ws, retryTag, async (cancellationToken) =>
        queryPurseForPeerPullCredit(ws, pullIni, cancellationToken),
      );
      logger.trace(
        "returning early from processPeerPullCredit for long-polling in background",
      );
      return {
        type: TaskRunResultType.Longpoll,
      };
    case PeerPullPaymentCreditStatus.PendingMergeKycRequired: {
      if (!pullIni.kycInfo) {
        throw Error("invalid state, kycInfo required");
      }
      return await longpollKycStatus(
        ws,
        pursePub,
        pullIni.exchangeBaseUrl,
        pullIni.kycInfo,
        "individual",
      );
    }
    case PeerPullPaymentCreditStatus.PendingCreatePurse:
      return handlePeerPullCreditCreatePurse(ws, pullIni);
    case PeerPullPaymentCreditStatus.AbortingDeletePurse:
      return await processPeerPullCreditAbortingDeletePurse(ws, pullIni);
    case PeerPullPaymentCreditStatus.PendingWithdrawing:
      return handlePeerPullCreditWithdrawing(ws, pullIni);
    case PeerPullPaymentCreditStatus.Aborted:
    case PeerPullPaymentCreditStatus.Failed:
    case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse:
    case PeerPullPaymentCreditStatus.SuspendedCreatePurse:
    case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired:
    case PeerPullPaymentCreditStatus.SuspendedReady:
    case PeerPullPaymentCreditStatus.SuspendedWithdrawing:
      break;
    default:
      assertUnreachable(pullIni.status);
  }
  return TaskRunResult.finished();
}
async function processPeerPullCreditKycRequired(
  ws: InternalWalletState,
  peerIni: PeerPullCreditRecord,
  kycPending: WalletKycUuid,
): Promise {
  const transactionId = constructTransactionIdentifier({
    tag: TransactionType.PeerPullCredit,
    pursePub: peerIni.pursePub,
  });
  const { pursePub } = peerIni;
  const userType = "individual";
  const url = new URL(
    `kyc-check/${kycPending.requirement_row}/${kycPending.h_payto}/${userType}`,
    peerIni.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.peerPullCredit])
      .runReadWrite(async (tx) => {
        const peerInc = await tx.peerPullCredit.get(pursePub);
        if (!peerInc) {
          return {
            transitionInfo: undefined,
            result: TaskRunResult.finished(),
          };
        }
        const oldTxState = computePeerPullCreditTransactionState(peerInc);
        peerInc.kycInfo = {
          paytoHash: kycPending.h_payto,
          requirementRow: kycPending.requirement_row,
        };
        peerInc.kycUrl = kycStatus.kyc_url;
        peerInc.status = PeerPullPaymentCreditStatus.PendingMergeKycRequired;
        const newTxState = computePeerPullCreditTransactionState(peerInc);
        await tx.peerPullCredit.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 TaskRunResult.pending();
  } else {
    throw Error(`unexpected response from kyc-check (${kycStatusRes.status})`);
  }
}
/**
 * Check fees and available exchanges for a peer push payment initiation.
 */
export async function checkPeerPullPaymentInitiation(
  ws: InternalWalletState,
  req: CheckPeerPullCreditRequest,
): Promise {
  // FIXME: We don't support exchanges with purse fees yet.
  // Select an exchange where we have money in the specified currency
  // FIXME: How do we handle regional currency scopes here? Is it an additional input?
  logger.trace("checking peer-pull-credit fees");
  const currency = Amounts.currencyOf(req.amount);
  let exchangeUrl;
  if (req.exchangeBaseUrl) {
    exchangeUrl = req.exchangeBaseUrl;
  } else {
    exchangeUrl = await getPreferredExchangeForCurrency(ws, currency);
  }
  if (!exchangeUrl) {
    throw Error("no exchange found for initiating a peer pull payment");
  }
  logger.trace(`found ${exchangeUrl} as preferred exchange`);
  const wi = await getExchangeWithdrawalInfo(
    ws,
    exchangeUrl,
    Amounts.parseOrThrow(req.amount),
    undefined,
  );
  logger.trace(`got withdrawal info`);
  let numCoins = 0;
  for (let i = 0; i < wi.selectedDenoms.selectedDenoms.length; i++) {
    numCoins += wi.selectedDenoms.selectedDenoms[i].count;
  }
  return {
    exchangeBaseUrl: exchangeUrl,
    amountEffective: wi.withdrawalAmountEffective,
    amountRaw: req.amount,
    numCoins,
  };
}
/**
 * Find a preferred exchange based on when we withdrew last from this exchange.
 */
async function getPreferredExchangeForCurrency(
  ws: InternalWalletState,
  currency: string,
): Promise {
  // Find an exchange with the matching currency.
  // Prefer exchanges with the most recent withdrawal.
  const url = await ws.db
    .mktx((x) => [x.exchanges])
    .runReadOnly(async (tx) => {
      const exchanges = await tx.exchanges.iter().toArray();
      let candidate = undefined;
      for (const e of exchanges) {
        if (e.detailsPointer?.currency !== currency) {
          continue;
        }
        if (!candidate) {
          candidate = e;
          continue;
        }
        if (candidate.lastWithdrawal && !e.lastWithdrawal) {
          continue;
        }
        if (candidate.lastWithdrawal && e.lastWithdrawal) {
          if (
            AbsoluteTime.cmp(
              AbsoluteTime.fromPreciseTimestamp(e.lastWithdrawal),
              AbsoluteTime.fromPreciseTimestamp(candidate.lastWithdrawal),
            ) > 0
          ) {
            candidate = e;
          }
        }
      }
      if (candidate) {
        return candidate.baseUrl;
      }
      return undefined;
    });
  return url;
}
/**
 * Initiate a peer pull payment.
 */
export async function initiatePeerPullPayment(
  ws: InternalWalletState,
  req: InitiatePeerPullCreditRequest,
): Promise {
  const currency = Amounts.currencyOf(req.partialContractTerms.amount);
  let maybeExchangeBaseUrl: string | undefined;
  if (req.exchangeBaseUrl) {
    maybeExchangeBaseUrl = req.exchangeBaseUrl;
  } else {
    maybeExchangeBaseUrl = await getPreferredExchangeForCurrency(ws, currency);
  }
  if (!maybeExchangeBaseUrl) {
    throw Error("no exchange found for initiating a peer pull payment");
  }
  const exchangeBaseUrl = maybeExchangeBaseUrl;
  await updateExchangeFromUrl(ws, exchangeBaseUrl);
  const mergeReserveInfo = await getMergeReserveInfo(ws, {
    exchangeBaseUrl: exchangeBaseUrl,
  });
  const mergeTimestamp = TalerPreciseTimestamp.now();
  const pursePair = await ws.cryptoApi.createEddsaKeypair({});
  const mergePair = await ws.cryptoApi.createEddsaKeypair({});
  const contractTerms = req.partialContractTerms;
  const hContractTerms = ContractTermsUtil.hashContractTerms(contractTerms);
  const contractKeyPair = await ws.cryptoApi.createEddsaKeypair({});
  const withdrawalGroupId = encodeCrock(getRandomBytes(32));
  const mergeReserveRowId = mergeReserveInfo.rowId;
  checkDbInvariant(!!mergeReserveRowId);
  const contractEncNonce = encodeCrock(getRandomBytes(24));
  const wi = await getExchangeWithdrawalInfo(
    ws,
    exchangeBaseUrl,
    Amounts.parseOrThrow(req.partialContractTerms.amount),
    undefined,
  );
  const transitionInfo = await ws.db
    .mktx((x) => [x.peerPullCredit, x.contractTerms])
    .runReadWrite(async (tx) => {
      const ppi: PeerPullCreditRecord = {
        amount: req.partialContractTerms.amount,
        contractTermsHash: hContractTerms,
        exchangeBaseUrl: exchangeBaseUrl,
        pursePriv: pursePair.priv,
        pursePub: pursePair.pub,
        mergePriv: mergePair.priv,
        mergePub: mergePair.pub,
        status: PeerPullPaymentCreditStatus.PendingCreatePurse,
        mergeTimestamp,
        contractEncNonce,
        mergeReserveRowId: mergeReserveRowId,
        contractPriv: contractKeyPair.priv,
        contractPub: contractKeyPair.pub,
        withdrawalGroupId,
        estimatedAmountEffective: wi.withdrawalAmountEffective,
      };
      await tx.peerPullCredit.put(ppi);
      const oldTxState: TransactionState = {
        major: TransactionMajorState.None,
      };
      const newTxState = computePeerPullCreditTransactionState(ppi);
      await tx.contractTerms.put({
        contractTermsRaw: contractTerms,
        h: hContractTerms,
      });
      return { oldTxState, newTxState };
    });
  const transactionId = constructTransactionIdentifier({
    tag: TransactionType.PeerPullCredit,
    pursePub: pursePair.pub,
  });
  // The pending-incoming balance has changed.
  ws.notify({ type: NotificationType.BalanceChange });
  notifyTransition(ws, transactionId, transitionInfo);
  ws.workAvailable.trigger();
  return {
    talerUri: stringifyTalerUri({
      type: TalerUriAction.PayPull,
      exchangeBaseUrl: exchangeBaseUrl,
      contractPriv: contractKeyPair.priv,
    }),
    transactionId,
  };
}
export async function suspendPeerPullCreditTransaction(
  ws: InternalWalletState,
  pursePub: string,
) {
  const taskId = constructTaskIdentifier({
    tag: PendingTaskType.PeerPullCredit,
    pursePub,
  });
  const transactionId = constructTransactionIdentifier({
    tag: TransactionType.PeerPullCredit,
    pursePub,
  });
  stopLongpolling(ws, taskId);
  const transitionInfo = await ws.db
    .mktx((x) => [x.peerPullCredit])
    .runReadWrite(async (tx) => {
      const pullCreditRec = await tx.peerPullCredit.get(pursePub);
      if (!pullCreditRec) {
        logger.warn(`peer pull credit ${pursePub} not found`);
        return;
      }
      let newStatus: PeerPullPaymentCreditStatus | undefined = undefined;
      switch (pullCreditRec.status) {
        case PeerPullPaymentCreditStatus.PendingCreatePurse:
          newStatus = PeerPullPaymentCreditStatus.SuspendedCreatePurse;
          break;
        case PeerPullPaymentCreditStatus.PendingMergeKycRequired:
          newStatus = PeerPullPaymentCreditStatus.SuspendedMergeKycRequired;
          break;
        case PeerPullPaymentCreditStatus.PendingWithdrawing:
          newStatus = PeerPullPaymentCreditStatus.SuspendedWithdrawing;
          break;
        case PeerPullPaymentCreditStatus.PendingReady:
          newStatus = PeerPullPaymentCreditStatus.SuspendedReady;
          break;
        case PeerPullPaymentCreditStatus.AbortingDeletePurse:
          newStatus = PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse;
          break;
        case PeerPullPaymentCreditStatus.Done:
        case PeerPullPaymentCreditStatus.SuspendedCreatePurse:
        case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired:
        case PeerPullPaymentCreditStatus.SuspendedReady:
        case PeerPullPaymentCreditStatus.SuspendedWithdrawing:
        case PeerPullPaymentCreditStatus.Aborted:
        case PeerPullPaymentCreditStatus.Failed:
        case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse:
          break;
        default:
          assertUnreachable(pullCreditRec.status);
      }
      if (newStatus != null) {
        const oldTxState = computePeerPullCreditTransactionState(pullCreditRec);
        pullCreditRec.status = newStatus;
        const newTxState = computePeerPullCreditTransactionState(pullCreditRec);
        await tx.peerPullCredit.put(pullCreditRec);
        return {
          oldTxState,
          newTxState,
        };
      }
      return undefined;
    });
  notifyTransition(ws, transactionId, transitionInfo);
}
export async function abortPeerPullCreditTransaction(
  ws: InternalWalletState,
  pursePub: string,
) {
  const taskId = constructTaskIdentifier({
    tag: PendingTaskType.PeerPullCredit,
    pursePub,
  });
  const transactionId = constructTransactionIdentifier({
    tag: TransactionType.PeerPullCredit,
    pursePub,
  });
  stopLongpolling(ws, taskId);
  const transitionInfo = await ws.db
    .mktx((x) => [x.peerPullCredit])
    .runReadWrite(async (tx) => {
      const pullCreditRec = await tx.peerPullCredit.get(pursePub);
      if (!pullCreditRec) {
        logger.warn(`peer pull credit ${pursePub} not found`);
        return;
      }
      let newStatus: PeerPullPaymentCreditStatus | undefined = undefined;
      switch (pullCreditRec.status) {
        case PeerPullPaymentCreditStatus.PendingCreatePurse:
        case PeerPullPaymentCreditStatus.PendingMergeKycRequired:
          newStatus = PeerPullPaymentCreditStatus.AbortingDeletePurse;
          break;
        case PeerPullPaymentCreditStatus.PendingWithdrawing:
          throw Error("can't abort anymore");
        case PeerPullPaymentCreditStatus.PendingReady:
          newStatus = PeerPullPaymentCreditStatus.AbortingDeletePurse;
          break;
        case PeerPullPaymentCreditStatus.Done:
        case PeerPullPaymentCreditStatus.SuspendedCreatePurse:
        case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired:
        case PeerPullPaymentCreditStatus.SuspendedReady:
        case PeerPullPaymentCreditStatus.SuspendedWithdrawing:
        case PeerPullPaymentCreditStatus.Aborted:
        case PeerPullPaymentCreditStatus.AbortingDeletePurse:
        case PeerPullPaymentCreditStatus.Failed:
        case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse:
          break;
        default:
          assertUnreachable(pullCreditRec.status);
      }
      if (newStatus != null) {
        const oldTxState = computePeerPullCreditTransactionState(pullCreditRec);
        pullCreditRec.status = newStatus;
        const newTxState = computePeerPullCreditTransactionState(pullCreditRec);
        await tx.peerPullCredit.put(pullCreditRec);
        return {
          oldTxState,
          newTxState,
        };
      }
      return undefined;
    });
  notifyTransition(ws, transactionId, transitionInfo);
}
export async function failPeerPullCreditTransaction(
  ws: InternalWalletState,
  pursePub: string,
) {
  const taskId = constructTaskIdentifier({
    tag: PendingTaskType.PeerPullCredit,
    pursePub,
  });
  const transactionId = constructTransactionIdentifier({
    tag: TransactionType.PeerPullCredit,
    pursePub,
  });
  stopLongpolling(ws, taskId);
  const transitionInfo = await ws.db
    .mktx((x) => [x.peerPullCredit])
    .runReadWrite(async (tx) => {
      const pullCreditRec = await tx.peerPullCredit.get(pursePub);
      if (!pullCreditRec) {
        logger.warn(`peer pull credit ${pursePub} not found`);
        return;
      }
      let newStatus: PeerPullPaymentCreditStatus | undefined = undefined;
      switch (pullCreditRec.status) {
        case PeerPullPaymentCreditStatus.PendingCreatePurse:
        case PeerPullPaymentCreditStatus.PendingMergeKycRequired:
        case PeerPullPaymentCreditStatus.PendingWithdrawing:
        case PeerPullPaymentCreditStatus.PendingReady:
        case PeerPullPaymentCreditStatus.Done:
        case PeerPullPaymentCreditStatus.SuspendedCreatePurse:
        case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired:
        case PeerPullPaymentCreditStatus.SuspendedReady:
        case PeerPullPaymentCreditStatus.SuspendedWithdrawing:
        case PeerPullPaymentCreditStatus.Aborted:
        case PeerPullPaymentCreditStatus.Failed:
          break;
        case PeerPullPaymentCreditStatus.AbortingDeletePurse:
        case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse:
          newStatus = PeerPullPaymentCreditStatus.Failed;
          break;
        default:
          assertUnreachable(pullCreditRec.status);
      }
      if (newStatus != null) {
        const oldTxState = computePeerPullCreditTransactionState(pullCreditRec);
        pullCreditRec.status = newStatus;
        const newTxState = computePeerPullCreditTransactionState(pullCreditRec);
        await tx.peerPullCredit.put(pullCreditRec);
        return {
          oldTxState,
          newTxState,
        };
      }
      return undefined;
    });
  notifyTransition(ws, transactionId, transitionInfo);
}
export async function resumePeerPullCreditTransaction(
  ws: InternalWalletState,
  pursePub: string,
) {
  const taskId = constructTaskIdentifier({
    tag: PendingTaskType.PeerPullCredit,
    pursePub,
  });
  const transactionId = constructTransactionIdentifier({
    tag: TransactionType.PeerPullCredit,
    pursePub,
  });
  stopLongpolling(ws, taskId);
  const transitionInfo = await ws.db
    .mktx((x) => [x.peerPullCredit])
    .runReadWrite(async (tx) => {
      const pullCreditRec = await tx.peerPullCredit.get(pursePub);
      if (!pullCreditRec) {
        logger.warn(`peer pull credit ${pursePub} not found`);
        return;
      }
      let newStatus: PeerPullPaymentCreditStatus | undefined = undefined;
      switch (pullCreditRec.status) {
        case PeerPullPaymentCreditStatus.PendingCreatePurse:
        case PeerPullPaymentCreditStatus.PendingMergeKycRequired:
        case PeerPullPaymentCreditStatus.PendingWithdrawing:
        case PeerPullPaymentCreditStatus.PendingReady:
        case PeerPullPaymentCreditStatus.AbortingDeletePurse:
        case PeerPullPaymentCreditStatus.Done:
        case PeerPullPaymentCreditStatus.Failed:
        case PeerPullPaymentCreditStatus.Aborted:
          break;
        case PeerPullPaymentCreditStatus.SuspendedCreatePurse:
          newStatus = PeerPullPaymentCreditStatus.PendingCreatePurse;
          break;
        case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired:
          newStatus = PeerPullPaymentCreditStatus.PendingMergeKycRequired;
          break;
        case PeerPullPaymentCreditStatus.SuspendedReady:
          newStatus = PeerPullPaymentCreditStatus.PendingReady;
          break;
        case PeerPullPaymentCreditStatus.SuspendedWithdrawing:
          newStatus = PeerPullPaymentCreditStatus.PendingWithdrawing;
          break;
        case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse:
          newStatus = PeerPullPaymentCreditStatus.AbortingDeletePurse;
          break;
        default:
          assertUnreachable(pullCreditRec.status);
      }
      if (newStatus != null) {
        const oldTxState = computePeerPullCreditTransactionState(pullCreditRec);
        pullCreditRec.status = newStatus;
        const newTxState = computePeerPullCreditTransactionState(pullCreditRec);
        await tx.peerPullCredit.put(pullCreditRec);
        return {
          oldTxState,
          newTxState,
        };
      }
      return undefined;
    });
  ws.workAvailable.trigger();
  notifyTransition(ws, transactionId, transitionInfo);
}
export function computePeerPullCreditTransactionState(
  pullCreditRecord: PeerPullCreditRecord,
): TransactionState {
  switch (pullCreditRecord.status) {
    case PeerPullPaymentCreditStatus.PendingCreatePurse:
      return {
        major: TransactionMajorState.Pending,
        minor: TransactionMinorState.CreatePurse,
      };
    case PeerPullPaymentCreditStatus.PendingMergeKycRequired:
      return {
        major: TransactionMajorState.Pending,
        minor: TransactionMinorState.MergeKycRequired,
      };
    case PeerPullPaymentCreditStatus.PendingReady:
      return {
        major: TransactionMajorState.Pending,
        minor: TransactionMinorState.Ready,
      };
    case PeerPullPaymentCreditStatus.Done:
      return {
        major: TransactionMajorState.Done,
      };
    case PeerPullPaymentCreditStatus.PendingWithdrawing:
      return {
        major: TransactionMajorState.Pending,
        minor: TransactionMinorState.Withdraw,
      };
    case PeerPullPaymentCreditStatus.SuspendedCreatePurse:
      return {
        major: TransactionMajorState.Suspended,
        minor: TransactionMinorState.CreatePurse,
      };
    case PeerPullPaymentCreditStatus.SuspendedReady:
      return {
        major: TransactionMajorState.Suspended,
        minor: TransactionMinorState.Ready,
      };
    case PeerPullPaymentCreditStatus.SuspendedWithdrawing:
      return {
        major: TransactionMajorState.Pending,
        minor: TransactionMinorState.Withdraw,
      };
    case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired:
      return {
        major: TransactionMajorState.Suspended,
        minor: TransactionMinorState.MergeKycRequired,
      };
    case PeerPullPaymentCreditStatus.Aborted:
      return {
        major: TransactionMajorState.Aborted,
      };
    case PeerPullPaymentCreditStatus.AbortingDeletePurse:
      return {
        major: TransactionMajorState.Aborting,
        minor: TransactionMinorState.DeletePurse,
      };
    case PeerPullPaymentCreditStatus.Failed:
      return {
        major: TransactionMajorState.Failed,
      };
    case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse:
      return {
        major: TransactionMajorState.Aborting,
        minor: TransactionMinorState.DeletePurse,
      };
  }
}
export function computePeerPullCreditTransactionActions(
  pullCreditRecord: PeerPullCreditRecord,
): TransactionAction[] {
  switch (pullCreditRecord.status) {
    case PeerPullPaymentCreditStatus.PendingCreatePurse:
      return [TransactionAction.Abort, TransactionAction.Suspend];
    case PeerPullPaymentCreditStatus.PendingMergeKycRequired:
      return [TransactionAction.Abort, TransactionAction.Suspend];
    case PeerPullPaymentCreditStatus.PendingReady:
      return [TransactionAction.Abort, TransactionAction.Suspend];
    case PeerPullPaymentCreditStatus.Done:
      return [TransactionAction.Delete];
    case PeerPullPaymentCreditStatus.PendingWithdrawing:
      return [TransactionAction.Abort, TransactionAction.Suspend];
    case PeerPullPaymentCreditStatus.SuspendedCreatePurse:
      return [TransactionAction.Resume, TransactionAction.Abort];
    case PeerPullPaymentCreditStatus.SuspendedReady:
      return [TransactionAction.Abort, TransactionAction.Resume];
    case PeerPullPaymentCreditStatus.SuspendedWithdrawing:
      return [TransactionAction.Resume, TransactionAction.Fail];
    case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired:
      return [TransactionAction.Resume, TransactionAction.Fail];
    case PeerPullPaymentCreditStatus.Aborted:
      return [TransactionAction.Delete];
    case PeerPullPaymentCreditStatus.AbortingDeletePurse:
      return [TransactionAction.Suspend, TransactionAction.Fail];
    case PeerPullPaymentCreditStatus.Failed:
      return [TransactionAction.Delete];
    case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse:
      return [TransactionAction.Resume, TransactionAction.Fail];
  }
}