/*
 This file is part of GNU Taler
 (C) 2022 GNUnet e.V.
 (C) 2025 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 {
  AmountJson,
  Amounts,
  ExchangePurseStatus,
  SelectedProspectiveCoin,
  TalerProtocolTimestamp,
  checkDbInvariant,
} from "@gnu-taler/taler-util";
import { SpendCoinDetails } from "./crypto/cryptoImplementation.js";
import {
  DbPeerPushPaymentCoinSelection,
  ReserveRecord,
  WalletIndexedDbTransaction,
} from "./db.js";
import { getTotalRefreshCost } from "./refresh.js";
import { WalletExecutionContext, getDenomInfo } from "./wallet.js";
import { updateWithdrawalDenomsForExchange } from "./withdraw.js";

/**
 * Get information about the coin selected for signatures.
 */
export async function queryCoinInfosForSelection(
  wex: WalletExecutionContext,
  csel: DbPeerPushPaymentCoinSelection,
): Promise<SpendCoinDetails[]> {
  let infos: SpendCoinDetails[] = [];
  await wex.runLegacyWalletDbTx(async (tx) => {
    for (let i = 0; i < csel.coinPubs.length; i++) {
      const coin = await tx.coins.get(csel.coinPubs[i]);
      if (!coin) {
        throw Error("coin not found anymore");
      }
      const denom = await getDenomInfo(
        wex,
        tx,
        coin.exchangeBaseUrl,
        coin.denomPubHash,
      );
      if (!denom) {
        throw Error("denom for coin not found anymore");
      }
      infos.push({
        coinPriv: coin.coinPriv,
        coinPub: coin.coinPub,
        denomPubHash: coin.denomPubHash,
        denomSig: coin.denomSig,
        ageCommitmentProof: coin.ageCommitmentProof,
        contribution: csel.contributions[i],
        feeDeposit: denom.feeDeposit,
      });
    }
  });
  return infos;
}

export async function getTotalPeerPaymentCostInTx(
  wex: WalletExecutionContext,
  tx: WalletIndexedDbTransaction,
  pcs: SelectedProspectiveCoin[],
): Promise<AmountJson> {
  const costs: AmountJson[] = [];
  for (let i = 0; i < pcs.length; i++) {
    const denomInfo = await getDenomInfo(
      wex,
      tx,
      pcs[i].exchangeBaseUrl,
      pcs[i].denomPubHash,
    );
    if (!denomInfo) {
      throw Error(
        "can't calculate payment cost, denomination for coin not found",
      );
    }
    const amountLeft = Amounts.sub(denomInfo.value, pcs[i].contribution).amount;
    const refreshCost = await getTotalRefreshCost(
      wex,
      tx,
      denomInfo,
      amountLeft,
    );
    costs.push(Amounts.parseOrThrow(pcs[i].contribution));
    costs.push(refreshCost);
  }
  const zero = Amounts.zeroOfAmount(pcs[0].contribution);
  return Amounts.sum([zero, ...costs]).amount;
}

export async function getTotalPeerPaymentCost(
  wex: WalletExecutionContext,
  pcs: SelectedProspectiveCoin[],
): Promise<AmountJson> {
  // We first need to make sure we have already validated
  // potential target denomination for the refresh.
  // This must happen *before* we start the transaction.
  if (pcs.length > 0) {
    // P2P payments currently support only one exchange.
    const exchangeBaseUrl = pcs[0].exchangeBaseUrl;
    await updateWithdrawalDenomsForExchange(wex, exchangeBaseUrl);
  }
  return await wex.runLegacyWalletDbTx(async (tx) => {
    return await getTotalPeerPaymentCostInTx(wex, tx, pcs);
  });
}

export async function getMergeReserveInfo(
  wex: WalletExecutionContext,
  req: {
    exchangeBaseUrl: string;
  },
): Promise<ReserveRecord> {
  // We have to eagerly create the key pair outside of the transaction,
  // due to the async crypto API.
  const newReservePair = await wex.cryptoApi.createEddsaKeypair({});

  const mergeReserveRecord: ReserveRecord = await wex.runLegacyWalletDbTx(
    async (tx) => {
      const ex = await tx.exchanges.get(req.exchangeBaseUrl);
      checkDbInvariant(!!ex, `no exchange record for ${req.exchangeBaseUrl}`);
      if (ex.currentMergeReserveRowId != null) {
        const reserve = await tx.reserves.get(ex.currentMergeReserveRowId);
        checkDbInvariant(
          !!reserve,
          `reserver ${ex.currentMergeReserveRowId} missing in db`,
        );
        return reserve;
      }
      const reserve: ReserveRecord = {
        reservePriv: newReservePair.priv,
        reservePub: newReservePair.pub,
      };
      const insertResp = await tx.reserves.put(reserve);
      checkDbInvariant(
        typeof insertResp.key === "number",
        `reserve key is not a number`,
      );
      reserve.rowId = insertResp.key;
      ex.currentMergeReserveRowId = reserve.rowId;
      await tx.exchanges.put(ex);
      return reserve;
    },
  );

  return mergeReserveRecord;
}

/** Check if a purse is merged */
export function isPurseMerged(purse: ExchangePurseStatus): boolean {
  const mergeTimestamp = purse.merge_timestamp;
  return (
    mergeTimestamp != null && !TalerProtocolTimestamp.isNever(mergeTimestamp)
  );
}

/** Check if a purse is deposited */
export function isPurseDeposited(purse: ExchangePurseStatus): boolean {
  const depositTimestamp = purse.deposit_timestamp;
  return (
    depositTimestamp != null &&
    !TalerProtocolTimestamp.isNever(depositTimestamp)
  );
}
