import GameController
import SessionCore
import SwiftTerm
import SwiftUI
import UIKit

/// T-iOS-11 · The terminal screen: SwiftTerm view + key bar + status banner.
///
/// Data flow (byte-shuttle preserved end to end):
/// - inbound: `SessionEvent.output` → `TerminalViewModel` sink → `feed(text:)`
///   (opaque ANSI/UTF-8 — the app NEVER parses terminal semantics);
/// - outbound: SwiftTerm delegate `send` → `viewModel.sendInput`,
///   `sizeChanged` → `viewModel.sendResize`; KeyBar taps and hardware
///   `UIKeyCommand`s go through `viewModel.send(key:)` — every label→bytes
///   lookup via `KeyByteMap`, bypassing SwiftTerm's text path so the soft
///   keyboard never pops (mirrors the web bar's ws.send bypass).
/// - IME: NO custom keydown/composition interception — SwiftTerm manages
///   composition itself (CLAUDE.md gotcha; plan §7 T-iOS-11).
///
/// Navigation/lifecycle wiring (engine open/close, scenePhase) lands in
/// T-iOS-15 — this screen only renders and routes.
struct TerminalScreen: View {
    let viewModel: TerminalViewModel
    /// T-iOS-29 · "在当前目录开新会话"：toolbar 常驻入口 + exit 横幅上的
    /// "开新会话"（同一动作 —— close→open fresh spawn，cwd 解析在
    /// `AppCoordinator.openNewSessionInCurrentCwd`）。nil = 两处入口都隐藏
    /// （预览/无 coordinator 的测试环境）。
    var onNewSessionInCwd: (@MainActor () -> Void)? = nil
    /// T-iPad-3 · 上下文菜单「结束会话」动作 —— wiring 侧路由到
    /// `APIClient.killSession`（带 Origin 的 G 端点）。nil = 无 kill 通道时
    /// 菜单不呈现该项（预览/未布线环境）。
    var onKillSession: (@MainActor () -> Void)? = nil

    /// T-iPad-3 · 硬件键盘在场标记（`GCKeyboard.coalesced != nil`），随
    /// 连接/断开通知更新，驱动 `KeyBarVisibility` 的自动默认。
    @State private var hasHardwareKeyboard = GCKeyboard.coalesced != nil
    /// T-iPad-3 · 用户对 KeyBar 的显式覆盖：nil = 跟随自动默认。
    @State private var keyBarUserOverride: Bool?

    /// 无障碍：减弱动态效果时，横幅进出塌成即时切换（DS.Motion.gated）。
    @Environment(\.accessibilityReduceMotion) private var reduceMotion

    private enum Copy {
        static let newSessionInCwd = "在当前目录开新会话"
        static let showKeyBar = "显示快捷键栏"
        static let hideKeyBar = "隐藏快捷键栏"
    }

    /// KeyBar 是否可见 —— 唯一判据经 `KeyBarVisibility` 纯谓词。
    private var isKeyBarVisible: Bool {
        KeyBarVisibility.isVisible(
            hardwareKeyboardPresent: hasHardwareKeyboard,
            userOverride: keyBarUserOverride
        )
    }

    var body: some View {
        TerminalHostView(
            viewModel: viewModel,
            keyBarVisible: isKeyBarVisible,
            onNewSessionInCwd: onNewSessionInCwd,
            onKillSession: onKillSession
        )
            .ignoresSafeArea(.container, edges: .bottom)
            .overlay(alignment: .top) {
                if let model = viewModel.bannerModel {
                    ReconnectBanner(model: model, onNewSession: onNewSessionInCwd)
                        .padding(.horizontal, DS.Space.md12)
                        .padding(.top, DS.Space.sm8)
                        .transition(.move(edge: .top).combined(with: .opacity))
                }
            }
            .animation(
                DS.Motion.gated(DS.Motion.base, reduceMotion: reduceMotion),
                value: viewModel.bannerModel
            )
            .toolbar {
                newSessionToolbarItem
                keyBarToggleToolbarItem
            }
            .onReceive(NotificationCenter.default.publisher(for: .GCKeyboardDidConnect)) { _ in
                hasHardwareKeyboard = true
            }
            .onReceive(NotificationCenter.default.publisher(for: .GCKeyboardDidDisconnect)) { _ in
                hasHardwareKeyboard = GCKeyboard.coalesced != nil
            }
            .onAppear { viewModel.start() }
    }

    /// T-iPad-3 · KeyBar 手动切换。仅在硬件键盘在场时出现 —— 无硬件键盘的
    /// iPhone 默认工具栏因此逐屏不变（零回归）；有硬件键盘时（自动隐藏后）
    /// 用户可一键取回/再隐。切换即写显式覆盖，压过自动默认。
    @ToolbarContentBuilder private var keyBarToggleToolbarItem: some ToolbarContent {
        if hasHardwareKeyboard {
            ToolbarItem(placement: .topBarTrailing) {
                Button {
                    keyBarUserOverride = !isKeyBarVisible
                } label: {
                    Label(
                        isKeyBarVisible ? Copy.hideKeyBar : Copy.showKeyBar,
                        systemImage: isKeyBarVisible
                            ? "keyboard.chevron.compact.down" : "keyboard"
                    )
                }
                .accessibilityIdentifier("terminal.keyBarToggleButton")
            }
        }
    }

    /// Mirrors web `tabs.ts newTab()` (M6): the + affordance opens a fresh
    /// session in the active session's cwd, if known.
    @ToolbarContentBuilder private var newSessionToolbarItem: some ToolbarContent {
        ToolbarItem(placement: .topBarTrailing) {
            if let onNewSessionInCwd {
                Button {
                    onNewSessionInCwd()
                } label: {
                    Label(Copy.newSessionInCwd, systemImage: "plus.rectangle.on.folder")
                }
                .accessibilityIdentifier("terminal.newInCwdButton")
            }
        }
    }
}

// MARK: - Terminal theme

/// Refined terminal theme (精致原生) matching the desktop: a fixed warm-dark
/// canvas (#100F0D bg / #ECE9E3 fg, web --bg/--text) so the terminal reads the
/// same on iOS and desktop regardless of app appearance, with caret/selection
/// in the one gold accent. Every value routes through `DS` (no literals); the
/// glyph font is Dynamic-Type-aware SF Mono so terminal text respects the
/// user's text-size choice at launch.
private enum TerminalTheme {
    @MainActor
    static func apply(to terminal: TerminalView) {
        terminal.font = UIFont.monospacedSystemFont(
            ofSize: UIFont.preferredFont(forTextStyle: .footnote).pointSize,
            weight: .regular
        )
        terminal.nativeBackgroundColor = UIColor(DS.Palette.terminalBackground)
        terminal.nativeForegroundColor = UIColor(DS.Palette.terminalForeground)
        terminal.caretColor = DS.Palette.accentUIColor()
        terminal.selectedTextBackgroundColor = DS.Palette.accentUIColor()
    }
}

// MARK: - SwiftTerm bridge

/// `UIViewRepresentable` around `SwiftTerm.TerminalView` (plan §3.5). The
/// KeyBar is installed as `inputAccessoryView`; hardware chords come from the
/// `KeyCommandTerminalView` subclass. Both route through the ViewModel.
private struct TerminalHostView: UIViewRepresentable {
    let viewModel: TerminalViewModel
    /// T-iPad-3 · KeyBar (`inputAccessoryView`) 可见性，由 `KeyBarVisibility`
    /// 谓词在 SwiftUI 侧算出；`updateUIView` 仅在变化时增量应用。
    let keyBarVisible: Bool
    var onNewSessionInCwd: (@MainActor () -> Void)? = nil
    var onKillSession: (@MainActor () -> Void)? = nil

    func makeCoordinator() -> Coordinator {
        Coordinator(viewModel: viewModel)
    }

    func makeUIView(context: Context) -> KeyCommandTerminalView {
        let terminal = KeyCommandTerminalView(frame: .zero)
        terminal.terminalDelegate = context.coordinator
        TerminalTheme.apply(to: terminal)

        let viewModel = viewModel
        terminal.onKeyCommand = { key in viewModel.send(key: key) }

        let keyBar = KeyBarView()
        keyBar.onKey = { key in viewModel.send(key: key) }
        terminal.installKeyBar(keyBar, visible: keyBarVisible)

        // T-iPad-3 · 指针右键/长按上下文菜单（仅 iPad 安装 —— iPhone 长按保持
        // SwiftTerm 选区手势）。动作复用既有通道，无新增网络路径。
        terminal.installPointerContextMenuIfSupported(
            onNewInCwd: onNewSessionInCwd,
            onKill: onKillSession
        )

        // Output sink: buffered replay flushes now, live bytes follow.
        // @MainActor-typed closure — feeding off the main actor cannot compile.
        viewModel.attachTerminalSink { [weak terminal] text in
            terminal?.feed(text: text)
        }
        return terminal
    }

    func updateUIView(_ uiView: KeyCommandTerminalView, context: Context) {
        // State-driven UI lives in SwiftUI (banner overlay); the terminal view
        // itself is driven by the sink/delegate. The only push is the KeyBar
        // visibility (hardware-keyboard aware / user toggle) — a no-op unless it
        // actually changed, so iPhone (always-visible) never reloads input views.
        uiView.setKeyBarVisible(keyBarVisible)
    }

    /// SwiftTerm's delegate is a pre-concurrency protocol; the conformance is
    /// `@preconcurrency` — SwiftTerm only calls it from the main thread (the
    /// view itself is `@MainActor`), which the runtime check enforces.
    @MainActor
    final class Coordinator: NSObject, @preconcurrency TerminalViewDelegate {
        private let viewModel: TerminalViewModel

        init(viewModel: TerminalViewModel) {
            self.viewModel = viewModel
        }

        /// User typed into SwiftTerm (soft/hardware keyboard, IME result):
        /// raw bytes, passed through verbatim (invariant #9).
        func send(source: TerminalView, data: ArraySlice<UInt8>) {
            viewModel.sendInput(String(decoding: data, as: UTF8.self))
        }

        /// Layout produced a new grid → server `resize` (SIGWINCH). This is
        /// also the latest-writer-wins size claim (v0.4 sizing model).
        func sizeChanged(source: TerminalView, newCols: Int, newRows: Int) {
            guard newCols > 0, newRows > 0 else { return } // pre-layout noise
            viewModel.sendResize(cols: newCols, rows: newRows)
        }

        /// Tapped link (OSC 8 / detected URL — mirrors web M2 WebLinksAddon).
        /// The link string is untrusted terminal output: http(s) only.
        func requestOpenLink(source: TerminalView, link: String, params: [String: String]) {
            guard let url = URL(string: link),
                  let scheme = url.scheme?.lowercased(),
                  scheme == "http" || scheme == "https"
            else { return }
            UIApplication.shared.open(url)
        }

        /// OSC 0/2 title (T-iOS-23): hostile input — the VM sanitizes
        /// (TitleSanitizer) before any UI/registry use, then the wiring
        /// surfaces it on the session-list row.
        func setTerminalTitle(source: TerminalView, title: String) {
            viewModel.setTerminalTitle(title)
        }

        // cwd surfaces in the session list via the server (T-iOS-13), not
        // from the local emulator — deliberate no-op.
        func hostCurrentDirectoryUpdate(source: TerminalView, directory: String?) {}
        func scrolled(source: TerminalView, position: Double) {}
        func rangeChanged(source: TerminalView, startY: Int, endY: Int) {}

        /// OSC 52 lets the HOST write the device clipboard silently — declined
        /// (server output is untrusted input; plan §4).
        func clipboardCopy(source: TerminalView, content: Data) {}
    }
}

/// `TerminalView` subclass adding hardware-keyboard `UIKeyCommand`s with the
/// SAME `KeyByteMap` mapping as the key bar (scope decision documented on
/// `HardwareKeyCommands`). Everything else — rendering, selection, IME —
/// is stock SwiftTerm.
final class KeyCommandTerminalView: TerminalView {
    /// Chord outlet; the screen routes it to `TerminalViewModel.send(key:)`.
    var onKeyCommand: (@MainActor (KeyByteMap.Key) -> Void)?

    /// Retained KeyBar so visibility can toggle it in/out of `inputAccessoryView`
    /// (T-iPad-3). The last-applied value avoids reloading input views when the
    /// visibility did not change (iPhone stays byte-identical).
    private var keyBar: KeyBarView?
    private var appliedKeyBarVisible = true
    /// Retained so the interaction's delegate outlives menu presentation.
    private var contextMenuDelegate: TerminalContextMenuInteractionDelegate?

    override var keyCommands: [UIKeyCommand]? {
        (super.keyCommands ?? [])
            + HardwareKeyCommands.build(action: #selector(runHardwareKeyCommand(_:)))
    }

    @objc private func runHardwareKeyCommand(_ sender: UIKeyCommand) {
        guard let key = HardwareKeyCommands.key(matching: sender) else { return }
        onKeyCommand?(key)
    }

    // MARK: - KeyBar install / visibility (T-iPad-3)

    /// Install the KeyBar as `inputAccessoryView`, honoring the initial
    /// visibility (hidden when a hardware keyboard makes it redundant).
    func installKeyBar(_ bar: KeyBarView, visible: Bool) {
        keyBar = bar
        appliedKeyBarVisible = visible
        inputAccessoryView = visible ? bar : nil
    }

    /// Apply a visibility change; a no-op when unchanged so no needless
    /// `reloadInputViews()` (the KeyByteMap routing on `keyBar.onKey` is
    /// untouched — the same bar is only detached/re-attached).
    func setKeyBarVisible(_ visible: Bool) {
        guard visible != appliedKeyBarVisible else { return }
        appliedKeyBarVisible = visible
        inputAccessoryView = visible ? keyBar : nil
        reloadInputViews()
    }

    // MARK: - Pointer context menu (T-iPad-3, iPad only)

    /// Install the secondary-click / long-press context menu — iPad only, so
    /// iPhone long-press keeps SwiftTerm's native selection gesture (zero
    /// regression). The delegate builds a fresh model per presentation so the
    /// copy item reflects the live selection.
    func installPointerContextMenuIfSupported(
        onNewInCwd: (@MainActor () -> Void)?,
        onKill: (@MainActor () -> Void)?
    ) {
        // `UIDevice.current.userInterfaceIdiom` (not `traitCollection`, which can
        // be `.unspecified` before the view joins a window at makeUIView time).
        guard TerminalContextMenu.isPointerMenuEnabled(
            idiom: UIDevice.current.userInterfaceIdiom
        ) else { return }
        let delegate = TerminalContextMenuInteractionDelegate { [weak self] in
            TerminalContextMenuModel(
                onNewInCwd: onNewInCwd,
                onKill: onKill,
                onCopySelection: { [weak self] in self?.copySelectionToPasteboard() },
                hasSelection: { [weak self] in self?.hasActiveSelection ?? false }
            )
        }
        contextMenuDelegate = delegate
        addInteraction(UIContextMenuInteraction(delegate: delegate))
    }

    /// Whether a selection exists — reuses SwiftTerm's own `copy` eligibility
    /// (`canPerformAction` returns `selection.active`); pure read, no bytes.
    var hasActiveSelection: Bool {
        canPerformAction(#selector(UIResponderStandardEditActions.copy(_:)), withSender: nil)
    }

    /// Copy the current selection via SwiftTerm's own `copy(_:)` (selection →
    /// `UIPasteboard`). Pure UI: it never writes to the PTY, so the byte stream
    /// is untouched (invariant preserved — same as pointer hover highlight).
    func copySelectionToPasteboard() {
        copy(nil)
    }
}
