/*
 * 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.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 kotlinx.coroutines.delay
import tech.libeufin.common.*
import tech.libeufin.ebics.*
import tech.libeufin.nexus.*
import tech.libeufin.nexus.db.*
import tech.libeufin.nexus.iso20022.*
import java.time.Instant
import kotlin.time.toKotlinDuration

fun batchToPain001Msg(account: IbanAccountMetadata, batch: PaymentBatch): Pain001Msg {
    return Pain001Msg(
        messageId = batch.messageId,
        timestamp = batch.creationDate,
        debtor = account,
        sum = batch.sum,
        txs = batch.payments.map { payment ->
            val payto = payment.creditor
            if (payto.receiverName == null) {
                logger.warn("Missing receiver-name for payto $payto")
            }
            Pain001Tx(
                creditor = IbanAccountMetadata(
                    iban = payto.iban,
                    bic = payto.bic,
                    name = payto.receiverName ?: "Unknown"
                ),
                amount = payment.amount,
                subject = payment.subject,
                endToEndId = payment.endToEndId
            )
        }
    )
}

/** 
 * Submit an initiated payments [batch] using [client].
 * 
 * Parse creditor IBAN account metadata then perform an EBICS direct credit
 * 
 * Returns the orderID
 */
private suspend fun submitBatch(
    client: EbicsClient,
    order: EbicsOrder,
    batch: PaymentBatch,
    cfg: NexusConfig,
    instant: Boolean,
): String {
    val ebicsCfg = cfg.ebics
    val msg = batchToPain001Msg(ebicsCfg.account, batch)
    val xml = createPain001(
        msg = msg,
        dialect = ebicsCfg.dialect,
        instant = instant
    )
    return client.upload(order, xml)
}

/** Submit all pending initiated payments using [client] */
private suspend fun submitAll(client: EbicsClient, requireAck: Boolean, cfg: NexusConfig, db: Database) {
    // Find a supported debit order
    var instantDebitOrder = cfg.ebics.dialect.instantDirectDebit()
    val debitOrder = cfg.ebics.dialect.directDebit()
    
    // Create batch if necessary
    db.initiated.batch(Instant.now(), randEbicsId(), requireAck)
    // Send submittable batches
    db.initiated.submittable().forEach { batch ->
        logger.debug("Submitting batch {}", batch.messageId)
        runCatching {
            if (instantDebitOrder != null) {
                try {
                    return@runCatching submitBatch(client, instantDebitOrder!!, batch, cfg, true)
                } catch (e: EbicsError.Code) {
                    // No longer try to submit using the instant method for now
                    logger.debug("Failed to submit using instant credit order ${e.fmt()}")
                    instantDebitOrder = null
                }
            }
            submitBatch(client, debitOrder, batch, cfg, false)
        }.fold(
            onSuccess = { orderId -> 
                db.initiated.batchSubmissionSuccess(batch.id, Instant.now(), orderId)
                val transactions = batch.payments.joinToString(",") { it.endToEndId }
                if (instantDebitOrder == null) {
                    logger.info("Batch ${batch.messageId} submitted as order $orderId: $transactions")
                } else {
                    logger.info("Instant batch ${batch.messageId} submitted as order $orderId: $transactions")
                }  
            },
            onFailure = { e ->
                db.initiated.batchSubmissionFailure(batch.id, Instant.now(), e.message)
                logger.error("Batch ${batch.messageId} submission failure: ${e.fmt()}")
                throw e
            }
        )
    }
}

class EbicsSubmit : EbicsCmd() {
    override fun help(context: Context) = "Submits pending initiated payments found in the database"

    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
            )

            if (transient) {
                logger.info("Transient mode: submitting what found and returning.")
                submitAll(client, cfg.submit.requireAck, cfg, db)
            } else {
                logger.debug("Running with a frequency of ${cfg.submit.frequencyRaw}")
                while (true) {
                    val now = Instant.now();
                    val success = try {
                        submitAll(client, cfg.submit.requireAck, cfg, db)
                        true
                    } catch (e: Exception) {
                        e.fmtLog(logger)
                        false
                    }
                    db.kv.updateTaskStatus(SUBMIT_TASK_KEY, now, success)
                    // TODO take submitBatch taken time in the delay
                    delay(cfg.submit.frequency.toKotlinDuration())
                }
            }
        }
    }
}