@file:OptIn(androidx.compose.material3.ExperimentalMaterial3Api::class)

package wang.yaojia.webterm.nav

import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.hilt.navigation.compose.hiltViewModel
import kotlinx.coroutines.delay
import wang.yaojia.webterm.hostregistry.Host
import wang.yaojia.webterm.screens.NewSessionRequest
import wang.yaojia.webterm.screens.TerminalScreen
import wang.yaojia.webterm.screens.TimelineSheet
import wang.yaojia.webterm.viewmodels.TimelineViewModel
import wang.yaojia.webterm.wiring.AppEnvironment
import wang.yaojia.webterm.wiring.RetainedSessionHolder
import wang.yaojia.webterm.wiring.SessionActivityBridge
import java.util.UUID

/** How long (steps × step-ms) to wait for the holder to publish its controller before giving up. */
private const val BRIDGE_WAIT_STEPS: Int = 200
private const val BRIDGE_WAIT_STEP_MS: Long = 50L

/**
 * # TerminalHost — the composition-root host for one terminal destination (A21/A22/A28/A29).
 *
 * Resolves [hostId] → its paired [Host] (for the dial endpoint), obtains the config-surviving
 * [RetainedSessionHolder] (per (host, session) via [hiltViewModel] — the nav back-stack entry is the
 * `ViewModelStoreOwner`; [holderKey] scopes a distinct holder when several are hosted under ONE owner,
 * e.g. the tablet detail pane), and renders [TerminalScreen]. It adds the two composition-root wirings
 * A21 left as seams:
 *
 *  - **A29 last-session lifecycle** — a per-session [SessionActivityBridge] folds the controller's
 *    control-event stream into [LastSessionStore][wang.yaojia.webterm.hostregistry.LastSessionStore]
 *    (set-on-`Adopted`, clear-on-`Exited`), so cold-start's「继续上次会话」points at the live session.
 *  - **A28 timeline** — the away-digest「展开」opens the [TimelineSheet] for the current session, its
 *    [TimelineViewModel] built over the per-host `ApiClient.events`.
 *
 * All Compose/lifecycle/emulator behaviour is device-QA (plan §7).
 *
 * @param sessionArg the raw route session arg ([NavRoutes.NEW_SESSION_SENTINEL] = spawn a new session).
 * @param cwd new-session working directory (only meaningful when spawning; ignored on re-attach).
 * @param onNewSessionInCwd nav callback for the toolbar/exit "在当前目录开新会话" action.
 * @param holderKey optional [hiltViewModel] key so the tablet detail pane can host a holder distinct
 *   from the stacked terminal destination under the SAME owner (null = the owner's default holder).
 */
@Composable
public fun TerminalHost(
    env: AppEnvironment,
    hostId: String?,
    sessionArg: String?,
    cwd: String?,
    onNewSessionInCwd: (NewSessionRequest) -> Unit,
    modifier: Modifier = Modifier,
    holderKey: String? = null,
) {
    if (hostId == null) {
        MessagePane("无效的主机。", modifier)
        return
    }

    // Resolve the paired host for its dial endpoint (untrusted at rest; a removed host stays "loading").
    val host by produceState<Host?>(initialValue = null, key1 = hostId) {
        value = runCatchingCancellable { env.hostStore.loadAll().firstOrNull { it.id == hostId } }.getOrNull()
    }
    val resolved = host
    if (resolved == null) {
        MessagePane("正在打开会话…", modifier)
        return
    }

    val sessionId = NavRoutes.attachSessionId(sessionArg)
    val holder: RetainedSessionHolder = hiltViewModel(key = holderKey ?: "$hostId/$sessionArg")

    // A29: drive LastSessionStore off the session's control-event stream once the holder publishes its
    // controller (bind() sets it during TerminalScreen's composition, after warm-up). A bounded wait
    // avoids a busy-spin if warm-up fails; the collection is cancelled when this composable leaves.
    LaunchedEffect(holder, hostId) {
        var controller = holder.controller
        var waited = 0
        while (controller == null && waited < BRIDGE_WAIT_STEPS) {
            delay(BRIDGE_WAIT_STEP_MS)
            controller = holder.controller
            waited++
        }
        controller?.let { live ->
            SessionActivityBridge(lastSessionStore = env.lastSessionStore, hostId = hostId)
                .observe(live.controlEvents())
        }
    }

    var showTimeline by remember(hostId, sessionArg) { mutableStateOf(false) }

    TerminalScreen(
        holder = holder,
        endpoint = resolved.endpoint,
        sessionId = sessionId,
        cwd = cwd,
        onNewSessionInCwd = onNewSessionInCwd,
        modifier = modifier,
        onWarmUp = { env.warmUp() },
        onDigestExpand = { if (sessionId != null) showTimeline = true },
    )

    if (showTimeline && sessionId != null) {
        val sessionUuid = remember(sessionId) { runCatching { UUID.fromString(sessionId) }.getOrNull() }
        val api = remember(resolved) { env.apiClientFactory.create(resolved.endpoint) }
        // A fresh VM each time the sheet opens (this block re-enters composition) → exactly one fetch.
        val timelineVm = remember { TimelineViewModel.forSession(sessionUuid) { id -> api.events(id) } }
        if (timelineVm != null) {
            TimelineSheet(viewModel = timelineVm, onDismiss = { showTimeline = false })
        } else {
            showTimeline = false
        }
    }
}
