/*
 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 { IDBValidKey } from "@gnu-taler/idb-bridge";
import {
  AmountJson,
  Amounts,
  ExchangePurseStatus,
  NotificationType,
  SelectedProspectiveCoin,
  TalerProtocolTimestamp,
  TransactionIdStr,
  TransactionMajorState,
  TransactionState,
  WalletNotification,
  checkDbInvariant,
} from "@gnu-taler/taler-util";
import { TransitionResultType } from "./common.js";
import { SpendCoinDetails } from "./crypto/cryptoImplementation.js";
import {
  DbPeerPushPaymentCoinSelection,
  ReserveRecord,
  TransactionMetaRecord,
  WalletDbReadOnlyTransaction,
  WalletDbReadWriteTransaction,
  WalletDbStoresArr,
  WalletDbStoresName,
  WalletStoresV1,
} from "./db.js";
import { getTotalRefreshCost } from "./refresh.js";
import { BalanceEffect, applyNotifyTransition } from "./transactions.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],
          feeDeposit: denom.feeDeposit,
        });
      }
    },
  );
  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);
    },
  );
}

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;
}

/** 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)
  );
}

/** Extract the stored type of a DB store */
type StoreType<Store extends WalletDbStoresName> =
  (typeof WalletStoresV1)[Store]["store"]["_dummy"];

interface RecordCtx<Store extends WalletDbStoresName> {
  store: Store;
  transactionId: TransactionIdStr;
  recordId: IDBValidKey;
  wex: WalletExecutionContext;
  recordMeta: (rec: StoreType<Store>) => TransactionMetaRecord;
  recordState: (rec: StoreType<Store>) => {
    txState: TransactionState;
    stId: number;
  };
}

/** Create a new record, update its metadata and notify its creation */
export async function recordCreate<
  Store extends WalletDbStoresName,
  ExtraStores extends WalletDbStoresArr = [],
>(
  ctx: RecordCtx<Store>,
  opts: { extraStores?: ExtraStores; label?: string },
  lambda: (
    tx: WalletDbReadWriteTransaction<
      [Store, "transactionsMeta", ...ExtraStores]
    >,
  ) => Promise<StoreType<Store>>,
) {
  const baseStore = [ctx.store, "transactionsMeta" as const];
  const storeNames = opts.extraStores
    ? [...baseStore, ...opts.extraStores]
    : baseStore;
  await ctx.wex.db.runReadWriteTx(
    { storeNames, label: opts.label },
    async (tx) => {
      const oldTxState: TransactionState = {
        major: TransactionMajorState.None,
      };
      const rec = await lambda(tx);
      // FIXME: DbReadWriteTransaction conditional type confuse Typescript, we should simplify invariable
      await (tx[ctx.store] as any).add(rec);
      await tx.transactionsMeta.put(ctx.recordMeta(rec));
      const newTxState = ctx.recordState(rec);
      applyNotifyTransition(tx.notify, ctx.transactionId, {
        oldTxState,
        newTxState: newTxState.txState,
        balanceEffect: BalanceEffect.Any,
        oldStId: 0,
        newStId: newTxState.stId,
      });
    },
  );
}

/**
 * Optionally update an existing record, ignore if missing.
 * If a transition occurs, update its metadata and notify.
 **/
export async function recordTransition<
  Store extends WalletDbStoresName,
  ExtraStores extends WalletDbStoresArr = [],
>(
  ctx: RecordCtx<Store>,
  opts: { extraStores?: ExtraStores; label?: string },
  lambda: (
    rec: StoreType<Store>,
    tx: WalletDbReadWriteTransaction<
      [Store, "transactionsMeta", ...ExtraStores]
    >,
  ) => Promise<TransitionResultType.Stay | TransitionResultType.Transition>,
): Promise<void> {
  const baseStore = [ctx.store, "transactionsMeta" as const];
  const storeNames = opts.extraStores
    ? [...baseStore, ...opts.extraStores]
    : baseStore;
  await ctx.wex.db.runReadWriteTx(
    { storeNames, label: opts.label },
    async (tx) => {
      const rec = await tx[ctx.store].get(ctx.recordId);
      if (rec == null) {
        // FIXME warn
        return;
      }
      const oldTxState = ctx.recordState(rec);
      const res = await lambda(rec, tx);
      switch (res) {
        case TransitionResultType.Transition: {
          await tx[ctx.store].put(rec);
          await tx.transactionsMeta.put(ctx.recordMeta(rec));
          const newTxState = ctx.recordState(rec);
          applyNotifyTransition(tx.notify, ctx.transactionId, {
            oldTxState: oldTxState.txState,
            newTxState: newTxState.txState,
            balanceEffect: BalanceEffect.Any,
            oldStId: oldTxState.stId,
            newStId: newTxState.stId,
          });
          return;
        }
        case TransitionResultType.Stay:
          return;
      }
    },
  );
}

/** Extract the stored type status if any */
type StoreTypeStatus<Store extends WalletDbStoresName> =
  StoreType<Store> extends { status: infer Status } ? Status : never;

/**
 * Optionally update an existing record status from a state to another, ignore if missing.
 * If a transition occurs, update its metadata and notify.
 */
export async function recordTransitionStatus<Store extends WalletDbStoresName>(
  ctx: RecordCtx<Store>,
  from: StoreTypeStatus<Store>,
  to: StoreTypeStatus<Store>,
): Promise<void> {
  await recordTransition(ctx, {}, async (rec, _) => {
    const it = rec as { status: StoreTypeStatus<Store> };
    if (it.status !== from) {
      return TransitionResultType.Stay;
    } else {
      it.status = to;
      return TransitionResultType.Transition;
    }
  });
}

/**
 * Optionally delete a record, update its metadata and notify.
 **/
export async function recordDelete<Store extends WalletDbStoresName>(
  ctx: RecordCtx<Store>,
  tx: WalletDbReadWriteTransaction<[Store, "transactionsMeta"]>,
  lambda: (
    rec: StoreType<Store>,
    notifs: WalletNotification[],
  ) => Promise<void> = async () => {},
): Promise<{ notifs: WalletNotification[] }> {
  const notifs: WalletNotification[] = [];
  const rec = await tx[ctx.store].get(ctx.recordId);
  if (rec == null) {
    return { notifs };
  }
  const oldTxState = ctx.recordState(rec);
  await lambda(rec, notifs);
  await tx[ctx.store].delete(ctx.recordId);
  await tx.transactionsMeta.delete(ctx.transactionId);
  notifs.push({
    type: NotificationType.TransactionStateTransition,
    transactionId: ctx.transactionId,
    oldTxState: oldTxState.txState,
    newTxState: {
      major: TransactionMajorState.Deleted,
    },
    newStId: -1,
  });
  return { notifs };
}

/**
 * Update record stored transaction metadata
 **/
export async function recordUpdateMeta<Store extends WalletDbStoresName>(
  ctx: RecordCtx<Store>,
  tx: WalletDbReadWriteTransaction<[Store, "transactionsMeta"]>,
): Promise<void> {
  const rec = await tx[ctx.store].get(ctx.recordId);
  if (rec == null) {
    await tx.transactionsMeta.delete(ctx.transactionId);
  } else {
    await tx.transactionsMeta.put(ctx.recordMeta(rec));
  }
}
