package wang.yaojia.webterm.wiring

import dagger.Lazy
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import wang.yaojia.webterm.session.SessionEngine
import wang.yaojia.webterm.wire.HostEndpoint
import wang.yaojia.webterm.wire.TermTransport
import wang.yaojia.webterm.wire.TimelineEvent
import javax.inject.Inject
import javax.inject.Singleton

/**
 * Mints a [SessionEngine] per session over the ONE shared [TermTransport] (A15 wiring; plan §0
 * invariant #4, §6.6). A [SessionEngine] is per-session (its [HostEndpoint] + confinement scope are
 * fixed at creation), so it is built on demand by the [RetainedSessionHolder], never a singleton.
 *
 * ### The transport is [Lazy] (FIX 3 — no AndroidKeyStore/Tink I/O on the injection/Main thread)
 * Resolving [TermTransport] builds the shared `OkHttpClient`, which reads the mTLS SSL material — a
 * path that first-touches AndroidKeyStore + Tink (slow, MUST be off `Main`). Injecting a
 * `dagger.Lazy<TermTransport>` means merely constructing this factory (and therefore `AppEnvironment`)
 * does NOT build the client. The heavy resolution happens exactly once, off `Main`, in [warm] (called
 * from `AppEnvironment.warmUp()` on `Dispatchers.IO`); afterwards [create] resolves the already-built
 * singleton instantly.
 *
 * ### Confinement (invariant #4)
 * [newConfinedScope] creates the engine's `limitedParallelism(1)` dispatcher — the single-thread
 * confinement that replaces the Swift actor. Each engine gets its OWN scope so the holder can cancel
 * it on teardown WITHOUT touching other sessions; ALL engine state is then touched only on that
 * dispatcher. The scope's root is a [SupervisorJob] so one failed child never tears the engine down.
 */
@Singleton
public class SessionEngineFactory @Inject constructor(
    private val transport: Lazy<TermTransport>,
) {
    /**
     * Test seam: builds the engine's confinement scope. Production creates a real
     * `Dispatchers.Default.limitedParallelism(1)` scope; a unit test substitutes a virtual-time
     * `TestDispatcher` so the holder/engine lifecycle runs deterministically under `runTest`.
     * `internal` — not part of the frozen public surface.
     */
    internal var confinedScopeFactory: () -> CoroutineScope = {
        CoroutineScope(Dispatchers.Default.limitedParallelism(1) + SupervisorJob())
    }

    /**
     * A fresh single-thread confinement scope for one engine. Owned by the caller
     * ([RetainedSessionHolder]) which cancels it AFTER `engine.close()` on teardown (§6.6).
     */
    public fun newConfinedScope(): CoroutineScope = confinedScopeFactory()

    /**
     * Resolve the shared [TermTransport] (→ builds the shared `OkHttpClient` → first mTLS touch) so the
     * slow AndroidKeyStore/Tink I/O runs on the CALLER's thread. Call ONLY from a background
     * dispatcher (`AppEnvironment.warmUp()` on `Dispatchers.IO`), never on `Main`.
     */
    public fun warm() {
        transport.get()
    }

    /**
     * Build a [SessionEngine] for [endpoint] on [scope]. [sessionId] null → a NEW session (with [cwd]
     * for new-session-in-cwd); a non-null id re-attaches so the server replays the ring buffer.
     * [awayTimeline] supplies the once-per-reconnect digest source (A21 wires it to `ApiClient.events`
     * for the adopted id); it defaults to empty so the freeze stays honest — the engine still emits an
     * (empty, UI-suppressed) digest.
     *
     * Resolves the [Lazy] transport; call [warm] off `Main` first so this is a cheap singleton read.
     */
    public fun create(
        endpoint: HostEndpoint,
        scope: CoroutineScope,
        sessionId: String? = null,
        cwd: String? = null,
        awayTimeline: suspend (sinceMs: Long) -> List<TimelineEvent> = { emptyList() },
    ): SessionEngine =
        SessionEngine(
            transport = transport.get(),
            endpoint = endpoint,
            scope = scope,
            initialSessionId = sessionId,
            initialCwd = cwd,
            awayTimeline = awayTimeline,
        )
}
