/*
 * 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/>
 */
package tech.libeufin.bank

import kotlinx.serialization.Serializable
import io.github.smiley4.schemakenerator.core.annotations.Description
import tech.libeufin.bank.db.Database
import tech.libeufin.common.*
import tech.libeufin.common.crypto.PwCrypto
import tech.libeufin.common.db.DatabaseConfig
import java.nio.file.Path
import java.time.Duration

/** Configuration for libeufin-bank */
data class BankConfig(
    private val cfg: TalerConfig,
    val name: String,
    val baseUrl: BaseURL,
    val regionalCurrency: String,
    val regionalCurrencySpec: CurrencySpecification,
    val wireTransferFees: TalerAmount,
    val minAmount: TalerAmount,
    val maxAmount: TalerAmount,
    val allowRegistration: Boolean,
    val allowAccountDeletion: Boolean,
    val allowEditName: Boolean,
    val allowEditCashout: Boolean,
    val defaultDebtLimit: TalerAmount,
    val registrationBonus: TalerAmount,
    val suggestedWithdrawalExchange: String?,
    val allowConversion: Boolean,
    val fiatCurrency: String?,
    val fiatCurrencySpec: CurrencySpecification?,
    val spaPath: Path?,
    val tanChannels: Map<TanChannel, Pair<Path, Map<String, String>>>,
    val payto: BankPaytoCtx,
    val wireMethod: WireMethod,
    val pwCrypto: PwCrypto,
    val gcAbortAfter: Duration,
    val gcCleanAfter: Duration,
    val gcDeleteAfter: Duration,
    val pwdCheckQuality: Boolean,
    val basicAuthCompat: Boolean
) {
    val dbCfg: DatabaseConfig by lazy {
        val sect = cfg.section("libeufin-bankdb-postgres")
        DatabaseConfig(
            dbConnStr = sect.stringsub("config").require(),
            sqlDir = sect.path("sql_dir").require()
        )
    }
    val serverCfg: ServerConfig by lazy { 
        cfg.loadServerConfig("libeufin-bank")
    }
}

@Description("Currency conversion rate configuration")
@Serializable
data class ConversionRate (
    @Description("Ratio for converting fiat to regional currency")
    val cashin_ratio: DecimalNumber,
    @Description("Fee charged on cash-in conversions")
    val cashin_fee: TalerAmount,
    @Description("Smallest possible cash-in amount")
    val cashin_tiny_amount: TalerAmount,
    @Description("Rounding mode for cash-in conversions")
    val cashin_rounding_mode: RoundingMode,
    @Description("Minimum amount for cash-in conversions")
    val cashin_min_amount: TalerAmount,
    @Description("Ratio for converting regional currency to fiat")
    val cashout_ratio: DecimalNumber,
    @Description("Fee charged on cash-out conversions")
    val cashout_fee: TalerAmount,
    @Description("Smallest possible cash-out amount")
    val cashout_tiny_amount: TalerAmount,
    @Description("Rounding mode for cash-out conversions")
    val cashout_rounding_mode: RoundingMode,
    @Description("Minimum amount for cash-out conversions")
    val cashout_min_amount: TalerAmount,
) {
    fun check(cfg: BankConfig) {
        for (regionalAmount in sequenceOf(cashin_fee, cashin_tiny_amount, cashout_min_amount)) {
            cfg.checkRegionalCurrency(regionalAmount)
        }
        for (fiatAmount in sequenceOf(cashout_fee, cashout_tiny_amount, cashin_min_amount)) {
            cfg.checkFiatCurrency(fiatAmount)
        }

        if (cashout_tiny_amount.isZero()) {
            throw badRequest("cashout_tiny_amount must be > 0")
        } else  if (cashin_tiny_amount.isZero()) {
            throw badRequest("cashin_tiny_amount must be > 0")
        } else if (cashout_tiny_amount.isSubCent()) {
            throw badRequest("Sub-cent amounts no supported by cashout, cashout_tiny_amount must be >= 0.01")
        }
    }
} 

/** Load bank config at [configPath] */
fun bankConfig(configPath: Path?): BankConfig = BANK_CONFIG_SOURCE.fromFile(configPath).loadBankConfig()

/** Run [lambda] with access to a database conn pool */
suspend fun BankConfig.withDb(lambda: suspend (Database, BankConfig) -> Unit) {
    Database(dbCfg, regionalCurrency, fiatCurrency, payto).use { lambda(it, this) }
}

private fun TalerConfig.loadBankConfig(): BankConfig = section("libeufin-bank").run {
    val regionalCurrency = string("currency").require()
    var fiatCurrency: String? = null
    var fiatCurrencySpec: CurrencySpecification? = null
    val allowConversion = boolean("allow_conversion").default(false)
    if (allowConversion) {
        fiatCurrency = string("fiat_currency").require()
        fiatCurrencySpec = currencySpecificationFor(fiatCurrency) 
    }
    val tanChannels = buildMap {
        for (channel in TanChannel.entries) {
            path("tan_$channel").orNull()?.let {
                put(channel, Pair(it, jsonMap("tan_${channel}_env").orNull() ?: mapOf()))
            }
        }
    }

    val baseUrl = baseURL("base_url").require()
    val hostname = baseUrl.url.host
    
    val method = map("wire_type", "payment target type", mapOf(
        "iban" to WireMethod.IBAN,
        "x-taler-bank" to WireMethod.X_TALER_BANK,
    )).default(WireMethod.IBAN, logger, " defaulting to 'iban' but will fail in a future update")

    if (method == WireMethod.X_TALER_BANK) {
        val xhostname = string("x_taler_bank_payto_hostname").orNull()
        if (xhostname != null && xhostname != hostname) {
            logger.warn("deprecated x_taler_bank_payto_hostname '${xhostname}' does not match base_url hostname '$hostname'")
        }
    }
    val payto = BankPaytoCtx(
        bic = string("iban_payto_bic").orNull(),
        hostname = hostname
    )

    val pwCrypto = map("pwd_hash_algorithm", "password hash algorithm", mapOf(
        "bcrypt" to json<PwCrypto.Bcrypt>("pwd_hash_config", "bcrypt JSON config").require()
    )).require()

    val ZERO = TalerAmount.zero(regionalCurrency)
    val MAX = TalerAmount.max(regionalCurrency)
    BankConfig(
        cfg = this@loadBankConfig,
        name = string("name").default("Taler Bank"),
        regionalCurrency = regionalCurrency,
        regionalCurrencySpec = currencySpecificationFor(regionalCurrency),
        allowRegistration = boolean("allow_registration").default(false),
        allowAccountDeletion = boolean("allow_account_deletion").default(false),
        allowEditName = boolean("allow_edit_name").default(false),
        allowEditCashout = boolean("allow_edit_cashout_payto_uri").default(false),
        allowConversion = allowConversion,
        defaultDebtLimit = amount("default_debt_limit", regionalCurrency).default(ZERO),
        registrationBonus = amount("registration_bonus", regionalCurrency).default(ZERO),
        wireTransferFees = amount("wire_transfer_fees", regionalCurrency).default(ZERO),
        minAmount = amount("min_wire_transfer_amount", regionalCurrency).default(ZERO),
        maxAmount = amount("max_wire_transfer_amount", regionalCurrency).default(MAX),
        suggestedWithdrawalExchange = string("suggested_withdrawal_exchange").orNull(),
        spaPath = path("spa").orNull(),
        baseUrl = baseUrl,
        fiatCurrency = fiatCurrency,
        fiatCurrencySpec = fiatCurrencySpec,
        tanChannels = tanChannels,
        payto = payto,
        wireMethod = method,
        pwCrypto = pwCrypto,
        gcAbortAfter = duration("gc_abort_after").require(),
        gcCleanAfter = duration("gc_clean_after").require(),
        gcDeleteAfter = duration("gc_delete_after").require(),
        pwdCheckQuality = boolean("pwd_check").require(),
        basicAuthCompat = boolean("pwd_auth_compat").require()
    )
}

private fun TalerConfig.currencySpecificationFor(currency: String): CurrencySpecification
    = sections.find {
        val section = section(it)
        it.startsWith("CURRENCY-") && section.boolean("enabled").require() && section.string("code").require() == currency
    }?.let { section(it).loadCurrencySpecification() } ?: run {
        logger.warn("Missing currency specification for $currency, using sane defaults")
        CurrencySpecification(
            name = currency,
            num_fractional_input_digits = 2,
            num_fractional_normal_digits = 2,
            num_fractional_trailing_zero_digits = 2,
            alt_unit_names = mapOf("0" to currency)
        )
    }

private fun TalerConfigSection.loadCurrencySpecification(): CurrencySpecification {
    return CurrencySpecification(
        name = string("name").require(),
        num_fractional_input_digits = number("fractional_input_digits").require(),
        num_fractional_normal_digits = number("fractional_normal_digits").require(),
        num_fractional_trailing_zero_digits = number("fractional_trailing_zero_digits").require(),
        alt_unit_names = jsonMap("alt_unit_names").require()
    )
}
