/*
 * 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/>
 */
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.*
import tech.libeufin.bank.db.ConversionDAO
import tech.libeufin.bank.db.ConversionDAO.ConversionResult
import tech.libeufin.bank.db.Database
import tech.libeufin.common.*

private suspend fun ApplicationCall.config(db: Database, cfg: BankConfig) {
    respond(
        ConversionConfig(
            regional_currency = cfg.regionalCurrency,
            regional_currency_specification = cfg.regionalCurrencySpec,
            fiat_currency = cfg.fiatCurrency!!,
            fiat_currency_specification = cfg.fiatCurrencySpec!!,
            conversion_rate = db.conversion.getDefaultRate()
        )
    )
}
private suspend fun ApplicationCall.setGlobal(db: Database, cfg: BankConfig) {
    val req = receive<ConversionRate>()
    req.check(cfg)
    db.conversion.updateConfig(req)
    respond(HttpStatusCode.NoContent)
}
fun Routing.conversionApi(db: Database, cfg: BankConfig) = conditional(cfg.allowConversion) {
    get("/conversion-info/config", {
        operationId = "getConversionConfig"
        description = "Get conversion configuration"
        tags = listOf("Conversion")
        response {
            code(HttpStatusCode.OK) { description = "Conversion configuration"; body<ConversionConfig>() }
        }
    }) { call.config(db, cfg) }
    get("/conversion-rate-classes/{CLASS_ID}/conversion-info/config", {
        operationId = "getConversionConfigForClass"
        description = "Get conversion configuration for a rate class"
        tags = listOf("Conversion")
        request {
            pathParameter<Long>("CLASS_ID") { description = "Conversion rate class ID" }
        }
        response {
            code(HttpStatusCode.OK) { description = "Conversion configuration for rate class"; body<ConversionConfig>() }
        }
    }) { call.config(db, cfg) }
    get("/accounts/{USERNAME}/conversion-info/config", {
        operationId = "getConversionConfigForAccount"
        description = "Get conversion configuration for an account"
        tags = listOf("Conversion")
        request {
            pathParameter<String>("USERNAME") { description = "Account username" }
        }
        response {
            code(HttpStatusCode.OK) { description = "Conversion configuration for account"; body<ConversionConfig>() }
        }
    }) { call.config(db, cfg) }
    authAdmin(db, cfg.pwCrypto, TokenLogicalScope.readwrite, cfg.basicAuthCompat) {
        post("/conversion-info/conversion-rate", {
            operationId = "setGlobalConversionRate"
            description = "Set global conversion rate"
            tags = listOf("Conversion")
            protected = true
            securitySchemeNames("bearerAuth", "basicAuth")
            request {
                body<ConversionRate>()
            }
            response {
                code(HttpStatusCode.NoContent) { description = "Conversion rate updated" }
            }
        }) { call.setGlobal(db, cfg) }
        post("/accounts/{USERNAME}/conversion-info/conversion-rate", {
            operationId = "setAccountConversionRate"
            description = "Set conversion rate for an account"
            tags = listOf("Conversion")
            protected = true
            securitySchemeNames("bearerAuth", "basicAuth")
            request {
                pathParameter<String>("USERNAME") { description = "Account username" }
                body<ConversionRate>()
            }
            response {
                code(HttpStatusCode.NoContent) { description = "Conversion rate updated" }
            }
        }) { call.setGlobal(db, cfg) }
        post("/conversion-rate-classes/{CLASS_ID}/conversion-info/conversion-rate", {
            operationId = "setClassConversionRate"
            description = "Set conversion rate for a rate class"
            tags = listOf("Conversion")
            protected = true
            securitySchemeNames("bearerAuth", "basicAuth")
            request {
                pathParameter<Long>("CLASS_ID") { description = "Conversion rate class ID" }
                body<ConversionRate>()
            }
            response {
                code(HttpStatusCode.NoContent) { description = "Conversion rate updated" }
            }
        }) { call.setGlobal(db, cfg) }
    }
    suspend fun ApplicationCall.convert(
        input: TalerAmount, 
        conversion: suspend ConversionDAO.(TalerAmount) -> ConversionResult,
        output: (TalerAmount) -> ConversionResponse
    ) {
        when (val res = db.conversion.(conversion)(input)) {
            is ConversionResult.Success -> respond(output(res.converted))
            ConversionResult.ToSmall -> throw conflict(
                "$input is too small to be converted",
                TalerErrorCode.BANK_BAD_CONVERSION
            )
            ConversionResult.IsExchange -> throw conflict(
                "exchange accounts cannot cashout",
                TalerErrorCode.BANK_ACCOUNT_IS_EXCHANGE
            )
            ConversionResult.NotExchange -> throw conflict(
                "only exchange accounts can cashin",
                TalerErrorCode.BANK_ACCOUNT_IS_NOT_EXCHANGE
            )
        }
    }
    get("/conversion-info/rate", {
        operationId = "getDefaultConversionRate"
        description = "Get default conversion rate"
        tags = listOf("Conversion")
        response {
            code(HttpStatusCode.OK) { description = "Default conversion rate"; body<ConversionRate>() }
        }
    }) {
        call.respond(db.conversion.getDefaultRate())
    }
    get("/conversion-info/cashout-rate", {
        operationId = "getDefaultCashoutRate"
        description = "Calculate cashout conversion rate"
        tags = listOf("Conversion")
        request {
            queryParameter<String>("amount_debit") {
                description = "Amount to convert from regional currency (mutually exclusive with amount_credit)"
                required = false
            }
            queryParameter<String>("amount_credit") {
                description = "Amount to convert to fiat currency (mutually exclusive with amount_debit)"
                required = false
            }
        }
        response {
            code(HttpStatusCode.OK) { description = "Cashout conversion result"; body<ConversionResponse>() }
        }
    }) {
        val params = RateParams.extract(call.request.queryParameters)

        params.debit?.let { cfg.checkRegionalCurrency(it) }
        params.credit?.let { cfg.checkFiatCurrency(it) }

        if (params.debit != null) {
            call.convert(params.debit, ConversionDAO::defaultToCashout) {
                ConversionResponse(params.debit, it)
            }
        } else {
            call.convert(params.credit!!, ConversionDAO::defaultFromCashout) {
                ConversionResponse(it, params.credit)
            }
        }
    }
    get("/conversion-info/cashin-rate", {
        operationId = "getDefaultCashinRate"
        description = "Calculate cashin conversion rate"
        tags = listOf("Conversion")
        request {
            queryParameter<String>("amount_debit") {
                description = "Amount to convert from fiat currency (mutually exclusive with amount_credit)"
                required = false
            }
            queryParameter<String>("amount_credit") {
                description = "Amount to convert to regional currency (mutually exclusive with amount_debit)"
                required = false
            }
        }
        response {
            code(HttpStatusCode.OK) { description = "Cashin conversion result"; body<ConversionResponse>() }
        }
    }) {
        val params = RateParams.extract(call.request.queryParameters)

        params.debit?.let { cfg.checkFiatCurrency(it) }
        params.credit?.let { cfg.checkRegionalCurrency(it) }

        if (params.debit != null) {
            call.convert(params.debit, ConversionDAO::defaultToCashin) {
                ConversionResponse(params.debit, it)
            }
        } else {
            call.convert(params.credit!!, ConversionDAO::defaultFromCashin) {
                ConversionResponse(it, params.credit)
            }
        }
    }
    authAdmin(db, cfg.pwCrypto, TokenLogicalScope.readonly, cfg.basicAuthCompat) {
        get("/conversion-rate-classes/{CLASS_ID}/conversion-info/rate", {
            operationId = "getClassConversionRate"
            description = "Get conversion rate for a rate class"
            tags = listOf("Conversion")
            protected = true
            securitySchemeNames("bearerAuth", "basicAuth")
            request {
                pathParameter<Long>("CLASS_ID") { description = "Conversion rate class ID" }
            }
            response {
                code(HttpStatusCode.OK) { description = "Class conversion rate"; body<ConversionRate>() }
            }
        }) {
            val id = call.longPath("CLASS_ID")
            val rate = db.conversion.getClassRate(id)
            call.respond(rate)
        }
        get("/conversion-rate-classes/{CLASS_ID}/conversion-info/cashout-rate", {
            operationId = "getClassCashoutRate"
            description = "Calculate cashout conversion rate for a rate class"
            tags = listOf("Conversion")
            protected = true
            securitySchemeNames("bearerAuth", "basicAuth")
            request {
                pathParameter<Long>("CLASS_ID") { description = "Conversion rate class ID" }
                queryParameter<String>("amount_debit") {
                    description = "Amount to convert from regional currency (mutually exclusive with amount_credit)"
                    required = false
                }
                queryParameter<String>("amount_credit") {
                    description = "Amount to convert to fiat currency (mutually exclusive with amount_debit)"
                    required = false
                }
            }
            response {
                code(HttpStatusCode.OK) { description = "Class cashout conversion result"; body<ConversionResponse>() }
            }
        }) {
            val id = call.longPath("CLASS_ID")
            val params = RateParams.extract(call.request.queryParameters)

            params.debit?.let { cfg.checkRegionalCurrency(it) }
            params.credit?.let { cfg.checkFiatCurrency(it) }

            if (params.debit != null) {
                call.convert(params.debit, { classToCashout(id, it) }) {
                    ConversionResponse(params.debit, it)
                }
            } else {
                call.convert(params.credit!!, { classFromCashout(id, it) }) {
                    ConversionResponse(it, params.credit)
                }
            }
        }
        get("/conversion-rate-classes/{CLASS_ID}/conversion-info/cashin-rate", {
            operationId = "getClassCashinRate"
            description = "Calculate cashin conversion rate for a rate class"
            tags = listOf("Conversion")
            protected = true
            securitySchemeNames("bearerAuth", "basicAuth")
            request {
                pathParameter<Long>("CLASS_ID") { description = "Conversion rate class ID" }
                queryParameter<String>("amount_debit") {
                    description = "Amount to convert from fiat currency (mutually exclusive with amount_credit)"
                    required = false
                }
                queryParameter<String>("amount_credit") {
                    description = "Amount to convert to regional currency (mutually exclusive with amount_debit)"
                    required = false
                }
            }
            response {
                code(HttpStatusCode.OK) { description = "Class cashin conversion result"; body<ConversionResponse>() }
            }
        }) {
            val id = call.longPath("CLASS_ID")
            val params = RateParams.extract(call.request.queryParameters)

            params.debit?.let { cfg.checkFiatCurrency(it) }
            params.credit?.let { cfg.checkRegionalCurrency(it) }

            if (params.debit != null) {
                call.convert(params.debit, { classToCashin(id, it) }) {
                    ConversionResponse(params.debit, it)
                }
            } else {
                call.convert(params.credit!!, { classFromCashin(id, it) }) {
                    ConversionResponse(it, params.credit)
                }
            }
        }
    }
    get("/accounts/{USERNAME}/conversion-info/cashin-rate", {
        operationId = "getAccountCashinRate"
        description = "Calculate cashin conversion rate for an account"
        tags = listOf("Conversion")
        request {
            pathParameter<String>("USERNAME") { description = "Account username" }
            queryParameter<String>("amount_debit") {
                description = "Amount to convert from fiat currency (mutually exclusive with amount_credit)"
                required = false
            }
            queryParameter<String>("amount_credit") {
                description = "Amount to convert to regional currency (mutually exclusive with amount_debit)"
                required = false
            }
        }
        response {
            code(HttpStatusCode.OK) { description = "Account cashin conversion result"; body<ConversionResponse>() }
        }
    }) {
        val params = RateParams.extract(call.request.queryParameters)

        params.debit?.let { cfg.checkFiatCurrency(it) }
        params.credit?.let { cfg.checkRegionalCurrency(it) }

        if (params.debit != null) {
            call.convert(params.debit, { userToCashin(call.pathUsername, it) }) {
                ConversionResponse(params.debit, it)
            }
        } else {
            call.convert(params.credit!!, { userFromCashin(call.pathUsername, it) }) {
                ConversionResponse(it, params.credit)
            }
        }
    }
    optAuth(db, cfg.pwCrypto, TokenLogicalScope.readonly, cfg.basicAuthCompat, allowAdmin = true) {
        get("/accounts/{USERNAME}/conversion-info/rate", {
            operationId = "getAccountConversionRate"
            description = "Get conversion rate for an account"
            tags = listOf("Conversion")
            protected = true
            securitySchemeNames("bearerAuth", "basicAuth")
            request {
                pathParameter<String>("USERNAME") { description = "Account username" }
            }
            response {
                code(HttpStatusCode.OK) { description = "Account conversion rate"; body<ConversionRate>() }
            }
        }) {
            val (isExchange, rate) = db.conversion.getUserRate(call.pathUsername)
            if (!isExchange && !call.isAuthenticated) {
                throw forbidden("Non exchange account rates are private")
            }
            call.respond(rate)
        }
    }
    auth(db, cfg.pwCrypto, TokenLogicalScope.readonly, cfg.basicAuthCompat, allowAdmin = true) {
        get("/accounts/{USERNAME}/conversion-info/cashout-rate", {
            operationId = "getAccountCashoutRate"
            description = "Calculate cashout conversion rate for an account"
            tags = listOf("Conversion")
            protected = true
            securitySchemeNames("bearerAuth", "basicAuth")
            request {
                pathParameter<String>("USERNAME") { description = "Account username" }
                queryParameter<String>("amount_debit") {
                    description = "Amount to convert from regional currency (mutually exclusive with amount_credit)"
                    required = false
                }
                queryParameter<String>("amount_credit") {
                    description = "Amount to convert to fiat currency (mutually exclusive with amount_debit)"
                    required = false
                }
            }
            response {
                code(HttpStatusCode.OK) { description = "Account cashout conversion result"; body<ConversionResponse>() }
            }
        }) {
            val params = RateParams.extract(call.request.queryParameters)

            params.debit?.let { cfg.checkRegionalCurrency(it) }
            params.credit?.let { cfg.checkFiatCurrency(it) }

            if (params.debit != null) {
                call.convert(params.debit, { userToCashout(call.pathUsername, it) }) {
                    ConversionResponse(params.debit, it)
                }
            } else {
                call.convert(params.credit!!, { userFromCashout(call.pathUsername, it) }) {
                    ConversionResponse(it, params.credit)
                }
            }
        } 
    }
}