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

// This file contains the Taler Wire Gateway API handlers.

package tech.libeufin.bank.api

import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.util.pipeline.*
import tech.libeufin.bank.*
import tech.libeufin.bank.auth.auth
import tech.libeufin.bank.auth.authAdmin
import tech.libeufin.bank.auth.username
import tech.libeufin.bank.db.Database
import tech.libeufin.bank.db.ExchangeDAO
import tech.libeufin.bank.db.ExchangeDAO.AddIncomingResult
import tech.libeufin.bank.db.ExchangeDAO.TransferResult
import tech.libeufin.common.*
import java.time.Instant


fun Routing.wireGatewayApi(db: Database, cfg: BankConfig) {
    get("/accounts/{USERNAME}/taler-wire-gateway/config") {
        call.respond(WireGatewayConfig(
            currency = cfg.regionalCurrency,
            support_account_check = false
        ))
    }
    auth(db, cfg.pwCrypto, TokenLogicalScope.readwrite_wiregateway, cfg.basicAuthCompat) {
        post("/accounts/{USERNAME}/taler-wire-gateway/transfer") {
            val req = call.receive<TransferRequest>()
            cfg.checkRegionalCurrency(req.amount)
            val res = db.exchange.transfer(
                req = req,
                username = call.username,
                timestamp = Instant.now()
            )
            when (res) {
                TransferResult.UnknownExchange -> throw unknownAccount(call.username)
                TransferResult.NotAnExchange -> throw notExchange(call.username)
                TransferResult.BothPartyAreExchange -> throw conflict(
                    "Wire transfer attempted with credit and debit party being both exchange account",
                    TalerErrorCode.BANK_ACCOUNT_IS_EXCHANGE
                )
                TransferResult.ReserveUidReuse -> throw conflict(
                    "request_uid used already",
                    TalerErrorCode.BANK_TRANSFER_REQUEST_UID_REUSED
                )
                TransferResult.WtidReuse -> throw conflict(
                    "wtid used already",
                    TalerErrorCode.BANK_TRANSFER_WTID_REUSED
                )
                TransferResult.BalanceInsufficient -> throw conflict(
                    "Insufficient balance for exchange",
                    TalerErrorCode.BANK_UNALLOWED_DEBIT
                )
                is TransferResult.Success -> call.respond(
                    TransferResponse(
                        timestamp = res.timestamp,
                        row_id = res.id
                    )
                )
            }
        }
    }
    auth(db, cfg.pwCrypto, TokenLogicalScope.readonly_wiregateway, cfg.basicAuthCompat) {
        suspend fun <T> ApplicationCall.historyEndpoint(
            reduce: (List<T>, String) -> Any, 
            dbLambda: suspend ExchangeDAO.(HistoryParams, Long) -> List<T>
        ) {
            val params = HistoryParams.extract(this.request.queryParameters)
            val bankAccount = this.bankInfo(db)
            
            if (!bankAccount.isTalerExchange)
                throw notExchange(username)

            val items = db.exchange.dbLambda(params, bankAccount.bankAccountId)

            if (items.isEmpty()) {
                this.respond(HttpStatusCode.NoContent)
            } else {
                this.respond(reduce(items, bankAccount.payto))
            }
        }
        get("/accounts/{USERNAME}/taler-wire-gateway/history/incoming") {
            call.historyEndpoint(::IncomingHistory, ExchangeDAO::incomingHistory)
        }
        get("/accounts/{USERNAME}/taler-wire-gateway/history/outgoing") {
            call.historyEndpoint(::OutgoingHistory, ExchangeDAO::outgoingHistory)
        }
        get("/accounts/{USERNAME}/taler-wire-gateway/transfers") {
            val params = TransferParams.extract(call.request.queryParameters)
            val bankAccount = call.bankInfo(db)
            if (!bankAccount.isTalerExchange)
                throw notExchange(call.username)
            
            if (params.status != null && params.status != TransferStatusState.success && params.status != TransferStatusState.permanent_failure) {
                call.respond(HttpStatusCode.NoContent)
            } else {
                val items = db.exchange.pageTransfer(params.page, bankAccount.bankAccountId, params.status)

                if (items.isEmpty()) {
                    call.respond(HttpStatusCode.NoContent)
                } else {
                    call.respond(TransferList(items, bankAccount.payto))
                }
            }
        }
        get("/accounts/{USERNAME}/taler-wire-gateway/transfers/{ROW_ID}") {
            val bankAccount = call.bankInfo(db)
            if (!bankAccount.isTalerExchange)
                throw notExchange(call.username)
            
            val txId = call.longPath("ROW_ID")
            val transfer = db.exchange.getTransfer(bankAccount.bankAccountId, txId) ?: throw notFound(
                "Transfer '$txId' not found",
                TalerErrorCode.BANK_TRANSACTION_NOT_FOUND
            )
            call.respond(transfer)
        }
        get("/accounts/{USERNAME}/taler-wire-gateway/account/check") {
            val bankAccount = call.bankInfo(db)
            if (!bankAccount.isTalerExchange)
                throw notExchange(call.username)
                
            val params = AccountCheckParams.extract(call.request.queryParameters)
            val account = params.account.expectRequestIban()
            
            val info = db.account.checkInfo(account) ?: throw unknownAccount(account.canonical)
            call.respond(info)
        }
    }
    authAdmin(db, cfg.pwCrypto, TokenLogicalScope.readwrite, cfg.basicAuthCompat) {
        suspend fun ApplicationCall.addIncoming(
            amount: TalerAmount,
            debitAccount: Payto,
            subject: String,
            metadata: IncomingSubject
        ) {
            cfg.checkRegionalCurrency(amount)
            val timestamp = Instant.now()
            val res = db.exchange.addIncoming(
                amount = amount,
                debitAccount = debitAccount,
                subject = subject,
                username = username,
                timestamp = timestamp,
                metadata = metadata
            )
            when (res) {
                AddIncomingResult.UnknownExchange -> throw unknownAccount(username)
                AddIncomingResult.NotAnExchange -> throw notExchange(username)
                AddIncomingResult.UnknownDebtor -> throw conflict(
                    "Debtor account $debitAccount was not found",
                    TalerErrorCode.BANK_UNKNOWN_DEBTOR
                )
                AddIncomingResult.BothPartyAreExchange -> throw conflict(
                    "Wire transfer attempted with credit and debit party being both exchange account",
                    TalerErrorCode.BANK_ACCOUNT_IS_EXCHANGE
                )
                AddIncomingResult.ReservePubReuse -> throw conflict(
                    "reserve_pub used already",
                    TalerErrorCode.BANK_DUPLICATE_RESERVE_PUB_SUBJECT
                )
                AddIncomingResult.BalanceInsufficient -> throw conflict(
                    "Insufficient balance for debitor",
                    TalerErrorCode.BANK_UNALLOWED_DEBIT
                )
                is AddIncomingResult.Success -> this.respond(
                    AddIncomingResponse(
                        timestamp = TalerProtocolTimestamp(timestamp),
                        row_id = res.id
                    )
                )
            }
        }
        post("/accounts/{USERNAME}/taler-wire-gateway/admin/add-incoming") {
            val req = call.receive<AddIncomingRequest>()
            call.addIncoming(
                amount = req.amount,
                debitAccount = req.debit_account,
                subject = "Admin incoming ${req.reserve_pub}",
                metadata = IncomingSubject.Reserve(req.reserve_pub)
            )
        }
        post("/accounts/{USERNAME}/taler-wire-gateway/admin/add-kycauth") {
            val req = call.receive<AddKycauthRequest>()
            call.addIncoming(
                amount = req.amount,
                debitAccount = req.debit_account,
                subject = "Admin incoming KYC:${req.account_pub}",
                metadata = IncomingSubject.Kyc(req.account_pub)
            )
        }
    }
}