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

import tech.libeufin.common.*
import tech.libeufin.common.db.DatabaseConfig
import tech.libeufin.nexus.db.Database
import tech.libeufin.ebics.EbicsKeysConfig
import tech.libeufin.ebics.EbicsSetupConfig
import tech.libeufin.ebics.EbicsHostConfig
import java.nio.file.Path
import java.time.Instant
import org.slf4j.Logger
import org.slf4j.LoggerFactory

private val logger: Logger = LoggerFactory.getLogger("libeufin-config")

val NEXUS_CONFIG_SOURCE = ConfigSource("libeufin", "libeufin-nexus", "libeufin-nexus")

data class NexusIngestConfig(
    val accountType: AccountType,
    val ignoreTransactionsBefore: Instant,
    val ignoreBouncesBefore: Instant,
    val restrictionPaytoRegex: Regex?,
    val bounceDeduceFee: Boolean,
    val bounceFee: TalerAmount
) {
    companion object {
        fun default(accountType: AccountType, currency: String = "KUDOS") 
            = NexusIngestConfig(accountType, Instant.MIN, Instant.MIN, null, false, TalerAmount.zero(currency))
    }
}

class NexusFetchConfig(config: TalerConfig, currency: String) {
    private val section = config.section("nexus-fetch")
    val frequency = section.duration("frequency").require()
    val frequencyRaw = section.string("frequency").require()
    val checkpointTime = section.time("checkpoint_time_of_day").require()
    val ignoreTransactionsBefore = section.date("ignore_transactions_before").default(Instant.MIN)
    val ignoreBouncesBefore = section.date("ignore_bounces_before").default(Instant.MIN)
    val restrictionPaytoRegex = section.regex("restriction_payto_regex").orNull()
    val bounceDeduceFee = section.boolean("bounce_deduce_fee").default(false)
    val bounceFee = section.amount("bounce_fee", currency).default(TalerAmount.zero(currency))
}

class NexusSubmitConfig(config: TalerConfig) {
    private val section = config.section("nexus-submit")
    val frequency = section.duration("frequency").require()
    val frequencyRaw = section.string("frequency").require()
    val requireAck = section.boolean("manual_ack").default(false)
}

class NexusSetupConfig(config: TalerConfig): EbicsSetupConfig {
    private val section = config.section("nexus-setup")
    override val bankAuthPubKey = section.hex("bank_authentication_pub_key_hash").orNull()
    override val bankEncPubKey = section.hex("bank_encryption_pub_key_hash").orNull()
}

class NexusHostConfig(sect: TalerConfigSection): EbicsHostConfig {
    /** The bank base URL */
    override val baseUrl = sect.string("host_base_url").require()
    /** The bank EBICS host ID */
    override val hostId = sect.string("host_id").require()
    /** EBICS user ID */
    override val userId = sect.string("user_id").require()
    /** EBICS partner ID */
    override val partnerId = sect.string("partner_id").require()
}

class NexusEbicsConfig( 
    sect: TalerConfigSection,
): EbicsKeysConfig {
    val host by lazy { NexusHostConfig(sect) }
    /** Bank account metadata */
    val account = IbanAccountMetadata(
        iban = sect.iban("iban").require(),
        bic = sect.string("bic").require(),
        name = sect.string("name").require()
    )
    val qrIban = sect.iban("qr_iban").orNull() 
    /** Bank account payto */
    val payto = IbanPayto.build(account.iban.toString(), account.bic, account.name)

    val dialect = sect.map("bank_dialect", "bank dialect", mapOf(
        "postfinance" to Dialect.postfinance,
        "gls" to Dialect.gls,
        "maerki_baumann" to Dialect.maerki_baumann,
        "valiant" to Dialect.valiant,
        "raiffeisen" to Dialect.raiffeisen,
    )).require()

    /** Path where we store the bank public keys */
    override val bankPublicKeysPath = sect.path("bank_public_keys_file").require()
    /** Path where we store our private keys */
    override val clientPrivateKeysPath = sect.path("client_private_keys_file").require()
}

class ApiConfig(section: TalerConfigSection) {
    val authMethod = section.requireAuthMethod()
}

/** Configuration for libeufin-nexus */
class NexusConfig internal constructor (val cfg: TalerConfig) {
    private val sect = cfg.section("nexus-ebics")

    val dbCfg by lazy { cfg.dbConfig() }
    val serverCfg by lazy {
        cfg.loadServerConfig("nexus-httpd")
    }

    /** The bank's currency */
    val currency = sect.string("currency").require()

    val accountType = sect.map("account_type", "account type", mapOf(
        "normal" to AccountType.normal,
        "exchange" to AccountType.exchange
    )).require()

    val fetch by lazy { NexusFetchConfig(cfg, currency) }
    val submit by lazy { NexusSubmitConfig(cfg) }
    val ebics by lazy { NexusEbicsConfig(sect) }
    val setup by lazy { NexusSetupConfig(cfg) }

    val ingest get() = NexusIngestConfig(
        accountType,
        fetch.ignoreTransactionsBefore,
        fetch.ignoreBouncesBefore,
        fetch.restrictionPaytoRegex,
        fetch.bounceDeduceFee,
        fetch.bounceFee
    )
   
    val wireGatewayApiCfg = cfg.section("nexus-httpd-wire-gateway-api").apiConf()
    val revenueApiCfg = cfg.section("nexus-httpd-revenue-api").apiConf()
    val observabilityApiCfg = cfg.section("nexus-httpd-observability-api").apiConf()
}

fun NexusConfig.checkCurrency(amount: TalerAmount) {
    if (amount.currency != currency) throw badRequest(
        "Wrong currency: expected $currency got ${amount.currency}",
        TalerErrorCode.GENERIC_CURRENCY_MISMATCH
    )
}

private fun TalerConfigSection.apiConf(): ApiConfig? {
    val enabled = boolean("enabled").require()
    return if (enabled) {
        return ApiConfig(this)
    } else {
        null
    }
}

enum class AccountType {
    normal,
    exchange
}

private fun TalerConfig.dbConfig(): DatabaseConfig {
    val sect = section("libeufin-nexusdb-postgres")
    val configOption = sect.string("config")
    return DatabaseConfig(
        dbConnStr = configOption.orNull() ?: section("nexus-postgres").string("config").orNull() ?: configOption.require(),
        sqlDir = sect.path("sql_dir").require()
    )
}

/** Load nexus config at [configPath] */
fun nexusConfig(configPath: Path?): NexusConfig {
    val config = NEXUS_CONFIG_SOURCE.fromFile(configPath)
    return NexusConfig(config)
}

/** Load nexus db config at [configPath] */
fun dbConfig(configPath: Path?): DatabaseConfig =
    NEXUS_CONFIG_SOURCE.fromFile(configPath).dbConfig()

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