package wang.yaojia.webterm.transport

import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.Call
import okhttp3.Callback
import okhttp3.Headers
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import wang.yaojia.webterm.wire.HttpMethod
import wang.yaojia.webterm.wire.HttpRequest
import wang.yaojia.webterm.wire.HttpResponse
import wang.yaojia.webterm.wire.HttpTransport
import java.io.IOException
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException

/**
 * The only concrete REST transport (A7): implements [HttpTransport] over the SAME shared
 * [OkHttpClient] as the WS transport (one client → mTLS + `cache(null)` apply uniformly).
 *
 * Contract (frozen [HttpTransport] doc + plan §4.3):
 *  - Headers are copied VERBATIM. This module NEVER adds `Origin` itself — `:api-client` stamps it
 *    only on the guarded routes; the read-only GETs must stay Origin-free. Getting this wrong breaks
 *    the CSWSH split.
 *  - A **non-2xx** status is RETURNED, not thrown (classification is the caller's job). Only a
 *    **transport-level** failure (connection refused, TLS, reset) throws — as OkHttp's [IOException].
 *
 * Uses the async `enqueue` path wrapped in [suspendCancellableCoroutine] so coroutine cancellation
 * cancels the in-flight [Call] (no thread parked on a blocking `execute`).
 */
public class OkHttpHttpTransport(
    private val client: OkHttpClient,
) : HttpTransport {

    override suspend fun send(request: HttpRequest): HttpResponse =
        suspendCancellableCoroutine { continuation ->
            val call = client.newCall(request.toOkHttpRequest())
            continuation.invokeOnCancellation { runCatching { call.cancel() } }
            call.enqueue(object : Callback {
                override fun onFailure(call: Call, e: IOException) {
                    continuation.resumeWithException(e) // transport-level failure → thrown
                }

                override fun onResponse(call: Call, response: Response) {
                    val mapped = try {
                        response.use { it.toHttpResponse() }
                    } catch (e: IOException) {
                        continuation.resumeWithException(e)
                        return
                    }
                    continuation.resume(mapped) // any status (incl. non-2xx) is returned
                }
            })
        }
}

private val EMPTY_REQUEST_BODY: RequestBody = ByteArray(0).toRequestBody(null)

private fun HttpRequest.toOkHttpRequest(): Request {
    val builder = Request.Builder().url(url)
    // Verbatim header copy — single-valued map, so replace (not append). NO Origin injected here.
    headers.forEach { (name, value) -> builder.header(name, value) }
    // Null media type → OkHttp adds no Content-Type, so a caller-supplied one is preserved verbatim.
    val requestBody = body?.toRequestBody(null)
    builder.method(method.name, bodyFor(method, requestBody))
    return builder.build()
}

/** OkHttp requires GET to have no body and POST/PUT to have one; DELETE accepts either. */
private fun bodyFor(method: HttpMethod, body: RequestBody?): RequestBody? = when (method) {
    HttpMethod.GET -> null
    HttpMethod.DELETE -> body
    HttpMethod.POST, HttpMethod.PUT -> body ?: EMPTY_REQUEST_BODY
}

private fun Response.toHttpResponse(): HttpResponse =
    HttpResponse(
        status = code,
        body = body?.bytes() ?: ByteArray(0),
        headers = headers.toSingleValueMap(),
    )

/** Flatten OkHttp's multimap headers to last-value-wins (matches the pure `Map<String,String>` DTO). */
private fun Headers.toSingleValueMap(): Map<String, String> =
    buildMap { for (i in 0 until size) put(name(i), value(i)) }
