import Foundation

struct RemoteCourseSummary: Codable, Identifiable, Hashable {
    var id: String
    var title: String
    var mode: String
    var updatedAt: String
    var byteSize: Int64?
    var itemCount: Int?
}

enum PublishClient {
    static func normalizeBaseURL(_ raw: String) -> URL? {
        var s = raw.trimmingCharacters(in: .whitespacesAndNewlines)
        if s.isEmpty { return nil }
        if !s.contains("://") {
            s = "http://\(s)"
        }
        while s.hasSuffix("/") { s.removeLast() }
        return URL(string: s)
    }

    static func fetchCourses(baseURL: URL) async throws -> [RemoteCourseSummary] {
        let url = baseURL.appendingPathComponent("api/courses")
        var request = URLRequest(url: url)
        request.timeoutInterval = 8
        do {
            let (data, response) = try await URLSession.shared.data(for: request)
            guard let http = response as? HTTPURLResponse, (200..<300).contains(http.statusCode) else {
                throw CoursePackError.network("请先在电脑打开分享")
            }
            return try JSONDecoder().decode([RemoteCourseSummary].self, from: data)
        } catch let err as CoursePackError {
            throw err
        } catch {
            throw CoursePackError.network("请先在电脑打开分享（无法连接）")
        }
    }

    /// Downloads a course pack zip. `progress` reports `0…1` as bytes arrive.
    static func downloadPack(
        baseURL: URL,
        courseId: String,
        expectedBytes: Int64? = nil,
        progress: (@Sendable (Double) -> Void)? = nil
    ) async throws -> URL {
        let encoded = courseId.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? courseId
        let url = baseURL.appendingPathComponent("api/courses/\(encoded)/pack")
        return try await PackDownloader.download(from: url, expectedBytes: expectedBytes, progress: progress)
    }
}

// MARK: - Download with byte progress

private final class PackDownloader: NSObject, URLSessionDownloadDelegate, @unchecked Sendable {
    private let continuation: CheckedContinuation<URL, Error>
    private let expectedBytes: Int64?
    private let onProgress: (@Sendable (Double) -> Void)?
    private var movedURL: URL?
    private var moveError: Error?
    private var settled = false

    private init(
        continuation: CheckedContinuation<URL, Error>,
        expectedBytes: Int64?,
        onProgress: (@Sendable (Double) -> Void)?
    ) {
        self.continuation = continuation
        self.expectedBytes = expectedBytes
        self.onProgress = onProgress
    }

    static func download(
        from url: URL,
        expectedBytes: Int64?,
        progress: (@Sendable (Double) -> Void)?
    ) async throws -> URL {
        try await withCheckedThrowingContinuation { continuation in
            let relay = PackDownloader(
                continuation: continuation,
                expectedBytes: expectedBytes,
                onProgress: progress
            )
            let session = URLSession(
                configuration: .ephemeral,
                delegate: relay,
                delegateQueue: nil
            )
            // URLSession retains the delegate until invalidated.
            progress?(0)
            session.downloadTask(with: url).resume()
        }
    }

    private func reportProgress(written: Int64, expected: Int64) {
        let total: Int64
        if expected > 0 {
            total = expected
        } else if let fallback = expectedBytes, fallback > 0 {
            total = fallback
        } else {
            return
        }
        let fraction = min(1, max(0, Double(written) / Double(total)))
        onProgress?(fraction)
    }

    private func finish(_ result: Result<URL, Error>, session: URLSession) {
        guard !settled else { return }
        settled = true
        session.finishTasksAndInvalidate()
        switch result {
        case .success(let url):
            onProgress?(1)
            continuation.resume(returning: url)
        case .failure(let error):
            continuation.resume(throwing: error)
        }
    }

    func urlSession(
        _ session: URLSession,
        downloadTask: URLSessionDownloadTask,
        didWriteData _: Int64,
        totalBytesWritten: Int64,
        totalBytesExpectedToWrite: Int64
    ) {
        reportProgress(written: totalBytesWritten, expected: totalBytesExpectedToWrite)
    }

    func urlSession(
        _ session: URLSession,
        downloadTask: URLSessionDownloadTask,
        didFinishDownloadingTo location: URL
    ) {
        let dest = FileManager.default.temporaryDirectory
            .appendingPathComponent("course-\(UUID().uuidString).zip")
        do {
            if FileManager.default.fileExists(atPath: dest.path) {
                try FileManager.default.removeItem(at: dest)
            }
            try FileManager.default.moveItem(at: location, to: dest)
            movedURL = dest
        } catch {
            moveError = error
        }
    }

    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        if let error {
            finish(.failure(CoursePackError.network("下载失败：\(error.localizedDescription)")), session: session)
            return
        }
        if let moveError {
            finish(.failure(moveError), session: session)
            return
        }
        guard let http = task.response as? HTTPURLResponse, (200..<300).contains(http.statusCode) else {
            finish(.failure(CoursePackError.network("下载失败（课程可能缺少媒体）")), session: session)
            return
        }
        guard let movedURL else {
            finish(.failure(CoursePackError.network("下载失败")), session: session)
            return
        }
        finish(.success(movedURL), session: session)
    }
}
