package kr.or.bok.rtgs.prada

import kr.or.bok.rtgs.common.QuorumContext
import java.util.concurrent.ConcurrentHashMap

/**
 * 정족수 집계기(B2) — bmi별로 "적용 완료(applied)"를 보고한 **서로 다른 센터**를 모아
 * 과반(quorum) 도달 여부를 판정한다.
 *
 * 신호원: `rtgs.result` = applied-ack. Hermes가 정산 트랜잭션 **커밋 후에만** 발행하므로
 * (publish-after-commit) "그 센터가 해당 seq를 durable 적용했다"는 사실과 정확히 동일하다.
 * `ResultMessage.processedCenter`가 보고 센터.
 *
 * 저장(§9.4 확정): **인메모리**. 재기동 시 맵은 비지만 — 이미 완결(ACCC/RJCT)된 건은 원장
 * (`transfer.status`)에 영속되어 안전하고, 미완결(ACSP)만 이후 도착 ack로 재집계된다.
 *
 * 순수 로직(스프링/JPA 비의존)이라 단위테스트로 규율을 고정한다. Prada 리스너는 concurrency=1
 * 단일 스레드지만, 방어적으로 동시성 안전 자료구조를 쓴다.
 */
class QuorumAggregator(private val quorum: QuorumContext) {
    private val votes = ConcurrentHashMap<String, MutableSet<String>>()
    private val finalized = ConcurrentHashMap.newKeySet<String>()

    /** 이미 완결 승격된 bmi인가(멱등 가드 — 완결 후 늦게 도착한 ack가 상태를 되돌리지 못하게). */
    fun isFinalized(bmi: String): Boolean = bmi in finalized

    /**
     * center의 적용 보고를 기록하고 과반 **최초 도달** 여부를 반환.
     * @return true = 이 호출로 과반에 처음 도달(완결로 승격). 이미 완결이거나 아직 미달이면 false.
     */
    fun record(bmi: String, center: String): Boolean {
        if (bmi in finalized) return false
        val set = votes.computeIfAbsent(bmi) { ConcurrentHashMap.newKeySet() }
        set.add(center)
        if (quorum.hasQuorum(set.size)) {
            val firstReach = finalized.add(bmi)   // add 성공 = 최초 1회만
            if (firstReach) votes.remove(bmi)     // 완결 후 표 정리(메모리 회수)
            return firstReach
        }
        return false
    }

    /** 현재까지 집계된 서로 다른 센터 수(지표/디버그). */
    fun count(bmi: String): Int = votes[bmi]?.size ?: 0
}
