package kr.or.bok.rtgs.prada

import com.fasterxml.jackson.databind.ObjectMapper
import io.micrometer.core.instrument.MeterRegistry
import kr.or.bok.rtgs.common.QuorumContext
import kr.or.bok.rtgs.common.ResultMessage
import kr.or.bok.rtgs.common.SettlementNotice
import kr.or.bok.rtgs.common.Topics
import kr.or.bok.rtgs.common.TxSts
import kr.or.bok.rtgs.common.ledger.SettlementViewEntity
import kr.or.bok.rtgs.prada.jpa.SettlementViewRepository
import kr.or.bok.rtgs.prada.jpa.TransferRepository
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.stereotype.Service
import org.springframework.transaction.annotation.Transactional

/**
 * PRADA(결과동기화) — 결제결과(rtgs.result)를 받아 완결 규율(과반 확정)을 적용하고,
 * 권위 원장(PostgreSQL)의 거래 상태를 최종(ACCC)으로 확정 + 조회 사본 기록.
 *
 * 완결 규율(B2): `rtgs.result`(= applied-ack)을 **전 센터분** 소비하여 bmi별 서로 다른
 * `processedCenter` 수를 집계(QuorumAggregator)한다. 과반(2/3) 도달 전에는 ACSP(예약),
 * 도달 시 ACCC(반려면 RJCT)로 승격. 1센터에서는 과반=1이라 첫 결과에 즉시 완결(기존 동작 보존).
 *
 * 완결 확정 후 **결과통보(rtgs.notify)** 를 발행한다. 실제 신청/수취기관 앞 pacs.002 송부는
 * 접수센터(Chanel)가 담당(개요 프로세스 (4)). 발행은 **트랜잭션 커밋 후**에 해 스테일 통보를 막는다.
 * 중복 통보 방지: **접수센터(origin) Prada만** notify 발행(각 센터 Prada가 독립 완결하므로).
 */
@Service
class PradaService(
    private val transfers: TransferRepository,
    private val views: SettlementViewRepository,
    private val mapper: ObjectMapper,
    private val kafka: KafkaTemplate<String, String>,
    private val metrics: MeterRegistry,
    @Value("\${rtgs.center-id:DC1}") private val centerId: String,
    @Value("\${rtgs.center-count:1}") private val centerCount: Int,
) {
    private val log = LoggerFactory.getLogger(javaClass)
    private val aggregator by lazy { QuorumAggregator(QuorumContext(centerId, centerCount)) }

    // 센터별 그룹(prada-DC1/DC2/DC3)이라 각 센터 Prada가 전 센터 result를 독립 소비.
    @KafkaListener(topics = [Topics.RESULT], groupId = "prada-\${rtgs.center-id:DC1}", concurrency = "1")
    fun onResult(payload: String) {
        val r = mapper.readValue(payload, ResultMessage::class.java)
        val notice = finalize(r)                    // @Transactional (반환 = 커밋 완료, 과반 최초 도달 시에만 non-null)
        // 완결(ACCC/RJCT)만, 그리고 접수센터(origin) Prada만 결과통보(중복 방지).
        if (notice != null && notice.originCenter == centerId) {
            metrics.counter("rtgs.finality", "status", notice.finalStatus.name).increment() // B4
            kafka.send(Topics.NOTIFY, notice.bmi, mapper.writeValueAsString(notice))
        }
    }

    /**
     * 완결 규율 적용 + 원장/사본 확정. 과반 **최초 도달** 시에만 완결 승격 + SettlementNotice 반환.
     * 과반 미달이면 ACSP(예약)로 기록하고 null 반환. 이미 완결된 bmi는 멱등 스킵(null).
     */
    @Transactional
    fun finalize(r: ResultMessage): SettlementNotice? {
        val e = r.journalEntry
        val bmi = e.core.bmi
        val now = System.currentTimeMillis()

        // 이미 완결된 건에 늦게 도착한 ack: 상태를 되돌리지 않도록 멱등 스킵.
        if (aggregator.isFinalized(bmi)) return null

        // rtgs.result = applied-ack. 보고 센터(processedCenter) 집계 → 과반 최초 도달 여부.
        val reachedQuorum = aggregator.record(bmi, r.processedCenter)
        val finalStatus = when {
            !reachedQuorum -> TxSts.ACSP           // 과반 미달: 예약 상태 유지(아직 완결 아님)
            r.status == TxSts.RJCT -> TxSts.RJCT    // 과반 도달 + 반려
            else -> TxSts.ACCC                      // 과반 도달 + 정상: 완결
        }

        transfers.findById(bmi).ifPresent {
            it.status = finalStatus
            it.updatedAt = now
            if (r.reason != null) it.reason = r.reason
            transfers.save(it)
        }

        views.save(
            SettlementViewEntity(
                bmi = e.core.bmi, globalSeq = e.globalSeq, finalStatus = finalStatus.name,
                senderCode = e.core.senderCode, receiverCode = e.core.receiverCode, amount = e.core.amount,
                originCenter = e.originCenter, processedCenter = r.processedCenter,
                debtorBalanceAfter = r.debtorBalanceAfter, creditorBalanceAfter = r.creditorBalanceAfter,
                finalizedAt = now,
            )
        )
        log.info("{} seq=#{} bmi={} (origin={})", finalStatus, e.globalSeq, e.core.bmi, e.originCenter)

        return if (finalStatus == TxSts.ACCC || finalStatus == TxSts.RJCT) {
            SettlementNotice(
                bmi = e.core.bmi, originCenter = e.originCenter,
                senderCode = e.core.senderCode, receiverCode = e.core.receiverCode,
                amount = e.core.amount, globalSeq = e.globalSeq,
                finalStatus = finalStatus, reason = r.reason, finalizedAt = now,
            )
        } else null
    }
}
