package kr.or.bok.rtgs.common.iso20022

import kr.or.bok.rtgs.common.CoreMessage
import kr.or.bok.rtgs.common.Msg
import org.w3c.dom.Element
import org.w3c.dom.Node
import java.io.ByteArrayInputStream
import java.nio.charset.StandardCharsets
import javax.xml.parsers.DocumentBuilderFactory

/**
 * ISO 20022 전문 XML <-> 객체 변환 + 핵심전문(CoreMessage) 추출.
 *
 * pacs.008.001.08은 네임스페이스가 있는 깊은 중첩 구조라 **DOM 파싱**으로 지역명(local name) 기준
 * 탐색해 핵심 항목만 뽑는다(네임스페이스 프리픽스에 견고, XXE 차단). pacs.002.001.10 응답은
 * **정식 네임스페이스 템플릿**으로 생성한다.
 */
object Iso20022Codec {

    private const val PACS002_NS = "urn:iso:std:iso:20022:tech:xsd:pacs.002.001.10"

    /** XXE 방어 + 네임스페이스 인식 DOM 파서 팩토리. */
    private val dbf: DocumentBuilderFactory = DocumentBuilderFactory.newInstance().apply {
        isNamespaceAware = true
        runCatching { setFeature("http://apache.org/xml/features/disallow-doctype-decl", true) }
        runCatching { setFeature("http://xml.org/sax/features/external-general-entities", false) }
        runCatching { setFeature("http://xml.org/sax/features/external-parameter-entities", false) }
        isXIncludeAware = false
        isExpandEntityReferences = false
    }

    /** 접수 pacs.008 원전문에서 핵심 항목 추출. */
    fun parsePacs008(rawXml: String): Pacs008View {
        val doc = dbf.newDocumentBuilder()
            .parse(ByteArrayInputStream(rawXml.toByteArray(StandardCharsets.UTF_8)))
        doc.documentElement.normalize()

        val tx = firstByLocal(doc.documentElement, "CdtTrfTxInf")
            ?: throw IllegalArgumentException("CdtTrfTxInf 없음")
        val amtEl = firstByLocal(tx, "IntrBkSttlmAmt")
            ?: throw IllegalArgumentException("IntrBkSttlmAmt 없음")

        return Pacs008View(
            bizMsgIdr = text(firstByLocal(tx, "EndToEndId")).ifBlank { null },
            msgDefIdr = Msg.PACS008,
            from = agentMmbId(childByLocal(tx, "DbtrAgt")),
            to = agentMmbId(childByLocal(tx, "CdtrAgt")),
            dbtrAcct = acctId(childByLocal(tx, "DbtrAcct")),
            cdtrAcct = acctId(childByLocal(tx, "CdtrAcct")),
            ccy = amtEl.getAttribute("Ccy").ifBlank { "KRW" },
            amount = text(amtEl).toLongOrNull() ?: 0L,
            creDtTm = text(firstByLocal(doc.documentElement, "CreDtTm")).ifBlank { null },
        )
    }

    /**
     * 핵심 명령문 추출. bmi가 비어 있으면 접수센터가 부여한다.
     * 원전문 해시(무결성)와 함께 경량 명령문(CoreMessage)을 만든다.
     */
    fun toCoreMessage(req: Pacs008View, rawXml: String, assignedBmi: String): CoreMessage {
        val bmi = req.bizMsgIdr?.takeIf { it.isNotBlank() } ?: assignedBmi
        return CoreMessage(
            bmi = bmi,
            msgType = req.msgDefIdr.ifBlank { Msg.PACS008 },
            senderCode = req.from,
            receiverCode = req.to,
            debtorAcct = req.dbtrAcct,
            creditorAcct = req.cdtrAcct,
            amount = req.amount,
            currency = req.ccy.ifBlank { "KRW" },
            origHash = CoreMessage.sha256Hex(rawXml),
        )
    }

    /** pacs.002.001.10(결과통보) 정식 전문 생성. */
    fun writePacs002(msg: Pacs002): String {
        val clrSysRef = msg.clrSysRef?.let {
            "\n      <ClrSysRef>${escape(it.take(35))}</ClrSysRef>"
        } ?: ""
        return """<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="$PACS002_NS">
  <FIToFIPmtStsRpt>
    <GrpHdr>
      <MsgId>${escape(msg.orgnlBizMsgIdr.ifBlank { "RTGS" }.take(35))}</MsgId>
      <CreDtTm>${escape(msg.creDtTm ?: "")}</CreDtTm>
    </GrpHdr>
    <TxInfAndSts>
      <OrgnlEndToEndId>${escape(msg.orgnlBizMsgIdr.take(35))}</OrgnlEndToEndId>
      <TxSts>${escape(msg.txSts)}</TxSts>$clrSysRef
    </TxInfAndSts>
  </FIToFIPmtStsRpt>
</Document>"""
    }

    // --- DOM 헬퍼 (지역명 기준 탐색) ---

    /** 직계 자식 중 지역명이 일치하는 첫 요소. */
    private fun childByLocal(parent: Element?, local: String): Element? {
        if (parent == null) return null
        val kids = parent.childNodes
        for (i in 0 until kids.length) {
            val n = kids.item(i)
            if (n.nodeType == Node.ELEMENT_NODE && localName(n) == local) return n as Element
        }
        return null
    }

    /** 하위(자손) 전체에서 지역명이 일치하는 첫 요소. */
    private fun firstByLocal(scope: Element, local: String): Element? {
        val nl = scope.getElementsByTagNameNS("*", local)
        if (nl.length > 0) return nl.item(0) as Element
        val nl2 = scope.getElementsByTagName(local) // 네임스페이스 없는 문서 대비
        return if (nl2.length > 0) nl2.item(0) as Element else null
    }

    /** Agent(BranchAndFinancialInstitutionIdentification6) → FinInstnId/ClrSysMmbId/MmbId. */
    private fun agentMmbId(agent: Element?): String =
        text(agent?.let { firstByLocal(it, "MmbId") })

    /** CashAccount38 → Id/Othr/Id (없으면 IBAN). */
    private fun acctId(acct: Element?): String {
        if (acct == null) return ""
        val id = childByLocal(acct, "Id") ?: return ""
        childByLocal(id, "Othr")?.let { othr -> return text(childByLocal(othr, "Id")) }
        return text(childByLocal(id, "IBAN"))
    }

    private fun localName(n: Node): String = n.localName ?: n.nodeName.substringAfter(':')

    private fun text(e: Element?): String = e?.textContent?.trim() ?: ""

    private fun escape(s: String): String = s
        .replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
}
