import Foundation

enum DownloadError: Error, LocalizedError {
    case badStatus(Int)
    case sizeMismatch(expected: Int, got: Int)

    var errorDescription: String? {
        switch self {
        case .badStatus(let code):
            return String(appLoc: "下载失败(HTTP \(code))")
        case .sizeMismatch(let expected, let got):
            return String(appLoc: "文件大小校验失败(预期 \(expected),实际 \(got))")
        }
    }
}

/// 下载单个文件,支持 HTTP Range 断点续传 + 完成后大小校验。
/// 用 `URLSessionDataDelegate` 把响应体分块写入 `.part`,完成后原子改名为成品。
///
/// 注意:文件大小一律用 `FileManager.attributesOfItem` 读取,**不用**
/// `URL.resourceValues(.fileSizeKey)` —— 后者会把结果缓存在 URL 实例上,
/// 续传时先读 offset 再读 finalSize 会拿到下载前的陈旧大小,导致误判校验失败。
///
/// 一个实例一次处理一个文件(串行)。共享状态用锁保证可见性。
final class FileDownloader: NSObject, URLSessionDataDelegate, @unchecked Sendable {
    private let configuration: URLSessionConfiguration

    private let lock = NSLock()
    private var handle: FileHandle?
    private var written: Int = 0
    private var onProgress: ((Int) -> Void)?
    private var responseError: Error?
    private var continuation: CheckedContinuation<Void, Error>?

    init(configuration: URLSessionConfiguration = .default) {
        self.configuration = configuration
        super.init()
    }

    /// 不走 URL 资源值缓存的文件大小读取。
    static func fileSize(at url: URL) -> Int {
        guard let attrs = try? FileManager.default.attributesOfItem(atPath: url.path),
              let size = attrs[.size] as? Int else { return 0 }
        return size
    }

    /// 从 `url` 下载到 `destination`。若存在 `destination.part` 则发 Range 请求续传;
    /// 完成后校验总大小 == `expectedBytes`,通过则原子改名为 `destination`。
    nonisolated func download(
        from url: URL,
        to destination: URL,
        expectedBytes: Int,
        onProgress: (@Sendable (Int) -> Void)? = nil
    ) async throws {
        let fm = FileManager.default
        let part = destination.appendingPathExtension("part")

        // 成品已存在且大小正确 → 跳过
        if Self.fileSize(at: destination) == expectedBytes,
           fm.fileExists(atPath: destination.path) {
            return
        }

        try fm.createDirectory(
            at: destination.deletingLastPathComponent(), withIntermediateDirectories: true)

        var offset = 0
        if fm.fileExists(atPath: part.path) {
            offset = Self.fileSize(at: part)
        } else {
            fm.createFile(atPath: part.path, contents: nil)
        }

        let fileHandle = try FileHandle(forWritingTo: part)
        try fileHandle.seekToEnd()

        lock.withLock {
            self.handle = fileHandle
            self.written = offset
            self.onProgress = onProgress
            self.responseError = nil
        }

        var request = URLRequest(url: url)
        if offset > 0 {
            request.setValue("bytes=\(offset)-", forHTTPHeaderField: "Range")
        }

        let session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
        defer { session.finishTasksAndInvalidate() }

        // 句柄在 didCompleteWithError 内关闭(同一 delegate 队列,串行于 didReceive)。
        try await withCheckedThrowingContinuation { (cont: CheckedContinuation<Void, Error>) in
            lock.lock()
            self.continuation = cont
            lock.unlock()
            session.dataTask(with: request).resume()
        }

        let finalSize = Self.fileSize(at: part)
        guard finalSize == expectedBytes else {
            try? fm.removeItem(at: part)
            throw DownloadError.sizeMismatch(expected: expectedBytes, got: finalSize)
        }

        if fm.fileExists(atPath: destination.path) {
            try fm.removeItem(at: destination)
        }
        try fm.moveItem(at: part, to: destination)
    }

    // MARK: - URLSessionDataDelegate (全部在串行 delegate 队列执行)

    nonisolated func urlSession(
        _ session: URLSession, dataTask: URLSessionDataTask,
        didReceive response: URLResponse,
        completionHandler: @escaping (URLSession.ResponseDisposition) -> Void
    ) {
        if let http = response as? HTTPURLResponse, http.statusCode >= 400 {
            lock.lock(); responseError = DownloadError.badStatus(http.statusCode); lock.unlock()
            completionHandler(.cancel)
        } else {
            completionHandler(.allow)
        }
    }

    nonisolated func urlSession(
        _ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data
    ) {
        lock.lock()
        try? handle?.write(contentsOf: data)
        written += data.count
        let progress = written
        let callback = onProgress
        lock.unlock()
        callback?(progress)
    }

    nonisolated func urlSession(
        _ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?
    ) {
        lock.lock()
        try? handle?.close()
        handle = nil
        let cont = continuation
        continuation = nil
        let respErr = responseError
        lock.unlock()

        if let respErr {
            cont?.resume(throwing: respErr)
        } else if let error {
            cont?.resume(throwing: error)
        } else {
            cont?.resume()
        }
    }
}
