package kr.or.bok.rtgs.gucci

import com.fasterxml.jackson.databind.ObjectMapper
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Component
import java.nio.charset.StandardCharsets
import java.util.Base64
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec

/** JWT 검증 실패(만료/서명오류/형식). */
class JwtException(msg: String) : RuntimeException(msg)

/** 토큰에서 복원한 인증 주체. */
data class Principal(val subject: String, val role: String, val orgCode: String?)

/**
 * 경량 JWT(HS256) — 외부 라이브러리 없이 표준 구조(header.payload.signature)로 서명·검증.
 * 프로토타입 자족 구현. 운영은 표준 라이브러리/키관리(HSM·회전)로 대체.
 */
@Component
class JwtService(
    private val mapper: ObjectMapper,
    @Value("\${gucci.jwt-secret}") private val secret: String,
    @Value("\${gucci.jwt-ttl-seconds:1800}") private val ttlSeconds: Long,
) {
    private val enc = Base64.getUrlEncoder().withoutPadding()
    private val dec = Base64.getUrlDecoder()

    fun ttl(): Long = ttlSeconds

    /** 주체 정보로 토큰 발급(iat/exp 포함). nowMs는 호출측이 주입(테스트 용이). */
    fun issue(p: Principal, nowMs: Long): String {
        val header = mapOf("alg" to "HS256", "typ" to "JWT")
        val iat = nowMs / 1000
        val payload = mapOf(
            "sub" to p.subject, "role" to p.role, "org" to p.orgCode,
            "iat" to iat, "exp" to iat + ttlSeconds,
        )
        val h = enc.encodeToString(mapper.writeValueAsBytes(header))
        val pl = enc.encodeToString(mapper.writeValueAsBytes(payload))
        val signingInput = "$h.$pl"
        return "$signingInput.${sign(signingInput)}"
    }

    /** 토큰 검증 후 주체 반환. 서명 불일치·만료·형식오류면 JwtException. */
    fun verify(token: String, nowMs: Long): Principal {
        val parts = token.split(".")
        if (parts.size != 3) throw JwtException("malformed token")
        val expected = sign("${parts[0]}.${parts[1]}")
        if (!constantTimeEquals(expected, parts[2])) throw JwtException("bad signature")
        val claims: Map<String, Any?> = mapper.readValue(dec.decode(parts[1]), Map::class.java)
            .let { @Suppress("UNCHECKED_CAST") (it as Map<String, Any?>) }
        val exp = (claims["exp"] as? Number)?.toLong() ?: throw JwtException("no exp")
        if (nowMs / 1000 > exp) throw JwtException("expired")
        return Principal(
            subject = claims["sub"] as? String ?: throw JwtException("no sub"),
            role = claims["role"] as? String ?: "",
            orgCode = claims["org"] as? String,
        )
    }

    private fun sign(input: String): String {
        val mac = Mac.getInstance("HmacSHA256")
        mac.init(SecretKeySpec(secret.toByteArray(StandardCharsets.UTF_8), "HmacSHA256"))
        return enc.encodeToString(mac.doFinal(input.toByteArray(StandardCharsets.UTF_8)))
    }

    private fun constantTimeEquals(a: String, b: String): Boolean {
        val x = a.toByteArray(); val y = b.toByteArray()
        if (x.size != y.size) return false
        var r = 0
        for (i in x.indices) r = r or (x[i].toInt() xor y[i].toInt())
        return r == 0
    }
}
