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

 GNU Taler is free software; you can redistribute it and/or modify it under the
 terms of the GNU Affero General Public License as published by the Free Software
 Foundation; either version 3, or (at your option) any later version.

 GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 A PARTICULAR PURPOSE.  See the GNU General Public License for more details.

 You should have received a copy of the GNU Affero General Public License along with
 GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>

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

/**
 * Type and schema definitions and helpers for the core GNU Taler protocol.
 *
 * Even though the rest of the wallet uses camelCase for fields, use snake_case
 * here, since that's the convention for the Taler JSON+HTTP API.
 */

/**
 * Imports.
 */

import { codecForAmountString } from "./amounts.js";
import {
  Codec,
  buildCodecForObject,
  codecForAny,
  codecForBoolean,
  codecForConstString,
  codecForList,
  codecForMap,
  codecForNumber,
  codecForString,
  codecOptional,
} from "./codec.js";
import { EddsaPrivP } from "./taler-crypto.js";
import {
  TalerProtocolDuration,
  TalerProtocolTimestamp,
  codecForTimestamp,
} from "./time.js";

// 64-byte hash code.
export type HashCode = string;

export type AmlOfficerPublicKeyP = string;

// 32-byte hash code.
export type ShortHashCode = string;

export type SHA256HashCode = ShortHashCode;

export type SHA512HashCode = HashCode;

// 32-byte nonce value, must only be used once.
export type CSNonce = string;

// 32-byte nonce value, must only be used once.
export type RefreshMasterSeed = string;

// 32-byte value representing a scalar multiplier
// for scalar operations on points on Curve25519.
export type Cs25519Scalar = string;

///
/// KEYS
///

// 16-byte access token used to authorize access.
export type ClaimToken = string;

// EdDSA and ECDHE public keys always point on Curve25519
// and represented  using the standard 256 bits Ed25519 compact format,
// converted to Crockford Base32.
export type EddsaPublicKey = EddsaPublicKeyString;

// EdDSA and ECDHE public keys always point on Curve25519
// and represented  using the standard 256 bits Ed25519 compact format,
// converted to Crockford Base32.
export type EddsaPrivateKey = EddsaPrivateKeyString;

// Edx25519 public keys are points on Curve25519 and represented using the
// standard 256 bits Ed25519 compact format converted to Crockford
// Base32.
//export type Edx25519PublicKey = string;

// Edx25519 private keys are always points on Curve25519
// and represented using the standard 256 bits Ed25519 compact format,
// converted to Crockford Base32.
//export type Edx25519PrivateKey = string;

// EdDSA and ECDHE public keys always point on Curve25519
// and represented  using the standard 256 bits Ed25519 compact format,
// converted to Crockford Base32.
export type EcdhePublicKey = string;

// Point on Curve25519 represented using the standard 256 bits Ed25519 compact format,
// converted to Crockford Base32.
export type CsRPublic = string;

// EdDSA and ECDHE public keys always point on Curve25519
// and represented  using the standard 256 bits Ed25519 compact format,
// converted to Crockford Base32.
export type EcdhePrivateKey = string;

export type CoinPublicKey = EddsaPublicKey;

// RSA public key converted to Crockford Base32.
export type RsaPublicKey = string;

export type WireTransferIdentifierRawP = string;
// Subset of numbers:  Integers in the
// inclusive range 0 .. (2^53 - 1).
export type SafeUint64 = number;

export type WadId = string;

export type Timestamp = TalerProtocolTimestamp;

export type RelativeTime = TalerProtocolDuration;

export type RsaSignature = string;

export type BlindedRsaSignature = string;

/**
 * DD51 https://docs.taler.net/design-documents/051-fractional-digits.html
 */
export interface CurrencySpecification {
  // Name of the currency.
  name: string;

  // how many digits the user may enter after the decimal_separator
  num_fractional_input_digits: Integer;

  // Number of fractional digits to render in normal font and size.
  num_fractional_normal_digits: Integer;

  // Number of fractional digits to render always, if needed by
  // padding with zeros.
  num_fractional_trailing_zero_digits: Integer;

  // map of powers of 10 to alternative currency names / symbols, must
  // always have an entry under "0" that defines the base name,
  // e.g.  "0 => €" or "3 => k€". For BTC, would be "0 => BTC, -3 => mBTC".
  // Communicates the currency symbol to be used.
  alt_unit_names: { [log10: string]: string };

  common_amounts?: AmountString[];
}

export interface InternationalizedString {
  [lang_tag: string]: string;
}

export type RsaPublicKeyString = string;
export type AgeMask = number;

// The string must be a data URL according to RFC 2397
// with explicit mediatype and base64 parameters.
//
//     data:<mediatype>;base64,<data>
//
// Supported mediatypes are image/jpeg and image/png.
// Invalid strings will be rejected by the wallet.
export type ImageDataUrl = string;

/**
 * 32-byte value representing a point on Curve25519.
 */
export type Cs25519Point = string;

export type LitAmountString = `${string}:${number}`;

export type LibtoolVersionString = string;

export type DecimalNumber = string;

declare const __amount_str: unique symbol;
export type AmountString =
  | (string & { [__amount_str]: true })
  | LitAmountString;
// export type AmountString = string;
export type Base32String = string;
declare const __eddsasign_str: unique symbol;
export type EddsaSignatureString = string; // & { [__eddsasign_str]: true };
declare const __eddsapub_str: unique symbol;
export type EddsaPublicKeyString = string; // & { [__eddsapub_str]: true };
declare const __eddsapriv_str: unique symbol;
export type EddsaPrivateKeyString = string; // & { [__eddsapriv_str]: true };
export type CoinPublicKeyString = string;

// FIXME: implement this codec
export const codecForURLString = codecForString;
// FIXME: implement this codec
export const codecForLibtoolVersion = codecForString;
// FIXME: implement this codec
export const codecForCurrencyName = codecForString;
// FIXME: implement this codec
export const codecForDecimalNumber = codecForString;
// FIXME: implement this codec
export const codecForEddsaPublicKey =
  codecForString as () => Codec<EddsaPublicKeyString>;
// FIXME: implement this codec
export const codecForEddsaPrivateKey =
  codecForString as () => Codec<EddsaPrivateKeyString>;
// FIXME: implement this codec
export const codecForEddsaSignature =
  codecForString as () => Codec<EddsaSignatureString>;

export const codecForInternationalizedString =
  (): Codec<InternationalizedString> => codecForMap(codecForString());

export const codecForCurrencySpecificiation =
  (): Codec<CurrencySpecification> =>
buildCodecForObject<CurrencySpecification>()
      .property("name", codecForString())
      .property("num_fractional_input_digits", codecForNumber())
      .property("num_fractional_normal_digits", codecForNumber())
      .property("num_fractional_trailing_zero_digits", codecForNumber())
      .property("alt_unit_names", codecForMap(codecForString()))
      .property(
        "common_amounts",
        codecOptional(codecForList(codecForAmountString())),
      )
      .deprecatedProperty("currency")
      .build("CurrencySpecification");

export interface TalerCommonConfigResponse {
  name: string;
  version: string;
}

export const codecForTalerCommonConfigResponse =
  (): Codec<TalerCommonConfigResponse> =>
    buildCodecForObject<TalerCommonConfigResponse>()
      .allowExtra()
      .property("name", codecForString())
      .property("version", codecForString())
      .build("TalerCommonConfigResponse");

export enum ExchangeProtocolVersion {
  /**
   * Current version supported by the wallet.
   */
  V12 = 12,
}

export enum MerchantProtocolVersion {
  /**
   * Current version supported by the wallet.
   */
  V3 = 3,
}

export type HashCodeString = string;

export type WireSalt = string;

export interface MerchantUsingTemplateDetails {
  summary?: string;
  amount?: AmountString;
}

export type Integer = number;

export interface BankConversionInfoConfig {
  // libtool-style representation of the Bank protocol version, see
  // https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning
  // The format is "current:revision:age".
  version: string;

  // Name of the API.
  name: "taler-conversion-info";

  regional_currency: string;

  fiat_currency: string;

  // Currency used by this bank.
  regional_currency_specification: CurrencySpecification;

  // External currency used during conversion.
  fiat_currency_specification: CurrencySpecification;
}

export const codecForBankConversionInfoConfig =
  (): Codec<BankConversionInfoConfig> =>
    buildCodecForObject<BankConversionInfoConfig>()
      .property("name", codecForConstString("taler-conversion-info"))
      .property("version", codecForString())
      .property("fiat_currency", codecForString())
      .property("regional_currency", codecForString())
      .property("fiat_currency_specification", codecForCurrencySpecificiation())
      .property(
        "regional_currency_specification",
        codecForCurrencySpecificiation(),
      )
      .build("BankConversionInfoConfig");

export interface DenominationExpiredMessage {
  // Taler error code.  Note that beyond
  // expiration this message format is also
  // used if the key is not yet valid, or
  // has been revoked.
  code: number;

  // Signature by the exchange over a
  // TALER_DenominationExpiredAffirmationPS.
  // Must have purpose TALER_SIGNATURE_EXCHANGE_AFFIRM_DENOM_EXPIRED.
  exchange_sig: EddsaSignatureString;

  // Public key of the exchange used to create
  // the 'exchange_sig.
  exchange_pub: EddsaPublicKeyString;

  // Hash of the denomination public key that is unknown.
  h_denom_pub: HashCodeString;

  // When was the signature created.
  timestamp: TalerProtocolTimestamp;

  // What kind of operation was requested that now
  // failed?
  oper: string;
}

export const codecForDenominationExpiredMessage = () =>
  buildCodecForObject<DenominationExpiredMessage>()
    .property("code", codecForNumber())
    .property("exchange_sig", codecForEddsaSignature())
    .property("exchange_pub", codecForEddsaPublicKey())
    .property("h_denom_pub", codecForString())
    .property("timestamp", codecForTimestamp)
    .property("oper", codecForString())
    .build("DenominationExpiredMessage");

export interface CoinHistoryResponse {
  // Current balance of the coin.
  balance: AmountString;

  // Hash of the coin's denomination.
  h_denom_pub: HashCodeString;

  // Transaction history for the coin.
  history: any[];
}

export const codecForCoinHistoryResponse = () =>
  buildCodecForObject<CoinHistoryResponse>()
    .property("balance", codecForAmountString())
    .property("h_denom_pub", codecForString())
    .property("history", codecForAny())
    .build("CoinHistoryResponse");

export type BankTokenScope =
  | "readonly"
  | "readwrite"
  | "revenue"
  | "wiregateway";
export interface TokenRequest {
  // Service-defined scope for the token.
  // Typical scopes would be "readonly" or "readwrite".
  scope: BankTokenScope;

  // Server may impose its own upper bound
  // on the token validity duration
  duration?: RelativeTime;

  // Is the token refreshable into a new token during its
  // validity?
  // Refreshable tokens effectively provide indefinite
  // access if they are refreshed in time.
  refreshable?: boolean;
}

export interface TokenSuccessResponse {
  // Expiration determined by the server.
  // Can be based on the token_duration
  // from the request, but ultimately the
  // server decides the expiration.
  expiration: Timestamp;

  // Opque access token.
  access_token: AccessToken;
}

export interface TokenInfos {
  tokens: TokenInfo[];
}

export interface TokenInfo {
  // Time when the token was created.
  creation_time: Timestamp;

  // Time when the token expires.
  expiration: Timestamp;

  // Scope for the token.
  scope: string;

  // Is the token refreshable into a new token during its
  // validity?
  // Refreshable tokens effectively provide indefinite
  // access if they are refreshed in time.
  refreshable: boolean;

  // Optional token description
  description?: string;

  // Opaque unique ID used for pagination.
  serial: Integer;
}

export const codecForTokenInfo = (): Codec<TokenInfo> =>
  buildCodecForObject<TokenInfo>()
    .property("creation_time", codecForTimestamp)
    .property("expiration", codecForTimestamp)
    .property("scope", codecForString())
    .property("refreshable", codecForBoolean())
    .property("description", codecOptional(codecForString()))
    .property("serial", codecForNumber())
    .build("TokenInfo");

export const codecForTokenInfoList = (): Codec<TokenInfos> =>
  buildCodecForObject<TokenInfos>()
    .property("tokens", codecForList(codecForTokenInfo()))
    .build("TokenInfoList");

//FIXME: implement this codec
export const codecForAccessToken = codecForString as () => Codec<AccessToken>;
export const codecForTokenSuccessResponse = (): Codec<TokenSuccessResponse> =>
  buildCodecForObject<TokenSuccessResponse>()
    .property("access_token", codecForAccessToken())
    .property("expiration", codecForTimestamp)
    .build("TalerAuthentication.TokenSuccessResponse");

// FIXME: implement this codec
export const codecForURN = codecForString;

declare const __ac_token: unique symbol;

/**
 * Use `createAccessToken(string)` function to build one.
 */
export type AccessToken = string & {
  [__ac_token]: true;
};

/**
 * Create a rfc8959 access token.
 * Adds secret-token: prefix if there is none.
 * Encode the token with rfc7230 to send in a http header.
 *
 * @param token
 * @returns
 */
export function createRFC8959AccessTokenEncoded(token: string): AccessToken {
  return (
    token.startsWith("secret-token:")
      ? token
      : `secret-token:${encodeURIComponent(token)}`
  ) as AccessToken;
}

/**
 * Create a rfc8959 access token.
 * Adds secret-token: prefix if there is none.
 *
 * @param token
 * @returns
 */
export function createRFC8959AccessTokenPlain(token: string): AccessToken {
  return (
    token.startsWith("secret-token:") ? token : `secret-token:${token}`
  ) as AccessToken;
}

/**
 * Convert string to access token.
 *
 * @param clientSecret
 * @returns
 */
export function createClientSecretAccessToken(
  clientSecret: string,
): AccessToken {
  return clientSecret as AccessToken;
}

export type UserAndPassword = {
  username: string;
  password: string;
};

export type UserAndToken = {
  username: string;
  token: AccessToken;
};

declare const opaque_OfficerAccount: unique symbol;
/**
 * Sealed private key for AML officer
 */
export type LockedAccount = string & { [opaque_OfficerAccount]: true };

declare const opaque_OfficerId: unique symbol;
/**
 * Public key for AML officer
 */
export type OfficerId = string & { [opaque_OfficerId]: true };

declare const opaque_OfficerSigningKey: unique symbol;

export interface OfficerAccount {
  id: OfficerId;
  signingKey: EddsaPrivP;
}

export interface ReserveAccount {
  id: EddsaPublicKeyString;
  signingKey: EddsaPrivP;
}

export type PaginationParams = {
  /**
   * row identifier as the starting point of the query
   */
  offset?: string;
  /**
   * max number of element in the result response
   * always greater than 0
   */
  limit?: number;
  /**
   * order
   */
  order?: "asc" | "dec";
};

export type LongPollParams = {
  /**
   * milliseconds the server should wait for at least one result to be shown
   */
  timeoutMs?: number;
};

export interface LoginToken {
  token: AccessToken;
  expiration: Timestamp;
}
