import Foundation

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

// The one place the direct/punch layer knows about Winsock (docs/WINDOWS_PORT.md §4.1).
//
// `UDPSocket`, `StunClient` and `DirectCandidateGatherer` are hand-rolled BSD sockets — the
// punch phase needs exact control of the local port, so Network.framework/NIO's datagram
// bootstrap aren't usable. Winsock is BSD-shaped but differs in ways that would otherwise
// smear `#if os(Windows)` through all three files:
//
//   * handles are `SOCKET` (an unsigned UINT_PTR), not a small non-negative `Int32`, so the
//     usual `fd >= 0` test is meaningless — failure is `INVALID_SOCKET`;
//   * errors come from `WSAGetLastError()`, not `errno`;
//   * non-blocking mode is `ioctlsocket(FIONBIO)`, not `fcntl(O_NONBLOCK)`;
//   * `setsockopt` takes `CChar` bytes and an `Int32` length (no `socklen_t`);
//   * `IPPROTO_*` import as an `IPPROTO` enum, so they need `.rawValue`;
//   * `in_addr` is a union, so `in_addr(s_addr:)` doesn't exist;
//   * there is no `getifaddrs` at all — interface enumeration is `GetAdaptersAddresses`.
//
// Everything above is contained here; the call sites stay platform-free.

/// The platform's socket handle. Signed on POSIX, unsigned on Windows.
#if os(Windows)
typealias DirectSocketHandle = SOCKET
#else
typealias DirectSocketHandle = Int32
#endif

#if os(Windows)
/// Windows spells the sockaddr family field `ADDRESS_FAMILY`; POSIX code says `sa_family_t`.
typealias sa_family_t = ADDRESS_FAMILY
#endif

enum DirectSock {

    // MARK: Lifetime

    static var invalidHandle: DirectSocketHandle {
        #if os(Windows)
        INVALID_SOCKET
        #else
        -1
        #endif
    }

    static func isValid(_ handle: DirectSocketHandle) -> Bool {
        #if os(Windows)
        handle != INVALID_SOCKET
        #else
        handle >= 0
        #endif
    }

    /// A UDP socket for `family` (AF_INET / AF_INET6), or `invalidHandle`.
    static func makeUDP(family: Int32) -> DirectSocketHandle {
        #if os(Windows)
        // Winsock must be initialised once per process before any other socket call.
        _ = winsockBootstrap
        return WinSDK.socket(family, SOCK_DGRAM, Int32(IPPROTO_UDP.rawValue))
        #elseif canImport(Darwin)
        return socket(family, SOCK_DGRAM, 0)
        #elseif canImport(Musl)
        return socket(family, Int32(SOCK_DGRAM), 0)
        #else
        return socket(family, Int32(SOCK_DGRAM.rawValue), 0)
        #endif
    }

    static func closeHandle(_ handle: DirectSocketHandle) {
        #if os(Windows)
        _ = closesocket(handle)
        #else
        _ = close(handle)
        #endif
    }

    // MARK: Errors

    /// The last socket error, in whatever numbering the platform uses. Callers only compare
    /// it against ``isWouldBlock(_:)`` or surface it in a `SocketError`, so the two numbering
    /// schemes never need reconciling.
    static func lastError() -> Int32 {
        #if os(Windows)
        WSAGetLastError()
        #else
        errno
        #endif
    }

    /// A full send buffer. UDP callers treat this as "dropped", not as a failure — punch and
    /// STUN datagrams have their own retransmit cadence.
    static func isWouldBlock(_ error: Int32) -> Bool {
        #if os(Windows)
        error == WSAEWOULDBLOCK
        #else
        error == EWOULDBLOCK || error == EAGAIN
        #endif
    }

    // MARK: Options

    static func setNonBlocking(_ handle: DirectSocketHandle) {
        #if os(Windows)
        var mode: u_long = 1
        // FIONBIO already imports as the Int32 `long` that ioctlsocket's `cmd` expects — no
        // bit-pattern conversion (that initialiser takes a UInt32 and this isn't one).
        _ = ioctlsocket(handle, FIONBIO, &mode)
        #else
        let flags = fcntl(handle, F_GETFL, 0)
        _ = fcntl(handle, F_SETFL, flags | O_NONBLOCK)
        #endif
    }

    /// Set an `Int32`-valued socket option (SO_REUSEADDR, IPV6_V6ONLY — both boolean here).
    static func setIntOption(_ handle: DirectSocketHandle, level: Int32, name: Int32, value: Int32) {
        var value = value
        #if os(Windows)
        withUnsafeBytes(of: &value) { raw in
            _ = setsockopt(
                handle, level, name,
                raw.baseAddress?.assumingMemoryBound(to: CChar.self), Int32(raw.count))
        }
        #else
        _ = setsockopt(handle, level, name, &value, socklen_t(MemoryLayout<Int32>.size))
        #endif
    }

    /// `IPPROTO_IPV6` as an option *level*. Windows imports it as an enum case.
    static var ipv6OptionLevel: Int32 {
        #if os(Windows)
        Int32(IPPROTO_IPV6.rawValue)
        #else
        Int32(IPPROTO_IPV6)
        #endif
    }

    // MARK: Addressing

    /// `INADDR_ANY` as an `in_addr`. Windows models `in_addr` as a union, so it has no
    /// `s_addr` initialiser.
    static func anyIPv4Address() -> in_addr {
        #if os(Windows)
        var addr = in_addr()
        addr.S_un.S_addr = 0
        return addr
        #else
        return in_addr(s_addr: INADDR_ANY)
        #endif
    }

    /// Presentation → network. `true` on success.
    static func pton(family: Int32, _ text: String, into raw: UnsafeMutableRawPointer) -> Bool {
        inet_pton(family, text, raw) == 1
    }

    /// Network → presentation, or nil.
    static func ntop(family: Int32, _ raw: UnsafeRawPointer) -> String? {
        let capacity = Int(family == AF_INET6 ? INET6_ADDRSTRLEN : INET_ADDRSTRLEN)
        var buffer = [CChar](repeating: 0, count: capacity)
        let ok = buffer.withUnsafeMutableBufferPointer { out -> Bool in
            #if os(Windows)
            return inet_ntop(family, raw, out.baseAddress, size_t(capacity)) != nil
            #else
            return inet_ntop(family, raw, out.baseAddress, socklen_t(capacity)) != nil
            #endif
        }
        guard ok else { return nil }
        return String(decoding: buffer.prefix(while: { $0 != 0 }).map(UInt8.init(bitPattern:)), as: UTF8.self)
    }

    // MARK: Calls whose length parameter differs in type

    static func bindSocket(
        _ handle: DirectSocketHandle, _ address: UnsafePointer<sockaddr>, _ length: Int
    ) -> Int32 {
        #if os(Windows)
        bind(handle, address, Int32(length))
        #else
        bind(handle, address, socklen_t(length))
        #endif
    }

    /// Reads the bound address back (used to learn the OS-assigned ephemeral port).
    static func localName(
        _ handle: DirectSocketHandle, _ address: UnsafeMutablePointer<sockaddr>, _ length: inout Int
    ) -> Int32 {
        #if os(Windows)
        var len = Int32(length)
        let result = getsockname(handle, address, &len)
        length = Int(len)
        return result
        #else
        var len = socklen_t(length)
        let result = getsockname(handle, address, &len)
        length = Int(len)
        return result
        #endif
    }

    /// Bytes sent, or negative on failure (inspect ``lastError()``).
    static func sendPacket(
        _ handle: DirectSocketHandle, _ buffer: UnsafeRawPointer?, _ count: Int,
        to address: UnsafePointer<sockaddr>, _ length: Int
    ) -> Int {
        #if os(Windows)
        return Int(
            sendto(
                handle, buffer?.assumingMemoryBound(to: CChar.self), Int32(count), 0,
                address, Int32(length)))
        #else
        return sendto(handle, buffer, count, 0, address, socklen_t(length))
        #endif
    }

    /// Bytes received, or <= 0 when nothing is queued.
    static func receivePacket(
        _ handle: DirectSocketHandle, _ buffer: UnsafeMutableRawPointer?, _ capacity: Int,
        from address: UnsafeMutablePointer<sockaddr>, _ length: inout Int
    ) -> Int {
        #if os(Windows)
        var len = Int32(length)
        let count = recvfrom(
            handle, buffer?.assumingMemoryBound(to: CChar.self), Int32(capacity), 0, address, &len)
        length = Int(len)
        return Int(count)
        #else
        var len = socklen_t(length)
        let count = recvfrom(handle, buffer, capacity, 0, address, &len)
        length = Int(len)
        return count
        #endif
    }

    // MARK: Interface enumeration

    /// Up, non-loopback unicast addresses of `family` (AF_INET / AF_INET6), in interface
    /// order and de-duplicated.
    static func interfaceAddresses(family: Int32) -> [String] {
        var results: [String] = []

        #if os(Windows)
        _ = winsockBootstrap
        // GetAdaptersAddresses wants a caller-sized buffer; 16 KB fits a typical machine and
        // ERROR_BUFFER_OVERFLOW hands back the size needed, so retry once with that.
        var size = ULONG(16 * 1024)
        let flags = ULONG(
            GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER
                | GAA_FLAG_SKIP_FRIENDLY_NAME)
        for _ in 0..<2 {
            let buffer = UnsafeMutableRawPointer.allocate(
                byteCount: Int(size), alignment: MemoryLayout<IP_ADAPTER_ADDRESSES_LH>.alignment)
            defer { buffer.deallocate() }
            let list = buffer.assumingMemoryBound(to: IP_ADAPTER_ADDRESSES_LH.self)
            let status = GetAdaptersAddresses(ULONG(family), flags, nil, list, &size)
            if status == ERROR_BUFFER_OVERFLOW { continue }  // `size` now holds what's needed
            guard status == ERROR_SUCCESS else { return [] }

            var adapter: UnsafeMutablePointer<IP_ADAPTER_ADDRESSES_LH>? = list
            while let current = adapter {
                defer { adapter = current.pointee.Next }
                guard current.pointee.OperStatus == IfOperStatusUp else { continue }
                guard current.pointee.IfType != IF_TYPE_SOFTWARE_LOOPBACK else { continue }

                var unicast = current.pointee.FirstUnicastAddress
                while let entry = unicast {
                    defer { unicast = entry.pointee.Next }
                    guard let sa = entry.pointee.Address.lpSockaddr,
                        Int32(sa.pointee.sa_family) == family
                    else { continue }
                    if let ip = presentation(of: sa, family: family) { results.append(ip) }
                }
            }
            break
        }
        #else
        var head: UnsafeMutablePointer<ifaddrs>?
        guard getifaddrs(&head) == 0, let start = head else { return [] }
        defer { freeifaddrs(head) }

        var node: UnsafeMutablePointer<ifaddrs>? = start
        while let current = node {
            defer { node = current.pointee.ifa_next }
            guard let sa = current.pointee.ifa_addr, Int32(sa.pointee.sa_family) == family,
                (UInt32(current.pointee.ifa_flags) & UInt32(IFF_UP)) != 0
            else { continue }
            if let ip = presentation(of: sa, family: family) { results.append(ip) }
        }
        #endif

        // An address can appear on several logical interfaces; keep first-seen order.
        var seen = Set<String>()
        return results.filter { seen.insert($0).inserted }
    }

    /// The textual address inside a `sockaddr` of the given family.
    private static func presentation(of sa: UnsafePointer<sockaddr>, family: Int32) -> String? {
        if family == AF_INET {
            return sa.withMemoryRebound(to: sockaddr_in.self, capacity: 1) { sin in
                withUnsafePointer(to: sin.pointee.sin_addr) { ntop(family: AF_INET, $0) }
            }
        }
        return sa.withMemoryRebound(to: sockaddr_in6.self, capacity: 1) { sin6 in
            withUnsafePointer(to: sin6.pointee.sin6_addr) { ntop(family: AF_INET6, $0) }
        }
    }

    // MARK: Winsock bootstrap

    #if os(Windows)
    /// `WSAStartup` exactly once. Winsock refuses every other call until it has run, and it
    /// is reference-counted, so the matching `WSACleanup` is deliberately never issued —
    /// the process keeps Winsock for its whole lifetime.
    private static let winsockBootstrap: Bool = {
        var data = WSADATA()
        return WSAStartup(0x0202, &data) == 0  // request Winsock 2.2
    }()
    #endif
}
