import Foundation
import Observation
import WireProtocol

/// T-iOS-27 · State for the read-only diff viewer (`DiffScreen`).
///
/// The fetch closure is injected —— 生产由 `forProject` 用 `DiffFetcher` 包一
/// 层（App 装配缝，T-iOS-26 的 ProjectDetail 入口按 (endpoint, path) 构造）；
/// 测试注入 fake。服务器数据的不可信处理（宽容解码、错误分类）已在
/// `DiffFetcher` 完成；本 VM 只区分用户可见的四种结局：
/// - 非空 files → `.loaded`：文件头/hunk 头/行**平铺**为惰性列表行模型
///   （巨 diff —— 服务器上限 DIFF_MAX_BYTES 2MB —— 绝不合成单个 Text 块）；
/// - `[]` → `.empty`（truncated 位透传，截断到空也要提示）；
/// - 400/非法请求 → `.failed(.pathInvalid)`、404 → `.failed(.notFound)`、
///   其余 → `.failed(.unavailable)` —— 全部可经 `load()` 重试；
/// - staged/unstaged 切换（`setStaged`）→ 以新范围重新 fetch，同值 no-op。
@MainActor
@Observable
final class DiffViewModel {
    /// 用户可见的失败三分类（文案映射在 DiffScreen 的 DiffCopy）。
    enum Failure: Equatable {
        case pathInvalid
        case notFound
        case unavailable
    }

    /// Rendering phase — an explicit enum so the screen can never show an
    /// error and diff rows at the same time (same discipline as Timeline).
    enum Phase: Equatable {
        case loading
        /// 服务器回了空 files（该范围无改动）。truncated 位仍需提示。
        case empty(truncated: Bool)
        case loaded(DiffPresentation)
        case failed(Failure)
    }

    private(set) var phase: Phase = .loading
    /// 当前范围：false = 工作区（unstaged），true = 已暂存（--staged）。
    private(set) var staged = false

    @ObservationIgnored
    private let fetch: @Sendable (_ staged: Bool) async throws -> DiffResult

    init(fetch: @escaping @Sendable (_ staged: Bool) async throws -> DiffResult) {
        self.fetch = fetch
    }

    /// 生产装配缝：`DiffScreen(endpoint:path:http:)` 经此构造（T-iOS-26 的
    /// ProjectDetail 入口只需转手这三样，无需触碰 DiffFetcher）。
    static func forProject(
        endpoint: HostEndpoint, path: String, http: any HTTPTransport
    ) -> DiffViewModel {
        let fetcher = DiffFetcher(endpoint: endpoint, http: http)
        return DiffViewModel(fetch: { staged in
            try await fetcher.fetch(path: path, staged: staged)
        })
    }

    /// Fetch and present. Also the「重试」path: callable again from `.failed`.
    func load() async {
        phase = .loading
        do {
            let result = try await fetch(staged)
            phase = Self.presentation(for: result)
        } catch let error as DiffFetchError {
            phase = .failed(Self.failure(for: error))
        } catch {
            phase = .failed(.unavailable) // 传输层等其余错误：可重试兜底
        }
    }

    /// staged/unstaged 切换 → 重新 fetch；同值绝不重复请求（任务 Steps）。
    func setStaged(_ newValue: Bool) async {
        guard newValue != staged else { return }
        staged = newValue
        await load()
    }

    // MARK: - 纯呈现归约（静态，可单测）

    static func presentation(for result: DiffResult) -> Phase {
        guard !result.files.isEmpty else {
            return .empty(truncated: result.truncated)
        }
        return .loaded(DiffPresentation(
            rows: makeRows(files: result.files), truncated: result.truncated
        ))
    }

    /// 平铺：文件头 → (binary 占位 | hunk 头 → 行…)…，逐文件顺序保持服务器
    /// 返回顺序；binary 短路 hunks（镜像 web renderDiffFile 的 early return，
    /// public/diff.ts:163-166）。id 为稳定递增序号（行内容可重复，不能当身份）。
    static func makeRows(files: [DiffFile]) -> [DiffRow] {
        var rows: [DiffRow] = []
        for file in files {
            rows.append(DiffRow(id: rows.count, kind: .fileHeader(header(for: file))))
            if file.binary {
                rows.append(DiffRow(id: rows.count, kind: .binaryNotice))
                continue
            }
            for hunk in file.hunks {
                rows.append(DiffRow(id: rows.count, kind: .hunkHeader(hunk.header)))
                for line in hunk.lines {
                    rows.append(DiffRow(
                        id: rows.count, kind: .line(kind: line.kind, text: line.text)
                    ))
                }
            }
        }
        return rows
    }

    private static func failure(for error: DiffFetchError) -> Failure {
        switch error {
        case .invalidRequest, .pathInvalid:
            return .pathInvalid
        case .projectNotFound:
            return .notFound
        case .invalidResponse, .unexpectedStatus:
            return .unavailable
        }
    }

    /// Rename 显示 “old → new”（镜像 web，public/diff.ts:146-150），其余显示
    /// newPath。路径是服务器字节 —— 屏幕侧一律 `Text(verbatim:)`。
    private static func header(for file: DiffFile) -> DiffFileHeader {
        let pathLabel = file.status == .renamed && file.oldPath != file.newPath
            ? "\(file.oldPath) → \(file.newPath)"
            : file.newPath
        return DiffFileHeader(
            pathLabel: pathLabel, added: file.added,
            removed: file.removed, status: file.status
        )
    }
}

// MARK: - 行模型（惰性列表的最小呈现单元）

/// Display-ready flattened diff（不可变快照）。
struct DiffPresentation: Equatable {
    let rows: [DiffRow]
    let truncated: Bool
}

/// One lazy-list row. `id` = 平铺序号（稳定、唯一——文本内容可重复）。
struct DiffRow: Equatable, Identifiable {
    enum Kind: Equatable {
        case fileHeader(DiffFileHeader)
        case binaryNotice
        case hunkHeader(String)
        case line(kind: DiffLineKind, text: String)
    }

    let id: Int
    let kind: Kind
}

/// File-header row payload（路径标签已含 rename 箭头）。
struct DiffFileHeader: Equatable {
    let pathLabel: String
    let added: Int
    let removed: Int
    let status: DiffFileStatus
}
