/*
 * 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 kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeoutOrNull
import tech.libeufin.bank.*
import tech.libeufin.common.EddsaPublicKey
import tech.libeufin.common.Payto
import tech.libeufin.common.TalerAmount
import tech.libeufin.common.db.*
import tech.libeufin.common.micros
import java.time.Instant
import java.util.*

/** Data access logic for withdrawal operations */
class WithdrawalDAO(private val db: Database) {
    /** Result status of withdrawal operation creation */
    enum class WithdrawalCreationResult {
        Success,
        UnknownAccount,
        AccountIsExchange,
        BalanceInsufficient,
        BadAmount
    }

    /** Create a new withdrawal operation */
    suspend fun create(
        username: String,
        uuid: UUID,
        amount: TalerAmount?,
        suggested_amount: TalerAmount?,
        no_amount_to_wallet: Boolean,
        timestamp: Instant,
        wireTransferFees: TalerAmount,
        minAmount: TalerAmount,
        maxAmount: TalerAmount
    ): WithdrawalCreationResult = db.serializable(
        """
        SELECT
            out_account_not_found,
            out_account_is_exchange,
            out_balance_insufficient,
            out_bad_amount
        FROM create_taler_withdrawal(
            ?,?,
            ${optAmount(amount)},
            ${optAmount(suggested_amount)},
            ?, ?, (?, ?)::taler_amount, (?, ?)::taler_amount, (?, ?)::taler_amount
        );
        """
    ) {
        
        bind(username)
        bind(uuid)
        bind(amount)
        bind(suggested_amount)
        bind(no_amount_to_wallet)
        bind(timestamp.micros())
        bind(wireTransferFees)
        bind(minAmount)
        bind(maxAmount)
        one {
            when {
                it.getBoolean("out_account_not_found") -> WithdrawalCreationResult.UnknownAccount
                it.getBoolean("out_account_is_exchange") -> WithdrawalCreationResult.AccountIsExchange
                it.getBoolean("out_balance_insufficient") -> WithdrawalCreationResult.BalanceInsufficient
                it.getBoolean("out_bad_amount") -> WithdrawalCreationResult.BadAmount
                else -> WithdrawalCreationResult.Success
            }
        }
    }

    /** Abort withdrawal operation [uuid] */
    suspend fun abort(
        uuid: UUID
    ): AbortResult = db.serializable(
        """
        SELECT
            out_no_op,
            out_already_confirmed
        FROM abort_taler_withdrawal(?)
        """
    ) {
        bind(uuid)
        one {
            when {
                it.getBoolean("out_no_op") -> AbortResult.UnknownOperation
                it.getBoolean("out_already_confirmed") -> AbortResult.AlreadyConfirmed
                else -> AbortResult.Success
            }
        }
    }

    /** Result withdrawal operation selection */
    sealed interface WithdrawalSelectionResult {
        data class Success(val status: WithdrawalStatus): WithdrawalSelectionResult
        data object UnknownOperation: WithdrawalSelectionResult
        data object AlreadySelected: WithdrawalSelectionResult
        data object ReservePubReuse: WithdrawalSelectionResult
        data object UnknownAccount: WithdrawalSelectionResult
        data object AccountIsNotExchange: WithdrawalSelectionResult
        data object AmountDiffers: WithdrawalSelectionResult
        data object BalanceInsufficient: WithdrawalSelectionResult
        data object BadAmount: WithdrawalSelectionResult
        data object AlreadyAborted: WithdrawalSelectionResult
    }

    /** Set details ([exchangePayto] & [reservePub] & [amount]) for withdrawal operation [uuid] */
    suspend fun setDetails(
        uuid: UUID,
        exchangePayto: Payto,
        reservePub: EddsaPublicKey,
        amount: TalerAmount?,
        wireTransferFees: TalerAmount,
        minAmount: TalerAmount,
        maxAmount: TalerAmount
    ): WithdrawalSelectionResult = db.serializable(
        """
        SELECT
            out_no_op,
            out_already_selected,
            out_reserve_pub_reuse,
            out_account_not_found,
            out_account_is_not_exchange,
            out_status,
            out_amount_differs,
            out_balance_insufficient,
            out_bad_amount,
            out_aborted
        FROM select_taler_withdrawal(
            ?, ?, ?, ?,
            ${optAmount(amount)},
            (?,?)::taler_amount, (?,?)::taler_amount, (?,?)::taler_amount
        );
        """
    ) {
        bind(uuid)
        bind(reservePub)
        bind("Taler withdrawal $reservePub")
        bind(exchangePayto.canonical)
        bind(amount)
        bind(wireTransferFees)
        bind(minAmount)
        bind(maxAmount)
        one {
            when {
                it.getBoolean("out_aborted") -> WithdrawalSelectionResult.AlreadyAborted
                it.getBoolean("out_balance_insufficient") -> WithdrawalSelectionResult.BalanceInsufficient
                it.getBoolean("out_bad_amount") -> WithdrawalSelectionResult.BadAmount
                it.getBoolean("out_no_op") -> WithdrawalSelectionResult.UnknownOperation
                it.getBoolean("out_already_selected") -> WithdrawalSelectionResult.AlreadySelected
                it.getBoolean("out_amount_differs") -> WithdrawalSelectionResult.AmountDiffers
                it.getBoolean("out_reserve_pub_reuse") -> WithdrawalSelectionResult.ReservePubReuse
                it.getBoolean("out_account_not_found") -> WithdrawalSelectionResult.UnknownAccount
                it.getBoolean("out_account_is_not_exchange") -> WithdrawalSelectionResult.AccountIsNotExchange
                else -> WithdrawalSelectionResult.Success(it.getEnum("out_status"))
            }
        }
    }

    /** Result status of withdrawal operation confirmation */
    enum class WithdrawalConfirmationResult {
        Success,
        UnknownOperation,
        BalanceInsufficient,
        BadAmount,
        NotSelected,
        AlreadyAborted,
        TanRequired,
        MissingAmount,
        AmountDiffers,
        ReservePubReuse
    }

    /** Confirm withdrawal operation [uuid] */
    suspend fun confirm(
        username: String,
        uuid: UUID,
        timestamp: Instant,
        amount: TalerAmount?,
        is2fa: Boolean,
        wireTransferFees: TalerAmount,
        minAmount: TalerAmount,
        maxAmount: TalerAmount
    ): WithdrawalConfirmationResult = db.serializable(
        """
        SELECT
            out_no_op,
            out_balance_insufficient,
            out_bad_amount,
            out_not_selected,
            out_aborted,
            out_tan_required,
            out_missing_amount,
            out_amount_differs,
            out_reserve_pub_reuse
        FROM confirm_taler_withdrawal(
            ?,?,?,?,(?,?)::taler_amount,(?,?)::taler_amount,(?,?)::taler_amount, ${optAmount(amount)}
        );
        """
    ) {
        bind(username)
        bind(uuid)
        bind(timestamp)
        bind(is2fa)
        bind(wireTransferFees)
        bind(minAmount)
        bind(maxAmount)
        bind(amount)
        one {
            when {
                it.getBoolean("out_no_op") -> WithdrawalConfirmationResult.UnknownOperation
                it.getBoolean("out_balance_insufficient") -> WithdrawalConfirmationResult.BalanceInsufficient
                it.getBoolean("out_bad_amount") -> WithdrawalConfirmationResult.BadAmount
                it.getBoolean("out_not_selected") -> WithdrawalConfirmationResult.NotSelected
                it.getBoolean("out_aborted") -> WithdrawalConfirmationResult.AlreadyAborted
                it.getBoolean("out_tan_required") -> WithdrawalConfirmationResult.TanRequired
                it.getBoolean("out_missing_amount") -> WithdrawalConfirmationResult.MissingAmount
                it.getBoolean("out_amount_differs") -> WithdrawalConfirmationResult.AmountDiffers
                it.getBoolean("out_reserve_pub_reuse") -> WithdrawalConfirmationResult.ReservePubReuse
                else -> WithdrawalConfirmationResult.Success
            }
        }
    }

    /** Get withdrawal operation [uuid] linked account username */
    suspend fun getUsername(uuid: UUID): String? = db.serializable(
        """
        SELECT username
        FROM taler_withdrawal_operations
            JOIN bank_accounts ON wallet_bank_account=bank_account_id
            JOIN customers ON customer_id=owning_customer_id
        WHERE withdrawal_uuid=?
        """
    ) {
        bind(uuid)
        oneOrNull { it.getString(1) }
    }

    private suspend fun <T> poll(
        uuid: UUID, 
        params: StatusParams, 
        status: (T) -> WithdrawalStatus,
        load: suspend () -> T?
    ): T? {
        return if (params.polling.timeout_ms > 0) {
            db.listenWithdrawals(uuid) { flow ->
                coroutineScope {
                    // Start buffering notification before loading transactions to not miss any
                    val polling = launch {
                        withTimeoutOrNull(params.polling.timeout_ms) {
                            flow.first { it != params.old_state }
                        }
                    }    
                    // Initial loading
                    val init = load()
                    // Long polling if there is no operation or its not confirmed
                    if (init?.run { status(this) == params.old_state } != false) {
                        polling.join()
                        load()
                    } else {
                        polling.cancel()
                        init
                    }
                }
            }
        } else {
            load()
        }
    }

    /** Pool public info of operation [uuid] */
    suspend fun pollInfo(uuid: UUID, params: StatusParams): WithdrawalPublicInfo? = 
        poll(uuid, params, status = { it.status }) {
            db.serializable(
                """
                SELECT
                CASE 
                    WHEN confirmation_done THEN 'confirmed'
                    WHEN aborted THEN 'aborted'
                    WHEN selection_done THEN 'selected'
                    ELSE 'pending'
                END as status
                ,(amount).val as amount_val
                ,(amount).frac as amount_frac
                ,(suggested_amount).val as suggested_amount_val
                ,(suggested_amount).frac as suggested_amount_frac
                ,selection_done     
                ,aborted     
                ,confirmation_done     
                ,reserve_pub
                ,wallet_user.username
                ,no_amount_to_wallet
                ,exchange_account.internal_payto as exchange_payto
                ,exchange_user.name as exchange_name
                FROM taler_withdrawal_operations
                    JOIN bank_accounts AS wallet_account ON wallet_bank_account=wallet_account.bank_account_id
                    JOIN customers AS wallet_user ON wallet_user.customer_id=wallet_account.owning_customer_id
                    LEFT JOIN bank_accounts AS exchange_account ON exchange_bank_account=exchange_account.bank_account_id
                    LEFT JOIN customers AS exchange_user ON exchange_user.customer_id=exchange_account.owning_customer_id
                WHERE withdrawal_uuid=?
                """
            ) {
                bind(uuid)
                oneOrNull {
                    WithdrawalPublicInfo(
                        status = it.getEnum("status"),
                        amount = it.getOptAmount("amount", db.bankCurrency),
                        suggested_amount = it.getOptAmount("suggested_amount", db.bankCurrency),
                        username = it.getString("username"),
                        selected_exchange_account = it.getOptBankPayto("exchange_payto", "exchange_name", db.ctx),
                        selected_reserve_pub = it.getBytes("reserve_pub")?.run(::EddsaPublicKey),
                        no_amount_to_wallet = it.getBoolean("no_amount_to_wallet")
                    )
                }
            }
        }

    /** Pool public status of operation [uuid] */
    suspend fun pollStatus(
        uuid: UUID,
        params: StatusParams,
        wire: WireMethod,
        maxAmount: TalerAmount
    ): BankWithdrawalOperationStatus? =
        poll(uuid, params, status = { it.status }) {
            db.serializable(
                """
                SELECT
                    CASE 
                    WHEN confirmation_done THEN 'confirmed'
                    WHEN aborted THEN 'aborted'
                    WHEN selection_done THEN 'selected'
                    ELSE 'pending'
                    END as status
                    ,(amount).val as amount_val
                    ,(amount).frac as amount_frac
                    ,(suggested_amount).val as suggested_amount_val
                    ,(suggested_amount).frac as suggested_amount_frac
                    ,selection_done     
                    ,aborted     
                    ,confirmation_done      
                    ,wallet_account.internal_payto
                    ,wallet_user.name
                    ,reserve_pub
                    ,exchange_account.internal_payto as exchange_payto
                    ,exchange_user.name as exchange_name
                    ,(max_amount).val as max_amount_val
                    ,(max_amount).frac as max_amount_frac
                    ,no_amount_to_wallet
                FROM taler_withdrawal_operations
                    JOIN bank_accounts AS wallet_account ON wallet_bank_account=wallet_account.bank_account_id
                    JOIN customers AS wallet_user ON wallet_user.customer_id=wallet_account.owning_customer_id
                    LEFT JOIN bank_accounts AS exchange_account ON exchange_bank_account=exchange_account.bank_account_id
                    LEFT JOIN customers AS exchange_user ON exchange_user.customer_id=exchange_account.owning_customer_id
                    ,account_max_amount(wallet_account.bank_account_id, (?, ?)::taler_amount) AS max_amount
                WHERE withdrawal_uuid=?
                """
            ) {
                bind(maxAmount)
                bind(uuid)
                oneOrNull {
                    BankWithdrawalOperationStatus(
                        status = it.getEnum("status"),
                        amount = it.getOptAmount("amount", db.bankCurrency),
                        suggested_amount = it.getOptAmount("suggested_amount", db.bankCurrency),
                        max_amount = it.getAmount("max_amount", db.bankCurrency),
                        selection_done = it.getBoolean("selection_done"),
                        transfer_done = it.getBoolean("confirmation_done"),
                        aborted = it.getBoolean("aborted"),
                        sender_wire = it.getBankPayto("internal_payto", "name", db.ctx),
                        confirm_transfer_url = null,
                        suggested_exchange = null,
                        selected_exchange_account = it.getOptBankPayto("exchange_payto", "exchange_name", db.ctx),
                        no_amount_to_wallet = it.getBoolean("no_amount_to_wallet"),
                        selected_reserve_pub = it.getBytes("reserve_pub")?.run(::EddsaPublicKey),
                        wire_types = listOf(
                            when (wire) {
                                WireMethod.IBAN -> "iban"
                                WireMethod.X_TALER_BANK -> "x-taler-bank"
                            } 
                        ),
                        currency = db.bankCurrency
                    )
                }
            }
        }
}