/*
 This file is part of GNU Taler
 (C) 2021-2024 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/>
 */

/**
 * Selection of denominations for withdrawals.
 *
 * @author Florian Dold
 */

/**
 * Imports.
 */
import {
  AbsoluteTime,
  AmountJson,
  Amounts,
  DenomSelectionState,
  ForcedDenomSel,
  Logger,
} from "@gnu-taler/taler-util";
import { DenominationRecord, timestampAbsoluteFromDb } from "./db.js";
import { isWithdrawableDenom } from "./denominations.js";

const logger = new Logger("denomSelection.ts");

/**
 * Get a list of denominations (with repetitions possible)
 * whose total value is as close as possible to the available
 * amount, but never larger.
 */
export function selectWithdrawalDenominations(
  amountAvailable: AmountJson,
  denoms: DenominationRecord[],
  opts: { limitCoins?: number } = {},
): DenomSelectionState {
  let remaining = Amounts.copy(amountAvailable);

  const selectedDenoms: {
    count: number;
    denomPubHash: string;
  }[] = [];

  let totalCoinValue = Amounts.zeroOfCurrency(amountAvailable.currency);
  let totalWithdrawCost = Amounts.zeroOfCurrency(amountAvailable.currency);
  let earliestDepositExpiration: AbsoluteTime | undefined;
  let hasDenomWithAgeRestriction = false;

  denoms = denoms.filter((d) => isWithdrawableDenom(d));
  denoms.sort((d1, d2) => Amounts.cmp(d2.value, d1.value));

  if (logger.shouldLogTrace()) {
    logger.trace(
      `selecting withdrawal denoms for ${Amounts.stringify(amountAvailable)}`,
    );
  }

  let totalCount = 0;

  for (const d of denoms) {
    const cost = Amounts.add(d.value, d.fees.feeWithdraw).amount;
    const res = Amounts.divmod(remaining, cost);
    let count = res.quotient;
    if (opts.limitCoins != null && totalCount + count > opts.limitCoins) {
      count = opts.limitCoins - totalCount;
    }
    remaining = Amounts.sub(remaining, Amounts.mult(cost, count).amount).amount;
    if (count > 0) {
      totalCoinValue = Amounts.add(
        totalCoinValue,
        Amounts.mult(d.value, count).amount,
      ).amount;
      totalWithdrawCost = Amounts.add(
        totalWithdrawCost,
        Amounts.mult(cost, count).amount,
      ).amount;
      selectedDenoms.push({
        count,
        denomPubHash: d.denomPubHash,
      });
      hasDenomWithAgeRestriction =
        hasDenomWithAgeRestriction || d.denomPub.age_mask > 0;
      const expireDeposit = timestampAbsoluteFromDb(d.stampExpireDeposit);
      if (!earliestDepositExpiration) {
        earliestDepositExpiration = expireDeposit;
      } else {
        earliestDepositExpiration = AbsoluteTime.min(
          expireDeposit,
          earliestDepositExpiration,
        );
      }
    }

    if (logger.shouldLogTrace()) {
      logger.trace(
        `denom_pub_hash=${
          d.denomPubHash
        }, count=${count}, val=${Amounts.stringify(
          d.value,
        )}, wdfee=${Amounts.stringify(d.fees.feeWithdraw)}`,
      );
    }

    if (Amounts.isZero(remaining)) {
      break;
    }
  }

  if (logger.shouldLogTrace()) {
    logger.trace("(end of denom selection)");
  }

  earliestDepositExpiration ??= AbsoluteTime.never();

  return {
    selectedDenoms,
    totalCoinValue: Amounts.stringify(totalCoinValue),
    totalWithdrawCost: Amounts.stringify(totalWithdrawCost),
    hasDenomWithAgeRestriction,
  };
}

export function selectForcedWithdrawalDenominations(
  amountAvailable: AmountJson,
  denoms: DenominationRecord[],
  forcedDenomSel: ForcedDenomSel,
): DenomSelectionState {
  const selectedDenoms: {
    count: number;
    denomPubHash: string;
  }[] = [];

  let totalCoinValue = Amounts.zeroOfCurrency(amountAvailable.currency);
  let totalWithdrawCost = Amounts.zeroOfCurrency(amountAvailable.currency);
  let earliestDepositExpiration: AbsoluteTime | undefined;
  let hasDenomWithAgeRestriction = false;

  denoms = denoms.filter((d) => isWithdrawableDenom(d));
  denoms.sort((d1, d2) => Amounts.cmp(d2.value, d1.value));

  for (const fds of forcedDenomSel.denoms) {
    const count = fds.count;
    const denom = denoms.find((x) => {
      return Amounts.cmp(x.value, fds.value) == 0;
    });
    if (!denom) {
      throw Error(
        `unable to find denom for forced selection (value ${fds.value})`,
      );
    }
    const cost = Amounts.add(denom.value, denom.fees.feeWithdraw).amount;
    totalCoinValue = Amounts.add(
      totalCoinValue,
      Amounts.mult(denom.value, count).amount,
    ).amount;
    totalWithdrawCost = Amounts.add(
      totalWithdrawCost,
      Amounts.mult(cost, count).amount,
    ).amount;
    selectedDenoms.push({
      count,
      denomPubHash: denom.denomPubHash,
    });
    hasDenomWithAgeRestriction =
      hasDenomWithAgeRestriction || denom.denomPub.age_mask > 0;
    const expireDeposit = timestampAbsoluteFromDb(denom.stampExpireDeposit);
    if (!earliestDepositExpiration) {
      earliestDepositExpiration = expireDeposit;
    } else {
      earliestDepositExpiration = AbsoluteTime.min(
        expireDeposit,
        earliestDepositExpiration,
      );
    }
  }

  earliestDepositExpiration ??= AbsoluteTime.never();

  return {
    selectedDenoms,
    totalCoinValue: Amounts.stringify(totalCoinValue),
    totalWithdrawCost: Amounts.stringify(totalWithdrawCost),
    hasDenomWithAgeRestriction,
  };
}
