/*
 * 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.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: BaseURL?,
        metadata: String?
    ): OutgoingRegistrationResult = db.serializable(
        """
        SELECT out_tx_id, out_initiated, out_found
        FROM register_outgoing((?,?)::taler_amount,(?,?)::taler_amount,?,?,?,?,?,?,?,?,?)
        """
    ) {
        val executionTime = payment.executionTime.micros()
        
        bind(payment.amount)
        bind(payment.debitFee ?: TalerAmount.zero(db.currency))
        bind(payment.subject)
        bind(executionTime)
        bind(payment.creditor?.toString())
        bind(payment.id.endToEndId)
        bind(payment.id.msgId)
        bind(payment.id.acctSvcrRef)
        bind(wtid)
        bind(baseUrl?.url?.toString())
        bind(metadata)

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

    interface InResult {
        val new: Boolean
        val completed: Boolean
        val bounceId: String?
    }

    /** Incoming payments bounce registration result */
    sealed interface IncomingBounceRegistrationResult {
        data class Success(val id: Long, override val bounceId: String, override val new: Boolean, override val completed: Boolean): IncomingBounceRegistrationResult, InResult
        data object Talerable: IncomingBounceRegistrationResult
    }

    /** Register an incoming payment and bounce it */
    suspend fun registerMalformedIncoming(
        payment: IncomingPayment,
        bounceAmount: TalerAmount,
        bounceEndToEndId: String,
        timestamp: Instant,
        cause: String
    ): IncomingBounceRegistrationResult = db.serializable(
        """
        SELECT out_found, out_tx_id, out_completed, out_bounce_id, out_talerable
        FROM register_and_bounce_incoming((?,?)::taler_amount,(?,?)::taler_amount,?,?,?,?,?,?,(?,?)::taler_amount,?,?, ?)
        """
    ) {
        bind(payment.amount)
        bind(payment.creditFee ?: TalerAmount.zero(db.currency))
        bind(payment.subject)
        bind(payment.executionTime)
        bind(payment.debtor?.toString())
        bind(payment.id.uetr)
        bind(payment.id.txId)
        bind(payment.id.acctSvcrRef)
        bind(bounceAmount)
        bind(timestamp)
        bind(bounceEndToEndId)
        bind(cause)
        one {
            if (it.getBoolean("out_talerable")) {
                IncomingBounceRegistrationResult.Talerable
            } else {
                IncomingBounceRegistrationResult.Success(
                    it.getLong("out_tx_id"),
                    it.getString("out_bounce_id"),
                    !it.getBoolean("out_found"),
                    it.getBoolean("out_completed")
                )
            }
        }
    }

    /** Incoming payments registration result */
    sealed interface IncomingRegistrationResult {
        data class Success(
            val id: Long,
            override val new: Boolean,
            override val completed: Boolean,
            override val bounceId: String?,
            val pending: Boolean
        ): IncomingRegistrationResult, InResult
        data object ReservePubReuse: IncomingRegistrationResult
        data object MappingReuse: IncomingRegistrationResult
        data object UnknownMapping: IncomingRegistrationResult
    }

    /** Register an talerable incoming payment */
    suspend fun registerTalerableIncoming(
        payment: IncomingPayment,
        metadata: IncomingSubject
    ): IncomingRegistrationResult = db.serializable(
        """
        SELECT
            out_reserve_pub_reuse,
            out_mapping_reuse,
            out_unknown_mapping,
            out_found,
            out_completed,
            out_pending,
            out_tx_id,
            out_bounce_id
        FROM register_incoming((?,?)::taler_amount,(?,?)::taler_amount,?,?,?,?,?,?,?::taler_incoming_type,?,NULL)
        """
    ) {
        bind(payment.amount)
        bind(payment.creditFee ?: TalerAmount.zero(db.currency))
        bind(payment.subject)
        bind(payment.executionTime)
        bind(payment.debtor?.toString())
        bind(payment.id.uetr)
        bind(payment.id.txId)
        bind(payment.id.acctSvcrRef)
        bind(metadata.type)
        bind(metadata.key)
        one {
            when {
                it.getBoolean("out_reserve_pub_reuse") -> IncomingRegistrationResult.ReservePubReuse
                it.getBoolean("out_mapping_reuse") -> IncomingRegistrationResult.MappingReuse
                it.getBoolean("out_unknown_mapping") -> IncomingRegistrationResult.UnknownMapping
                else -> IncomingRegistrationResult.Success(
                    it.getLong("out_tx_id"),
                    !it.getBoolean("out_found"),
                    it.getBoolean("out_completed"),
                    it.getString("out_bounce_id"),
                    it.getBoolean("out_pending")
                )
            }
        }
    }

    /** Register an QR-Bill incoming payment */
    suspend fun registerQrBillIncoming(
        payment: IncomingPayment,
        reference: String
    ): IncomingRegistrationResult = db.serializable(
        """
        SELECT
            out_reserve_pub_reuse,
            out_mapping_reuse,
            out_unknown_mapping,
            out_found,
            out_completed,
            out_pending,
            out_tx_id,
            out_bounce_id
        FROM register_incoming((?,?)::taler_amount,(?,?)::taler_amount,?,?,?,?,?,?,NULL,NULL,?)
        """
    ) {
        bind(payment.amount)
        bind(payment.creditFee ?: TalerAmount.zero(db.currency))
        bind(payment.subject)
        bind(payment.executionTime)
        bind(payment.debtor?.toString())
        bind(payment.id.uetr)
        bind(payment.id.txId)
        bind(payment.id.acctSvcrRef)
        bind(reference)
        one {
            when {
                it.getBoolean("out_reserve_pub_reuse") -> IncomingRegistrationResult.ReservePubReuse
                it.getBoolean("out_mapping_reuse") -> IncomingRegistrationResult.MappingReuse
                it.getBoolean("out_unknown_mapping") -> IncomingRegistrationResult.UnknownMapping
                else -> IncomingRegistrationResult.Success(
                    it.getLong("out_tx_id"),
                    !it.getBoolean("out_found"),
                    it.getBoolean("out_completed"),
                    it.getString("out_bounce_id"),
                    it.getBoolean("out_pending")
                )
            }
        }
    }

    /** Register an incoming payment */
    suspend fun registerIncoming(
        payment: IncomingPayment
    ): IncomingRegistrationResult.Success = db.serializable(
        """
        SELECT out_found, out_completed, out_tx_id, out_bounce_id
        FROM register_incoming((?,?)::taler_amount,(?,?)::taler_amount,?,?,?,?,?,?,NULL,NULL,NULL)
        """
    ) {
        bind(payment.amount)
        bind(payment.creditFee ?: TalerAmount.zero(db.currency))
        bind(payment.subject)
        bind(payment.executionTime)
        bind(payment.debtor?.toString())
        bind(payment.id.uetr)
        bind(payment.id.txId)
        bind(payment.id.acctSvcrRef)
        one {
            IncomingRegistrationResult.Success(
                it.getLong("out_tx_id"),
                !it.getBoolean("out_found"),
                it.getBoolean("out_completed"),
                it.getString("out_bounce_id"),
                false
            )
        }
    }

    /** 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 debit_payto IS NOT NULL AND subject IS NOT NULL AND
        """, "incoming_transaction_id") {
            RevenueIncomingBankTransaction(
                row_id = it.getLong("incoming_transaction_id"),
                date = it.getTalerTimestamp("execution_time"),
                amount = it.getAmount("amount", db.currency),
                credit_fee = it.getAmount("credit_fee", db.currency).notZeroOrNull(),
                debit_account = it.getString("debit_payto"),
                subject = it.getString("subject")
            )
        }
}