/*
 * This file is part of LibEuFin.
 * Copyright (C) 2024-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.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.types.*
import com.github.ajalt.mordant.terminal.*
import tech.libeufin.common.*
import tech.libeufin.nexus.*
import tech.libeufin.nexus.ebics.*
import tech.libeufin.nexus.iso20022.*
import java.util.zip.*
import java.time.Instant
import java.io.*

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

    private val common by CommonOption()
    private val ebicsLog by ebicsLogOption()

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

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

    private val common by CommonOption()
    private val amount by option(
        "--amount",
        help = "The payment amount, 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, payto 'message' parameter takes the precedence"
    )
    private val payto by argument(
        help = "The debited account IBAN payto URI"
    ).convert { Payto.parse(it).expectIban() }

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

            require(amount.currency == cfg.currency) {
                "Wrong currency: expected ${cfg.currency} got ${amount.currency}"
            }
            
            registerIncomingPayment(db, cfg.ingest,
                IncomingPayment(
                    amount = amount,
                    debtor = payto,
                    subject = subject,
                    creditFee = creditFee,
                    executionTime = Instant.now(),
                    id = IncomingId(null, randEbicsId(), null)
                )
            )
        }
    }
}

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

    private val common by CommonOption()

    override fun run() = cliCmd(logger, common.log) {
        val nexusCgf = nexusConfig(common.config)
        val cfg = nexusCgf.ebics
        val (clientKeys, bankKeys) = expectFullKeys(cfg)
        val order = cfg.dialect.downloadDoc(OrderDoc.acknowledgement, false)
        val client = httpClient()
        val result = tech.libeufin.nexus.test.txCheck(client, cfg, clientKeys, bankKeys, order, 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: CliktCommand("ebics-btd") {
    override fun help(context: Context) = "Perform EBICS requests"
    
    private val common by CommonOption()
    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, common.log) {
        nexusConfig(common.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,
                httpClient(),
                db,
                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 ListCmd: CliktCommand("list") {
    override fun help(context: Context) = "List nexus transactions"

    private val common by CommonOption()
    private val kind: ListKind by argument(
        help = "Which list to print",
        helpTags = ListKind.entries.associate { Pair(it.name, it.description()) }
    ).enum<ListKind>()

    override fun run() = cliCmd(logger, common.log) {
        nexusConfig(common.config).withDb { db, cfg ->
            fun fmtPayto(payto: String): String {
                if (payto == null) return ""
                try {
                    val parsed = Payto.parse(payto).expectIban()
                    return buildString {
                        append(parsed.iban.toString())
                        if (parsed.bic != null) append(" ${parsed.bic}")
                        if (parsed.receiverName != null) append(" ${parsed.receiverName}")
                    }
                } catch (e: Exception) {
                    return payto.removePrefix("payto://")
                }
            }
            when (kind) {
                ListKind.incoming -> {
                    val txs = db.list.incoming()
                    for (tx in txs) {
                        if (tx.creditFee.isZero()) {
                            println("${tx.date} ${tx.id} ${tx.amount}")
                        } else {
                            println("${tx.date} ${tx.id} ${tx.amount}-${tx.creditFee}")
                        }
                        if (tx.debtor != null) {
                            println("  debtor: ${fmtPayto(tx.debtor)}")
                        }
                        if (tx.subject != null) {
                            println("  subject: ${tx.subject}")
                        }
                        if (tx.talerable != null) {
                            println("  talerable: ${tx.talerable}")
                        }
                        if (tx.bounced != null) {
                            println("  bounced: ${tx.bounced}")
                        }
                        println()
                    }
                }
                ListKind.outgoing -> {
                    val txs = db.list.outgoing()
                    for (tx in txs) {
                        println("${tx.date} ${tx.id} ${tx.amount}")
                        if (tx.creditor != null) {
                            println("  creditor: ${fmtPayto(tx.creditor)}")
                        }
                        println("  subject: ${tx.subject}")
                        if (tx.wtid != null) {
                            println("  talerable: ${tx.wtid} ${tx.exchangeBaseUrl}")
                        }
                        println()
                    }
                }
                ListKind.initiated -> {
                    val txs = db.list.initiated()
                    for (tx in txs) {
                        println("${tx.date} ${tx.id} ${tx.amount}")
                        println("  creditor: ${fmtPayto(tx.creditor)}")
                        println("  subject: ${tx.subject}")
                        if (tx.batch != null) {
                            println("  batch: ${tx.batch}")
                        }
                        println("  submission: ${tx.submissionTime} ${tx.submissionCounter}")
                        println("  status: ${tx.status} ${tx.msg ?: ""}")
                        println()
                    }
                }
            }
        }
    }
}

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

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

    override fun run() = Unit
}