// SPDX-License-Identifier: GPL-3.0-only

import Foundation
import NetworkExtension
import Darwin

struct IPv4Network: Hashable {
    let address: UInt32
    let prefix: Int

    var mask: UInt32 { prefix == 0 ? 0 : UInt32.max << UInt32(32 - prefix) }
    var network: UInt32 { address & mask }
    var addressText: String { IPv4Routing.text(network) }
    var maskText: String { IPv4Routing.text(mask) }

    func overlaps(_ other: IPv4Network) -> Bool {
        (network & other.mask) == other.network || (other.network & mask) == network
    }
}

struct IPv4InterfaceNetwork: Hashable {
    let interfaceName: String
    let network: IPv4Network
}

enum IPv4Routing {
    static func network(_ address: String, prefix: Int) -> IPv4Network? {
        guard (0...32).contains(prefix), let value = value(address) else { return nil }
        return IPv4Network(address: value, prefix: prefix)
    }

    static func network(_ address: String, mask: String) -> IPv4Network? {
        guard let prefix = prefix(mask) else { return nil }
        return network(address, prefix: prefix)
    }

    static func prefix(_ mask: String) -> Int? {
        guard let value = value(mask) else { return nil }
        var zeroSeen = false
        var result = 0
        for bit in (0..<32).reversed() {
            let set = value & (1 << UInt32(bit)) != 0
            if set && zeroSeen { return nil }
            if set { result += 1 } else { zeroSeen = true }
        }
        return result
    }

    static func physicalNetworks() -> [IPv4InterfaceNetwork] {
        var pointer: UnsafeMutablePointer<ifaddrs>?
        guard getifaddrs(&pointer) == 0 else { return [] }
        defer { freeifaddrs(pointer) }
        var result = Set<IPv4InterfaceNetwork>()
        var current = pointer
        while let item = current?.pointee {
            defer { current = item.ifa_next }
            guard item.ifa_addr?.pointee.sa_family == UInt8(AF_INET),
                  item.ifa_netmask != nil else { continue }

            let flags = Int32(item.ifa_flags)
            guard flags & IFF_UP != 0, flags & IFF_LOOPBACK == 0 else { continue }

            let name = String(cString: item.ifa_name)
            if isTunnelInterface(name) { continue }

            let address = item.ifa_addr!.withMemoryRebound(to: sockaddr_in.self, capacity: 1) {
                UInt32(bigEndian: $0.pointee.sin_addr.s_addr)
            }
            let mask = item.ifa_netmask!.withMemoryRebound(to: sockaddr_in.self, capacity: 1) {
                UInt32(bigEndian: $0.pointee.sin_addr.s_addr)
            }
            // A /0 is a default route, not a directly attached local network. Treating
            // it as local would make every possible VPN overlay look conflicting.
            guard address != 0, let prefix = prefix(text(mask)), prefix > 0 else { continue }
            result.insert(IPv4InterfaceNetwork(
                interfaceName: name,
                network: IPv4Network(address: address, prefix: prefix)
            ))
        }
        return result.sorted {
            if $0.interfaceName != $1.interfaceName { return $0.interfaceName < $1.interfaceName }
            if $0.network.network != $1.network.network { return $0.network.network < $1.network.network }
            return $0.network.prefix < $1.network.prefix
        }
    }

    static func resolvedAddresses(_ endpoint: String) -> [String] {
        resolvedEndpoints(endpoint).compactMap {
            URL(string: "udp://\($0)")?.host
        }
    }

    static func resolvedEndpoints(_ endpoint: String) -> [String] {
        let urlValue = endpoint.contains("://") ? endpoint : "udp://\(endpoint)"
        guard let components = URLComponents(string: urlValue),
              let host = components.host, let port = components.port else { return [] }
        return resolve(host).map { "\($0):\(port)" }
    }

    private static func resolve(_ host: String) -> [String] {
        var hints = addrinfo(ai_flags: AI_ADDRCONFIG, ai_family: AF_INET, ai_socktype: SOCK_DGRAM,
            ai_protocol: IPPROTO_UDP, ai_addrlen: 0, ai_canonname: nil, ai_addr: nil, ai_next: nil)
        var result: UnsafeMutablePointer<addrinfo>?
        guard getaddrinfo(host, nil, &hints, &result) == 0 else { return [] }
        defer { freeaddrinfo(result) }
        var values = Set<String>()
        var current = result
        while let info = current?.pointee {
            if let address = info.ai_addr?.withMemoryRebound(to: sockaddr_in.self, capacity: 1, {
                var value = $0.pointee.sin_addr
                var buffer = [CChar](repeating: 0, count: Int(INET_ADDRSTRLEN))
                return inet_ntop(AF_INET, &value, &buffer, socklen_t(INET_ADDRSTRLEN)).map { String(cString: $0) }
            }) { values.insert(address) }
            current = info.ai_next
        }
        return Array(values)
    }

    fileprivate static func value(_ text: String) -> UInt32? {
        var address = in_addr()
        guard inet_pton(AF_INET, text, &address) == 1 else { return nil }
        return UInt32(bigEndian: address.s_addr)
    }

    fileprivate static func text(_ value: UInt32) -> String {
        var address = in_addr(s_addr: value.bigEndian)
        var buffer = [CChar](repeating: 0, count: Int(INET_ADDRSTRLEN))
        return inet_ntop(AF_INET, &address, &buffer, socklen_t(INET_ADDRSTRLEN)).map { String(cString: $0) } ?? "0.0.0.0"
    }

    private static func isTunnelInterface(_ name: String) -> Bool {
        let tunnelPrefixes = ["utun", "ipsec", "ppp", "tun", "tap", "gif", "stf"]
        return name == "lo0" || tunnelPrefixes.contains(where: name.hasPrefix)
    }
}
