package wang.yaojia.webterm.api.routes

import wang.yaojia.webterm.wire.HostEndpoint
import wang.yaojia.webterm.wire.HttpMethod
import wang.yaojia.webterm.wire.HttpRequest
import java.net.URI

/** Named HTTP status codes used by the client (no magic numbers, plan §4). */
internal object HttpStatus {
    const val OK = 200
    const val NO_CONTENT = 204
    const val BAD_REQUEST = 400
    const val FORBIDDEN = 403
    const val NOT_FOUND = 404
    const val TOO_MANY_REQUESTS = 429
    const val INTERNAL_SERVER_ERROR = 500

    /** Inclusive bounds of the 4xx/5xx band a guarded-write maps to a `Rejected` outcome. */
    const val CLIENT_ERROR_MIN = 400
    const val SERVER_ERROR_MAX = 599
}

/** Header / content-type names (no magic strings inline). */
internal object HeaderName {
    const val ORIGIN = "Origin"
    const val CONTENT_TYPE = "Content-Type"
}

internal object ContentType {
    const val JSON = "application/json"
}

/**
 * Whether a route mutates server state — THE security split (plan §4.3): `Origin` is stamped
 * **iff** [GUARDED]. If the server ever reclassifies a RO route as guarded, tests go red instead of
 * passing by coincidence.
 */
internal enum class OriginPolicy {
    /** Read-only GET — MUST NOT carry Origin. */
    READ_ONLY,

    /** State-changing — MUST carry `Origin: endpoint.originHeader`, byte-equal; server 403s a
     *  missing/foreign Origin (CSWSH defence). */
    GUARDED,
}

/**
 * One buildable API route — an immutable snapshot; building never mutates. The Android analogue of
 * iOS `APIRoute`. [percentEncodedQuery] is pre-encoded ONCE by the route builder (never at call
 * sites). Origin stamping happens HERE and only here (single point).
 */
internal class ApiRoute(
    val method: HttpMethod,
    val path: String,
    val originPolicy: OriginPolicy,
    val body: ByteArray? = null,
    val percentEncodedQuery: String? = null,
) {
    /**
     * Build the [HttpRequest] against [endpoint]'s scheme/host/port: the path is REPLACED, the
     * query is REPLACED by [percentEncodedQuery] (dropped when null), fragment/credentials are
     * dropped — the same derivation philosophy as `HostEndpoint.wsUrl`. Returns `null` if the base
     * URL cannot be parsed (surfaced by the client as `InvalidRequest`).
     */
    fun toHttpRequest(endpoint: HostEndpoint): HttpRequest? {
        val url = buildUrl(endpoint.baseUrl, path, percentEncodedQuery) ?: return null
        val headers = LinkedHashMap<String, String>()
        if (originPolicy == OriginPolicy.GUARDED) {
            headers[HeaderName.ORIGIN] = endpoint.originHeader
        }
        if (body != null) {
            headers[HeaderName.CONTENT_TYPE] = ContentType.JSON
        }
        return HttpRequest(method = method, url = url, headers = headers, body = body)
    }

    private companion object {
        /**
         * Rebuild `<scheme>://<host>[:<port>]<path>[?<query>]` from the dialed base URL, keeping the
         * dialed port verbatim (like `wsUrl`, unlike the default-port-dropping Origin). The path and
         * pre-encoded query are appended verbatim; any path/query/fragment/credentials the base URL
         * carried are dropped.
         */
        fun buildUrl(baseUrl: String, path: String, query: String?): String? {
            val uri = runCatching { URI(baseUrl.trim()) }.getOrNull() ?: return null
            val scheme = uri.scheme?.lowercase() ?: return null
            val host = uri.host ?: return null
            if (host.isEmpty()) return null
            val serializedHost = if (host.contains(":") && !host.startsWith("[")) "[$host]" else host
            val portPart = if (uri.port != -1) ":${uri.port}" else ""
            val queryPart = if (query != null) "?$query" else ""
            return "$scheme://$serializedHost$portPart$path$queryPart"
        }
    }
}
