package kr.or.bok.rtgs.louisvuitton

import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.time.Duration
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.Executors
import java.util.concurrent.ThreadLocalRandom
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicLong

/** 콘솔 부하테스트 요청 파라미터(동시 요청 수 기반 closed-loop). */
data class LoadTestParams(
    val concurrency: Int = 10,
    val durationSec: Int = 15,
    val chanelUrl: String = "http://localhost:8091",
    val amountMin: Long = 1,
    val amountMax: Long = 200,
)

/**
 * 루이비똥 내장 부하생성기 — Chanel `/pay/customer`에 pacs.008.001.08 전문을 실시간 생성·투입.
 *
 * 동시 요청 수 기반 closed-loop: 가상 스레드 워커 N개가 각자 완료 즉시 다음 건을 보내며 D초간 반복.
 * 외부 k6 의존 없이 서비스 안에서 백엔드와 동일한 전문 포맷을 쓴다(포맷 어긋남 방지).
 * 한 번에 하나의 테스트만 수행한다(running 가드).
 */
@Service
class LoadTestService(private val accounts: AccountRepository) {

    private val log = LoggerFactory.getLogger(javaClass)

    private val http: HttpClient = HttpClient.newBuilder()
        .connectTimeout(Duration.ofSeconds(5))
        .executor(Executors.newVirtualThreadPerTaskExecutor())
        .build()

    // ---- 실행 상태 ----
    private val running = AtomicBoolean(false)
    @Volatile private var params: LoadTestParams = LoadTestParams()
    @Volatile private var startMs: Long = 0
    @Volatile private var endMs: Long = 0
    @Volatile private var deadline: Long = 0
    @Volatile private var codes: List<String> = emptyList()

    private val sent = AtomicLong(0)
    private val accepted = AtomicLong(0)
    private val rejected = AtomicLong(0)
    private val errors = AtomicLong(0)
    private val latSumMs = AtomicLong(0)
    private val latencies = ConcurrentLinkedQueue<Long>()   // ms 표본

    // BMI 일련번호: JVM 시작 시각으로 시드 → 재기동/여러 run에도 사실상 유일.
    private val serialSeq = AtomicLong(System.currentTimeMillis() % 10_000_000_000L)
    private val bizDate: String = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"))

    val isRunning: Boolean get() = running.get()

    /** 테스트 시작. 이미 실행 중이면 false. */
    fun start(p: LoadTestParams): Boolean {
        if (!running.compareAndSet(false, true)) return false
        val codeList = accounts.findAll().map { it.code }.filter { it.isNotBlank() }.sorted()
        if (codeList.size < 2) {
            running.set(false)
            throw IllegalStateException("참가기관 계좌가 2개 미만입니다(현재 ${codeList.size}). 부하 생성 불가.")
        }
        params = p
        codes = codeList
        sent.set(0); accepted.set(0); rejected.set(0); errors.set(0); latSumMs.set(0)
        latencies.clear()
        startMs = System.currentTimeMillis()
        endMs = 0
        deadline = startMs + p.durationSec.coerceIn(1, 3600) * 1000L
        val workerCount = p.concurrency.coerceIn(1, 2000)

        // 코디네이터(데몬) — 워커 완료까지 대기 후 종료 마킹. start()는 즉시 반환.
        Thread {
            val exec = Executors.newVirtualThreadPerTaskExecutor()
            try {
                val tasks = (1..workerCount).map { exec.submit { worker() } }
                tasks.forEach { runCatching { it.get() } }
            } finally {
                exec.shutdown()
                endMs = System.currentTimeMillis()
                running.set(false)
                log.info("[loadtest] done: sent={} accepted={} rejected={} errors={} in {}ms",
                    sent.get(), accepted.get(), rejected.get(), errors.get(), endMs - startMs)
            }
        }.apply { isDaemon = true; name = "loadtest-coordinator" }.start()

        log.warn("[loadtest] start: concurrency={} durationSec={} target={} amount={}~{}",
            workerCount, p.durationSec, p.chanelUrl, p.amountMin, p.amountMax)
        return true
    }

    /** 실행 중 워커를 즉시 종료(마감을 현재로 당김). */
    fun stop() {
        if (running.get()) {
            deadline = System.currentTimeMillis()
            log.warn("[loadtest] stop requested")
        }
    }

    private fun worker() {
        val rnd = ThreadLocalRandom.current()
        val url = URI.create(params.chanelUrl.trimEnd('/') + "/pay/customer")
        val lo = params.amountMin
        val span = (params.amountMax - params.amountMin + 1).coerceAtLeast(1)
        while (System.currentTimeMillis() < deadline) {
            val s = codes[rnd.nextInt(codes.size)]
            var r = codes[rnd.nextInt(codes.size)]
            while (r == s) r = codes[rnd.nextInt(codes.size)]
            val amount = lo + (rnd.nextLong(span))
            val xml = buildPacs008(s, r, amount)
            val req = HttpRequest.newBuilder(url)
                .timeout(Duration.ofSeconds(30))
                .header("Content-Type", "application/xml")
                .POST(HttpRequest.BodyPublishers.ofString(xml))
                .build()
            val t0 = System.nanoTime()
            try {
                val resp = http.send(req, HttpResponse.BodyHandlers.ofString())
                record(System.nanoTime() - t0)
                when {
                    resp.statusCode() == 200 && resp.body().contains("RCVD") -> accepted.incrementAndGet()
                    resp.statusCode() == 200 -> rejected.incrementAndGet()
                    else -> errors.incrementAndGet()
                }
            } catch (e: Exception) {
                record(System.nanoTime() - t0)
                errors.incrementAndGet()
            }
        }
    }

    private fun record(nanos: Long) {
        val ms = nanos / 1_000_000
        sent.incrementAndGet()
        latSumMs.addAndGet(ms)
        latencies.add(ms)
    }

    /** BMI = 날짜(8) + 송신기관(4) + 일련번호(10) = 22자리. */
    private fun buildPacs008(s: String, r: String, amount: Long): String {
        val serial = (serialSeq.incrementAndGet() % 10_000_000_000L).toString().padStart(10, '0')
        val bmi = bizDate + s + serial
        val creDtTm = java.time.LocalDateTime.now().withNano(0)
            .format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"))
        return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
            "<Document xmlns=\"urn:iso:std:iso:20022:tech:xsd:pacs.008.001.08\">" +
            "<FIToFICstmrCdtTrf>" +
            "<GrpHdr><MsgId>$bmi</MsgId><CreDtTm>$creDtTm</CreDtTm><NbOfTxs>1</NbOfTxs>" +
            "<SttlmInf><SttlmMtd>CLRG</SttlmMtd></SttlmInf></GrpHdr>" +
            "<CdtTrfTxInf>" +
            "<PmtId><EndToEndId>$bmi</EndToEndId></PmtId>" +
            "<IntrBkSttlmAmt Ccy=\"KRW\">$amount</IntrBkSttlmAmt>" +
            "<ChrgBr>SLEV</ChrgBr>" +
            "<Dbtr><Nm>BANK-$s</Nm></Dbtr>" +
            "<DbtrAcct><Id><Othr><Id>ACC-$s</Id></Othr></Id></DbtrAcct>" +
            "<DbtrAgt><FinInstnId><ClrSysMmbId><MmbId>$s</MmbId></ClrSysMmbId></FinInstnId></DbtrAgt>" +
            "<CdtrAgt><FinInstnId><ClrSysMmbId><MmbId>$r</MmbId></ClrSysMmbId></FinInstnId></CdtrAgt>" +
            "<Cdtr><Nm>BANK-$r</Nm></Cdtr>" +
            "<CdtrAcct><Id><Othr><Id>ACC-$r</Id></Othr></Id></CdtrAcct>" +
            "</CdtTrfTxInf></FIToFICstmrCdtTrf></Document>"
    }

    /** 실시간 스냅샷(진행 중/후 모두). */
    fun status(): Map<String, Any?> {
        val now = if (running.get()) System.currentTimeMillis() else endMs.takeIf { it > 0 } ?: System.currentTimeMillis()
        val elapsedMs = (now - startMs).coerceAtLeast(0)
        val elapsedSec = elapsedMs / 1000.0
        val sentN = sent.get()
        return mapOf(
            "running" to running.get(),
            "params" to params,
            "elapsedMs" to elapsedMs,
            "durationSec" to params.durationSec,
            "sent" to sentN,
            "accepted" to accepted.get(),
            "rejected" to rejected.get(),
            "errors" to errors.get(),
            "tps" to if (elapsedSec > 0) round2(sentN / elapsedSec) else 0.0,
            "avgLatencyMs" to if (sentN > 0) latSumMs.get() / sentN else 0,
        )
    }

    /** 최종 요약(백분위 포함). 실행 중이면 그 시점까지의 값. */
    fun result(): Map<String, Any?> {
        val sentN = sent.get()
        val acc = accepted.get()
        val end = if (endMs > 0) endMs else System.currentTimeMillis()
        val durMs = (end - startMs).coerceAtLeast(1)
        val durSec = durMs / 1000.0
        val arr = latencies.toLongArray().also { it.sort() }
        fun pct(p: Double): Long = if (arr.isEmpty()) 0 else arr[((p / 100.0 * arr.size).toInt()).coerceIn(0, arr.size - 1)]
        return mapOf(
            "running" to running.get(),
            "params" to params,
            "durationMs" to durMs,
            "sent" to sentN,
            "accepted" to acc,
            "rejected" to rejected.get(),
            "errors" to errors.get(),
            "successRate" to if (sentN > 0) round2(acc * 100.0 / sentN) else 0.0,
            "throughputTps" to if (durSec > 0) round2(sentN / durSec) else 0.0,
            "latencyMs" to mapOf(
                "min" to (arr.firstOrNull() ?: 0),
                "avg" to if (sentN > 0) latSumMs.get() / sentN else 0,
                "p50" to pct(50.0), "p90" to pct(90.0), "p95" to pct(95.0),
                "p99" to pct(99.0), "max" to (arr.lastOrNull() ?: 0),
            ),
        )
    }

    private fun round2(v: Double): Double = Math.round(v * 100.0) / 100.0
}
