import SwiftUI
import WireProtocol

/// T-iOS-24 · Full activity-timeline drill-down, presented as a sheet from the
/// away-digest「展开」affordance (TerminalContainerView wiring).
///
/// Mirrors the web timeline panel (public/timeline.ts): rows are
/// "HH:MM · icon · label", newest-first, capped at
/// `TimelineViewModel.maxEvents`. `label` is SERVER text (untrusted display
/// input) — rendered via `Text(verbatim:)` only, `lineLimit(1)` (same row
/// discipline as AwayDigestView; web sets it via textContent, SEC-H6).
struct TimelineSheet: View {
    let viewModel: TimelineViewModel

    var body: some View {
        NavigationStack {
            content
                .navigationTitle(TimelineCopy.title)
                .navigationBarTitleDisplayMode(.inline)
        }
        .presentationDetents([.medium, .large])
        .task { await viewModel.load() } // fresh VM per presentation → one fetch
    }

    // MARK: - Phase switch

    @ViewBuilder private var content: some View {
        switch viewModel.phase {
        case .loading:
            ProgressView()
                .frame(maxWidth: .infinity, maxHeight: .infinity)
        case .empty:
            emptyState
        case .failed:
            failedState
        case .loaded(let events):
            eventList(events)
        }
    }

    /// Server replied `[]` — covers both "no activity yet" and timeline
    /// capture disabled host-side (src/server.ts:589-591). An empty timeline
    /// is a normal state, NEVER an error (task spec).
    private var emptyState: some View {
        ContentUnavailableView(
            TimelineCopy.emptyTitle,
            systemImage: "clock.badge.questionmark",
            description: Text(TimelineCopy.emptyDetail)
        )
    }

    /// Explicit, retryable error state — the fetch can fail transiently
    /// (LAN hop, host asleep); retry re-runs the same load path.
    private var failedState: some View {
        ContentUnavailableView {
            Label(TimelineCopy.loadFailed, systemImage: "wifi.exclamationmark")
        } description: {
            Text(TimelineCopy.loadFailedDetail)
        } actions: {
            Button(TimelineCopy.retry) {
                Task { await viewModel.load() }
            }
            .buttonStyle(.borderedProminent)
            .tint(DS.Palette.accent)
        }
    }

    // MARK: - Rows (newest-first, already ordered by the VM)

    private func eventList(_ events: [TimelineEvent]) -> some View {
        List {
            // Offset identity (not `at`): the server can ingest several
            // events in the same millisecond, and rows are static.
            ForEach(Array(events.enumerated()), id: \.offset) { _, event in
                row(event)
            }
        }
        .listStyle(.plain)
    }

    private func row(_ event: TimelineEvent) -> some View {
        HStack(spacing: DS.Space.md12) {
            // Timestamp — mono/tabular so HH:mm columns line up (direction).
            Text(TimelineRowFormat.timeLabel(atMs: event.at))
                .font(DS.Typography.mono(.caption))
                .foregroundStyle(DS.Palette.textSecondary)
            // class → glyph + semantic color (frozen mapping; unit-tested).
            Text(verbatim: TimelineClassStyle.glyph(for: event.class))
                .font(DS.Typography.callout)
                .foregroundStyle(TimelineClassStyle.color(for: event.class))
                .frame(width: DS.Space.xxl24)
            // Server-derived phrase — untrusted: verbatim (never
            // LocalizedStringKey/Markdown) + hard single-line truncation.
            Text(verbatim: event.label)
                .font(DS.Typography.callout)
                .foregroundStyle(DS.Palette.textPrimary)
                .lineLimit(1)
            Spacer(minLength: 0)
        }
        .padding(.vertical, DS.Space.xs2)
        .accessibilityElement(children: .combine)
    }
}

// MARK: - class → icon / color mapping

/// Glyphs mirror web `timelineIcon` verbatim (public/timeline.ts:88-96).
/// Colors are semantic — the web CSS defines NO tl-icon-* colors, so iOS
/// aligns with SessionListScreen's status color convention (waiting=orange,
/// stuck=red) and extends it. Total functions: the server is untrusted, so an
/// unknown class degrades to a neutral glyph/color instead of trapping (even
/// though `APIClient.events` already drops unknown classes — defense in depth).
enum TimelineClassStyle {
    static let fallbackGlyph = "•"

    static func glyph(for cls: String) -> String {
        switch cls {
        case "tool": return "🔧"
        case "waiting": return "⏳"
        case "done": return "✓"
        case "stuck": return "⚠"
        case "user": return "💬"
        default: return fallbackGlyph
        }
    }

    static func color(for cls: String) -> Color {
        // All from the frozen design system — no raw SwiftUI colors (UX finding).
        switch cls {
        case "tool": return DS.Palette.timelineTool
        case "waiting": return DS.Palette.statusWaiting
        case "done": return DS.Palette.statusWorking
        case "stuck": return DS.Palette.statusStuck
        case "user": return DS.Palette.timelineUser
        default: return DS.Palette.textSecondary
        }
    }
}

// MARK: - Row time formatting

/// Mirrors web `formatHHMM` (public/timeline.ts:101-106): 24-hour wall-clock
/// "HH:mm". Fixed POSIX locale so a 12-hour user locale can't leak AM/PM into
/// the fixed format; timezone injectable for deterministic tests.
enum TimelineRowFormat {
    private static let millisecondsPerSecond = 1_000.0

    static func timeLabel(atMs: Int, timeZone: TimeZone = .current) -> String {
        let formatter = DateFormatter()
        formatter.locale = Locale(identifier: "en_US_POSIX")
        formatter.timeZone = timeZone
        formatter.dateFormat = "HH:mm"
        let date = Date(timeIntervalSince1970: Double(atMs) / millisecondsPerSecond)
        return formatter.string(from: date)
    }
}

// MARK: - 用户可见文案（中文具名常量，plan 工程标准）

enum TimelineCopy {
    static let title = "活动时间线"
    static let emptyTitle = "暂无活动"
    static let emptyDetail = "会话还没有可展示的事件；主机关闭时间线（TIMELINE_ENABLED=0）时也会显示为空。"
    static let loadFailed = "时间线加载失败"
    static let loadFailedDetail = "无法从主机获取活动时间线，请检查连接后重试。"
    static let retry = "重试"
}
