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

import org.slf4j.Logger
import org.slf4j.LoggerFactory
import tech.libeufin.bank.BankAccountTransactionInfo
import tech.libeufin.common.*
import tech.libeufin.common.db.*
import java.sql.Types
import java.time.Instant

private val logger: Logger = LoggerFactory.getLogger("libeufin-bank-tx-dao")

/** Data access logic for transactions */
class TransactionDAO(private val db: Database) {
    /** Result status of bank transaction creation */
    sealed interface BankTransactionResult {
        data class Success(val id: Long): BankTransactionResult
        data object UnknownCreditor: BankTransactionResult
        data object AdminCreditor: BankTransactionResult
        data object UnknownDebtor: BankTransactionResult
        data object BothPartySame: BankTransactionResult
        data object BalanceInsufficient: BankTransactionResult
        data object BadAmount: BankTransactionResult
        data object TanRequired: BankTransactionResult
        data object RequestUidReuse: BankTransactionResult
    }

    /** Create a new transaction */
    suspend fun create(
        creditAccountPayto: Payto,
        debitAccountUsername: String,
        subject: String,
        amount: TalerAmount,
        timestamp: Instant,
        is2fa: Boolean,
        requestUid: ShortHashCode?,
        wireTransferFees: TalerAmount,
        minAmount: TalerAmount,
        maxAmount: TalerAmount
    ): BankTransactionResult {
        val (type: IncomingType?, metadata: EddsaPublicKey?, bounceCause: String?) = runCatching { parseIncomingSubject(subject) }.fold(
            onSuccess = { metadata -> 
                if (metadata is IncomingSubject.AdminBalanceAdjust) {
                    Triple(null, null, "unsupported admin balance adjust")
                } else {
                    Triple(metadata.type, metadata.key, null)
                }
            },
            onFailure = { e -> Triple(null, null, "malformed metadata - ${e.message}") }
        )
        return db.serializable("""
            SELECT 
                out_creditor_not_found 
                ,out_debtor_not_found
                ,out_same_account
                ,out_balance_insufficient
                ,out_bad_amount
                ,out_request_uid_reuse
                ,out_tan_required
                ,out_credit_bank_account_id
                ,out_debit_bank_account_id
                ,out_credit_row_id
                ,out_debit_row_id
                ,out_creditor_is_exchange 
                ,out_debtor_is_exchange
                ,out_creditor_admin
                ,out_idempotent
            FROM bank_transaction(?,?,?,(?,?)::taler_amount,?,?,?,(?,?)::taler_amount,(?,?)::taler_amount,(?,?)::taler_amount,?::taler_incoming_type,?,?)
        """
        ) {
            bind(creditAccountPayto.canonical)
            bind(debitAccountUsername)
            bind(subject)
            bind(amount)
            bind(timestamp)
            bind(is2fa)
            bind(requestUid)
            bind(wireTransferFees)
            bind(minAmount)
            bind(maxAmount)
            bind(type)
            bind(metadata)
            bind(bounceCause)
            one {
                when {
                    it.getBoolean("out_creditor_not_found") -> BankTransactionResult.UnknownCreditor
                    it.getBoolean("out_debtor_not_found") -> BankTransactionResult.UnknownDebtor
                    it.getBoolean("out_same_account") -> BankTransactionResult.BothPartySame
                    it.getBoolean("out_balance_insufficient") -> BankTransactionResult.BalanceInsufficient
                    it.getBoolean("out_bad_amount") -> BankTransactionResult.BadAmount
                    it.getBoolean("out_creditor_admin") -> BankTransactionResult.AdminCreditor
                    it.getBoolean("out_request_uid_reuse") -> BankTransactionResult.RequestUidReuse
                    it.getBoolean("out_idempotent") -> BankTransactionResult.Success(it.getLong("out_debit_row_id"))
                    it.getBoolean("out_tan_required") -> BankTransactionResult.TanRequired
                    else -> BankTransactionResult.Success(it.getLong("out_debit_row_id"))
                }
            }
        }
    }
    
    /** Get transaction [rowId] owned by [username] */
    suspend fun get(rowId: Long, username: String): BankAccountTransactionInfo? = db.serializable(
        """
        SELECT 
            creditor_payto
            ,creditor_name
            ,debtor_payto
            ,debtor_name
            ,subject
            ,(amount).val AS amount_val
            ,(amount).frac AS amount_frac
            ,transaction_date
            ,direction
            ,bank_transaction_id
        FROM bank_account_transactions
            JOIN bank_accounts ON bank_account_transactions.bank_account_id=bank_accounts.bank_account_id
            JOIN customers ON customer_id=owning_customer_id 
        WHERE bank_transaction_id=? AND username=?
        """
    ) {
        bind(rowId)
        bind(username)
        oneOrNull {
            BankAccountTransactionInfo(
                creditor_payto_uri = it.getBankPayto("creditor_payto", "creditor_name", db.ctx),
                debtor_payto_uri = it.getBankPayto("debtor_payto", "debtor_name", db.ctx),
                amount = it.getAmount("amount", db.bankCurrency),
                direction = it.getEnum("direction"),
                subject = it.getString("subject"),
                date = it.getTalerTimestamp("transaction_date"),
                row_id = it.getLong("bank_transaction_id")
            )
        }
    }

    /** Pool [accountId] transactions history */
    suspend fun pollHistory(
        params: HistoryParams, 
        accountId: Long
    ): List<BankAccountTransactionInfo> {
        return db.poolHistory(params, accountId, db::listenBank,  """
            SELECT
                bank_transaction_id
                ,transaction_date
                ,(amount).val AS amount_val
                ,(amount).frac AS amount_frac
                ,debtor_payto
                ,debtor_name
                ,creditor_payto
                ,creditor_name
                ,subject
                ,direction
            FROM bank_account_transactions WHERE
        """) {
            BankAccountTransactionInfo(
                row_id = it.getLong("bank_transaction_id"),
                date = it.getTalerTimestamp("transaction_date"),
                creditor_payto_uri = it.getBankPayto("creditor_payto", "creditor_name", db.ctx),
                debtor_payto_uri = it.getBankPayto("debtor_payto", "debtor_name", db.ctx),
                amount = it.getAmount("amount", db.bankCurrency),
                subject = it.getString("subject"),
                direction = it.getEnum("direction")
            )
        }
    }

    /** Query [accountId] history of incoming transactions to its account */
    suspend fun revenueHistory(
        params: HistoryParams, 
        accountId: Long
    ): List<RevenueIncomingBankTransaction> 
        = db.poolHistory(params, accountId, db::listenRevenue, """
            SELECT
                bank_transaction_id
                ,transaction_date
                ,(amount).val AS amount_val
                ,(amount).frac AS amount_frac
                ,debtor_payto
                ,debtor_name
                ,subject
            FROM bank_account_transactions WHERE direction='credit' AND
        """) {
            RevenueIncomingBankTransaction(
                row_id = it.getLong("bank_transaction_id"),
                date = it.getTalerTimestamp("transaction_date"),
                amount = it.getAmount("amount", db.bankCurrency),
                debit_account = it.getBankPayto("debtor_payto", "debtor_name", db.ctx),
                subject = it.getString("subject")
            )
        }
}