/*
 This file is part of GNU Taler
 (C) 2022 GNUnet e.V.

 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/>
 */

/**
 * Imports.
 */
import {
  AmountJson,
  AmountString,
  Amounts,
  Codec,
  SelectedProspectiveCoin,
  TalerProtocolTimestamp,
  buildCodecForObject,
  checkDbInvariant,
  codecForAmountString,
  codecForTimestamp,
  codecOptional,
} from "@gnu-taler/taler-util";
import { SpendCoinDetails } from "./crypto/cryptoImplementation.js";
import {
  DbPeerPushPaymentCoinSelection,
  ReserveRecord,
  WalletDbReadOnlyTransaction,
} from "./db.js";
import { getTotalRefreshCost } from "./refresh.js";
import { WalletExecutionContext, getDenomInfo } from "./wallet.js";

/**
 * Get information about the coin selected for signatures.
 */
export async function queryCoinInfosForSelection(
  wex: WalletExecutionContext,
  csel: DbPeerPushPaymentCoinSelection,
): Promise<SpendCoinDetails[]> {
  let infos: SpendCoinDetails[] = [];
  await wex.db.runReadOnlyTx(
    { storeNames: ["coins", "denominations"] },
    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],
        });
      }
    },
  );
  return infos;
}

export async function getTotalPeerPaymentCostInTx(
  wex: WalletExecutionContext,
  tx: WalletDbReadOnlyTransaction<["coins", "denominations"]>,
  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> {
  return wex.db.runReadOnlyTx(
    { storeNames: ["coins", "denominations"] },
    async (tx) => {
      return getTotalPeerPaymentCostInTx(wex, tx, pcs);
    },
  );
}

interface ExchangePurseStatus {
  balance: AmountString;
  deposit_timestamp?: TalerProtocolTimestamp;
  merge_timestamp?: TalerProtocolTimestamp;
}

export const codecForExchangePurseStatus = (): Codec<ExchangePurseStatus> =>
  buildCodecForObject<ExchangePurseStatus>()
    .property("balance", codecForAmountString())
    .property("deposit_timestamp", codecOptional(codecForTimestamp))
    .property("merge_timestamp", codecOptional(codecForTimestamp))
    .build("ExchangePurseStatus");

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.db.runReadWriteTx(
    { storeNames: ["exchanges", "reserves"] },
    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;
}
