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

package tech.libeufin.nexus.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.common.*
import tech.libeufin.nexus.NexusConfig
import tech.libeufin.nexus.checkCurrency
import tech.libeufin.nexus.db.Database
import tech.libeufin.nexus.db.ExchangeDAO
import tech.libeufin.nexus.db.ExchangeDAO.TransferResult
import tech.libeufin.nexus.db.PaymentDAO.IncomingRegistrationResult
import tech.libeufin.nexus.ebics.randEbicsId
import tech.libeufin.nexus.iso20022.*
import java.time.Instant


fun Routing.wireGatewayApi(db: Database, cfg: NexusConfig) = conditional(cfg.wireGatewayApiCfg) {
    get("/taler-wire-gateway/config") {
        call.respond(WireGatewayConfig(
            currency = cfg.currency,
            support_account_check = true
        ))
    }
    auth(cfg.wireGatewayApiCfg) {
        post("/taler-wire-gateway/transfer") {
            val req = call.receive<TransferRequest>()
            cfg.checkCurrency(req.amount)
            req.credit_account.expectRequestIban()
            val endToEndId = randEbicsId()
            val res = db.exchange.transfer(
                req,
                endToEndId,
                Instant.now()
            )
            when (res) {
                TransferResult.RequestUidReuse -> throw conflict(
                    "request_uid used already",
                    TalerErrorCode.BANK_TRANSFER_REQUEST_UID_REUSED
                )
                TransferResult.WtidReuse -> throw conflict(
                    "wtid used already",
                    TalerErrorCode.BANK_TRANSFER_WTID_REUSED
                )
                is TransferResult.Success -> call.respond(
                    TransferResponse(
                        timestamp = res.timestamp,
                        row_id = res.id
                    )
                )
            }
        }
        get("/taler-wire-gateway/transfers") {
            val params = TransferParams.extract(call.request.queryParameters)
            val items = db.exchange.pageTransfer(params)

            if (items.isEmpty()) {
                call.respond(HttpStatusCode.NoContent)
            } else {
                call.respond(TransferList(items, cfg.ebics.payto))
            }
        }
        get("/taler-wire-gateway/transfers/{ROW_ID}") {
            val id = call.longPath("ROW_ID")
            val transfer = db.exchange.getTransfer(id) ?: throw notFound(
                "Transfer '$id' not found",
                TalerErrorCode.BANK_TRANSACTION_NOT_FOUND
            )
            call.respond(transfer)
        }
        suspend fun <T> ApplicationCall.historyEndpoint(
            reduce: (List<T>, String) -> Any, 
            dbLambda: suspend ExchangeDAO.(HistoryParams) -> List<T>
        ) {
            val params = HistoryParams.extract(this.request.queryParameters)
            val items = db.exchange.dbLambda(params)
            if (items.isEmpty()) {
                this.respond(HttpStatusCode.NoContent)
            } else {
                this.respond(reduce(items, cfg.ebics.payto))
            }
        }
        get("/taler-wire-gateway/history/incoming") {
            call.historyEndpoint(::IncomingHistory, ExchangeDAO::incomingHistory)
        }
        get("/taler-wire-gateway/history/outgoing") {
            call.historyEndpoint(::OutgoingHistory, ExchangeDAO::outgoingHistory)
        }
        get("/taler-wire-gateway/account/check") {
            throw notImplemented()
        }
        suspend fun ApplicationCall.addIncoming(
            amount: TalerAmount,
            debitAccount: Payto,
            subject: String,
            metadata: IncomingSubject
        ) {
            cfg.checkCurrency(amount)
            val debitAccount = debitAccount.expectRequestIban()
            val timestamp = Instant.now()
            val res = db.payment.registerTalerableIncoming(IncomingPayment(
                amount = amount,
                debtor = debitAccount,
                subject = subject,
                executionTime = timestamp,
                id = IncomingId(null, randEbicsId(), null)
            ), metadata)
            when (res) {
                IncomingRegistrationResult.ReservePubReuse -> throw conflict(
                    "reserve_pub used already",
                    TalerErrorCode.BANK_DUPLICATE_RESERVE_PUB_SUBJECT
                )
                is IncomingRegistrationResult.Success -> respond(
                    AddIncomingResponse(
                        timestamp = TalerProtocolTimestamp(timestamp),
                        row_id = res.id
                    )
                )
            }
        }
        post("/taler-wire-gateway/admin/add-incoming") {
            val req = call.receive<AddIncomingRequest>()
            call.addIncoming(
                amount = req.amount,
                debitAccount = req.debit_account,
                subject = "Manual incoming ${req.reserve_pub}",
                metadata = IncomingSubject.Reserve(req.reserve_pub)
            )
        }
        post("/taler-wire-gateway/admin/add-kycauth") {
            val req = call.receive<AddKycauthRequest>()
            call.addIncoming(
                amount = req.amount,
                debitAccount = req.debit_account,
                subject = "Manual incoming KYC:${req.account_pub}",
                metadata = IncomingSubject.Kyc(req.account_pub)
            )
        }
    }
}