import Foundation

#if canImport(Darwin)
import Darwin
#elseif canImport(Musl)
import Musl
#elseif canImport(Glibc)
import Glibc
#elseif os(Windows)
import WinSDK
#endif

/// One way a peer might reach this device's punch socket directly (SYNC_PROTOCOL §3.3) —
/// the unit both sides exchange inside the Noise session before punching. Codable is the
/// wire shape (rides in `DirectOffer`/`DirectAnswer`); every field decodes-if-present so
/// adding one never strands an older peer.
public struct DirectCandidate: Sendable, Codable, Equatable {
    public enum Kind: String, Sendable, Codable {
        /// A private/interface address — reaches the peer only when routing happens to
        /// allow it (VPN, unusual topology). LAN-proper is the LAN transport's job; these
        /// ride along because they're free and occasionally the only working pair.
        case local
        /// The server-reflexive (STUN-learned) public v4 — the hole-punch target.
        case reflexive
        /// A global IPv6 address — probed *first*: when both ends have one there is no
        /// NAT to punch, just a firewall to traverse, and the same probe does both.
        case ipv6
    }

    public var kind: Kind
    public var ip: String
    public var port: UInt16

    public init(kind: Kind, ip: String, port: UInt16) {
        self.kind = kind
        self.ip = ip
        self.port = port
    }

    public var endpoint: UDPEndpoint {
        ip.contains(":") ? .v6(ip: ip, port: port) : .v4(ip: ip, port: port)
    }
}

/// Everything one side contributes to a punch attempt: its candidates, its NAT class
/// (so the peer can decide "both symmetric ⇒ don't bother"), and the secret token the
/// peer must echo in punch probes. Tokens travel only inside Noise, so a probe carrying
/// the right token *is* proof the sender is our authenticated peer — no off-path host
/// can validate a path.
public struct DirectPunchOffer: Sendable, Codable, Equatable {
    public var candidates: [DirectCandidate]
    public var natClass: NATClass
    /// The token *this* side expects to receive back in `PUNCH` probes and on every RUDP
    /// datagram (16 random bytes) — exchanged only inside Noise, so it authenticates the
    /// peer's packets on the otherwise-plaintext (Noise rides on top) direct socket.
    public var token: Data

    public init(candidates: [DirectCandidate], natClass: NATClass, token: Data) {
        self.candidates = candidates
        self.natClass = natClass
        self.token = token
    }
}

/// The client's pick after punching (SYNC_PROTOCOL §3.3): the host candidate it proved a
/// round trip to. The host stops probing, rebinds that socket's port as its QUIC listener,
/// and answers `HostMsg.directGo`.
public struct DirectSelect: Sendable, Codable, Equatable {
    public var ip: String
    public var port: UInt16

    public init(ip: String, port: UInt16) {
        self.ip = ip
        self.port = port
    }

    public init(endpoint: UDPEndpoint) {
        self.ip = endpoint.ip
        self.port = endpoint.port
    }
}

/// Gathers this device's candidate set for one punch attempt: interface addresses (via
/// getifaddrs on POSIX, GetAdaptersAddresses on Windows — the NAT sim exercises this on
/// Linux) + the STUN reflexive mapping for the v4 socket. Probing order is encoded in the
/// returned array: v6 first (cheapest win), then reflexive (the actual hole punch), then
/// locals.
public enum DirectCandidateGatherer {
    public struct Sockets: Sendable {
        public let v4: UDPSocket
        /// Present only when the device has a global IPv6 address to advertise.
        public let v6: UDPSocket?

        public init(v4: UDPSocket, v6: UDPSocket?) {
            self.v4 = v4
            self.v6 = v6
        }

        public func close() {
            v4.closeSocket()
            v6?.closeSocket()
        }
    }

    /// Fresh sockets for one attempt — ephemeral ports, one per family. The v6 socket is
    /// created only when a global v6 interface address exists.
    public static func openSockets() throws -> Sockets {
        let v4 = try UDPSocket(port: 0, ipv6: false)
        var v6: UDPSocket?
        if !globalIPv6Addresses().isEmpty {
            v6 = try? UDPSocket(port: 0, ipv6: true)
        }
        return Sockets(v4: v4, v6: v6)
    }

    /// The full candidate set + NAT classification for one attempt. STUN runs on the v4
    /// socket (the punch socket); the v6 candidates carry the v6 socket's port.
    public static func gather(
        sockets: Sockets,
        stunServers: [String]? = nil
    ) async -> (candidates: [DirectCandidate], natClass: NATClass) {
        var candidates: [DirectCandidate] = []

        if let v6 = sockets.v6 {
            candidates += globalIPv6Addresses().map {
                DirectCandidate(kind: .ipv6, ip: $0, port: v6.localPort)
            }
        }

        let discovery = await StunClient.discover(socket: sockets.v4, servers: stunServers)
        if let reflexive = discovery.reflexive {
            candidates.append(
                DirectCandidate(kind: .reflexive, ip: reflexive.ip, port: reflexive.port))
        }

        candidates += privateIPv4Addresses().map {
            DirectCandidate(kind: .local, ip: $0, port: sockets.v4.localPort)
        }

        return (candidates, discovery.natClass)
    }

    // MARK: Interface enumeration

    /// Non-loopback IPv4 interface addresses. Not filtered to RFC 1918 — a machine with a
    /// public v4 on an interface should absolutely advertise it (that's a punch-free dial).
    public static func privateIPv4Addresses() -> [String] {
        interfaceAddresses(family: AF_INET).filter { $0 != "127.0.0.1" }
    }

    /// Global-scope IPv6 only (2000::/3): link-local needs a zone index a remote peer
    /// can't use, ULA doesn't route across the internet, loopback is noise.
    public static func globalIPv6Addresses() -> [String] {
        interfaceAddresses(family: AF_INET6).filter { ip in
            guard let first = ip.split(separator: ":").first,
                  let leading = UInt16(first, radix: 16)
            else { return false }
            return (0x2000...0x3FFF).contains(leading)
        }
    }

    /// Deferred to ``DirectSock`` because the two platforms have nothing in common here:
    /// POSIX walks `getifaddrs`, Windows has no such call and must page through
    /// `GetAdaptersAddresses`. Both return up, non-loopback unicast addresses, de-duplicated
    /// in interface order.
    private static func interfaceAddresses(family: Int32) -> [String] {
        DirectSock.interfaceAddresses(family: family)
    }
}
