import ClientTLS
import Foundation
import HostRegistry
import Observation
import SwiftUI
import WireProtocol

/// T-iOS-15 · Navigation + lifecycle owner: Pairing → SessionList → Terminal
/// with the production dependency graph (plan §7 T-iOS-15 step 1).
///
/// - Cold start: read the host store once — no paired host → Pairing, else
///   SessionList (`ColdStartPolicy.initialRoute`).
/// - `SessionListScreen.onOpen` → build ONE `TerminalSessionController`
///   (single foreground session, plan §1) and push the terminal.
/// - Back → `closeTerminal()` → `engine.close()` (detach; PTY keeps running).
/// - scenePhase is forwarded to the open controller (`.background` →
///   suspend/close, `.active` → resume/rebuild). The privacy shade is view
///   layer (RootView + PrivacyShadePolicy), not coordinator state.
@MainActor
@Observable
final class AppCoordinator {
    private(set) var route: ColdStartPolicy.RootRoute = .loading
    private(set) var terminalController: TerminalSessionController?
    /// First-run pairing VM (root route). Recreated per entry to keep pairing
    /// state machines single-shot.
    private(set) var rootPairingViewModel: PairingViewModel?
    /// Add-host pairing VM (sheet from the list header).
    private(set) var addHostPairingViewModel: PairingViewModel?
    var isAddHostPresented = false
    /// T-iOS-26 · Projects sheet（入口在 RootView 的 toolbar —— 不碰
    /// `SessionListScreen`，该文件 W7 内归 T-iOS-23）。每次呈现新建 VM，
    /// prefs 每次进入都重新拉取。
    private(set) var projectsViewModel: ProjectsViewModel?
    var isProjectsPresented = false
    /// C-iOS-3 (HIGH reachability fix) · "设备证书" sheet (import / rotate the
    /// mTLS device cert via `ClientCertScreen`). No VM state — the screen owns
    /// its own `ClientCertViewModel` over the keychain store; dismissal needs no
    /// refresh because the transports resolve the identity lazily per connection.
    var isDeviceCertPresented = false
    /// B3 · "自动获取证书" sheet — the zero-`.p12` enrollment flow
    /// (`EnrollmentScreen`): one login → Secure-Enclave key + CSR → device cert,
    /// presented automatically on the existing mTLS path. VM built per entry.
    var isEnrollmentPresented = false
    private(set) var enrollmentViewModel: EnrollmentViewModel?
    /// B3 · Re-entrancy guard so overlapping foregrounds never launch two silent
    /// renews at once (a shared PTY-style single-flight for the rotation pass).
    @ObservationIgnored private var isRotating = false
    /// B3 (HIGH observability fix) · Persistent, OBSERVABLE flag: the last silent
    /// rotation pass ended in `.failed` (keychain read fault or renew error). Set
    /// alongside the os.Logger line so a silently-failing renewal surfaces to the
    /// UI (a warning banner) instead of vanishing into the log. Cleared by the
    /// next non-failed pass — a recovered renewal drops the warning automatically.
    private(set) var isCertificateRenewalFailing = false

    let sessionList: SessionListViewModel
    @ObservationIgnored let environment: AppEnvironment
    /// T-iOS-22 · Deep-link handler; all routing/wiring logic lives in
    /// DeepLinkRouter.swift (incl. the `makeDeepLinkHandler` extension).
    @ObservationIgnored private(set) lazy var deepLink: DeepLinkHandler = makeDeepLinkHandler()

    init(environment: AppEnvironment) {
        self.environment = environment
        sessionList = SessionListViewModel(
            hostStore: environment.hostStore,
            http: environment.http,
            clock: ContinuousClock(),
            unreadStore: environment.unreadStore
        )
    }

    // MARK: - Cold start

    /// Decide the boot route from the host store. A store READ failure routes
    /// to the session list, whose own `reloadHosts` surfaces the explicit
    /// error copy (never a silent empty pairing screen hiding a broken store).
    func bootstrap() async {
        guard route == .loading else { return }
        do {
            let hosts = try await environment.hostStore.loadAll()
            route = ColdStartPolicy.initialRoute(pairedHostCount: hosts.count)
        } catch {
            route = .sessions
        }
        if route == .pairing {
            rootPairingViewModel = makePairingViewModel()
        }
        runCertificateRotationIfDue() // B3 · renew a due device cert on cold launch
        await deepLink.markReady() // flush a cold-launch deep link (T-iOS-22)
    }

    /// First-run pairing done → move to the list (the paired host is already
    /// in the store — PairingViewModel upserts before signalling).
    func completeFirstPairing(_ host: HostRegistry.Host) {
        rootPairingViewModel = nil
        route = .sessions
        Task { await sessionList.reloadHosts() }
    }

    // MARK: - Add-host sheet (list header hook)

    func presentAddHost() {
        addHostPairingViewModel = makePairingViewModel()
        isAddHostPresented = true
    }

    func completeAddHost(_ host: HostRegistry.Host) {
        isAddHostPresented = false
        addHostDismissed()
    }

    /// Sheet gone (paired OR cancelled): drop the VM and refresh hosts — the
    /// list VM keeps the active host if it still exists.
    func addHostDismissed() {
        addHostPairingViewModel = nil
        Task { await sessionList.reloadHosts() }
    }

    // MARK: - Projects (T-iOS-26)

    /// Toolbar 入口（RootView）：以当前活跃主机呈现 Projects sheet。
    func presentProjects() {
        guard let host = sessionList.activeHost else { return }
        projectsViewModel = ProjectsViewModel(host: host, http: environment.http)
        isProjectsPresented = true
    }

    /// Sheet 消失（打开会话 OR 手动关闭）：丢弃 VM。
    func projectsDismissed() {
        projectsViewModel = nil
    }

    // MARK: - Device certificate (C-iOS-3)

    /// Toolbar host-menu 入口：呈现设备证书导入/轮换 sheet。
    func presentDeviceCert() {
        isDeviceCertPresented = true
    }

    // MARK: - Device enrollment (B3, zero-.p12 auto-cert)

    /// Toolbar host-menu 入口：呈现「自动获取证书」注册 sheet。VM 每次进入重建。
    func presentEnrollment() {
        enrollmentViewModel = makeEnrollmentViewModel()
        isEnrollmentPresented = true
    }

    /// Sheet 消失：丢弃 VM，并立刻跑一次轮换检查（刚注册完的证书据此登记续期）。
    func enrollmentDismissed() {
        enrollmentViewModel = nil
        runCertificateRotationIfDue()
    }

    /// 生产 VM：登录 → 安全区生成密钥+CSR → `/device/enroll` → 存钥匙串，全部经
    /// `DeviceEnrollmentFlow`（复用 ClientTLS 库，本层零加密/钥匙串逻辑）。
    private func makeEnrollmentViewModel() -> EnrollmentViewModel {
        let http = environment.http
        return EnrollmentViewModel(
            enrollOperation: { password, subdomain, deviceName, url in
                try await makeDeviceEnrollmentFlow(controlPlaneURL: url, http: http)
                    .run(password: password, subdomain: subdomain, deviceName: deviceName)
            },
            loadSummary: { (try? KeychainClientIdentityStore().loadSummary()) ?? nil }
        )
    }

    /// B3 · Silent rotation: check the installed leaf's timing and, if the renew
    /// window has opened, renew over mTLS against the SAME Secure-Enclave key.
    /// Fire-and-forget on launch + every foreground; single-flight via
    /// `isRotating`. A `.notEnrolled` / `.notDue` pass is cheap and common.
    func runCertificateRotationIfDue() {
        guard !isRotating else { return }
        isRotating = true
        let scheduler = makeCertificateRotationScheduler(http: environment.http)
        Task { [weak self] in
            let outcome = await scheduler.runIfDue()
            self?.isRotating = false
            // Surface a silently-failing renewal as observable state (not only a
            // log line); a later successful/not-due pass clears it.
            self?.isCertificateRenewalFailing = (outcome == .failed)
        }
    }

    /// "在此仓库开新会话"：关 sheet → fresh spawn（`attach(null, cwd)`）+
    /// attach 后注入 `claude\r`（帧序由 engine 的 attach-first 队列保证）。
    func openProject(_ request: ProjectOpenRequest) {
        guard terminalController == nil else { return } // one foreground session
        isProjectsPresented = false
        projectsViewModel = nil
        startTerminal(
            host: request.host, sessionId: nil,
            spawnCwd: request.cwd, bootstrapInput: request.bootstrapInput
        )
    }

    // MARK: - Terminal open/close

    /// `SessionListScreen.onOpen` (one navigation signal per tap) and the
    /// "继续上次" banner both land here. `sessionId == nil` = new session.
    func open(_ request: SessionListViewModel.OpenRequest) {
        guard terminalController == nil else { return } // one foreground session
        startTerminal(host: request.host, sessionId: request.sessionId)
    }

    /// 唯一的 controller 构造点（普通打开与 T-iOS-26 项目内 spawn 共用）。
    private func startTerminal(
        host: HostRegistry.Host,
        sessionId: UUID?,
        spawnCwd: String? = nil,
        bootstrapInput: String? = nil
    ) {
        let controller = TerminalSessionController(
            host: host,
            sessionId: sessionId,
            environment: environment,
            onPendingChanged: { [weak self] sessionId, pending in
                self?.sessionList.setPendingApproval(sessionId: sessionId, pending: pending)
            },
            onTitleChanged: { [weak self] sessionId, title in
                // T-iOS-23 · OSC title → list row (already sanitized in the
                // VM; the list VM sanitizes once more at its own boundary).
                self?.sessionList.setSessionTitle(sessionId: sessionId, title: title)
            },
            spawnCwd: spawnCwd,
            bootstrapInput: bootstrapInput
        )
        terminalController = controller
        controller.start()
    }

    /// Back navigation popped the terminal: explicit detach. Also the first
    /// half of every session SWITCH (single live WS invariant, plan §1):
    /// list back-nav and `openDeepLinkedSession` both close here before the
    /// next `open` — one engine at a time, always close→open with replay.
    func closeTerminal() {
        // T-iOS-23 · leaving = seen: stamp the unread watermark for the
        // adopted session so output watched in the terminal never relights
        // the list dot.
        if let sessionId = terminalController?.terminalViewModel.sessionId {
            sessionList.markSeen(sessionId: sessionId)
        }
        terminalController?.teardown()
        terminalController = nil
    }

    /// T-iPad-3 · 终端指针上下文菜单「结束会话」→ 复用既有 kill 通道
    /// （`SessionListViewModel.kill` → `APIClient.killSession`，带 Origin，无新
    /// 网络路径），随后 detach detail。无 adopted 会话 → no-op。
    func killCurrentSession() {
        guard let sessionId = terminalController?.terminalViewModel.sessionId else { return }
        Task { await sessionList.kill(sessionId: sessionId) }
        closeTerminal()
    }

    // MARK: - Split-view sidebar bridge (T-iPad-2)

    /// Detail 面板当前显示的会话 → sidebar 选中态（二向绑定的 getter）。只反映
    /// **adopted** 会话；未 adopted / 无打开的 detail → nil（占位「选择或新建
    /// 会话」）。分栏与 stack 共享同一 `terminalController`（归 coordinator
    /// 所有，与布局分支正交）。
    var selectedSidebarItem: SidebarItem? {
        terminalController?.terminalViewModel.sessionId.map(SidebarItem.session)
    }

    /// `NavigationSplitView` 的选中绑定。getter 反映 detail 会话；setter 复用
    /// 既有路由（`open` / `presentProjects`）—— 分栏只是又一个触发面，绝不新开
    /// 一条会话生命周期。取消选中（set nil）不主动关闭 detail。
    var sidebarSelection: Binding<SidebarItem?> {
        Binding(
            get: { self.selectedSidebarItem },
            set: { item in
                guard let item else { return }
                self.selectSidebarItem(item)
            }
        )
    }

    /// sidebar 触发面：把选中项映射到**同一** open(id) / open(nil) /
    /// presentProjects API。切会话沿用单活 WS 不变式的 close→open（与
    /// new-in-cwd / deep-link 切换同一套原语），`.projects` 走既有 sheet 呈现。
    func selectSidebarItem(_ item: SidebarItem) {
        switch item {
        case .session(let sessionId):
            switchTerminal(sessionId: sessionId)
        case .newSession:
            switchTerminal(sessionId: nil)
        case .projects:
            presentProjects()
        }
    }

    /// 单活 WS 不变式下的会话切换：已在显示同一会话 → no-op（免无谓 churn）；
    /// 否则先 `closeTerminal()`（旧会话记 last-seen 水位、engine detach）再
    /// `open()`。复用既有原语，**非**平行生命周期。无 activeHost → no-op。
    private func switchTerminal(sessionId: UUID?) {
        guard let host = sessionList.activeHost else { return }
        if let sessionId, selectedSidebarItem == .session(sessionId) { return }
        if terminalController != nil { closeTerminal() }
        open(SessionListViewModel.OpenRequest(id: UUID(), host: host, sessionId: sessionId))
    }

    // MARK: - "在当前目录开新会话" (T-iOS-29)

    /// 当前终端会话的 cwd。解析次序：/live-sessions 行数据（server-adopted
    /// id 匹配行，列表 VM 的最近快照）→ controller 的 `spawnCwd`（T-iOS-26
    /// 项目 fresh-spawn 尚未进列表轮询）→ nil（未知）。服务器数据不可信：
    /// 非绝对路径按未知处理（ProjectsViewModel 同款纪律；engine 侧还会再验）。
    var currentTerminalCwd: String? {
        guard let controller = terminalController else { return nil }
        let fromRows = controller.terminalViewModel.sessionId.flatMap { id in
            sessionList.rows.first(where: { $0.id == id })?.info.cwd
        }
        let candidate = fromRows ?? controller.spawnCwd
        return candidate.flatMap { Validation.isAbsoluteCwd($0) ? $0 : nil }
    }

    /// TerminalScreen 工具栏与 exit 横幅共用动作：在当前会话的 cwd fresh
    /// spawn（`attach(null, cwd)`，镜像 web tabs.ts `newTab()` M6）。单活
    /// WS 不变式：先 `closeTerminal()`（旧会话记 last-seen 水位、engine
    /// detach），再开新 controller。cwd 未知 → 普通新会话；无打开的终端
    /// → no-op。不注入 bootstrap —— "开新 shell"不是"起 claude"。
    func openNewSessionInCurrentCwd() {
        guard let controller = terminalController else { return }
        let host = controller.host
        let cwd = currentTerminalCwd
        closeTerminal()
        startTerminal(host: host, sessionId: nil, spawnCwd: cwd)
    }

    // MARK: - "继续上次" (cold start step 5)

    var continueLastSessionId: UUID? {
        ColdStartPolicy.continueLastSessionId(
            activeHost: sessionList.activeHost,
            store: environment.lastSessionStore
        )
    }

    func openContinueLast() {
        guard let host = sessionList.activeHost, let sessionId = continueLastSessionId else {
            return
        }
        open(SessionListViewModel.OpenRequest(id: UUID(), host: host, sessionId: sessionId))
    }

    // MARK: - scenePhase (plan §7 T-iOS-15 step 3)

    func handleScenePhase(_ phase: ScenePhase) {
        switch phase {
        case .background:
            terminalController?.suspend()
        case .active:
            terminalController?.resumeIfNeeded()
            runCertificateRotationIfDue() // B3 · check for a due renewal each foreground
        case .inactive:
            break // transient; shade covers it at the view layer
        @unknown default:
            break
        }
    }

    // MARK: - Helpers

    private func makePairingViewModel() -> PairingViewModel {
        PairingViewModel(store: environment.hostStore, probe: environment.probe)
    }
}

/// T-iPad-2 · split 模式下 sidebar 的选中项。与现有 `AppCoordinator` 路由
/// 一一等价映射：`.session(id)` == `open(id)`、`.newSession` == `open(nil)`、
/// `.projects` == `presentProjects()`。
enum SidebarItem: Hashable {
    case session(UUID)
    case newSession
    case projects
}
