/*
 This file is part of GNU Taler
 (C) 2020 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 
 */
/**
 * Imports.
 */
import {
  WalletReserveHistoryItem,
  WalletReserveHistoryItemType,
} from "../types/dbTypes";
import {
  ReserveTransaction,
  ReserveTransactionType,
} from "../types/ReserveTransaction";
import * as Amounts from "../util/amounts";
import { timestampCmp } from "./time";
import { deepCopy } from "./helpers";
import { AmountJson } from "../util/amounts";
/**
 * Helpers for dealing with reserve histories.
 *
 * @author Florian Dold 
 */
export interface ReserveReconciliationResult {
  /**
   * The wallet's local history reconciled with the exchange's reserve history.
   */
  updatedLocalHistory: WalletReserveHistoryItem[];
  /**
   * History items that were newly created, subset of the
   * updatedLocalHistory items.
   */
  newAddedItems: WalletReserveHistoryItem[];
  /**
   * History items that were newly matched, subset of the
   * updatedLocalHistory items.
   */
  newMatchedItems: WalletReserveHistoryItem[];
}
/**
 * Various totals computed from the wallet's view
 * on the reserve history.
 */
export interface ReserveHistorySummary {
  /**
   * Balance computed by the wallet, should match the balance
   * computed by the reserve.
   */
  computedReserveBalance: Amounts.AmountJson;
  /**
   * Reserve balance that is still available for withdrawal.
   */
  unclaimedReserveAmount: Amounts.AmountJson;
  /**
   * Amount that we're still expecting to come into the reserve.
   */
  awaitedReserveAmount: Amounts.AmountJson;
  /**
   * Amount withdrawn from the reserve so far.  Only counts
   * finished withdrawals, not withdrawals in progress.
   */
  withdrawnAmount: Amounts.AmountJson;
}
/**
 * Check if two reserve history items (exchange's version) match.
 */
function isRemoteHistoryMatch(
  t1: ReserveTransaction,
  t2: ReserveTransaction,
): boolean {
  switch (t1.type) {
    case ReserveTransactionType.Closing: {
      return t1.type === t2.type && t1.wtid == t2.wtid;
    }
    case ReserveTransactionType.Credit: {
      return t1.type === t2.type && t1.wire_reference === t2.wire_reference;
    }
    case ReserveTransactionType.Recoup: {
      return (
        t1.type === t2.type &&
        t1.coin_pub === t2.coin_pub &&
        timestampCmp(t1.timestamp, t2.timestamp) === 0
      );
    }
    case ReserveTransactionType.Withdraw: {
      return t1.type === t2.type && t1.h_coin_envelope === t2.h_coin_envelope;
    }
  }
}
/**
 * Check a local reserve history item and a remote history item are a match.
 */
export function isLocalRemoteHistoryMatch(
  t1: WalletReserveHistoryItem,
  t2: ReserveTransaction,
): boolean {
  switch (t1.type) {
    case WalletReserveHistoryItemType.Credit: {
      return (
        t2.type === ReserveTransactionType.Credit &&
        !!t1.expectedAmount &&
        Amounts.cmp(t1.expectedAmount, Amounts.parseOrThrow(t2.amount)) === 0
      );
    }
    case WalletReserveHistoryItemType.Withdraw:
      return (
        t2.type === ReserveTransactionType.Withdraw &&
        !!t1.expectedAmount &&
        Amounts.cmp(t1.expectedAmount, Amounts.parseOrThrow(t2.amount)) === 0
      );
    case WalletReserveHistoryItemType.Recoup: {
      return (
        t2.type === ReserveTransactionType.Recoup &&
        !!t1.expectedAmount &&
        Amounts.cmp(t1.expectedAmount, Amounts.parseOrThrow(t2.amount)) === 0
      );
    }
  }
  return false;
}
/**
 * Compute totals for the wallet's view of the reserve history.
 */
export function summarizeReserveHistory(
  localHistory: WalletReserveHistoryItem[],
  currency: string,
): ReserveHistorySummary {
  const posAmounts: AmountJson[] = [];
  const negAmounts: AmountJson[] = [];
  const expectedPosAmounts: AmountJson[] = [];
  const expectedNegAmounts: AmountJson[] = [];
  const withdrawnAmounts: AmountJson[] = [];
  for (const item of localHistory) {
    switch (item.type) {
      case WalletReserveHistoryItemType.Credit:
        if (item.matchedExchangeTransaction) {
          posAmounts.push(
            Amounts.parseOrThrow(item.matchedExchangeTransaction.amount),
          );
        } else if (item.expectedAmount) {
          expectedPosAmounts.push(item.expectedAmount);
        }
        break;
      case WalletReserveHistoryItemType.Recoup:
        if (item.matchedExchangeTransaction) {
          if (item.matchedExchangeTransaction) {
            posAmounts.push(
              Amounts.parseOrThrow(item.matchedExchangeTransaction.amount),
            );
          } else if (item.expectedAmount) {
            expectedPosAmounts.push(item.expectedAmount);
          } else {
            throw Error("invariant failed");
          }
        }
        break;
      case WalletReserveHistoryItemType.Closing:
        if (item.matchedExchangeTransaction) {
          negAmounts.push(
            Amounts.parseOrThrow(item.matchedExchangeTransaction.amount),
          );
        } else {
          throw Error("invariant failed");
        }
        break;
      case WalletReserveHistoryItemType.Withdraw:
        if (item.matchedExchangeTransaction) {
          negAmounts.push(
            Amounts.parseOrThrow(item.matchedExchangeTransaction.amount),
          );
          withdrawnAmounts.push(
            Amounts.parseOrThrow(item.matchedExchangeTransaction.amount),
          );
        } else if (item.expectedAmount) {
          expectedNegAmounts.push(item.expectedAmount);
        } else {
          throw Error("invariant failed");
        }
        break;
    }
  }
  const z = Amounts.getZero(currency);
  const computedBalance = Amounts.sub(
    Amounts.add(z, ...posAmounts).amount,
    ...negAmounts,
  ).amount;
  const unclaimedReserveAmount = Amounts.sub(
    Amounts.add(z, ...posAmounts).amount,
    ...negAmounts,
    ...expectedNegAmounts,
  ).amount;
  const awaitedReserveAmount = Amounts.sub(
    Amounts.add(z, ...expectedPosAmounts).amount,
    ...expectedNegAmounts,
  ).amount;
  const withdrawnAmount = Amounts.add(z, ...withdrawnAmounts).amount;
  return {
    computedReserveBalance: computedBalance,
    unclaimedReserveAmount: unclaimedReserveAmount,
    awaitedReserveAmount: awaitedReserveAmount,
    withdrawnAmount,
  };
}
/**
 * Reconcile the wallet's local model of the reserve history
 * with the reserve history of the exchange.
 */
export function reconcileReserveHistory(
  localHistory: WalletReserveHistoryItem[],
  remoteHistory: ReserveTransaction[],
): ReserveReconciliationResult {
  const updatedLocalHistory: WalletReserveHistoryItem[] = deepCopy(
    localHistory,
  );
  const newMatchedItems: WalletReserveHistoryItem[] = [];
  const newAddedItems: WalletReserveHistoryItem[] = [];
  const remoteMatched = remoteHistory.map(() => false);
  const localMatched = localHistory.map(() => false);
  // Take care of deposits
  // First, see which pairs are already a definite match.
  for (let remoteIndex = 0; remoteIndex < remoteHistory.length; remoteIndex++) {
    const rhi = remoteHistory[remoteIndex];
    for (let localIndex = 0; localIndex < localHistory.length; localIndex++) {
      if (localMatched[localIndex]) {
        continue;
      }
      const lhi = localHistory[localIndex];
      if (!lhi.matchedExchangeTransaction) {
        continue;
      }
      if (isRemoteHistoryMatch(rhi, lhi.matchedExchangeTransaction)) {
        localMatched[localIndex] = true;
        remoteMatched[remoteIndex] = true;
        break;
      }
    }
  }
  // Check that all previously matched items are still matched
  for (let localIndex = 0; localIndex < localHistory.length; localIndex++) {
    if (localMatched[localIndex]) {
      continue;
    }
    const lhi = localHistory[localIndex];
    if (lhi.matchedExchangeTransaction) {
      // Don't use for further matching
      localMatched[localIndex] = true;
      // FIXME: emit some error here!
      throw Error("previously matched reserve history item now unmatched");
    }
  }
  // Next, find out if there are any exact new matches between local and remote
  // history items
  for (let localIndex = 0; localIndex < localHistory.length; localIndex++) {
    if (localMatched[localIndex]) {
      continue;
    }
    const lhi = localHistory[localIndex];
    for (
      let remoteIndex = 0;
      remoteIndex < remoteHistory.length;
      remoteIndex++
    ) {
      const rhi = remoteHistory[remoteIndex];
      if (remoteMatched[remoteIndex]) {
        continue;
      }
      if (isLocalRemoteHistoryMatch(lhi, rhi)) {
        localMatched[localIndex] = true;
        remoteMatched[remoteIndex] = true;
        updatedLocalHistory[localIndex].matchedExchangeTransaction = rhi as any;
        newMatchedItems.push(lhi);
        break;
      }
    }
  }
  // Finally we add new history items
  for (let remoteIndex = 0; remoteIndex < remoteHistory.length; remoteIndex++) {
    if (remoteMatched[remoteIndex]) {
      continue;
    }
    const rhi = remoteHistory[remoteIndex];
    let newItem: WalletReserveHistoryItem;
    switch (rhi.type) {
      case ReserveTransactionType.Closing: {
        newItem = {
          type: WalletReserveHistoryItemType.Closing,
          matchedExchangeTransaction: rhi,
        };
        break;
      }
      case ReserveTransactionType.Credit: {
        newItem = {
          type: WalletReserveHistoryItemType.Credit,
          matchedExchangeTransaction: rhi,
        };
        break;
      }
      case ReserveTransactionType.Recoup: {
        newItem = {
          type: WalletReserveHistoryItemType.Recoup,
          matchedExchangeTransaction: rhi,
        };
        break;
      }
      case ReserveTransactionType.Withdraw: {
        newItem = {
          type: WalletReserveHistoryItemType.Withdraw,
          matchedExchangeTransaction: rhi,
        };
        break;
      }
    }
    updatedLocalHistory.push(newItem);
    newAddedItems.push(newItem);
  }
  return {
    updatedLocalHistory,
    newAddedItems,
    newMatchedItems,
  };
}