package wang.yaojia.webterm.wiring

import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import wang.yaojia.webterm.session.TitleSanitizer
import wang.yaojia.webterm.terminalview.RemoteTerminalSession
import wang.yaojia.webterm.wire.ClientMessage

/**
 * # TerminalSessionControllerImpl (A21) — the real terminal-session controller.
 *
 * A15 froze the [TerminalSessionController] INTERFACE and shipped [DefaultTerminalSessionController] as
 * a minimal skeleton that owns the [SessionEngine][wang.yaojia.webterm.session.SessionEngine]↔[EventBus]
 * ↔UI wiring (start/pump/`output`/`events`/`sendInput`/`resize`/`notifyForegrounded`/`decideGate`/`close`).
 * A21 adds the terminal-render half: the [RemoteTerminalSession] (the forked Termux emulator), the
 * output→emulator feed, and the OSC-title → [TitleSanitizer] wiring.
 *
 * ### Config-survival: OWNED BY THE HOLDER, not the composition (§6.6 / FIX 3)
 * The [RemoteTerminalSession] (emulator + scrollback + scroll position) MUST survive a config change
 * (rotation/fold): §6.6 requires the emulator re-bind to a recreated `AndroidView` with NO replay
 * round-trip. So this whole controller is constructed and OWNED by the config-surviving
 * [RetainedSessionHolder] (its `viewModelScope`), NEVER `remember`ed in the composition (a
 * composition-remembered emulator dies on every rotation). `TerminalScreen` obtains it FROM the holder
 * and re-binds the surviving emulator on rotation; only a genuine background (generation bump) rebuilds
 * a fresh emulator that replays the ring.
 *
 * ### Why it WRAPS the skeleton (a constraint-driven, recorded shape)
 * The engine + per-session [EventBus] live in the holder alongside this controller. The real impl layers
 * the terminal-render wiring ON TOP of the [base] [DefaultTerminalSessionController] via interface
 * delegation (`by base`): every lifecycle/UI→engine call still routes through the skeleton to the
 * engine, and A21 only ADDS the two new responsibilities the plan assigns it:
 *  - **`output` → [RemoteTerminalSession.feedRemote]** — the decoded terminal-byte stream (`base.output`
 *    is already `Output→ByteArray` UTF-8-decoded off `Main` by [EventBus.outputBytes]) is fed verbatim
 *    to the emulator. The collector is wired in [start] BEFORE `base.start()`, so the `attached` frame +
 *    multi-MB ring replay are never dropped (plan §6.2), and it runs on `Main.immediate` (§6.6).
 *  - **OSC title → [TitleSanitizer]** — `:terminal-view` has no `:session-core` edge, so the raw title
 *    from the emulator is sanitized HERE (in `:app`) before it ever reaches the UI (plan §6.5 / §8).
 *
 * Emulator-originated bytes (DA/DSR replies, resize) flow back out through [routeEmulatorMessage] into
 * `base` → the engine's ordered send pump, so typing, key-bar taps and emulator replies share one wire
 * order (plan §6.3).
 *
 * @param base the holder-owned skeleton controller (a [DefaultTerminalSessionController]), UN-STARTED.
 * @param onSanitizedTitle receives the already-[TitleSanitizer]-sanitized OSC 0/2 title (inert display).
 * @param outputCollectorScope the scope the `output`→`feedRemote` collector runs in. The holder passes
 *   its config-surviving engine scope, so the collector is cancelled with the SESSION (not the
 *   composition) — a recompose never drops a frame (§6.6) and the surviving emulator keeps consuming.
 * @param outputDispatcher where the `output`→`feedRemote` collector runs (default `Main.immediate`,
 *   §6.6; injected as a virtual-time test dispatcher so the holder lifecycle is JVM-testable off `Main`).
 * @param remoteSessionFactory inject seam for the emulator-backed [RemoteTerminalSession] — the holder
 *   supplies it (defaulting to a real emulator) so a JVM test can substitute a virtual-time session.
 */
public class TerminalSessionControllerImpl(
    private val base: TerminalSessionController,
    onSanitizedTitle: (String) -> Unit,
    private val outputCollectorScope: CoroutineScope,
    private val outputDispatcher: CoroutineDispatcher = Dispatchers.Main.immediate,
    remoteSessionFactory: (engineSend: (ClientMessage) -> Unit, onTitle: (String) -> Unit) -> RemoteTerminalSession =
        { engineSend, onTitle -> RemoteTerminalSession(engineSend = engineSend, onTitleChanged = onTitle) },
) : TerminalSessionController by base {

    /**
     * The forked Termux emulator for this session (no local process). The screen puts it on-screen via
     * `attachView` and drives `updateSize`; A21 feeds it the remote output and routes its replies out.
     */
    public val remoteSession: RemoteTerminalSession =
        remoteSessionFactory(::routeEmulatorMessage) { raw -> onSanitizedTitle(TitleSanitizer.sanitize(raw)) }

    private var outputWired: Boolean = false

    /**
     * Wire `output` → [RemoteTerminalSession.feedRemote] on `Main.immediate` (idempotent), THEN start the
     * engine via `base.start()`. Wiring the feed before the engine starts guarantees the `attached` frame
     * and the multi-MB replay land in the already-registered [EventBus] output mailbox (plan §6.2).
     */
    override fun start() {
        if (!outputWired) {
            outputWired = true
            outputCollectorScope.launch(outputDispatcher) {
                base.output.collect { remoteSession.feedRemote(it) }
            }
        }
        base.start()
    }

    /** Detach (base) AND release the emulator's append thread. Idempotent — safe to call more than once. */
    override fun close() {
        base.close()
        remoteSession.close()
    }

    /**
     * The emulator's outbound sink → the engine's ordered send pump (via [base]). The Termux
     * `TerminalOutput` only ever emits [ClientMessage.Input] (DA/DSR/mouse replies) and, from
     * `updateSize`, [ClientMessage.Resize]; approve/reject/attach never originate here.
     */
    private fun routeEmulatorMessage(message: ClientMessage) {
        when (message) {
            is ClientMessage.Input -> base.sendInput(message.data)
            is ClientMessage.Resize -> base.resize(message.cols, message.rows)
            else -> Unit
        }
    }
}
