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

import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.security.MessageDigest
import io.github.smiley4.schemakenerator.core.annotations.Description
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import tech.libeufin.common.crypto.CryptoUtil.hashStringNbo
import tech.libeufin.common.crypto.NBO

enum class IncomingType {
    reserve,
    kyc,
    map
}

@Description("State of a wire transfer")
enum class TransferStatusState {
    pending,
    transient_failure,
    permanent_failure,
    success
}

/** Response GET /taler-wire-gateway/config */
@Serializable
@Description("Wire gateway configuration response")
data class WireGatewayConfig(
    @Description("Currency supported by the gateway")
    val currency: String,
    @Description("Whether account check is supported")
    val support_account_check: Boolean
) {
    @Description("API name identifier")
    val name: String = "taler-wire-gateway"
    @Description("API version string")
    val version: String = WIRE_GATEWAY_API_VERSION
}

/** Request POST /taler-wire-gateway/transfer */
@Serializable
@Description("Wire transfer request")
data class TransferRequest(
    @Description("Unique identifier for this request")
    val request_uid: HashCode,
    @Description("Amount to transfer")
    val amount: TalerAmount,
    @Description("Base URL of the exchange")
    val exchange_base_url: BaseURL,
    @Description("Wire transfer identifier")
    val wtid: ShortHashCode,
    @Description("Payto URI of the credit account")
    val credit_account: Payto,
    @Description("Optional transfer metadata")
    val metadata: String? = null,
) {
    init {
        if (metadata != null && !METADATA_REGEX.matches(metadata))
            throw badRequest("metadata '$metadata' is malformed, must match [a-zA-Z0-9-.+:]{1,40}")
    }

    companion object {
        private val METADATA_REGEX = Regex("^[a-zA-Z0-9-.:]{1,40}$")
    }
}

/** Response POST /taler-wire-gateway/transfer */
@Serializable
@Description("Wire transfer response")
data class TransferResponse(
    @Description("Timestamp of the transfer")
    val timestamp: TalerTimestamp,
    @Description("Database row identifier")
    val row_id: Long
)

/** Request GET /taler-wire-gateway/transfers */
@Serializable
@Description("List of wire transfers")
data class TransferList(
    @Description("List of transfer statuses")
    val transfers: List<TransferListStatus>,
    @Description("Payto URI of the debit account")
    val debit_account: String
)

@Serializable
@Description("Transfer status in a list response")
data class TransferListStatus(
    @Description("Database row identifier")
    val row_id: Long,
    @Description("Current transfer status")
    val status: TransferStatusState,
    @Description("Transfer amount")
    val amount: TalerAmount,
    @Description("Payto URI of the credit account")
    val credit_account: String,
    @Description("Timestamp of the transfer")
    val timestamp: TalerTimestamp
)

/** Request GET /taler-wire-gateway/transfers/{ROW_iD} */
@Serializable
@Description("Detailed status of a single transfer")
data class TransferStatus(
    @Description("Current transfer status")
    val status: TransferStatusState,
    @Description("Optional status message")
    val status_msg: String? = null,
    @Description("Transfer amount")
    val amount: TalerAmount,
    @Description("URL of the originating exchange")
    val origin_exchange_url: String,
    @Description("Optional transfer metadata")
    val metadata: String? = null,
    @Description("Wire transfer identifier")
    val wtid: ShortHashCode,
    @Description("Payto URI of the credit account")
    val credit_account: String,
    @Description("Timestamp of the transfer")
    val timestamp: TalerTimestamp
)

/** Request POST /taler-wire-gateway/admin/add-incoming */
@Serializable
@Description("Request to add an incoming transaction")
data class AddIncomingRequest(
    @Description("Amount of the incoming transaction")
    val amount: TalerAmount,
    @Description("Reserve public key")
    val reserve_pub: EddsaPublicKey,
    @Description("Payto URI of the debit account")
    val debit_account: Payto
)

/** Response POST /taler-wire-gateway/admin/add-incoming */
@Serializable
@Description("Response to adding an incoming transaction")
data class AddIncomingResponse(
    @Description("Timestamp of the transaction")
    val timestamp: TalerTimestamp,
    @Description("Database row identifier")
    val row_id: Long
)

/** Request POST /taler-wire-gateway/admin/add-kycauth */
@Serializable
@Description("Request to add a KYC auth transaction")
data class AddKycauthRequest(
    @Description("Amount of the KYC auth transaction")
    val amount: TalerAmount,
    @Description("Account public key for KYC")
    val account_pub: EddsaPublicKey,
    @Description("Payto URI of the debit account")
    val debit_account: Payto
)

/** Request POST /taler-wire-gateway/admin/add-mapped */
@Serializable
data class AddMappedRequest(
    val amount: TalerAmount,
    val authorization_pub: EddsaPublicKey,
    val debit_account: Payto
)

/** Response GET /taler-wire-gateway/history/incoming */
@Serializable
@Description("History of incoming transactions")
data class IncomingHistory(
    @Description("List of incoming transactions")
    val incoming_transactions: List<IncomingBankTransaction>,
    @Description("Payto URI of the credit account")
    val credit_account: String
)

/** Inner response GET /taler-wire-gateway/history/incoming */
@Serializable
@Description("Incoming bank transaction details")
sealed interface IncomingBankTransaction {
    val row_id: Long
    val date: TalerTimestamp
    val amount: TalerAmount
    val debit_account: String
    val credit_fee: TalerAmount?
}

@Serializable
@SerialName("KYCAUTH")
@Description("Incoming KYC authentication transaction")
data class IncomingKycAuthTransaction(
    @Description("Database row identifier")
    override val row_id: Long,
    @Description("Timestamp of the transaction")
    override val date: TalerTimestamp,
    @Description("Transaction amount")
    override val amount: TalerAmount,
    @Description("Optional credit fee")
    override val credit_fee: TalerAmount? = null,
    @Description("Payto URI of the debit account")
    override val debit_account: String,
    @Description("Account public key for KYC")
    val account_pub: EddsaPublicKey,
    @Description("Optional authorization public key")
    val authorization_pub: EddsaPublicKey? = null,
    @Description("Optional authorization signature")
    val authorization_sig: EddsaSignature? = null,
) : IncomingBankTransaction

@Serializable
@SerialName("RESERVE")
@Description("Incoming reserve transaction")
data class IncomingReserveTransaction(
    @Description("Database row identifier")
    override val row_id: Long,
    @Description("Timestamp of the transaction")
    override val date: TalerTimestamp,
    @Description("Transaction amount")
    override val amount: TalerAmount,
    @Description("Optional credit fee")
    override val credit_fee: TalerAmount? = null,
    @Description("Payto URI of the debit account")
    override val debit_account: String,
    @Description("Reserve public key")
    val reserve_pub: EddsaPublicKey,
    @Description("Optional authorization public key")
    val authorization_pub: EddsaPublicKey? = null,
    @Description("Optional authorization signature")
    val authorization_sig: EddsaSignature? = null,
) : IncomingBankTransaction

@Serializable
@SerialName("WAD")
@Description("Incoming WAD transaction")
data class IncomingWadTransaction(
    @Description("Database row identifier")
    override val row_id: Long,
    @Description("Timestamp of the transaction")
    override val date: TalerTimestamp,
    @Description("Transaction amount")
    override val amount: TalerAmount,
    @Description("Optional credit fee")
    override val credit_fee: TalerAmount? = null,
    @Description("Payto URI of the debit account")
    override val debit_account: String,
    @Description("URL of the originating exchange")
    val origin_exchange_url: String,
    @Description("WAD identifier")
    val wad_id: String // TODO 24 bytes Base32
) : IncomingBankTransaction

/** Response GET /taler-wire-gateway/history/outgoing */
@Serializable
@Description("History of outgoing transactions")
data class OutgoingHistory(
    @Description("List of outgoing transactions")
    val outgoing_transactions: List<OutgoingTransaction>,
    @Description("Payto URI of the debit account")
    val debit_account: String
)

/** Inner response GET /taler-wire-gateway/history/outgoing */
@Serializable
@Description("Single outgoing transaction details")
data class OutgoingTransaction(
    @Description("Database row identifier")
    val row_id: Long, // DB row ID of the payment.
    @Description("Timestamp of the transaction")
    val date: TalerTimestamp,
    @Description("Transaction amount")
    val amount: TalerAmount,
    @Description("Payto URI of the credit account")
    val credit_account: String,
    @Description("Wire transfer identifier")
    val wtid: ShortHashCode,
    @Description("Base URL of the exchange")
    val exchange_base_url: String,
    @Description("Optional transfer metadata")
    val metadata: String? = null,
    @Description("Optional debit fee")
    val debit_fee: TalerAmount? = null
)

/** Response GET /taler-wire-gateway/account/check */
@Serializable
@Description("Account information response")
class AccountInfo()

/** Response GET /taler-prepared-transfer/config */
@Serializable
@Description("Prepared transfer configuration")
data class PreparedTransferConfig(
    @Description("Currency supported")
    val currency: String,
    @Description("List of supported subject formats")
    val supported_formats: List<SubjectFormat>
) {
    @Description("API name identifier")
    val name: String = "taler-prepared-transfer"
    @Description("API version string")
    val version: String = WIRE_TRANSFER_API_VERSION
}


/** Inner response GET /taler-prepared-transfer/registration */
@Serializable
@Description("Transfer subject information")
sealed interface TransferSubject {
    @Serializable
    @SerialName("SIMPLE")
    @Description("Simple text transfer subject")
    data class Simple(
        @Description("Plain text transfer subject")
        val subject: String,
        @Description("Credit amount for the transfer")
        val credit_amount: TalerAmount
    ) : TransferSubject

    @Serializable
    @SerialName("URI")
    @Description("URI-based transfer subject")
    data class Uri(
        @Description("Taler URI for the transfer")
        val uri: String,
        @Description("Credit amount for the transfer")
        val credit_amount: TalerAmount
    ) : TransferSubject

    @Serializable
    @SerialName("CH_QR_BILL")
    @Description("Swiss QR bill transfer subject")
    data class QrBill(
        @Description("QR reference number for the bill")
        val qr_reference_number: String,
        @Description("Credit amount for the transfer")
        val credit_amount: TalerAmount,
    ) : TransferSubject
}

@Serializable
@Description("Supported transfer subject format")
enum class SubjectFormat {
    SIMPLE,
    URI,
    CH_QR_BILL
}

@Serializable
@Description("Public key algorithm")
enum class PublicKeyAlg {
    EdDSA
}

@Serializable
@Description("Type of wire transfer")
enum class TransferType {
    reserve,
    kyc
}

@Serializable
@Description("Request to generate a transfer subject")
data class SubjectRequest(
    @Description("Payto URI of the credit account")
    val credit_account: Payto,
    @Description("Type of transfer")
    val type: TransferType,
    @Description("Whether subject is recurrent")
    val recurrent: Boolean,
    @Description("Credit amount for the transfer")
    val credit_amount: TalerAmount,
    @Description("Public key algorithm")
    val alg: PublicKeyAlg,
    @Description("Account public key")
    val account_pub: EddsaPublicKey,
    @Description("Authorization public key")
    val authorization_pub: EddsaPublicKey,
    @Description("Authorization signature")
    val authorization_sig: EddsaSignature,
) : NBO {
    /* Network bytes */
    override fun nbo(): ByteArray = ByteBuffer.allocate(104).apply {
        order(ByteOrder.BIG_ENDIAN)
        putInt(capacity())
        putInt(1224)
        put(hashStringNbo(credit_account.toString()))
        put(credit_amount.nbo())
        putInt(when (type) {
            TransferType.reserve -> 1
            TransferType.kyc -> 2
        })
        putShort(if (recurrent) 2 else 1)
        putShort(when (alg) {
            PublicKeyAlg.EdDSA -> 1
        })
        put(account_pub.raw)
    }.array()

    fun sign(priv: ByteArray): SubjectRequest =
        this.copy(authorization_sig = this.signNbo(priv))

    fun verify(): Boolean =
        this.verifyNbo(this.authorization_sig, this.authorization_pub)
}

@Serializable
@Description("Result of subject generation")
data class SubjectResult(
    @Description("List of generated transfer subjects")
    val subjects: List<TransferSubject>,
    @Description("Expiration timestamp of the subjects")
    val expiration: TalerTimestamp
)

@Serializable
@Description("Request to unregister a subject")
data class Unregistration(
    @Description("Timestamp of the unregistration")
    val timestamp: TalerTimestamp,
    @Description("Authorization public key")
    val authorization_pub: EddsaPublicKey,
    @Description("Authorization signature")
    val authorization_sig: EddsaSignature
) : NBO {
    /* Network bytes */
    override fun nbo(): ByteArray = ByteBuffer.allocate(16).apply {
        order(ByteOrder.BIG_ENDIAN)
        putInt(capacity())
        putInt(1225)
        putLong(timestamp.instant.epochSecond * 1000 * 1000)
    }.array()

    fun sign(priv: ByteArray): Unregistration =
        this.copy(authorization_sig = this.signNbo(priv))

    fun verify(): Boolean =
        this.verifyNbo(this.authorization_sig, this.authorization_pub)
}

/** Response GET /taler-revenue/config */
@Serializable
@Description("Revenue API configuration response")
data class RevenueConfig(
    @Description("Currency supported by the API")
    val currency: String
) {
    @Description("API name identifier")
    val name: String = "taler-revenue"
    @Description("API version string")
    val version: String = REVENUE_API_VERSION
}

/** Request GET /taler-revenue/history */
@Serializable
@Description("History of revenue incoming transactions")
data class RevenueIncomingHistory(
    @Description("List of incoming revenue transactions")
    val incoming_transactions: List<RevenueIncomingBankTransaction>,
    @Description("Payto URI of the credit account")
    val credit_account: String
)

/** Inner request GET /taler-revenue/history */
@Serializable
@Description("Single revenue incoming bank transaction")
data class RevenueIncomingBankTransaction(
    @Description("Database row identifier")
    val row_id: Long,
    @Description("Timestamp of the transaction")
    val date: TalerTimestamp,
    @Description("Transaction amount")
    val amount: TalerAmount,
    @Description("Optional credit fee")
    val credit_fee: TalerAmount? = null,
    @Description("Payto URI of the debit account")
    val debit_account: String,
    @Description("Transaction subject line")
    val subject: String
)

/** Response GET /taler-observability/config */
@Serializable
@Description("Observability API configuration response")
class TalerObservabilityConfig() {
    @Description("API name identifier")
    val name: String = "taler-observability"
    @Description("API version string")
    val version: String = OBSERVABILITY_API_VERSION
}
