/*
 * 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.nexus.cli

import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.Context
import com.github.ajalt.clikt.parameters.groups.provideDelegate
import com.github.ajalt.clikt.parameters.options.flag
import com.github.ajalt.clikt.parameters.options.option
import io.ktor.client.*
import tech.libeufin.common.*
import tech.libeufin.common.crypto.CryptoUtil
import tech.libeufin.ebics.ClientPrivateKeysFile
import tech.libeufin.ebics.BankPublicKeysFile
import tech.libeufin.ebics.ebicsSetup
import tech.libeufin.ebics.askUserToAcceptKeys
import tech.libeufin.ebics.EbicsLogger
import tech.libeufin.ebics.EbicsKeyMng
import tech.libeufin.nexus.*
import tech.libeufin.ebics.*
import java.nio.file.FileAlreadyExistsException
import java.nio.file.Path
import java.nio.file.StandardOpenOption
import java.time.Instant
import kotlin.io.path.Path
import kotlin.io.path.writeBytes

fun expectFullKeys(cfg: EbicsKeysConfig): Pair<ClientPrivateKeysFile, BankPublicKeysFile> =
    expectFullKeys(cfg, "libeufin-nexus ebics-setup")

/**
 * CLI class implementing the "ebics-setup" subcommand.
 */
class EbicsSetup: TalerCmd() {
    override fun help(context: Context) = "Set up the EBICS subscriber"

    private val forceKeysResubmission by option(
        help = "Resubmits all the keys to the bank"
    ).flag(default = false)
    private val autoAcceptKeys by option(
        help = "Accepts the bank keys without interactively asking the user"
    ).flag(default = false)
    private val generateRegistrationPdf by option(
        help = "Generates the PDF with the client public keys to send to the bank"
    ).flag(default = false)
    private val ebicsLog by ebicsLogOption()
    /**
     * This function collects the main steps of setting up an EBICS access.
     */
    override fun run() = cliCmd(logger) {
        val cfg = nexusConfig(config)
        val setupCfg = cfg.setup

        val client = httpClient()
        val ebicsLogger = EbicsLogger(ebicsLog)

        val ebics3 = when (cfg.ebics.dialect) {
            // TODO GLS needs EBICS 2.5 for key management
            Dialect.gls -> false
            else -> true
        }
        val (clientKeys, bankKeys) = ebicsSetup(
            client,
            ebicsLogger,
            cfg.ebics,
            cfg.ebics.host,
            cfg.setup,
            forceKeysResubmission,
            generateRegistrationPdf,
            autoAcceptKeys,
            ebics3
        )
        
        // Check account information
        logger.info("Doing administrative request HKD")
        cfg.withDb { db, _ ->
            EbicsClient(
                cfg.ebics.host,
                client, 
                db.ebics,
                ebicsLogger,
                clientKeys,
                bankKeys
            ).download(EbicsOrder.V3.HKD) { stream ->
                val (partner, users) = EbicsAdministrative.parseHKD(stream)
                val user = users.find { it -> it.id == cfg.ebics.host.userId }
                // Debug logging
                logger.debug {
                    buildString {
                        if (partner.name != null || partner.accounts.isNotEmpty()) {
                            append("Partner Info: ")
                            if (partner.name != null) {
                                append("'")
                                append(partner.name)
                                append("'")
                            }
                            for ((currency, iban, bic) in partner.accounts) {
                                append(' ')
                                append(currency)
                                append('-')
                                append(iban)
                                append('-')
                                append(bic)
                            }
                            append('\n')
                        }
                        append("Supported orders:\n")
                        for ((order, description) in partner.orders) {
                            append("- ")
                            append(order.description())
                            append(": ")
                            append(description)
                            append('\n')
                        }
                        if (user != null) {
                            append("Authorized orders:\n")
                            for ((order) in partner.orders) {
                                append("- ")
                                append(order.description())
                                append('\n')
                            }
                        }
                    }
                }

                // Check partner info match config
                if (partner.name != null && partner.name != cfg.ebics.account.name)
                    logger.warn("Expected NAME '${cfg.ebics.account.name}' from config got '${partner.name}' from bank")
                val account = partner.accounts.find { it.iban == cfg.ebics.account.iban.toString() }
                if (account != null) {
                    if (account.currency != null && account.currency != cfg.currency)
                        logger.error("Expected CURRENCY '${cfg.currency}' from config got '${account.currency}' from bank")
                    if (account.bic != cfg.ebics.account.bic)
                        logger.error("Expected BIC '${cfg.ebics.account.bic}' from config got '${account.bic}' from bank")
                } else if (partner.accounts.isNotEmpty()) {
                    val ibans = partner.accounts.map { it.iban }.joinToString(" ")
                    logger.error("Expected IBAN ${cfg.ebics.account.iban} from config got $ibans from bank")
                }

                val instantDebitOrder = cfg.ebics.dialect.instantDirectDebit()
                val debitOrder = cfg.ebics.dialect.directDebit()
                val requireOrders = cfg.ebics.dialect.downloadOrders()

                val partnerOrders = partner.orders.map { it.order }

                // Check partner support for direct debit orders
                if (instantDebitOrder != null && instantDebitOrder !in partnerOrders) {
                    logger.warn("Unsupported instant debit order: ${instantDebitOrder.description()}")
                }
                if (debitOrder !in partnerOrders) {
                    logger.warn("Unsupported debit order: ${debitOrder.description()}")
                }
                
                // Check partner support required orders
                val unsupportedOrder = requireOrders subtract partnerOrders
                if (unsupportedOrder.isNotEmpty()) {
                    logger.warn("Unsupported orders: {}", unsupportedOrder.map(EbicsOrder::description).joinToString(" "))
                }

                if (user != null) {
                    // Check user is authorized for direct debit orders
                    if (instantDebitOrder != null && instantDebitOrder in partnerOrders && instantDebitOrder !in user.permissions) {
                        logger.warn("Unauthorized instant debit order: ${instantDebitOrder.description()}")
                    }
                    if (debitOrder in partnerOrders && debitOrder !in user.permissions) {
                        logger.warn("Unauthorized debit order: ${debitOrder.description()}")
                    }

                    // Check user is authorized for required orders
                    val unauthorizedOrders = requireOrders subtract user.permissions subtract unsupportedOrder
                    if (unauthorizedOrders.isNotEmpty()) {
                        logger.warn("Unauthorized orders: {}", unauthorizedOrders.map(EbicsOrder::description).joinToString(" "))
                    }

                    logger.info("Subscriber status: {}", user.status.description)
                }
            }
        }
        
        println("setup ready")
    }
}