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

import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.Context
import com.github.ajalt.clikt.core.subcommands
import com.github.ajalt.clikt.parameters.arguments.*
import com.github.ajalt.clikt.parameters.groups.provideDelegate
import com.github.ajalt.clikt.parameters.options.convert
import com.github.ajalt.clikt.parameters.options.default
import com.github.ajalt.clikt.parameters.options.flag
import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.options.required
import com.github.ajalt.clikt.parameters.types.*
import com.github.ajalt.mordant.terminal.*
import tech.libeufin.common.*
import tech.libeufin.nexus.*
import tech.libeufin.nexus.iso20022.*
import tech.libeufin.ebics.*
import tech.libeufin.ebics.test.txCheck
import java.util.zip.*
import java.time.Instant
import java.io.*

class Wss : TalerCmd() {
    override fun help(context: Context) = "Listen to EBICS instant notification over websocket"

    private val ebicsLog by ebicsLogOption()

    override fun run() = cliCmd(logger) {
        nexusConfig(config).withDb { db, cfg ->
            val (clientKeys, bankKeys) = expectFullKeys(cfg.ebics)
            val client = EbicsClient(
                cfg.ebics.host,
                httpClient(),
                db.ebics,
                EbicsLogger(ebicsLog),
                clientKeys,
                bankKeys
            )
            val wssNotifications = listenForNotification(client)
            if (wssNotifications != null) {
                while (true) {
                    wssNotifications.receive()
                    logger.debug("{}", wssNotifications)
                }
            }
        }
    }
}

class FakeIncoming : TalerCmd() {
    override fun help(context: Context) = "Genere a fake incoming payment"

    private val amount by option(
        "--amount",
        help = "The payment amount, credit-payto 'amount' parameter takes the precedence"
    ).convert { TalerAmount(it) }
    private val creditFee by option(
        "--credit-fee",
        help = "The payment credit fee"
    ).convert { TalerAmount(it) }
    private val subject by option(
        "--subject",
        help = "The payment subject, credit-payto 'message' parameter takes the precedence"
    )
    private val chQrr by option(
        "--ch-qrr",
        help = "The payment reference, credit-payto 'ch-qrr' parameter takes the precedence"
    )
    private val creditPayto by option(
        "--credit-payto",
        help = "The credited account IBAN payto URI"
    ).convert { Payto.parse(it).expectIban() }
    private val debitPayto by option(
        "--debit-payto",
        help = "The debited account IBAN payto URI"
    ).convert { Payto.parse(it).expectIban() }
    private val payto by argument(
        name = "deprecated",
        help = "deprecated"
    ).convert { Payto.parse(it).expectIban() }.optional()

    override fun run() = cliCmd(logger) {
        nexusConfig(config).withDb { db, cfg ->
            val amount = requireNotNull(creditPayto?.amount ?: payto?.amount ?: amount) { "Missing amount" }
            val subject = creditPayto?.message ?: payto?.message ?: subject
            val reference = creditPayto?.chQrr ?: payto?.chQrr ?: chQrr

            creditPayto?.let {
                if (reference != null) {
                    require(it.iban == cfg.ebics.qrIban) {
                        "Creditor must be the exchange QRR account expected ${cfg.ebics.account.iban} got ${it.iban}"
                    }
                } else {
                    require(it.iban == cfg.ebics.account.iban) {
                        "Creditor must be the exchange expected ${cfg.ebics.account.iban} got ${it.iban}"
                    }
                }
            }

            require(amount.currency == cfg.currency) {
                "Wrong currency: expected ${cfg.currency} got ${amount.currency}"
            }
            registerIncomingPayment(
                db, cfg.ingest,
                IncomingPayment(
                    amount = amount,
                    debtor = debitPayto ?: payto ?: IbanPayto.rand("Testing Account", Country.valueOf(cfg.ebics.account.iban.value.substring(0 until 2))),
                    subject = reference ?: subject ?: "",
                    creditFee = creditFee,
                    executionTime = Instant.now(),
                    id = IncomingId(null, randEbicsId(), null)
                )
            )
        }
    }
}

class TxCheck : TalerCmd() {
    override fun help(context: Context) = "Check transaction semantic"

    override fun run() = cliCmd(logger) {
        val nexusCgf = nexusConfig(config)
        val cfg = nexusCgf.ebics
        val (clientKeys, bankKeys) = expectFullKeys(cfg)
        val order = cfg.dialect.downloadDoc(OrderDoc.acknowledgement)
        val client = httpClient()
        val result = txCheck(client, cfg.host, clientKeys, bankKeys, order[0], cfg.dialect.directDebit())
        println("$result")
    }
}

enum class ListKind {
    incoming,
    outgoing,
    initiated;

    fun description(): String = when (this) {
        incoming -> "Incoming transactions"
        outgoing -> "Outgoing transactions"
        initiated -> "Initiated transactions"
    }
}

class EbicsDownload : TalerCmd("ebics-btd") {
    override fun help(context: Context) = "Perform EBICS requests"

    private val type by option().default("BTD")
    private val name by option()
    private val scope by option()
    private val messageName by option()
    private val messageVersion by option()
    private val container by option()
    private val option by option()
    private val ebicsLog by ebicsLogOption()
    private val pinnedStart by option(
        help = "Constant YYYY-MM-DD date for the earliest document" +
                " to download (only consumed in --transient mode).  The" +
                " latest document is always until the current time."
    )
    private val peek by option(
        "--peek",
        help = "Do not consume fetched documents"
    ).flag()
    private val dryRun by option().flag()

    class DryRun : Exception()

    override fun run() = cliCmd(logger) {
        nexusConfig(config).withDb { db, cfg ->
            val (clientKeys, bankKeys) = expectFullKeys(cfg.ebics)
            val pinnedStartVal = pinnedStart
            val pinnedStartArg = if (pinnedStartVal != null) {
                logger.debug("Pinning start date to: $pinnedStartVal")
                dateToInstant(pinnedStartVal)
            } else null
            val client = EbicsClient(
                cfg.ebics.host,
                httpClient(),
                db.ebics,
                EbicsLogger(ebicsLog),
                clientKeys,
                bankKeys
            )
            try {
                client.download(
                    EbicsOrder.V3(type, name, scope, messageName, messageVersion, container, option),
                    pinnedStartArg,
                    null,
                    peek
                ) { stream ->
                    if (container == "ZIP") {
                        stream.unzipEach { fileName, xmlContent ->
                            println(fileName)
                            println(xmlContent.readText())
                        }
                    } else {
                        println(stream.readText())
                    }
                    if (dryRun) throw DryRun()
                }
            } catch (e: DryRun) {
                // We throw DryRun to not consume files while testing
            }
        }
    }
}

class IbanGen : CliktCommand("gen") {
    override fun help(context: Context) = "Generate fake IBANs for testing"

    private val country by option().enum<Country>().required()

    override fun run() {
        println(IBAN.rand(country))
    }
}

class IbanCmd : CliktCommand("iban") {
    init {
        subcommands(IbanGen())
    }

    override fun run() = Unit
}

class TestingCmd : CliktCommand("testing") {
    init {
        subcommands(IbanCmd(), FakeIncoming(), ListCmd(), EbicsDownload(), TxCheck(), Wss())
    }

    override fun help(context: Context) = "Testing helper commands"

    override fun run() = Unit
}
