/*
 * 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 java.time.Instant

/** Data access logic for exchange specific logic */
class ExchangeDAO(private val db: Database) {
    /** Query history of taler incoming transactions  */
    suspend fun incomingHistory(
        params: HistoryParams
    ): List<IncomingBankTransaction> 
        = db.poolHistoryGlobal(params, db::listenIncoming, """
            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
                ,type
                ,metadata
                ,authorization_pub
                ,authorization_sig
            FROM talerable_incoming_transactions
                JOIN incoming_transactions USING(incoming_transaction_id)
            WHERE
        """, "incoming_transaction_id") {
            when (it.getEnum<IncomingType>("type")) {
                IncomingType.reserve -> IncomingReserveTransaction(
                    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"),
                    reserve_pub = EddsaPublicKey(it.getBytes("metadata")),
                    authorization_pub = it.getOptKey("authorization_pub"),
                    authorization_sig = it.getOptSig("authorization_sig")
                )
                IncomingType.kyc -> IncomingKycAuthTransaction(
                    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"),
                    account_pub = EddsaPublicKey(it.getBytes("metadata")),
                    authorization_pub = it.getOptKey("authorization_pub"),
                    authorization_sig = it.getOptSig("authorization_sig")
                )
                IncomingType.map -> throw UnsupportedOperationException()
            }
        }

    /** Query exchange history of taler outgoing transactions  */
    suspend fun outgoingHistory(
        params: HistoryParams
    ): List<OutgoingTransaction>
        = db.poolHistoryGlobal(params, db::listenOutgoing,  """
            SELECT
                outgoing_transaction_id
                ,execution_time
                ,(amount).val AS amount_val
                ,(amount).frac AS amount_frac
                ,(debit_fee).val AS debit_fee_val
                ,(debit_fee).frac AS debit_fee_frac
                ,credit_payto
                ,wtid
                ,exchange_base_url
                ,metadata
            FROM talerable_outgoing_transactions
                JOIN outgoing_transactions USING(outgoing_transaction_id)
            WHERE
        """, "outgoing_transaction_id") {
            OutgoingTransaction(
                row_id = it.getLong("outgoing_transaction_id"),
                date = it.getTalerTimestamp("execution_time"),
                amount = it.getAmount("amount", db.currency),
                debit_fee = it.getAmount("debit_fee", db.currency).notZeroOrNull(),
                credit_account = it.getString("credit_payto"),
                wtid = ShortHashCode(it.getBytes("wtid")),
                exchange_base_url = it.getString("exchange_base_url"),
                metadata = it.getString("metadata"),
            )
        }

    /** Result of taler transfer transaction creation */
    sealed interface TransferResult {
        /** Transaction [id] and wire transfer [timestamp] */
        data class Success(val id: Long, val timestamp: TalerTimestamp): TransferResult
        data object RequestUidReuse: TransferResult
        data object WtidReuse: TransferResult
    }

    /** Perform a Taler transfer */
    suspend fun transfer(
        req: TransferRequest,
        endToEndId: String,
        timestamp: Instant
    ): TransferResult = db.serializable(
        """
        SELECT
            out_request_uid_reuse
            ,out_wtid_reuse
            ,out_tx_row_id
            ,out_timestamp
        FROM taler_transfer (
            ?, ?, ?,
            (?,?)::taler_amount,
            ?, ?, ?, ?, ?
        )
        """
    ) {
        val subject = fmtOutgoingSubject(req.wtid, req.exchange_base_url, req.metadata)

        bind(req.request_uid)
        bind(req.wtid)
        bind(subject)
        bind(req.amount)
        bind(req.exchange_base_url.toString())
        bind(req.metadata)
        bind(req.credit_account.toString())
        bind(endToEndId)
        bind(timestamp)
        one {
            when {
                it.getBoolean("out_request_uid_reuse") -> TransferResult.RequestUidReuse
                it.getBoolean("out_wtid_reuse") -> TransferResult.WtidReuse
                else -> TransferResult.Success(
                    id = it.getLong("out_tx_row_id"),
                    timestamp = it.getTalerTimestamp("out_timestamp")
                )
            }
        }
    }

    /** Get status of transfer [id] */
    suspend fun getTransfer(
        id: Long
    ): TransferStatus? = db.serializable(
        """
        SELECT
            wtid
            ,exchange_base_url
            ,metadata
            ,(amount).val AS amount_val
            ,(amount).frac AS amount_frac
            ,credit_payto
            ,initiation_time
            ,status
            ,status_msg
        FROM transfer_operations
            JOIN initiated_outgoing_transactions USING (initiated_outgoing_transaction_id)
        WHERE initiated_outgoing_transaction_id=?
        """
    ) {
        bind(id)
        oneOrNull {
            TransferStatus(
                status = it.getEnum<SubmissionState>("status").toTransferStatus(),
                status_msg = it.getString("status_msg"),
                amount = it.getAmount("amount", db.currency),
                origin_exchange_url = it.getString("exchange_base_url"),
                metadata = it.getString("metadata"),
                wtid = ShortHashCode(it.getBytes("wtid")),
                credit_account = it.getString("credit_payto"),
                timestamp = it.getTalerTimestamp("initiation_time"),
            )
        }
    }

    /** Get a page of transfers status */
    suspend fun pageTransfer(
        params: TransferParams
    ): List<TransferListStatus> = db.page(
        params.page,
        "initiated_outgoing_transaction_id",
        """
        SELECT
            initiated_outgoing_transaction_id
            ,(amount).val AS amount_val
            ,(amount).frac AS amount_frac
            ,status
            ,credit_payto
            ,initiation_time
        FROM transfer_operations
            JOIN initiated_outgoing_transactions USING (initiated_outgoing_transaction_id)
        WHERE ${
            when (params.status) {
                null -> ""
                TransferStatusState.pending -> "(status=?::submission_state OR status=?::submission_state) AND"
                else -> "status=?::submission_state AND"
            }               
        }
        """,
        {
            when (params.status) {
                null -> {}
                TransferStatusState.pending -> {
                    bind(SubmissionState.pending)
                    bind(SubmissionState.unsubmitted)
                }
                else -> {
                    bind(params.status)
                }
            }
        }
    ) {
        TransferListStatus(
            row_id = it.getLong("initiated_outgoing_transaction_id"),
            status = it.getEnum<SubmissionState>("status").toTransferStatus(),
            amount = it.getAmount("amount", db.currency),
            credit_account = it.getString("credit_payto"),
            timestamp = it.getTalerTimestamp("initiation_time"),
        )
    }
}