import Foundation
import os
import Observation

// MARK: - TransportError

enum TransportError: LocalizedError {
    case noTransportServer
    case uploadFailed(statusCode: Int)
    case downloadFailed(statusCode: Int)
    case invalidResponse
    case missingTag

    var errorDescription: String? {
        switch self {
        case .noTransportServer: return "Transport server URL not set"
        case .uploadFailed(let code): return "Upload failed (HTTP \(code))"
        case .downloadFailed(let code): return "Download failed (HTTP \(code))"
        case .invalidResponse: return "Invalid response from transport server"
        case .missingTag: return "Upload response missing file tag"
        }
    }
}

// MARK: - TransportLimiter

/// Limits concurrent CDN upload/download operations to prevent I/O storms
/// when user scrolls through media-heavy chats. Without this, unbounded
/// Task.detached can spawn 50+ concurrent transfers → CPU spike → phone overheating.
private actor TransportLimiter {
    static let shared = TransportLimiter()
    private let maxConcurrent = 4
    private var running = 0
    private var waiters: [CheckedContinuation<Void, Never>] = []

    func acquire() async {
        if running < maxConcurrent {
            running += 1
            return
        }
        await withCheckedContinuation { continuation in
            waiters.append(continuation)
        }
    }

    func release() {
        if let next = waiters.first {
            waiters.removeFirst()
            next.resume()
        } else {
            running -= 1
        }
    }
}

// MARK: - UploadProgressDelegate

/// Inline delegate for tracking URLSession upload byte progress.
private final class UploadProgressDelegate: NSObject, URLSessionTaskDelegate, @unchecked Sendable {
    let onProgress: @MainActor (Double) -> Void

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

    func urlSession(
        _ session: URLSession,
        task: URLSessionTask,
        didSendBodyData bytesSent: Int64,
        totalBytesSent: Int64,
        totalBytesExpectedToSend: Int64
    ) {
        guard totalBytesExpectedToSend > 0 else { return }
        let fraction = Double(totalBytesSent) / Double(totalBytesExpectedToSend)
        Task { @MainActor in self.onProgress(min(fraction, 1.0)) }
    }
}

// MARK: - TransportManager

/// Manages file upload/download to the transport server.
/// Desktop parity: `TransportProvider.tsx`.
///
/// Flow:
/// 1. After handshake, client sends `PacketRequestTransport` (0x0F)
/// 2. Server responds with transport server URL
/// 3. Upload: `POST {url}/u` with multipart form data → returns `{"t": "tag"}`
/// 4. Download: `GET {url}/d/{tag}` → returns file content
@Observable
final class TransportManager: @unchecked Sendable {

    static let shared = TransportManager()

    private static let logger = Logger(subsystem: "com.rosetta.messenger", category: "Transport")

    /// Transport server URL received from server via PacketRequestTransport (0x0F).
    private(set) var transportServer: String?

    /// Upload progress per message ID (0.0–1.0). Cells read this to show real upload progress.
    /// Cleared when upload completes or fails.
    @MainActor static var uploadProgress: [String: Double] = [:]

    /// Notification posted on MainActor when upload progress changes. Object = messageId (String).
    static let uploadProgressNotification = Notification.Name("TransportManager.uploadProgress")

    private let session: URLSession

    private init() {
        let config = URLSessionConfiguration.default
        config.timeoutIntervalForRequest = 60
        config.timeoutIntervalForResource = 300
        session = URLSession(configuration: config)
    }

    // MARK: - Configuration

    /// Called when PacketRequestTransport (0x0F) response arrives.
    @MainActor
    func setTransportServer(_ url: String) {
        let trimmed = url.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !trimmed.isEmpty else { return }
        transportServer = trimmed
        Self.logger.info("Transport server set: \(trimmed)")
    }

    /// Resets transport server (on disconnect).
    @MainActor
    func reset() {
        transportServer = nil
    }

    // MARK: - Upload

    /// Uploads file content to the transport server.
    /// Desktop parity: `usePrepareAttachment.ts` `uploadFile()`.
    ///
    /// - Parameters:
    ///   - id: Unique file identifier (used as filename in multipart).
    ///   - content: Raw file content to upload.
    /// - Returns: `(tag, server)` — server-assigned tag and the transport server URL used.
    /// Android parity: retry with exponential backoff (1s, 2s, 4s) on upload failure.
    private static let maxUploadRetries = 3

    func uploadFile(id: String, content: Data) async throws -> (tag: String, server: String) {
        try await uploadFile(id: id, content: content, onProgress: nil)
    }

    /// Upload with optional progress reporting.
    /// Progress callback fires on MainActor with values 0.0–1.0.
    func uploadFile(
        id: String,
        content: Data,
        onProgress: (@MainActor (Double) -> Void)?
    ) async throws -> (tag: String, server: String) {
        await TransportLimiter.shared.acquire()
        defer { Task { await TransportLimiter.shared.release() } }

        guard let serverUrl = await MainActor.run(body: { transportServer }) else {
            throw TransportError.noTransportServer
        }

        guard let url = URL(string: "\(serverUrl)/u") else {
            throw TransportError.noTransportServer
        }

        Self.logger.info("Uploading file \(id) (\(content.count) bytes) to \(serverUrl)/u")

        let boundary = "Boundary-\(UUID().uuidString)"
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")

        // Build multipart body (matches desktop: FormData.append('file', Blob, id))
        var body = Data()
        body.append("--\(boundary)\r\n".data(using: .utf8)!)
        body.append("Content-Disposition: form-data; name=\"file\"; filename=\"\(id)\"\r\n".data(using: .utf8)!)
        body.append("Content-Type: application/octet-stream\r\n\r\n".data(using: .utf8)!)
        body.append(content)
        body.append("\r\n--\(boundary)--\r\n".data(using: .utf8)!)

        let delegate = onProgress.map { UploadProgressDelegate(onProgress: $0) }

        var lastError: Error = TransportError.invalidResponse
        for attempt in 0..<Self.maxUploadRetries {
            do {
                let (data, response): (Data, URLResponse)
                if let delegate {
                    (data, response) = try await session.upload(for: request, from: body, delegate: delegate)
                } else {
                    request.httpBody = body
                    (data, response) = try await session.data(for: request)
                }

                guard let httpResponse = response as? HTTPURLResponse else {
                    throw TransportError.invalidResponse
                }

                guard httpResponse.statusCode == 200 else {
                    Self.logger.error("Upload failed: HTTP \(httpResponse.statusCode)")
                    throw TransportError.uploadFailed(statusCode: httpResponse.statusCode)
                }

                // Parse JSON response: {"t": "tag"}
                guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
                      let tag = json["t"] as? String else {
                    throw TransportError.missingTag
                }

                if let onProgress { await onProgress(1.0) }
                Self.logger.info("Upload complete: id=\(id), tag=\(tag), server=\(serverUrl)")
                return (tag: tag, server: serverUrl)
            } catch {
                lastError = error
                if attempt < Self.maxUploadRetries - 1 {
                    let delayMs = 1000 * (1 << attempt) // 1s, 2s, 4s
                    Self.logger.warning("Upload retry \(attempt + 1)/\(Self.maxUploadRetries) for \(id) in \(delayMs)ms: \(error.localizedDescription)")
                    try? await Task.sleep(nanoseconds: UInt64(delayMs) * 1_000_000)
                }
            }
        }
        throw lastError
    }

    // MARK: - Download

    /// Downloads file content from a transport server.
    /// Desktop parity: `useAttachment.ts` `downloadFile(id, tag, server)`.
    /// Android parity: retry with exponential backoff (1s, 2s, 4s) on download failure.
    ///
    /// - Parameters:
    ///   - tag: Server-assigned file tag from upload response.
    ///   - server: Per-attachment transport server URL. Falls back to global transport if empty/nil.
    /// - Returns: Raw file content.
    private static let maxDownloadRetries = 3

    func downloadFile(tag: String, server: String? = nil) async throws -> Data {
        try await downloadFile(tag: tag, server: server, onProgress: nil)
    }

    /// Downloads file with optional progress reporting.
    /// Progress callback fires on MainActor with values 0.0–1.0.
    func downloadFile(
        tag: String,
        server: String? = nil,
        onProgress: (@MainActor (Double) -> Void)?
    ) async throws -> Data {
        await TransportLimiter.shared.acquire()
        defer { Task { await TransportLimiter.shared.release() } }

        let serverUrl: String
        if let explicit = server, !explicit.isEmpty {
            serverUrl = explicit
        } else {
            guard let global = await MainActor.run(body: { transportServer }) else {
                throw TransportError.noTransportServer
            }
            serverUrl = global
        }

        guard let url = URL(string: "\(serverUrl)/d/\(tag)") else {
            throw TransportError.noTransportServer
        }

        Self.logger.info("Downloading file tag=\(tag) from \(serverUrl)/d/\(tag)")

        let request = URLRequest(url: url)
        var lastError: Error = TransportError.invalidResponse
        for attempt in 0..<Self.maxDownloadRetries {
            do {
                if let onProgress {
                    // Stream bytes for progress reporting
                    let (asyncBytes, response) = try await session.bytes(for: request)

                    guard let httpResponse = response as? HTTPURLResponse else {
                        throw TransportError.invalidResponse
                    }
                    guard httpResponse.statusCode == 200 else {
                        throw TransportError.downloadFailed(statusCode: httpResponse.statusCode)
                    }

                    let expectedLength = httpResponse.expectedContentLength
                    var data = Data()
                    if expectedLength > 0 { data.reserveCapacity(Int(expectedLength)) }
                    var lastReportedKB = 0

                    for try await byte in asyncBytes {
                        data.append(byte)
                        let currentKB = data.count / 4096
                        if expectedLength > 0, currentKB > lastReportedKB {
                            lastReportedKB = currentKB
                            let fraction = Double(data.count) / Double(expectedLength)
                            await onProgress(min(fraction, 1.0))
                        }
                    }
                    await onProgress(1.0)
                    Self.logger.info("Download complete: tag=\(tag), \(data.count) bytes")
                    return data
                } else {
                    // Fast path — no progress needed
                    let (data, response) = try await session.data(for: request)

                    guard let httpResponse = response as? HTTPURLResponse else {
                        throw TransportError.invalidResponse
                    }
                    guard httpResponse.statusCode == 200 else {
                        Self.logger.error("Download failed: HTTP \(httpResponse.statusCode)")
                        throw TransportError.downloadFailed(statusCode: httpResponse.statusCode)
                    }

                    Self.logger.info("Download complete: tag=\(tag), \(data.count) bytes")
                    return data
                }
            } catch {
                lastError = error
                if attempt < Self.maxDownloadRetries - 1 {
                    let delayMs = 1000 * (1 << attempt) // 1s, 2s, 4s
                    Self.logger.warning("Download retry \(attempt + 1)/\(Self.maxDownloadRetries) for tag=\(tag) in \(delayMs)ms: \(error.localizedDescription)")
                    try? await Task.sleep(nanoseconds: UInt64(delayMs) * 1_000_000)
                }
            }
        }
        throw lastError
    }
}
