/*
 * This file is part of LibEuFin.
 * Copyright (C) 2024, 2025, 2026 Taler Systems S.A.

 * LibEuFin 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.

 * LibEuFin 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 Affero General
 * Public License for more details.

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

package tech.libeufin.bank

import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import io.github.smiley4.schemakenerator.core.annotations.Description
import io.github.smiley4.schemakenerator.core.annotations.Name
import tech.libeufin.common.*
import java.time.Instant

/**
 * Allowed lengths for fractional digits in amounts.
 */
enum class FracDigits {
    TWO, EIGHT
}

// Allowed values for bank transactions directions.
@Description("Direction of a bank transaction")
enum class TransactionDirection {
    credit,
    debit
}

@Description("Status of a cashout operation")
enum class CashoutStatus {
    pending,
    aborted,
    confirmed
}

@Description("Status of a withdrawal operation")
enum class WithdrawalStatus {
    pending,
    aborted,
    selected,
    confirmed
}

@Description("Status of a bank account")
enum class AccountStatus {
    active,
    locked,
    deleted
}

@Description("Rounding mode for currency conversion")
enum class RoundingMode {
    zero,
    up,
    nearest
}

enum class Timeframe {
    hour,
    day,
    month,
    year
}

enum class Operation {
    account_reconfig,
    account_delete,
    account_auth_reconfig,
    bank_transaction,
    cashout,
    withdrawal,
    create_token
}

@Description("Wire transfer method type")
enum class WireMethod {
    IBAN,
    X_TALER_BANK
}

@Serializable(with = Option.Serializer::class)
sealed class Option<out T> {
    data object None : Option<Nothing>()
    data class Some<T>(val value: T) : Option<T>()

    fun get(): T? {
        return when (this) {
            None -> null
            is Some -> this.value
        }
    }

    inline fun some(lambda: (T) -> Unit) {
        if (this is Some) {
            lambda(value)
        }
    }

    fun isSome(): Boolean = this is Some

    @OptIn(ExperimentalSerializationApi::class)
    internal class Serializer<T> (
        private val valueSerializer: KSerializer<T>
    ) : KSerializer<Option<T>> {
        override val descriptor: SerialDescriptor = valueSerializer.descriptor

        override fun serialize(encoder: Encoder, value: Option<T>) {
            when (value) {
                None -> encoder.encodeNull()
                is Some -> valueSerializer.serialize(encoder, value.value)
            }
        }

        override fun deserialize(decoder: Decoder): Option<T> {
            return Some(valueSerializer.deserialize(decoder))
        }
    }

    companion object {
        fun <T> invert(optional: Option<T>?): Option<T?> {
            return when (optional) {
                null -> Option.None
                is Option.None -> Option.Some(null)
                is Option.Some -> Option.Some(optional.value)
            }
        }
    }
}

@Serializable
@Description("Response containing TAN challenges")
data class ChallengeResponse(
    @Description("List of pending challenges")
    val challenges: List<Challenge>,
    @Description("Whether all challenges must be solved")
    val combi_and: Boolean
)

@Serializable
@Description("Single TAN challenge details")
data class Challenge(
    @Description("Unique challenge identifier")
    val challenge_id: String,
    @Description("Channel used for TAN delivery")
    val tan_channel: TanChannel,
    @Description("Masked contact information for TAN")
    val tan_info: String
)

@Serializable
@Description("Response after requesting a TAN challenge")
data class ChallengeRequestResponse(
    @Description("Expiration time for solving the challenge")
    val solve_expiration: TalerTimestamp,
    @Description("Earliest time to request retransmission")
    val earliest_retransmission: TalerTimestamp
)

/**
 * HTTP response type of successful token refresh.
 * access_token is the Crockford encoding of the 32 byte
 * access token, whereas 'expiration' is the point in time
 * when this token expires.
 */
@Serializable
@Description("Successful token creation or refresh response")
data class TokenSuccessResponse(
    @Description("Crockford-encoded access token")
    val access_token: String,
    @Description("Token expiration timestamp")
    val expiration: TalerTimestamp
)


/* Contains contact data to send TAN challges to the
* users, to let them complete cashout operations. */
@Serializable
@Description("Contact data for TAN challenge delivery")
data class ChallengeContactData(
    @Description("Email address for TAN delivery")
    val email: Option<String?> = Option.None,
    @Description("Phone number for TAN delivery")
    val phone: Option<String?> = Option.None
) {
    init {
        if (email.get()?.let { !EMAIL_PATTERN.matches(it) } == true)
            throw badRequest("email contact data '$email' is malformed")

        if (phone.get()?.let { !PHONE_PATTERN.matches(it) } == true)
            throw badRequest("phone contact data '$phone' is malformed")
    }
    companion object {
        private val EMAIL_PATTERN = Regex("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,4}")
        private val PHONE_PATTERN = Regex("\\+?[0-9]+")
    }
}

// Type expected at POST /accounts
@Serializable
@Description("Request to register a new bank account")
data class RegisterAccountRequest(
    @Description("Unique account username")
    val username: String,
    @Description("Account password")
    val password: String,
    @Description("Legal name of account holder")
    val name: String,
    @Description("Whether account is publicly visible")
    val is_public: Boolean = false,
    @Description("Whether account is a Taler exchange")
    val is_taler_exchange: Boolean = false,
    @Description("Contact data for TAN challenges")
    val contact_data: ChallengeContactData? = null,
    @Description("Cashout payto URI for fiat withdrawals")
    val cashout_payto_uri: IbanPayto? = null,
    @Description("Payto URI for the account")
    val payto_uri: Payto? = null,
    @Description("Maximum allowed debit balance")
    val debit_threshold: TalerAmount? = null,
    @Description("Preferred TAN channel (deprecated)")
    val tan_channel: TanChannel? = null,
    @Description("Set of enabled TAN channels")
    val tan_channels: Set<TanChannel>? = null,
    @Description("Conversion rate class identifier")
    val conversion_rate_class_id: Long? = null
) {
    init {
        if (!USERNAME_REGEX.matches(username))
            throw badRequest("username '$username' is malformed, must match [a-zA-Z0-9\\-\\._~]{1,126}")
        else if (tan_channel != null && tan_channels != null)
            throw badRequest("you must only use either tan_channel or tan_channels")
    }

    @Description("Computed set of enabled TAN channels")
    val channels: Set<TanChannel> get() {
        if (tan_channels != null) {
            return tan_channels
        } else if (tan_channel != null) {
            return setOf(tan_channel)
        } else {
            return emptySet()
        }
    }

    companion object {
        private val USERNAME_REGEX = Regex("^[a-zA-Z0-9-._~]{1,126}$")
    }
}

@Serializable
@Description("Response after successful account registration")
data class RegisterAccountResponse(
    @Description("Internal payto URI of the new account")
    val internal_payto_uri: String
)

/**
 * Request of PATCH /accounts/{USERNAME}
 */
@Serializable
@Description("Request to reconfigure an existing account")
data class AccountReconfiguration(
    @Description("Updated contact data for TAN challenges")
    val contact_data: ChallengeContactData? = null,
    @Description("Updated cashout payto URI")
    val cashout_payto_uri: Option<IbanPayto?> = Option.None,
    @Description("Updated legal name of account holder")
    val name: String? = null,
    @Description("Whether account is publicly visible")
    val is_public: Boolean? = null,
    @Description("Updated maximum allowed debit balance")
    val debit_threshold: TalerAmount? = null,
    @Description("Updated preferred TAN channel (deprecated)")
    val tan_channel: Option<TanChannel?> = Option.None,
    @Description("Updated set of enabled TAN channels")
    val tan_channels: Option<Set<TanChannel>> = Option.None,
    @Description("Whether account is a Taler exchange")
    val is_taler_exchange: Boolean? = null,
    @Description("Updated conversion rate class identifier")
    val conversion_rate_class_id: Option<Long?> = Option.None
) {
    init {
        if (tan_channel.isSome() && tan_channels.isSome())
            throw badRequest("you must only use either tan_channel or tan_channels")
    }

    @Description("Computed set of enabled TAN channels")
    val channels: Option<Set<TanChannel>> get() {
        if (tan_channels.isSome()) {
            return tan_channels
        } else if (tan_channel is Option.Some) {
            if (tan_channel.value == null) {
                return Option.Some(emptySet())
            } else {
                return Option.Some(setOf(tan_channel.value))
            }
        } else {
            return Option.None
        }
    }
}

/**
 * Type expected at POST /accounts/{USERNAME}/token
 * It complies with Taler's design document #49
 */
@Serializable
@Description("Request to create an authentication token")
data class TokenRequest(
    @Description("Permission scope for the token")
    val scope: TokenScope,
    @Description("Token validity duration")
    val duration: RelativeTime? = null,
    @Description("Human-readable token description")
    val description: String? = null,
    @Description("Whether the token can be refreshed")
    val refreshable: Boolean = false
)

@Serializable
@Description("Monitor statistics response")
sealed interface MonitorResponse {
    val talerInCount: Long
    val talerInVolume: TalerAmount
    val talerOutCount: Long
    val talerOutVolume: TalerAmount
}

@Serializable
@SerialName("no-conversions")
@Name("MonitorNoConversion", qualifiedName = "no-conversions")
@Description("Monitor stats without currency conversion")
data class MonitorNoConversion(
    @Description("Number of incoming Taler transactions")
    override val talerInCount: Long,
    @Description("Total volume of incoming Taler transactions")
    override val talerInVolume: TalerAmount,
    @Description("Number of outgoing Taler transactions")
    override val talerOutCount: Long,
    @Description("Total volume of outgoing Taler transactions")
    override val talerOutVolume: TalerAmount
) : MonitorResponse

@Serializable
@SerialName("with-conversions")
@Name("MonitorWithConversion", qualifiedName = "with-conversions")
@Description("Monitor stats with currency conversion")
data class MonitorWithConversion(
    @Description("Number of cash-in operations")
    val cashinCount: Long,
    @Description("Total regional currency cash-in volume")
    val cashinRegionalVolume: TalerAmount,
    @Description("Total fiat currency cash-in volume")
    val cashinFiatVolume: TalerAmount,
    @Description("Number of cashout operations")
    val cashoutCount: Long,
    @Description("Total regional currency cashout volume")
    val cashoutRegionalVolume: TalerAmount,
    @Description("Total fiat currency cashout volume")
    val cashoutFiatVolume: TalerAmount,
    @Description("Number of incoming Taler transactions")
    override val talerInCount: Long,
    @Description("Total volume of incoming Taler transactions")
    override val talerInVolume: TalerAmount,
    @Description("Number of outgoing Taler transactions")
    override val talerOutCount: Long,
    @Description("Total volume of outgoing Taler transactions")
    override val talerOutVolume: TalerAmount
) : MonitorResponse

typealias Tans = List<Pair<TanChannel, String>>

/**
 * Convenience type to get bank account information
 * from/to the database.
 */
data class BankInfo(
    val username: String,
    val payto: String,
    val bankAccountId: Long,
    val isTalerExchange: Boolean,
    override val phone: String?,
    override val email: String?,
    override val channels: Set<TanChannel>
): TanInfo

interface TanInfo {
    val phone: String?
    val email: String?
    val channels: Set<TanChannel>

    val mfa: Tans get() = channels.map { channel ->
        val info = when (channel) {
            TanChannel.sms -> phone
            TanChannel.email -> email
        }
        Pair(channel, requireNotNull(info))
    }
}

// Allowed values for cashout TAN channels.
@Description("Channel for TAN delivery")
enum class TanChannel {
    sms,
    email
}

// Scopes for authentication tokens.
@Description("Scope for authentication tokens")
enum class TokenScope {
    readonly,
    readwrite,
    revenue,
    wiregateway,
    observability;


    fun logical(): TokenLogicalScope = when (this) {
        readonly -> TokenLogicalScope.readonly
        readwrite -> TokenLogicalScope.readwrite
        revenue -> TokenLogicalScope.revenue
        wiregateway -> TokenLogicalScope.readwrite_wiregateway
        observability -> TokenLogicalScope.observability
    }
}

enum class TokenLogicalScope {
    readonly,
    readwrite,
    revenue,
    refreshable,
    readonly_wiregateway,
    readwrite_wiregateway,
    observability
}

data class BearerToken(
    val scope: TokenScope,
    val isRefreshable: Boolean,
    val creationTime: Instant,
    val expirationTime: Instant
)

@Serializable
@Description("Information about an authentication token")
data class TokenInfo(
    @Description("Token creation timestamp")
    val creation_time: TalerTimestamp,
    @Description("Token expiration timestamp")
    val expiration: TalerTimestamp,
    @Description("Permission scope of the token")
    val scope: TokenScope,
    @Description("Whether the token can be refreshed")
    val isRefreshable: Boolean,
    @Description("Human-readable token description")
    val description: String? = null,
    @Description("Timestamp of last token usage")
    val last_access: TalerTimestamp,
    @Description("Row identifier of the token")
    val row_id: Long,
    @Description("Unique token identifier")
    val token_id: Long
)


@Serializable
@Description("List of authentication tokens")
data class TokenInfos (
    @Description("Array of token information entries")
    val tokens: List<TokenInfo>
)

@Serializable
@Description("Bank configuration response")
data class Config(
    @Description("Regional currency code")
    val currency: String,
    @Description("Currency specification for display")
    val currency_specification: CurrencySpecification,
    @Description("Base URL of the bank")
    val base_url: BaseURL?,
    @Description("Human-readable bank name")
    val bank_name: String,
    @Description("Whether currency conversion is enabled")
    val allow_conversion: Boolean,
    @Description("Whether account registration is open")
    val allow_registrations: Boolean,
    @Description("Whether account deletion is allowed")
    val allow_deletions: Boolean,
    @Description("Whether users can edit their legal name")
    val allow_edit_name: Boolean,
    @Description("Whether users can edit their cashout payto URI")
    val allow_edit_cashout_payto_uri: Boolean,
    @Description("Default debit threshold for new accounts")
    val default_debit_threshold: TalerAmount,
    @Description("Supported TAN channels for two-factor authentication")
    val supported_tan_channels: Set<TanChannel>,
    @Description("Wire transfer method (IBAN or X_TALER_BANK)")
    val wire_type: WireMethod,
    @Description("Fee charged per wire transfer")
    val wire_transfer_fees: TalerAmount,
    @Description("Minimum wire transfer amount")
    val min_wire_transfer_amount: TalerAmount,
    @Description("Maximum wire transfer amount")
    val max_wire_transfer_amount: TalerAmount
) {
    @Description("API name identifier")
    val name: String = "taler-corebank"
    @Description("API version string")
    val version: String = COREBANK_API_VERSION
}

@Serializable
@Description("Currency conversion configuration")
data class ConversionConfig(
    @Description("Regional currency code")
    val regional_currency: String,
    @Description("Regional currency display specification")
    val regional_currency_specification: CurrencySpecification,
    @Description("Fiat currency code")
    val fiat_currency: String,
    @Description("Fiat currency display specification")
    val fiat_currency_specification: CurrencySpecification,
    @Description("Applicable conversion rate")
    val conversion_rate: ConversionRate
) {
    @Description("API name identifier")
    val name: String = "taler-conversion-info"
    @Description("API version string")
    val version: String = CONVERSION_API_VERSION
}

@Serializable
@Description("Taler integration API configuration response")
data class TalerIntegrationConfigResponse(
    @Description("Currency used by this bank")
    val currency: String,
    @Description("Currency display specification")
    val currency_specification: CurrencySpecification
) {
    @Description("API name identifier")
    val name: String = "taler-bank-integration"
    @Description("API version string")
    val version: String = INTEGRATION_API_VERSION
}

@Description("Indicates whether a transaction is a credit or debit")
enum class CreditDebitInfo {
    credit, debit
}

@Serializable
@Description("Account balance information")
data class Balance(
    @Description("Balance amount")
    val amount: TalerAmount,
    @Description("Whether balance is credit or debit")
    val credit_debit_indicator: CreditDebitInfo,
)

/**
 * GET /accounts response.
 */
@Serializable
@Description("Minimal account data for account listings")
data class AccountMinimalData(
    @Description("Account username")
    val username: String,
    @Description("Legal name of account holder")
    val name: String,
    @Description("Payto URI of the account")
    val payto_uri: String,
    @Description("Current account balance")
    val balance: Balance,
    @Description("Maximum allowed debit balance")
    val debit_threshold: TalerAmount,
    @Description("Whether account is publicly visible")
    val is_public: Boolean,
    @Description("Whether account is a Taler exchange")
    val is_taler_exchange: Boolean,
    @Description("Whether account is locked")
    val is_locked: Boolean,
    @Description("Row identifier of the account")
    val row_id: Long,
    @Description("Current account status")
    val status: AccountStatus,
    @Description("Conversion rate class identifier")
    val conversion_rate_class_id: Long? = null,
    @Description("Applicable conversion rate")
    val conversion_rate: ConversionRate? = null
)

/**
 * Response type of GET /accounts.
 */
@Serializable
@Description("Paginated list of bank accounts")
data class ListBankAccountsResponse(
    @Description("Array of account summary entries")
    val accounts: List<AccountMinimalData>
)

/**
 * GET /accounts/$USERNAME response.
 */
@Serializable
@Description("Detailed account data for a single account")
data class AccountData(
    @Description("Legal name of account holder")
    val name: String,
    @Description("Payto URI of the account")
    val payto_uri: String,
    @Description("Current account balance")
    val balance: Balance,
    @Description("Maximum allowed debit balance")
    val debit_threshold: TalerAmount,
    @Description("Contact data for TAN challenges")
    val contact_data: ChallengeContactData? = null,
    @Description("Cashout payto URI for fiat withdrawals")
    val cashout_payto_uri: String? = null,
    @Description("Preferred TAN channel (deprecated)")
    val tan_channel: TanChannel? = null,
    @Description("Set of enabled TAN channels")
    val tan_channels: Set<TanChannel> = emptySet(),
    @Description("Whether account is publicly visible")
    val is_public: Boolean,
    @Description("Whether account is a Taler exchange")
    val is_taler_exchange: Boolean,
    @Description("Whether account is locked")
    val is_locked: Boolean,
    @Description("Current account status")
    val status: AccountStatus,
    @Description("Conversion rate class identifier")
    val conversion_rate_class_id: Long? = null,
    @Description("Applicable conversion rate")
    val conversion_rate: ConversionRate? = null
)

@Serializable
@Description("Request to create a bank transaction")
data class TransactionCreateRequest(
    @Description("Recipient payto URI")
    val payto_uri: Payto,
    @Description("Transaction amount")
    val amount: TalerAmount?,
    @Description("Idempotency key for the request")
    val request_uid: ShortHashCode?
)

@Serializable
@Description("Response after creating a bank transaction")
data class TransactionCreateResponse(
    @Description("Row identifier of the new transaction")
    val row_id: Long
)

/* History element, either from GET /transactions/T_ID
  or from GET /transactions */
@Serializable
@Description("Details of a single bank transaction")
data class BankAccountTransactionInfo(
    @Description("Payto URI of the creditor")
    val creditor_payto_uri: String,
    @Description("Payto URI of the debtor")
    val debtor_payto_uri: String,
    @Description("Transaction amount")
    val amount: TalerAmount,
    @Description("Transaction direction (credit or debit)")
    val direction: TransactionDirection,
    @Description("Wire transfer subject line")
    val subject: String,
    @Description("Row identifier of the transaction")
    val row_id: Long, // is T_ID
    @Description("Transaction timestamp")
    val date: TalerTimestamp
)

// Response type for histories, namely GET /transactions
@Serializable
@Description("Paginated list of bank transactions")
data class BankAccountTransactionsResponse(
    @Description("Array of transaction entries")
    val transactions: List<BankAccountTransactionInfo>
)

// Taler withdrawal request.
@Serializable
@Description("Request to create a Taler withdrawal")
data class BankAccountCreateWithdrawalRequest(
    @Description("Exact withdrawal amount")
    val amount: TalerAmount? = null,
    @Description("Suggested withdrawal amount for wallet")
    val suggested_amount: TalerAmount? = null,
    @Description("Whether wallet should choose the amount")
    val no_amount_to_wallet: Boolean = false
)

// Taler withdrawal response.
@Serializable
@Description("Response after creating a Taler withdrawal")
data class BankAccountCreateWithdrawalResponse(
    @Description("Unique withdrawal operation identifier")
    val withdrawal_id: String,
    @Description("Taler URI for the wallet to process")
    val taler_withdraw_uri: String
)

@Serializable
@Description("Public information about a withdrawal operation")
data class WithdrawalPublicInfo (
    @Description("Current withdrawal status")
    val status: WithdrawalStatus,
    @Description("Withdrawal amount if fixed")
    val amount: TalerAmount? = null,
    @Description("Suggested withdrawal amount")
    val suggested_amount: TalerAmount? = null,
    @Description("Whether wallet should choose the amount")
    val no_amount_to_wallet: Boolean,
    @Description("Username of the withdrawing account")
    val username: String,
    @Description("Selected reserve public key")
    val selected_reserve_pub: EddsaPublicKey? = null,
    @Description("Selected exchange payto account")
    val selected_exchange_account: String? = null,
)

@Serializable
@Description("Currency display specification")
data class CurrencySpecification(
    @Description("Human-readable currency name")
    val name: String,
    @Description("Fractional digits for input")
    val num_fractional_input_digits: Int,
    @Description("Fractional digits for normal display")
    val num_fractional_normal_digits: Int,
    @Description("Trailing zero digits to display")
    val num_fractional_trailing_zero_digits: Int,
    @Description("Alternative unit names by power of ten")
    val alt_unit_names: Map<String, String>
)


@Serializable
@Description("Detailed withdrawal operation status")
data class BankWithdrawalOperationStatus(
    @Description("Current withdrawal status")
    val status: WithdrawalStatus,
    @Description("Withdrawal amount if fixed")
    val amount: TalerAmount? = null,
    @Description("Suggested withdrawal amount")
    val suggested_amount: TalerAmount? = null,
    @Description("Minimum allowed withdrawal amount")
    val min_amount: TalerAmount? = null,
    @Description("Maximum allowed withdrawal amount")
    val max_amount: TalerAmount? = null,
    @Description("Card processing fees")
    val card_fees: TalerAmount? = null,
    @Description("Sender wire account details")
    val sender_wire: String? = null,
    @Description("Suggested exchange base URL")
    val suggested_exchange: String? = null,
    @Description("Required exchange base URL")
    val required_exchange: String? = null,
    @Description("URL to confirm the wire transfer")
    val confirm_transfer_url: String? = null,
    @Description("Supported wire transfer types")
    val wire_types: List<String>,
    @Description("Selected reserve public key")
    val selected_reserve_pub: EddsaPublicKey? = null,
    @Description("Selected exchange payto account")
    val selected_exchange_account: String? = null,
    @Description("Whether wallet should choose the amount")
    val no_amount_to_wallet: Boolean = false,
    @Description("Currency of the withdrawal")
    val currency: String? = null,
    // TODO deprecated remove in the next breaking release
    @Description("Whether withdrawal was aborted (deprecated)")
    val aborted: Boolean,
    @Description("Whether exchange was selected (deprecated)")
    val selection_done: Boolean,
    @Description("Whether wire transfer completed (deprecated)")
    val transfer_done: Boolean,
)

/**
 * Selection request on a Taler withdrawal.
 */
@Serializable
@Description("Wallet request to select exchange for withdrawal")
data class BankWithdrawalOperationPostRequest(
    @Description("Reserve public key from the wallet")
    val reserve_pub: EddsaPublicKey,
    @Description("Selected exchange payto URI")
    val selected_exchange: Payto,
    @Description("Withdrawal amount chosen by wallet")
    val amount: TalerAmount? = null
)

/**
 * Response to the wallet after it selects the exchange
 * and the reserve pub.
 */
@Serializable
@Description("Response after wallet selects exchange")
data class BankWithdrawalOperationPostResponse(
    @Description("Current withdrawal status")
    val status: WithdrawalStatus,
    @Description("URL to confirm the wire transfer")
    val confirm_transfer_url: String? = null,
    // TODO deprecated remove in the next breaking release
    @Description("Whether wire transfer completed (deprecated)")
    val transfer_done: Boolean,
)

@Serializable
@Description("Request to initiate a cashout operation")
data class CashoutRequest(
    @Description("Idempotency key for the request")
    val request_uid: ShortHashCode,
    @Description("Wire transfer subject line")
    val subject: String?,
    @Description("Amount debited in regional currency")
    val amount_debit: TalerAmount,
    @Description("Amount credited in fiat currency")
    val amount_credit: TalerAmount
)

@Serializable
@Description("Response after creating a cashout operation")
data class CashoutResponse(
    @Description("Unique cashout operation identifier")
    val cashout_id: Long,
)

@Serializable
@Description("List of cashout operations for an account")
data class Cashouts(
    @Description("Array of cashout summary entries")
    val cashouts: List<CashoutInfo>,
)

@Serializable
@Description("Summary of a single cashout operation")
data class CashoutInfo(
    @Description("Unique cashout operation identifier")
    val cashout_id: Long,
    @Description("Current cashout status")
    val status: CashoutStatus,
)


@Serializable
@Description("Global list of cashout operations")
data class GlobalCashouts(
    @Description("Array of global cashout entries")
    val cashouts: List<GlobalCashoutInfo>,
)

@Serializable
@Description("Global cashout entry with username")
data class GlobalCashoutInfo(
    @Description("Unique cashout operation identifier")
    val cashout_id: Long,
    @Description("Account username of the cashout owner")
    val username: String,
    @Description("Current cashout status")
    val status: CashoutStatus,
)

@Serializable
@Description("Detailed status of a cashout operation")
data class CashoutStatusResponse(
    @Description("Amount debited in regional currency")
    val amount_debit: TalerAmount,
    @Description("Amount credited in fiat currency")
    val amount_credit: TalerAmount,
    @Description("Wire transfer subject line")
    val subject: String,
    @Description("Cashout creation timestamp")
    val creation_time: TalerTimestamp,
    @Description("Cashout confirmation timestamp")
    val confirmation_time: TalerTimestamp? = null // TODO update doc
)

@Serializable
@Description("Request to solve a TAN challenge")
data class ChallengeSolve(
    @Description("TAN code entered by the user")
    val tan: String
)

@Serializable
@Description("Currency conversion result")
data class ConversionResponse(
    @Description("Amount debited from source currency")
    val amount_debit: TalerAmount,
    @Description("Amount credited in target currency")
    val amount_credit: TalerAmount,
)

/**
 * Response to GET /public-accounts
 */
@Serializable
@Description("List of publicly visible accounts")
data class PublicAccountsResponse(
    @Description("Array of public account entries")
    val public_accounts: List<PublicAccount>
)

/**
 * Single element of GET /public-accounts list.
 */
@Serializable
@Description("Public account summary information")
data class PublicAccount(
    @Description("Account username")
    val username: String,
    @Description("Payto URI of the account")
    val payto_uri: String,
    @Description("Current account balance")
    val balance: Balance,
    @Description("Whether account is a Taler exchange")
    val is_taler_exchange: Boolean,
    @Description("Row identifier of the account")
    val row_id: Long
)

/**
 * Request of PATCH /accounts/{USERNAME}/auth
 */
@Serializable
@Description("Request to change account password")
data class AccountPasswordChange(
    @Description("New password for the account")
    val new_password: String,
    @Description("Current password for verification")
    val old_password: String? = null
)

// Request POST /accounts/{USERNAME}/withdrawals/{WITHDRAWAL_ID}/confirm
@Serializable
@Description("Request to confirm a withdrawal operation")
data class BankAccountConfirmWithdrawalRequest(
    @Description("Final withdrawal amount")
    val amount: TalerAmount? = null
)

/**
* Request POST /conversion-rate-classes
* Request PATCH /conversion-rate-classes/{CLASS_ID}
*/
@Serializable
@Description("Input for creating or updating a conversion rate class")
data class ConversionRateClassInput(
    @Description("Human-readable class name")
    val name: String,
    @Description("Description of the rate class")
    val description: String? = null,
    @Description("Cash-in conversion ratio")
    val cashin_ratio: DecimalNumber? = null,
    @Description("Fee applied on cash-in operations")
    val cashin_fee: TalerAmount? = null,
    @Description("Rounding mode for cash-in amounts")
    val cashin_rounding_mode: RoundingMode? = null,
    @Description("Minimum amount for cash-in operations")
    val cashin_min_amount: TalerAmount? = null,
    @Description("Cashout conversion ratio")
    val cashout_ratio: DecimalNumber? = null,
    @Description("Fee applied on cashout operations")
    val cashout_fee: TalerAmount? = null,
    @Description("Rounding mode for cashout amounts")
    val cashout_rounding_mode: RoundingMode? = null,
    @Description("Minimum amount for cashout operations")
    val cashout_min_amount: TalerAmount? = null
)

/** Response POST /conversion-rate-classes */
@Serializable
@Description("Response after creating a conversion rate class")
data class ConversionRateClassResponse(
    @Description("Identifier of the created rate class")
    val conversion_rate_class_id: Long,
)

/**
* Response GET /conversion-rate-classes/{CLASS_ID}
*/
@Serializable
@Description("Detailed conversion rate class information")
data class ConversionRateClass(
    @Description("Unique rate class identifier")
    val conversion_rate_class_id: Long,
    @Description("Human-readable class name")
    val name: String,
    @Description("Description of the rate class")
    val description: String? = null,
    @Description("Number of users in this class")
    val num_users: Int,
    @Description("Cash-in conversion ratio")
    val cashin_ratio: DecimalNumber? = null,
    @Description("Fee applied on cash-in operations")
    val cashin_fee: TalerAmount? = null,
    @Description("Rounding mode for cash-in amounts")
    val cashin_rounding_mode: RoundingMode? = null,
    @Description("Minimum amount for cash-in operations")
    val cashin_min_amount: TalerAmount? = null,
    @Description("Cashout conversion ratio")
    val cashout_ratio: DecimalNumber? = null,
    @Description("Fee applied on cashout operations")
    val cashout_fee: TalerAmount? = null,
    @Description("Rounding mode for cashout amounts")
    val cashout_rounding_mode: RoundingMode? = null,
    @Description("Minimum amount for cashout operations")
    val cashout_min_amount: TalerAmount? = null
)

/**
* Response GET /conversion-rate-classes
*/
@Serializable
@Description("List of all conversion rate classes")
data class ConversionRateClasses(
    @Description("Array of conversion rate class entries")
    val classes: List<ConversionRateClass>
)
