package kr.or.bok.rtgs.louisvuitton

import org.slf4j.LoggerFactory
import org.springframework.http.client.SimpleClientHttpRequestFactory
import org.springframework.stereotype.Component
import org.springframework.web.client.RestClient

/**
 * 포탈용 서비스 지표 수집기.
 *
 * 각 RTGS 서비스의 actuator `/actuator/metrics/{name}` JSON을 폴링해 업무/부하 지표를 모은다.
 * - DB/Kafka 미접근(순수 HTTP), 다운 서비스는 reachable=false 로만 표기(전체는 성공 반환).
 * - 대시보드 5초 폴링 대비 결과를 3초 메모이즈(과호출 방지).
 */
@Component
class ServiceMetricsCollector {
    private val log = LoggerFactory.getLogger(javaClass)

    /** 서비스명 → actuator 포트(로컬 단일 센터). */
    private val ports = linkedMapOf(
        "sequencer" to 8090, "chanel" to 8091, "dior" to 8092, "hermes" to 8093,
        "prada" to 8094, "gucci" to 8095, "louisvuitton" to 8099,
    )

    private val http: RestClient = RestClient.builder()
        .requestFactory(SimpleClientHttpRequestFactory().apply {
            setConnectTimeout(500)
            setReadTimeout(800)
        })
        .build()

    @Volatile private var cache: Map<String, Any?>? = null
    @Volatile private var cachedAt: Long = 0

    /** 3초 캐시 스냅샷. */
    fun snapshot(now: Long): Map<String, Any?> {
        val c = cache
        if (c != null && now - cachedAt < 3000) return c
        val fresh = collect(now)
        cache = fresh; cachedAt = now
        return fresh
    }

    private fun collect(now: Long): Map<String, Any?> {
        val services = ports.map { (svc, port) ->
            val base = "http://127.0.0.1:$port"
            val reachable = ping(base)
            val metrics = if (reachable) metricsFor(svc, base) else emptyMap()
            mapOf("service" to svc, "port" to port, "reachable" to reachable, "metrics" to metrics)
        }
        return mapOf("collectedAt" to now, "services" to services)
    }

    /** 서비스별 관심 지표 수집. 업무지표 없는 서비스는 생존/부하만. */
    private fun metricsFor(svc: String, base: String): Map<String, Any?> {
        val m = linkedMapOf<String, Any?>()
        m["uptimeSec"] = value(base, "process.uptime")?.let { it.toLong() }
        m["httpRequests"] = count(base, "http.server.requests")?.toLong()
        m["memUsedMb"] = value(base, "jvm.memory.used")?.let { (it / 1_048_576).toLong() }
        when (svc) {
            "hermes" -> {
                m["settlementsAccc"] = count(base, "rtgs.settlements", "status" to "ACCC")?.toLong() ?: 0L
                m["settlementsRjct"] = count(base, "rtgs.settlements", "status" to "RJCT")?.toLong() ?: 0L
                val settleCnt = count(base, "rtgs.settle")
                val settleTotal = statistic(base, "rtgs.settle", "TOTAL_TIME")
                m["settleMeanMs"] = if (settleCnt != null && settleCnt > 0 && settleTotal != null)
                    Math.round(settleTotal / settleCnt * 1000.0) / 1.0 else 0.0
                m["settleMaxMs"] = statistic(base, "rtgs.settle", "MAX")?.let { Math.round(it * 1000.0) / 1.0 } ?: 0.0
                m["journalGap"] = count(base, "rtgs.journal.gap")?.toLong() ?: 0L
            }
            "prada" -> {
                m["finalityAccc"] = count(base, "rtgs.finality", "status" to "ACCC")?.toLong() ?: 0L
                m["finalityRjct"] = count(base, "rtgs.finality", "status" to "RJCT")?.toLong() ?: 0L
            }
        }
        return m
    }

    // ---------- actuator /metrics 파싱 헬퍼 ----------

    private fun ping(base: String): Boolean =
        runCatching { http.get().uri("$base/actuator/health").retrieve().toBodilessEntity() }.isSuccess

    /** COUNT 통계(카운터/타이머 건수). tag 옵션(status:ACCC 등). */
    private fun count(base: String, name: String, tag: Pair<String, String>? = null): Double? =
        statistic(base, name, "COUNT", tag)

    /** VALUE 통계(게이지: process.uptime, jvm.memory.used 등). */
    private fun value(base: String, name: String, tag: Pair<String, String>? = null): Double? =
        statistic(base, name, "VALUE", tag)

    /** 지정 statistic(COUNT/VALUE/TOTAL_TIME/MAX)의 measurement 값. 실패 시 null. */
    @Suppress("UNCHECKED_CAST")
    private fun statistic(base: String, name: String, stat: String, tag: Pair<String, String>? = null): Double? {
        val uri = buildString {
            append(base).append("/actuator/metrics/").append(name)
            if (tag != null) append("?tag=").append(tag.first).append(":").append(tag.second)
        }
        return runCatching {
            val body = http.get().uri(uri).retrieve().body(Map::class.java) as? Map<String, Any?> ?: return null
            val measurements = body["measurements"] as? List<Map<String, Any?>> ?: return null
            measurements.firstOrNull { it["statistic"] == stat }?.get("value")?.let { (it as? Number)?.toDouble() }
        }.getOrNull()
    }
}
