package kr.or.bok.rtgs.gucci

import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Component
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicInteger

/**
 * 유량제어(rate limit) — subject(기관)별 고정 윈도우 카운터. 순번기 상한·코어 보호(성능목표).
 * 프로토타입은 인메모리(센터 로컬). 다센터·다인스턴스는 분산 카운터(Redis 등)로 확장.
 */
@Component
class RateLimiter(
    @Value("\${gucci.rate-limit-per-window:50}") private val limit: Int,
    @Value("\${gucci.rate-window-seconds:10}") private val windowSeconds: Long,
) {
    private class Window(val startSec: Long) { val count = AtomicInteger(0) }
    private val windows = ConcurrentHashMap<String, Window>()

    /** 허용이면 true. 초과면 false(429). nowMs 주입(테스트 용이). */
    fun allow(subject: String, nowMs: Long): Boolean {
        val bucket = nowMs / 1000 / windowSeconds
        val w = windows.compute(subject) { _, cur ->
            if (cur == null || cur.startSec != bucket) Window(bucket) else cur
        }!!
        return w.count.incrementAndGet() <= limit
    }
}

/**
 * 재전송(replay) 방지 — 요청별 nonce+timestamp. 시각오차 초과 또는 nonce 재사용이면 거부.
 * 부인방지(원전문 해시)와 함께 이중제출·재생 공격 차단.
 */
@Component
class ReplayGuard(
    @Value("\${gucci.replay-skew-seconds:30}") private val skewSeconds: Long,
    @Value("\${gucci.replay-nonce-ttl-seconds:120}") private val nonceTtlSeconds: Long,
) {
    private val seen = ConcurrentHashMap<String, Long>() // nonce -> 만료 epochSec

    sealed class Result { object Ok : Result(); data class Reject(val reason: String) : Result() }

    /** 검사 + 통과 시 nonce 등록. nowMs 주입. */
    fun check(nonce: String?, timestampSec: Long?, nowMs: Long): Result {
        if (nonce.isNullOrBlank() || timestampSec == null) return Result.Reject("missing nonce/timestamp")
        val nowSec = nowMs / 1000
        if (kotlin.math.abs(nowSec - timestampSec) > skewSeconds) return Result.Reject("timestamp out of window")
        prune(nowSec)
        val prev = seen.putIfAbsent(nonce, nowSec + nonceTtlSeconds)
        return if (prev != null) Result.Reject("nonce replayed") else Result.Ok
    }

    private fun prune(nowSec: Long) {
        if (seen.size > 10_000) seen.entries.removeIf { it.value < nowSec }
    }
}
