/*
 * This file is part of LibEuFin.
 * Copyright (C) 2024 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.nexus.db

import tech.libeufin.common.*
import tech.libeufin.common.db.*
import tech.libeufin.nexus.iso20022.IncomingPayment
import tech.libeufin.nexus.iso20022.OutgoingPayment
import java.sql.Types
import java.time.Instant

/** Data access logic for incoming & outgoing payments */
class PaymentDAO(private val db: Database) {
    /** Outgoing payments registration result */
    data class OutgoingRegistrationResult(
        val id: Long,
        val initiated: Boolean,
        val new: Boolean
    )

    /** Register an outgoing payment reconciling it with its initiated payment counterpart if present */
    suspend fun registerOutgoing(
        payment: OutgoingPayment, 
        wtid: ShortHashCode?,
        baseUrl: ExchangeUrl?,
    ): OutgoingRegistrationResult = db.serializable(
        """
        SELECT out_tx_id, out_initiated, out_found
        FROM register_outgoing((?,?)::taler_amount,?,?,?,?,?,?,?)
        """
    ) {
        val executionTime = payment.executionTime.micros()
        
        setLong(1, payment.amount.value)
        setInt(2, payment.amount.frac)
        setString(3, payment.subject)
        setLong(4, executionTime)
        setString(5, payment.creditorPayto.toString())
        setString(6, payment.endToEndId)
        setBytes(7, wtid?.raw)
        setString(8, baseUrl?.url)
        setString(9, payment.msgId)

        one {
            OutgoingRegistrationResult(
                it.getLong("out_tx_id"),
                it.getBoolean("out_initiated"),
                !it.getBoolean("out_found")
            )
        }
    }

    /** Incoming payments bounce registration result */
    data class IncomingBounceRegistrationResult(
        val id: Long,
        val bounceId: String,
        val new: Boolean
    )

    /** Register an incoming payment and bounce it */
    suspend fun registerMalformedIncoming(
        payment: IncomingPayment,
        bounceAmount: TalerAmount,
        bounceEndToEndId: String,
        timestamp: Instant
    ): IncomingBounceRegistrationResult = db.serializable(
        """
        SELECT out_found, out_tx_id, out_bounce_id
        FROM register_and_bounce_incoming((?,?)::taler_amount,(?,?)::taler_amount,?,?,?,?,(?,?)::taler_amount,?,?)
        """
    ) {
        setLong(1, payment.amount.value)
        setInt(2, payment.amount.frac)
        setLong(3, payment.creditFee?.value ?: 0L)
        setInt(4, payment.creditFee?.frac ?: 0)
        setString(5, payment.subject)
        setLong(6, payment.executionTime.micros())
        setString(7, payment.debtorPayto.toString())
        setString(8, payment.bankId)
        setLong(9, bounceAmount.value)
        setInt(10, bounceAmount.frac)
        setLong(11, timestamp.micros())
        setString(12, bounceEndToEndId)
        one {
            IncomingBounceRegistrationResult(
                it.getLong("out_tx_id"),
                it.getString("out_bounce_id"),
                !it.getBoolean("out_found")
            )
        }
    }

    /** Incoming payments registration result */
    sealed interface IncomingRegistrationResult {
        data class Success(val id: Long, val new: Boolean): IncomingRegistrationResult
        data object ReservePubReuse: IncomingRegistrationResult
    }

    /** Register an talerable incoming payment */
    suspend fun registerTalerableIncoming(
        payment: IncomingPayment,
        metadata: TalerIncomingMetadata
    ): IncomingRegistrationResult = db.serializable(
        """
        SELECT out_reserve_pub_reuse, out_found, out_tx_id
        FROM register_incoming((?,?)::taler_amount,(?,?)::taler_amount,?,?,?,?,?::taler_incoming_type,?,?)
        """
    ) {
        val executionTime = payment.executionTime.micros()
        setLong(1, payment.amount.value)
        setInt(2, payment.amount.frac)
        setLong(3, payment.creditFee?.value ?: 0L)
        setInt(4, payment.creditFee?.frac ?: 0)
        setString(5, payment.subject)
        setLong(6, executionTime)
        setString(7, payment.debtorPayto.toString())
        setString(8, payment.bankId)
        setString(9, metadata.type.name)
        when (metadata.type) {
            TalerIncomingType.reserve -> {
                setBytes(10, metadata.key.raw)
                setNull(11, Types.BINARY)
            }
            TalerIncomingType.kyc -> {
                setNull(10, Types.BINARY)
                setBytes(11, metadata.key.raw)
            }
            TalerIncomingType.wad -> throw UnsupportedOperationException()
        }
        one {
            when {
                it.getBoolean("out_reserve_pub_reuse") -> IncomingRegistrationResult.ReservePubReuse
                else -> IncomingRegistrationResult.Success(
                    it.getLong("out_tx_id"),
                    !it.getBoolean("out_found")
                )
            }
        }
    }

    /** Register an incoming payment */
    suspend fun registerIncoming(
        payment: IncomingPayment
    ): IncomingRegistrationResult.Success = db.serializable(
        """
        SELECT out_found, out_tx_id
        FROM register_incoming((?,?)::taler_amount,(?,?)::taler_amount,?,?,?,?,NULL,NULL,NULL)
        """
    ) {
        val executionTime = payment.executionTime.micros()
        setLong(1, payment.amount.value)
        setInt(2, payment.amount.frac)
        setLong(3, payment.creditFee?.value ?: 0L)
        setInt(4, payment.creditFee?.frac ?: 0)
        setString(5, payment.subject)
        setLong(6, executionTime)
        setString(7, payment.debtorPayto.toString())
        setString(8, payment.bankId)
        one {
            IncomingRegistrationResult.Success(
                it.getLong("out_tx_id"),
                !it.getBoolean("out_found")
            )
        }
    }

    /** Query history of incoming transactions */
    suspend fun revenueHistory(
        params: HistoryParams
    ): List<RevenueIncomingBankTransaction> 
        = db.poolHistoryGlobal(params, db::listenRevenue, """
            SELECT
                 incoming_transaction_id
                ,execution_time
                ,(amount).val AS amount_val
                ,(amount).frac AS amount_frac
                ,(credit_fee).val AS credit_fee_val
                ,(credit_fee).frac AS credit_fee_frac
                ,debit_payto
                ,subject
            FROM incoming_transactions WHERE
        """, "incoming_transaction_id") {
            RevenueIncomingBankTransaction(
                row_id = it.getLong("incoming_transaction_id"),
                date = it.getTalerTimestamp("execution_time"),
                amount = it.getAmount("amount", db.bankCurrency),
                credit_fee = it.getAmount("credit_fee", db.bankCurrency).notZeroOrNull(),
                debit_account = it.getString("debit_payto"),
                subject = it.getString("subject")
            )
        }
}