import Foundation
import ComposeApp

/// Three-step direct-to-B2 image upload.
///
/// Flow:
///   1. POST /api/uploads/presign → server returns a B2 POST policy + form
///      fields scoped to a single object key with a content-length-range
///      condition that B2 enforces at the protocol level.
///   2. Multipart POST the bytes directly to B2, no API server in the data
///      path. B2 rejects the upload if the bytes don't match the policy.
///   3. Caller passes the returned `uploadId` to /api/task-completions/ or
///      /api/documents/ via `upload_ids[]`. The server HEADs the object,
///      confirms the size, and creates the linked entity rows.
///
/// All errors map to `PresignedUploaderError` — the Swift call site can
/// translate to user-facing copy without parsing nested HTTP details.
enum PresignedUploaderError: Error, LocalizedError {
    case notAuthenticated
    case presignFailed(status: Int, body: String)
    case uploadFailed(status: Int, body: String)
    case sessionError(Error)

    var errorDescription: String? {
        switch self {
        case .notAuthenticated:
            return "You're not signed in."
        case .presignFailed(let status, _):
            switch status {
            case 413: return "That photo is too large after resizing. Try a different one."
            case 422: return "That image format isn't supported."
            case 429: return "You're uploading too many photos. Try again in a few minutes."
            default:  return "Couldn't start upload (server returned \(status))."
            }
        case .uploadFailed(let status, _):
            return "Upload failed (B2 returned \(status))."
        case .sessionError(let err):
            return err.localizedDescription
        }
    }
}

/// Category passed to the presign endpoint. Matches the Go server's
/// `UploadCategory` constants in `internal/models/pending_upload.go`.
enum UploadCategory: String {
    case completion = "completion"
    case documentImage = "document_image"
    case documentFile = "document_file"
}

/// Presigned-URL upload helper. Stateless — instantiate freely.
///
/// Concurrency: each `upload(...)` call runs to completion sequentially.
/// For multiple images the caller can run several uploads in parallel via
/// `withTaskGroup`; the server's per-user concurrency cap (10 in-flight
/// presigns) is enforced server-side.
final class PresignedUploader {

    /// API base URL — read from KMP's ApiConfig so iOS and Android stay
    /// in sync (LOCAL vs DEV vs PROD without divergent constants).
    private let apiBaseURL: String

    /// Bearer token. Read once at init; if the user re-auths mid-session,
    /// the caller should construct a fresh PresignedUploader.
    private let authToken: String

    private let session: URLSession

    init?(session: URLSession = .shared) {
        // ApiConfig.shared.getBaseUrl() resolves Environment (LOCAL/DEV/PROD).
        // DataManager.shared.authToken is a StateFlow<String?> — read the
        // current value via .value (SKIE-exposed property).
        let baseUrl = ApiConfig.shared.getBaseUrl()
        guard let token = DataManager.shared.authToken.value as String? else {
            return nil
        }
        self.apiBaseURL = baseUrl
        self.authToken = token
        self.session = session
    }

    /// Upload `data` to B2 in the named category. Returns the
    /// pending_uploads.id the caller passes via `upload_ids[]` to attach
    /// the object to a real entity.
    func upload(
        data: Data,
        category: UploadCategory,
        contentType: String = "image/jpeg",
        fileName: String = "image.jpg"
    ) async throws -> Int32 {
        // Step 1: presign
        let presigned = try await requestPresign(
            category: category,
            contentType: contentType,
            contentLength: Int64(data.count)
        )

        // Step 2: direct POST to B2
        try await postToStorage(
            uploadURL: presigned.uploadUrl,
            fields: presigned.fields,
            data: data,
            contentType: contentType,
            fileName: fileName
        )

        return Int32(presigned.id)
    }

    /// Upload several images in parallel, returning their upload_ids in
    /// input order. Stops at the first failure and surfaces it.
    func uploadAll(
        items: [(Data, String)],
        category: UploadCategory,
        contentType: String = "image/jpeg"
    ) async throws -> [Int32] {
        try await withThrowingTaskGroup(of: (Int, Int32).self) { group in
            for (idx, item) in items.enumerated() {
                let (data, name) = item
                group.addTask { [self] in
                    let id = try await upload(
                        data: data,
                        category: category,
                        contentType: contentType,
                        fileName: name
                    )
                    return (idx, id)
                }
            }
            var pairs: [(Int, Int32)] = []
            for try await pair in group {
                pairs.append(pair)
            }
            return pairs.sorted { $0.0 < $1.0 }.map { $0.1 }
        }
    }

    // MARK: - Step 1: presign

    private struct PresignBody: Encodable {
        let category: String
        let content_type: String
        let content_length: Int64
    }

    private struct PresignResponse: Decodable {
        let id: Int
        let upload_url: String
        let fields: [String: String]
        let key: String
        let expires_at: String

        // Map snake_case to nicer Swift names at the call site.
        var uploadUrl: String { upload_url }
    }

    private func requestPresign(
        category: UploadCategory,
        contentType: String,
        contentLength: Int64
    ) async throws -> PresignResponse {
        guard var url = URL(string: apiBaseURL) else {
            throw PresignedUploaderError.presignFailed(status: 0, body: "invalid base url")
        }
        url.appendPathComponent("uploads/presign/")

        var req = URLRequest(url: url)
        req.httpMethod = "POST"
        req.setValue("application/json", forHTTPHeaderField: "Content-Type")
        req.setValue("Token \(authToken)", forHTTPHeaderField: "Authorization")
        req.httpBody = try JSONEncoder().encode(PresignBody(
            category: category.rawValue,
            content_type: contentType,
            content_length: contentLength
        ))

        let (body, response): (Data, URLResponse)
        do {
            (body, response) = try await session.data(for: req)
        } catch {
            throw PresignedUploaderError.sessionError(error)
        }
        guard let http = response as? HTTPURLResponse else {
            throw PresignedUploaderError.presignFailed(status: 0, body: "no response")
        }
        guard (200..<300).contains(http.statusCode) else {
            throw PresignedUploaderError.presignFailed(
                status: http.statusCode,
                body: String(data: body, encoding: .utf8) ?? ""
            )
        }
        do {
            return try JSONDecoder().decode(PresignResponse.self, from: body)
        } catch {
            throw PresignedUploaderError.presignFailed(status: http.statusCode, body: "decode failed: \(error)")
        }
    }

    // MARK: - Step 2: POST to B2

    private func postToStorage(
        uploadURL: String,
        fields: [String: String],
        data: Data,
        contentType: String,
        fileName: String
    ) async throws {
        guard let url = URL(string: uploadURL) else {
            throw PresignedUploaderError.uploadFailed(status: 0, body: "invalid upload url")
        }

        // Build a multipart/form-data body with all policy fields followed
        // by a single "file" part (S3 POST policy mandates the file part
        // come last).
        let boundary = "Boundary-\(UUID().uuidString)"
        var body = Data()
        let crlf = "\r\n"
        let appendString: (String) -> Void = { s in
            body.append(s.data(using: .utf8) ?? Data())
        }

        // Stable order: ensure "key" and "Content-Type" appear before the
        // file part so the policy signature validates. Unspecified order
        // for the rest — S3 accepts any.
        let orderedKeys = ["key", "Content-Type", "policy", "x-amz-algorithm",
                           "x-amz-credential", "x-amz-date", "x-amz-signature",
                           "x-amz-meta-uid"]
        var emitted = Set<String>()
        for k in orderedKeys {
            if let v = fields[k] {
                appendString("--\(boundary)\(crlf)")
                appendString("Content-Disposition: form-data; name=\"\(k)\"\(crlf)\(crlf)")
                appendString(v)
                appendString(crlf)
                emitted.insert(k)
            }
        }
        for (k, v) in fields where !emitted.contains(k) {
            appendString("--\(boundary)\(crlf)")
            appendString("Content-Disposition: form-data; name=\"\(k)\"\(crlf)\(crlf)")
            appendString(v)
            appendString(crlf)
        }

        // file part — must be last
        appendString("--\(boundary)\(crlf)")
        appendString("Content-Disposition: form-data; name=\"file\"; filename=\"\(fileName)\"\(crlf)")
        appendString("Content-Type: \(contentType)\(crlf)\(crlf)")
        body.append(data)
        appendString(crlf)
        appendString("--\(boundary)--\(crlf)")

        var req = URLRequest(url: url)
        req.httpMethod = "POST"
        req.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
        req.httpBody = body

        let (respBody, response): (Data, URLResponse)
        do {
            (respBody, response) = try await session.data(for: req)
        } catch {
            throw PresignedUploaderError.sessionError(error)
        }
        guard let http = response as? HTTPURLResponse else {
            throw PresignedUploaderError.uploadFailed(status: 0, body: "no response")
        }
        guard (200..<300).contains(http.statusCode) else {
            throw PresignedUploaderError.uploadFailed(
                status: http.statusCode,
                body: String(data: respBody, encoding: .utf8) ?? ""
            )
        }
    }
}
