/*
 This file is part of GNU Taler
 (C) 2019-2024 Taler Systems S.A.

 GNU Taler is free software; you can redistribute it and/or modify it under the
 terms of the GNU Affero 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 Affero General Public License along with
 GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>

 SPDX-License-Identifier: AGPL-3.0-or-later
 */

/**
 * Type and schema definitions for the wallet's transaction list.
 *
 * @author Florian Dold
 * @author Torsten Grote
 */

/**
 * Imports.
 */
import {
  Codec,
  buildCodecForObject,
  codecForAny,
  codecForBoolean,
  codecForConstString,
  codecForEither,
  codecForList,
  codecForNumber,
  codecForString,
  codecOptional,
} from "./codec.js";
import {
  TalerPreciseTimestamp,
  TalerProtocolDuration,
  TalerProtocolTimestamp,
  codecForPreciseTimestamp,
} from "./time.js";
import {
  AmountString,
  InternationalizedString,
  codecForInternationalizedString,
} from "./types-taler-common.js";
import {
  MerchantContractTerms,
  MerchantInfo,
  codecForMerchantInfo,
} from "./types-taler-merchant.js";
import {
  RefreshReason,
  ScopeInfo,
  TalerErrorDetail,
  TransactionIdStr,
  TransactionStateFilter,
  WithdrawalExchangeAccountDetails,
  codecForScopeInfo,
} from "./types-taler-wallet.js";

export interface TransactionsRequest {
  /**
   * return only transactions in the given currency
   *
   * it will be removed in next release
   *
   * @deprecated use scopeInfo
   */
  currency?: string;

  /**
   * return only transactions in the given scopeInfo
   */
  scopeInfo?: ScopeInfo;

  /**
   * if present, results will be limited to transactions related to the given search string
   */
  search?: string;

  /**
   * Sort order of the transaction items.
   * By default, items are sorted ascending by their
   * main timestamp.
   *
   * ascending: ascending by timestamp, but pending transactions first
   * descending: ascending by timestamp, but pending transactions first
   * stable-ascending: ascending by timestamp, with pending transactions amidst other transactions
   *    (stable in the sense of: pending transactions don't jump around)
   */
  sort?: "ascending" | "descending" | "stable-ascending";

  /**
   * If true, include all refreshes in the transactions list.
   */
  includeRefreshes?: boolean;

  filterByState?: TransactionStateFilter;
}

export interface GetTransactionsV2Request {
  /**
   * Return only transactions in the given currency.
   */
  currency?: string;

  /**
   * Return only transactions in the given scopeInfo
   */
  scopeInfo?: ScopeInfo;

  /**
   * If true, include all refreshes in the transactions list.
   */
  includeRefreshes?: boolean;

  /**
   * If true, include transactions that would usually be filtered out.
   * Implies includeRefreshes.
   */
  includeAll?: boolean;

  /**
   * Only return transactions before/after this offset.
   */
  offsetTransactionId?: TransactionIdStr;

  /**
   * Only return transactions before/after the transaction with this
   * timestamp.
   *
   * Used as a fallback if the offsetTransactionId was deleted.
   */
  offsetTimestamp?: TalerPreciseTimestamp;

  /**
   * Number of transactions to return.
   *
   * When the limit is positive, results are returned
   * in ascending order of their timestamp.  If no offset is specified,
   * the result list begins with the first transaction.
   * If an offset is specified, transactions after the offset are returned.
   *
   * When the limit is negative, results are returned
   * in descending order of their timestamp.  If no offset is specified,
   * the result list begins with with the last transaction.
   * If an offset is specified, transactions before the offset are returned.
   */
  limit?: number;

  /**
   * Filter transactions by their state / state category.
   *
   * If not specified, all transactions are returned.
   */
  filterByState?: "final" | "nonfinal" | "done";
}

export interface TransactionState {
  major: TransactionMajorState;
  minor?: TransactionMinorState;
}

export type TransactionStateWildcard = "*";

export enum TransactionMajorState {
  // No state, only used when reporting transitions into the initial state
  None = "none",
  Pending = "pending",
  Done = "done",
  Aborting = "aborting",
  Aborted = "aborted",
  Dialog = "dialog",
  Finalizing = "finalizing",
  // Plain suspended is always a suspended pending state.
  Suspended = "suspended",
  SuspendedFinalizing = "suspended-finalizing",
  SuspendedAborting = "suspended-aborting",
  Failed = "failed",
  Expired = "expired",
  // Only used for the notification, never in the transaction history
  Deleted = "deleted",
}

export enum TransactionMinorState {
  // Placeholder until D37 is fully implemented
  Unknown = "unknown",
  Deposit = "deposit",
  KycRequired = "kyc",
  KycInit = "kyc-init",
  MergeKycRequired = "merge-kyc",
  BalanceKycRequired = "balance-kyc",
  BalanceKycInit = "balance-kyc-init",
  KycAuthRequired = "kyc-auth",
  Track = "track",
  SubmitPayment = "submit-payment",
  RebindSession = "rebind-session",
  Refresh = "refresh",
  Pickup = "pickup",
  AutoRefund = "auto-refund",
  User = "user",
  Bank = "bank",
  Exchange = "exchange",
  ClaimProposal = "claim-proposal",
  CheckRefund = "check-refund",
  CreatePurse = "create-purse",
  DeletePurse = "delete-purse",
  RefreshExpired = "refresh-expired",
  Ready = "ready",
  Merge = "merge",
  Repurchase = "repurchase",
  BankRegisterReserve = "bank-register-reserve",
  BankConfirmTransfer = "bank-confirm-transfer",
  WithdrawCoins = "withdraw-coins",
  ExchangeWaitReserve = "exchange-wait-reserve",
  AbortingBank = "aborting-bank",
  Aborting = "aborting",
  Refused = "refused",
  Withdraw = "withdraw",
  MerchantOrderProposed = "merchant-order-proposed",
  Proposed = "proposed",
  RefundAvailable = "refund-available",
  AcceptRefund = "accept-refund",
  PaidByOther = "paid-by-other",
  CompletedByOtherWallet = "completed-by-other-wallet",
}

export enum TransactionAction {
  Delete = "delete",
  Suspend = "suspend",
  Resume = "resume",
  Abort = "abort",
  Fail = "fail",
  Retry = "retry",
}

export interface TransactionsResponse {
  // a list of past and pending transactions sorted by pending, timestamp and transactionId.
  // In case two events are both pending and have the same timestamp,
  // they are sorted by the transactionId
  // (lexically ascending and locale-independent comparison).
  transactions: Transaction[];
}

export interface TransactionCommon {
  // opaque unique ID for the transaction, used as a starting point for paginating queries
  // and for invoking actions on the transaction (e.g. deleting/hiding it from the history)
  transactionId: TransactionIdStr;

  // the type of the transaction; different types might provide additional information
  type: TransactionType;

  // main timestamp of the transaction
  timestamp: TalerPreciseTimestamp;

  /**
   * Scope of this tx
   */
  scopes: ScopeInfo[];

  /**
   * Transaction state, as per DD37.
   */
  txState: TransactionState;

  /**
   * Wallet-internal state ID, only used for debugging and testing.
   */
  stId: number;

  /**
   * Possible transitions based on the current state.
   */
  txActions: TransactionAction[];

  /**
   * Raw amount of the transaction (exclusive of fees or other extra costs).
   */
  amountRaw: AmountString;

  /**
   * Amount added or removed from the wallet's balance (including all fees and other costs).
   */
  amountEffective: AmountString;

  error?: TalerErrorDetail;

  abortReason?: TalerErrorDetail;

  failReason?: TalerErrorDetail;

  /**
   * If the transaction minor state is in KycRequired this field is going to
   * have the location where the user need to go to complete KYC information.
   */
  kycUrl?: string;

  /**
   * KYC payto hash. Useful for testing, not so useful for UIs.
   */
  kycPaytoHash?: string;

  /**
   * KYC access token. Useful for testing, not so useful for UIs.
   */
  kycAccessToken?: string;

  kycAuthTransferInfo?: KycAuthTransferInfo;
}

export interface KycAuthTransferInfo {
  /**
   * Payto URI of the account that must make the transfer.
   *
   * The KYC auth transfer will *not* work if it originates
   * from a different account.
   */
  debitPaytoUri: string;

  /**
   * Account public key that must be included in the subject.
   */
  accountPub: string;

  /**
   * Amount that the exchange expects to be deposited.
   *
   * Usually corresponds to the TINY_AMOUNT configuration of the exchange,
   * and thus is the smallest amount that can be transferred
   * via a bank transfer.
   */
  amount: AmountString;

  /**
   * Possible target payto URIs.
   */
  creditPaytoUris: string[];
}

export type Transaction =
  | TransactionWithdrawal
  | TransactionPayment
  | TransactionRefund
  | TransactionRefresh
  | TransactionDeposit
  | TransactionPeerPullCredit
  | TransactionPeerPullDebit
  | TransactionPeerPushCredit
  | TransactionPeerPushDebit
  | TransactionInternalWithdrawal
  | TransactionRecoup
  | TransactionDenomLoss;

export enum TransactionType {
  Withdrawal = "withdrawal",
  InternalWithdrawal = "internal-withdrawal",
  Payment = "payment",
  Refund = "refund",
  Refresh = "refresh",
  Deposit = "deposit",
  PeerPushDebit = "peer-push-debit",
  PeerPushCredit = "peer-push-credit",
  PeerPullDebit = "peer-pull-debit",
  PeerPullCredit = "peer-pull-credit",
  Recoup = "recoup",
  DenomLoss = "denom-loss",
}

export enum WithdrawalType {
  TalerBankIntegrationApi = "taler-bank-integration-api",
  ManualTransfer = "manual-transfer",
}

export type WithdrawalDetails =
  | WithdrawalDetailsForManualTransfer
  | WithdrawalDetailsForTalerBankIntegrationApi;

interface WithdrawalDetailsForManualTransfer {
  type: WithdrawalType.ManualTransfer;

  /**
   * Payto URIs that the exchange supports.
   *
   * Already contains the amount and message.
   *
   * @deprecated in favor of exchangeCreditAccounts
   */
  exchangePaytoUris: string[];

  exchangeCreditAccountDetails?: WithdrawalExchangeAccountDetails[];

  // Public key of the reserve
  reservePub: string;

  /**
   * Is the reserve ready for withdrawal?
   */
  reserveIsReady: boolean;

  /**
   * How long does the exchange wait to transfer back funds from a
   * reserve?
   */
  reserveClosingDelay: TalerProtocolDuration;
}

interface WithdrawalDetailsForTalerBankIntegrationApi {
  type: WithdrawalType.TalerBankIntegrationApi;

  /**
   * Set to true if the bank has confirmed the withdrawal, false if not.
   * An unconfirmed withdrawal usually requires user-input and should be highlighted in the UI.
   * See also bankConfirmationUrl below.
   */
  confirmed: boolean;

  /**
   * If the withdrawal is unconfirmed, this can include a URL for user
   * initiated confirmation.
   */
  bankConfirmationUrl?: string;

  // Public key of the reserve
  reservePub: string;

  /**
   * Is the reserve ready for withdrawal?
   */
  reserveIsReady: boolean;

  /**
   * Is the bank transfer for the withdrawal externally confirmed?
   */
  externalConfirmation?: boolean;

  exchangeCreditAccountDetails?: WithdrawalExchangeAccountDetails[];
}

export enum DenomLossEventType {
  DenomExpired = "denom-expired",
  DenomVanished = "denom-vanished",
  DenomUnoffered = "denom-unoffered",
}

/**
 * A transaction to indicate financial loss due to denominations
 * that became unusable for deposits.
 */
export interface TransactionDenomLoss extends TransactionCommon {
  type: TransactionType.DenomLoss;
  lossEventType: DenomLossEventType;
  exchangeBaseUrl: string;
}

/**
 * A withdrawal transaction (either bank-integrated or manual).
 */
export interface TransactionWithdrawal extends TransactionCommon {
  type: TransactionType.Withdrawal;

  /**
   * Exchange of the withdrawal.
   */
  exchangeBaseUrl: string | undefined;

  /**
   * Amount that got subtracted from the reserve balance.
   */
  amountRaw: AmountString;

  /**
   * Amount that actually was (or will be) added to the wallet's balance.
   */
  amountEffective: AmountString;

  withdrawalDetails: WithdrawalDetails;
}

/**
 * Internal withdrawal operation, only reported on request.
 *
 * Some transactions (peer-*-credit) internally do a withdrawal,
 * but only the peer-*-credit transaction is reported.
 *
 * The internal withdrawal transaction allows to access the details of
 * the underlying withdrawal for testing/debugging.
 *
 * It is usually not reported, so that amounts of transactions properly
 * add up, since the amountEffecive of the withdrawal is already reported
 * in the peer-*-credit transaction.
 */
export interface TransactionInternalWithdrawal extends TransactionCommon {
  type: TransactionType.InternalWithdrawal;

  /**
   * Exchange of the withdrawal.
   */
  exchangeBaseUrl: string;

  /**
   * Amount that got subtracted from the reserve balance.
   */
  amountRaw: AmountString;

  /**
   * Amount that actually was (or will be) added to the wallet's balance.
   */
  amountEffective: AmountString;

  withdrawalDetails: WithdrawalDetails;
}

export interface PeerInfoShort {
  expiration: TalerProtocolTimestamp | undefined;
  summary: string | undefined;
  iconId: string | undefined;
}

/**
 * Credit because we were paid for a P2P invoice we created.
 */
export interface TransactionPeerPullCredit extends TransactionCommon {
  type: TransactionType.PeerPullCredit;

  info: PeerInfoShort;
  /**
   * Exchange used.
   */
  exchangeBaseUrl: string;

  /**
   * Amount that got subtracted from the reserve balance.
   */
  amountRaw: AmountString;

  /**
   * Amount that actually was (or will be) added to the wallet's balance.
   */
  amountEffective: AmountString;

  /**
   * URI to send to the other party.
   *
   * Only available in the right state.
   */
  talerUri: string | undefined;
}

/**
 * Debit because we paid someone's invoice.
 */
export interface TransactionPeerPullDebit extends TransactionCommon {
  type: TransactionType.PeerPullDebit;

  info: PeerInfoShort;
  /**
   * Exchange used.
   */
  exchangeBaseUrl: string;

  amountRaw: AmountString;

  amountEffective: AmountString;
}

/**
 * We sent money via a P2P payment.
 */
export interface TransactionPeerPushDebit extends TransactionCommon {
  type: TransactionType.PeerPushDebit;

  info: PeerInfoShort;
  /**
   * Exchange used.
   */
  exchangeBaseUrl: string;

  /**
   * Amount that got subtracted from the reserve balance.
   */
  amountRaw: AmountString;

  /**
   * Amount that actually was (or will be) added to the wallet's balance.
   */
  amountEffective: AmountString;

  /**
   * URI to accept the payment.
   *
   * Only present if the transaction is in a state where the other party can
   * accept the payment.
   */
  talerUri?: string;
}

/**
 * We received money via a P2P payment.
 */
export interface TransactionPeerPushCredit extends TransactionCommon {
  type: TransactionType.PeerPushCredit;

  info: PeerInfoShort;
  /**
   * Exchange used.
   */
  exchangeBaseUrl: string;

  /**
   * Amount that got subtracted from the reserve balance.
   */
  amountRaw: AmountString;

  /**
   * Amount that actually was (or will be) added to the wallet's balance.
   */
  amountEffective: AmountString;
}

/**
 * The exchange revoked a key and the wallet recoups funds.
 */
export interface TransactionRecoup extends TransactionCommon {
  type: TransactionType.Recoup;
}

export enum PaymentStatus {
  /**
   * Explicitly aborted after timeout / failure
   */
  Aborted = "aborted",

  /**
   * Payment failed, wallet will auto-retry.
   * User should be given the option to retry now / abort.
   */
  Failed = "failed",

  /**
   * Paid successfully
   */
  Paid = "paid",

  /**
   * User accepted, payment is processing.
   */
  Accepted = "accepted",
}

export interface TransactionPayment extends TransactionCommon {
  type: TransactionType.Payment;

  /**
   * Additional information about the payment.
   */
  info: OrderShortInfo;

  /**
   * Full contract terms.
   *
   * Only included if explicitly included in the request.
   */
  contractTerms?: MerchantContractTerms;

  /**
   * Amount that must be paid for the contract
   */
  amountRaw: AmountString;

  /**
   * Amount that was paid, including deposit, wire and refresh fees.
   */
  amountEffective: AmountString;

  /**
   * Amount that has been refunded by the merchant
   */
  totalRefundRaw: AmountString;

  /**
   * Amount will be added to the wallet's balance after fees and refreshing
   */
  totalRefundEffective: AmountString;

  /**
   * Amount pending to be picked up
   */
  refundPending: AmountString | undefined;

  /**
   * Reference to applied refunds
   */
  refunds: RefundInfoShort[];

  /**
   * Is the wallet currently checking for a refund?
   */
  refundQueryActive: boolean;

  /**
   * PoS confirmation codes, separated by newlines.
   * Only present for purchases that support PoS confirmation.
   */
  posConfirmation: string | undefined;

  /**
   * Until when will the posConfirmation be valid?
   */
  posConfirmationDeadline?: TalerProtocolTimestamp;

  /**
   * Did we receive the payment via a taler://pay-template/ URI
   * and did the URI contain a nfc=1 flag?
   */
  posConfirmationViaNfc?: boolean;
}

export interface OrderShortInfo {
  /**
   * Order ID, uniquely identifies the order within a merchant instance
   */
  orderId: string;

  /**
   * Hash of the contract terms.
   */
  contractTermsHash: string;

  /**
   * More information about the merchant
   */
  merchant: MerchantInfo;

  /**
   * Summary of the order, given by the merchant
   */
  summary: string;

  /**
   * Map from IETF BCP 47 language tags to localized summaries
   */
  summary_i18n?: InternationalizedString;

  /**
   * URL of the fulfillment, given by the merchant
   */
  fulfillmentUrl?: string;

  /**
   * Plain text message that should be shown to the user
   * when the payment is complete.
   */
  fulfillmentMessage?: string;

  /**
   * Translations of fulfillmentMessage.
   */
  fulfillmentMessage_i18n?: InternationalizedString;
}

export interface RefundInfoShort {
  transactionId: string;
  timestamp: TalerProtocolTimestamp;
  amountEffective: AmountString;
  amountRaw: AmountString;
}

/**
 * Summary information about the payment that we got a refund for.
 */
export interface RefundPaymentInfo {
  summary: string;
  summary_i18n?: InternationalizedString;
  /**
   * More information about the merchant
   */
  merchant: MerchantInfo;
}

export interface TransactionRefund extends TransactionCommon {
  type: TransactionType.Refund;

  // Amount that has been refunded by the merchant
  amountRaw: AmountString;

  // Amount will be added to the wallet's balance after fees and refreshing
  amountEffective: AmountString;

  // ID for the transaction that is refunded
  refundedTransactionId: string;

  paymentInfo: RefundPaymentInfo | undefined;
}

/**
 * A transaction shown for refreshes.
 * Only shown for (1) refreshes not associated with other transactions
 * and (2) refreshes in an error state.
 */
export interface TransactionRefresh extends TransactionCommon {
  type: TransactionType.Refresh;

  refreshReason: RefreshReason;

  /**
   * Transaction ID that caused this refresh.
   */
  originatingTransactionId?: string;

  /**
   * Always zero for refreshes
   */
  amountRaw: AmountString;

  /**
   * Fees, i.e. the effective, negative effect of the refresh
   * on the balance.
   *
   * Only applicable for stand-alone refreshes, and zero for
   * other refreshes where the transaction itself accounts for the
   * refresh fee.
   */
  amountEffective: AmountString;

  refreshInputAmount: AmountString;
  refreshOutputAmount: AmountString;
}

export interface DepositTransactionTrackingState {
  // Raw wire transfer identifier of the deposit.
  wireTransferId: string;
  // When was the wire transfer given to the bank.
  timestampExecuted: TalerProtocolTimestamp;
  // Total amount transfer for this wtid (including fees)
  amountRaw: AmountString;
  // Wire fee amount for this exchange
  wireFee: AmountString;
}

/**
 * Deposit transaction, which effectively sends
 * money from this wallet somewhere else.
 */
export interface TransactionDeposit extends TransactionCommon {
  type: TransactionType.Deposit;

  depositGroupId: string;

  /**
   * Target for the deposit.
   */
  targetPaytoUri: string;

  /**
   * Raw amount that is being deposited
   */
  amountRaw: AmountString;

  /**
   * Deposit account public key.
   */
  accountPub: string;

  /**
   * Effective amount that is being deposited
   */
  amountEffective: AmountString;

  wireTransferDeadline: TalerProtocolTimestamp;

  wireTransferProgress: number;

  /**
   * Did all the deposit requests succeed?
   */
  deposited: boolean;

  trackingState: Array<DepositTransactionTrackingState>;
}

export interface TransactionByIdRequest {
  transactionId: string;

  /**
   * If set to true, report the full contract terms in the response
   * if the transaction has them.
   */
  includeContractTerms?: boolean;
}

export const codecForTransactionByIdRequest =
  (): Codec<TransactionByIdRequest> =>
    buildCodecForObject<TransactionByIdRequest>()
      .property("transactionId", codecForString())
      .property("includeContractTerms", codecOptional(codecForBoolean()))
      .build("TransactionByIdRequest");

export const codecForGetTransactionsV2Request =
  (): Codec<GetTransactionsV2Request> =>
    buildCodecForObject<GetTransactionsV2Request>()
      .property("currency", codecOptional(codecForString()))
      .property("scopeInfo", codecOptional(codecForScopeInfo()))
      .property(
        "offsetTransactionId",
        codecOptional(codecForString() as Codec<TransactionIdStr>),
      )
      .property("offsetTimestamp", codecOptional(codecForPreciseTimestamp))
      .property("limit", codecOptional(codecForNumber()))
      .property(
        "filterByState",
        codecOptional(
          codecForEither(
            codecForConstString("final"),
            codecForConstString("nonfinal"),
            codecForConstString("done"),
          ),
        ),
      )
      .property("includeRefreshes", codecOptional(codecForBoolean()))
      .property("includeAll", codecOptional(codecForBoolean()))
      .build("GetTransactionsV2Request");

export const codecForTransactionsRequest = (): Codec<TransactionsRequest> =>
  buildCodecForObject<TransactionsRequest>()
    .property("currency", codecOptional(codecForString()))
    .property("scopeInfo", codecOptional(codecForScopeInfo()))
    .property("search", codecOptional(codecForString()))
    .property(
      "sort",
      codecOptional(
        codecForEither(
          codecForConstString("ascending"),
          codecForConstString("descending"),
          codecForConstString("stable-ascending"),
        ),
      ),
    )
    .property("includeRefreshes", codecOptional(codecForBoolean()))
    .build("TransactionsRequest");

// FIXME: do full validation here!
export const codecForTransactionsResponse = (): Codec<TransactionsResponse> =>
  buildCodecForObject<TransactionsResponse>()
    .property("transactions", codecForList(codecForAny()))
    .build("TransactionsResponse");

export const codecForOrderShortInfo = (): Codec<OrderShortInfo> =>
  buildCodecForObject<OrderShortInfo>()
    .property("contractTermsHash", codecForString())
    .property("fulfillmentMessage", codecOptional(codecForString()))
    .property(
      "fulfillmentMessage_i18n",
      codecOptional(codecForInternationalizedString()),
    )
    .property("fulfillmentUrl", codecOptional(codecForString()))
    .property("merchant", codecForMerchantInfo())
    .property("orderId", codecForString())
    .property("summary", codecForString())
    .property("summary_i18n", codecOptional(codecForInternationalizedString()))
    .build("OrderShortInfo");

export interface ListAssociatedRefreshesRequest {
  transactionId: string;
}

export const codecForListAssociatedRefreshesRequest =
  (): Codec<ListAssociatedRefreshesRequest> =>
    buildCodecForObject<ListAssociatedRefreshesRequest>()
      .property("transactionId", codecForString())
      .build("ListAssociatedRefreshesRequest");

export interface ListAssociatedRefreshesResponse {
  transactionIds: string[];
}
