import APIClient
import ClientTLS
import Foundation
import os
import SwiftUI
import UIKit
import WireProtocol

/// T-iOS-28 · 会话缩略图管线（`GET /live-sessions/:id/preview` → 离屏
/// SwiftTerm → 快照 UIImage → 列表行）。本文件是纯逻辑侧：请求/键、LRU 缓存、
/// 并发上限闸、管线与 SwiftUI 槽位；真正的离屏渲染在
/// `SessionThumbnailRenderer.swift`（唯一 import SwiftTerm 的一侧）。
///
/// 设计要点（对应 Steps）：
/// - **缓存键 = (sessionId, lastOutputAt)**：`lastOutputAt`（T-iOS-37）不变 =
///   会话无新输出 = 缩略图不可能变 → 绝不重渲染。nil（P1 前旧服务器）意味着
///   没有失效信号：渲染一次后永久命中——宁可陈旧也不随每次 5s 轮询无界重
///   渲染（文档化降级）。
/// - **并发上限**：离屏渲染要 spawn 一个完整 SwiftTerm 终端，列表滚动绝不能
///   无界并发——`SessionThumbnailRenderGate`（FIFO、permit 转移）把「取数 +
///   渲染」整段管线钳在 `maxConcurrentRenders`。
/// - **失败也缓存**：404/网络错/校验失败 → `.placeholder` 同键入缓存，滚动
///   不会打爆故障主机；会话一有新输出（新 lastOutputAt）自然重试。
/// - **服务器是不可信输入源**：preview 的 `data` 只喂 SwiftTerm（它就是 ANSI
///   解释器），永不字符串处理；但 id/几何/字节数在边界校验，不合格拒渲染。
struct SessionThumbnailRequest: Equatable, Sendable {
    let endpoint: HostEndpoint
    let sessionId: UUID
    /// 服务器 `LiveSessionInfo.lastOutputAt`（ms since epoch；可选新增字段）。
    let lastOutputAt: Int?

    var key: SessionThumbnailKey {
        SessionThumbnailKey(sessionId: sessionId, lastOutputAt: lastOutputAt)
    }
}

/// 缓存身份：同键 = 「同一会话且自上次渲染以来没有任何新输出」。
struct SessionThumbnailKey: Hashable, Sendable {
    let sessionId: UUID
    let lastOutputAt: Int?
}

/// 管线产物。`Equatable` 对 `.rendered` 按图像身份比较（同一缓存条目 ===）。
enum SessionThumbnailImage: Sendable {
    case rendered(UIImage)
    case placeholder

    var isPlaceholder: Bool {
        if case .placeholder = self { return true }
        return false
    }

    var uiImage: UIImage? {
        if case .rendered(let image) = self { return image }
        return nil
    }
}

extension SessionThumbnailImage: Equatable {
    static func == (lhs: Self, rhs: Self) -> Bool {
        switch (lhs, rhs) {
        case (.placeholder, .placeholder): return true
        case (.rendered(let a), .rendered(let b)): return a === b
        default: return false
        }
    }
}

/// 本 feature 的具名常量（局部常量；WireProtocol 的 `Tunables` 已冻结，不动）。
enum SessionThumbnailTunable {
    /// 同时在飞的「取数 + 离屏渲染」管线数上限——滚动不得无界 spawn 终端。
    static let maxConcurrentRenders = 2
    /// LRU 缓存条目上限（每条 ≈ 一张 2x 小快照，内存有界）。
    static let maxCacheEntries = 32
    /// 防御性字节上限：服务器默认 tail 24 KiB（src/config.ts:46
    /// DEFAULT_PREVIEW_BYTES，env `PREVIEW_BYTES` 可调）——留 ~10× 余量；
    /// 超限视为异常主机，拒渲染（喂给解释器前唯一的一次尺寸检查）。
    static let maxPreviewDataBytes = 256 * 1024
    /// 缩略图渲染网格上限：几何是不可信输入，钳制它把离屏绘制的行数/位图
    /// 尺寸钳在有界范围（真实终端极少超过 300 列；超宽会话只损失换行保真）。
    static let maxRenderCols = 300
    static let maxRenderRows = 120
    /// 镜像 web 预览的 `Math.max(2, …)`（public/preview-grid.ts:112-117）。
    static let minRenderGrid = 2
}

// MARK: - LRU 缓存（不可变快照；持有方整体替换）

/// 定容 LRU：`order` 尾部为最近使用。所有操作返回新副本（不可变风格）。
struct SessionThumbnailCache {
    let maxEntries: Int
    private let entries: [SessionThumbnailKey: SessionThumbnailImage]
    private let order: [SessionThumbnailKey]

    init(maxEntries: Int) {
        self.init(maxEntries: maxEntries, entries: [:], order: [])
    }

    private init(
        maxEntries: Int,
        entries: [SessionThumbnailKey: SessionThumbnailImage],
        order: [SessionThumbnailKey]
    ) {
        self.maxEntries = max(1, maxEntries)
        self.entries = entries
        self.order = order
    }

    func value(for key: SessionThumbnailKey) -> SessionThumbnailImage? {
        entries[key]
    }

    /// 命中后前移（most-recently-used）。未含该键则原样返回。
    func bumping(_ key: SessionThumbnailKey) -> SessionThumbnailCache {
        guard entries[key] != nil else { return self }
        return SessionThumbnailCache(
            maxEntries: maxEntries, entries: entries,
            order: order.filter { $0 != key } + [key]
        )
    }

    /// 插入并按容量逐出 least-recently-used。
    func inserting(
        _ image: SessionThumbnailImage, for key: SessionThumbnailKey
    ) -> SessionThumbnailCache {
        var newEntries = entries.merging([key: image]) { _, new in new }
        var newOrder = order.filter { $0 != key } + [key]
        while newOrder.count > maxEntries, let oldest = newOrder.first {
            newOrder = Array(newOrder.dropFirst())
            newEntries = newEntries.filter { $0.key != oldest }
        }
        return SessionThumbnailCache(
            maxEntries: maxEntries, entries: newEntries, order: newOrder
        )
    }
}

// MARK: - 并发上限闸（FIFO、permit 转移）

/// 渲染管线的并发闸：`acquire` 超限即按 FIFO 排队；`release` 时若有排队者，
/// permit 原地转移（activeCount 不变）。测试屏障 `waitUntilWaiting` 与
/// FakeClock.waitForSleepers 同手法——continuation，零轮询零真睡。
@MainActor
final class SessionThumbnailRenderGate {
    let limit: Int
    private(set) var activeCount = 0
    private(set) var peakActiveCount = 0
    private var waiters: [CheckedContinuation<Void, Never>] = []
    private var barriers: [(target: Int, cont: CheckedContinuation<Void, Never>)] = []

    var waitingCount: Int { waiters.count }

    init(limit: Int) {
        self.limit = max(1, limit)
    }

    func acquire() async {
        if activeCount < limit {
            activeCount += 1
            peakActiveCount = max(peakActiveCount, activeCount)
            return
        }
        await withCheckedContinuation { cont in
            waiters = waiters + [cont]
            notifyBarriers()
        }
        // 被 release() 唤醒 = permit 已转移给我们，activeCount 保持不变。
        peakActiveCount = max(peakActiveCount, activeCount)
    }

    func release() {
        guard waiters.isEmpty else {
            let next = waiters[0]
            waiters = Array(waiters.dropFirst())
            next.resume() // permit 转移，activeCount 不动
            return
        }
        activeCount = max(0, activeCount - 1)
    }

    /// 测试屏障：挂起直到至少 `count` 个 acquire 在排队。
    func waitUntilWaiting(count: Int) async {
        if waiters.count >= count { return }
        await withCheckedContinuation { cont in
            barriers = barriers + [(count, cont)]
        }
    }

    private func notifyBarriers() {
        let met = barriers.filter { $0.target <= waiters.count }
        barriers = barriers.filter { $0.target > waiters.count }
        for barrier in met { barrier.cont.resume() }
    }
}

// MARK: - 管线

@MainActor
final class SessionThumbnailPipeline {
    /// 取数缝：生产侧 = `APIClient.preview(id:)`（RO GET，无 Origin）。
    typealias PreviewLoader = @MainActor (SessionThumbnailRequest) async throws -> SessionPreview
    /// 渲染缝：生产侧 = `SessionThumbnailRenderer.render`（离屏 SwiftTerm）。
    /// 入参几何已经过 `clampedGeometry` 钳制。
    typealias PreviewRenderer = @MainActor (_ data: String, _ cols: Int, _ rows: Int) -> UIImage?

    private let loader: PreviewLoader
    private let renderer: PreviewRenderer
    private let gate: SessionThumbnailRenderGate
    private var cache: SessionThumbnailCache
    /// 同键并发去重：第二个请求 await 第一个的任务，绝不双渲染。
    private var inFlight: [SessionThumbnailKey: Task<SessionThumbnailImage, Never>] = [:]
    private let logger = Logger(
        subsystem: SessionThumbnailLog.subsystem, category: SessionThumbnailLog.category
    )

    init(
        loader: @escaping PreviewLoader,
        renderer: @escaping PreviewRenderer,
        gate: SessionThumbnailRenderGate? = nil,
        maxCacheEntries: Int = SessionThumbnailTunable.maxCacheEntries
    ) {
        self.loader = loader
        self.renderer = renderer
        self.gate = gate
            ?? SessionThumbnailRenderGate(limit: SessionThumbnailTunable.maxConcurrentRenders)
        cache = SessionThumbnailCache(maxEntries: maxCacheEntries)
    }

    /// 生产装配：ephemeral URLSession（preview 字节可能含屏上密钥，内存缓存
    /// only——同 T-iOS-19 对 RO GET 的裁定），并携带 C-iOS-2 的设备证书身份，
    /// 这样对隧道主机的预览取数也能通过 mTLS（否则缩略图会被 nginx 拒握手）。
    /// 身份经 provider 每次握手时按需从 keychain 载入（MEDIUM 无需重启修复：
    /// 中途导入的证书下次取数即生效；缺证=nil，对本地主机无副作用）。
    static func live() -> SessionThumbnailPipeline {
        let identityStore = KeychainClientIdentityStore()
        let transport = URLSessionHTTPTransport(identityProvider: {
            identityStore.loadedIdentityOrNil()
        })
        return SessionThumbnailPipeline(
            loader: { request in
                try await APIClient(endpoint: request.endpoint, http: transport)
                    .preview(id: request.sessionId)
            },
            renderer: { data, cols, rows in
                SessionThumbnailRenderer.render(data: data, cols: cols, rows: rows)
            }
        )
    }

    /// 取（或渲染）一张缩略图。永不抛错：任何失败显式降级为 `.placeholder`
    /// （占位图就是缩略图的错误 UI；细节进内部日志，绝不静默无痕）。
    func thumbnail(for request: SessionThumbnailRequest) async -> SessionThumbnailImage {
        let key = request.key
        if let hit = cache.value(for: key) {
            cache = cache.bumping(key)
            return hit
        }
        if let running = inFlight[key] {
            return await running.value
        }
        let task = Task { await produce(request) }
        inFlight = inFlight.merging([key: task]) { _, new in new }
        let result = await task.value
        inFlight = inFlight.filter { $0.key != key }
        cache = cache.inserting(result, for: key)
        return result
    }

    private func produce(_ request: SessionThumbnailRequest) async -> SessionThumbnailImage {
        await gate.acquire()
        defer { gate.release() }
        let preview: SessionPreview
        do {
            preview = try await loader(request)
        } catch {
            logger.debug("preview fetch failed: \(String(describing: error), privacy: .public)")
            return .placeholder
        }
        guard Self.isAcceptable(preview, for: request),
              let grid = Self.clampedGeometry(cols: preview.cols, rows: preview.rows) else {
            logger.debug("preview rejected at boundary (id/bytes/geometry)")
            return .placeholder
        }
        guard let image = renderer(preview.data, grid.cols, grid.rows) else {
            logger.debug("offscreen snapshot returned nil")
            return .placeholder
        }
        return .rendered(image)
    }

    // MARK: - 不可信边界校验（data 本身只喂 SwiftTerm，绝不内容处理）

    /// id 必须回显请求的会话；字节数必须在防御上限内。
    static func isAcceptable(
        _ preview: SessionPreview, for request: SessionThumbnailRequest
    ) -> Bool {
        preview.id == request.sessionId
            && preview.data.utf8.count <= SessionThumbnailTunable.maxPreviewDataBytes
    }

    /// 几何校验 + 钳制：出服务器 resize 契约（`WireConstants.resizeRange`，
    /// 1...1000）→ nil（拒渲染）；合法值钳到缩略图网格界（min 镜像 web
    /// `max(2,·)`，max 钳位图/绘制成本）。
    static func clampedGeometry(cols: Int, rows: Int) -> (cols: Int, rows: Int)? {
        guard Validation.isValidResize(cols: cols, rows: rows) else { return nil }
        let clampedCols = min(
            max(cols, SessionThumbnailTunable.minRenderGrid),
            SessionThumbnailTunable.maxRenderCols
        )
        let clampedRows = min(
            max(rows, SessionThumbnailTunable.minRenderGrid),
            SessionThumbnailTunable.maxRenderRows
        )
        return (clampedCols, clampedRows)
    }
}

// MARK: - 列表行槽位（SwiftUI）

/// 会话列表行里的缩略图槽。`.task(id: key)` 驱动：lastOutputAt 变化自动换键
/// 重取；缓存命中时立即出图。占位态（加载中 / 失败 / 会话已无预览）统一为
/// 终端图标卡片——对一张缩略图，占位就是全部错误 UI。
struct SessionThumbnailView: View {
    let request: SessionThumbnailRequest
    let pipeline: SessionThumbnailPipeline
    @State private var image: SessionThumbnailImage?
    @Environment(\.accessibilityReduceMotion) private var reduceMotion

    /// 媒体尺寸：宽度锁死在 `SessionThumbnailRenderer.snapshotTargetWidth`（=2×88）
    /// 的一半，快照 2x 后像素密度足够；非设计 token（缩略图固有尺寸）。
    private enum Metrics {
        static let width: CGFloat = 88
        static let height: CGFloat = 56
    }

    var body: some View {
        ZStack {
            placeholder
            if let snapshot = image?.uiImage {
                Image(uiImage: snapshot)
                    .resizable()
                    .aspectRatio(contentMode: .fill)
                    .transition(.opacity)
            }
        }
        .frame(width: Metrics.width, height: Metrics.height)
        .clipShape(RoundedRectangle(cornerRadius: DS.Radius.sm8))
        .overlay(
            RoundedRectangle(cornerRadius: DS.Radius.sm8)
                .strokeBorder(DS.Palette.hairline, lineWidth: DS.Stroke.hairline)
        )
        // 快照淡入（遵守 Reduce Motion，塌成即时切换）。
        .animation(DS.Motion.gated(DS.Motion.base, reduceMotion: reduceMotion), value: image)
        .accessibilityLabel(SessionThumbnailCopy.thumbnailLabel)
        .task(id: request.key) {
            image = await pipeline.thumbnail(for: request)
        }
    }

    /// 加载中 / 失败 / 无预览的占位：卡片底 + 发丝边（overlay 提供）+ 终端图标。
    /// 用 DS 卡片色，不再是硬编码黑块（direction: "not a raw black rect"）。
    private var placeholder: some View {
        ZStack {
            DS.Palette.card
            Image(systemName: "terminal")
                .imageScale(.large)
                .foregroundStyle(DS.Palette.textTertiary)
        }
    }
}

/// 用户可见文案（中文具名常量）。
enum SessionThumbnailCopy {
    static let thumbnailLabel = "会话画面缩略图"
}

private enum SessionThumbnailLog {
    static let subsystem = "com.yaojia.webterm"
    static let category = "session-thumbnail"
}
