package kr.or.bok.rtgs.hermes

import com.fasterxml.jackson.databind.ObjectMapper
import io.micrometer.core.instrument.MeterRegistry
import io.micrometer.core.instrument.Timer
import kr.or.bok.rtgs.common.JournalEntry
import kr.or.bok.rtgs.common.ResultMessage
import kr.or.bok.rtgs.common.Topics
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Value
import org.springframework.kafka.annotation.KafkaListener
import org.springframework.kafka.core.KafkaTemplate
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Service

/**
 * 저널 순차 소비 + 순서 탐지·교정(복원력방안 2.3-부속, S4·B3). 실제 정산은 SettlementApplier(트랜잭션).
 * **결과(rtgs.result) 발행은 정산 트랜잭션 커밋 이후**에 한다(publish-after-commit) — Prada가
 * 커밋 전 transfer를 읽어 최종확정을 놓치는 레이스 방지. rtgs.result는 B2에서 applied-ack으로도 쓰인다.
 *
 * 순서 게이트는 GapBuffer(단위테스트로 규율 고정). 단일 파티션이라 정상 시 결번이 없고, 결번은
 * 곧 phantom(발행 안 된 seq)이므로 **타임아웃 후 skip**으로 정체를 자가해소한다(B3).
 */
@Service
class HermesService(
    private val applier: SettlementApplier,
    private val kafka: KafkaTemplate<String, String>,
    private val mapper: ObjectMapper,
    private val metrics: MeterRegistry,
    @Value("\${rtgs.gap-timeout-ms:5000}") private val gapTimeoutMs: Long,
) {
    private val log = LoggerFactory.getLogger(javaClass)

    private val gate = GapBuffer()
    // B4 메트릭: 정산 처리시간(TPS·지연) + gap 버퍼링 + gap 타임아웃(phantom skip)
    private val settleTimer: Timer = Timer.builder("rtgs.settle").description("정산 처리시간").register(metrics)
    private val gapCounter = metrics.counter("rtgs.journal.gap")
    private val gapTimeoutCounter = metrics.counter("rtgs.journal.gap.timeout")

    // 센터별 그룹(hermes-DC1/DC2/DC3)이라 각 센터가 저널 전량을 독립 재생.
    @KafkaListener(topics = [Topics.JOURNAL], groupId = "hermes-\${rtgs.center-id:DC1}", concurrency = "1")
    fun onJournal(payload: String) {
        val entry = mapper.readValue(payload, JournalEntry::class.java)
        val before = gate.pendingCount()
        val ready = gate.offer(entry, System.currentTimeMillis())
        if (ready.isEmpty() && gate.pendingCount() > before) {
            gapCounter.increment()
            log.warn("GAP: got seq={} expected={} -> buffered (pending={})", entry.globalSeq, gate.expected, gate.pendingCount())
        }
        ready.forEach { apply(it) }
    }

    /**
     * B3 정체 감지: pending이 있고 마지막 진전 이후 gapTimeoutMs 초과면, expected는 phantom으로
     * 확정(단일 파티션 순서 보장)하고 skip해 진행을 재개한다. 실엔트리 스킵이 아니라 결번 정리.
     */
    @Scheduled(fixedDelayString = "\${rtgs.gap-check-ms:1000}")
    fun checkGapTimeout() {
        val now = System.currentTimeMillis()
        if (gate.stalledMillis(now) < gapTimeoutMs) return
        val missing = gate.expected
        val ready = gate.skipExpected(now)
        if (ready.isNotEmpty()) {
            gapTimeoutCounter.increment()
            log.error("GAP TIMEOUT: seq={} 미도착 {}ms 초과 → phantom 확정 skip, 재개(적용 {}건, 다음 expected={})",
                missing, gapTimeoutMs, ready.size, gate.expected)
            ready.forEach { apply(it) }
        }
    }

    /** 정산 적용(시간 계측) 후 결과 발행. */
    private fun apply(entry: JournalEntry) {
        val result = settleTimer.recordCallable { applier.apply(entry) }
        publishAfterCommit(result)
        if (result != null) metrics.counter("rtgs.settlements", "status", result.status.name).increment()
    }

    /** 트랜잭션 커밋 후(apply 반환 = 커밋 완료) 결과 발행. null이면 멱등 스킵. */
    private fun publishAfterCommit(result: ResultMessage?) {
        if (result == null) return
        kafka.send(Topics.RESULT, result.journalEntry.core.bmi, mapper.writeValueAsString(result))
    }
}
