//
//  LobbyZipUpgrader.swift
//  ylgamehall
//
//  H5 zip 升级：下载远端 game_zip → 临时目录解压 → 原子 rename 到 lobbyRoot。
//  详见 docs/Development-Plan.md ADR-008-G / Design §6.3.4 / msext NewRootVC.m:1789-1869。
//

import Foundation
import ZIPFoundation

public actor LobbyZipUpgrader {

    public static let shared = LobbyZipUpgrader()

    public enum Outcome: Sendable {
        /// 本地版本已是最新（含远端 gameZip 为空 / 版本号未提升的情况），未做任何下载
        case noop
        /// 完成升级。`from → to` 是版本号变化。
        case upgraded(from: Int, to: Int)
    }

    public enum UpgradeError: Error, Sendable {
        case badGameZipURL(String)
        case downloadFailed(any Error)
        case httpStatus(Int)
        case unzipFailed(any Error)
        case stagingMoveFailed(any Error)
    }

    public init() {}

    /// URLSession 配置：60s 单请求 / 300s 总体（zip 约 11 MB，4G 约 10s）。
    /// 每次 upgradeIfNeeded 用此 config 临时建 session，绑 download delegate 接收
    /// didWriteData 进度回调；ad-hoc delegate（`session.download(from:delegate:)`）
    /// 不会触发 URLSessionDownloadDelegate 的 download-specific 方法，必须走 session 级。
    nonisolated private static var defaultConfig: URLSessionConfiguration {
        let cfg = URLSessionConfiguration.default
        cfg.timeoutIntervalForRequest = 60
        cfg.timeoutIntervalForResource = 300
        return cfg
    }

    /// 比较远端 game_version 与本地 version.xml。需要升级则下载 + 解压 + 原子 rename。
    ///
    /// - Parameters:
    ///   - resolved: VersionResolver.resolve(...) 的结果
    ///   - onProgress: 下载进度回调 0.0…1.0，由 URLSession delegate 在后台线程派发；
    ///     UI 层负责把闭包 hop 到 MainActor。默认 no-op 兼容仅做版本对比的烟雾测试。
    /// - Returns: `.noop` 已是最新；`.upgraded(from:to:)` 完成升级
    public func upgradeIfNeeded(
        resolved: ResolvedVersion,
        onProgress: @escaping @Sendable (Double) -> Void = { _ in }
    ) async throws -> Outcome {
        let localGameVersion = LocalVersionReader.localGameVersion
        guard resolved.gameVersion > localGameVersion,
              let zipURLString = resolved.gameZip,
              let zipURL = URL(string: zipURLString)
        else {
            return .noop
        }

        // 1. 下载到 tmp（URLSession 默认管理临时位置），delegate 上报 0…1 进度。
        //    delegate 必须在 URLSession init 时绑定，否则 didWriteData 不会被调用
        //    （async API `download(from:delegate:)` 的 ad-hoc delegate 只收 task-level 回调）。
        let progressDelegate = DownloadProgressDelegate(onProgress: onProgress)
        let opQueue = OperationQueue()
        opQueue.maxConcurrentOperationCount = 1
        let session = URLSession(
            configuration: Self.defaultConfig,
            delegate: progressDelegate,
            delegateQueue: opQueue
        )
        defer { session.invalidateAndCancel() }

        let downloadedURL: URL
        let response: URLResponse
        do {
            (downloadedURL, response) = try await session.download(from: zipURL)
        } catch {
            throw UpgradeError.downloadFailed(error)
        }
        if let http = response as? HTTPURLResponse,
           !(200..<300).contains(http.statusCode) {
            throw UpgradeError.httpStatus(http.statusCode)
        }
        // 下载完成兜底报 1.0：部分 server 不发 Content-Length 或末尾片段晚到，
        // 不显式补这一下，进度条可能停在 99% 直到解压结束才跳走
        onProgress(1.0)

        // 2. 解压到隔离 staging 目录
        let staging = SandboxPaths.caches
            .appendingPathComponent("lobby-staging-\(UUID().uuidString)")
        let fm = FileManager.default
        do {
            try fm.createDirectory(at: staging, withIntermediateDirectories: true)
            try fm.unzipItem(at: downloadedURL, to: staging)
        } catch {
            try? fm.removeItem(at: staging)
            throw UpgradeError.unzipFailed(error)
        }
        try? fm.removeItem(at: downloadedURL)

        // 3. 原子 rename：删旧 lobbyRoot → 把 staging 移到位
        //    （msext "removeItem + ZipArchive overWrite:YES" 半途崩溃留半残；
        //     我们 staging 完整解压后再 rename，半途崩溃只留 staging，下次启动可清）
        let lobbyRoot = SandboxPaths.lobbyRoot
        do {
            if fm.fileExists(atPath: lobbyRoot.path) {
                try fm.removeItem(at: lobbyRoot)
            }
            // 父目录确保存在（Library/Caches 永远存在，无需创建中间路径）
            try fm.moveItem(at: staging, to: lobbyRoot)
        } catch {
            try? fm.removeItem(at: staging)
            throw UpgradeError.stagingMoveFailed(error)
        }

        return .upgraded(from: localGameVersion, to: resolved.gameVersion)
    }
}

/// URLSession 下载进度桥接：把 `didWriteData` 的 byte 计数换算成 0…1，回调上层。
///
/// 之所以放在文件内私有：仅 LobbyZipUpgrader 用，不构成对外 API。
/// `@unchecked Sendable`：URLSessionDownloadDelegate 协议本身不带 Sendable；
/// 我们的状态仅是一个 `@Sendable` 闭包，无可变共享，标记安全。
private final class DownloadProgressDelegate: NSObject,
                                              URLSessionDownloadDelegate,
                                              @unchecked Sendable {
    private let onProgress: @Sendable (Double) -> Void

    init(onProgress: @escaping @Sendable (Double) -> Void) {
        self.onProgress = onProgress
        super.init()
    }

    func urlSession(_ session: URLSession,
                    downloadTask: URLSessionDownloadTask,
                    didWriteData bytesWritten: Int64,
                    totalBytesWritten: Int64,
                    totalBytesExpectedToWrite: Int64) {
        guard totalBytesExpectedToWrite > 0 else { return }
        let p = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)
        onProgress(min(max(p, 0), 1))
    }

    /// 必须实现的协议方法。async download API 内部已接管下载文件落地，
    /// 这里无需把文件搬到任何位置。
    func urlSession(_ session: URLSession,
                    downloadTask: URLSessionDownloadTask,
                    didFinishDownloadingTo location: URL) {
        // no-op
    }
}
