package wang.yaojia.webterm.wire

import java.net.URI

/**
 * The four network tiers of the pairing/warning table (plan §5.4). The SINGLE canonical type —
 * hoisted into :wire-protocol so :api-client and :client-tls share ONE classifier (the two module
 * copies had drifted). Faithful port of iOS `APIClient.HostNetworkTier`; the UI layers
 * (PairingViewModel scheme+address 分层提示) consume ONE classification instead of re-deriving it.
 *
 * | tier | matches | §5.4 hint |
 * |---|---|---|
 * | [LOOPBACK]    | localhost · 127.0.0.0/8 · ::1                                    | no warning |
 * | [PRIVATE_LAN] | RFC1918 (10/8, 172.16/12, 192.168/16) · 169.254/16 · mDNS .local | non-blocking ws:// cleartext notice |
 * | [TAILSCALE]   | CGNAT 100.64.0.0/10 · MagicDNS *.ts.net                          | no cleartext warning (WireGuard-encrypted) |
 * | [PUBLIC]      | everything else (incl. out-of-range / malformed — fail-safe)     | strongest blocking warning |
 */
public enum class HostNetworkTier {
    LOOPBACK,
    PRIVATE_LAN,
    TAILSCALE,
    PUBLIC,
}

/**
 * Pure, stateless classifier over a URL host string (plan §5.4 tiers). Faithful port of iOS
 * `HostClassifier`.
 *
 * IPv6 note: only loopback `::1` is tiered; other IPv6 literals (incl. ULA `fd00::/8`) fall to
 * [HostNetworkTier.PUBLIC] — fail-safe over-warning, matching v1 scope (the app dials IPv4 LAN /
 * CGNAT / MagicDNS targets). The finer IPv6 handling lives in the UI layer if ever needed.
 */
public object HostClassifier {
    private const val MAGIC_DNS_SUFFIX = ".ts.net"
    private const val MDNS_SUFFIX = ".local"
    private val LOOPBACK_NAMES: Set<String> = setOf("localhost", "::1", "[::1]")
    private const val IPV4_OCTET_COUNT = 4
    private val IPV4_OCTET_RANGE = 0..255

    /**
     * Classify a bare host string (as returned by URL host parsing — no scheme, no port).
     * Unknown/malformed input is [HostNetworkTier.PUBLIC] by design (fail-safe).
     */
    public fun classify(host: String): HostNetworkTier {
        val lowered = host.lowercase()
        if (lowered in LOOPBACK_NAMES) return HostNetworkTier.LOOPBACK
        if (lowered.endsWith(MAGIC_DNS_SUFFIX)) return HostNetworkTier.TAILSCALE
        if (lowered.endsWith(MDNS_SUFFIX)) return HostNetworkTier.PRIVATE_LAN
        val octets = ipv4Octets(lowered) ?: return HostNetworkTier.PUBLIC
        return classifyIpv4(octets)
    }

    /**
     * Convenience for callers holding a [HostEndpoint] (the VM's shape). A `baseUrl` that no longer
     * yields a host classifies as [HostNetworkTier.PUBLIC] (fail-safe).
     */
    public fun classify(endpoint: HostEndpoint): HostNetworkTier = classify(hostOf(endpoint))

    /**
     * Extract the bare host from a (validated) [HostEndpoint]. Shared by [classify] and pairing-error
     * diagnosis so host derivation happens in ONE place. Trims defensively (never trust at a
     * boundary) and returns "" on any parse failure (→ PUBLIC, fail-safe).
     */
    public fun hostOf(endpoint: HostEndpoint): String =
        runCatching { URI(endpoint.baseUrl.trim()).host }.getOrNull() ?: ""

    private fun classifyIpv4(octets: List<Int>): HostNetworkTier {
        val first = octets[0]
        val second = octets[1]
        return when {
            first == 127 -> HostNetworkTier.LOOPBACK // loopback 127/8
            first == 10 -> HostNetworkTier.PRIVATE_LAN // RFC1918 10/8
            first == 192 && second == 168 -> HostNetworkTier.PRIVATE_LAN // RFC1918 192.168/16
            first == 172 && second in 16..31 -> HostNetworkTier.PRIVATE_LAN // RFC1918 172.16/12
            first == 169 && second == 254 -> HostNetworkTier.PRIVATE_LAN // link-local 169.254/16
            first == 100 && second in 64..127 -> HostNetworkTier.TAILSCALE // CGNAT 100.64/10 (Tailscale)
            else -> HostNetworkTier.PUBLIC
        }
    }

    /**
     * Strict dotted-quad parse; anything else (hostnames, IPv6, out-of-range octets, wrong arity)
     * → null. Public so pairing-error diagnosis can reuse the exact same rule.
     */
    public fun ipv4Octets(host: String): List<Int>? {
        val parts = host.split(".")
        if (parts.size != IPV4_OCTET_COUNT) return null
        val octets = parts.map { it.toIntOrNull() ?: return null }
        if (octets.any { it !in IPV4_OCTET_RANGE }) return null
        return octets
    }
}
