package wang.yaojia.webterm.wiring

import dagger.Lazy
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import wang.yaojia.webterm.api.pairing.runPairingProbe
import wang.yaojia.webterm.hostregistry.HostStore
import wang.yaojia.webterm.hostregistry.LastSessionStore
import wang.yaojia.webterm.tlsandroid.IdentityRepository
import wang.yaojia.webterm.viewmodels.PairingProber
import wang.yaojia.webterm.wire.HttpTransport
import wang.yaojia.webterm.wire.TermTransport
import javax.inject.Inject
import javax.inject.Singleton

/**
 * The composition root (plan §3 `:app` wiring): the single typed handle to the frozen object graph
 * that AW4 screens/ViewModels resolve their collaborators through. Hilt assembles the graph via the
 * per-feature `di/` Hilt module boundaries; this aggregate is what the app-level nav / ViewModels inject
 * to reach the app-scoped singletons without naming every provider.
 *
 * ### Confinement contract (invariant #4 — recorded here at the seam)
 * The object graph enforces exactly two cross-boundary paths, and NOTHING here widens them:
 *  - **UI→engine:** through [TerminalSessionController] (`sendInput`/`resize`/`notifyForegrounded`/
 *    `decideGate`), whose engine runs on its own `limitedParallelism(1)` confinement.
 *  - **engine→UI:** through the PER-SESSION [EventBus]'s per-consumer channels (R10). The bus is minted
 *    inside [RetainedSessionHolder.bind]; it is NOT an app singleton (FIX 1) so sessions never cross-talk.
 * Engine state is never touched off-confinement and emulator/scrollback state never off-`Main`.
 *
 * ### Off-`Main` warm-up (FIX 3)
 * The mTLS transports and [identityRepository] are behind `dagger.Lazy` so constructing this root does
 * NO AndroidKeyStore/Tink/`OkHttpClient` I/O on the injection (often `Main`) thread. [warmUp] resolves
 * them on `Dispatchers.IO`, so the shared client is built and the device identity first-touched off the
 * UI thread. A21/A27 call [warmUp] before the first `bind()` / cert-screen open.
 *
 * What lives here (all app singletons):
 *  - [hostStore] / [lastSessionStore] — persisted, non-secret UI state (DataStore).
 *  - [identityRepository] — the mTLS device identity bridged onto the ONE shared `OkHttpClient` (lazy).
 *  - [apiClientFactory] / [sessionEngineFactory] — per-host / per-session builders over the shared
 *    transports (lazy).
 *  - [coldStartPolicy] — the cold-launch route seam (A29).
 * Per-session state (the [SessionEngine][wang.yaojia.webterm.session.SessionEngine] + emulator + its
 * [EventBus]) is NOT here — it lives in the config-surviving [RetainedSessionHolder].
 */
@Singleton
public class AppEnvironment @Inject constructor(
    public val hostStore: HostStore,
    public val lastSessionStore: LastSessionStore,
    public val apiClientFactory: ApiClientFactory,
    public val sessionEngineFactory: SessionEngineFactory,
    public val coldStartPolicy: ColdStartPolicy,
    /**
     * B4 · Builds a [DeviceEnroller][wang.yaojia.webterm.tlsandroid.DeviceEnroller] per control-plane URL
     * for the zero-`.p12` enrollment screen (A-enroll). App-scoped; the screen calls it off `Main`.
     */
    public val enrollmentFlowFactory: EnrollmentFlowFactory,
    private val identityRepositoryLazy: Lazy<IdentityRepository>,
    private val httpTransportLazy: Lazy<HttpTransport>,
    private val termTransportLazy: Lazy<TermTransport>,
) {
    /**
     * The mTLS device identity (A27 cert screen). Resolving the [Lazy] merely constructs the repository
     * (I/O-free per A11); its FIRST touch (`currentSummary`/`hasInstalledIdentity`/handshake) does the
     * slow AndroidKeyStore + Tink I/O and MUST run off `Main` — [warmUp] does that first touch, and A27
     * likewise touches it off the UI thread.
     */
    public val identityRepository: IdentityRepository get() = identityRepositoryLazy.get()

    /**
     * The shared REST [HttpTransport] (A24 diff-fetcher seam). Resolving the [Lazy] builds the shared
     * `OkHttpClient` (mTLS I/O) — call ONLY after [warmUp] has run off `Main`, else this first-touch
     * does AndroidKeyStore/Tink work on the calling thread.
     */
    public val httpTransport: HttpTransport get() = httpTransportLazy.get()

    /**
     * Build the two-step pairing prober (A19) over the ONE shared transports. The transports are
     * resolved LAZILY inside `probe()` (not at build time) so constructing the prober on the UI thread
     * never triggers the mTLS/OkHttp build — the actual resolve happens in the probe's own coroutine,
     * after [warmUp].
     */
    public fun buildPairingProber(): PairingProber =
        PairingProber { endpoint -> runPairingProbe(endpoint, httpTransportLazy.get(), termTransportLazy.get()) }

    /**
     * Build the shared `OkHttpClient` and first-touch the device identity OFF `Main` (FIX 3). Resolving
     * either transport builds the one shared client (which reads the mTLS material); touching the
     * identity forces its first AndroidKeyStore/Tink read. Idempotent at the singleton level — the
     * client/identity are built once and cached. Safe to call from any coroutine; hops to
     * `Dispatchers.IO` internally.
     */
    public suspend fun warmUp() {
        withContext(Dispatchers.IO) {
            sessionEngineFactory.warm() // builds TermTransport → the shared OkHttpClient → reads SSL material
            apiClientFactory.warm() // HttpTransport reuses the SAME already-built client
            runCatching { identityRepositoryLazy.get().hasInstalledIdentity() } // first AndroidKeyStore/Tink touch
        }
    }
}
