import SwiftUI
import WireProtocol

/// T-iOS-27 · Read-only diff viewer（镜像 web public/diff.ts 的 render-only
/// 半边；解析只在服务器 src/http/diff.ts）。
///
/// 入口：T-iOS-26 的 ProjectDetail 以 `(endpoint, path)` 构造并 push/present
/// 本屏（本任务先行导出可呈现单元）。TerminalScreen 侧的 per-session cwd 入口
/// 未接 —— TerminalScreen（T-iOS-11 Owns）没有暴露 toolbar hook，接线移交
/// T-iOS-26/29。
///
/// 安全（SEC-H4 同款纪律）：diff 内容是**不可信服务器字节** —— 所有服务器
/// 文本一律 `Text(verbatim:)`（绝不 LocalizedStringKey/Markdown/链接探测），
/// 等宽单行呈现；行列表是惰性 List，巨 diff（服务器上限 2MB）不合成单个
/// Text 块。
struct DiffScreen: View {
    @State private var viewModel: DiffViewModel

    private enum Metrics {
        /// Faint per-line tint alpha for added/removed/hunk rows. This is a
        /// **code-diff syntax constant** (like terminal/xterm colors), NOT a
        /// card/status design token — the frozen `DS.Opacity` scale
        /// (stale/exited/pressed) has no semantic slot for a syntax highlight
        /// fill. The tint HUE always comes from `DS.Palette` (below); only this
        /// scanning-aid alpha is diff-local.
        static let lineTintOpacity = 0.12
    }

    /// 生产入口（T-iOS-26 消费）：`(endpoint, path)` + 注入的传输层。
    init(endpoint: HostEndpoint, path: String, http: any HTTPTransport) {
        _viewModel = State(initialValue: .forProject(
            endpoint: endpoint, path: path, http: http
        ))
    }

    /// 测试/预览缝：直接注入 VM。
    init(viewModel: DiffViewModel) {
        _viewModel = State(initialValue: viewModel)
    }

    var body: some View {
        VStack(spacing: 0) {
            scopePicker
                .padding(.horizontal, DS.Space.lg16)
                .padding(.vertical, DS.Space.sm8)
            content
        }
        .navigationTitle(DiffCopy.title)
        .navigationBarTitleDisplayMode(.inline)
        .task { await viewModel.load() } // fresh screen → one initial fetch
    }

    // MARK: - staged/unstaged 切换（re-fetch 由 VM 去重）

    private var scopePicker: some View {
        Picker(DiffCopy.scopePickerLabel, selection: Binding(
            get: { viewModel.staged },
            set: { newValue in Task { await viewModel.setStaged(newValue) } }
        )) {
            Text(DiffCopy.working).tag(false)
            Text(DiffCopy.staged).tag(true)
        }
        .pickerStyle(.segmented)
    }

    // MARK: - Phase switch

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

    /// 该范围下无改动（服务器回空 files）。截断到空也要保留 banner ——
    /// “没有改动”与“改动太大被截没了”是两回事。
    private func emptyState(truncated: Bool) -> some View {
        VStack(spacing: 0) {
            if truncated { truncatedBanner }
            ContentUnavailableView(
                DiffCopy.emptyTitle,
                systemImage: "checkmark.circle",
                description: Text(DiffCopy.emptyDetail)
            )
        }
    }

    /// 显式、可重试的错误态（path 非法 400/404 → 友好话术，任务 Steps）。
    private func failedState(_ failure: DiffViewModel.Failure) -> some View {
        let copy = Self.failureCopy(failure)
        return ContentUnavailableView {
            Label(copy.title, systemImage: "exclamationmark.triangle")
        } description: {
            Text(copy.detail)
        } actions: {
            Button(DiffCopy.retry) {
                Task { await viewModel.load() }
            }
            .buttonStyle(.borderedProminent)
            .tint(DS.Palette.accent)
        }
    }

    static func failureCopy(_ failure: DiffViewModel.Failure) -> (title: String, detail: String) {
        switch failure {
        case .pathInvalid:
            return (DiffCopy.failedPathInvalid, DiffCopy.failedPathInvalidDetail)
        case .notFound:
            return (DiffCopy.failedNotFound, DiffCopy.failedNotFoundDetail)
        case .unavailable:
            return (DiffCopy.failedUnavailable, DiffCopy.failedUnavailableDetail)
        }
    }

    // MARK: - 行列表（惰性；行模型已由 VM 平铺）

    private func diffList(_ presentation: DiffPresentation) -> some View {
        List {
            if presentation.truncated {
                truncatedBanner
                    .listRowSeparator(.hidden)
            }
            ForEach(presentation.rows) { row in
                rowView(row)
            }
        }
        .listStyle(.plain)
        .environment(\.defaultMinListRowHeight, 0) // 行高随内容，diff 行要紧凑
    }

    private var truncatedBanner: some View {
        Label(DiffCopy.truncatedBanner, systemImage: "scissors")
            .font(DS.Typography.caption)
            .foregroundStyle(DS.Palette.statusWaiting)
    }

    @ViewBuilder private func rowView(_ row: DiffRow) -> some View {
        switch row.kind {
        case .fileHeader(let header):
            fileHeaderRow(header)
        case .binaryNotice:
            Text(DiffCopy.binaryFile)
                .font(DS.Typography.caption.italic())
                .foregroundStyle(DS.Palette.textSecondary)
                .listRowSeparator(.hidden)
        case .hunkHeader(let header):
            diffTextRow(
                header, color: DS.Palette.accent,
                background: DS.Palette.accent.opacity(Metrics.lineTintOpacity)
            )
        case .line(let kind, let text):
            diffTextRow(
                text, color: Self.lineColor(kind), background: Self.lineBackground(kind)
            )
        }
    }

    private func fileHeaderRow(_ header: DiffFileHeader) -> some View {
        HStack(spacing: DS.Space.sm8) {
            // 路径是服务器字节：verbatim + 单行中段截断（等宽对齐）。
            Text(verbatim: header.pathLabel)
                .font(DS.Typography.mono(.footnote).weight(.semibold))
                .foregroundStyle(DS.Palette.textPrimary)
                .lineLimit(1)
                .truncationMode(.middle)
            Spacer(minLength: 0)
            Text(verbatim: "+\(header.added)")
                .font(DS.Typography.mono(.caption))
                .foregroundStyle(DS.Palette.statusWorking)
            Text(verbatim: "-\(header.removed)")
                .font(DS.Typography.mono(.caption))
                .foregroundStyle(DS.Palette.statusStuck)
            Text(DiffStatusStyle.label(for: header.status))
                .font(DS.Typography.caption)
                .foregroundStyle(DiffStatusStyle.color(for: header.status))
        }
        .padding(.top, DS.Space.lg16)
        .accessibilityElement(children: .combine)
    }

    /// One monospaced diff line. UNTRUSTED server bytes: `Text(verbatim:)`,
    /// single-line tail truncation (read-only skim view; no wrapping blob).
    private func diffTextRow(_ text: String, color: Color, background: Color) -> some View {
        Text(verbatim: text)
            .font(DS.Typography.mono(.caption))
            .foregroundStyle(color)
            .lineLimit(1)
            .truncationMode(.tail)
            .frame(maxWidth: .infinity, alignment: .leading)
            .listRowBackground(background)
            .listRowSeparator(.hidden)
            .listRowInsets(EdgeInsets(
                top: DS.Space.xs2,
                leading: DS.Space.md12,
                bottom: DS.Space.xs2,
                trailing: DS.Space.md12
            ))
    }

    // MARK: - kind → 颜色（全函数；未知 kind 已在解码层降级为 .context）
    // 语义色一律取自 DS.Palette（added=working 绿 / removed=stuck 红 /
    // hunk=accent），与全 App 品牌色对齐；仅淡底 alpha 是 diff 语法常量。

    static func lineColor(_ kind: DiffLineKind) -> Color {
        switch kind {
        case .added: return DS.Palette.statusWorking
        case .removed: return DS.Palette.statusStuck
        case .context: return DS.Palette.textPrimary
        case .hunk: return DS.Palette.accent
        case .meta: return DS.Palette.textSecondary
        }
    }

    static func lineBackground(_ kind: DiffLineKind) -> Color {
        switch kind {
        case .added: return DS.Palette.statusWorking.opacity(Metrics.lineTintOpacity)
        case .removed: return DS.Palette.statusStuck.opacity(Metrics.lineTintOpacity)
        case .hunk: return DS.Palette.accent.opacity(Metrics.lineTintOpacity)
        case .context, .meta: return .clear
        }
    }
}

// MARK: - status → 标签/颜色（全函数映射）

enum DiffStatusStyle {
    static func label(for status: DiffFileStatus) -> String {
        switch status {
        case .modified: return "修改"
        case .added: return "新增"
        case .deleted: return "删除"
        case .renamed: return "重命名"
        case .binary: return "二进制"
        case .untracked: return "未跟踪"
        }
    }

    static func color(for status: DiffFileStatus) -> Color {
        switch status {
        case .added, .untracked: return DS.Palette.statusWorking
        case .deleted: return DS.Palette.statusStuck
        case .renamed: return DS.Palette.accent
        case .modified, .binary: return DS.Palette.textSecondary
        }
    }
}

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

enum DiffCopy {
    static let title = "代码差异"
    static let scopePickerLabel = "差异范围"
    static let working = "工作区"
    static let staged = "已暂存"
    static let truncatedBanner = "差异过大，已截断显示（主机侧 DIFF_MAX_BYTES / DIFF_MAX_FILES 限制）。"
    static let emptyTitle = "无改动"
    static let emptyDetail = "当前范围下没有可显示的改动。"
    static let binaryFile = "二进制文件"
    static let failedPathInvalid = "路径无效"
    static let failedPathInvalidDetail = "请求的项目路径无效（服务器返回 400），请从项目列表重新进入。"
    static let failedNotFound = "项目未找到"
    static let failedNotFoundDetail = "该路径不是主机上的 git 仓库，或已被移除（404）。"
    static let failedUnavailable = "差异加载失败"
    static let failedUnavailableDetail = "无法从主机获取 diff，请检查连接后重试。"
    static let retry = "重试"
}
