import Foundation

/// manifest 拉取 / 缓存 / 新鲜度管理（进程内，由 Prewarmer 持有）。
///
/// 策略（design.md Decision 5）：
/// - 内存缓存整个 session；TTL 读自响应头 `Cache-Control: max-age`，缺省 300s。
/// - ``ensureFresh(_:)``：缺失则拉取并排队回调（pending replay）；已有但过期则后台重拉
///   （stale-while-revalidate）。
/// - `isFetching` 保护：每 TTL 窗口最多一次重拉。
/// - 拉取失败静默 no-op。
///
/// 回调在后台线程触发；涉及 WKWebView 的动作由调用方切主线程。
final class ManifestStore {

    private var manifest: WarmupManifest?
    private var lastFetchAt: TimeInterval = 0
    private var ttl: TimeInterval = 300
    private var manifestUrl: String = ""
    private var isFetching = false
    private var pending: [(WarmupManifest) -> Void] = []
    private let lock = NSLock()

    /// pending 回调队列上限。弱网下 `prewarm` 可能多次入队，设上限防御异常累积。
    private static let maxPending = 16

    /// version 变化钩子（用于去重重置），由 Prewarmer 注入。
    var onVersionChanged: (() -> Void)?

    func setManifestUrl(_ url: String) {
        lock.lock(); manifestUrl = url; lock.unlock()
    }

    func getManifest() -> WarmupManifest? {
        lock.lock(); defer { lock.unlock() }
        return manifest
    }

    func isStale() -> Bool {
        lock.lock(); defer { lock.unlock() }
        return (Date().timeIntervalSince1970 - lastFetchAt) > ttl
    }

    /// 确保 manifest 可用且新鲜。
    /// - 无缓存：触发拉取，`onReady` 排队等 manifest 到达后回调。
    /// - 有缓存但过期：立即以当前缓存回调 + 后台重拉（不阻塞）。
    /// - 有缓存且新鲜：立即以当前缓存回调。
    func ensureFresh(_ onReady: ((WarmupManifest) -> Void)?) {
        lock.lock()
        let current = manifest
        let stale = (Date().timeIntervalSince1970 - lastFetchAt) > ttl
        if current == nil, let cb = onReady {
            // 上限保护：超限直接丢弃新回调，避免弱网下 pending 无界增长。
            if pending.count < Self.maxPending { pending.append(cb) }
        }
        lock.unlock()

        if let current = current {
            onReady?(current)
            if stale {
                fetch()
            }
            return
        }
        fetch()
    }

    /// 后台拉取（带 isFetching 保护，失败静默）。
    private func fetch() {
        lock.lock()
        let url = manifestUrl
        if url.isEmpty || isFetching {
            lock.unlock()
            return
        }
        isFetching = true
        lock.unlock()

        guard let u = URL(string: url) else {
            lock.lock(); isFetching = false; lock.unlock()
            return
        }
        var request = URLRequest(url: u)
        request.httpMethod = "GET"
        request.setValue("application/json", forHTTPHeaderField: "Accept")
        request.timeoutInterval = 8

        let task = URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
            guard let self = self else { return }
            defer {
                self.lock.lock(); self.isFetching = false; self.lock.unlock()
            }
            guard error == nil,
                  let http = response as? HTTPURLResponse,
                  http.statusCode == 200,
                  let data = data,
                  let parsed = WarmupManifest.parse(data) else {
                // 拉取失败：清空 pending（符合"失败静默不回调"契约），避免回调无界累积。
                // 下次 prewarm 会重新入队重试；dedup 未 markDone 故不会误判已预热。
                self.lock.lock()
                self.pending.removeAll()
                self.lock.unlock()
                return
            }
            let newTtl = ManifestStore.parseMaxAge(HTTPHeaderCompat.value(for: "Cache-Control", in: response)) ?? 300

            var callbacks: [(WarmupManifest) -> Void] = []
            var versionChanged = false
            self.lock.lock()
            let oldVersion = self.manifest?.version ?? ""
            versionChanged = !oldVersion.isEmpty && oldVersion != parsed.version
            self.manifest = parsed
            self.ttl = newTtl
            self.lastFetchAt = Date().timeIntervalSince1970
            callbacks = self.pending
            self.pending.removeAll()
            self.lock.unlock()

            callbacks.forEach { $0(parsed) }
            if versionChanged {
                self.onVersionChanged?()
            }
        }
        task.resume()
    }

    /// 解析 `Cache-Control: max-age=NNN` → 秒；无则返回 nil。
    static func parseMaxAge(_ headerValue: String?) -> TimeInterval? {
        guard let cc = headerValue?.lowercased() else { return nil }
        guard let range = cc.range(of: "max-age=") else { return nil }
        let rest = cc[range.upperBound...]
        let token = rest.split(separator: ",").first.map { String($0) } ?? String(rest)
        let trimmed = token.trimmingCharacters(in: .whitespaces)
        guard let seconds = TimeInterval(trimmed), seconds > 0 else { return nil }
        return seconds
    }
}
