// MainTerminalView.swift
// Wires SessionConnection → TerminalViewController + ModifierBar
// MVP: auto-picks the first session (or creates one named "pi")

import Combine
import SwiftUI

@MainActor
struct MainTerminalView: View {
    let credential: SidecarCredential

    @State private var terminalVC = TerminalViewController()
    @State private var connection: SessionConnection? = nil
    @State private var statusText = "Connecting…"
    @State private var currentPiState: PiState? = nil
    @State private var sessionName = ""
    @State private var showSwitcher = false
    @State private var showSettings = false                   // T-2.11
    @State private var activeSessionId: String? = nil        // T-2.6
    @State private var lastCapturedSeq: UInt64? = nil        // T-2.10: seq captured on background
    @State private var cancellables = Set<AnyCancellable>()
    @StateObject private var registry = SessionRegistry()    // T-2.6
    @EnvironmentObject var appState: AppState

    /// True when running under XCUITest. Skips the live SwiftTerm view + WS
    /// connection, which otherwise keep the app non-idle and cause every
    /// XCUITest interaction to block for ~120 s.
    ///
    /// Always `false` in Release builds (test hooks are `#if DEBUG` only).
    private var isUITest: Bool {
        #if DEBUG
        return ProcessInfo.processInfo.arguments.contains("--uitest")
        #else
        return false
        #endif
    }

    /// True when the stub-connection mode is active (uitest + background lifecycle).
    ///
    /// Always `false` in Release builds.
    private var isUITestStub: Bool {
        #if DEBUG
        let args = ProcessInfo.processInfo.arguments
        return args.contains("--uitest") && args.contains("--uitest-with-stub-connection")
        #else
        return false
        #endif
    }

    var body: some View {
        VStack(spacing: 0) {
            // ── Status bar ──────────────────────────────────────────
            StatusBar(
                sessionName: sessionName,
                connectionStatus: statusText,
                piState: $currentPiState,
                onSwitcher: { showSwitcher = true },
                onUnpair: { appState.unpair() },
                onSettings: { showSettings = true }           // T-2.11
            )

            // ── Terminal ────────────────────────────────────────────
            if isUITest {
                Color.black
                    .overlay(Text("UITest mode").foregroundStyle(.white).font(.caption))
                    .accessibilityIdentifier("terminal.placeholder")
            } else {
                TerminalViewRepresentable(controller: terminalVC)
                    .ignoresSafeArea(edges: .bottom)
            }

            Divider()

            // ── Modifier bar ────────────────────────────────────────
            ModifierBar { frame in
                Task {
                    try? await connection?.send(frame)
                }
            }
            .padding(.vertical, 4)
            .background(Color(uiColor: .secondarySystemBackground))
        }
        .task { await initialBootstrap() }
        .task { await registry.refresh(credential: credential) }    // T-2.6
        .onReceive(appState.lifecycleTransitions) { isBackground in  // T-2.10
            Task { @MainActor in
                if isBackground {
                    guard let conn = connection else { return }
                    if isUITestStub {
                        lastCapturedSeq = 1  // stub sentinel: ensures "Reconnecting…"
                    } else {
                        lastCapturedSeq = conn.scrollback.sizeBytes > 0
                            ? ResumeCursor().lastSeq(for: conn.id)
                            : nil
                    }
                    await conn.suspend()
                } else {
                    guard !appState.isLocked, let conn = connection else { return }
                    await conn.resume(from: lastCapturedSeq)
                }
            }
        }
        .onChange(of: activeSessionId) { _, newId in
            guard let newId else { return }
            // UI-test mode: no terminal view, no WS — just update the label.
            if isUITest { sessionName = newId; return }
            // Avoid reconnect storm if id already matches the current connection.
            if connection?.id == newId { return }
            Task {
                if let oldConn = connection { await oldConn.suspend() }
                cancellables.removeAll()
                connection = nil
                currentPiState = nil
                sessionName = ""
                terminalVC.feed(data: Data("\u{1B}[H\u{1B}[2J".utf8))
                await bootstrap(sessionId: newId)
            }
        }
        .sheet(isPresented: $showSwitcher) {                        // T-2.6
            SessionSwitcher(registry: registry, credential: credential) { session in
                activeSessionId = session.id
            }
        }
        .sheet(isPresented: $showSettings) {                        // T-2.11
            SettingsView(credential: credential)
                .environmentObject(appState)
        }
    }

    // MARK: - Bootstrap

    private func initialBootstrap() async {
#if DEBUG
        // UI-test mode: skip the live WebSocket. SwiftTerm's constant redraw
        // on incoming frames prevents XCUITest's idle wait from completing,
        // making every .tap() block for ~120 s. Status bar, modifier bar, and
        // both sheets still render normally.
        if ProcessInfo.processInfo.arguments.contains("--uitest") {
            if isUITestStub {
                await bootstrapStubConnection()
            } else {
                statusText = "● uitest"
                sessionName = "uitest"
            }
            return
        }
#endif
        statusText = "Looking for sessions…"
        do {
            let sessionId = try await resolveSession()
            await bootstrap(sessionId: sessionId)
        } catch {
            statusText = "Error: \(error.localizedDescription)"
        }
    }

    private func bootstrap(sessionId: String) async {
        // Keep activeSessionId in sync. The .onChange handler guards against
        // re-entry via `connection?.id == newId` check (connection is still nil here
        // on first call, but we set it below before any further state change).
        activeSessionId = sessionId
        statusText = "Connecting to \(sessionId)…"
        let conn = SessionConnection(id: sessionId, credential: credential)

            // Wire live ANSI stream → terminal
            conn.stream
                .receive(on: DispatchQueue.main)
                .sink { [terminalVC] data in
                    terminalVC.feed(data: data)
                }
                .store(in: &cancellables)

            // Wire snapshots → terminal (already contains ESC[H + ESC[2J prefix)
            conn.snapshots
                .receive(on: DispatchQueue.main)
                .sink { [terminalVC] data in
                    terminalVC.feed(data: data)
                }
                .store(in: &cancellables)

            // Wire connection state → status text
            // T-2.10: show "Reconnecting…" if isStreamFrozen (lastSeq was non-nil)
            conn.$connectionState
                .receive(on: DispatchQueue.main)
                .sink { [weak conn] state in
                    switch state {
                    case .connected:    statusText = "● \(sessionId)"
                    case .connecting:   statusText = conn?.isStreamFrozen == true ? "Reconnecting…" : "Connecting…"
                    case .disconnected: statusText = "Disconnected"
                    }
                }
                .store(in: &cancellables)

            // Wire resize callback — fires on layout + font changes.
            terminalVC.onResize = { [weak conn] cols, rows in
                guard let conn else { return }
                Task { try? await conn.send(.resize(cols: cols, rows: rows)) }
            }

            // Wire keyboard input from SwiftTerm → sidecar.
            terminalVC.onInput = { [weak conn] data in
                guard let conn,
                      let text = String(data: data, encoding: .utf8) else { return }
                Task { try? await conn.send(.keys(data: text)) }
            }

            // On first connection:
            //   1. Clear SwiftTerm immediately (removes stale content)
            //   2. Send resize so tmux + shell know the real dimensions
            //   3. Wait for SIGWINCH to propagate and shell to redraw
            //   4. Snapshot the now-stable screen state
            conn.$connectionState
                .filter { $0 == .connected }
                .first()
                .receive(on: DispatchQueue.main)
                .sink { [weak conn, terminalVC] _ in
                    // Clear immediately — don't show stale/mismatched content.
                    terminalVC.feed(data: Data("\u{1B}[H\u{1B}[2J".utf8))

                    Task { @MainActor in
                        let (cols, rows) = terminalVC.terminalSize
                        if cols > 0 && rows > 0 {
                            try? await conn?.send(.resize(cols: cols, rows: rows))
                        }
                        // 600 ms: SIGWINCH → fish/bash redraws → tmux pane settles.
                        // SSH sessions need extra round-trip time.
                        try? await Task.sleep(nanoseconds: 600_000_000)
                        try? await conn?.send(.snapshotRequest)
                    }
                }
                .store(in: &cancellables)

            // Wire stateEvents → currentPiState
            conn.stateEvents
                .compactMap { event -> PiState? in
                    if case .state(let s, _, _) = event { return s } else { return nil }
                }
                .receive(on: DispatchQueue.main)
                .sink { state in currentPiState = state }
                .store(in: &cancellables)

            // Wire stateEvents → sessionName
            conn.stateEvents
                .compactMap { event -> String? in
                    if case .sessionMeta(let name, _, _) = event { return name } else { return nil }
                }
                .receive(on: DispatchQueue.main)
                .sink { name in sessionName = name }
                .store(in: &cancellables)

            connection = conn

            let lastSeq: UInt64? = conn.scrollback.sizeBytes > 0
                ? ResumeCursor().lastSeq(for: sessionId)
                : nil
            await conn.resume(from: lastSeq)
    }

    // MARK: - Stub connection (uitest-with-stub-connection)

#if DEBUG
    /// Creates an in-process stub SessionConnection for UI tests that need
    /// to exercise the background/foreground lifecycle without a real sidecar.
    ///
    /// Gated with `#if DEBUG` — never compiled into Release builds.
    private func bootstrapStubConnection() async {
        let stubId = "stub"
        sessionName = stubId
        activeSessionId = stubId
        let conn = SessionConnection(id: stubId, credential: credential)
        conn.stubMode = true

        conn.$connectionState
            .receive(on: DispatchQueue.main)
            .sink { [weak conn] state in
                switch state {
                case .connected:    statusText = "● \(stubId)"
                case .connecting:   statusText = conn?.isStreamFrozen == true ? "Reconnecting…" : "Connecting…"
                case .disconnected: statusText = "Disconnected"
                }
            }
            .store(in: &cancellables)

        connection = conn
        await conn.resume(from: nil)
    }
#endif

    // MARK: - Session resolution

    /// Returns the first existing session id, or creates one named "pi".
    private func resolveSession() async throws -> String {
        let url = URL(string: "http://\(credential.host):\(credential.port)/sessions")!
        var req = URLRequest(url: url)
        req.setValue("Bearer \(credential.bearerToken)", forHTTPHeaderField: "Authorization")

        let (data, _) = try await URLSession.shared.data(for: req)

        struct SessionItem: Decodable { let id: String }
        let sessions = (try? JSONDecoder().decode([SessionItem].self, from: data)) ?? []

        if let first = sessions.first {
            return first.id
        }

        // No sessions — create one
        var createReq = URLRequest(url: url)
        createReq.httpMethod = "POST"
        createReq.setValue("application/json", forHTTPHeaderField: "Content-Type")
        createReq.setValue("Bearer \(credential.bearerToken)", forHTTPHeaderField: "Authorization")
        createReq.httpBody = try? JSONEncoder().encode(["name": "pi"])

        let (createData, _) = try await URLSession.shared.data(for: createReq)
        struct Created: Decodable { let id: String }
        if let created = try? JSONDecoder().decode(Created.self, from: createData) {
            return created.id
        }

        throw URLError(.cannotParseResponse)
    }
}
