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

import io.ktor.client.HttpClient
import io.ktor.http.ContentType
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.Context
import com.github.ajalt.clikt.core.ProgramResult
import com.github.ajalt.clikt.parameters.arguments.*
import com.github.ajalt.clikt.parameters.groups.provideDelegate
import com.github.ajalt.clikt.parameters.options.*
import com.github.ajalt.clikt.parameters.types.enum
import kotlin.math.min
import kotlinx.coroutines.*
import tech.libeufin.common.*
import tech.libeufin.ebics.*
import tech.libeufin.ebisync.*
import tech.libeufin.ebisync.db.Database
import java.time.*
import java.time.temporal.*
import java.io.IOException
import kotlin.time.toKotlinDuration

sealed interface DestinationClient {
    data class AzureBlobStorage(val client: AzureBlogStorage, val container: String): DestinationClient

    companion object {
        fun prepare(dest: Destination, client: HttpClient): DestinationClient? {
            return when (dest) {
                Destination.None -> null
                is Destination.AzureBlobStorage -> DestinationClient.AzureBlobStorage(
                    AzureBlogStorage(dest.accountName, dest.accountKey, dest.apiUrl, client),
                    dest.container
                )
            }
        }
    }

    suspend fun uploadFile(name: String, xml: ByteArray) {
        when (this) {
            is DestinationClient.AzureBlobStorage -> this.client.putBlob(this.container, name, xml, ContentType.Application.Xml)
        }
    }
}

suspend fun submit(dest: DestinationClient, ebics: EbicsClient, db: Database, orders: List<EbicsOrder>) {
    for (order in orders) {
        try {
            ebics.download(order) { payload ->
                val doc = order.doc();
                if (doc == OrderDoc.acknowledgement) {
                    // TODO HAC
                } else {
                    payload.unzipEach { fileName, xml ->
                        val bytes = xml.use { it.readBytes() }
                        logger.info("upload $fileName")
                        dest.uploadFile(fileName, bytes)
                    }
                }
            }
        }  catch (e: EbicsError.Code) {
            when (e.bankCode) {
                EbicsReturnCode.EBICS_NO_DOWNLOAD_DATA_AVAILABLE -> continue
                else -> throw e
            }
        }
    }
} 

class Fetch : EbicsCmd() {
    override fun help(context: Context) = "Downloads EBICS files from the bank and store them in the configured destination"

    private val pinnedStart by option(
        help = "Only supported in --transient mode, this option lets specify the earliest timestamp of the downloaded documents",
        metavar = "YYYY-MM-DD"
    ).convert { dateToInstant(it) }
    private val peek by option("--peek",
        help = "Only supported in --transient mode, do not consume fetched documents"
    ).flag()
    private val transientCheckpoint by option("--checkpoint",
        help = "Only supported in --transient mode, run a checkpoint"
    ).flag()
     private val check by option(
        help = "Check whether a destination is configured. Exit with 0 if at destination is configured, otherwise 1"
    ).flag()

    override fun run() = cliCmd(logger) {
        ebisyncConfig(config).withDb { db, cfg ->
            val (clientKeys, bankKeys) = expectFullKeys(cfg)

            val httpClient = httpClient();

            val dest = DestinationClient.prepare(cfg.fetch.destination, httpClient)
            if (check) {
                if (dest == null) {
                    logger.info("No destination configured, not starting the fetcher")
                    throw ProgramResult(1)
                } else {
                    throw ProgramResult(0)
                }
            } else if (dest == null) {
                throw ProgramResult(0)
            }

            val client = EbicsClient(
                cfg,
                httpClient,
                db.ebics,
                EbicsLogger(ebicsLog),
                clientKeys,
                bankKeys
            )

            // Try to obtain real-time notification channel if not transient
            val wssNotification = if (transient) {
                logger.info("Transient mode: fetching once and returning")
                null
            } else {
                val tmp = listenForNotification(client)
                logger.info("Running with a frequency of ${cfg.fetch.frequencyRaw}")
                tmp
            }

            var lastFetch = Instant.EPOCH
            
            while (true) {
                val checkpoint = db.kv.get<TaskStatus>(CHECKPOINT_KEY) ?: TaskStatus()
                var nextFetch = lastFetch + cfg.fetch.frequency
                var nextCheckpoint = run {
                    // We never ran, we must checkpoint now
                    if (checkpoint.last_trial == null) {
                        Instant.EPOCH
                    } else {
                        // We run today at checkpointTime
                        val checkpointDate = OffsetDateTime.now().with(cfg.fetch.checkpointTime)
                        val checkpointToday = checkpointDate.toInstant()
                        // If we already ran today we ruAn tomorrow
                        if (checkpoint.last_trial > checkpointToday) {
                            checkpointDate.plusDays(1).toInstant()
                        } else {
                            checkpointToday
                        }
                    }
                }

                val now = Instant.now()
                var success: Boolean = true
                if (
                    // Run transient checkpoint at request
                    (transient && transientCheckpoint) ||
                    // Or run recurrent checkpoint
                    (!transient && now > nextCheckpoint)
                ) {
                    logger.info("Running checkpoint")
                    
                    val since = if (transient && pinnedStart != null && (checkpoint.last_successfull == null || pinnedStart!!.isBefore(checkpoint.last_successfull))) {
                        pinnedStart
                    } else {
                        checkpoint.last_successfull
                    }
                    success = try {
                        /// We fetch HKD to only fetch supported EBICS orders and get the document versions
                        val orders = client.download(EbicsOrder.V3.HKD) { stream ->
                            val hkd = EbicsAdministrative.parseHKD(stream)
                            val supportedOrder = hkd.partner.orders.map { it.order }
                            logger.debug {
                                val fmt = supportedOrder.map(EbicsOrder::description).joinToString(" ")
                                "HKD: ${fmt}"
                            }
                            supportedOrder.filter { it.isDownload() }
                        }
                        submit(dest, client, db, orders)
                        true
                    } catch (e: Exception) {
                        e.fmtLog(logger)
                        false
                    }
                    db.kv.updateTaskStatus(CHECKPOINT_KEY, now, success)
                    lastFetch = now
                } else if (transient || now > nextFetch) {
                    if (!transient) logger.info("Running at frequency")
                    success = try {
                        /// We fetch HAA to only fetch pending & supported EBICS orders and get the document versions
                        val orders = client.download(EbicsOrder.V3.HAA) { stream ->
                            val haa = EbicsAdministrative.parseHAA(stream)
                            logger.debug {
                                val orders = haa.orders.map(EbicsOrder::description).joinToString(" ")
                                "HAA: ${orders}"
                            }
                            haa.orders
                        }
                        // TODO pinned starts
                        submit(dest, client, db, orders)
                        true
                    } catch (e: Exception) {
                        e.fmtLog(logger)
                        false
                    }
                    lastFetch = now
                }
                db.kv.updateTaskStatus(SUBMIT_TASK_KEY, now, success)

                if (transient) throw ProgramResult(if (!success) 1 else 0)

                val delay = min(ChronoUnit.MILLIS.between(now, nextFetch), ChronoUnit.MILLIS.between(now, nextCheckpoint))
                if (wssNotification == null) {
                    delay(delay)
                } else {
                    val notifications = withTimeoutOrNull(delay) {
                        wssNotification.receive()
                    }
                    if (notifications != null) {
                        logger.info("Running at real-time notifications reception")
                        submit(dest, client, db, notifications)
                    }
                }
            }
        }
    }
}