//
//  PolicyRuntime.swift
//  Tun
//
//  Created by Codex on 2026/5/21.
//

import Foundation

struct PolicyRuntime: @unchecked Sendable {
    enum InboundDecision {
        case allow
        case deny
        case missingPolicy
    }

    private let policyRuleSnapshot: PolicyRuleSnapshot
    private let flowSessionTable: FlowSessionTable

    init(policyRuleSnapshot: PolicyRuleSnapshot, flowSessionTable: FlowSessionTable) {
        self.policyRuleSnapshot = policyRuleSnapshot
        self.flowSessionTable = flowSessionTable
    }

    func evaluateInbound(srcIdentityID: UInt32, ipPacket: IPPacketView) -> InboundDecision {
        if self.isAllowedBySession(ipPacket: ipPacket) {
            SDLLogger.log("[PolicyRuntime] session hit, src_identify_id: \(srcIdentityID), allow: \(debugInfo(ipPacket: ipPacket))")
            return .allow
        }

        if case .icmp = ipPacket.transportPacket {
            SDLLogger.log("[PolicyRuntime] icmp hit, src_identify_id: \(srcIdentityID), allow: \(debugInfo(ipPacket: ipPacket))")
            return .allow
        }

        guard let ruleMap = self.policyRuleSnapshot.lookup(srcIdentityID) else {
            return .missingPolicy
        }
        
        let isAllowed = self.isAllowedByRule(ruleMap: ruleMap, ipPacket: ipPacket)
        SDLLogger.log("[PolicyRuntime] rule hit: \(isAllowed), src_identify_id: \(srcIdentityID), allow: \(debugInfo(ipPacket: ipPacket))")
        
        return isAllowed ? .allow : .deny
    }

    private func isAllowedByRule(ruleMap: PolicyRuleMap, ipPacket: IPPacketView) -> Bool {
        let proto = ipPacket.header.proto

        switch ipPacket.transportPacket {
        case .tcp(_, let dstPort, _):
            return ruleMap.isAllow(proto: proto, port: dstPort)
        case .udp(_, let dstPort, _):
            return ruleMap.isAllow(proto: proto, port: dstPort)
        default:
            return false
        }
    }

    private func isAllowedBySession(ipPacket: IPPacketView) -> Bool {
        guard let reverseFlowSession = ipPacket.flowSession()?.reverse() else {
            return false
        }

        switch ipPacket.transportPacket {
        case .tcp(_, _, let flags):
            return self.flowSessionTable.allowInboundTCP(reverseFlowSession, flags: flags)
        case .udp(let srcPort, _, _):
            return self.flowSessionTable.allowInboundUDP(reverseFlowSession, isDNS: srcPort == 53)
        default:
            return false
        }
    }
    
    private func debugInfo(ipPacket: IPPacketView) -> String {
        switch ipPacket.transportPacket {
        case .tcp(_, let dstPort, _):
            return "tcp: \(dstPort)"
        case .udp(_, let dstPort, _):
            return "udp: \(dstPort)"
        default:
            return "unknown"
        }
    }
    
}
