package kr.or.bok.rtgs.hermes

import kr.or.bok.rtgs.common.JournalEntry
import kr.or.bok.rtgs.common.ResultMessage
import kr.or.bok.rtgs.common.TxSts
import kr.or.bok.rtgs.common.ledger.JournalLog
import kr.or.bok.rtgs.common.ledger.TransferRecord
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

/**
 * 실제 정산을 강한 일관성 트랜잭션으로 수행. 한 저널 엔트리 = 한 트랜잭션.
 * 선저널 → 잔액 이체 → transfer 상태확정. **결과 발행은 하지 않고 ResultMessage만 반환**한다
 * (Kafka 발행은 호출측 HermesService가 트랜잭션 커밋 이후에 수행 — publish-after-commit).
 * 이렇게 해야 Prada가 커밋 전 transfer를 읽는 레이스(ACSP 잔류)를 방지한다.
 */
@Service
class SettlementApplier(
    private val accounts: AccountRepository,
    private val transfers: TransferRepository,
    private val journalLogs: JournalLogRepository,
    @Value("\${rtgs.center-id:DC1}") private val centerId: String,
) {
    private val log = LoggerFactory.getLogger(javaClass)

    /** @return 발행할 ResultMessage. 멱등 스킵이면 null. */
    @Transactional
    fun apply(e: JournalEntry): ResultMessage? {
        val bmi = e.core.bmi
        val existing = transfers.findById(bmi).orElse(null)
        if (existing != null && (existing.status == TxSts.ACSP || existing.status == TxSts.ACCC)) return null // 멱등

        if (!journalLogs.existsById(e.globalSeq)) {
            journalLogs.save(JournalLog(globalSeq = e.globalSeq, bmi = bmi, seqEpochMillis = e.seqEpochMillis, applied = false))
        }

        val now = System.currentTimeMillis()
        val sender = accounts.findById(e.core.senderCode).orElse(null)
        val receiver = accounts.findById(e.core.receiverCode).orElse(null)

        if (sender == null || receiver == null) {
            return settle(e, TxSts.RJCT, now, null, null, "unknown account(s): ${e.core.senderCode}/${e.core.receiverCode}")
        }
        if (sender.balance < e.core.amount) {
            return settle(e, TxSts.RJCT, now, sender.balance, receiver.balance, "insufficient balance")
        }

        // 이체(강한 일관성): 송신기관 당좌 차감 + 수신기관 당좌 가산
        sender.balance -= e.core.amount
        receiver.balance += e.core.amount
        accounts.save(sender)
        accounts.save(receiver)
        val result = settle(e, TxSts.ACSP, now, sender.balance, receiver.balance, null)
        log.info("ACSP seq=#{} bmi={} {}({})->{}({})", e.globalSeq, bmi,
            e.core.senderCode, sender.balance, e.core.receiverCode, receiver.balance)
        return result
    }

    private fun settle(e: JournalEntry, status: TxSts, now: Long, dBal: Long?, cBal: Long?, reason: String?): ResultMessage {
        // 원자적 upsert(신규 INSERT / 기존 UPDATE) — Dior와의 동시 삽입 경합에서 중복키 예외 없음.
        transfers.upsertTransfer(
            bmi = e.core.bmi, seq = e.globalSeq, status = status.name,
            sender = e.core.senderCode, receiver = e.core.receiverCode, amount = e.core.amount,
            origin = e.originCenter, createdAt = e.seqEpochMillis, updatedAt = now, reason = reason,
            acspAt = now,
        )
        journalLogs.findById(e.globalSeq).ifPresent { it.applied = true; journalLogs.save(it) }
        return ResultMessage(
            journalEntry = e, status = status, processedCenter = centerId,
            debtorBalanceAfter = dBal, creditorBalanceAfter = cBal, reason = reason,
        )
    }
}
