package wang.yaojia.webterm.wire

import java.net.URI

/**
 * A paired web-terminal host (FROZEN contract, plan §3.1). The SINGLE point of derivation for
 * the `Origin` header and the WS URL — hand-assembling either anywhere else is a review
 * CRITICAL (CSWSH defence, TECH_DOC §7). Android analogue of iOS `WireProtocol.HostEndpoint`.
 *
 * Derivations are computed once by [fromBaseUrl] and stored immutably:
 *  - [originHeader] = `<scheme>://<host>[:<port>]`, omitting the scheme's default port
 *    (http/80, https/443), scheme+host lowercased, IPv6 bracketed — byte-identical to the
 *    browser's Origin serialization. The server re-parses BOTH sides via `new URL()` and
 *    compares protocol/hostname/port (`src/http/origin.ts:31-51`), so this must match one of
 *    the server's NIC-derived allowed origins exactly or the WS upgrade 401s.
 *  - [wsUrl] = same host+port as dialed, scheme http->ws / https->wss, path replaced by
 *    [WireConstants.WS_PATH]; query/fragment/credentials dropped. The dialed port is kept
 *    verbatim here (unlike the origin, which drops default ports).
 *
 * Pure Kotlin/JVM only — `java.net.URI` is JVM stdlib parsing, not an Android import.
 * Persistence (A12 host-registry) stores the [baseUrl] string and reconstructs via
 * [fromBaseUrl], re-validating on load (untrusted at rest).
 */
public class HostEndpoint private constructor(
    /** The URL the user dialed (`http(s)://<host>[:<port>]`). Path/query/fragment/credentials it carried are ignored by the derivations. */
    public val baseUrl: String,
    /** Derived WS endpoint (`ws(s)://…/term`). Read-only. */
    public val wsUrl: String,
    /** Derived `Origin` header value — see class doc. Never hand-assemble elsewhere. */
    public val originHeader: String,
) {
    override fun equals(other: Any?): Boolean =
        other is HostEndpoint &&
            baseUrl == other.baseUrl &&
            wsUrl == other.wsUrl &&
            originHeader == other.originHeader

    override fun hashCode(): Int {
        var result = baseUrl.hashCode()
        result = 31 * result + wsUrl.hashCode()
        result = 31 * result + originHeader.hashCode()
        return result
    }

    override fun toString(): String = "HostEndpoint(baseUrl=$baseUrl, wsUrl=$wsUrl, originHeader=$originHeader)"

    public companion object {
        private val WS_SCHEME_BY_HTTP: Map<String, String> = mapOf("http" to "ws", "https" to "wss")
        private val DEFAULT_PORT_BY_SCHEME: Map<String, Int> = mapOf("http" to 80, "https" to 443)

        /**
         * Validating factory: returns null unless [baseUrl] parses as an http(s) URL with a
         * non-empty host. QR-scan / manual-entry payloads are untrusted external input — reject
         * early (plan §5).
         */
        public fun fromBaseUrl(baseUrl: String): HostEndpoint? {
            // Trim ONCE, here, and store the trimmed value: downstream re-parsers (PairingProbe
            // httpBaseUrl, HostClassifier.hostOf) do `URI(endpoint.baseUrl)` and would otherwise
            // throw / misclassify on stray surrounding whitespace from a QR scan or manual entry.
            val trimmed = baseUrl.trim()
            val uri = try {
                URI(trimmed)
            } catch (_: Exception) {
                return null
            }
            val scheme = uri.scheme?.lowercase() ?: return null
            val wsScheme = WS_SCHEME_BY_HTTP[scheme] ?: return null // rejects non-http(s)
            val host = uri.host ?: return null
            if (host.isEmpty()) return null
            val port: Int? = uri.port.takeIf { it != -1 }

            return HostEndpoint(
                baseUrl = trimmed,
                wsUrl = deriveWsUrl(wsScheme, host, port),
                originHeader = deriveOrigin(scheme, host.lowercase(), port),
            )
        }

        private fun deriveOrigin(scheme: String, host: String, port: Int?): String {
            val serializedHost = bracketIfIpv6(host)
            val defaultPort = DEFAULT_PORT_BY_SCHEME[scheme]
            return if (port == null || port == defaultPort) {
                "$scheme://$serializedHost"
            } else {
                "$scheme://$serializedHost:$port"
            }
        }

        private fun deriveWsUrl(wsScheme: String, host: String, port: Int?): String {
            val serializedHost = bracketIfIpv6(host)
            val portPart = if (port == null) "" else ":$port"
            return "$wsScheme://$serializedHost$portPart${WireConstants.WS_PATH}"
        }

        /**
         * IPv6 literals need brackets in an Origin / URL. `URI.getHost()` has returned them both
         * bare and pre-bracketed across JVMs — wrap idempotently.
         */
        private fun bracketIfIpv6(host: String): String =
            if (host.contains(":") && !host.startsWith("[")) "[$host]" else host
    }
}
