/*
 * 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 io.ktor.http.*
import io.ktor.util.*
import io.ktor.server.application.*
import io.ktor.server.plugins.*
import io.ktor.server.request.*
import io.ktor.server.routing.*
import io.ktor.server.util.*
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import tech.libeufin.bank.auth.pathUsername
import tech.libeufin.bank.auth.AUTH_INFO
import tech.libeufin.bank.db.AccountDAO.AccountCreationResult
import tech.libeufin.bank.db.Database
import tech.libeufin.bank.BankConfig
import tech.libeufin.common.*
import tech.libeufin.common.api.intercept
import java.util.*

private val BANK_INFO = AttributeKey<BankInfo>("bank_info")

/** Retrieve the bank account info for the selected username*/
suspend fun ApplicationCall.bankInfo(db: Database): BankInfo {
    val username = this.pathUsername

    // CHeck cached bank info
    val cachedInfo = this.attributes.getOrNull(BANK_INFO)
    if (cachedInfo != null) {
        require(cachedInfo.username == username)
        return cachedInfo
    }

    // Check cached auth info
    val authInfo = this.attributes.getOrNull(AUTH_INFO)
    if (authInfo != null && authInfo.username == username) {
        this.attributes.put(BANK_INFO, authInfo)
        return authInfo
    }

    // Else load from db
    val info = db.account.bankInfo(pathUsername) ?: throw unknownAccount(pathUsername)
    this.attributes.put(BANK_INFO, info)
    return info
}

/**
 *  Builds the taler://withdraw-URI.  Such URI will serve the requests
 *  from wallets, when they need to manage the operation.  For example,
 *  a URI like taler://withdraw/$BANK_URL/taler-integration/$WO_ID needs
 *  the bank to implement the Taler integratino API at the following base URL:
 *
 *      https://$BANK_URL/taler-integration
 */
fun BankConfig.talerWithdrawUri(id: UUID): String {
    val base = this.baseUrl.url
    val protocol = if (base.protocol.name == "http") "taler+http" else "taler"
    val port = if (base.port != -1) ":${base.port}" else ""
    return "${protocol}://withdraw/${base.host}${port}${base.encodedPath}taler-integration/${id}"
}

fun BankConfig.withdrawConfirmUrl(id: UUID): String {
    val base = this.baseUrl.url
    return "${base}webui/#/operation/${id}"
}

/**
 * This function creates the admin account ONLY IF it was
 * NOT found in the database.  It sets it to a random password that
 * is only meant to be overridden by a dedicated CLI tool.
 *
 * It returns false in case of problems, true otherwise.
 */
suspend fun createAdminAccount(db: Database, cfg: BankConfig, pw: String? = null): AccountCreationResult {
    var pwStr = pw ?: Base32Crockford32B.secureRand().toString()
    
    val payto = when (cfg.wireMethod) {
        WireMethod.IBAN -> IbanPayto.rand()
        WireMethod.X_TALER_BANK -> XTalerBankPayto.forUsername("admin")
    }

    return db.account.create(
        username = "admin",
        password = pwStr,
        name = "Bank administrator",
        internalPayto = payto,
        isPublic = false,
        isTalerExchange = false,
        maxDebt = cfg.defaultDebtLimit,
        bonus = TalerAmount(0, 0, cfg.regionalCurrency),
        checkPaytoIdempotent = false,
        email = null,
        phone = null,
        cashoutPayto = null,
        tanChannels = emptySet(),
        pwCrypto = cfg.pwCrypto,
        conversionRateClassId = null,
    )
}

fun Route.conditional(implemented: Boolean, callback: Route.() -> Unit): Route =
    intercept("Conditional", callback) {
        if (!implemented) {
            throw notImplemented()
        }
    }