import HostRegistry
import SwiftUI
import WireProtocol

/// T-iOS-13 · Session list screen (merged chooser + dashboard). Pure
/// presentation over `SessionListViewModel` — polling cadence, badge priority,
/// staleness, optimistic kill and the navigation signal are all VM logic,
/// unit-tested in `SessionListViewModelTests`.
///
/// UX polish (G1-list): the row is restructured for the 5-second glance —
/// prominent title, a `StatusBadge` (semantic color + distinct SF Symbol, never
/// color alone), a monospaced meta line, and telemetry as proper
/// `TelemetryChip`s. Rows are DS `Card`s; active/exited sessions are split under
/// `SectionHeader`s (presentation-only — the VM already groups exited last).
/// Every color/spacing/radius/font/motion comes from `DS.*`.
///
/// The T-iOS-15 wiring provides `onOpen` (push `TerminalScreen`, open with
/// `request.sessionId` — nil = new session) and `onAddHost` (present the
/// pairing sheet; call `viewModel.reloadHosts()` when it completes).
struct SessionListScreen: View {
    var viewModel: SessionListViewModel
    /// Navigation hook: fired once per `OpenRequest` (unique id per tap).
    var onOpen: (SessionListViewModel.OpenRequest) -> Void = { _ in }
    /// Host-switch header hook: "添加主机" entry (pairing sheet, T-iOS-15).
    var onAddHost: () -> Void = {}
    /// C-iOS-3 (HIGH reachability fix) · "设备证书" entry — presents
    /// `ClientCertScreen` (import / rotate the mTLS device cert). The device
    /// cert is a global concern, but the toolbar host-menu is the app's only
    /// settings-like surface and is present in every empty state, so this is the
    /// nav idiom that makes the orphaned screen reachable.
    var onDeviceCert: () -> Void = {}
    /// B3 · "自动获取证书" entry — presents `EnrollmentScreen` (the zero-`.p12`
    /// enrollment flow: login → Secure-Enclave key + CSR → device cert). Same
    /// host-menu surface as `onDeviceCert`; that screen imports a `.p12`, this
    /// one obtains a hardware-bound cert with no file at all.
    var onEnroll: () -> Void = {}
    /// T-iOS-28 (additive slot) · shared thumbnail pipeline: one cache + one
    /// render-concurrency gate across ALL rows (scrolling must never spawn
    /// unbounded offscreen terminals). `@State` keeps it stable across body
    /// rebuilds; `live()` is cheap (the URLSession is a static shared).
    @State private var thumbnails = SessionThumbnailPipeline.live()

    var body: some View {
        content
            // Accent is not injected app-wide; scope it here so native chrome
            // (nav bar, bordered controls) picks up the DS indigo. Presentation
            // only — no behavior change.
            .tint(DS.Palette.accent)
            .navigationTitle(ScreenCopy.title)
            .toolbar { hostMenu }
            .onAppear { viewModel.appeared() }
            .onDisappear { viewModel.disappeared() }
            .onChange(of: viewModel.openRequest) { _, request in
                guard let request else { return }
                onOpen(request)
            }
    }

    @ViewBuilder private var content: some View {
        switch viewModel.emptyState {
        case .notPaired:
            notPairedView
        case .noSessions:
            noSessionsView
        case nil:
            sessionList
        }
    }

    // MARK: - List

    private var sessionList: some View {
        List {
            if let message = viewModel.fetchErrorMessage {
                errorRow(message)
            }
            if let message = viewModel.killErrorMessage {
                errorRow(message)
            }

            newSessionRow

            if !activeRows.isEmpty {
                Section {
                    ForEach(activeRows) { row in sessionRow(row) }
                } header: {
                    SectionHeader(title: ScreenCopy.activeSection)
                }
            }
            if !exitedRows.isEmpty {
                Section {
                    ForEach(exitedRows) { row in sessionRow(row) }
                } header: {
                    SectionHeader(title: ScreenCopy.exitedSection)
                }
            }
        }
        .listStyle(.plain)
        .refreshable { await viewModel.refresh() }
    }

    /// Split the VM's (already exited-last) rows into the two visual groups.
    /// Presentation only — no ordering/logic decision lives here.
    private var activeRows: [SessionListViewModel.SessionRow] {
        viewModel.rows.filter { !$0.info.exited }
    }

    private var exitedRows: [SessionListViewModel.SessionRow] {
        viewModel.rows.filter { $0.info.exited }
    }

    /// Inviting primary entry — accent-tinted card row.
    private var newSessionRow: some View {
        Button {
            viewModel.requestNewSession()
        } label: {
            NewSessionRow()
        }
        .buttonStyle(.plain)
        .accessibilityIdentifier("sessions.newButton")
        .listRowSeparator(.hidden)
        .listRowInsets(rowInsets(vertical: DS.Space.sm8))
        .listRowBackground(Color.clear)
    }

    private func sessionRow(_ row: SessionListViewModel.SessionRow) -> some View {
        Button {
            viewModel.openSession(id: row.id)
        } label: {
            SessionRowView(row: row, thumbnail: thumbnailSlot(for: row))
        }
        .buttonStyle(.plain)
        .listRowSeparator(.hidden)
        .listRowInsets(rowInsets(vertical: DS.Space.xs4))
        .listRowBackground(Color.clear)
        .swipeActions(edge: .trailing, allowsFullSwipe: true) {
            Button(role: .destructive) {
                Task { await viewModel.kill(sessionId: row.id) }
            } label: {
                Label(ScreenCopy.kill, systemImage: "xmark.circle.fill")
            }
        }
    }

    /// Carded-list row inset: full-bleed gutter (`lg16`) + a small vertical gap
    /// so adjacent cards breathe.
    private func rowInsets(vertical: CGFloat) -> EdgeInsets {
        EdgeInsets(
            top: vertical, leading: DS.Space.lg16,
            bottom: vertical, trailing: DS.Space.lg16
        )
    }

    /// T-iOS-28 · build one row's thumbnail slot. No paired host (defensive —
    /// rows imply a host) → no slot; the request key carries `lastOutputAt`
    /// so unchanged sessions render exactly once (pipeline cache).
    private func thumbnailSlot(
        for row: SessionListViewModel.SessionRow
    ) -> SessionThumbnailView? {
        guard let endpoint = viewModel.activeHost?.endpoint else { return nil }
        return SessionThumbnailView(
            request: SessionThumbnailRequest(
                endpoint: endpoint,
                sessionId: row.id,
                lastOutputAt: row.info.lastOutputAt
            ),
            pipeline: thumbnails
        )
    }

    private func errorRow(_ message: String) -> some View {
        Label(message, systemImage: "exclamationmark.triangle.fill")
            .font(DS.Typography.caption)
            .foregroundStyle(DS.Palette.statusStuck)
            .listRowSeparator(.hidden)
            .listRowInsets(rowInsets(vertical: DS.Space.xs4))
            .listRowBackground(Color.clear)
    }

    // MARK: - Empty states

    private var notPairedView: some View {
        ContentUnavailableView {
            Label(ScreenCopy.notPairedTitle, systemImage: "personalhotspot")
        } description: {
            Text(ScreenCopy.notPairedHint)
        } actions: {
            Button(ScreenCopy.addHost) { onAddHost() }
                .buttonStyle(DSButtonStyle(kind: .primary))
        }
    }

    private var noSessionsView: some View {
        ContentUnavailableView {
            Label(ScreenCopy.noSessionsTitle, systemImage: "terminal")
        } description: {
            Text(ScreenCopy.noSessionsHint)
        } actions: {
            Button(ScreenCopy.newSession) { viewModel.requestNewSession() }
                .buttonStyle(DSButtonStyle(kind: .primary))
                .accessibilityIdentifier("sessions.newButton")
        }
    }

    // MARK: - Host-switch header (multi-host from HostStore)

    private var hostMenu: some ToolbarContent {
        ToolbarItem(placement: .topBarTrailing) {
            Menu {
                ForEach(viewModel.hosts) { host in
                    Button {
                        Task { await viewModel.selectHost(id: host.id) }
                    } label: {
                        if host.id == viewModel.activeHost?.id {
                            Label(host.name, systemImage: "checkmark")
                        } else {
                            Text(host.name)
                        }
                    }
                }
                Divider()
                Button {
                    onAddHost()
                } label: {
                    Label(ScreenCopy.addHost, systemImage: "plus")
                }
                Button {
                    onEnroll()
                } label: {
                    Label(ScreenCopy.enroll, systemImage: "checkmark.shield")
                }
                .accessibilityIdentifier("sessions.enroll")
                Button {
                    onDeviceCert()
                } label: {
                    Label(ScreenCopy.deviceCert, systemImage: "lock.shield")
                }
                .accessibilityIdentifier("sessions.deviceCert")
            } label: {
                Label(
                    viewModel.activeHost?.name ?? ScreenCopy.hostMenuFallback,
                    systemImage: "desktopcomputer"
                )
            }
            .accessibilityIdentifier("sessions.hostMenu")
        }
    }
}

// MARK: - Row

/// Inviting "新建会话" card — accent icon + accent title on a DS `Card`.
private struct NewSessionRow: View {
    var body: some View {
        Card(padding: DS.Space.md12) {
            HStack(spacing: DS.Space.md12) {
                Image(systemName: "plus.circle.fill")
                    .font(DS.Typography.title)
                    .foregroundStyle(DS.Palette.accent)
                Text(ScreenCopy.newSession)
                    .font(DS.Typography.headline)
                    .foregroundStyle(DS.Palette.accent)
                Spacer(minLength: 0)
            }
        }
    }
}

/// One session row: `StatusBadge` · title · mono meta · telemetry chips ·
/// optional live thumbnail — laid out inside a DS `Card`.
private struct SessionRowView: View {
    let row: SessionListViewModel.SessionRow
    /// T-iOS-28 (additive) · trailing live-preview thumbnail; nil = no slot
    /// (no host / preview-less contexts) and the row lays out as before.
    var thumbnail: SessionThumbnailView?

    var body: some View {
        Card(padding: DS.Space.md12) {
            HStack(alignment: .top, spacing: DS.Space.md12) {
                // ⚠ pending / exited outrank the raw status (VM decides pending;
                // exited is a row fact) — resolved to a single DisplayStatus so
                // the badge shows color + distinct shape + Chinese VoiceOver.
                StatusBadge(status: displayStatus)
                    .padding(.top, DS.Space.xs2)
                VStack(alignment: .leading, spacing: DS.Space.xs4) {
                    titleLine
                    Text(meta)
                        .dsMetaText()
                        .lineLimit(1)
                    if let telemetry = row.telemetry, !telemetry.isEmpty {
                        TelemetryChips(model: telemetry)
                    }
                }
                if let thumbnail {
                    Spacer(minLength: DS.Space.sm8)
                    thumbnail
                }
            }
        }
        .opacity(row.info.exited ? DS.Opacity.exited : 1)
        .accessibilityElement(children: .combine)
    }

    private var titleLine: some View {
        HStack(spacing: DS.Space.sm8) {
            // T-iOS-23: OSC titles are attacker-controlled — already sanitized
            // in the VM, rendered verbatim (no Markdown / LocalizedStringKey
            // interpretation), one line only.
            Text(verbatim: title)
                .font(DS.Typography.headline)
                .foregroundStyle(DS.Palette.textPrimary)
                .lineLimit(1)
            if row.isUnread {
                unreadDot
            }
            Spacer(minLength: 0)
        }
    }

    /// Unread dot (T-iOS-23): output newer than the local last-seen watermark.
    /// Accent (indigo) continues the web selection color — distinct from the
    /// gray/green status shapes.
    private var unreadDot: some View {
        Circle()
            .fill(DS.Palette.accent)
            .frame(width: DS.Space.sm8, height: DS.Space.sm8)
            .accessibilityLabel(ScreenCopy.unreadLabel)
    }

    /// Resolve the row's status to one `DisplayStatus`. Exited is a terminal
    /// fact (top precedence); otherwise the VM's badge priority stands
    /// (pending ⚠ outranks the live status). No VM logic re-implemented here.
    private var displayStatus: DisplayStatus {
        if row.info.exited { return .exited }
        switch row.indicator {
        case .pendingApproval: return .pendingApproval
        case .status(let status): return DisplayStatus(status)
        }
    }

    /// Sanitized OSC title first (T-iOS-23, mirrors web autoTitle precedence,
    /// public/tabs.ts:570), else the cwd-derived name. Both are server-supplied
    /// display text (untrusted → verbatim Text only).
    private var title: String {
        if let osc = row.title, !osc.isEmpty { return osc }
        guard let cwd = row.info.cwd, !cwd.isEmpty else { return ScreenCopy.unknownDirectory }
        return URL(fileURLWithPath: cwd).lastPathComponent
    }

    /// Client count + `cols×rows`, rendered mono-tabular via `.dsMetaText()`.
    /// The exited state is conveyed by the badge + section + dimming, so it is
    /// not duplicated here.
    private var meta: String {
        [
            ScreenCopy.clientCount(row.info.clientCount),
            "\(row.info.cols)×\(row.info.rows)",
        ].joined(separator: " · ")
    }
}

// MARK: - Screen copy

private enum ScreenCopy {
    static let title = "会话"
    static let newSession = "新建会话"
    static let kill = "结束"
    static let addHost = "配对新主机"
    static let deviceCert = "设备证书"
    static let enroll = "自动获取证书"
    static let hostMenuFallback = "主机"
    static let notPairedTitle = "还没有配对的主机"
    static let notPairedHint = "先配对你电脑上的 web-terminal（扫码或手输地址），会话会出现在这里。"
    static let noSessionsTitle = "主机上没有运行中的会话"
    static let noSessionsHint = "新建一个会话开始工作；关掉 App 后会话仍会在主机上继续跑。"
    static let unknownDirectory = "未知目录"
    static let unreadLabel = "有新输出"
    static let activeSection = "运行中"
    static let exitedSection = "已结束"

    static func clientCount(_ count: Int) -> String {
        "\(count) 台设备在看"
    }
}
