/*
 * 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/>
 */

// 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.github.smiley4.ktoropenapi.*
import tech.libeufin.bank.*
import tech.libeufin.bank.auth.auth
import tech.libeufin.bank.auth.authAdmin
import tech.libeufin.bank.auth.pathUsername
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", {
        operationId = "getWireGatewayConfig"
        description = "Get wire gateway configuration"
        tags = listOf("Wire Gateway")
        request {
            pathParameter<String>("USERNAME") { description = "Account username" }
        }
        response {
            code(HttpStatusCode.OK) { description = "Wire gateway configuration"; body<WireGatewayConfig>() }
        }
    }) {
        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", {
            operationId = "createWireGatewayTransfer"
            description = "Initiate a wire transfer from an exchange account"
            tags = listOf("Wire Gateway")
            protected = true
            securitySchemeNames("bearerAuth", "basicAuth")
            request {
                pathParameter<String>("USERNAME") { description = "Account username" }
                body<TransferRequest>()
            }
            response {
                code(HttpStatusCode.OK) { description = "Transfer initiated"; body<TransferResponse>() }
                code(HttpStatusCode.Conflict) { description = "Transfer conflict" }
            }
        }) {
            val req = call.receive<TransferRequest>()
            cfg.checkRegionalCurrency(req.amount)
            val res = db.exchange.transfer(
                req = req,
                username = call.pathUsername,
                timestamp = Instant.now(),
                conversion = cfg.allowConversion
            )
            when (res) {
                TransferResult.UnknownExchange -> throw unknownAccount(call.pathUsername)
                TransferResult.NotAnExchange -> throw notExchange(call.pathUsername)
                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
                )

                TransferResult.AdminCreditor -> throw conflict(
                    "Cannot transfer money to admin account when conversion is disabled",
                    TalerErrorCode.BANK_ADMIN_CREDITOR
                )

                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(pathUsername)

            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", {
            operationId = "getIncomingHistory"
            description = "Get incoming wire transfer history"
            tags = listOf("Wire Gateway")
            protected = true
            securitySchemeNames("bearerAuth", "basicAuth")
            request {
                pathParameter<String>("USERNAME") { description = "Account username" }
                queryParameter<Int>("limit") {
                    description = "Maximum number of results to return (default: -20, negative means descending)"
                    required = false
                }
                queryParameter<Long>("offset") {
                    description = "Row ID offset for pagination"
                    required = false
                }
                queryParameter<Long>("timeout_ms") {
                    description = "Timeout for long polling in milliseconds (default: 0, no long polling)"
                    required = false
                }
            }
            response {
                code(HttpStatusCode.OK) { description = "Incoming transfer history"; body<IncomingHistory>() }
                code(HttpStatusCode.NoContent) { description = "No incoming transfers found" }
            }
        }) {
            call.historyEndpoint(::IncomingHistory, ExchangeDAO::incomingHistory)
        }
        get("/accounts/{USERNAME}/taler-wire-gateway/history/outgoing", {
            operationId = "getOutgoingHistory"
            description = "Get outgoing wire transfer history"
            tags = listOf("Wire Gateway")
            protected = true
            securitySchemeNames("bearerAuth", "basicAuth")
            request {
                pathParameter<String>("USERNAME") { description = "Account username" }
                queryParameter<Int>("limit") {
                    description = "Maximum number of results to return (default: -20, negative means descending)"
                    required = false
                }
                queryParameter<Long>("offset") {
                    description = "Row ID offset for pagination"
                    required = false
                }
                queryParameter<Long>("timeout_ms") {
                    description = "Timeout for long polling in milliseconds (default: 0, no long polling)"
                    required = false
                }
            }
            response {
                code(HttpStatusCode.OK) { description = "Outgoing transfer history"; body<OutgoingHistory>() }
                code(HttpStatusCode.NoContent) { description = "No outgoing transfers found" }
            }
        }) {
            call.historyEndpoint(::OutgoingHistory, ExchangeDAO::outgoingHistory)
        }
        get("/accounts/{USERNAME}/taler-wire-gateway/transfers", {
            operationId = "listWireGatewayTransfers"
            description = "List wire gateway transfers"
            tags = listOf("Wire Gateway")
            protected = true
            securitySchemeNames("bearerAuth", "basicAuth")
            request {
                pathParameter<String>("USERNAME") { description = "Account username" }
                queryParameter<Int>("limit") {
                    description = "Maximum number of results to return (default: -20, negative means descending)"
                    required = false
                }
                queryParameter<Long>("offset") {
                    description = "Row ID offset for pagination"
                    required = false
                }
                queryParameter<String>("status") {
                    description = "Filter by transfer status (pending, transient_failure, permanent_failure, success)"
                    required = false
                }
            }
            response {
                code(HttpStatusCode.OK) { description = "List of transfers"; body<TransferList>() }
                code(HttpStatusCode.NoContent) { description = "No transfers found" }
            }
        }) {
            val params = TransferParams.extract(call.request.queryParameters)
            val bankAccount = call.bankInfo(db)
            if (!bankAccount.isTalerExchange)
                throw notExchange(call.pathUsername)

            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}", {
            operationId = "getWireGatewayTransfer"
            description = "Get a specific wire gateway transfer"
            tags = listOf("Wire Gateway")
            protected = true
            securitySchemeNames("bearerAuth", "basicAuth")
            request {
                pathParameter<String>("USERNAME") { description = "Account username" }
                pathParameter<Long>("ROW_ID") { description = "Transfer row ID" }
            }
            response {
                code(HttpStatusCode.OK) { description = "Transfer details"; body<TransferStatus>() }
                code(HttpStatusCode.NotFound) { description = "Transfer not found" }
            }
        }) {
            val bankAccount = call.bankInfo(db)
            if (!bankAccount.isTalerExchange)
                throw notExchange(call.pathUsername)

            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", {
            operationId = "checkAccount"
            description = "Check if an account exists"
            tags = listOf("Wire Gateway")
            protected = true
            securitySchemeNames("bearerAuth", "basicAuth")
            request {
                pathParameter<String>("USERNAME") { description = "Account username" }
                queryParameter<String>("account") {
                    description = "Payto URI of the account to check (required)"
                    required = true
                }
            }
            response {
                code(HttpStatusCode.OK) { description = "Account information"; body<AccountInfo>() }
                code(HttpStatusCode.NotFound) { description = "Account not found" }
            }
        }) {
            val bankAccount = call.bankInfo(db)
            if (!bankAccount.isTalerExchange)
                throw notExchange(call.pathUsername)

            val params = AccountCheckParams.extract(call.request.queryParameters)
            val account = params.account.expectIban()

            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 = pathUsername,
                timestamp = timestamp,
                metadata = metadata
            )
            when (res) {
                AddIncomingResult.UnknownExchange -> throw unknownAccount(pathUsername)
                AddIncomingResult.NotAnExchange -> throw notExchange(pathUsername)
                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
                )
                AddIncomingResult.MappingReuse -> throw conflict(
                    "authorization_pub used already",
                    TalerErrorCode.BANK_TRANSFER_MAPPING_REUSED
                )
                AddIncomingResult.UnknownMapping -> throw conflict(
                    "authorization_pub unknown",
                    TalerErrorCode.BANK_TRANSFER_MAPPING_UNKNOWN
                )
                is AddIncomingResult.Success -> this.respond(
                    AddIncomingResponse(
                        timestamp = TalerTimestamp(timestamp),
                        row_id = res.id
                    )
                )
            }
        }
        post("/accounts/{USERNAME}/taler-wire-gateway/admin/add-incoming", {
            operationId = "addIncoming"
            description = "Add an incoming wire transfer (admin)"
            tags = listOf("Wire Gateway")
            protected = true
            securitySchemeNames("bearerAuth", "basicAuth")
            request {
                pathParameter<String>("USERNAME") { description = "Account username" }
                body<AddIncomingRequest>()
            }
            response {
                code(HttpStatusCode.OK) { description = "Incoming transfer added"; body<AddIncomingResponse>() }
                code(HttpStatusCode.Conflict) { description = "Transfer conflict" }
            }
        }) {
            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", {
            operationId = "addKycauth"
            description = "Add an incoming KYC auth wire transfer (admin)"
            tags = listOf("Wire Gateway")
            protected = true
            securitySchemeNames("bearerAuth", "basicAuth")
            request {
                pathParameter<String>("USERNAME") { description = "Account username" }
                body<AddKycauthRequest>()
            }
            response {
                code(HttpStatusCode.OK) { description = "KYC auth transfer added"; body<AddIncomingResponse>() }
                code(HttpStatusCode.Conflict) { description = "Transfer conflict" }
            }
        }) {
            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)
            )
        }
        post("/accounts/{USERNAME}/taler-wire-gateway/admin/add-mapped", {
            operationId = "addMapped"
            description = "Add an incoming mapped wire transfer (admin)"
            tags = listOf("Wire Gateway")
            protected = true
            securitySchemeNames("bearerAuth", "basicAuth")
            request {
                pathParameter<String>("USERNAME") { description = "Account username" }
                body<AddMappedRequest>()
            }
            response {
                code(HttpStatusCode.OK) { description = "Mapped transfer added"; body<AddIncomingResponse>() }
                code(HttpStatusCode.Conflict) { description = "Transfer conflict" }
            }
        }) {
            val req = call.receive<AddMappedRequest>()
            call.addIncoming(
                amount = req.amount,
                debitAccount = req.debit_account,
                subject = "Manual incoming MAP:${req.authorization_pub}",
                metadata = IncomingSubject.Map(req.authorization_pub)
            )
        }
    }
}
