/*
 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
 */

/**
 * Types used by clients of the wallet.
 *
 * These types are defined in a separate file make tree shaking easier, since
 * some components use these types (via RPC) but do not depend on the wallet
 * code directly.
 *
 * @author Florian Dold <dold@taler.net>
 */

/**
 * Imports.
 */
import { AmountJson, codecForAmountString } from "./amounts.js";
import {
  Codec,
  Context,
  DecodingError,
  buildCodecForObject,
  buildCodecForUnion,
  codecForAny,
  codecForBoolean,
  codecForConstString,
  codecForEither,
  codecForList,
  codecForMap,
  codecForNumber,
  codecForString,
  codecOptional,
  renderContext,
} from "./codec.js";
import {
  AmountString,
  CurrencySpecification,
  EddsaPrivateKeyString,
  InternationalizedString,
  TalerMerchantApi,
  TemplateParams,
  WithdrawalOperationStatus,
  canonicalizeBaseUrl,
} from "./index.js";
import { VersionMatchResult } from "./libtool-version.js";
import { PaytoString, PaytoUri, codecForPaytoString } from "./payto.js";
import { QrCodeSpec } from "./qr.js";
import { AgeCommitmentProof } from "./taler-crypto.js";
import { TalerErrorCode } from "./taler-error-codes.js";
import {
  AbsoluteTime,
  TalerPreciseTimestamp,
  TalerProtocolDuration,
  TalerProtocolTimestamp,
  codecForAbsoluteTime,
  codecForPreciseTimestamp,
  codecForTimestamp,
} from "./time.js";
import {
  AccountRestriction,
  AuditorDenomSig,
  CoinEnvelope,
  DenomKeyType,
  DenominationPubKey,
  ExchangeAuditor,
  ExchangeWireAccount,
  PeerContractTerms,
  UnblindedSignature,
  codecForExchangeWireAccount,
  codecForPeerContractTerms,
} from "./types-taler-exchange.js";
import {
  MerchantContractTerms,
  MerchantInfo,
  codecForMerchantContractTerms,
} from "./types-taler-merchant.js";
import { BackupRecovery } from "./types-taler-sync.js";
import {
  OrderShortInfo,
  TransactionState,
  TransactionType,
} from "./types-taler-wallet-transactions.js";

/**
 * Identifier for a transaction in the wallet.
 */
declare const __txId: unique symbol;
export type TransactionIdStr = `txn:${string}:${string}` & { [__txId]: true };

/**
 * Identifier for a pending task in the wallet.
 */
declare const __pndId: unique symbol;
export type PendingIdStr = `pnd:${string}:${string}` & { [__pndId]: true };

declare const __tmbId: unique symbol;
export type TombstoneIdStr = `tmb:${string}:${string}` & { [__tmbId]: true };

function codecForTransactionIdStr(): Codec<TransactionIdStr> {
  return {
    decode(x: any, c?: Context): TransactionIdStr {
      if (typeof x === "string" && x.startsWith("txn:")) {
        return x as TransactionIdStr;
      }
      throw new DecodingError(
        `expected string starting with "txn:" at ${renderContext(
          c,
        )} but got ${x}`,
      );
    },
  };
}

function codecForPendingIdStr(): Codec<PendingIdStr> {
  return {
    decode(x: any, c?: Context): PendingIdStr {
      if (typeof x === "string" && x.startsWith("txn:")) {
        return x as PendingIdStr;
      }
      throw new DecodingError(
        `expected string starting with "txn:" at ${renderContext(
          c,
        )} but got ${x}`,
      );
    },
  };
}

function codecForTombstoneIdStr(): Codec<TombstoneIdStr> {
  return {
    decode(x: any, c?: Context): TombstoneIdStr {
      if (typeof x === "string" && x.startsWith("tmb:")) {
        return x as TombstoneIdStr;
      }
      throw new DecodingError(
        `expected string starting with "tmb:" at ${renderContext(
          c,
        )} but got ${x}`,
      );
    },
  };
}

export function codecForCanonBaseUrl(): Codec<string> {
  return {
    decode(x: any, c?: Context): string {
      if (typeof x === "string") {
        const canon = canonicalizeBaseUrl(x);
        if (x !== canon) {
          throw new DecodingError(
            `expected canonicalized base URL at ${renderContext(
              c,
            )} but got value '${x}'`,
          );
        }
        return x;
      }
      throw new DecodingError(
        `expected base URL at ${renderContext(c)} but got type ${typeof x}`,
      );
    },
  };
}

/**
 * Response for the create reserve request to the wallet.
 */
export class CreateReserveResponse {
  /**
   * Exchange URL where the bank should create the reserve.
   * The URL is canonicalized in the response.
   */
  exchange: string;

  /**
   * Reserve public key of the newly created reserve.
   */
  reservePub: string;
}

export interface GetBalanceDetailRequest {
  currency: string;
}

export const codecForGetBalanceDetailRequest =
  (): Codec<GetBalanceDetailRequest> =>
    buildCodecForObject<GetBalanceDetailRequest>()
      .property("currency", codecForString())
      .build("GetBalanceDetailRequest");

/**
 * How the amount should be interpreted in a transaction
 * Effective = how the balance is change
 * Raw = effective amount without fee
 *
 * Depending on the transaction, raw can be higher than effective
 */
export enum TransactionAmountMode {
  Effective = "effective",
  Raw = "raw",
}

export type GetPlanForOperationRequest =
  | GetPlanForWithdrawRequest
  | GetPlanForDepositRequest;
// | GetPlanForPushDebitRequest
// | GetPlanForPullCreditRequest
// | GetPlanForPaymentRequest
// | GetPlanForTipRequest
// | GetPlanForRefundRequest
// | GetPlanForPullDebitRequest
// | GetPlanForPushCreditRequest;

interface GetPlanForWalletInitiatedOperation {
  instructedAmount: AmountString;
  mode: TransactionAmountMode;
}

export interface ConvertAmountRequest {
  amount: AmountString;
  type: TransactionAmountMode;
  depositPaytoUri: PaytoString;
}

export const codecForConvertAmountRequest =
  buildCodecForObject<ConvertAmountRequest>()
    .property("amount", codecForAmountString())
    .property("depositPaytoUri", codecForPaytoString())
    .property(
      "type",
      codecForEither(
        codecForConstString(TransactionAmountMode.Raw),
        codecForConstString(TransactionAmountMode.Effective),
      ),
    )
    .build("ConvertAmountRequest");

export interface GetAmountRequest {
  currency: string;
}

export const codecForGetAmountRequest = buildCodecForObject<GetAmountRequest>()
  .property("currency", codecForString())
  .build("GetAmountRequest");

interface GetPlanToCompleteOperation {
  instructedAmount: AmountString;
}

const codecForGetPlanForWalletInitiatedOperation = <
  T extends GetPlanForWalletInitiatedOperation,
>() =>
  buildCodecForObject<T>()
    .property(
      "mode",
      codecForEither(
        codecForConstString(TransactionAmountMode.Raw),
        codecForConstString(TransactionAmountMode.Effective),
      ),
    )
    .property("instructedAmount", codecForAmountString());

interface GetPlanForWithdrawRequest extends GetPlanForWalletInitiatedOperation {
  type: TransactionType.Withdrawal;
  exchangeUrl?: string;
}
interface GetPlanForDepositRequest extends GetPlanForWalletInitiatedOperation {
  type: TransactionType.Deposit;
  account: string; //payto string
}
interface GetPlanForPushDebitRequest
  extends GetPlanForWalletInitiatedOperation {
  type: TransactionType.PeerPushDebit;
}

interface GetPlanForPullCreditRequest
  extends GetPlanForWalletInitiatedOperation {
  type: TransactionType.PeerPullCredit;
  exchangeUrl: string;
}

const codecForGetPlanForWithdrawRequest =
  codecForGetPlanForWalletInitiatedOperation<GetPlanForWithdrawRequest>()
    .property("type", codecForConstString(TransactionType.Withdrawal))
    .property("exchangeUrl", codecOptional(codecForString()))
    .build("GetPlanForWithdrawRequest");

const codecForGetPlanForDepositRequest =
  codecForGetPlanForWalletInitiatedOperation<GetPlanForDepositRequest>()
    .property("type", codecForConstString(TransactionType.Deposit))
    .property("account", codecForString())
    .build("GetPlanForDepositRequest");

const codecForGetPlanForPushDebitRequest =
  codecForGetPlanForWalletInitiatedOperation<GetPlanForPushDebitRequest>()
    .property("type", codecForConstString(TransactionType.PeerPushDebit))
    .build("GetPlanForPushDebitRequest");

const codecForGetPlanForPullCreditRequest =
  codecForGetPlanForWalletInitiatedOperation<GetPlanForPullCreditRequest>()
    .property("type", codecForConstString(TransactionType.PeerPullCredit))
    .property("exchangeUrl", codecForString())
    .build("GetPlanForPullCreditRequest");

interface GetPlanForPaymentRequest extends GetPlanToCompleteOperation {
  type: TransactionType.Payment;
  wireMethod: string;
  ageRestriction: number;
  maxDepositFee: AmountString;
}

interface GetPlanForPullDebitRequest extends GetPlanToCompleteOperation {
  type: TransactionType.PeerPullDebit;
}

interface GetPlanForPushCreditRequest extends GetPlanToCompleteOperation {
  type: TransactionType.PeerPushCredit;
}

const codecForGetPlanForPaymentRequest =
  buildCodecForObject<GetPlanForPaymentRequest>()
    .property("type", codecForConstString(TransactionType.Payment))
    .property("maxDepositFee", codecForAmountString())
    .build("GetPlanForPaymentRequest");

const codecForGetPlanForPullDebitRequest =
  buildCodecForObject<GetPlanForPullDebitRequest>()
    .property("type", codecForConstString(TransactionType.PeerPullDebit))
    .build("GetPlanForPullDebitRequest");

const codecForGetPlanForPushCreditRequest =
  buildCodecForObject<GetPlanForPushCreditRequest>()
    .property("type", codecForConstString(TransactionType.PeerPushCredit))
    .build("GetPlanForPushCreditRequest");

export const codecForGetPlanForOperationRequest =
  (): Codec<GetPlanForOperationRequest> =>
    buildCodecForUnion<GetPlanForOperationRequest>()
      .discriminateOn("type")
      .alternative(
        TransactionType.Withdrawal,
        codecForGetPlanForWithdrawRequest,
      )
      .alternative(TransactionType.Deposit, codecForGetPlanForDepositRequest)
      // .alternative(
      //   TransactionType.PeerPushDebit,
      //   codecForGetPlanForPushDebitRequest,
      // )
      // .alternative(
      //   TransactionType.PeerPullCredit,
      //   codecForGetPlanForPullCreditRequest,
      // )
      // .alternative(TransactionType.Payment, codecForGetPlanForPaymentRequest)
      // .alternative(
      //   TransactionType.PeerPullDebit,
      //   codecForGetPlanForPullDebitRequest,
      // )
      // .alternative(
      //   TransactionType.PeerPushCredit,
      //   codecForGetPlanForPushCreditRequest,
      // )
      .build("GetPlanForOperationRequest");

export interface GetPlanForOperationResponse {
  effectiveAmount: AmountString;
  rawAmount: AmountString;
  counterPartyAmount?: AmountString;
  details: any;
}

export const codecForGetPlanForOperationResponse =
  (): Codec<GetPlanForOperationResponse> =>
    buildCodecForObject<GetPlanForOperationResponse>()
      .property("effectiveAmount", codecForAmountString())
      .property("rawAmount", codecForAmountString())
      .property("details", codecForAny())
      .property("counterPartyAmount", codecOptional(codecForAmountString()))
      .build("GetPlanForOperationResponse");

export interface AmountResponse {
  effectiveAmount: AmountString;
  rawAmount: AmountString;
}

export const codecForAmountResponse = (): Codec<AmountResponse> =>
  buildCodecForObject<AmountResponse>()
    .property("effectiveAmount", codecForAmountString())
    .property("rawAmount", codecForAmountString())
    .build("AmountResponse");

export enum BalanceFlag {
  IncomingKyc = "incoming-kyc",
  IncomingAml = "incoming-aml",
  IncomingConfirmation = "incoming-confirmation",
  OutgoingKyc = "outgoing-kyc",
}

export interface WalletBalance {
  scopeInfo: ScopeInfo;
  available: AmountString;
  pendingIncoming: AmountString;
  pendingOutgoing: AmountString;

  /**
   * Does the balance for this currency have a pending
   * transaction?
   *
   * @deprecated use flags and pendingIncoming/pendingOutgoing instead
   */
  hasPendingTransactions: boolean;

  /**
   * Is there a transaction that requires user input?
   *
   * @deprecated use flags instead
   */
  requiresUserInput: boolean;

  flags: BalanceFlag[];
}

export const codecForScopeInfoGlobal = (): Codec<ScopeInfoGlobal> =>
  buildCodecForObject<ScopeInfoGlobal>()
    .property("currency", codecForString())
    .property("type", codecForConstString(ScopeType.Global))
    .build("ScopeInfoGlobal");

export const codecForScopeInfoExchange = (): Codec<ScopeInfoExchange> =>
  buildCodecForObject<ScopeInfoExchange>()
    .property("currency", codecForString())
    .property("type", codecForConstString(ScopeType.Exchange))
    .property("url", codecForString())
    .build("ScopeInfoExchange");

export const codecForScopeInfoAuditor = (): Codec<ScopeInfoAuditor> =>
  buildCodecForObject<ScopeInfoAuditor>()
    .property("currency", codecForString())
    .property("type", codecForConstString(ScopeType.Auditor))
    .property("url", codecForString())
    .build("ScopeInfoAuditor");

export const codecForScopeInfo = (): Codec<ScopeInfo> =>
  buildCodecForUnion<ScopeInfo>()
    .discriminateOn("type")
    .alternative(ScopeType.Global, codecForScopeInfoGlobal())
    .alternative(ScopeType.Exchange, codecForScopeInfoExchange())
    .alternative(ScopeType.Auditor, codecForScopeInfoAuditor())
    .build("ScopeInfo");

export interface GetCurrencySpecificationRequest {
  scope: ScopeInfo;
}

export const codecForGetCurrencyInfoRequest =
  (): Codec<GetCurrencySpecificationRequest> =>
    buildCodecForObject<GetCurrencySpecificationRequest>()
      .property("scope", codecForScopeInfo())
      .build("GetCurrencySpecificationRequest");

export interface ListExchangesForScopedCurrencyRequest {
  scope: ScopeInfo;
}

export const codecForListExchangesForScopedCurrencyRequest =
  (): Codec<ListExchangesForScopedCurrencyRequest> =>
    buildCodecForObject<ListExchangesForScopedCurrencyRequest>()
      .property("scope", codecForScopeInfo())
      .build("ListExchangesForScopedCurrencyRequest");

export interface GetCurrencySpecificationResponse {
  currencySpecification: CurrencySpecification;
}

export interface BuiltinExchange {
  exchangeBaseUrl: string;
  currencyHint: string;
}

export interface PartialWalletRunConfig {
  builtin?: Partial<WalletRunConfig["builtin"]>;
  testing?: Partial<WalletRunConfig["testing"]>;
  features?: Partial<WalletRunConfig["features"]>;
  lazyTaskLoop?: Partial<WalletRunConfig["lazyTaskLoop"]>;
}

export interface WalletRunConfig {
  /**
   * Initialization values useful for a complete startup.
   *
   * These are values may be overridden by different wallets
   */
  builtin: {
    exchanges: BuiltinExchange[];
  };

  /**
   * Unsafe options which it should only be used to create
   * testing environment.
   */
  testing: {
    /**
     * Allow withdrawal of denominations even though they are about to expire.
     */
    denomselAllowLate: boolean;
    devModeActive: boolean;
    insecureTrustExchange: boolean;
    preventThrottling: boolean;
    skipDefaults: boolean;
    emitObservabilityEvents?: boolean;
  };

  /**
   * Configurations values that may be safe to show to the user
   */
  features: {
    allowHttp: boolean;
  };

  /**
   * Start processing tasks only when explicitly required, even after
   * init has been called.
   *
   * Useful when the wallet is started to make single read-only request,
   * as otherwise wallet-core starts making network request and process
   * unrelated pending tasks.
   */
  lazyTaskLoop: boolean;
}

export interface InitRequest {
  config?: PartialWalletRunConfig;
}

export const codecForInitRequest = (): Codec<InitRequest> =>
  buildCodecForObject<InitRequest>()
    .property("config", codecForAny())
    .build("InitRequest");

export interface InitResponse {
  versionInfo: WalletCoreVersion;
}

export enum ScopeType {
  Global = "global",
  Exchange = "exchange",
  Auditor = "auditor",
}

export type ScopeInfoGlobal = { type: ScopeType.Global; currency: string };

export type ScopeInfoExchange = {
  type: ScopeType.Exchange;
  currency: string;
  url: string;
};

export type ScopeInfoAuditor = {
  type: ScopeType.Auditor;
  currency: string;
  url: string;
};

export type ScopeInfo = ScopeInfoGlobal | ScopeInfoExchange | ScopeInfoAuditor;

/**
 * Encode scope info as a string.
 *
 * Format must be stable as it's used in the database.
 */
export function stringifyScopeInfo(si: ScopeInfo): string {
  switch (si.type) {
    case ScopeType.Global:
      return `taler-si:global/${si.currency}}`;
    case ScopeType.Auditor:
      return `taler-si:auditor/${si.currency}/${encodeURIComponent(si.url)}`;
    case ScopeType.Exchange:
      return `taler-si:exchange/${si.currency}/${encodeURIComponent(si.url)}`;
  }
}

export interface BalancesResponse {
  balances: WalletBalance[];
}

export const codecForBalance = (): Codec<WalletBalance> =>
  buildCodecForObject<WalletBalance>()
    .property("scopeInfo", codecForAny()) // FIXME
    .property("available", codecForAmountString())
    .property("hasPendingTransactions", codecForBoolean())
    .property("pendingIncoming", codecForAmountString())
    .property("pendingOutgoing", codecForAmountString())
    .property("requiresUserInput", codecForBoolean())
    .property("flags", codecForAny()) // FIXME
    .build("Balance");

export const codecForBalancesResponse = (): Codec<BalancesResponse> =>
  buildCodecForObject<BalancesResponse>()
    .property("balances", codecForList(codecForBalance()))
    .build("BalancesResponse");

/**
 * For terseness.
 */
export function mkAmount(
  value: number,
  fraction: number,
  currency: string,
): AmountJson {
  return { value, fraction, currency };
}

/**
 * Status of a coin.
 */
export enum CoinStatus {
  /**
   * Withdrawn and never shown to anybody.
   */
  Fresh = "fresh",

  /**
   * Coin was lost as the denomination is not usable anymore.
   */
  DenomLoss = "denom-loss",

  /**
   * Fresh, but currently marked as "suspended", thus won't be used
   * for spending.  Used for testing.
   */
  FreshSuspended = "fresh-suspended",

  /**
   * A coin that has been spent and refreshed.
   */
  Dormant = "dormant",
}

export type WalletCoinHistoryItem =
  | {
      type: "withdraw";
      transactionId: TransactionIdStr;
    }
  | {
      type: "spend";
      transactionId: TransactionIdStr;
      amount: AmountString;
    }
  | {
      type: "refresh";
      transactionId: TransactionIdStr;
      amount: AmountString;
    }
  | {
      type: "recoup";
      transactionId: TransactionIdStr;
      amount: AmountString;
    }
  | {
      type: "refund";
      transactionId: TransactionIdStr;
      amount: AmountString;
    };

/**
 * Easy to process format for the public data of coins
 * managed by the wallet.
 */
export interface CoinDumpJson {
  coins: Array<{
    /**
     * The coin's denomination's public key.
     */
    denomPub: DenominationPubKey;
    /**
     * Hash of denom_pub.
     */
    denomPubHash: string;
    /**
     * Value of the denomination (without any fees).
     */
    denomValue: string;
    /**
     * Public key of the coin.
     */
    coinPub: string;
    /**
     * Base URL of the exchange for the coin.
     */
    exchangeBaseUrl: string;
    /**
     * Public key of the parent coin.
     * Only present if this coin was obtained via refreshing.
     */
    refreshParentCoinPub: string | undefined;
    /**
     * Public key of the reserve for this coin.
     * Only present if this coin was obtained via refreshing.
     */
    withdrawalReservePub: string | undefined;
    /**
     * Status of the coin.
     */
    coinStatus: CoinStatus;
    /**
     * Information about the age restriction
     */
    ageCommitmentProof: AgeCommitmentProof | undefined;
    history: WalletCoinHistoryItem[];
  }>;
}

export enum ConfirmPayResultType {
  Done = "done",
  Pending = "pending",
}

/**
 * Result for confirmPay
 */
export interface ConfirmPayResultDone {
  type: ConfirmPayResultType.Done;
  contractTerms: MerchantContractTerms;
  transactionId: TransactionIdStr;
}

export interface ConfirmPayResultPending {
  type: ConfirmPayResultType.Pending;
  transactionId: TransactionIdStr;
  lastError: TalerErrorDetail | undefined;
}

export const codecForTalerErrorDetail = (): Codec<TalerErrorDetail> =>
  buildCodecForObject<TalerErrorDetail>()
    .property("code", codecForNumber())
    .property("when", codecOptional(codecForAbsoluteTime))
    .property("hint", codecOptional(codecForString()))
    .build("TalerErrorDetail");

export type ConfirmPayResult = ConfirmPayResultDone | ConfirmPayResultPending;

export const codecForConfirmPayResultPending =
  (): Codec<ConfirmPayResultPending> =>
    buildCodecForObject<ConfirmPayResultPending>()
      .property("lastError", codecOptional(codecForTalerErrorDetail()))
      .property("transactionId", codecForTransactionIdStr())
      .property("type", codecForConstString(ConfirmPayResultType.Pending))
      .build("ConfirmPayResultPending");

export const codecForConfirmPayResultDone = (): Codec<ConfirmPayResultDone> =>
  buildCodecForObject<ConfirmPayResultDone>()
    .property("type", codecForConstString(ConfirmPayResultType.Done))
    .property("transactionId", codecForTransactionIdStr())
    .property("contractTerms", codecForMerchantContractTerms())
    .build("ConfirmPayResultDone");

export const codecForConfirmPayResult = (): Codec<ConfirmPayResult> =>
  buildCodecForUnion<ConfirmPayResult>()
    .discriminateOn("type")
    .alternative(
      ConfirmPayResultType.Pending,
      codecForConfirmPayResultPending(),
    )
    .alternative(ConfirmPayResultType.Done, codecForConfirmPayResultDone())
    .build("ConfirmPayResult");

/**
 * Information about all sender wire details known to the wallet,
 * as well as exchanges that accept these wire types.
 */
export interface SenderWireInfos {
  /**
   * Mapping from exchange base url to list of accepted
   * wire types.
   */
  exchangeWireTypes: { [exchangeBaseUrl: string]: string[] };

  /**
   * Sender wire information stored in the wallet.
   */
  senderWires: string[];
}

/**
 * Request to mark a reserve as confirmed.
 */
export interface ConfirmReserveRequest {
  /**
   * Public key of then reserve that should be marked
   * as confirmed.
   */
  reservePub: string;
}

export const codecForConfirmReserveRequest = (): Codec<ConfirmReserveRequest> =>
  buildCodecForObject<ConfirmReserveRequest>()
    .property("reservePub", codecForString())
    .build("ConfirmReserveRequest");

export interface PrepareRefundResult {
  proposalId: string;

  effectivePaid: AmountString;
  gone: AmountString;
  granted: AmountString;
  pending: boolean;
  awaiting: AmountString;

  info: OrderShortInfo;
}

export interface BenchmarkResult {
  time: { [s: string]: number };
  repetitions: number;
}

export enum PreparePayResultType {
  PaymentPossible = "payment-possible",
  InsufficientBalance = "insufficient-balance",
  AlreadyConfirmed = "already-confirmed",
}

export const codecForPreparePayResultPaymentPossible =
  (): Codec<PreparePayResultPaymentPossible> =>
    buildCodecForObject<PreparePayResultPaymentPossible>()
      .property("amountEffective", codecForAmountString())
      .property("amountRaw", codecForAmountString())
      .property("contractTerms", codecForMerchantContractTerms())
      .property("transactionId", codecForTransactionIdStr())
      .property("proposalId", codecForString())
      .property("contractTermsHash", codecForString())
      .property("talerUri", codecForString())
      .property(
        "status",
        codecForConstString(PreparePayResultType.PaymentPossible),
      )
      .build("PreparePayResultPaymentPossible");

export interface BalanceDetails {}

/**
 * Detailed reason for why the wallet's balance is insufficient.
 */
export interface PaymentInsufficientBalanceDetails {
  /**
   * Amount requested by the merchant.
   */
  amountRequested: AmountString;

  /**
   * Balance of type "available" (see balance.ts for definition).
   */
  balanceAvailable: AmountString;

  /**
   * Balance of type "material" (see balance.ts for definition).
   */
  balanceMaterial: AmountString;

  /**
   * Balance of type "age-acceptable" (see balance.ts for definition).
   */
  balanceAgeAcceptable: AmountString;

  /**
   * Balance of type "merchant-acceptable" (see balance.ts for definition).
   */
  balanceReceiverAcceptable: AmountString;

  /**
   * Balance of type "merchant-depositable" (see balance.ts for definition).
   */
  balanceReceiverDepositable: AmountString;

  balanceExchangeDepositable: AmountString;

  /**
   * Maximum effective amount that the wallet can spend,
   * when all fees are paid by the wallet.
   */
  maxEffectiveSpendAmount: AmountString;

  perExchange: {
    [url: string]: {
      balanceAvailable: AmountString;
      balanceMaterial: AmountString;
      balanceExchangeDepositable: AmountString;
      balanceAgeAcceptable: AmountString;
      balanceReceiverAcceptable: AmountString;
      balanceReceiverDepositable: AmountString;
      maxEffectiveSpendAmount: AmountString;
      /**
       * Exchange doesn't have global fees configured for the relevant year,
       * p2p payments aren't possible.
       */
      missingGlobalFees: boolean;
    };
  };
}

export const codecForPayMerchantInsufficientBalanceDetails =
  (): Codec<PaymentInsufficientBalanceDetails> =>
    buildCodecForObject<PaymentInsufficientBalanceDetails>()
      .property("amountRequested", codecForAmountString())
      .property("balanceAgeAcceptable", codecForAmountString())
      .property("balanceAvailable", codecForAmountString())
      .property("balanceMaterial", codecForAmountString())
      .property("balanceReceiverAcceptable", codecForAmountString())
      .property("balanceReceiverDepositable", codecForAmountString())
      .property("balanceExchangeDepositable", codecForAmountString())
      .property("perExchange", codecForAny())
      .property("maxEffectiveSpendAmount", codecForAmountString())
      .build("PayMerchantInsufficientBalanceDetails");

export const codecForPreparePayResultInsufficientBalance =
  (): Codec<PreparePayResultInsufficientBalance> =>
    buildCodecForObject<PreparePayResultInsufficientBalance>()
      .property("amountRaw", codecForAmountString())
      .property("contractTerms", codecForAny())
      .property("talerUri", codecForString())
      .property("proposalId", codecForString())
      .property("transactionId", codecForTransactionIdStr())
      .property(
        "status",
        codecForConstString(PreparePayResultType.InsufficientBalance),
      )
      .property(
        "balanceDetails",
        codecForPayMerchantInsufficientBalanceDetails(),
      )
      .build("PreparePayResultInsufficientBalance");

export const codecForPreparePayResultAlreadyConfirmed =
  (): Codec<PreparePayResultAlreadyConfirmed> =>
    buildCodecForObject<PreparePayResultAlreadyConfirmed>()
      .property(
        "status",
        codecForConstString(PreparePayResultType.AlreadyConfirmed),
      )
      .property("amountEffective", codecOptional(codecForAmountString()))
      .property("amountRaw", codecForAmountString())
      .property("paid", codecForBoolean())
      .property("talerUri", codecForString())
      .property("contractTerms", codecForAny())
      .property("contractTermsHash", codecForString())
      .property("transactionId", codecForTransactionIdStr())
      .property("proposalId", codecForString())
      .build("PreparePayResultAlreadyConfirmed");

export const codecForPreparePayResult = (): Codec<PreparePayResult> =>
  buildCodecForUnion<PreparePayResult>()
    .discriminateOn("status")
    .alternative(
      PreparePayResultType.AlreadyConfirmed,
      codecForPreparePayResultAlreadyConfirmed(),
    )
    .alternative(
      PreparePayResultType.InsufficientBalance,
      codecForPreparePayResultInsufficientBalance(),
    )
    .alternative(
      PreparePayResultType.PaymentPossible,
      codecForPreparePayResultPaymentPossible(),
    )
    .build("PreparePayResult");

/**
 * Result of a prepare pay operation.
 */
export type PreparePayResult =
  | PreparePayResultInsufficientBalance
  | PreparePayResultAlreadyConfirmed
  | PreparePayResultPaymentPossible;

/**
 * Payment is possible.
 */
export interface PreparePayResultPaymentPossible {
  status: PreparePayResultType.PaymentPossible;
  transactionId: TransactionIdStr;
  /**
   * @deprecated use transactionId instead
   */
  proposalId: string;
  contractTerms: MerchantContractTerms;
  contractTermsHash: string;
  amountRaw: AmountString;
  amountEffective: AmountString;
  talerUri: string;
}

export interface PreparePayResultInsufficientBalance {
  status: PreparePayResultType.InsufficientBalance;
  transactionId: TransactionIdStr;
  /**
   * @deprecated use transactionId
   */
  proposalId: string;
  contractTerms: MerchantContractTerms;
  amountRaw: AmountString;
  talerUri: string;
  balanceDetails: PaymentInsufficientBalanceDetails;
}

export interface PreparePayResultAlreadyConfirmed {
  status: PreparePayResultType.AlreadyConfirmed;
  transactionId: TransactionIdStr;
  contractTerms: MerchantContractTerms;
  paid: boolean;
  amountRaw: AmountString;
  amountEffective: AmountString | undefined;
  contractTermsHash: string;
  /**
   * @deprecated use transactionId
   */
  proposalId: string;
  talerUri: string;
}

export interface BankWithdrawDetails {
  status: WithdrawalOperationStatus;
  currency: string;
  amount: AmountJson | undefined;
  editableAmount: boolean;
  maxAmount: AmountJson | undefined;
  wireFee: AmountJson | undefined;
  senderWire?: string;
  exchange?: string;
  editableExchange: boolean;
  confirmTransferUrl?: string;
  wireTypes: string[];
  operationId: string;
  apiBaseUrl: string;
}

export interface AcceptWithdrawalResponse {
  reservePub: string;
  confirmTransferUrl?: string;
  transactionId: TransactionIdStr;
}

/**
 * Details about a purchase, including refund status.
 */
export interface PurchaseDetails {
  contractTerms: Record<string, undefined>;
  hasRefund: boolean;
  totalRefundAmount: AmountJson;
  totalRefundAndRefreshFees: AmountJson;
}

export interface WalletDiagnostics {
  walletManifestVersion: string;
  walletManifestDisplayVersion: string;
  errors: string[];
  firefoxIdbProblem: boolean;
  dbOutdated: boolean;
}

export interface TalerErrorDetail {
  code: TalerErrorCode;
  when?: AbsoluteTime;
  hint?: string;
  [x: string]: unknown;
}

/**
 * Minimal information needed about a planchet for unblinding a signature.
 *
 * Can be a withdrawal/refresh planchet.
 */
export interface PlanchetUnblindInfo {
  denomPub: DenominationPubKey;
  blindingKey: string;
}

export interface WithdrawalPlanchet {
  coinPub: string;
  coinPriv: string;
  reservePub: string;
  denomPubHash: string;
  denomPub: DenominationPubKey;
  blindingKey: string;
  withdrawSig: string;
  coinEv: CoinEnvelope;
  coinValue: AmountJson;
  coinEvHash: string;
  ageCommitmentProof?: AgeCommitmentProof;
}

export interface PlanchetCreationRequest {
  secretSeed: string;
  coinIndex: number;
  value: AmountJson;
  feeWithdraw: AmountJson;
  denomPub: DenominationPubKey;
  reservePub: string;
  reservePriv: string;
  restrictAge?: number;
}

/**
 * Reasons for why a coin is being refreshed.
 */
export enum RefreshReason {
  Manual = "manual",
  PayMerchant = "pay-merchant",
  PayDeposit = "pay-deposit",
  PayPeerPush = "pay-peer-push",
  PayPeerPull = "pay-peer-pull",
  Refund = "refund",
  AbortPay = "abort-pay",
  AbortDeposit = "abort-deposit",
  AbortPeerPushDebit = "abort-peer-push-debit",
  AbortPeerPullDebit = "abort-peer-pull-debit",
  Recoup = "recoup",
  BackupRestored = "backup-restored",
  Scheduled = "scheduled",
}

/**
 * Request to refresh a single coin.
 */
export interface CoinRefreshRequest {
  readonly coinPub: string;
  readonly amount: AmountString;
}

/**
 * Private data required to make a deposit permission.
 */
export interface DepositInfo {
  exchangeBaseUrl: string;
  contractTermsHash: string;
  coinPub: string;
  coinPriv: string;
  spendAmount: AmountJson;
  timestamp: TalerProtocolTimestamp;
  refundDeadline: TalerProtocolTimestamp;
  merchantPub: string;
  feeDeposit: AmountJson;
  wireInfoHash: string;
  denomKeyType: DenomKeyType;
  denomPubHash: string;
  denomSig: UnblindedSignature;

  requiredMinimumAge?: number;

  ageCommitmentProof?: AgeCommitmentProof;
}

export interface ExchangesShortListResponse {
  exchanges: ShortExchangeListItem[];
}

export interface ExchangesListResponse {
  exchanges: ExchangeListItem[];
}

export interface ExchangeDetailedResponse {
  exchange: ExchangeFullDetails;
}

export interface WalletCoreVersion {
  implementationSemver: string;
  implementationGitHash: string;

  /**
   * Wallet-core protocol version supported by this implementation
   * of the API ("server" version).
   */
  version: string;
  exchange: string;
  merchant: string;

  bankIntegrationApiRange: string;
  bankConversionApiRange: string;
  corebankApiRange: string;

  /**
   * @deprecated as bank was split into multiple APIs with separate versioning
   */
  bank: string;

  /**
   * @deprecated
   */
  hash: string | undefined;

  /**
   * @deprecated will be removed
   */
  devMode: boolean;
}

export interface KnownBankAccountsInfo {
  uri: PaytoUri;
  kyc_completed: boolean;
  currency: string;
  alias: string;
}

export interface KnownBankAccounts {
  accounts: KnownBankAccountsInfo[];
}

/**
 * Wire fee for one wire method
 */
export interface WireFee {
  /**
   * Fee for wire transfers.
   */
  wireFee: AmountString;

  /**
   * Fees to close and refund a reserve.
   */
  closingFee: AmountString;

  /**
   * Start date of the fee.
   */
  startStamp: TalerProtocolTimestamp;

  /**
   * End date of the fee.
   */
  endStamp: TalerProtocolTimestamp;

  /**
   * Signature made by the exchange master key.
   */
  sig: string;
}

export type WireFeeMap = { [wireMethod: string]: WireFee[] };

export interface WireInfo {
  feesForType: WireFeeMap;
  accounts: ExchangeWireAccount[];
}

export interface ExchangeGlobalFees {
  startDate: TalerProtocolTimestamp;
  endDate: TalerProtocolTimestamp;

  historyFee: AmountString;
  accountFee: AmountString;
  purseFee: AmountString;

  historyTimeout: TalerProtocolDuration;
  purseTimeout: TalerProtocolDuration;

  purseLimit: number;

  signature: string;
}

const codecForWireFee = (): Codec<WireFee> =>
  buildCodecForObject<WireFee>()
    .property("sig", codecForString())
    .property("wireFee", codecForAmountString())
    .property("closingFee", codecForAmountString())
    .property("startStamp", codecForTimestamp)
    .property("endStamp", codecForTimestamp)
    .build("codecForWireFee");

const codecForWireInfo = (): Codec<WireInfo> =>
  buildCodecForObject<WireInfo>()
    .property("feesForType", codecForMap(codecForList(codecForWireFee())))
    .property("accounts", codecForList(codecForExchangeWireAccount()))
    .build("codecForWireInfo");

export interface DenominationInfo {
  /**
   * Value of one coin of the denomination.
   */
  value: AmountString;

  /**
   * Hash of the denomination public key.
   * Stored in the database for faster lookups.
   */
  denomPubHash: string;

  denomPub: DenominationPubKey;

  /**
   * Fee for withdrawing.
   */
  feeWithdraw: AmountString;

  /**
   * Fee for depositing.
   */
  feeDeposit: AmountString;

  /**
   * Fee for refreshing.
   */
  feeRefresh: AmountString;

  /**
   * Fee for refunding.
   */
  feeRefund: AmountString;

  /**
   * Validity start date of the denomination.
   */
  stampStart: TalerProtocolTimestamp;

  /**
   * Date after which the currency can't be withdrawn anymore.
   */
  stampExpireWithdraw: TalerProtocolTimestamp;

  /**
   * Date after the denomination officially doesn't exist anymore.
   */
  stampExpireLegal: TalerProtocolTimestamp;

  /**
   * Data after which coins of this denomination can't be deposited anymore.
   */
  stampExpireDeposit: TalerProtocolTimestamp;

  exchangeBaseUrl: string;
}

export type DenomOperation = "deposit" | "withdraw" | "refresh" | "refund";
export type DenomOperationMap<T> = { [op in DenomOperation]: T };

export interface FeeDescription {
  group: string;
  from: AbsoluteTime;
  until: AbsoluteTime;
  fee?: AmountString;
}

export interface FeeDescriptionPair {
  group: string;
  from: AbsoluteTime;
  until: AbsoluteTime;
  left?: AmountString;
  right?: AmountString;
}

export interface TimePoint<T> {
  id: string;
  group: string;
  fee: AmountString;
  type: "start" | "end";
  moment: AbsoluteTime;
  denom: T;
}

export interface ExchangeFullDetails {
  exchangeBaseUrl: string;
  currency: string;
  paytoUris: string[];
  auditors: ExchangeAuditor[];
  wireInfo: WireInfo;
  denomFees: DenomOperationMap<FeeDescription[]>;
  transferFees: Record<string, FeeDescription[]>;
  globalFees: FeeDescription[];
}

export enum ExchangeTosStatus {
  Pending = "pending",
  Proposed = "proposed",
  Accepted = "accepted",
  MissingTos = "missing-tos",
}

export enum ExchangeEntryStatus {
  Preset = "preset",
  Ephemeral = "ephemeral",
  Used = "used",
}

export enum ExchangeUpdateStatus {
  Initial = "initial",
  InitialUpdate = "initial-update",
  Suspended = "suspended",
  UnavailableUpdate = "unavailable-update",
  Ready = "ready",
  ReadyUpdate = "ready-update",
  OutdatedUpdate = "outdated-update",
}

export interface OperationErrorInfo {
  error: TalerErrorDetail;
}

export interface ShortExchangeListItem {
  exchangeBaseUrl: string;
}

/**
 * Info about an exchange entry in the wallet.
 */
export interface ExchangeListItem {
  exchangeBaseUrl: string;
  masterPub: string | undefined;
  currency: string;
  paytoUris: string[];
  tosStatus: ExchangeTosStatus;
  exchangeEntryStatus: ExchangeEntryStatus;
  exchangeUpdateStatus: ExchangeUpdateStatus;
  ageRestrictionOptions: number[];

  /**
   * P2P payments are disabled with this exchange
   * (e.g. because no global fees are configured).
   */
  peerPaymentsDisabled: boolean;

  /**
   * Set to true if this exchange doesn't charge any fees.
   */
  noFees: boolean;

  scopeInfo: ScopeInfo;

  lastUpdateTimestamp: TalerPreciseTimestamp | undefined;

  /**
   * Information about the last error that occurred when trying
   * to update the exchange info.
   */
  lastUpdateErrorInfo?: OperationErrorInfo;

  unavailableReason?: TalerErrorDetail;
}

const codecForAuditorDenomSig = (): Codec<AuditorDenomSig> =>
  buildCodecForObject<AuditorDenomSig>()
    .property("denom_pub_h", codecForString())
    .property("auditor_sig", codecForString())
    .build("AuditorDenomSig");

const codecForExchangeAuditor = (): Codec<ExchangeAuditor> =>
  buildCodecForObject<ExchangeAuditor>()
    .property("auditor_pub", codecForString())
    .property("auditor_url", codecForString())
    .property("denomination_keys", codecForList(codecForAuditorDenomSig()))
    .build("codecForExchangeAuditor");

export const codecForFeeDescriptionPair = (): Codec<FeeDescriptionPair> =>
  buildCodecForObject<FeeDescriptionPair>()
    .property("group", codecForString())
    .property("from", codecForAbsoluteTime)
    .property("until", codecForAbsoluteTime)
    .property("left", codecOptional(codecForAmountString()))
    .property("right", codecOptional(codecForAmountString()))
    .build("FeeDescriptionPair");

export const codecForFeeDescription = (): Codec<FeeDescription> =>
  buildCodecForObject<FeeDescription>()
    .property("group", codecForString())
    .property("from", codecForAbsoluteTime)
    .property("until", codecForAbsoluteTime)
    .property("fee", codecOptional(codecForAmountString()))
    .build("FeeDescription");

export const codecForFeesByOperations = (): Codec<
  DenomOperationMap<FeeDescription[]>
> =>
  buildCodecForObject<DenomOperationMap<FeeDescription[]>>()
    .property("deposit", codecForList(codecForFeeDescription()))
    .property("withdraw", codecForList(codecForFeeDescription()))
    .property("refresh", codecForList(codecForFeeDescription()))
    .property("refund", codecForList(codecForFeeDescription()))
    .build("DenomOperationMap");

export const codecForExchangeFullDetails = (): Codec<ExchangeFullDetails> =>
  buildCodecForObject<ExchangeFullDetails>()
    .property("currency", codecForString())
    .property("exchangeBaseUrl", codecForCanonBaseUrl())
    .property("paytoUris", codecForList(codecForString()))
    .property("auditors", codecForList(codecForExchangeAuditor()))
    .property("wireInfo", codecForWireInfo())
    .property("denomFees", codecForFeesByOperations())
    .property(
      "transferFees",
      codecForMap(codecForList(codecForFeeDescription())),
    )
    .property("globalFees", codecForList(codecForFeeDescription()))
    .build("ExchangeFullDetails");

export const codecForExchangeListItem = (): Codec<ExchangeListItem> =>
  buildCodecForObject<ExchangeListItem>()
    .property("currency", codecForString())
    .property("exchangeBaseUrl", codecForCanonBaseUrl())
    .property("masterPub", codecOptional(codecForString()))
    .property("paytoUris", codecForList(codecForString()))
    .property("tosStatus", codecForAny())
    .property("exchangeEntryStatus", codecForAny())
    .property("exchangeUpdateStatus", codecForAny())
    .property("ageRestrictionOptions", codecForList(codecForNumber()))
    .property("scopeInfo", codecForScopeInfo())
    .property("lastUpdateErrorInfo", codecForAny())
    .property("lastUpdateTimestamp", codecOptional(codecForPreciseTimestamp))
    .property("noFees", codecForBoolean())
    .property("peerPaymentsDisabled", codecForBoolean())
    .build("ExchangeListItem");

export const codecForExchangesListResponse = (): Codec<ExchangesListResponse> =>
  buildCodecForObject<ExchangesListResponse>()
    .property("exchanges", codecForList(codecForExchangeListItem()))
    .build("ExchangesListResponse");

export interface AcceptManualWithdrawalResult {
  /**
   * Payto URIs that can be used to fund the withdrawal.
   *
   * @deprecated in favor of withdrawalAccountsList
   */
  exchangePaytoUris: string[];

  /**
   * Public key of the newly created reserve.
   */
  reservePub: string;

  withdrawalAccountsList: WithdrawalExchangeAccountDetails[];

  transactionId: TransactionIdStr;
}

export interface WithdrawalDetailsForAmount {
  /**
   * Did the user accept the current version of the exchange's
   * terms of service?
   *
   * @deprecated the client should query the exchange entry instead
   */
  tosAccepted: boolean;

  /**
   * Amount that the user will transfer to the exchange.
   */
  amountRaw: AmountString;

  /**
   * Amount that will be added to the user's wallet balance.
   */
  amountEffective: AmountString;

  /**
   * Number of coins that would be used for withdrawal.
   *
   * The UIs should warn if this number is too high (roughly at >100).
   */
  numCoins: number;

  /**
   * Ways to pay the exchange.
   *
   * @deprecated in favor of withdrawalAccountsList
   */
  paytoUris: string[];

  /**
   * Ways to pay the exchange, including accounts that require currency conversion.
   */
  withdrawalAccountsList: WithdrawalExchangeAccountDetails[];

  /**
   * If the exchange supports age-restricted coins it will return
   * the array of ages.
   */
  ageRestrictionOptions?: number[];

  /**
   * Scope info of the currency withdrawn.
   */
  scopeInfo: ScopeInfo;
}

export interface DenomSelItem {
  denomPubHash: string;
  count: number;
  /**
   * Number of denoms/planchets to skip, because
   * a re-denomination effectively deleted them.
   */
  skip?: number;
}

/**
 * Selected denominations withn some extra info.
 */
export interface DenomSelectionState {
  totalCoinValue: AmountString;
  totalWithdrawCost: AmountString;
  selectedDenoms: DenomSelItem[];
  earliestDepositExpiration: TalerProtocolTimestamp;
  hasDenomWithAgeRestriction: boolean;
}

/**
 * Information about what will happen doing a withdrawal.
 *
 * Sent to the wallet frontend to be rendered and shown to the user.
 */
export interface ExchangeWithdrawalDetails {
  exchangePaytoUris: string[];

  /**
   * Filtered wire info to send to the bank.
   */
  exchangeWireAccounts: string[];

  exchangeCreditAccountDetails: WithdrawalExchangeAccountDetails[];

  /**
   * Selected denominations for withdraw.
   */
  selectedDenoms: DenomSelectionState;

  /**
   * Did the user already accept the current terms of service for the exchange?
   */
  termsOfServiceAccepted: boolean;

  /**
   * The earliest deposit expiration of the selected coins.
   */
  earliestDepositExpiration: TalerProtocolTimestamp;

  /**
   * Result of checking the wallet's version
   * against the exchange's version.
   *
   * Older exchanges don't return version information.
   */
  versionMatch: VersionMatchResult | undefined;

  /**
   * Libtool-style version string for the exchange or "unknown"
   * for older exchanges.
   */
  exchangeVersion: string;

  /**
   * Libtool-style version string for the wallet.
   */
  walletVersion: string;

  /**
   * Amount that will be subtracted from the reserve's balance.
   */
  withdrawalAmountRaw: AmountString;

  /**
   * Amount that will actually be added to the wallet's balance.
   */
  withdrawalAmountEffective: AmountString;

  /**
   * If the exchange supports age-restricted coins it will return
   * the array of ages.
   *
   */
  ageRestrictionOptions?: number[];

  scopeInfo: ScopeInfo;
}

export interface GetExchangeTosResult {
  /**
   * Markdown version of the current ToS.
   */
  content: string;

  /**
   * Version tag of the current ToS.
   */
  currentEtag: string;

  /**
   * Version tag of the last ToS that the user has accepted,
   * if any.
   */
  acceptedEtag: string | undefined;

  /**
   * Accepted content type
   */
  contentType: string;

  /**
   * Language of the returned content.
   *
   * If missing, language is unknown.
   */
  contentLanguage: string | undefined;

  /**
   * Available languages as advertised by the exchange.
   */
  tosAvailableLanguages: string[];

  tosStatus: ExchangeTosStatus;
}

export interface TestPayArgs {
  merchantBaseUrl: string;
  merchantAuthToken?: string;
  amount: AmountString;
  summary: string;
  forcedCoinSel?: ForcedCoinSel;
}

export const codecForTestPayArgs = (): Codec<TestPayArgs> =>
  buildCodecForObject<TestPayArgs>()
    .property("merchantBaseUrl", codecForCanonBaseUrl())
    .property("merchantAuthToken", codecOptional(codecForString()))
    .property("amount", codecForAmountString())
    .property("summary", codecForString())
    .property("forcedCoinSel", codecForAny())
    .build("TestPayArgs");

export interface IntegrationTestArgs {
  exchangeBaseUrl: string;
  corebankApiBaseUrl: string;
  merchantBaseUrl: string;
  merchantAuthToken?: string;
  amountToWithdraw: AmountString;
  amountToSpend: AmountString;
}

export const codecForIntegrationTestArgs = (): Codec<IntegrationTestArgs> =>
  buildCodecForObject<IntegrationTestArgs>()
    .property("exchangeBaseUrl", codecForCanonBaseUrl())
    .property("merchantBaseUrl", codecForCanonBaseUrl())
    .property("merchantAuthToken", codecOptional(codecForString()))
    .property("amountToSpend", codecForAmountString())
    .property("amountToWithdraw", codecForAmountString())
    .property("corebankApiBaseUrl", codecForCanonBaseUrl())
    .build("IntegrationTestArgs");

export interface IntegrationTestV2Args {
  exchangeBaseUrl: string;
  corebankApiBaseUrl: string;
  merchantBaseUrl: string;
  merchantAuthToken?: string;
}

export const codecForIntegrationTestV2Args = (): Codec<IntegrationTestV2Args> =>
  buildCodecForObject<IntegrationTestV2Args>()
    .property("exchangeBaseUrl", codecForCanonBaseUrl())
    .property("merchantBaseUrl", codecForCanonBaseUrl())
    .property("merchantAuthToken", codecOptional(codecForString()))
    .property("corebankApiBaseUrl", codecForCanonBaseUrl())
    .build("IntegrationTestV2Args");

export interface GetExchangeEntryByUrlRequest {
  exchangeBaseUrl: string;
}

export const codecForGetExchangeEntryByUrlRequest =
  (): Codec<GetExchangeEntryByUrlRequest> =>
    buildCodecForObject<GetExchangeEntryByUrlRequest>()
      .property("exchangeBaseUrl", codecForCanonBaseUrl())
      .build("GetExchangeEntryByUrlRequest");

export type GetExchangeEntryByUrlResponse = ExchangeListItem;

export interface AddExchangeRequest {
  exchangeBaseUrl: string;

  /**
   * @deprecated use a separate API call to start a forced exchange update instead
   */
  forceUpdate?: boolean;
}

export const codecForAddExchangeRequest = (): Codec<AddExchangeRequest> =>
  buildCodecForObject<AddExchangeRequest>()
    .property("exchangeBaseUrl", codecForCanonBaseUrl())
    .property("forceUpdate", codecOptional(codecForBoolean()))
    .build("AddExchangeRequest");

export interface UpdateExchangeEntryRequest {
  exchangeBaseUrl: string;
  force?: boolean;
}

export const codecForUpdateExchangeEntryRequest =
  (): Codec<UpdateExchangeEntryRequest> =>
    buildCodecForObject<UpdateExchangeEntryRequest>()
      .property("exchangeBaseUrl", codecForCanonBaseUrl())
      .property("force", codecOptional(codecForBoolean()))
      .build("UpdateExchangeEntryRequest");

export interface GetExchangeResourcesRequest {
  exchangeBaseUrl: string;
}

export const codecForGetExchangeResourcesRequest =
  (): Codec<GetExchangeResourcesRequest> =>
    buildCodecForObject<GetExchangeResourcesRequest>()
      .property("exchangeBaseUrl", codecForCanonBaseUrl())
      .build("GetExchangeResourcesRequest");

export interface GetExchangeResourcesResponse {
  hasResources: boolean;
}

export interface DeleteExchangeRequest {
  exchangeBaseUrl: string;
  purge?: boolean;
}

export const codecForDeleteExchangeRequest = (): Codec<DeleteExchangeRequest> =>
  buildCodecForObject<DeleteExchangeRequest>()
    .property("exchangeBaseUrl", codecForCanonBaseUrl())
    .property("purge", codecOptional(codecForBoolean()))
    .build("DeleteExchangeRequest");

export interface ForceExchangeUpdateRequest {
  exchangeBaseUrl: string;
}

export const codecForForceExchangeUpdateRequest =
  (): Codec<AddExchangeRequest> =>
    buildCodecForObject<AddExchangeRequest>()
      .property("exchangeBaseUrl", codecForCanonBaseUrl())
      .build("AddExchangeRequest");

export interface GetExchangeTosRequest {
  exchangeBaseUrl: string;
  acceptedFormat?: string[];
  acceptLanguage?: string;
}

export const codecForGetExchangeTosRequest = (): Codec<GetExchangeTosRequest> =>
  buildCodecForObject<GetExchangeTosRequest>()
    .property("exchangeBaseUrl", codecForCanonBaseUrl())
    .property("acceptedFormat", codecOptional(codecForList(codecForString())))
    .property("acceptLanguage", codecOptional(codecForString()))
    .build("GetExchangeTosRequest");

export interface AcceptManualWithdrawalRequest {
  exchangeBaseUrl: string;
  amount: AmountString;
  restrictAge?: number;

  /**
   * Instead of generating a fresh, random reserve key pair,
   * use the provided reserve private key.
   *
   * Use with caution.  Usage of this field may be restricted
   * to developer mode.
   */
  forceReservePriv?: EddsaPrivateKeyString;
}

export const codecForAcceptManualWithdrawalRequest =
  (): Codec<AcceptManualWithdrawalRequest> =>
    buildCodecForObject<AcceptManualWithdrawalRequest>()
      .property("exchangeBaseUrl", codecForCanonBaseUrl())
      .property("amount", codecForAmountString())
      .property("restrictAge", codecOptional(codecForNumber()))
      .property("forceReservePriv", codecOptional(codecForString()))
      .build("AcceptManualWithdrawalRequest");

export interface GetWithdrawalDetailsForAmountRequest {
  exchangeBaseUrl: string;
  amount: AmountString;
  restrictAge?: number;

  /**
   * ID provided by the client to cancel the request.
   *
   * If the same request is made again with the same clientCancellationId,
   * all previous requests are cancelled.
   *
   * The cancelled request will receive an error response with
   * an error code that indicates the cancellation.
   *
   * The cancellation is best-effort, responses might still arrive.
   */
  clientCancellationId?: string;
}

export interface PrepareBankIntegratedWithdrawalRequest {
  talerWithdrawUri: string;
}

export const codecForPrepareBankIntegratedWithdrawalRequest =
  (): Codec<PrepareBankIntegratedWithdrawalRequest> =>
    buildCodecForObject<PrepareBankIntegratedWithdrawalRequest>()
      .property("talerWithdrawUri", codecForString())
      .build("PrepareBankIntegratedWithdrawalRequest");

export interface PrepareBankIntegratedWithdrawalResponse {
  transactionId: TransactionIdStr;
  info: WithdrawUriInfoResponse;
}

export interface ConfirmWithdrawalRequest {
  transactionId: string;
  exchangeBaseUrl: string;
  amount: AmountString;
  forcedDenomSel?: ForcedDenomSel;
  restrictAge?: number;
}

export const codecForConfirmWithdrawalRequestRequest =
  (): Codec<ConfirmWithdrawalRequest> =>
    buildCodecForObject<ConfirmWithdrawalRequest>()
      .property("transactionId", codecForString())
      .property("amount", codecForAmountString())
      .property("exchangeBaseUrl", codecForCanonBaseUrl())
      .property("forcedDenomSel", codecForAny())
      .property("restrictAge", codecOptional(codecForNumber()))
      .build("ConfirmWithdrawalRequest");

export interface AcceptBankIntegratedWithdrawalRequest {
  talerWithdrawUri: string;
  exchangeBaseUrl: string;
  forcedDenomSel?: ForcedDenomSel;
  /**
   * Amount to withdraw.
   * If the bank's withdrawal operation uses a fixed amount,
   * this field must either be left undefined or its value must match
   * the amount from the withdrawal operation.
   */
  amount?: AmountString;
  restrictAge?: number;
}

export const codecForAcceptBankIntegratedWithdrawalRequest =
  (): Codec<AcceptBankIntegratedWithdrawalRequest> =>
    buildCodecForObject<AcceptBankIntegratedWithdrawalRequest>()
      .property("exchangeBaseUrl", codecForCanonBaseUrl())
      .property("talerWithdrawUri", codecForString())
      .property("forcedDenomSel", codecForAny())
      .property("amount", codecOptional(codecForAmountString()))
      .property("restrictAge", codecOptional(codecForNumber()))
      .build("AcceptBankIntegratedWithdrawalRequest");

export const codecForGetWithdrawalDetailsForAmountRequest =
  (): Codec<GetWithdrawalDetailsForAmountRequest> =>
    buildCodecForObject<GetWithdrawalDetailsForAmountRequest>()
      .property("exchangeBaseUrl", codecForCanonBaseUrl())
      .property("amount", codecForAmountString())
      .property("restrictAge", codecOptional(codecForNumber()))
      .property("clientCancellationId", codecOptional(codecForString()))
      .build("GetWithdrawalDetailsForAmountRequest");

export interface AcceptExchangeTosRequest {
  exchangeBaseUrl: string;
}

export const codecForAcceptExchangeTosRequest =
  (): Codec<AcceptExchangeTosRequest> =>
    buildCodecForObject<AcceptExchangeTosRequest>()
      .property("exchangeBaseUrl", codecForCanonBaseUrl())
      .build("AcceptExchangeTosRequest");

export interface ForgetExchangeTosRequest {
  exchangeBaseUrl: string;
}

export const codecForForgetExchangeTosRequest =
  (): Codec<ForgetExchangeTosRequest> =>
    buildCodecForObject<ForgetExchangeTosRequest>()
      .property("exchangeBaseUrl", codecForCanonBaseUrl())
      .build("ForgetExchangeTosRequest");

export interface AcceptRefundRequest {
  transactionId: TransactionIdStr;
}

export const codecForApplyRefundRequest = (): Codec<AcceptRefundRequest> =>
  buildCodecForObject<AcceptRefundRequest>()
    .property("transactionId", codecForTransactionIdStr())
    .build("AcceptRefundRequest");

export interface ApplyRefundFromPurchaseIdRequest {
  purchaseId: string;
}

export const codecForApplyRefundFromPurchaseIdRequest =
  (): Codec<ApplyRefundFromPurchaseIdRequest> =>
    buildCodecForObject<ApplyRefundFromPurchaseIdRequest>()
      .property("purchaseId", codecForString())
      .build("ApplyRefundFromPurchaseIdRequest");

export interface GetWithdrawalDetailsForUriRequest {
  talerWithdrawUri: string;
  /**
   * @deprecated not used
   */
  restrictAge?: number;
}

export const codecForGetWithdrawalDetailsForUri =
  (): Codec<GetWithdrawalDetailsForUriRequest> =>
    buildCodecForObject<GetWithdrawalDetailsForUriRequest>()
      .property("talerWithdrawUri", codecForString())
      .property("restrictAge", codecOptional(codecForNumber()))
      .build("GetWithdrawalDetailsForUriRequest");

export interface ListKnownBankAccountsRequest {
  currency?: string;
}

export const codecForListKnownBankAccounts =
  (): Codec<ListKnownBankAccountsRequest> =>
    buildCodecForObject<ListKnownBankAccountsRequest>()
      .property("currency", codecOptional(codecForString()))
      .build("ListKnownBankAccountsRequest");

export interface AddKnownBankAccountsRequest {
  payto: string;
  alias: string;
  currency: string;
}
export const codecForAddKnownBankAccounts =
  (): Codec<AddKnownBankAccountsRequest> =>
    buildCodecForObject<AddKnownBankAccountsRequest>()
      .property("payto", codecForString())
      .property("alias", codecForString())
      .property("currency", codecForString())
      .build("AddKnownBankAccountsRequest");

export interface ForgetKnownBankAccountsRequest {
  payto: string;
}

export const codecForForgetKnownBankAccounts =
  (): Codec<ForgetKnownBankAccountsRequest> =>
    buildCodecForObject<ForgetKnownBankAccountsRequest>()
      .property("payto", codecForString())
      .build("ForgetKnownBankAccountsRequest");

export interface AbortProposalRequest {
  proposalId: string;
}

export const codecForAbortProposalRequest = (): Codec<AbortProposalRequest> =>
  buildCodecForObject<AbortProposalRequest>()
    .property("proposalId", codecForString())
    .build("AbortProposalRequest");

export interface GetContractTermsDetailsRequest {
  // @deprecated use transaction id
  proposalId?: string;
  transactionId?: string;
}

export const codecForGetContractTermsDetails =
  (): Codec<GetContractTermsDetailsRequest> =>
    buildCodecForObject<GetContractTermsDetailsRequest>()
      .property("proposalId", codecOptional(codecForString()))
      .property("transactionId", codecOptional(codecForString()))
      .build("GetContractTermsDetails");

export interface PreparePayRequest {
  talerPayUri: string;
}

export const codecForPreparePayRequest = (): Codec<PreparePayRequest> =>
  buildCodecForObject<PreparePayRequest>()
    .property("talerPayUri", codecForString())
    .build("PreparePay");

export interface SharePaymentRequest {
  merchantBaseUrl: string;
  orderId: string;
}
export const codecForSharePaymentRequest = (): Codec<SharePaymentRequest> =>
  buildCodecForObject<SharePaymentRequest>()
    .property("merchantBaseUrl", codecForCanonBaseUrl())
    .property("orderId", codecForString())
    .build("SharePaymentRequest");

export interface SharePaymentResult {
  privatePayUri: string;
}
export const codecForSharePaymentResult = (): Codec<SharePaymentResult> =>
  buildCodecForObject<SharePaymentResult>()
    .property("privatePayUri", codecForString())
    .build("SharePaymentResult");

export interface CheckPayTemplateRequest {
  talerPayTemplateUri: string;
}

export type CheckPayTemplateReponse = {
  templateDetails: TalerMerchantApi.WalletTemplateDetails;
  supportedCurrencies: string[];
};

export const codecForCheckPayTemplateRequest =
  (): Codec<CheckPayTemplateRequest> =>
    buildCodecForObject<CheckPayTemplateRequest>()
      .property("talerPayTemplateUri", codecForString())
      .build("CheckPayTemplateRequest");

export interface PreparePayTemplateRequest {
  talerPayTemplateUri: string;
  templateParams?: TemplateParams;
}

export const codecForPreparePayTemplateRequest =
  (): Codec<PreparePayTemplateRequest> =>
    buildCodecForObject<PreparePayTemplateRequest>()
      .property("talerPayTemplateUri", codecForString())
      .property("templateParams", codecForAny())
      .build("PreparePayTemplate");

export interface ConfirmPayRequest {
  /**
   * @deprecated use transactionId instead
   */
  proposalId?: string;
  transactionId?: TransactionIdStr;
  sessionId?: string;
  forcedCoinSel?: ForcedCoinSel;
}

export const codecForConfirmPayRequest = (): Codec<ConfirmPayRequest> =>
  buildCodecForObject<ConfirmPayRequest>()
    .property("proposalId", codecOptional(codecForString()))
    .property("transactionId", codecOptional(codecForTransactionIdStr()))
    .property("sessionId", codecOptional(codecForString()))
    .property("forcedCoinSel", codecForAny())
    .build("ConfirmPay");

export interface CoreApiRequestEnvelope {
  id: string;
  operation: string;
  args: unknown;
}

export type CoreApiResponse = CoreApiResponseSuccess | CoreApiResponseError;

export type CoreApiMessageEnvelope = CoreApiResponse | CoreApiNotification;

export interface CoreApiNotification {
  type: "notification";
  payload: unknown;
}

export interface CoreApiResponseSuccess {
  // To distinguish the message from notifications
  type: "response";
  operation: string;
  id: string;
  result: unknown;
}

export interface CoreApiResponseError {
  // To distinguish the message from notifications
  type: "error";
  operation: string;
  id: string;
  error: TalerErrorDetail;
}

export interface WithdrawTestBalanceRequest {
  amount: AmountString;
  /**
   * Corebank API base URL.
   */
  corebankApiBaseUrl: string;
  exchangeBaseUrl: string;
  forcedDenomSel?: ForcedDenomSel;
}

/**
 * Request to the crypto worker to make a sync signature.
 */
export interface MakeSyncSignatureRequest {
  accountPriv: string;
  oldHash: string | undefined;
  newHash: string;
}

/**
 * Planchet for a coin during refresh.
 */
export interface RefreshPlanchetInfo {
  /**
   * Public key for the coin.
   */
  coinPub: string;

  /**
   * Private key for the coin.
   */
  coinPriv: string;

  /**
   * Blinded public key.
   */
  coinEv: CoinEnvelope;

  coinEvHash: string;

  /**
   * Blinding key used.
   */
  blindingKey: string;

  maxAge: number;
  ageCommitmentProof?: AgeCommitmentProof;
}

/**
 * Strategy for loading recovery information.
 */
export enum RecoveryMergeStrategy {
  /**
   * Keep the local wallet root key, import and take over providers.
   */
  Ours = "ours",

  /**
   * Migrate to the wallet root key from the recovery information.
   */
  Theirs = "theirs",
}

/**
 * Load recovery information into the wallet.
 */
export interface RecoveryLoadRequest {
  recovery: BackupRecovery;
  strategy?: RecoveryMergeStrategy;
}

export const codecForWithdrawTestBalance =
  (): Codec<WithdrawTestBalanceRequest> =>
    buildCodecForObject<WithdrawTestBalanceRequest>()
      .property("amount", codecForAmountString())
      .property("exchangeBaseUrl", codecForCanonBaseUrl())
      .property("forcedDenomSel", codecForAny())
      .property("corebankApiBaseUrl", codecForCanonBaseUrl())
      .build("WithdrawTestBalanceRequest");

export interface SetCoinSuspendedRequest {
  coinPub: string;
  suspended: boolean;
}

export const codecForSetCoinSuspendedRequest =
  (): Codec<SetCoinSuspendedRequest> =>
    buildCodecForObject<SetCoinSuspendedRequest>()
      .property("coinPub", codecForString())
      .property("suspended", codecForBoolean())
      .build("SetCoinSuspendedRequest");

export interface RefreshCoinSpec {
  coinPub: string;
  amount?: AmountString;
}

export const codecForRefreshCoinSpec = (): Codec<RefreshCoinSpec> =>
  buildCodecForObject<RefreshCoinSpec>()
    .property("amount", codecForAmountString())
    .property("coinPub", codecForString())
    .build("ForceRefreshRequest");

export interface ForceRefreshRequest {
  refreshCoinSpecs: RefreshCoinSpec[];
}

export const codecForForceRefreshRequest = (): Codec<ForceRefreshRequest> =>
  buildCodecForObject<ForceRefreshRequest>()
    .property("refreshCoinSpecs", codecForList(codecForRefreshCoinSpec()))
    .build("ForceRefreshRequest");

export interface PrepareRefundRequest {
  talerRefundUri: string;
}

export interface StartRefundQueryForUriResponse {
  /**
   * Transaction id of the *payment* where the refund query was started.
   */
  transactionId: TransactionIdStr;
}

export const codecForPrepareRefundRequest = (): Codec<PrepareRefundRequest> =>
  buildCodecForObject<PrepareRefundRequest>()
    .property("talerRefundUri", codecForString())
    .build("PrepareRefundRequest");

export interface StartRefundQueryRequest {
  transactionId: TransactionIdStr;
}

export const codecForStartRefundQueryRequest =
  (): Codec<StartRefundQueryRequest> =>
    buildCodecForObject<StartRefundQueryRequest>()
      .property("transactionId", codecForTransactionIdStr())
      .build("StartRefundQueryRequest");

export interface FailTransactionRequest {
  transactionId: TransactionIdStr;
}

export const codecForFailTransactionRequest =
  (): Codec<FailTransactionRequest> =>
    buildCodecForObject<FailTransactionRequest>()
      .property("transactionId", codecForTransactionIdStr())
      .build("FailTransactionRequest");

export interface SuspendTransactionRequest {
  transactionId: TransactionIdStr;
}

export const codecForSuspendTransaction =
  (): Codec<SuspendTransactionRequest> =>
    buildCodecForObject<AbortTransactionRequest>()
      .property("transactionId", codecForTransactionIdStr())
      .build("SuspendTransactionRequest");

export interface ResumeTransactionRequest {
  transactionId: TransactionIdStr;
}

export const codecForResumeTransaction = (): Codec<ResumeTransactionRequest> =>
  buildCodecForObject<ResumeTransactionRequest>()
    .property("transactionId", codecForTransactionIdStr())
    .build("ResumeTransactionRequest");

export interface AbortTransactionRequest {
  transactionId: TransactionIdStr;
}

export interface FailTransactionRequest {
  transactionId: TransactionIdStr;
}

export const codecForAbortTransaction = (): Codec<AbortTransactionRequest> =>
  buildCodecForObject<AbortTransactionRequest>()
    .property("transactionId", codecForTransactionIdStr())
    .build("AbortTransactionRequest");

export interface DepositGroupFees {
  coin: AmountString;
  wire: AmountString;
  refresh: AmountString;
}

export interface CreateDepositGroupRequest {
  /**
   * Pre-allocated transaction ID.
   * Allows clients to easily handle notifications
   * that occur while the operation has been created but
   * before the creation request has returned.
   */
  transactionId?: TransactionIdStr;
  depositPaytoUri: string;
  amount: AmountString;
}

export interface PrepareDepositRequest {
  /**
   * Payto URI to identify the (bank) account that the exchange will wire
   * the money to.
   */
  depositPaytoUri: string;

  /**
   * Amount that should be deposited.
   *
   * Raw amount, fees will be added on top.
   */
  amount: AmountString;

  /**
   * ID provided by the client to cancel the request.
   *
   * If the same request is made again with the same clientCancellationId,
   * all previous requests are cancelled.
   *
   * The cancelled request will receive an error response with
   * an error code that indicates the cancellation.
   *
   * The cancellation is best-effort, responses might still arrive.
   */
  clientCancellationId?: string;
}

export const codecForPrepareDepositRequest = (): Codec<PrepareDepositRequest> =>
  buildCodecForObject<PrepareDepositRequest>()
    .property("amount", codecForAmountString())
    .property("depositPaytoUri", codecForString())
    .property("clientCancellationId", codecOptional(codecForString()))
    .build("PrepareDepositRequest");

export interface PrepareDepositResponse {
  totalDepositCost: AmountString;
  effectiveDepositAmount: AmountString;
  fees: DepositGroupFees;
}

export const codecForCreateDepositGroupRequest =
  (): Codec<CreateDepositGroupRequest> =>
    buildCodecForObject<CreateDepositGroupRequest>()
      .property("amount", codecForAmountString())
      .property("depositPaytoUri", codecForString())
      .property("transactionId", codecOptional(codecForTransactionIdStr()))
      .build("CreateDepositGroupRequest");

export interface CreateDepositGroupResponse {
  depositGroupId: string;
  transactionId: TransactionIdStr;
}

export interface TxIdResponse {
  transactionId: TransactionIdStr;
}

export interface WithdrawUriInfoResponse {
  operationId: string;
  status: WithdrawalOperationStatus;
  confirmTransferUrl?: string;
  currency: string;
  amount: AmountString | undefined;
  editableAmount: boolean;
  maxAmount: AmountString | undefined;
  wireFee: AmountString | undefined;
  defaultExchangeBaseUrl?: string;
  editableExchange: boolean;
  possibleExchanges: ExchangeListItem[];
}

export const codecForWithdrawUriInfoResponse =
  (): Codec<WithdrawUriInfoResponse> =>
    buildCodecForObject<WithdrawUriInfoResponse>()
      .property("operationId", codecForString())
      .property("confirmTransferUrl", codecOptional(codecForString()))
      .property(
        "status",
        codecForEither(
          codecForConstString("pending"),
          codecForConstString("selected"),
          codecForConstString("aborted"),
          codecForConstString("confirmed"),
        ),
      )
      .property("amount", codecOptional(codecForAmountString()))
      .property("maxAmount", codecOptional(codecForAmountString()))
      .property("wireFee", codecOptional(codecForAmountString()))
      .property("currency", codecForString())
      .property("editableAmount", codecForBoolean())
      .property("editableExchange", codecForBoolean())
      .property("defaultExchangeBaseUrl", codecOptional(codecForCanonBaseUrl()))
      .property("possibleExchanges", codecForList(codecForExchangeListItem()))
      .build("WithdrawUriInfoResponse");

export interface WalletCurrencyInfo {
  trustedAuditors: {
    currency: string;
    auditorPub: string;
    auditorBaseUrl: string;
  }[];
  trustedExchanges: {
    currency: string;
    exchangeMasterPub: string;
    exchangeBaseUrl: string;
  }[];
}

export interface TestingListTasksForTransactionRequest {
  transactionId: TransactionIdStr;
}

export interface TestingListTasksForTransactionsResponse {
  taskIdList: string[];
}

export const codecForTestingListTasksForTransactionRequest =
  (): Codec<TestingListTasksForTransactionRequest> =>
    buildCodecForObject<TestingListTasksForTransactionRequest>()
      .property("transactionId", codecForTransactionIdStr())
      .build("TestingListTasksForTransactionRequest");

export interface DeleteTransactionRequest {
  transactionId: TransactionIdStr;
}

export interface RetryTransactionRequest {
  transactionId: TransactionIdStr;
}

export const codecForDeleteTransactionRequest =
  (): Codec<DeleteTransactionRequest> =>
    buildCodecForObject<DeleteTransactionRequest>()
      .property("transactionId", codecForTransactionIdStr())
      .build("DeleteTransactionRequest");

export const codecForRetryTransactionRequest =
  (): Codec<RetryTransactionRequest> =>
    buildCodecForObject<RetryTransactionRequest>()
      .property("transactionId", codecForTransactionIdStr())
      .build("RetryTransactionRequest");

export interface SetWalletDeviceIdRequest {
  /**
   * New wallet device ID to set.
   */
  walletDeviceId: string;
}

export const codecForSetWalletDeviceIdRequest =
  (): Codec<SetWalletDeviceIdRequest> =>
    buildCodecForObject<SetWalletDeviceIdRequest>()
      .property("walletDeviceId", codecForString())
      .build("SetWalletDeviceIdRequest");

export interface WithdrawFakebankRequest {
  amount: AmountString;
  exchange: string;
  bank: string;
}

export enum AttentionPriority {
  High = "high",
  Medium = "medium",
  Low = "low",
}

export interface UserAttentionByIdRequest {
  entityId: string;
  type: AttentionType;
}

export const codecForUserAttentionByIdRequest =
  (): Codec<UserAttentionByIdRequest> =>
    buildCodecForObject<UserAttentionByIdRequest>()
      .property("type", codecForAny())
      .property("entityId", codecForString())
      .build("UserAttentionByIdRequest");

export const codecForUserAttentionsRequest = (): Codec<UserAttentionsRequest> =>
  buildCodecForObject<UserAttentionsRequest>()
    .property(
      "priority",
      codecOptional(
        codecForEither(
          codecForConstString(AttentionPriority.Low),
          codecForConstString(AttentionPriority.Medium),
          codecForConstString(AttentionPriority.High),
        ),
      ),
    )
    .build("UserAttentionsRequest");

export interface UserAttentionsRequest {
  priority?: AttentionPriority;
}

export type AttentionInfo =
  | AttentionKycWithdrawal
  | AttentionBackupUnpaid
  | AttentionBackupExpiresSoon
  | AttentionMerchantRefund
  | AttentionExchangeTosChanged
  | AttentionExchangeKeyExpired
  | AttentionExchangeDenominationExpired
  | AttentionAuditorTosChanged
  | AttentionAuditorKeyExpires
  | AttentionAuditorDenominationExpires
  | AttentionPullPaymentPaid
  | AttentionPushPaymentReceived;

export enum AttentionType {
  KycWithdrawal = "kyc-withdrawal",

  BackupUnpaid = "backup-unpaid",
  BackupExpiresSoon = "backup-expires-soon",
  MerchantRefund = "merchant-refund",

  ExchangeTosChanged = "exchange-tos-changed",
  ExchangeKeyExpired = "exchange-key-expired",
  ExchangeKeyExpiresSoon = "exchange-key-expires-soon",
  ExchangeDenominationsExpired = "exchange-denominations-expired",
  ExchangeDenominationsExpiresSoon = "exchange-denominations-expires-soon",

  AuditorTosChanged = "auditor-tos-changed",
  AuditorKeyExpires = "auditor-key-expires",
  AuditorDenominationsExpires = "auditor-denominations-expires",

  PullPaymentPaid = "pull-payment-paid",
  PushPaymentReceived = "push-payment-withdrawn",
}

export const UserAttentionPriority: {
  [type in AttentionType]: AttentionPriority;
} = {
  "kyc-withdrawal": AttentionPriority.Medium,

  "backup-unpaid": AttentionPriority.High,
  "backup-expires-soon": AttentionPriority.Medium,
  "merchant-refund": AttentionPriority.Medium,

  "exchange-tos-changed": AttentionPriority.Medium,

  "exchange-key-expired": AttentionPriority.High,
  "exchange-key-expires-soon": AttentionPriority.Medium,
  "exchange-denominations-expired": AttentionPriority.High,
  "exchange-denominations-expires-soon": AttentionPriority.Medium,

  "auditor-tos-changed": AttentionPriority.Medium,
  "auditor-key-expires": AttentionPriority.Medium,
  "auditor-denominations-expires": AttentionPriority.Medium,

  "pull-payment-paid": AttentionPriority.High,
  "push-payment-withdrawn": AttentionPriority.High,
};

interface AttentionBackupExpiresSoon {
  type: AttentionType.BackupExpiresSoon;
  provider_base_url: string;
}
interface AttentionBackupUnpaid {
  type: AttentionType.BackupUnpaid;
  provider_base_url: string;
  talerUri: string;
}

interface AttentionMerchantRefund {
  type: AttentionType.MerchantRefund;
  transactionId: TransactionIdStr;
}

interface AttentionKycWithdrawal {
  type: AttentionType.KycWithdrawal;
  transactionId: TransactionIdStr;
}

interface AttentionExchangeTosChanged {
  type: AttentionType.ExchangeTosChanged;
  exchange_base_url: string;
}
interface AttentionExchangeKeyExpired {
  type: AttentionType.ExchangeKeyExpired;
  exchange_base_url: string;
}
interface AttentionExchangeDenominationExpired {
  type: AttentionType.ExchangeDenominationsExpired;
  exchange_base_url: string;
}
interface AttentionAuditorTosChanged {
  type: AttentionType.AuditorTosChanged;
  auditor_base_url: string;
}

interface AttentionAuditorKeyExpires {
  type: AttentionType.AuditorKeyExpires;
  auditor_base_url: string;
}
interface AttentionAuditorDenominationExpires {
  type: AttentionType.AuditorDenominationsExpires;
  auditor_base_url: string;
}
interface AttentionPullPaymentPaid {
  type: AttentionType.PullPaymentPaid;
  transactionId: TransactionIdStr;
}

interface AttentionPushPaymentReceived {
  type: AttentionType.PushPaymentReceived;
  transactionId: TransactionIdStr;
}

export type UserAttentionUnreadList = Array<{
  info: AttentionInfo;
  when: TalerPreciseTimestamp;
  read: boolean;
}>;

export interface UserAttentionsResponse {
  pending: UserAttentionUnreadList;
}

export interface UserAttentionsCountResponse {
  total: number;
}

export const codecForWithdrawFakebankRequest =
  (): Codec<WithdrawFakebankRequest> =>
    buildCodecForObject<WithdrawFakebankRequest>()
      .property("amount", codecForAmountString())
      .property("bank", codecForString())
      .property("exchange", codecForString())
      .build("WithdrawFakebankRequest");

export interface ActiveTask {
  taskId: string;
  transaction: TransactionIdStr | undefined;
  firstTry: AbsoluteTime | undefined;
  nextTry: AbsoluteTime | undefined;
  retryCounter: number | undefined;
  lastError: TalerErrorDetail | undefined;
}

export interface GetActiveTasksResponse {
  tasks: ActiveTask[];
}

export const codecForActiveTask = (): Codec<ActiveTask> =>
  buildCodecForObject<ActiveTask>()
    .property("taskId", codecForString())
    .property("transaction", codecOptional(codecForTransactionIdStr()))
    .property("retryCounter", codecOptional(codecForNumber()))
    .property("firstTry", codecOptional(codecForAbsoluteTime))
    .property("nextTry", codecOptional(codecForAbsoluteTime))
    .property("lastError", codecOptional(codecForTalerErrorDetail()))
    .build("ActiveTask");

export const codecForGetActiveTasks = (): Codec<GetActiveTasksResponse> =>
  buildCodecForObject<GetActiveTasksResponse>()
    .property("tasks", codecForList(codecForActiveTask()))
    .build("GetActiveTasks");

export interface ImportDbRequest {
  dump: any;
}

export const codecForImportDbRequest = (): Codec<ImportDbRequest> =>
  buildCodecForObject<ImportDbRequest>()
    .property("dump", codecForAny())
    .build("ImportDbRequest");

export interface ForcedDenomSel {
  denoms: {
    value: AmountString;
    count: number;
  }[];
}

/**
 * Forced coin selection for deposits/payments.
 */
export interface ForcedCoinSel {
  coins: {
    value: AmountString;
    contribution: AmountString;
  }[];
}

export interface TestPayResult {
  /**
   * Number of coins used for the payment.
   */
  numCoins: number;
}

export interface SelectedCoin {
  denomPubHash: string;
  coinPub: string;
  contribution: AmountString;
  exchangeBaseUrl: string;
}

export interface SelectedProspectiveCoin {
  denomPubHash: string;
  contribution: AmountString;
  exchangeBaseUrl: string;
}

/**
 * Result of selecting coins, contains the exchange, and selected
 * coins with their denomination.
 */
export interface PayCoinSelection {
  coins: SelectedCoin[];

  /**
   * How much of the wire fees is the customer paying?
   */
  customerWireFees: AmountString;

  /**
   * How much of the deposit fees is the customer paying?
   */
  customerDepositFees: AmountString;
}

export interface ProspectivePayCoinSelection {
  prospectiveCoins: SelectedProspectiveCoin[];

  /**
   * How much of the wire fees is the customer paying?
   */
  customerWireFees: AmountString;

  /**
   * How much of the deposit fees is the customer paying?
   */
  customerDepositFees: AmountString;
}

export interface CheckPeerPushDebitRequest {
  /**
   * Preferred exchange to use for the p2p payment.
   */
  exchangeBaseUrl?: string;

  /**
   * Instructed amount.
   *
   * FIXME: Allow specifying the instructed amount type.
   */
  amount: AmountString;

  /**
   * ID provided by the client to cancel the request.
   *
   * If the same request is made again with the same clientCancellationId,
   * all previous requests are cancelled.
   *
   * The cancelled request will receive an error response with
   * an error code that indicates the cancellation.
   *
   * The cancellation is best-effort, responses might still arrive.
   */
  clientCancellationId?: string;
}

export const codecForCheckPeerPushDebitRequest =
  (): Codec<CheckPeerPushDebitRequest> =>
    buildCodecForObject<CheckPeerPushDebitRequest>()
      .property("exchangeBaseUrl", codecOptional(codecForCanonBaseUrl()))
      .property("amount", codecForAmountString())
      .property("clientCancellationId", codecOptional(codecForString()))
      .build("CheckPeerPushDebitRequest");

export interface CheckPeerPushDebitResponse {
  amountRaw: AmountString;
  amountEffective: AmountString;
  exchangeBaseUrl: string;
  /**
   * Maximum expiration date, based on how close the coins
   * used for the payment are to expiry.
   *
   * The value is based on when the wallet would typically
   * automatically refresh the coins on its own, leaving enough
   * time to get a refund for the push payment and refresh the
   * coin.
   */
  maxExpirationDate: TalerProtocolTimestamp;
}

export interface InitiatePeerPushDebitRequest {
  exchangeBaseUrl?: string;
  partialContractTerms: PeerContractTerms;
}

export interface InitiatePeerPushDebitResponse {
  exchangeBaseUrl: string;
  pursePub: string;
  mergePriv: string;
  contractPriv: string;
  transactionId: TransactionIdStr;
}

export const codecForInitiatePeerPushDebitRequest =
  (): Codec<InitiatePeerPushDebitRequest> =>
    buildCodecForObject<InitiatePeerPushDebitRequest>()
      .property("partialContractTerms", codecForPeerContractTerms())
      .build("InitiatePeerPushDebitRequest");

export interface PreparePeerPushCreditRequest {
  talerUri: string;
}

export interface PreparePeerPullDebitRequest {
  talerUri: string;
}

export interface PreparePeerPushCreditResponse {
  contractTerms: PeerContractTerms;
  amountRaw: AmountString;
  amountEffective: AmountString;

  transactionId: TransactionIdStr;

  exchangeBaseUrl: string;

  /**
   * @deprecated use transaction ID instead.
   */
  peerPushCreditId: string;

  /**
   * @deprecated
   */
  amount: AmountString;
}

export interface PreparePeerPullDebitResponse {
  contractTerms: PeerContractTerms;
  /**
   * @deprecated Redundant field with bad name, will be removed soon.
   */
  amount: AmountString;

  amountRaw: AmountString;
  amountEffective: AmountString;

  peerPullDebitId: string;

  transactionId: TransactionIdStr;
}

export const codecForPreparePeerPushCreditRequest =
  (): Codec<PreparePeerPushCreditRequest> =>
    buildCodecForObject<PreparePeerPushCreditRequest>()
      .property("talerUri", codecForString())
      .build("CheckPeerPushPaymentRequest");

export const codecForCheckPeerPullPaymentRequest =
  (): Codec<PreparePeerPullDebitRequest> =>
    buildCodecForObject<PreparePeerPullDebitRequest>()
      .property("talerUri", codecForString())
      .build("PreparePeerPullDebitRequest");

export interface ConfirmPeerPushCreditRequest {
  transactionId: string;
}
export interface AcceptPeerPushPaymentResponse {
  transactionId: TransactionIdStr;
}

export interface AcceptPeerPullPaymentResponse {
  transactionId: TransactionIdStr;
}

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

export interface ConfirmPeerPullDebitRequest {
  transactionId: TransactionIdStr;
}

export interface ApplyDevExperimentRequest {
  devExperimentUri: string;
}

export const codecForApplyDevExperiment =
  (): Codec<ApplyDevExperimentRequest> =>
    buildCodecForObject<ApplyDevExperimentRequest>()
      .property("devExperimentUri", codecForString())
      .build("ApplyDevExperimentRequest");

export const codecForAcceptPeerPullPaymentRequest =
  (): Codec<ConfirmPeerPullDebitRequest> =>
    buildCodecForObject<ConfirmPeerPullDebitRequest>()
      .property("transactionId", codecForTransactionIdStr())
      .build("ConfirmPeerPullDebitRequest");

export interface CheckPeerPullCreditRequest {
  exchangeBaseUrl?: string;
  amount: AmountString;

  /**
   * ID provided by the client to cancel the request.
   *
   * If the same request is made again with the same clientCancellationId,
   * all previous requests are cancelled.
   *
   * The cancelled request will receive an error response with
   * an error code that indicates the cancellation.
   *
   * The cancellation is best-effort, responses might still arrive.
   */
  clientCancellationId?: string;
}

export const codecForPreparePeerPullPaymentRequest =
  (): Codec<CheckPeerPullCreditRequest> =>
    buildCodecForObject<CheckPeerPullCreditRequest>()
      .property("amount", codecForAmountString())
      .property("exchangeBaseUrl", codecOptional(codecForCanonBaseUrl()))
      .property("clientCancellationId", codecOptional(codecForString()))
      .build("CheckPeerPullCreditRequest");

export interface CheckPeerPullCreditResponse {
  exchangeBaseUrl: string;
  amountRaw: AmountString;
  amountEffective: AmountString;

  /**
   * Number of coins that will be used,
   * can be used by the UI to warn if excessively large.
   */
  numCoins: number;
}

export interface InitiatePeerPullCreditRequest {
  exchangeBaseUrl?: string;
  partialContractTerms: PeerContractTerms;
}

export const codecForInitiatePeerPullPaymentRequest =
  (): Codec<InitiatePeerPullCreditRequest> =>
    buildCodecForObject<InitiatePeerPullCreditRequest>()
      .property("partialContractTerms", codecForPeerContractTerms())
      .property("exchangeBaseUrl", codecOptional(codecForCanonBaseUrl()))
      .build("InitiatePeerPullCreditRequest");

export interface InitiatePeerPullCreditResponse {
  /**
   * Taler URI for the other party to make the payment
   * that was requested.
   *
   * @deprecated since it's not necessarily valid yet until the tx is in the right state
   */
  talerUri: string;

  transactionId: TransactionIdStr;
}

export interface CanonicalizeBaseUrlRequest {
  url: string;
}

export const codecForCanonicalizeBaseUrlRequest =
  (): Codec<CanonicalizeBaseUrlRequest> =>
    buildCodecForObject<CanonicalizeBaseUrlRequest>()
      .property("url", codecForString())
      .build("CanonicalizeBaseUrlRequest");

export interface CanonicalizeBaseUrlResponse {
  url: string;
}

export interface ValidateIbanRequest {
  iban: string;
}

export const codecForValidateIbanRequest = (): Codec<ValidateIbanRequest> =>
  buildCodecForObject<ValidateIbanRequest>()
    .property("iban", codecForString())
    .build("ValidateIbanRequest");

export interface ValidateIbanResponse {
  valid: boolean;
}

export const codecForValidateIbanResponse = (): Codec<ValidateIbanResponse> =>
  buildCodecForObject<ValidateIbanResponse>()
    .property("valid", codecForBoolean())
    .build("ValidateIbanResponse");

export type TransactionStateFilter = "nonfinal";

export interface TransactionRecordFilter {
  onlyState?: TransactionStateFilter;
  onlyCurrency?: string;
}

export interface StoredBackupList {
  storedBackups: {
    name: string;
  }[];
}

export interface CreateStoredBackupResponse {
  name: string;
}

export interface RecoverStoredBackupRequest {
  name: string;
}

export interface DeleteStoredBackupRequest {
  name: string;
}

export const codecForDeleteStoredBackupRequest =
  (): Codec<DeleteStoredBackupRequest> =>
    buildCodecForObject<DeleteStoredBackupRequest>()
      .property("name", codecForString())
      .build("DeleteStoredBackupRequest");

export const codecForRecoverStoredBackupRequest =
  (): Codec<RecoverStoredBackupRequest> =>
    buildCodecForObject<RecoverStoredBackupRequest>()
      .property("name", codecForString())
      .build("RecoverStoredBackupRequest");

export interface TestingSetTimetravelRequest {
  offsetMs: number;
}

export const codecForTestingSetTimetravelRequest =
  (): Codec<TestingSetTimetravelRequest> =>
    buildCodecForObject<TestingSetTimetravelRequest>()
      .property("offsetMs", codecForNumber())
      .build("TestingSetTimetravelRequest");

export interface AllowedAuditorInfo {
  auditorBaseUrl: string;
  auditorPub: string;
}

export interface AllowedExchangeInfo {
  exchangeBaseUrl: string;
  exchangePub: string;
}

/**
 * Data extracted from the contract terms that is relevant for payment
 * processing in the wallet.
 */
export interface WalletContractData {
  /**
   * Fulfillment URL, or the empty string if the order has no fulfillment URL.
   *
   * Stored as a non-nullable string as we use this field for IndexedDB indexing.
   */
  fulfillmentUrl: string;

  contractTermsHash: string;
  fulfillmentMessage?: string;
  fulfillmentMessageI18n?: InternationalizedString;
  merchantSig: string;
  merchantPub: string;
  merchant: MerchantInfo;
  amount: AmountString;
  orderId: string;
  merchantBaseUrl: string;
  summary: string;
  summaryI18n: { [lang_tag: string]: string } | undefined;
  autoRefund: TalerProtocolDuration | undefined;
  payDeadline: TalerProtocolTimestamp;
  refundDeadline: TalerProtocolTimestamp;
  allowedExchanges: AllowedExchangeInfo[];
  timestamp: TalerProtocolTimestamp;
  wireMethod: string;
  wireInfoHash: string;
  maxDepositFee: AmountString;
  minimumAge?: number;
}

export interface TestingWaitTransactionRequest {
  transactionId: TransactionIdStr;
  txState: TransactionState;
}

export interface TestingGetReserveHistoryRequest {
  reservePub: string;
  exchangeBaseUrl: string;
}

export const codecForTestingGetReserveHistoryRequest =
  (): Codec<TestingGetReserveHistoryRequest> =>
    buildCodecForObject<TestingGetReserveHistoryRequest>()
      .property("reservePub", codecForString())
      .property("exchangeBaseUrl", codecForString())
      .build("TestingGetReserveHistoryRequest");

export interface TestingGetDenomStatsRequest {
  exchangeBaseUrl: string;
}

export interface TestingGetDenomStatsResponse {
  numKnown: number;
  numOffered: number;
  numLost: number;
}

export const codecForTestingGetDenomStatsRequest =
  (): Codec<TestingGetDenomStatsRequest> =>
    buildCodecForObject<TestingGetDenomStatsRequest>()
      .property("exchangeBaseUrl", codecForCanonBaseUrl())
      .build("TestingGetDenomStatsRequest");

export interface WithdrawalExchangeAccountDetails {
  /**
   * Payto URI to credit the exchange.
   *
   * Depending on whether the (manual!) withdrawal is accepted or just
   * being checked, this already includes the subject with the
   * reserve public key.
   */
  paytoUri: string;

  /**
   * Status that indicates whether the account can be used
   * by the user to send funds for a withdrawal.
   *
   * ok: account should be shown to the user
   * error: account should not be shown to the user, UIs might render the error (in conversionError),
   *   especially in dev mode.
   */
  status: "ok" | "error";

  /**
   * Transfer amount. Might be in a different currency than the requested
   * amount for withdrawal.
   *
   * Absent if this is a conversion account and the conversion failed.
   */
  transferAmount?: AmountString;

  /**
   * Currency specification for the external currency.
   *
   * Only included if this account requires a currency conversion.
   */
  currencySpecification?: CurrencySpecification;

  /**
   * Further restrictions for sending money to the
   * exchange.
   */
  creditRestrictions?: AccountRestriction[];

  /**
   * Label given to the account or the account's bank by the exchange.
   */
  bankLabel?: string;

  /*
   * Display priority assigned to this bank account by the exchange.
   */
  priority?: number;

  /**
   * Error that happened when attempting to request the conversion rate.
   */
  conversionError?: TalerErrorDetail;
}

export interface PrepareWithdrawExchangeRequest {
  /**
   * A taler://withdraw-exchange URI.
   */
  talerUri: string;
}

export const codecForPrepareWithdrawExchangeRequest =
  (): Codec<PrepareWithdrawExchangeRequest> =>
    buildCodecForObject<PrepareWithdrawExchangeRequest>()
      .property("talerUri", codecForString())
      .build("PrepareWithdrawExchangeRequest");

export interface PrepareWithdrawExchangeResponse {
  /**
   * Base URL of the exchange that already existed
   * or was ephemerally added as an exchange entry to
   * the wallet.
   */
  exchangeBaseUrl: string;

  /**
   * Amount from the taler://withdraw-exchange URI.
   * Only present if specified in the URI.
   */
  amount?: AmountString;
}

export interface ExchangeEntryState {
  tosStatus: ExchangeTosStatus;
  exchangeEntryStatus: ExchangeEntryStatus;
  exchangeUpdateStatus: ExchangeUpdateStatus;
}

export interface ListGlobalCurrencyAuditorsResponse {
  auditors: {
    currency: string;
    auditorBaseUrl: string;
    auditorPub: string;
  }[];
}

export interface ListGlobalCurrencyExchangesResponse {
  exchanges: {
    currency: string;
    exchangeBaseUrl: string;
    exchangeMasterPub: string;
  }[];
}

export interface AddGlobalCurrencyExchangeRequest {
  currency: string;
  exchangeBaseUrl: string;
  exchangeMasterPub: string;
}

export const codecForAddGlobalCurrencyExchangeRequest =
  (): Codec<AddGlobalCurrencyExchangeRequest> =>
    buildCodecForObject<AddGlobalCurrencyExchangeRequest>()
      .property("currency", codecForString())
      .property("exchangeBaseUrl", codecForCanonBaseUrl())
      .property("exchangeMasterPub", codecForString())
      .build("AddGlobalCurrencyExchangeRequest");

export interface RemoveGlobalCurrencyExchangeRequest {
  currency: string;
  exchangeBaseUrl: string;
  exchangeMasterPub: string;
}

export const codecForRemoveGlobalCurrencyExchangeRequest =
  (): Codec<RemoveGlobalCurrencyExchangeRequest> =>
    buildCodecForObject<RemoveGlobalCurrencyExchangeRequest>()
      .property("currency", codecForString())
      .property("exchangeBaseUrl", codecForCanonBaseUrl())
      .property("exchangeMasterPub", codecForString())
      .build("RemoveGlobalCurrencyExchangeRequest");

export interface AddGlobalCurrencyAuditorRequest {
  currency: string;
  auditorBaseUrl: string;
  auditorPub: string;
}

export const codecForAddGlobalCurrencyAuditorRequest =
  (): Codec<AddGlobalCurrencyAuditorRequest> =>
    buildCodecForObject<AddGlobalCurrencyAuditorRequest>()
      .property("currency", codecForString())
      .property("auditorBaseUrl", codecForCanonBaseUrl())
      .property("auditorPub", codecForString())
      .build("AddGlobalCurrencyAuditorRequest");

export interface RemoveGlobalCurrencyAuditorRequest {
  currency: string;
  auditorBaseUrl: string;
  auditorPub: string;
}

export const codecForRemoveGlobalCurrencyAuditorRequest =
  (): Codec<RemoveGlobalCurrencyAuditorRequest> =>
    buildCodecForObject<RemoveGlobalCurrencyAuditorRequest>()
      .property("currency", codecForString())
      .property("auditorBaseUrl", codecForCanonBaseUrl())
      .property("auditorPub", codecForString())
      .build("RemoveGlobalCurrencyAuditorRequest");

/**
 * Information about one provider.
 *
 * We don't store the account key here,
 * as that's derived from the wallet root key.
 */
export interface ProviderInfo {
  active: boolean;
  syncProviderBaseUrl: string;
  name: string;
  terms?: BackupProviderTerms;
  /**
   * Last communication issue with the provider.
   */
  lastError?: TalerErrorDetail;
  lastSuccessfulBackupTimestamp?: TalerPreciseTimestamp;
  lastAttemptedBackupTimestamp?: TalerPreciseTimestamp;
  paymentProposalIds: string[];
  backupProblem?: BackupProblem;
  paymentStatus: ProviderPaymentStatus;
}

export interface BackupProviderTerms {
  supportedProtocolVersion: string;
  annualFee: AmountString;
  storageLimitInMegabytes: number;
}

export type BackupProblem =
  | BackupUnreadableProblem
  | BackupConflictingDeviceProblem;

export interface BackupUnreadableProblem {
  type: "backup-unreadable";
}

export interface BackupConflictingDeviceProblem {
  type: "backup-conflicting-device";
  otherDeviceId: string;
  myDeviceId: string;
  backupTimestamp: AbsoluteTime;
}

export type ProviderPaymentStatus =
  | ProviderPaymentTermsChanged
  | ProviderPaymentPaid
  | ProviderPaymentInsufficientBalance
  | ProviderPaymentUnpaid
  | ProviderPaymentPending;

export enum ProviderPaymentType {
  Unpaid = "unpaid",
  Pending = "pending",
  InsufficientBalance = "insufficient-balance",
  Paid = "paid",
  TermsChanged = "terms-changed",
}

export interface ProviderPaymentUnpaid {
  type: ProviderPaymentType.Unpaid;
}

export interface ProviderPaymentInsufficientBalance {
  type: ProviderPaymentType.InsufficientBalance;
  amount: AmountString;
}

export interface ProviderPaymentPending {
  type: ProviderPaymentType.Pending;
  talerUri?: string;
}

export interface ProviderPaymentPaid {
  type: ProviderPaymentType.Paid;
  paidUntil: AbsoluteTime;
}

export interface ProviderPaymentTermsChanged {
  type: ProviderPaymentType.TermsChanged;
  paidUntil: AbsoluteTime;
  oldTerms: BackupProviderTerms;
  newTerms: BackupProviderTerms;
}

// FIXME: Does not really belong here, move to sync API
export interface SyncTermsOfServiceResponse {
  // maximum backup size supported
  storage_limit_in_megabytes: number;

  // Fee for an account, per year.
  annual_fee: AmountString;

  // protocol version supported by the server,
  // for now always "0.0".
  version: string;
}

// FIXME: Does not really belong here, move to sync API
export const codecForSyncTermsOfServiceResponse =
  (): Codec<SyncTermsOfServiceResponse> =>
    buildCodecForObject<SyncTermsOfServiceResponse>()
      .property("storage_limit_in_megabytes", codecForNumber())
      .property("annual_fee", codecForAmountString())
      .property("version", codecForString())
      .build("SyncTermsOfServiceResponse");

export interface HintNetworkAvailabilityRequest {
  isNetworkAvailable: boolean;
}

export const codecForHintNetworkAvailabilityRequest =
  (): Codec<HintNetworkAvailabilityRequest> =>
    buildCodecForObject<HintNetworkAvailabilityRequest>()
      .property("isNetworkAvailable", codecForBoolean())
      .build("HintNetworkAvailabilityRequest");

export interface GetDepositWireTypesForCurrencyRequest {
  currency: string;
  /**
   * Optional scope info to further restrict the result.
   * Currency must match the currency field.
   */
  scopeInfo?: ScopeInfo;
}

export const codecForGetDepositWireTypesForCurrencyRequest =
  (): Codec<GetDepositWireTypesForCurrencyRequest> =>
    buildCodecForObject<GetDepositWireTypesForCurrencyRequest>()
      .property("currency", codecForString())
      .property("scopeInfo", codecOptional(codecForScopeInfo()))
      .build("GetDepositWireTypesForCurrencyRequest");

/**
 * Response with wire types that are supported for a deposit.
 *
 * In the future, we might surface more information here, such as debit restrictions
 * by the exchange, which then can be shown by UIs to the user before they
 * enter their payment information.
 */
export interface GetDepositWireTypesForCurrencyResponse {
  wireTypes: string[];
}

export interface GetQrCodesForPaytoRequest {
  paytoUri: string;
}

export const codecForGetQrCodesForPaytoRequest = () =>
  buildCodecForObject<GetQrCodesForPaytoRequest>()
    .property("paytoUri", codecForString())
    .build("GetQrCodesForPaytoRequest");

export interface GetQrCodesForPaytoResponse {
  codes: QrCodeSpec[];
}

export type EmptyObject = Record<string, never>;

export const codecForEmptyObject = (): Codec<EmptyObject> =>
  buildCodecForObject<EmptyObject>().build("EmptyObject");
