import Foundation
import HostRegistry
import Observation
import SessionCore
import WireProtocol

/// T-iOS-15 · One open terminal = one controller: builds and owns the
/// per-session stack (engine → fan-out → TerminalViewModel + GateViewModel +
/// SessionActivityBridge) and drives the scenePhase lifecycle.
///
/// Lifecycle (plan §7 T-iOS-15):
/// - `.background` → `suspend()`: `engine.close()` — a clean detach (the
///   server-side PTY keeps running), never a half-dead socket that iOS will
///   kill mid-suspend anyway.
/// - `.active` after a suspend → `resumeIfNeeded()`: `close()` is terminal for
///   an engine, so the stack is REBUILT and re-opened against the
///   server-adopted sessionId. The fresh SwiftTerm view (new `generation` →
///   new UIView) replays the full ring buffer and fires `sizeChanged` on first
///   layout — that resize is the latest-writer-wins full-screen reclaim
///   (换设备夺回全屏), immediately after the attach.
/// - `.active` with the engine still alive (transient `.inactive`, e.g.
///   control center / app switcher peek): `engine.notifyForegrounded(dims:)`
///   with `TerminalViewModel.lastSentDims` — reconnect-if-needed + resize
///   reclaim (latest-writer-wins). Skipped only when no valid dims were ever
///   sent (SwiftTerm has not laid out yet → its first `sizeChanged` covers
///   it). The W4 documented deviation is CLOSED (orchestrator added the
///   `lastSentDims` accessor on behalf of the T-iOS-11 owner).
@MainActor
@Observable
final class TerminalSessionController: Identifiable {
    let id = UUID()

    /// Bumped on every rebuild; the container view uses it as the SwiftUI
    /// identity of `TerminalScreen`, forcing a fresh `makeUIView` so the new
    /// ViewModel's output sink actually attaches to a new SwiftTerm view.
    private(set) var generation = 0
    private(set) var terminalViewModel: TerminalViewModel
    private(set) var gateViewModel: GateViewModel

    /// T-iOS-29 · read-only exposure for the coordinator's new-in-cwd flow
    /// (the host to reopen against — a controller is host-bound for life).
    @ObservationIgnored let host: HostRegistry.Host
    @ObservationIgnored private let environment: AppEnvironment
    @ObservationIgnored private let onPendingChanged: @MainActor (UUID, Bool) -> Void
    /// T-iOS-23 · OSC-title outlet (adopted sessionId + SANITIZED title) —
    /// re-attached to every rebuilt TerminalViewModel so a background→
    /// foreground rebuild never silently drops the list-title feed.
    @ObservationIgnored private let onTitleChanged: @MainActor (UUID, String) -> Void
    /// T-iOS-26 · fresh-spawn 变体：新会话的工作目录（`attach(null, cwd)`）。
    /// 仅在目标 sessionId 为 nil 时生效 —— 服务器对既有会话忽略 cwd。
    /// （T-iOS-29 起对 coordinator 只读可见：fresh spawn 未进列表轮询时，
    /// new-in-cwd 以它为 cwd 回退。）
    @ObservationIgnored let spawnCwd: String?
    /// T-iOS-26 · attach 后注入的首条输入（如 `claude\r`）。engine 的
    /// attach-first 队列语义保证它绝不先于 attach 出线；采纳既有会话 id 后
    /// 的重开（suspend→resume）不再注入。
    @ObservationIgnored private let bootstrapInput: String?
    /// 最近一次 open(+bootstrap) 提交任务 —— 测试屏障（ProjectOpenWiringTests）。
    @ObservationIgnored private(set) var openTask: Task<Void, Never>?
    @ObservationIgnored private var engine: SessionEngine
    @ObservationIgnored private var fanOut: EventFanOut<SessionEvent>
    @ObservationIgnored private var bridge: SessionActivityBridge
    /// What the next (re)open attaches to: the server-adopted id once known,
    /// else the list's requested id (nil = new session).
    @ObservationIgnored private var targetSessionId: UUID?
    @ObservationIgnored private var isSuspended = false
    @ObservationIgnored private var hasStarted = false
    @ObservationIgnored private var isTornDown = false

    /// Fan-out branch order (single place, no magic indices).
    private enum Branch {
        static let terminal = 0
        static let gate = 1
        static let activity = 2
        static let count = 3
    }

    init(
        host: HostRegistry.Host,
        sessionId: UUID?,
        environment: AppEnvironment,
        onPendingChanged: @escaping @MainActor (UUID, Bool) -> Void,
        onTitleChanged: @escaping @MainActor (UUID, String) -> Void = { _, _ in },
        spawnCwd: String? = nil,
        bootstrapInput: String? = nil
    ) {
        self.host = host
        self.environment = environment
        self.onPendingChanged = onPendingChanged
        self.onTitleChanged = onTitleChanged
        self.spawnCwd = spawnCwd
        self.bootstrapInput = bootstrapInput
        self.targetSessionId = sessionId
        let stack = Self.makeStack(
            host: host, environment: environment, onPendingChanged: onPendingChanged
        )
        engine = stack.engine
        fanOut = stack.fanOut
        terminalViewModel = stack.terminalViewModel
        gateViewModel = stack.gateViewModel
        bridge = stack.bridge
        terminalViewModel.onTitleChanged = onTitleChanged
    }

    // MARK: - Lifecycle entry points

    /// Kick off: consumers first, then the engine connect (events buffer
    /// unbounded, so this order is belt-and-braces, not a race fix).
    func start() {
        guard !hasStarted, !isTornDown else { return }
        hasStarted = true
        startConsumers()
        openEngineAtTarget()
    }

    /// scenePhase → `.background`: clean detach (PTY keeps running).
    func suspend() {
        guard hasStarted, !isSuspended, !isTornDown else { return }
        isSuspended = true
        rememberAdoptedSession()
        stopConsumers()
        let engine = engine
        Task { await engine.close() }
    }

    /// scenePhase → `.active`: rebuild-and-reopen if we suspended; with the
    /// engine still alive, notifyForegrounded (reconnect + size reclaim).
    func resumeIfNeeded() {
        guard !isTornDown else { return }
        guard isSuspended else {
            notifyForegroundedIfPossible()
            return
        }
        isSuspended = false
        rebuildStack()
        startConsumers()
        openEngineAtTarget()
    }

    /// (Re)open 当前目标：既有会话 → 裸 attach；fresh spawn（目标 id 为
    /// nil）→ 带 cwd attach + attach 后注入 bootstrap（T-iOS-26）。suspend
    /// 前已采纳的 id 走前一分支，bootstrap 不会重放。
    private func openEngineAtTarget() {
        let engine = engine
        let sessionId = targetSessionId
        let cwd = sessionId == nil ? spawnCwd : nil
        let bootstrap = sessionId == nil ? bootstrapInput : nil
        openTask = Task {
            await engine.open(sessionId: sessionId, cwd: cwd)
            if let bootstrap {
                await engine.send(.input(data: bootstrap))
            }
        }
    }

    /// Alive-engine `.active` hop: feed the engine the last VALID dims the
    /// terminal reported so it reconnects (if dropped during `.inactive`) and
    /// re-stamps the PTY size (latest-writer-wins reclaims full screen).
    private func notifyForegroundedIfPossible() {
        guard hasStarted, let dims = terminalViewModel.lastSentDims else { return }
        let engine = engine
        Task { await engine.notifyForegrounded(dims: dims.asTuple) }
    }

    /// Back navigation / screen dismissed: detach for good. Idempotent.
    func teardown() {
        guard !isTornDown else { return }
        isTornDown = true
        rememberAdoptedSession()
        stopConsumers()
        let engine = engine
        Task { await engine.close() }
    }

    // MARK: - Timeline drill-down (T-iOS-24, additive)

    /// Per-host timeline fetcher for the container's `TimelineSheet` — the
    /// same `APIClient.events` wrapper the engine's away-digest uses; the
    /// single derivation point stays `AppEnvironment.makeEventsSource`.
    var timelineEventsSource: @Sendable (UUID) async throws -> [TimelineEvent] {
        environment.makeEventsSource(endpoint: host.endpoint)
    }

    // MARK: - Stack assembly

    private struct Stack {
        let engine: SessionEngine
        let fanOut: EventFanOut<SessionEvent>
        let terminalViewModel: TerminalViewModel
        let gateViewModel: GateViewModel
        let bridge: SessionActivityBridge
    }

    private static func makeStack(
        host: HostRegistry.Host,
        environment: AppEnvironment,
        onPendingChanged: @escaping @MainActor (UUID, Bool) -> Void
    ) -> Stack {
        let engine = SessionEngine(
            transport: environment.termTransport,
            clock: ContinuousClock(),
            endpoint: host.endpoint,
            eventsSource: environment.makeEventsSource(endpoint: host.endpoint)
        )
        let fanOut = EventFanOut(source: engine.events, branchCount: Branch.count)
        return Stack(
            engine: engine,
            fanOut: fanOut,
            terminalViewModel: TerminalViewModel(
                engine: engine, events: fanOut.branches[Branch.terminal]
            ),
            gateViewModel: GateViewModel(
                engine: engine, events: fanOut.branches[Branch.gate],
                haptics: GateHaptics(), clock: ContinuousClock()
            ),
            bridge: SessionActivityBridge(
                events: fanOut.branches[Branch.activity], hostId: host.id,
                lastSessionStore: environment.lastSessionStore,
                onPendingChanged: onPendingChanged
            )
        )
    }

    private func rebuildStack() {
        fanOut.cancel()
        let stack = Self.makeStack(
            host: host, environment: environment, onPendingChanged: onPendingChanged
        )
        engine = stack.engine
        fanOut = stack.fanOut
        terminalViewModel = stack.terminalViewModel
        gateViewModel = stack.gateViewModel
        bridge = stack.bridge
        terminalViewModel.onTitleChanged = onTitleChanged // rebuilt VM re-wired
        generation += 1 // new SwiftUI identity → fresh SwiftTerm view
    }

    private func startConsumers() {
        terminalViewModel.start()
        gateViewModel.start()
        bridge.start()
    }

    private func stopConsumers() {
        terminalViewModel.stop()
        gateViewModel.stop()
        bridge.stop()
    }

    /// The reopen target: always prefer the server-adopted id (it may differ
    /// from what was requested — unknown UUIDs mint new sessions).
    private func rememberAdoptedSession() {
        targetSessionId = bridge.adoptedSessionId ?? targetSessionId
    }
}
