package kr.or.bok.rtgs.gucci

import kr.or.bok.rtgs.common.iso20022.Iso20022Codec
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Value
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestHeader
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.client.RestClient
import org.springframework.web.server.ResponseStatusException

/**
 * GUCCI 관문 API — 인증(로그인·JWT) + 인증된 리버스프록시(→ Chanel) + 유량제어 + 재전송 차단 + 감사로그.
 * 참가기관은 Gucci(:8095)를 통해 접수/조회하고, 코어(Chanel:8091)는 내부에 둔다.
 */
@RestController
class GucciController(
    private val auth: AuthService,
    private val jwt: JwtService,
    private val rateLimiter: RateLimiter,
    private val replayGuard: ReplayGuard,
    private val http: RestClient,
    @Value("\${gucci.chanel-base-url}") private val chanelBase: String,
    @Value("\${rtgs.center-id:DC1}") private val centerId: String,
) {
    private val log = LoggerFactory.getLogger(javaClass)
    private fun now() = System.currentTimeMillis()

    data class LoginReq(val username: String = "", val secret: String = "")
    data class ChangePwReq(val username: String = "", val oldSecret: String = "", val newSecret: String = "")

    /** ① 로그인 인증 → JWT 발급. */
    @PostMapping("/gucci/auth/login")
    fun login(@RequestBody req: LoginReq): Map<String, Any?> = try {
        val r = auth.login(req.username, req.secret, now())
        mapOf("token" to r.token, "role" to r.role, "orgCode" to r.orgCode,
            "tokenType" to "Bearer", "expiresIn" to r.expiresIn, "mustChangePassword" to r.mustChangePassword)
    } catch (e: AuthFailedException) {
        throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "invalid credentials")
    }

    /** ①-2 초기암호/비밀번호 변경. */
    @PostMapping("/gucci/auth/change-password")
    fun changePassword(@RequestBody req: ChangePwReq): Map<String, Any?> = try {
        auth.changePassword(req.username, req.oldSecret, req.newSecret)
        mapOf("ok" to true)
    } catch (e: AuthFailedException) {
        throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "invalid credentials")
    }

    /** ② 자금이체 신청(접수) — 인증·유량·재전송·기관바인딩 검사 후 Chanel로 전달. */
    @PostMapping(
        "/gucci/pay/customer",
        consumes = [MediaType.APPLICATION_XML_VALUE, MediaType.TEXT_XML_VALUE, MediaType.TEXT_PLAIN_VALUE],
        produces = [MediaType.APPLICATION_XML_VALUE],
    )
    fun pay(
        @RequestBody rawXml: String,
        @RequestHeader(value = "Authorization", required = false) authz: String?,
        @RequestHeader(value = "X-Nonce", required = false) nonce: String?,
        @RequestHeader(value = "X-Timestamp", required = false) timestamp: String?,
    ): String {
        val p = authenticate(authz)
        requireRole(p, "ORG_S", "ADMIN")            // 송신기관만 신청
        enforceRate(p)
        enforceReplay(p, nonce, timestamp)
        enforceOrgBinding(p, rawXml)                // 토큰 기관 == 전문 송신기관
        audit("PAY", p, "allow", null)
        return forwardPost("/pay/customer", rawXml)
    }

    /** ③ 거래 상태 조회 — 인증된 참가기관/관리자. */
    @GetMapping("/gucci/inquiry/{bmi}", produces = [MediaType.APPLICATION_JSON_VALUE])
    fun inquiry(@PathVariable bmi: String, @RequestHeader("Authorization", required = false) authz: String?): String {
        val p = authenticate(authz); enforceRate(p)
        audit("INQUIRY", p, "allow", bmi)
        return forwardGet("/inquiry/$bmi")
    }

    /** ④ 참가기관 잔액 목록 — 관리자만(대사·모니터링). */
    @GetMapping("/gucci/accounts", produces = [MediaType.APPLICATION_JSON_VALUE])
    fun accounts(@RequestHeader("Authorization", required = false) authz: String?): String {
        val p = authenticate(authz); requireRole(p, "ADMIN"); enforceRate(p)
        audit("ACCOUNTS", p, "allow", null)
        return forwardGet("/accounts")
    }

    /** ⑤ 센터 헬스체크(관측용). 로컬은 단일 센터. 다센터·정족수/펜싱은 G3. */
    @GetMapping("/gucci/health/centers", produces = [MediaType.APPLICATION_JSON_VALUE])
    fun centers(): Map<String, Any?> {
        val chanelUp = runCatching { http.get().uri("$chanelBase/accounts").retrieve().toBodilessEntity() }.isSuccess
        return mapOf(
            "self" to centerId,
            "centers" to listOf(mapOf("id" to centerId, "core(chanel)" to if (chanelUp) "UP" else "DOWN")),
            "note" to "단일 센터(G1). 다센터 정족수/펜싱은 G3(고사양 PC 이후).",
        )
    }

    // ---------- 내부 검사·전달 ----------

    private fun authenticate(authz: String?): Principal {
        val token = authz?.removePrefix("Bearer ")?.trim()
        if (token.isNullOrBlank()) throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "missing bearer token")
        return try { jwt.verify(token, now()) }
        catch (e: JwtException) { throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "invalid token: ${e.message}") }
    }

    private fun requireRole(p: Principal, vararg allowed: String) {
        if (p.role !in allowed) {
            audit("AUTHZ", p, "deny", "role ${p.role} not in ${allowed.toList()}")
            throw ResponseStatusException(HttpStatus.FORBIDDEN, "role not permitted")
        }
    }

    private fun enforceRate(p: Principal) {
        if (!rateLimiter.allow(p.subject, now())) {
            audit("RATE", p, "deny", "rate limit exceeded")
            throw ResponseStatusException(HttpStatus.TOO_MANY_REQUESTS, "rate limit exceeded")
        }
    }

    private fun enforceReplay(p: Principal, nonce: String?, timestamp: String?) {
        val ts = timestamp?.toLongOrNull()
        when (val r = replayGuard.check(nonce, ts, now())) {
            is ReplayGuard.Result.Reject -> {
                audit("REPLAY", p, "deny", r.reason)
                throw ResponseStatusException(HttpStatus.BAD_REQUEST, "replay check failed: ${r.reason}")
            }
            else -> {}
        }
    }

    /** 토큰의 기관코드와 전문 송신기관(DbtrAgt MmbId) 일치 강제(관리자 예외). */
    private fun enforceOrgBinding(p: Principal, rawXml: String) {
        if (p.role == "ADMIN") return
        val sender = runCatching { Iso20022Codec.parsePacs008(rawXml).from }.getOrNull() ?: return // 파싱 실패는 Chanel XSD가 반려
        if (p.orgCode != sender) {
            audit("ORGBIND", p, "deny", "token org ${p.orgCode} != sender $sender")
            throw ResponseStatusException(HttpStatus.FORBIDDEN, "token org does not match message sender")
        }
    }

    private fun forwardPost(path: String, body: String): String = try {
        http.post().uri("$chanelBase$path")
            .contentType(MediaType.APPLICATION_XML)
            .body(body).retrieve().body(String::class.java) ?: ""
    } catch (e: Exception) {
        throw ResponseStatusException(HttpStatus.BAD_GATEWAY, "core unavailable: ${e.message}")
    }

    private fun forwardGet(path: String): String = try {
        http.get().uri("$chanelBase$path").retrieve().body(String::class.java) ?: ""
    } catch (e: Exception) {
        throw ResponseStatusException(HttpStatus.BAD_GATEWAY, "core unavailable: ${e.message}")
    }

    private fun audit(event: String, p: Principal, decision: String, detail: String?) {
        log.info("AUDIT event={} subject={} role={} org={} decision={} detail={}",
            event, p.subject, p.role, p.orgCode, decision, detail ?: "")
    }
}
