/*
 * This file is part of LibEuFin.
 * Copyright (C) 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.api

import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.github.smiley4.ktoropenapi.*
import io.ktor.util.pipeline.*
import io.prometheus.metrics.core.metrics.*
import io.prometheus.metrics.model.registry.PrometheusRegistry
import io.prometheus.metrics.model.snapshots.Unit
import io.prometheus.metrics.instrumentation.jvm.JvmMetrics;
import io.prometheus.metrics.expositionformats.ExpositionFormats
import tech.libeufin.common.*
import tech.libeufin.common.db.*
import tech.libeufin.nexus.*
import tech.libeufin.nexus.db.*
import tech.libeufin.nexus.db.KvDAO.*
import tech.libeufin.nexus.db.ExchangeDAO.TransferResult
import tech.libeufin.nexus.db.PaymentDAO.IncomingRegistrationResult
import tech.libeufin.nexus.iso20022.*
import tech.libeufin.ebics.randEbicsId
import java.time.Instant
import java.io.ByteArrayOutputStream

object Metrics {
    @Volatile
    private var incomingTxTotal: Long = 0
    @Volatile
    private var outgoingTxTotal: Long = 0
    @Volatile
    private var incomingTalerTxTotal: Long = 0
    @Volatile
    private var outgoingTalerTxTotal: Long = 0
    @Volatile
    private var bouncedTotal: Long = 0
    @Volatile
    private var initiatedStatus: Map<String, Long> = emptyMap()
    @Volatile
    private var submitStatus: TaskStatus = TaskStatus()
    @Volatile
    private var fetchStatus: TaskStatus = TaskStatus()

    init {
        // Register JVM metrics 
        JvmMetrics.builder().register()

        // Register custom metrics
        CounterWithCallback.builder()
            .name("libeufin_nexus_tx_incoming_total")
            .help("Number of registered incoming transactions")
            .callback { it.call(incomingTxTotal.toDouble()) }
            .register()
        CounterWithCallback.builder()
            .name("libeufin_nexus_tx_outgoing_total")
            .help("Number of initiated outgoing transactions")
            .callback { it.call(outgoingTxTotal.toDouble()) }
            .register()
        CounterWithCallback.builder()
            .name("libeufin_nexus_tx_incoming_talerable_total")
            .help("Number of registered incoming talerable transactions")
            .callback { it.call(incomingTalerTxTotal.toDouble()) }
            .register()
        CounterWithCallback.builder()
            .name("libeufin_nexus_tx_outgoing_talerable_total")
            .help("Number of initiated outgoing talerable transactions")
            .callback { it.call(outgoingTalerTxTotal.toDouble()) }
            .register()
        CounterWithCallback.builder()
            .name("libeufin_nexus_tx_bounced_total")
            .help("Number of bounced transactions")
            .callback { it.call(bouncedTotal.toDouble()) }
            .register()
        GaugeWithCallback.builder()
            .name("libeufin_nexus_tx_initiated")
            .help("Status of initiated transaction")
            .labelNames("status")
            .callback {
                for ((label, count) in initiatedStatus) {
                    it.call(count.toDouble(), label)
                }
            }
            .register()
        GaugeWithCallback.builder()
            .name("libeufin_nexus_task_execution_timestamp_seconds")
            .help("Status of initiated transaction")
            .unit(Unit.SECONDS)
            .labelNames("name")
            .callback {
                submitStatus.last_trial?.let { timestamp ->
                    it.call(timestamp.getEpochSecond().toDouble(), "submit")
                }
                fetchStatus.last_trial?.let { timestamp ->
                    it.call(timestamp.getEpochSecond().toDouble(), "fetch")
                }
            }
            .register()
        GaugeWithCallback.builder()
            .name("libeufin_nexus_task_success_timestamp_seconds")
            .help("Status of initiated transaction")
            .unit(Unit.SECONDS)
            .labelNames("name")
            .callback {
                submitStatus.last_successfull?.let { timestamp ->
                    it.call(timestamp.getEpochSecond().toDouble(), "submit")
                }
                fetchStatus.last_successfull?.let { timestamp ->
                    it.call(timestamp.getEpochSecond().toDouble(), "fetch")
                }
            }
            .register()
    }

    // Load metrics from the database
    suspend fun sync(db: Database) {
        db.serializable(
            """
                SELECT
                (SELECT count(*) FROM incoming_transactions) AS incoming_tx_count,
                (SELECT count(*) FROM outgoing_transactions) AS outgoing_tx_count,
                (SELECT count(*) FROM talerable_incoming_transactions) AS incoming_talerable_tx_count,
                (SELECT count(*) FROM talerable_outgoing_transactions) AS outgoing_talerable_tx_count,
                (SELECT count(*) FROM bounced_transactions) AS bounced_tx_count,
                (SELECT value FROM kv WHERE key='$SUBMIT_TASK_KEY') AS submit_status,
                (SELECT value FROM kv WHERE key='$FETCH_TASK_KEY') AS fetch_status
            """
        ) {
            one {
                incomingTxTotal = it.getLong("incoming_tx_count")
                outgoingTxTotal = it.getLong("outgoing_tx_count")
                incomingTalerTxTotal = it.getLong("incoming_talerable_tx_count")
                outgoingTalerTxTotal = it.getLong("outgoing_talerable_tx_count")
                bouncedTotal = it.getLong("bounced_tx_count")
                submitStatus = it.getJson<TaskStatus>("submit_status") ?: TaskStatus()
                fetchStatus = it.getJson<TaskStatus>("fetch_status") ?: TaskStatus()
                Unit
            }
        }

        db.serializable(
            """
                SELECT count(*) as count, status FROM initiated_outgoing_transactions GROUP BY status
            """
        ) {
            initiatedStatus = all {
                it.getString("status") to it.getLong("count")
            }.toMap()
        }
    }
}

fun Routing.observabilityApi(db: Database, cfg: NexusConfig) = conditional(cfg.observabilityApiCfg) {
    get("/taler-observability/config", {
        operationId = "getObservabilityConfig"
        description = "Get the configuration of the observability API"
        tags = listOf("Observability")
        response {
            code(HttpStatusCode.OK) { description = "Configuration of the observability API"; body<TalerObservabilityConfig>() }
        }
    }) {
        call.respond(TalerObservabilityConfig())
    }
    auth(cfg.observabilityApiCfg) {
        get("/taler-observability/metrics", {
            operationId = "getMetrics"
            description = "Get Prometheus metrics"
            tags = listOf("Observability")
            protected = true
            securitySchemeNames("bearerAuth", "basicAuth")
            response {
                code(HttpStatusCode.OK) { description = "Prometheus text format metrics" }
            }
        }) {
            Metrics.sync(db)
            val snapshot = PrometheusRegistry.defaultRegistry.scrape()
            val outputStream = ByteArrayOutputStream()
            ExpositionFormats.init().getPrometheusTextFormatWriter().write(outputStream, snapshot)
            call.respondText(outputStream.toString(Charsets.UTF_8), ContentType.parse("text/plain; version=0.0.4; charset=utf-8"))
        }
    }
}