import APIClient
import Foundation
import HostRegistry
import Observation
import WireProtocol

/// T-iOS-26 · Projects 列表状态（镜像 web v0.6 projects 面板的数据面）：
/// `GET /projects` 的分组网格 + `GET/PUT /prefs` 的跨端收藏/折叠往返 +
/// "在此仓库开新会话" 的导航信号。
///
/// Documented decisions:
/// - **prefs 是 clobber 敏感面**：`PUT /prefs` 服务器侧整体替换 blob
///   （src/server.ts:286-288），所以一律经 `UiPrefs` 的 unknown-key-preserving
///   API 改写（web/未来服务器写入的未知顶层键原样带回）；prefs GET 失败时
///   toggle 只改本地、**绝不 PUT**（空底盘上写 = 清掉服务器收藏）。
/// - **PUT 成功采纳服务器 echo 为新真相**（服务器会 sanitize，本地状态与
///   服务器逐字节对齐）；失败保留本地改动 + 显式文案，下次 toggle 自然重试。
/// - **prefs 只在首次 load 拉一次**（镜像 web mountProjects.init：刷新节奏
///   只重取项目，绝不用旧服务器值clobber本地未同步的编辑）。
/// - 列表数据是不可信服务器输入：解码宽容（APIClient 已做），路径在铸造
///   OpenRequest 前再过 `Validation.isAbsoluteCwd`（deep-link 同款纪律）。
@MainActor
@Observable
final class ProjectsViewModel {
    // MARK: - Observable state

    private(set) var projects: [ProjectInfo] = []
    private(set) var hasLoadedOnce = false
    /// 最近一次项目刷新失败（旧列表保留 —— 一次丢包不清屏）。
    private(set) var fetchErrorMessage: String?
    /// prefs 加载失败：收藏/折叠只在本地生效、不回写（防 clobber）。
    private(set) var prefsErrorMessage: String?
    /// 最近一次 PUT /prefs 失败（本地改动已保留）。
    private(set) var prefsSyncErrorMessage: String?
    /// "在此仓库开新会话" 被拒（非法路径）。
    private(set) var openErrorMessage: String?
    /// 收藏的项目路径（保序：新收藏追加尾部）。
    private(set) var favourites: [String] = []
    /// 组 key → 已折叠（只存 true；展开是默认态 —— 与 web/服务器一致）。
    private(set) var collapsedGroups: [String: Bool] = [:]
    /// 导航信号（T-iOS-26）：每次请求新 id，重复点按也能触发 onChange。
    private(set) var openRequest: ProjectOpenRequest?
    /// 搜索框绑定（.searchable）。
    var searchText = ""

    // MARK: - Dependencies & internal state

    let host: HostRegistry.Host
    @ObservationIgnored let http: any HTTPTransport
    /// 最近一次已知的完整 prefs blob（含未知键）。nil = 从未成功加载 →
    /// 绝不 PUT。
    @ObservationIgnored private var prefsBase: UiPrefs?

    private var client: APIClient {
        APIClient(endpoint: host.endpoint, http: http)
    }

    init(host: HostRegistry.Host, http: any HTTPTransport) {
        self.host = host
        self.http = http
    }

    // MARK: - Derived view state

    /// 分组快照：搜索过滤 → web 同款分组（收藏优先排序在组内完成）。
    var groups: [ProjectGroup] {
        ProjectGrouping.group(
            ProjectGrouping.filter(projects, query: searchText),
            favourites: Set(favourites)
        )
    }

    var isSearching: Bool {
        !searchText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
    }

    /// 搜索中强制全展开（结果绝不藏在折叠 caret 后，镜像 web renderGrid）。
    func isCollapsed(_ group: ProjectGroup) -> Bool {
        group.isCollapsible && !isSearching && collapsedGroups[group.key] == true
    }

    func isFavourite(_ path: String) -> Bool {
        favourites.contains(path)
    }

    /// 空态文案：无项目 vs 无搜索结果（镜像 web renderGrid 的两种 msg）。
    var emptyStateMessage: String? {
        guard hasLoadedOnce, fetchErrorMessage == nil else { return nil }
        if projects.isEmpty { return ProjectsCopy.emptyNoProjects }
        if ProjectGrouping.filter(projects, query: searchText).isEmpty {
            return ProjectsCopy.emptyNoMatch
        }
        return nil
    }

    // MARK: - Load / refresh

    /// 首次进入：prefs（一次）+ 项目。prefs 之后留在内存（镜像 web init）。
    func load() async {
        await loadPrefsIfNeeded()
        await refresh()
    }

    /// 只重取项目（下拉刷新与重试共用）。失败保留旧列表 + 显式文案。
    func refresh() async {
        do {
            projects = try await client.projects()
            fetchErrorMessage = nil
            hasLoadedOnce = true
        } catch {
            fetchErrorMessage = ProjectsCopy.fetchFailed(Self.errorDetail(error))
        }
    }

    private func loadPrefsIfNeeded() async {
        guard prefsBase == nil else { return }
        do {
            let prefs = try await client.prefs()
            adopt(prefs)
            prefsErrorMessage = nil
        } catch {
            prefsErrorMessage = ProjectsCopy.prefsLoadFailed
        }
    }

    // MARK: - Favourites / collapse（跨端 prefs 往返）

    func toggleFavourite(path: String) async {
        favourites = favourites.contains(path)
            ? favourites.filter { $0 != path }
            : favourites + [path]
        await persistPrefs()
    }

    func toggleCollapsed(key: String) async {
        if collapsedGroups[key] == true {
            collapsedGroups = collapsedGroups.filter { $0.key != key }
        } else {
            collapsedGroups = collapsedGroups.merging([key: true]) { _, new in new }
        }
        await persistPrefs()
    }

    /// 以最近已知的完整 blob 为底盘改写两个已知键（未知键原样带回），PUT
    /// 后采纳服务器 echo。无底盘（prefs 从未加载成功）→ 本地生效、不回写。
    private func persistPrefs() async {
        guard let base = prefsBase else { return }
        let next = base.withFavourites(favourites).withCollapsed(collapsedGroups)
        prefsBase = next // 乐观：连续 toggle 在同一底盘上叠加
        do {
            let echoed = try await client.putPrefs(next)
            adopt(echoed)
            prefsSyncErrorMessage = nil
        } catch {
            prefsSyncErrorMessage = ProjectsCopy.prefsSyncFailed(Self.errorDetail(error))
        }
    }

    private func adopt(_ prefs: UiPrefs) {
        prefsBase = prefs
        favourites = prefs.favourites
        collapsedGroups = prefs.collapsed
    }

    // MARK: - 在此仓库开新会话（T-iOS-26 的核心动作）

    /// `attach(null, cwd)` + attach 后注入 `claude\r`（帧序由 engine 的
    /// attach-first 队列保证）。路径是服务器数据 → 不可信，铸造前先验证。
    func requestOpenClaude(cwd: String) {
        guard Validation.isAbsoluteCwd(cwd) else {
            openErrorMessage = ProjectsCopy.openClaudeInvalidPath
            return
        }
        openErrorMessage = nil
        openRequest = ProjectOpenRequest(
            id: UUID(), host: host, cwd: cwd,
            bootstrapInput: ProjectLaunch.claudeBootstrapInput
        )
    }

    // MARK: - Detail assembly（详情/差异屏的装配缝）

    func makeDetailViewModel(path: String) -> ProjectDetailViewModel {
        .forHost(endpoint: host.endpoint, http: http, path: path)
    }

    // MARK: - Helpers

    private static func errorDetail(_ error: any Error) -> String {
        (error as? APIClientError)?.message ?? error.localizedDescription
    }
}

/// "在此仓库开新会话" 的导航信号 —— `SessionListViewModel.OpenRequest` 的
/// cwd+bootstrap 平行变体（该类型属 T-iOS-13 文件，不越界扩展；由
/// AppCoordinator.openProject 消费）。
struct ProjectOpenRequest: Equatable, Sendable, Identifiable {
    let id: UUID
    let host: HostRegistry.Host
    /// 新会话的工作目录（已验证为绝对路径）。
    let cwd: String
    /// attach 后注入的首条输入（nil = 只开 shell）。
    let bootstrapInput: String?
}

/// 项目内启动命令（镜像 public/tabs.ts:679 `openProject` 的默认 cmd）。
enum ProjectLaunch {
    /// Enter 是 `\r`（0x0D）不是 `\n` —— 合成输入的经典坑（CLAUDE.md Gotchas）。
    static let claudeBootstrapInput = "claude\r"
}

/// 用户可见文案（中文具名常量，plan §4）。
enum ProjectsCopy {
    static let title = "项目"
    static let searchPrompt = "筛选项目…"
    static let activeGroupLabel = "活跃中"
    static let otherGroupLabel = "其他"
    static let allGroupLabel = "全部项目"
    static let dirtyBadge = "未提交"
    static let emptyNoProjects = "未发现项目。请检查主机的 PROJECT_ROOTS 配置。"
    static let emptyNoMatch = "没有匹配的项目。"
    static let prefsLoadFailed = "云端收藏/折叠状态加载失败，本次修改仅在本机生效。"
    static let openClaudeInvalidPath = "项目路径无效，无法开新会话。"

    static func fetchFailed(_ detail: String) -> String {
        "刷新项目列表失败：\(detail)"
    }

    static func prefsSyncFailed(_ detail: String) -> String {
        "收藏/折叠状态同步失败：\(detail)"
    }

    static func activeCountBadge(_ count: Int) -> String {
        "● \(count) 活跃"
    }
}
