package wang.yaojia.webterm.transport

import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.launch
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okhttp3.WebSocket
import okhttp3.WebSocketListener
import okio.ByteString
import wang.yaojia.webterm.wire.ConnectionPinger
import wang.yaojia.webterm.wire.TransportConnection
import java.io.IOException

/** RFC 6455 normal-closure status code, used for a client-initiated detach. */
internal const val NORMAL_CLOSURE_CODE: Int = 1000

/**
 * One live OkHttp WebSocket, adapted to the frozen [TransportConnection] contract (A7).
 *
 * The connection is opened eagerly in the constructor (`newWebSocket`), so [send] and [close] act on
 * a real socket and [awaitOpen] can rethrow a connect-time failure verbatim (the pairing probe, A9,
 * classifies POSIX/TLS codes off it). Inbound frames land in an UNLIMITED [inbox] mailbox from the
 * listener (`onMessage → trySend`, never dropped even before a collector attaches); [frames] is a
 * `channelFlow` that drains the mailbox with backpressure and turns the mailbox's terminal state into
 * the two distinguishable flow outcomes the engine relies on:
 *  - a clean `onClosing`/`onClosed` → mailbox closed normally → the flow **completes normally**;
 *  - an `onFailure` → mailbox closed with the cause → the flow **throws** it.
 *
 * Inbound frames are forwarded up UNMODIFIED — there is NO transport-level size cap. `SessionEngine`
 * (A14) self-measures each frame's UTF-8 byte size and is the single authoritative classifier of an
 * oversized ring-buffer replay (the non-retryable `REPLAY_TOO_LARGE` failure); the transport just
 * shuttles bytes.
 *
 * `close()` is a client DETACH — it sends a 1000 close; the server-side PTY keeps running (invariant
 * #2). It is NEVER a kill.
 *
 * Single-collection by contract (one live WS, consumed once), mirroring the `FakeTermTransport`
 * double.
 */
internal class OkHttpWebSocketConnection(
    client: OkHttpClient,
    request: Request,
) : TransportConnection {

    private val inbox = Channel<String>(Channel.UNLIMITED)
    private val opened = CompletableDeferred<Unit>()

    @Volatile
    private var terminated = false

    private val webSocket: WebSocket = client.newWebSocket(request, WsListener())

    /**
     * Liveness-based pinger. OkHttp (unlike iOS `URLSessionWebSocketTask.sendPing`) exposes NO manual
     * ping with an observable pong via [WebSocketListener], so a true per-ping round-trip is not
     * available. Real keep-alive is delegated to OkHttp's `pingInterval` (set on the streaming WS
     * client in [OkHttpTermTransport]); this hook reports whether the connection is still live so the
     * pure `PingScheduler` (A5) can count a dead connection as a miss.
     */
    val pinger: ConnectionPinger = object : ConnectionPinger {
        override suspend fun ping(): Boolean = !terminated
    }

    /** Suspends until the WS handshake completes; rethrows the connect-time failure verbatim. */
    suspend fun awaitOpen() {
        opened.await()
    }

    override val frames: Flow<String> = channelFlow {
        val scope = this
        val pump = launch {
            try {
                // Suspending send → lossless, in-order forwarding (the mailbox holds the backlog).
                for (frame in inbox) {
                    scope.send(frame)
                }
                scope.close() // mailbox drained after a clean close → complete normally
            } catch (e: CancellationException) {
                throw e // cooperative cancellation must propagate, never be swallowed into a close
            } catch (cause: Throwable) {
                scope.close(cause) // mailbox closed with a cause → propagate the error
            }
        }
        awaitClose {
            pump.cancel()
            // Downstream cancelled its collection → tear the socket down (harmless if already closed).
            webSocket.cancel()
        }
    }

    override suspend fun send(frame: String) {
        if (!webSocket.send(frame)) {
            throw IOException("WS send failed: the connection is closing or closed")
        }
    }

    override suspend fun close() {
        webSocket.close(NORMAL_CLOSURE_CODE, null)
        finish(null)
    }

    /**
     * Abort the underlying socket, tearing down an IN-FLIGHT handshake so a dial cancelled (or
     * failed) before the connection opens never leaks the just-created WebSocket. Idempotent and
     * harmless once the socket is already opened, closing or closed.
     */
    fun cancel() {
        webSocket.cancel()
        finish(null)
    }

    /** Idempotently terminate: mark dead and close the mailbox (null = clean, non-null = error). */
    private fun finish(cause: Throwable?) {
        terminated = true
        inbox.close(cause)
    }

    private inner class WsListener : WebSocketListener() {
        override fun onOpen(webSocket: WebSocket, response: Response) {
            opened.complete(Unit)
        }

        // Forwarded verbatim — the engine (A14) is the authoritative oversized-replay classifier.
        override fun onMessage(webSocket: WebSocket, text: String) {
            inbox.trySend(text)
        }

        override fun onMessage(webSocket: WebSocket, bytes: ByteString) {
            inbox.trySend(bytes.utf8())
        }

        override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
            webSocket.close(NORMAL_CLOSURE_CODE, null) // complete the closing handshake
            finish(null)
        }

        override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
            finish(null)
        }

        override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
            finish(t)
            // If we never opened, unblock connect() with the verbatim cause; no-op once opened.
            opened.completeExceptionally(t)
        }
    }
}
