package com.jihe.punchnet.sdlan.network

import com.google.protobuf.kotlin.toByteString
import com.google.gson.JsonElement
import com.jihe.punchnet.protobuf.PunchProto.SDLData
import com.jihe.punchnet.protobuf.PunchProto.SDLPeerInfo
import com.jihe.punchnet.protobuf.PunchProto.SDLQueryInfo
import com.jihe.punchnet.protobuf.PunchProto.SDLRegister
import com.jihe.punchnet.protobuf.PunchProto.SDLRegisterAck
import com.jihe.punchnet.protobuf.PunchProto.SDLArpRequest
import com.jihe.punchnet.protobuf.PunchProto.SDLArpResponse
import com.jihe.punchnet.protobuf.PunchProto.SDLExposedServiceRequest
import com.jihe.punchnet.protobuf.PunchProto.SDLPolicyRequest

import com.jihe.punchnet.sdlan.config.EtherType
import com.jihe.punchnet.sdlan.config.SDLanConfig
import com.jihe.punchnet.sdlan.logs.TerminalLogger
import com.jihe.punchnet.sdlan.utils.isMultiBroadcast
import com.jihe.punchnet.sdlan.utils.ipStringToInt
import com.jihe.punchnet.sdlan.utils.ipToString
import com.jihe.punchnet.sdlan.utils.macToString
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import java.net.URI
import java.nio.ByteBuffer
import java.nio.ByteOrder


suspend fun handlePacketData(
    scope: CoroutineScope,
    node: Node,
    body: ByteBuffer,
    senderSock: SDLanSock
) {
    val data: SDLData
    try {
        data = SDLData.parseFrom(body)
    } catch (e: Exception) {
        TerminalLogger.errorf { "failed to decode DATA: $e" }
        return
    }

    if (data.isP2P) {
        TerminalLogger.debugf {
            "[P2P] Rx data from ${senderSock}"
        }
        // CoroutineScope(Dispatchers.Default).async {
        scope.async {
            checkPeerRegistrationNeeded(
                node,
                false,
                data.srcMac,
                NatType.NoNat,
                senderSock,
            )
        }
    } else {
        TerminalLogger.debugf {
            "[PsP] Rx data from ${macToString(data.srcMac)} via ${senderSock}"
        }
    }
    handleTunPacket(node, senderSock, !data.isP2P, data)
}

suspend fun handleTunPacket(
    node: Node,
    senderSock: SDLanSock,
    fromSN: Boolean,
    data: SDLData,
) {
    val payload = data.data
    val origin = node.aes.decrypt(payload.toByteArray())
    if (origin == null) {
        TerminalLogger.errorf { "error to decrypt data" }
        return
    }
    TerminalLogger.debugf {"handle data from net"}
    node.iface?.handleDataFromNet(node, origin)
}

fun handlePacketRegisterACK(
    node: Node,
    body: ByteBuffer,
    senderSock: SDLanSock
) {

    val ack: SDLRegisterAck
    try {
        ack = SDLRegisterAck.parseFrom(body)
    } catch (e: Exception) {
        TerminalLogger.errorf { "failed to decode REGISTERACK: $e" }
        return
    }

    val viaMulticast = isMultiBroadcast(ack.dstMac)
    if (viaMulticast && ack.srcMac == node.mac) {
        TerminalLogger.debugf { "skip register from self" }
        return
    }

    TerminalLogger.infof {
        "Rx REGISTERACK from ${macToString(ack.srcMac)} [${senderSock}] to ${macToString(ack.dstMac)}"
    }

    peerSetP2PConfirmed(node, ack.srcMac, senderSock)
}

fun peerSetP2PConfirmed(node: Node, mac: Mac, sock: SDLanSock) {
    val scan = node.pendingPeers.remove(mac)
    if (scan == null) {
        TerminalLogger.errorf { "failed to find sender in pending peers: ${sock}" }
        return
    }
    val now = System.currentTimeMillis()/1000
    var scan2 = node.knownPeers.get(mac)
    if (scan2 == null) {
        node.knownPeers[mac] = scan
        scan2 = node.knownPeers.get(mac)
    }
    scan2?.sock = sock
    scan2?.lastP2P?.set(now)
    scan2?.lastSeen?.set(now)

    TerminalLogger.debugf {
        "P2P connection established: ${macToString(mac)} [${sock}]"
    }

    TerminalLogger.debugf {
        "===> new Peer: ${macToString(mac)} -> ${sock}"
    }

}

suspend fun handlePacketRegister(
    node: Node,
    body: ByteBuffer,
    senderSock: SDLanSock
) {
    if(!node.aes.isAuthorized()) {
        TerminalLogger.errorf { "drop register due to not authorized"}
        return
    }
    val reg: SDLRegister
    try {
        reg = SDLRegister.parseFrom(body)
    } catch (e: Exception) {
        TerminalLogger.errorf { "failed to parse REGISTER: $e"}
        return
    }
    val srcMac = reg.srcMac

    val viaMulticast = isMultiBroadcast(reg.dstMac)
    if (viaMulticast && reg.srcMac == node.mac) {
        TerminalLogger.debugf { "skip register from self" }
        return
    }

    // must be from peer

    TerminalLogger.infof {
        "[P2P] Rx REGISTER from ${senderSock}, deleting from pending"
    }
    val remoteNAT = node.pendingPeers.remove(srcMac)?.natType?:NatType.Invalid

    sendRegisterACK(node,senderSock, reg)

    checkPeerRegistrationNeeded(node, false, reg.srcMac, remoteNAT, senderSock)
}

suspend fun sendRegisterACK(
    node: Node,
    sender: SDLanSock,
    reg: SDLRegister,
) {
    if (!node.config.allowP2P) {
        TerminalLogger.debugf {
            "Skipping REGISTER ACK as P2P is not allowed"
        }
        return
    }

    // must be authed
    val ack = SDLRegisterAck.newBuilder()
        .setNetworkId(node.networkID.get())
        .setSrcMac(node.deviceConfig.mac)
        .setDstMac(reg.srcMac)
        .build()

    val data = encodeToUDPMessage(ack, PacketType.RegisterACK)
    sendToSock(node, data, sender)
}

suspend fun checkPeerRegistrationNeeded(
    node: Node,
    fromSN: Boolean,
    srcMac: Mac,
    remoteNat: NatType,
    peerSock: SDLanSock,
) {
    val peer = node.knownPeers.get(srcMac)
    if (peer == null) {
        TerminalLogger.debugf {
            "not found in known, send REGISTER to peer"
        }
        registerWithNewPeer(node,srcMac, remoteNat, peerSock, System.currentTimeMillis()/1000)
        return
    }

    val now = System.currentTimeMillis()/1000
    if (!fromSN) {
        peer.lastP2P.set(now)
    }

    if (peerSock.family != peer.sock.family) {
        TerminalLogger.errorf { "family changed, just ignore it"}
        return
    }
    if (now - peer.lastSeen.get() > 3) {
        checkKnownPeerSockChanged(node, fromSN, srcMac, peerSock, now)
    }

}
suspend fun checkKnownPeerSockChanged(
    node: Node,
    fromSN: Boolean,
    srcMac: Mac,
    peerSock: SDLanSock,
    now: Long,
) {
    if (isMultiBroadcast(srcMac)) {
        return
    }
    val peer = node.knownPeers.get(srcMac)
    if (peer == null) {
        return
    }

    if (!peerSock.is_equal(peer.sock)) {
        if (!fromSN) {
            TerminalLogger.infof {
                "peer changed: ${srcMac}: ${peer.sock} -> $peerSock"
            }
            val remoteNAT = peer.natType
            node.knownPeers.remove(srcMac)

            registerWithNewPeer(node, srcMac, remoteNAT, peerSock, now)
        }
    } else {
        peer.lastSeen.set(now)
    }
}

suspend fun registerWithNewPeer(
    node: Node,
    // fromSN: Boolean,
    mac: Mac,
    remoteNat: NatType,
    peerSock: SDLanSock,
    now: Long,
) {
    var peer = node.pendingPeers.get(mac)
    if (peer == null) {
        TerminalLogger.debugf {
            "===> new pending: ${macToString(mac)} => ${peerSock}"
        }
        node.pendingPeers[mac] = EdgePeer(0, node.deviceConfig.ip.netBitLen, peerSock, null, now)
        peer = node.pendingPeers.get(mac)
        peer?.lastSeen?.set(now)

        sendRegister(node, remoteNat, peerSock, mac)
        registerWithLocalPeers(node)
    } else {
        peer.sock = peerSock
        peer.lastSeen.set(now)
    }

}

suspend fun registerWithLocalPeers(node: Node) {
    if (!node.config.dropMulticast) {
        sendRegister(node, NatType.NoNat, node.multicastSock, SDLanConfig.BROADCASTMAC)
    }
}

suspend fun sendRegister(
    node: Node,
    natType: NatType,
    sock: SDLanSock,
    mac: Mac,
) {
    if (!node.config.allowP2P) {
        TerminalLogger.debugf { "skipping REGISTER as p2p is disabled" }
        return
    }

    if (!node.aes.isAuthorized()) {
        TerminalLogger.debugf { "skipping REGISTER as not authed" }
        return
    }

    val register = SDLRegister.newBuilder()
        .setNetworkId(node.networkID.get())
        .setSrcMac(node.deviceConfig.mac)
        .setDstMac(mac)
        .build()
    val msg = encodeToUDPMessage(register, PacketType.Register)

    TerminalLogger.debugf { "send REGISTER to ${macToString(mac)} at $sock, natType=$natType" }
    sendToSock(node, msg, sock)

    if (natType == NatType.Symmetric || natType == NatType.PortRestricted) {
        TerminalLogger.debugf { "guess ports for symmetric/restricted nat" }
        val guessOffsets = intArrayOf(-1, 1, -2, 2)
        for (offset in guessOffsets) {
            val guessPort = sock.port + offset
            if (guessPort in 1..65535) {
                val guessSock = SDLanSock(sock.family, guessPort, sock.ip)
                sendToSock(node, msg, guessSock)
            }
        }
    }
}

suspend fun sendPacketToNet(node: Node, dstmac: Mac, encrypted: ByteArray, size: Long) {
    var is_p2p: Boolean = false
    var is_multicast: Boolean = false
    var destination: SDLanSock

    if (isMultiBroadcast(dstmac)) {
        node.stats.txSup.addAndGet(size)
        node.stats.txBroadcast.addAndGet(size)
        destination = node.config.stunServer
        is_multicast = true
    } else {
        val peer = node.knownPeers.get(dstmac)
        if (peer == null) {
            node.stats.txSup.addAndGet(size)
            destination = node.config.stunServer
        } else {
            val now = System.currentTimeMillis() / 1000
            if (now - peer.lastP2P.get() >= peer.timeout / 2) {
                TerminalLogger.warning { "last p2p is too old, deleting from known hosts" }
                node.knownPeers.remove(dstmac)
                node.stats.txSup.addAndGet(size)
                destination = node.config.stunServer
            } else {
                is_p2p = true
                node.stats.txP2P.addAndGet(size)
                destination = peer.sock
            }
        }
    }

    if (!is_p2p && !is_multicast) {
        TerminalLogger.debugf { "check_query_peer_info" }
        checkQueryPeerInfo(node, dstmac)
    }

    val transportIdentityId = node.connectData?.identityId ?: 0
    val data = com.jihe.punchnet.protobuf.PunchProto.SDLData.newBuilder()
        .setIsP2P(is_p2p)
        .setNetworkId(node.networkID.get())
        .setTtl(2)
        .setSrcMac(node.mac)
        .setDstMac(dstmac)
        .setData(com.google.protobuf.ByteString.copyFrom(encrypted))
        .setSessionToken(node.sessionToken)
        .setIdentityId(transportIdentityId)
        .build()

    val msg = encodeToUDPMessage(data, PacketType.Data)
    TerminalLogger.debugf {
        "send PACKET to ${destination} with isP2P=$is_p2p, ttl=${data.ttl}, " +
            "src=${macToString(data.srcMac)}, dst=${macToString(data.dstMac)}, " +
            "network=${data.networkId}, identityId=${data.identityId}, apiIdentity=${node.connectData?.identityId ?: 0}, " +
            "sessionTokenBytes=${data.sessionToken.size()}, encryptedBytes=${encrypted.size}"
    }
    sendToSock(node, msg, destination)
}

// Removed dummy findPeerDestination

fun describeIpv4Packet(data: ByteArray): String {
    if (data.size < 20) {
        return "short-ipv4 bytes=${data.size}"
    }
    val version = (data[0].toInt() ushr 4) and 0x0f
    if (version != 4) {
        return "non-ipv4 version=$version bytes=${data.size}"
    }
    val ihl = (data[0].toInt() and 0x0f) * 4
    if (ihl < 20 || data.size < ihl) {
        return "bad-ipv4-header ihl=$ihl bytes=${data.size}"
    }
    val proto = data[9].toInt() and 0xff
    val srcIp = ByteBuffer.wrap(data, 12, 4).order(ByteOrder.BIG_ENDIAN).int
    val dstIp = ByteBuffer.wrap(data, 16, 4).order(ByteOrder.BIG_ENDIAN).int
    val protoName = when (proto) {
        1 -> "ICMP"
        6 -> "TCP"
        17 -> "UDP"
        else -> "proto-$proto"
    }
    val portInfo = if ((proto == 6 || proto == 17) && data.size >= ihl + 4) {
        val srcPort = ByteBuffer.wrap(data, ihl, 2).order(ByteOrder.BIG_ENDIAN).short.toInt() and 0xffff
        val dstPort = ByteBuffer.wrap(data, ihl + 2, 2).order(ByteOrder.BIG_ENDIAN).short.toInt() and 0xffff
        " srcPort=$srcPort dstPort=$dstPort"
    } else {
        ""
    }
    return "$protoName ${ipToString(srcIp)} -> ${ipToString(dstIp)}$portInfo bytes=${data.size}"
}

fun describePolicyRules(rules: ByteArray): String {
    if (rules.isEmpty()) {
        return "empty"
    }
    val result = mutableListOf<String>()
    var offset = 0
    while (offset + 3 <= rules.size) {
        val proto = rules[offset].toInt() and 0xff
        val port = ByteBuffer.wrap(rules, offset + 1, 2).order(ByteOrder.BIG_ENDIAN).short.toInt() and 0xffff
        val protoName = when (proto) {
            1 -> "ICMP"
            6 -> "TCP"
            17 -> "UDP"
            else -> "proto-$proto"
        }
        result.add("$protoName/$port")
        offset += 3
    }
    if (offset != rules.size) {
        result.add("trailingBytes=${rules.size - offset}")
    }
    return result.joinToString(",")
}

data class PolicyDecision(
    val state: PolicyDecisionState,
    val reason: String,
)

enum class PolicyDecisionState {
    Allowed,
    Pending,
    Denied,
}

fun policyDecisionForIpv4Packet(node: Node, data: ByteArray): PolicyDecision {
    if (data.size < 20) {
        return PolicyDecision(PolicyDecisionState.Denied, "too-short")
    }
    val version = (data[0].toInt() ushr 4) and 0x0f
    if (version != 4) {
        return PolicyDecision(PolicyDecisionState.Denied, "non-ipv4")
    }
    val ihl = (data[0].toInt() and 0x0f) * 4
    if (ihl < 20 || data.size < ihl) {
        return PolicyDecision(PolicyDecisionState.Denied, "bad-ipv4-header")
    }
    val proto = data[9].toInt() and 0xff
    val targetIp = ByteBuffer.wrap(data, 16, 4).order(ByteOrder.BIG_ENDIAN).int
    val port = if ((proto == 6 || proto == 17) && data.size >= ihl + 4) {
        ByteBuffer.wrap(data, ihl + 2, 2).order(ByteOrder.BIG_ENDIAN).short.toInt() and 0xffff
    } else {
        0
    }
    val candidates = policyCandidateIdsForTarget(node, targetIp)
    if (candidates.isEmpty()) {
        return PolicyDecision(PolicyDecisionState.Denied, "no policy identity for ${ipToString(targetIp)}")
    }
    val missing = candidates.filterNot { node.policyRules.containsKey(it) }
    if (missing.isNotEmpty()) {
        return PolicyDecision(PolicyDecisionState.Pending, "waiting policy for ${ipToString(targetIp)}, ids=${missing.joinToString(",")}")
    }
    for (candidate in candidates) {
        val rules = node.policyRules[candidate] ?: continue
        if (policyRulesAllow(rules, proto, port)) {
            return PolicyDecision(
                PolicyDecisionState.Allowed,
                "allowed by policy id=$candidate ${protocolName(proto)}/$port"
            )
        }
    }
    return PolicyDecision(
        PolicyDecisionState.Denied,
        "no allow rule for ${ipToString(targetIp)} ${protocolName(proto)}/$port, ids=${candidates.joinToString(",")}"
    )
}

fun policyCandidateIdsForTarget(node: Node, targetIp: Int): List<Int> {
    val result = mutableListOf<Int>()
    node.connectData?.nodeList.orEmpty().firstOrNull {
        runCatching { ipStringToInt(it.ip) == targetIp }.getOrDefault(false)
    }?.let { result.add(it.id) }
    result.addAll(matchingResourceIdsForTarget(node, targetIp))
    return result.distinct()
}

fun policyRulesAllow(rules: ByteArray, proto: Int, port: Int): Boolean {
    if (rules.isEmpty()) {
        return false
    }
    var offset = 0
    while (offset + 3 <= rules.size) {
        val ruleProto = rules[offset].toInt() and 0xff
        val rulePort = ByteBuffer.wrap(rules, offset + 1, 2).order(ByteOrder.BIG_ENDIAN).short.toInt() and 0xffff
        if (ruleProto == proto && (rulePort == port || rulePort == 0)) {
            return true
        }
        offset += 3
    }
    return false
}

fun protocolName(proto: Int): String {
    return when (proto) {
        1 -> "ICMP"
        6 -> "TCP"
        17 -> "UDP"
        else -> "proto-$proto"
    }
}

fun describeConnectDataForLog(node: Node): String {
    val connectData = node.connectData ?: return "connect data is null"
    val localNodeId = localNodeId(node) ?: 0
    val nodeSummary = connectData.nodeList.orEmpty().joinToString(";") {
        "id=${it.id},ip=${it.ip},status=${it.connectionStatus}"
    }.ifEmpty { "none" }
    val resourceSummary = connectData.resourceList.orEmpty().map { resource ->
        val uri = runCatching { URI(resource.url) }.getOrNull()
        val host = uri?.host ?: "invalid-host"
        val scheme = uri?.scheme ?: "unknown"
        val port = when {
            uri == null -> -1
            uri.port > 0 -> uri.port
            scheme.equals("http", ignoreCase = true) -> 80
            scheme.equals("https", ignoreCase = true) -> 443
            else -> -1
        }
        "id=${resource.id},${scheme}://${host}:${port},status=${resource.connectionStatus}"
    }.joinToString(";").ifEmpty { "none" }
    return "connect summary: ip=${connectData.ip}/${connectData.maskLen}, identity=${connectData.identityId}, localNodeId=$localNodeId, " +
        "nodes=${connectData.nodeList.orEmpty().size}[$nodeSummary], " +
        "resources=${connectData.resourceList.orEmpty().size}[$resourceSummary], " +
        "acl=${describeAclForLog(connectData.acl)}"
}

fun localNodeId(node: Node): Int? {
    val localIp = node.connectData?.ip ?: return null
    return node.connectData?.nodeList.orEmpty().firstOrNull { it.ip == localIp }?.id
}

fun describeAclForLog(acl: JsonElement?): String {
    if (acl == null || acl.isJsonNull) {
        return "null"
    }
    if (acl.isJsonArray) {
        val array = acl.asJsonArray
        if (array.size() == 0) {
            return "array(size=0)"
        }
        val samples = array.take(3).mapIndexed { index, item ->
            "[$index]=${describeAclElementForLog(item)}"
        }
        return "array(size=${array.size()}, ${samples.joinToString(",")})"
    }
    if (acl.isJsonObject) {
        return describeAclObjectForLog(acl)
    }
    if (acl.isJsonPrimitive) {
        return "primitive"
    }
    return acl.javaClass.simpleName
}

private fun describeAclElementForLog(element: JsonElement): String {
    return when {
        element.isJsonObject -> describeAclObjectForLog(element)
        element.isJsonArray -> "array(size=${element.asJsonArray.size()})"
        element.isJsonNull -> "null"
        element.isJsonPrimitive -> "primitive"
        else -> element.javaClass.simpleName
    }
}

private fun describeAclObjectForLog(element: JsonElement): String {
    val obj = element.asJsonObject
    val fields = obj.entrySet().take(12).joinToString(",") { (key, value) ->
        "$key=${describeAclValueForLog(value)}"
    }
    val suffix = if (obj.entrySet().size > 12) ",..." else ""
    return "object($fields$suffix)"
}

private fun describeAclValueForLog(value: JsonElement): String {
    return when {
        value.isJsonNull -> "null"
        value.isJsonArray -> "array(${value.asJsonArray.size()})"
        value.isJsonObject -> "object(${value.asJsonObject.entrySet().joinToString(",") { it.key }})"
        value.isJsonPrimitive -> {
            val primitive = value.asJsonPrimitive
            when {
                primitive.isNumber -> primitive.asNumber.toString()
                primitive.isBoolean -> primitive.asBoolean.toString()
                primitive.isString -> primitive.asString.take(48)
                else -> "primitive"
            }
        }
        else -> value.javaClass.simpleName
    }
}


suspend fun findPeerDestination(node: Node, dstmac: Mac, size: Long): SDLanSock {
    var is_p2p: Boolean = false
    var is_multicast: Boolean = false
    var result: SDLanSock
    if (isMultiBroadcast(dstmac)) {
        node.stats.txSup.addAndGet(size)
        node.stats.txBroadcast.addAndGet(size)
        result = node.config.stunServer
        is_multicast = true
    } else {
        val peer = node.knownPeers.get(dstmac)
        if (peer == null) {
            node.stats.txSup.addAndGet(size)
            result = node.config.stunServer
        } else {
            val now = System.currentTimeMillis()/1000
            if (now - peer.lastP2P.get() >= peer.timeout/2) {
                TerminalLogger.warning { "last p2p is too old, deleting from known hosts" }
                node.knownPeers.remove(dstmac)
                node.stats.txSup.addAndGet(size)
                result = node.config.stunServer
            } else {
                is_p2p = true
                node.stats.txP2P.addAndGet(size)
                result = peer.sock
            }
        }
    }

    if(!is_p2p && !is_multicast) {
        TerminalLogger.debugf { "check_query_peer_info" }
        checkQueryPeerInfo(node, dstmac)
    }

    return result
}

suspend fun checkQueryPeerInfo(node: Node, dstmac: Mac) {
    val now = System.currentTimeMillis()/1000
    val peer = node.pendingPeers.get(dstmac)

    val needSendQuery: Boolean
    if (peer != null) {
        if (now - peer.lastSentQuery.get() > SDLanConfig.RegisterInterval) {
            needSendQuery = true
            peer.lastSentQuery.set(now)
        } else {
            needSendQuery = false
        }
    } else {
        val sock = SDLanSock(IPFamily.IPV4, 0, ByteArray(4))
        val peer = EdgePeer(
            0,
            node.deviceConfig.ip.netBitLen,
            sock,
            null,
            now,
        )
        node.pendingPeers[dstmac] = peer
        needSendQuery = true
    }

    if (needSendQuery) {
        TerminalLogger.debugf { "send query for ${macToString(dstmac)}" }
        registerWithLocalPeers(node)
        sendQueryPeer(node, dstmac)
    }

}

suspend fun sendQueryPeer(node: Node, dstmac: Mac) {
    if (!node.aes.isAuthorized()) {
        TerminalLogger.errorf { "not authed for send query" }
        return
    }
    val query = SDLQueryInfo.newBuilder()
        .setDstMac(dstmac)
        .build()
    val msg = encodeToControlMessage(query, PacketType.QueryInfo)
    node.toSocket.send(msg.toByteArray())
}

fun formEthernetPacket(srcmac: ByteArray, dstmac: ByteArray, data: ByteArray): ByteBuffer {
    val buffer = ByteBuffer.allocate(14 + data.size).order(ByteOrder.BIG_ENDIAN)
    buffer.put(dstmac)
    buffer.put(srcmac)
    buffer.putShort(EtherType.IPV4)
    buffer.put(data)
    buffer.flip()
    return buffer
}

// frommac is self's mac
// fromip is self ip
// queryip is the targetip
suspend fun sendArpRequest(node: Node, queryip: Int) {
    if (!node.aes.isAuthorized()) {
        TerminalLogger.errorf { "not authed for send arp request" }
        return
    }
    val arpReq = SDLArpRequest.newBuilder()
        .setTargetIp(queryip)
        .setOriginIp(node.deviceConfig.ip.netAddr)
        .build()
    val msg = encodeToControlMessage(arpReq, PacketType.ArpRequest)
    node.toSocket.send(msg.toByteArray())
    TerminalLogger.debugf { "sent QUIC ARP request for ip: ${com.jihe.punchnet.sdlan.utils.ipToString(queryip)}" }
}

suspend fun handlePacketArpResponse(node: Node, content: ByteArray) {
    val arpRes: SDLArpResponse
    try {
        arpRes = SDLArpResponse.parseFrom(content)
    } catch (e: Exception) {
        TerminalLogger.errorf { "failed to decode arp response: $e"}
        return
    }

    val targetIp = arpRes.targetIp
    val targetMac = arpRes.targetMac
    
    if (targetMac.isEmpty) {
        TerminalLogger.debugf { "ARP response returned empty MAC for IP: ${com.jihe.punchnet.sdlan.utils.ipToString(targetIp)}" }
        return
    }

    TerminalLogger.debugf { "Rx QUIC ARP response for IP: ${com.jihe.punchnet.sdlan.utils.ipToString(targetIp)}, MAC: ${macToString(targetMac)}" }
    sendPolicyRequestForTargetIp(node, targetIp)
    if (node.iface is IfaceTun) {
        val tun = node.iface as IfaceTun
        tun.arpTable.addToARPTable(targetIp, targetMac.toByteArray())
        tun.arpWaitList.arpArrived(node, targetIp, targetMac)
    }
}

suspend fun sendExposedServiceRequest(node: Node) {
    if (!node.aes.isAuthorized()) {
        return
    }
    val req = SDLExposedServiceRequest.newBuilder()
        .setVersion(0)
        .build()
    node.toSocket.send(encodeToControlMessage(req, PacketType.ExposedServiceRequest).toByteArray())
    TerminalLogger.debugf { "sent ExposedServiceRequest version=0" }
}

suspend fun sendPolicyRequestForTargetIp(node: Node, targetIp: Int) {
    if (!node.aes.isAuthorized()) {
        return
    }
    val srcIdentityId = node.connectData?.identityId ?: 0
    if (srcIdentityId == 0) {
        TerminalLogger.warning { "skip PolicyRequest for ${ipToString(targetIp)}: missing src identity" }
        return
    }

    val dstNode = node.connectData?.nodeList?.firstOrNull {
        runCatching { ipStringToInt(it.ip) == targetIp }.getOrDefault(false)
    }
    if (dstNode == null) {
        TerminalLogger.warning { "skip PolicyRequest for ${ipToString(targetIp)}: target not in node_list" }
        return
    }

    TerminalLogger.debugf {
        "policy target ${ipToString(targetIp)}: srcIdentity=$srcIdentityId, apiIdentity=${node.connectData?.identityId ?: 0}, " +
            "nodeId=${dstNode.id}, nodeStatus=${dstNode.connectionStatus}, " +
            describeResourcesForTarget(node, targetIp) + ", acl=${describeAclForLog(node.connectData?.acl)}"
    }

    sendPolicyRequest(node, srcIdentityId, dstNode.id, targetIp, "node:${dstNode.name}/${dstNode.connectionStatus}")

    matchingResourceIdsForTarget(node, targetIp)
        .filter { it != dstNode.id }
        .forEach { resourceId ->
            sendPolicyRequest(node, srcIdentityId, resourceId, targetIp, "resource")
        }
}

suspend fun sendPolicyRequest(
    node: Node,
    srcIdentityId: Int,
    dstIdentityId: Int,
    targetIp: Int,
    source: String,
) {
    if (node.queriedPolicies.putIfAbsent(dstIdentityId, 0) != null) {
        return
    }
    node.queriedPolicyTargets[dstIdentityId] = targetIp
    val req = SDLPolicyRequest.newBuilder()
        .setSrcIdentityId(srcIdentityId)
        .setDstIdentityId(dstIdentityId)
        .setVersion(0)
        .build()
    node.toSocket.send(encodeToControlMessage(req, PacketType.PolicyRequest).toByteArray())
    TerminalLogger.debugf {
        "sent PolicyRequest srcIdentity=$srcIdentityId, dstIdentity=$dstIdentityId, dstIp=${ipToString(targetIp)}, source=$source, version=0"
    }
}

fun describeResourcesForTarget(node: Node, targetIp: Int): String {
    val targetIpText = ipToString(targetIp)
    val matches = node.connectData?.resourceList.orEmpty().mapNotNull { resource ->
        val uri = runCatching { URI(resource.url) }.getOrNull() ?: return@mapNotNull null
        if (uri.host != targetIpText) {
            return@mapNotNull null
        }
        val port = when {
            uri.port > 0 -> uri.port
            uri.scheme.equals("http", ignoreCase = true) -> 80
            uri.scheme.equals("https", ignoreCase = true) -> 443
            else -> -1
        }
        "id=${resource.id}:${uri.scheme ?: "unknown"}/$port"
    }.distinct()
    return if (matches.isEmpty()) {
        "no matching resource in connect resource_list for $targetIpText"
    } else {
        "matching resources for $targetIpText: ${matches.joinToString(",")}"
    }
}

fun matchingResourceIdsForTarget(node: Node, targetIp: Int): List<Int> {
    val targetIpText = ipToString(targetIp)
    return node.connectData?.resourceList.orEmpty().mapNotNull { resource ->
        val uri = runCatching { URI(resource.url) }.getOrNull() ?: return@mapNotNull null
        if (uri.host == targetIpText) resource.id else null
    }.distinct()
}

suspend fun handlePacketPeerInfo(node: Node, content: ByteArray) {
    val pinfo: SDLPeerInfo
    try {
        pinfo = SDLPeerInfo.parseFrom(content)
    } catch (e: Exception) {
        TerminalLogger.errorf { "failed to decode peer info: $e"}
        return
    }

    if (pinfo.dstMac == SDLanConfig.BROADCASTMAC) {
        // pong from sn
        return
    }

    if (!pinfo.hasV4Info() || pinfo.v4Info.v4.size() != 4 || pinfo.v4Info.port == 0) {
        TerminalLogger.warning { "PeerInfo for ${macToString(pinfo.dstMac)} has no usable IPv4 endpoint" }
        return
    }

    val remoteNat = NatType.fromUByte(pinfo.v4Info.natType.toUByte())
    val pending = node.pendingPeers.get(pinfo.dstMac)
    if (pending == null) {
        TerminalLogger.debugf { "Rx PEERINFO unknown peer: ${macToString(pinfo.dstMac)}" }
        return
    }
    pending.sock = SDLanSock(IPFamily.IPV4, pinfo.v4Info.port, pinfo.v4Info.v4.toByteArray())
    pending.natType = remoteNat
    TerminalLogger.debugf { "Rx PEERINFO for ${macToString(pinfo.dstMac)} is at ${pending.sock}" }
    sendRegister(node, remoteNat, pending.sock, pinfo.dstMac)
}

suspend fun handleEvent(node: Node, cmdprotobuf: ByteArray) {
    val evt: com.jihe.punchnet.protobuf.PunchProto.SDLEvent
    try {
        evt = com.jihe.punchnet.protobuf.PunchProto.SDLEvent.parseFrom(cmdprotobuf)
    } catch (e: Exception) {
        TerminalLogger.errorf {"failed to decode SDLEvent: $e"}
        return
    }
    
    when(evt.eventCase) {
        com.jihe.punchnet.protobuf.PunchProto.SDLEvent.EventCase.SEND_REGISTER -> {
            val reg = evt.sendRegister
            val remoteNat = NatType.fromUByte(reg.natType.toUByte())
            val ip = byteArrayOf(
                (reg.natIp ushr 24).and(0xff).toByte(),
                (reg.natIp ushr 16).and(0xff).toByte(),
                (reg.natIp ushr 8).and(0xff).toByte(),
                (reg.natIp).and(0xff).toByte(),
            )
            checkPeerRegistrationNeeded(node,false, reg.dstMac, remoteNat, SDLanSock(IPFamily.IPV4, reg.natPort, ip))
        }
        else -> {
            TerminalLogger.warning { "unhandled event: ${evt.eventCase}" }
        }
    }
}

fun ipInt2ByteArray(ip: Int): ByteArray {
    return byteArrayOf(
        (ip ushr 24).and(0xff).toByte(),
        (ip ushr 16).and(0xff).toByte(),
        (ip ushr 8).and(0xff).toByte(),
        ip.and(0xff).toByte(),
    )
}
