package wang.yaojia.webterm.screens

import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import android.view.View
import android.view.WindowManager
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalWindowInfo
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.delay
import wang.yaojia.webterm.components.AwayDigestView
import wang.yaojia.webterm.components.GateBanner
import wang.yaojia.webterm.components.HardwareKeyRouter
import wang.yaojia.webterm.components.KeyBar
import wang.yaojia.webterm.components.PlanGateSheet
import wang.yaojia.webterm.components.ReconnectBanner
import wang.yaojia.webterm.components.TelemetryChips
import wang.yaojia.webterm.designsystem.LocalReduceMotion
import wang.yaojia.webterm.designsystem.Spacing
import wang.yaojia.webterm.designsystem.WebTermColors
import wang.yaojia.webterm.terminalview.RemoteTerminalView
import wang.yaojia.webterm.wire.GateKind
import wang.yaojia.webterm.wire.HostEndpoint
import wang.yaojia.webterm.wiring.RetainedSessionHolder
import wang.yaojia.webterm.wiring.TerminalSessionControllerImpl

/**
 * The arguments to open a NEW session — the pure output of the "new session in current cwd" decision.
 * `sessionId` is ALWAYS null (a fresh spawn); [cwd] is the current session's directory (`attach(null,
 * cwd)`, plan §1). A distinct type so the nav layer that consumes it (a NEW nav-scoped holder) reads a
 * stable contract.
 */
public data class NewSessionRequest(val sessionId: String?, val cwd: String?)

/**
 * The new-session-in-cwd routing decision (plan §1, §5 A21): the phone toolbar action and the exit-/
 * replay-too-large banner action both produce the SAME request — spawn a new session ([sessionId] null)
 * in the current session's [currentCwd]. Pure + JVM-testable.
 */
public object NewSessionInCwd {
    public fun requestFor(currentCwd: String?): NewSessionRequest =
        NewSessionRequest(sessionId = null, cwd = currentCwd)
}

/**
 * # TerminalScreen (A21/A22) — the live terminal, banner, cockpit gate surfaces, key-bar and toolbar.
 *
 * Hosts the forked Termux emulator ([RemoteTerminalView]) via an [AndroidView] and wires it to the
 * holder-owned [TerminalSessionControllerImpl]. Structure top→bottom: a toolbar (sanitized title +
 * "在当前目录开新会话"), the [ReconnectBanner], the away-digest ([AwayDigestView]) + telemetry chips
 * ([TelemetryChips]), the tool-gate card ([GateBanner]) above the terminal surface (with the privacy
 * cover), the [KeyBar] pinned above the IME, and — for plan gates — the [PlanGateSheet]
 * `ModalBottomSheet` overlay. Every server-derived string is rendered INERT by its component (§8).
 *
 * The Compose / AndroidView / lifecycle / privacy-shade / haptic behaviour here is DEVICE-QA (plan §7);
 * the banner reduction, gate stale-guard and new-session-in-cwd routing are the JVM-tested cores.
 *
 * ### Wiring order (FIX 1/2/3 — register-before-start, no split, config-survival)
 *  - The controller + its config-surviving emulator + the retained presenters ([RetainedSessionHolder.gateViewModel],
 *    [RetainedSessionHolder.bannerState], [RetainedSessionHolder.title]) are OWNED BY THE HOLDER and
 *    obtained via `holder.bind()` — NEVER `remember`ed here (a remembered emulator dies on rotation).
 *    `bind()` registers every consumer's `controlEvents()`/`output` mailbox EAGERLY and returns the
 *    controller UN-STARTED; this screen then calls `controller.start()` exactly once. That
 *    register-before-start ordering is what guarantees no `attached`/replay drop and no event split.
 *  - The banner and the gate presenter each hold their OWN control mailbox (FIX 1) — collected here via
 *    `collectAsStateWithLifecycle` (STARTED-scoped, §6.6).
 *
 * ### Lifecycle & config-change survival (plan §6.6)
 *  - `warmUp()` runs OFF-`Main` (its own `Dispatchers.IO` hop) BEFORE the first `bind()`; if it THROWS
 *    (mTLS/OkHttp build failure), the screen surfaces a retry instead of hanging not-ready (FIX 5).
 *  - The engine + emulator + presenters survive config changes in the retained [holder]; the
 *    `AndroidView` is keyed by the Compose-observable `holder.generation` so a REAL background→foreground
 *    cycle rebuilds a fresh emulator (ring replay), while a rotation re-binds the survivor.
 *    `holder.onStop(isChangingConfigurations)` drives that split.
 *  - `FLAG_SECURE` + a cover Composable on `ON_STOP` keep terminal bytes out of the recents snapshot (§8).
 *  - Latest-writer-wins resize: `ON_RESUME` and window-focus re-send the remembered dims via
 *    `notifyForegrounded` so the active device reclaims full-screen (plan §6.4).
 *
 * @param holder the nav-scoped, config-surviving session holder (`hiltViewModel()` at the destination).
 * @param onNewSessionInCwd nav callback that opens a new session for the produced [NewSessionRequest].
 * @param onWarmUp `AppEnvironment.warmUp` — builds the shared OkHttp/mTLS stack off `Main` before bind.
 * @param onDigestExpand the A28 away-digest「展开」seam — the composition root opens the timeline sheet for
 *   the current session (default no-op keeps the screen usable standalone). This only WIRES the existing
 *   [AwayDigestView.onExpand][wang.yaojia.webterm.components.AwayDigestView] callback outward; no internal
 *   logic changes.
 */
@Composable
public fun TerminalScreen(
    holder: RetainedSessionHolder,
    endpoint: HostEndpoint,
    sessionId: String?,
    cwd: String?,
    onNewSessionInCwd: (NewSessionRequest) -> Unit,
    modifier: Modifier = Modifier,
    onWarmUp: suspend () -> Unit = {},
    onDigestExpand: () -> Unit = {},
) {
    val context = LocalContext.current
    val activity = remember(context) { context.findActivity() }

    // Privacy shade: FLAG_SECURE while the terminal is on screen so the recents thumbnail never leaks
    // terminal bytes (plan §8). Cleared when the screen leaves.
    DisposableEffect(activity) {
        activity?.window?.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
        onDispose { activity?.window?.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) }
    }

    // Warm the shared OkHttp/mTLS stack OFF-Main before the first bind (A15 / AppEnvironment.warmUp).
    // FIX 5: if warmUp() throws (mTLS/OkHttp build failure off-Main), surface a retry instead of hanging
    // not-ready forever. CancellationException is rethrown so a config-change cancel is never swallowed.
    var ready by remember(endpoint, sessionId) { mutableStateOf(false) }
    var warmUpFailed by remember(endpoint, sessionId) { mutableStateOf(false) }
    var warmUpAttempt by remember(endpoint, sessionId) { mutableIntStateOf(0) }
    LaunchedEffect(endpoint, sessionId, warmUpAttempt) {
        warmUpFailed = false
        try {
            onWarmUp()
            ready = true
        } catch (e: CancellationException) {
            throw e
        } catch (_: Throwable) {
            warmUpFailed = true
        }
    }
    if (!ready) {
        if (warmUpFailed) WarmUpErrorPane(modifier, onRetry = { warmUpAttempt += 1 }) else LoadingPane(modifier)
        return
    }

    // FIX 2/FIX 3: the controller (+ its config-surviving emulator) and the retained presenters are
    // OWNED BY THE HOLDER. Reading the Compose-observable generation re-keys the AndroidView on a
    // genuine-background rebuild; a config change (generation unchanged) re-binds the survivor. bind() is
    // idempotent for the same (endpoint, sessionId) and registers every mailbox before returning UN-STARTED.
    val generation = holder.generation
    val controller = holder.bind(endpoint, sessionId, cwd)
    val gateViewModel = holder.gateViewModel ?: return // populated by bind(); guarded defensively

    // FIX 1: banner + gate each collect their OWN retained controlEvents() mailbox (folded in the holder),
    // so there is no split. Read the retained state here, STARTED-scoped (§6.6).
    val banner by holder.bannerState.collectAsStateWithLifecycle()
    val gateUi by gateViewModel.uiState.collectAsStateWithLifecycle()
    val title by holder.title

    // Start the engine ONCE, AFTER bind() registered every consumer's mailbox (register-before-start).
    // Idempotent: a config-change recomposition re-obtains the SAME survivor and start() is a no-op.
    LaunchedEffect(controller) { controller.start() }

    // Arrival haptic (FIX 4): one buzz per NEW gate (rising edge), gated by the OS reduce-motion setting.
    val haptics = LocalHapticFeedback.current
    val reduceMotion = LocalReduceMotion.current
    LaunchedEffect(gateViewModel, reduceMotion) {
        gateViewModel.gateArrivals.collect {
            if (!reduceMotion) haptics.performHapticFeedback(HapticFeedbackType.LongPress)
        }
    }

    // Privacy cover on ON_STOP + config-change-aware holder teardown + ON_RESUME full-screen reclaim.
    var covered by remember { mutableStateOf(false) }
    val lifecycleOwner = LocalLifecycleOwner.current
    DisposableEffect(lifecycleOwner, activity, controller) {
        val observer = LifecycleEventObserver { _, event ->
            when (event) {
                Lifecycle.Event.ON_STOP -> {
                    covered = true
                    holder.onStop(activity?.isChangingConfigurations == true)
                }
                Lifecycle.Event.ON_START -> covered = false
                Lifecycle.Event.ON_RESUME -> controller.notifyForegrounded(null, null)
                else -> Unit
            }
        }
        lifecycleOwner.lifecycle.addObserver(observer)
        onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
    }

    // Window-focus reclaim (device switch) → re-send remembered dims (latest-writer-wins, §6.4).
    val windowInfo = LocalWindowInfo.current
    LaunchedEffect(windowInfo, controller) {
        snapshotFlow { windowInfo.isWindowFocused }.collect { focused ->
            if (focused) controller.notifyForegrounded(null, null)
        }
    }

    val requestNewSession: () -> Unit = { onNewSessionInCwd(NewSessionInCwd.requestFor(cwd)) }

    // A slow wall-clock tick so the telemetry chips grey out as the last frame ages (§ staleness).
    val nowMs by produceState(initialValue = System.currentTimeMillis()) {
        while (true) {
            delay(TELEMETRY_TICK_MS)
            value = System.currentTimeMillis()
        }
    }

    Column(modifier = modifier.fillMaxSize()) {
        TerminalToolbar(title = title, onNewSessionInCwd = requestNewSession)
        ReconnectBanner(model = banner, onNewSession = requestNewSession)
        // Cockpit surfaces (A22): away-digest (once per reconnect) + statusLine telemetry chips. Both hide
        // themselves when their state is null/empty; server strings render inert (§8).
        gateUi.digest?.let { AwayDigestView(digest = it, onExpand = onDigestExpand) } // onExpand → A28 timeline (wired)
        gateUi.telemetry?.let { TelemetryChips(telemetry = it, nowMs = nowMs) }
        // Tool gate → an inline approve/reject card; plan gates use the ModalBottomSheet below.
        gateUi.gate?.takeIf { it.kind == GateKind.TOOL }?.let { toolGate ->
            GateBanner(gate = toolGate, onDecide = gateViewModel::decide)
        }
        Box(modifier = Modifier.weight(1f).fillMaxWidth()) {
            key(generation) {
                AndroidView(
                    modifier = Modifier.fillMaxSize(),
                    factory = { ctx ->
                        val remoteView = RemoteTerminalView(ctx)
                        // Hardware chords (DECCKM split): Esc/Ctrl-letter/⇧Tab app-side, arrows/Enter/Tab
                        // deferred to Termux's KeyHandler (returns false) — plan §6.3.
                        remoteView.onKeyCommand = { keyCode, keyEvent ->
                            HardwareKeyRouter.handle(keyCode, keyEvent, controller::sendInput)
                        }
                        // A layout/size change feeds the latest-writer-wins reclaim path (§6.4). The pixel→
                        // grid metric read (Termux's package-private mFontWidth) + emulator resize is
                        // completed in the view layer on-device; here we reclaim full-screen.
                        remoteView.onViewSizeChanged = { _, _ -> controller.notifyForegrounded(null, null) }
                        controller.remoteSession.attachView(remoteView)
                        remoteView.hostView()
                    },
                    // Config change / real background: unbind the view but the holder-owned emulator
                    // survives (FIX 3) — never close it here (that is the holder teardown's job).
                    onRelease = { controller.remoteSession.detachView() },
                )
            }
            if (covered) PrivacyCover()
        }
        // The IME-bypass touch key-bar, pinned above the keyboard (hidden on wide screens / hardware kbd).
        KeyBar(onSend = controller::sendInput)
    }

    // Plan gate → a three-way ModalBottomSheet overlay. Dismissing (scrim/back) resolves NOTHING — the
    // gate stays held until an explicit decision (PlanGateSheet contract); the epoch stale-guard lives in
    // GateViewModel.decide.
    gateUi.gate?.takeIf { it.kind == GateKind.PLAN }?.let { planGate ->
        PlanGateSheet(gate = planGate, onDecide = gateViewModel::decide, onDismiss = {})
    }
}

/** Telemetry-chip staleness tick interval (ms) — re-reads the wall clock so chips grey as a frame ages. */
private const val TELEMETRY_TICK_MS: Long = 1_000L

/** Toolbar: the inert (already-sanitized) session title + the new-session-in-cwd action. */
@Composable
private fun TerminalToolbar(title: String, onNewSessionInCwd: () -> Unit) {
    Surface(color = MaterialTheme.colorScheme.surface, modifier = Modifier.fillMaxWidth()) {
        Row(
            verticalAlignment = Alignment.CenterVertically,
            horizontalArrangement = Arrangement.spacedBy(Spacing.sm8),
            modifier = Modifier.fillMaxWidth().padding(horizontal = Spacing.md12, vertical = Spacing.xs4),
        ) {
            Text(
                text = title.ifEmpty { "终端" },
                style = MaterialTheme.typography.titleMedium,
                color = MaterialTheme.colorScheme.onSurface,
                maxLines = 1,
                overflow = TextOverflow.Ellipsis,
                modifier = Modifier.weight(1f),
            )
            TextButton(onClick = onNewSessionInCwd) { Text("在当前目录开新会话") }
        }
    }
}

/** The privacy cover shown while backgrounded so the recents snapshot never shows terminal content. */
@Composable
private fun PrivacyCover() {
    Surface(color = WebTermColors.terminalBackground, modifier = Modifier.fillMaxSize()) {
        Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) {
            Text("WebTerm", color = WebTermColors.terminalForeground.copy(alpha = 0.4f))
        }
    }
}

/** Placeholder while the shared OkHttp/mTLS stack warms up off-Main before the first bind. */
@Composable
private fun LoadingPane(modifier: Modifier) {
    Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
        Text("连接中…", color = MaterialTheme.colorScheme.onBackground)
    }
}

/**
 * Shown when `warmUp()` threw building the mTLS/OkHttp stack (FIX 5) — an actionable retry instead of a
 * permanent not-ready hang. The failure detail is intentionally NOT rendered (it may carry mTLS/host
 * internals); the copy is a fixed, inert string.
 */
@Composable
private fun WarmUpErrorPane(modifier: Modifier, onRetry: () -> Unit) {
    Column(
        modifier = modifier.fillMaxSize().padding(Spacing.lg16),
        verticalArrangement = Arrangement.spacedBy(Spacing.sm8, Alignment.CenterVertically),
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        Text("连接初始化失败", color = MaterialTheme.colorScheme.onBackground)
        TextButton(onClick = onRetry) { Text("重试") }
    }
}

/** Unwrap a [Context] to its host [Activity] (for FLAG_SECURE + isChangingConfigurations). */
private tailrec fun Context.findActivity(): Activity? = when (this) {
    is Activity -> this
    is ContextWrapper -> baseContext.findActivity()
    else -> null
}

/**
 * The [android.view.View] to add to the Compose tree — the Termux `TerminalView` behind
 * [RemoteTerminalView].
 *
 * BOUNDARY WORKAROUND (recorded): A16's `RemoteTerminalView.terminalView` is typed
 * `com.termux.view.TerminalView`, but `:terminal-view` scopes the Termux `terminal-view` artifact as
 * `implementation`, so that concrete type is NOT on `:app`'s compile classpath (and neither
 * `app/build.gradle.kts` nor `:terminal-view`/`RemoteTerminalView` is in A21's editable lane). We
 * therefore read the already-attached stock view through its public getter as a plain [View] — a
 * `TerminalView` IS a `View`, so hosting it in an `AndroidView` is correct; only the compile-time type
 * name is avoided. The clean fix (out of A21's lane) is to `api`-expose the Termux view from
 * `:terminal-view` or add `libs.termux.terminal.view` to `:app`, then `AndroidView { remote.terminalView }`.
 */
private fun RemoteTerminalView.hostView(): View =
    RemoteTerminalView::class.java.getMethod("getTerminalView").invoke(this) as View
