package kr.or.bok.rtgs.hermes

import kr.or.bok.rtgs.common.JournalEntry
import java.util.concurrent.ConcurrentSkipListMap

/**
 * 저널 순차 게이트(B3) — globalSeq **오름차순**으로만 적용되도록 버퍼링/재정렬한다.
 *
 * rtgs.journal은 **단일 파티션**이라 Kafka가 offset(=순번기 발행) 순서대로 전달한다. 따라서
 * "더 큰 seq가 도착했는데 `expected`는 안 온" 상황 = 그 `expected`가 **영영 발행되지 않은
 * phantom**(B1의 SEQUENCE nextval 소비 후 미기록 등)이라는 의미다. 단일 파티션 순서 보장상
 * 뒤늦게 채워질 일이 없으므로, 타임아웃 후 skip은 **실엔트리 스킵이 아니라** 결번 정리로 안전하다.
 *
 * 순수 로직(스프링 비의존) — 단위테스트로 규율 고정. Hermes 리스너(concurrency=1)와 타임아웃
 * 스케줄러가 함께 접근하므로 메서드는 @Synchronized.
 */
class GapBuffer {
    @Volatile
    var expected: Long = 0L
        private set

    @Volatile
    var lastAdvanceMillis: Long = 0L
        private set

    private val pending = ConcurrentSkipListMap<Long, JournalEntry>()

    fun pendingCount(): Int = pending.size
    fun highestPending(): Long? = if (pending.isEmpty()) null else pending.lastKey()

    /** 도착 엔트리를 넣고, 지금 적용 가능한 엔트리들을 **순서대로** 반환(없으면 빈 리스트). */
    @Synchronized
    fun offer(entry: JournalEntry, nowMillis: Long): List<JournalEntry> {
        if (expected == 0L) {          // 첫 엔트리가 기대 순번을 앵커링
            expected = entry.globalSeq
            lastAdvanceMillis = nowMillis
        }
        return when {
            entry.globalSeq < expected -> emptyList()                         // old/dup
            entry.globalSeq > expected -> { pending[entry.globalSeq] = entry; emptyList() } // gap → 버퍼
            else -> drainFrom(entry, nowMillis)                               // 기대 순번 → 적용 + 이어받기
        }
    }

    /** expected(=phantom 확정)를 건너뛰고, 이어서 적용 가능한 엔트리들을 반환. */
    @Synchronized
    fun skipExpected(nowMillis: Long): List<JournalEntry> {
        if (pending.isEmpty()) return emptyList()   // 스킵할 이유 없음(대기자 없음)
        expected++
        val ready = mutableListOf<JournalEntry>()
        while (pending.containsKey(expected)) { ready += pending.remove(expected)!!; expected++ }
        if (ready.isNotEmpty()) lastAdvanceMillis = nowMillis
        return ready
    }

    private fun drainFrom(head: JournalEntry, nowMillis: Long): List<JournalEntry> {
        val ready = mutableListOf(head)
        expected++
        while (pending.containsKey(expected)) { ready += pending.remove(expected)!!; expected++ }
        lastAdvanceMillis = nowMillis
        return ready
    }

    /** pending이 있으면 마지막 진전 이후 경과(ms), 없으면 0(정체 아님). */
    fun stalledMillis(nowMillis: Long): Long =
        if (pending.isEmpty()) 0L else (nowMillis - lastAdvanceMillis).coerceAtLeast(0L)
}
