package wang.yaojia.webterm.wiring

import androidx.compose.runtime.State
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import wang.yaojia.webterm.components.BannerModel
import wang.yaojia.webterm.components.bannerModel
import wang.yaojia.webterm.session.SessionEngine
import wang.yaojia.webterm.terminalview.RemoteTerminalSession
import wang.yaojia.webterm.viewmodels.GateViewModel
import wang.yaojia.webterm.wire.ClientMessage
import wang.yaojia.webterm.wire.HostEndpoint
import javax.inject.Inject

/**
 * The config-surviving home for a session's ENTIRE per-session unit — the [SessionEngine], its
 * per-session [EventBus], the [RemoteTerminalSession] emulator (via [TerminalSessionControllerImpl]),
 * and the retained UI presenters ([GateViewModel] + [bannerState] + [title]) — plan §6.6, the
 * load-bearing lifecycle contract for A16/A21/A22. A `@HiltViewModel` retained across Activity
 * recreation, so a **rotation / fold / multi-window / dark-mode** change disposes the Compose tree but
 * NOT this unit: the surviving emulator is re-bound to the recreated `AndroidView` with no detach, no
 * ~2 MB replay round-trip, and no scroll-position loss.
 *
 * ### Config change vs real background (the distinction that drives everything)
 *  - **Config change** — the Activity is recreated but the ViewModel survives (Android guarantees this).
 *    [onStop] with `isChangingConfigurations == true` does NOTHING: the engine + emulator keep running,
 *    [generation] does NOT bump, and the recreated Compose tree calls [bind] again to reuse the survivor
 *    (re-binding the same emulator to the new `AndroidView`).
 *  - **Real background** — `ON_STOP` that is NOT a config change. [onStop] with
 *    `isChangingConfigurations == false` detaches cleanly (server PTY survives) and bumps [generation]
 *    so the NEXT foreground return rebuilds a fresh emulator that replays the ring and re-fires resize.
 *
 * ### The emulator lives HERE, not in composition (FIX 3 — §6.6 config-survival)
 * [TerminalSessionControllerImpl] (which owns the [RemoteTerminalSession] emulator + scrollback) is
 * constructed and OWNED here, in the config-surviving `viewModelScope`, NOT `remember`ed in the
 * composition — a composition-remembered emulator dies on every rotation. `TerminalScreen` obtains the
 * controller FROM [bind] and re-binds the surviving emulator; the output→emulator collector runs in the
 * engine's confined [engineScope], so it is cancelled with the SESSION, not the composition (§6.6).
 *
 * ### [generation] is Compose-observable (FIX 2)
 * Backed by [mutableIntStateOf], so a real-background rebuild (a bump) RECOMPOSES `TerminalScreen` and
 * re-keys its `AndroidView` (fresh emulator + ring replay); a config change (no bump) leaves it unchanged
 * so the recreated `AndroidView` re-binds the SAME surviving emulator.
 *
 * ### Retained control-event consumers (FIX 1/FIX 4 — no split, config-survival)
 * The banner AND the [GateViewModel] each need the FULL control-event stream. Each is created here and
 * subscribes to its OWN [TerminalSessionController.controlEvents] mailbox — so they never split events —
 * and folds in [engineScope], so their state survives a rotation (a composition-scoped fold would reset
 * to `Hidden`/empty and, worse, leak a fresh orphaned mailbox on every rotation). All consumers register
 * their mailbox EAGERLY inside [bind], which returns the controller UN-STARTED — so `controller.start()`
 * (called by the screen) can never drop the `attached` frame or the multi-MB replay.
 *
 * ### close() BEFORE scope-cancel — made STRUCTURAL (FIX 5 — do NOT reorder)
 * [SessionEngine] does **not** close its live WS on scope cancellation, so cancelling the confinement
 * scope alone would leak the socket to a timeout death — never a clean RFC-6455 `1000` detach. Teardown
 * therefore calls [SessionEngine.close] FIRST and **awaits the returned [kotlinx.coroutines.Job]**
 * (`closeJob.join()`) so the `1000` close frame is handed to the transport writer BEFORE the scope is
 * cancelled, THEN releases the per-session [EventBus] mailboxes and cancels the scope. The
 * [RemoteTerminalSession] append thread is released too (session end).
 *
 * ### Single-session-for-life (no silent mis-route)
 * A holder binds exactly ONE (endpoint, sessionId) for its lifetime. A re-[bind] with the SAME key is
 * the config-change reuse path; a re-[bind] with a DIFFERENT key is a nav-scoping programming error and
 * throws (a new session needs a new nav-scoped holder), so a mis-scoped multi-tab UI can never show the
 * wrong session behind a silently-ignored argument.
 */
@HiltViewModel
public class RetainedSessionHolder @Inject constructor(
    private val engineFactory: SessionEngineFactory,
) : ViewModel() {

    private var engine: SessionEngine? = null
    private var engineScope: CoroutineScope? = null
    private var eventBus: EventBus? = null
    private var controllerImpl: TerminalSessionControllerImpl? = null
    private var gateVm: GateViewModel? = null

    /** The (endpoint, sessionId) this holder is bound to for life; a re-[bind] must match it. */
    private var boundKey: BoundKey? = null

    /** Identity of the single session a holder owns for life; [cwd] is excluded (spawn-only, not identity). */
    private data class BoundKey(val endpoint: HostEndpoint, val sessionId: String?)

    /**
     * Bumped ONLY on a genuine background detach (never on a config change), and Compose-observable (FIX
     * 2): the terminal `AndroidView` is keyed by this so a real background→foreground cycle RECOMPOSES
     * and rebuilds a fresh emulator (ring replay), while a rotation re-binds the SAME survivor.
     */
    private val generationState = mutableIntStateOf(0)
    public val generation: Int get() = generationState.intValue

    /** The live controller for the bound session, or null before the first [bind] / after teardown. */
    public val controller: TerminalSessionController? get() = controllerImpl

    /** The retained gate/telemetry/digest presenter (A22), or null before [bind] / after teardown. */
    public val gateViewModel: GateViewModel? get() = gateVm

    /** The sanitized OSC 0/2 title (inert), reset per session. Read by the toolbar; survives a rotation. */
    private val titleState = mutableStateOf("")
    public val title: State<String> get() = titleState

    /** The retained reconnect-banner model, folded from its OWN control mailbox; survives a rotation. */
    private val bannerFlow = MutableStateFlow<BannerModel>(BannerModel.Hidden)
    public val bannerState: StateFlow<BannerModel> = bannerFlow.asStateFlow()

    /**
     * Where the `output`→`feedRemote` collector runs (default `Main.immediate`, §6.6). Null → resolve
     * `Dispatchers.Main.immediate` lazily in [bind] (NOT eagerly at construction — that would touch
     * `Dispatchers.Main` and throw in a `runTest` without `setMain`). A JVM test injects a virtual-time
     * dispatcher so the holder lifecycle drives off `Main`. `internal` — not frozen surface.
     */
    internal var mainDispatcher: CoroutineDispatcher? = null

    /**
     * Emulator factory seam. Production builds a real [RemoteTerminalSession]; a JVM test substitutes a
     * virtual-time one so the holder lifecycle test never spins a real append thread. `internal`.
     */
    internal var remoteSessionFactory:
        (engineSend: (ClientMessage) -> Unit, onTitle: (String) -> Unit) -> RemoteTerminalSession =
        { engineSend, onTitle -> RemoteTerminalSession(engineSend = engineSend, onTitleChanged = onTitle) }

    /**
     * Bind the session for [endpoint], or REUSE the survivor across a config change. Called by the
     * terminal screen on (re)composition. [sessionId] null spawns a new session (with [cwd]); a
     * non-null id re-attaches for ring replay. Idempotent for the SAME (endpoint, sessionId): a second
     * call while that session is live returns the existing controller (config-change re-bind) and never
     * spawns a duplicate engine or emulator.
     *
     * A holder is **single-session-for-life**: a re-bind whose (endpoint, sessionId) does NOT match the
     * bound one throws [IllegalStateException] — a nav-scoping programming error, since the short-circuit
     * would otherwise silently ignore the new args and show the WRONG session. A different session must
     * get its own nav-scoped holder.
     *
     * Returns the controller **UN-STARTED**: every consumer (output + banner + gate mailboxes) is already
     * registered EAGERLY here, and the terminal screen calls `controller.start()` afterwards. This
     * register-before-start ordering guarantees no `attached`/replay frame is dropped and no control
     * event is split (FIX 1).
     */
    public fun bind(
        endpoint: HostEndpoint,
        sessionId: String? = null,
        cwd: String? = null,
    ): TerminalSessionControllerImpl {
        val requested = BoundKey(endpoint, sessionId)
        controllerImpl?.let { existing ->
            check(requested == boundKey) {
                "RetainedSessionHolder is single-session-for-life: already bound to $boundKey but bind() " +
                    "requested $requested. A different session needs a new nav-scoped holder."
            }
            return existing // same key → config-change reuse of the live engine/emulator/presenters
        }
        val scope = engineFactory.newConfinedScope()
        val bus = EventBus() // one bus per engine, lives on the engine's confined scope
        val newEngine = engineFactory.create(endpoint, scope, sessionId, cwd)
        val base = DefaultTerminalSessionController(newEngine, bus, scope)
        // FIX 3: the render controller (+ its emulator) is owned HERE; the output collector runs in the
        // confined [scope] on [mainDispatcher], so it dies with the session, not the composition (§6.6).
        val controller = TerminalSessionControllerImpl(
            base = base,
            onSanitizedTitle = { titleState.value = it },
            outputCollectorScope = scope,
            outputDispatcher = mainDispatcher ?: Dispatchers.Main.immediate,
            remoteSessionFactory = remoteSessionFactory,
        )
        // FIX 1/FIX 4: each retained consumer registers its OWN control mailbox EAGERLY (before start)
        // and folds in the confined [scope] so its state survives a config change.
        val gate = GateViewModel(controller) // registers the gate mailbox at construction
        gate.bind(scope)
        val bannerEvents = controller.controlEvents() // registers the banner mailbox
        scope.launch { bannerEvents.collect { bannerFlow.value = bannerModel(bannerFlow.value, it) } }

        engine = newEngine
        engineScope = scope
        eventBus = bus
        controllerImpl = controller
        gateVm = gate
        boundKey = requested
        // DO NOT start here — the terminal screen calls controller.start() after obtaining the presenters.
        return controller
    }

    /**
     * Drive from the Activity's `onStop`. [isChangingConfigurations] (`Activity.isChangingConfigurations`)
     * separates a rotation/config change (survive) from a real background (clean detach + generation bump).
     */
    public fun onStop(isChangingConfigurations: Boolean) {
        if (isChangingConfigurations) return // config change → the survivor is re-bound on recreate
        teardown(bumpGeneration = true) // real background → clean detach; server PTY survives
    }

    /** Genuine destruction (nav pop / not a config change): detach cleanly; the VM won't be re-bound. */
    override fun onCleared() {
        teardown(bumpGeneration = false)
        super.onCleared()
    }

    private fun teardown(bumpGeneration: Boolean) {
        val liveEngine = engine ?: return
        val scope = engineScope
        val bus = eventBus
        val impl = controllerImpl
        // (1) Clean detach FIRST — the server-side PTY keeps running (invariant #2, §6.6 follow-up).
        val closeJob = liveEngine.close()
        // (2) AWAIT the clean close (the RFC-6455 `1000` frame handed to the transport writer), THEN
        // release the per-session EventBus mailboxes, and ONLY THEN cancel the confinement scope (FIX 5).
        // Joining makes close-before-cancel structural rather than relying on implicit same-dispatcher
        // FIFO — the socket is never leaked to a timeout death; closing the bus here (session end, not a
        // config-change cancel) frees the mailboxes exactly when the session dies.
        scope?.launch {
            closeJob.join()
            bus?.close()
            scope.cancel()
        }
        // Release the emulator's append thread (session end); the output collector then no-ops on the
        // closed command channel until the scope cancel above stops it.
        impl?.remoteSession?.close()
        engine = null
        engineScope = null
        eventBus = null
        controllerImpl = null
        gateVm = null
        boundKey = null
        // Reset the retained presenters so a generation-bump rebuild starts clean.
        titleState.value = ""
        bannerFlow.value = BannerModel.Hidden
        if (bumpGeneration) generationState.intValue += 1
    }
}
