/*
 This file is part of GNU Taler
 (C) 2019 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 
 */
/**
 * Imports.
 */
import {
  ExchangeUpdateStatus,
  ProposalStatus,
  ReserveRecordStatus,
  Stores,
} from "../types/dbTypes";
import {
  PendingOperationsResponse,
  PendingOperationType,
} from "../types/pending";
import { Duration, getTimestampNow, Timestamp } from "../types/walletTypes";
import { TransactionHandle } from "../util/query";
import { InternalWalletState } from "./state";
function updateRetryDelay(
  oldDelay: Duration,
  now: Timestamp,
  retryTimestamp: Timestamp,
): Duration {
  if (retryTimestamp.t_ms <= now.t_ms) {
    return { d_ms: 0 };
  }
  return { d_ms: Math.min(oldDelay.d_ms, retryTimestamp.t_ms - now.t_ms) };
}
async function gatherExchangePending(
  tx: TransactionHandle,
  now: Timestamp,
  resp: PendingOperationsResponse,
  onlyDue: boolean = false,
): Promise {
  if (onlyDue) {
    // FIXME: exchanges should also be updated regularly
    return;
  }
  await tx.iter(Stores.exchanges).forEach(e => {
    switch (e.updateStatus) {
      case ExchangeUpdateStatus.Finished:
        if (e.lastError) {
          resp.pendingOperations.push({
            type: PendingOperationType.Bug,
            givesLifeness: false,
            message:
              "Exchange record is in FINISHED state but has lastError set",
            details: {
              exchangeBaseUrl: e.baseUrl,
            },
          });
        }
        if (!e.details) {
          resp.pendingOperations.push({
            type: PendingOperationType.Bug,
            givesLifeness: false,
            message:
              "Exchange record does not have details, but no update in progress.",
            details: {
              exchangeBaseUrl: e.baseUrl,
            },
          });
        }
        if (!e.wireInfo) {
          resp.pendingOperations.push({
            type: PendingOperationType.Bug,
            givesLifeness: false,
            message:
              "Exchange record does not have wire info, but no update in progress.",
            details: {
              exchangeBaseUrl: e.baseUrl,
            },
          });
        }
        break;
      case ExchangeUpdateStatus.FetchKeys:
        resp.pendingOperations.push({
          type: PendingOperationType.ExchangeUpdate,
          givesLifeness: false,
          stage: "fetch-keys",
          exchangeBaseUrl: e.baseUrl,
          lastError: e.lastError,
          reason: e.updateReason || "unknown",
        });
        break;
      case ExchangeUpdateStatus.FetchWire:
        resp.pendingOperations.push({
          type: PendingOperationType.ExchangeUpdate,
          givesLifeness: false,
          stage: "fetch-wire",
          exchangeBaseUrl: e.baseUrl,
          lastError: e.lastError,
          reason: e.updateReason || "unknown",
        });
        break;
      case ExchangeUpdateStatus.FinalizeUpdate:
          resp.pendingOperations.push({
            type: PendingOperationType.ExchangeUpdate,
            givesLifeness: false,
            stage: "finalize-update",
            exchangeBaseUrl: e.baseUrl,
            lastError: e.lastError,
            reason: e.updateReason || "unknown",
          });
        break;
      default:
        resp.pendingOperations.push({
          type: PendingOperationType.Bug,
          givesLifeness: false,
          message: "Unknown exchangeUpdateStatus",
          details: {
            exchangeBaseUrl: e.baseUrl,
            exchangeUpdateStatus: e.updateStatus,
          },
        });
        break;
    }
  });
}
async function gatherReservePending(
  tx: TransactionHandle,
  now: Timestamp,
  resp: PendingOperationsResponse,
  onlyDue: boolean = false,
): Promise {
  // FIXME: this should be optimized by using an index for "onlyDue==true".
  await tx.iter(Stores.reserves).forEach(reserve => {
    const reserveType = reserve.bankWithdrawStatusUrl ? "taler-bank" : "manual";
    if (!reserve.retryInfo.active) {
      return;
    }
    switch (reserve.reserveStatus) {
      case ReserveRecordStatus.DORMANT:
        // nothing to report as pending
        break;
      case ReserveRecordStatus.UNCONFIRMED:
        if (onlyDue) {
          break;
        }
        resp.pendingOperations.push({
          type: PendingOperationType.Reserve,
          givesLifeness: false,
          stage: reserve.reserveStatus,
          timestampCreated: reserve.created,
          reserveType,
          reservePub: reserve.reservePub,
          retryInfo: reserve.retryInfo,
        });
        break;
      case ReserveRecordStatus.WAIT_CONFIRM_BANK:
      case ReserveRecordStatus.WITHDRAWING:
      case ReserveRecordStatus.QUERYING_STATUS:
      case ReserveRecordStatus.REGISTERING_BANK:
        resp.nextRetryDelay = updateRetryDelay(
          resp.nextRetryDelay,
          now,
          reserve.retryInfo.nextRetry,
        );
        if (onlyDue && reserve.retryInfo.nextRetry.t_ms > now.t_ms) {
          return;
        }
        resp.pendingOperations.push({
          type: PendingOperationType.Reserve,
          givesLifeness: true,
          stage: reserve.reserveStatus,
          timestampCreated: reserve.created,
          reserveType,
          reservePub: reserve.reservePub,
          retryInfo: reserve.retryInfo,
        });
        break;
      default:
        resp.pendingOperations.push({
          type: PendingOperationType.Bug,
          givesLifeness: false,
          message: "Unknown reserve record status",
          details: {
            reservePub: reserve.reservePub,
            reserveStatus: reserve.reserveStatus,
          },
        });
        break;
    }
  });
}
async function gatherRefreshPending(
  tx: TransactionHandle,
  now: Timestamp,
  resp: PendingOperationsResponse,
  onlyDue: boolean = false,
): Promise {
  await tx.iter(Stores.refreshGroups).forEach(r => {
    if (r.finishedTimestamp) {
      return;
    }
    resp.nextRetryDelay = updateRetryDelay(
      resp.nextRetryDelay,
      now,
      r.retryInfo.nextRetry,
    );
    if (onlyDue && r.retryInfo.nextRetry.t_ms > now.t_ms) {
      return;
    }
    resp.pendingOperations.push({
      type: PendingOperationType.Refresh,
      givesLifeness: true,
      refreshGroupId: r.refreshGroupId,
    });
  });
}
async function gatherWithdrawalPending(
  tx: TransactionHandle,
  now: Timestamp,
  resp: PendingOperationsResponse,
  onlyDue: boolean = false,
): Promise {
  await tx.iter(Stores.withdrawalSession).forEach(wsr => {
    if (wsr.finishTimestamp) {
      return;
    }
    resp.nextRetryDelay = updateRetryDelay(
      resp.nextRetryDelay,
      now,
      wsr.retryInfo.nextRetry,
    );
    if (onlyDue && wsr.retryInfo.nextRetry.t_ms > now.t_ms) {
      return;
    }
    const numCoinsWithdrawn = wsr.withdrawn.reduce(
      (a, x) => a + (x ? 1 : 0),
      0,
    );
    const numCoinsTotal = wsr.withdrawn.length;
    resp.pendingOperations.push({
      type: PendingOperationType.Withdraw,
      givesLifeness: true,
      numCoinsTotal,
      numCoinsWithdrawn,
      source: wsr.source,
      withdrawSessionId: wsr.withdrawSessionId,
    });
  });
}
async function gatherProposalPending(
  tx: TransactionHandle,
  now: Timestamp,
  resp: PendingOperationsResponse,
  onlyDue: boolean = false,
): Promise {
  await tx.iter(Stores.proposals).forEach(proposal => {
    if (proposal.proposalStatus == ProposalStatus.PROPOSED) {
      if (onlyDue) {
        return;
      }
      resp.pendingOperations.push({
        type: PendingOperationType.ProposalChoice,
        givesLifeness: false,
        merchantBaseUrl: proposal.download!!.contractTerms.merchant_base_url,
        proposalId: proposal.proposalId,
        proposalTimestamp: proposal.timestamp,
      });
    } else if (proposal.proposalStatus == ProposalStatus.DOWNLOADING) {
      resp.nextRetryDelay = updateRetryDelay(
        resp.nextRetryDelay,
        now,
        proposal.retryInfo.nextRetry,
      );
      if (onlyDue && proposal.retryInfo.nextRetry.t_ms > now.t_ms) {
        return;
      }
      resp.pendingOperations.push({
        type: PendingOperationType.ProposalDownload,
        givesLifeness: true,
        merchantBaseUrl: proposal.merchantBaseUrl,
        orderId: proposal.orderId,
        proposalId: proposal.proposalId,
        proposalTimestamp: proposal.timestamp,
        lastError: proposal.lastError,
        retryInfo: proposal.retryInfo,
      });
    }
  });
}
async function gatherTipPending(
  tx: TransactionHandle,
  now: Timestamp,
  resp: PendingOperationsResponse,
  onlyDue: boolean = false,
): Promise {
  await tx.iter(Stores.tips).forEach(tip => {
    if (tip.pickedUp) {
      return;
    }
    resp.nextRetryDelay = updateRetryDelay(
      resp.nextRetryDelay,
      now,
      tip.retryInfo.nextRetry,
    );
    if (onlyDue && tip.retryInfo.nextRetry.t_ms > now.t_ms) {
      return;
    }
    if (tip.acceptedTimestamp) {
      resp.pendingOperations.push({
        type: PendingOperationType.TipPickup,
        givesLifeness: true,
        merchantBaseUrl: tip.merchantBaseUrl,
        tipId: tip.tipId,
        merchantTipId: tip.merchantTipId,
      });
    }
  });
}
async function gatherPurchasePending(
  tx: TransactionHandle,
  now: Timestamp,
  resp: PendingOperationsResponse,
  onlyDue: boolean = false,
): Promise {
  await tx.iter(Stores.purchases).forEach(pr => {
    if (pr.paymentSubmitPending) {
      resp.nextRetryDelay = updateRetryDelay(
        resp.nextRetryDelay,
        now,
        pr.payRetryInfo.nextRetry,
      );
      if (!onlyDue || pr.payRetryInfo.nextRetry.t_ms <= now.t_ms) {
        resp.pendingOperations.push({
          type: PendingOperationType.Pay,
          givesLifeness: true,
          isReplay: false,
          proposalId: pr.proposalId,
          retryInfo: pr.payRetryInfo,
          lastError: pr.lastPayError,
        });
      }
    }
    if (pr.refundStatusRequested) {
      resp.nextRetryDelay = updateRetryDelay(
        resp.nextRetryDelay,
        now,
        pr.refundStatusRetryInfo.nextRetry,
      );
      if (!onlyDue || pr.refundStatusRetryInfo.nextRetry.t_ms <= now.t_ms) {
        resp.pendingOperations.push({
          type: PendingOperationType.RefundQuery,
          givesLifeness: true,
          proposalId: pr.proposalId,
          retryInfo: pr.refundStatusRetryInfo,
          lastError: pr.lastRefundStatusError,
        });
      }
    }
    const numRefundsPending = Object.keys(pr.refundState.refundsPending).length;
    if (numRefundsPending > 0) {
      const numRefundsDone = Object.keys(pr.refundState.refundsDone).length;
      resp.nextRetryDelay = updateRetryDelay(
        resp.nextRetryDelay,
        now,
        pr.refundApplyRetryInfo.nextRetry,
      );
      if (!onlyDue || pr.refundApplyRetryInfo.nextRetry.t_ms <= now.t_ms) {
        resp.pendingOperations.push({
          type: PendingOperationType.RefundApply,
          numRefundsDone,
          numRefundsPending,
          givesLifeness: true,
          proposalId: pr.proposalId,
          retryInfo: pr.refundApplyRetryInfo,
          lastError: pr.lastRefundApplyError,
        });
      }
    }
  });
}
export async function getPendingOperations(
  ws: InternalWalletState,
  onlyDue: boolean = false,
): Promise {
  const resp: PendingOperationsResponse = {
    nextRetryDelay: { d_ms: Number.MAX_SAFE_INTEGER },
    pendingOperations: [],
  };
  const now = getTimestampNow();
  await ws.db.runWithReadTransaction(
    [
      Stores.exchanges,
      Stores.reserves,
      Stores.refreshGroups,
      Stores.coins,
      Stores.withdrawalSession,
      Stores.proposals,
      Stores.tips,
      Stores.purchases,
    ],
    async tx => {
      await gatherExchangePending(tx, now, resp, onlyDue);
      await gatherReservePending(tx, now, resp, onlyDue);
      await gatherRefreshPending(tx, now, resp, onlyDue);
      await gatherWithdrawalPending(tx, now, resp, onlyDue);
      await gatherProposalPending(tx, now, resp, onlyDue);
      await gatherTipPending(tx, now, resp, onlyDue);
      await gatherPurchasePending(tx, now, resp, onlyDue);
    },
  );
  return resp;
}