import Alamofire
import Foundation
import LilithAgentCore
import LilithLogging
import MacSyncShared // ContentTypeMapping, ActivityLog, macSyncResolveServerURL
import SwiftyJSON

private let log = AppLogger.logger(for: "IMessage.BlobSync")

// MARK: - Response types

private struct MissingAttachmentItem {
    let externalId: String
    let filename: String?
    let transferName: String?
    let mimeType: String?
    let size: Int?
}

// MARK: - BlobSyncManager

/// Uploads raw attachment blobs for iMessage attachments that were synced metadata-only.
///
/// Runs after initial message sync completes. Fetches the list of missing blobs from the
/// server in pages, locates each file on disk via its filename/transferName path, and
/// PUTs the raw bytes to the server using multipart upload.
final class BlobSyncManager: @unchecked Sendable {

    private let apiClient: APIClient
    private let activityLog: ActivityLog
    private let pageSize = 200

    init(apiClient: APIClient, activityLog: ActivityLog) {
        self.apiClient = apiClient
        self.activityLog = activityLog
    }

    // MARK: - Public API

    func syncBlobs() async {
        log.info("BlobSyncManager: starting blob sync pass")
        await MainActor.run { activityLog.info("Syncing attachment blobs...") }

        var totalUploaded = 0
        var totalSkipped = 0
        var totalFailed = 0
        var totalQueued = 0
        var page = 0

        while true {
            let items: [MissingAttachmentItem]
            do {
                items = try await fetchMissingPage()
            } catch {
                log.warning("BlobSyncManager: failed to fetch missing list: \(error)")
                await MainActor.run { activityLog.warning("Blob sync: failed to fetch missing list — \(error.localizedDescription)") }
                break
            }

            if items.isEmpty {
                log.info("BlobSyncManager: no more missing blobs (page=\(page))")
                break
            }

            page += 1
            log.info("BlobSyncManager: page \(page), uploading \(items.count) blobs")

            for item in items {
                guard let (data, contentType) = readAttachmentFile(item: item, queuedCount: &totalQueued) else {
                    totalSkipped += 1
                    continue
                }

                do {
                    let storageKind = try await uploadBlob(externalId: item.externalId, data: data, contentType: contentType)
                    if storageKind == "missing" {
                        totalSkipped += 1
                        log.info("BlobSyncManager: oversized (>25MB), kept missing: \(item.externalId)")
                    } else {
                        totalUploaded += 1
                    }
                } catch {
                    totalFailed += 1
                    log.warning("BlobSyncManager: upload failed for \(item.externalId): \(error)")
                }
            }

            if items.count < pageSize { break }
        }

        let queuedNote = totalQueued > 0 ? ", \(totalQueued) queued for iCloud download" : ""
        let summary = "Blob sync complete: \(totalUploaded) uploaded, \(totalSkipped) skipped, \(totalFailed) failed\(queuedNote)"
        log.info("BlobSyncManager: \(summary)")
        await MainActor.run { activityLog.success(summary) }
    }

    // MARK: - Private helpers

    private func fetchMissingPage() async throws -> [MissingAttachmentItem] {
        let data = try await apiClient.authenticatedRequest(
            "/client/imessage/attachments/missing?limit=\(pageSize)",
            method: .get
        )
        let json = JSON(data)
        guard json["success"].boolValue else {
            throw APIError.serverError(statusCode: 0, message: json["error"]["message"].stringValue)
        }
        return json["data"]["items"].arrayValue.map { item in
            MissingAttachmentItem(
                externalId: item["externalId"].stringValue,
                filename: item["filename"].string,
                transferName: item["transferName"].string,
                mimeType: item["mimeType"].string,
                size: item["size"].int
            )
        }
    }

    private func readAttachmentFile(item: MissingAttachmentItem, queuedCount: inout Int) -> (Data, String)? {
        let candidates: [String?] = [item.filename, item.transferName]
        let homeDir = FileManager.default.homeDirectoryForCurrentUser.path
        let fm = FileManager.default

        for candidate in candidates.compactMap({ $0 }) {
            guard ContentTypeMapping.isAllowedAttachmentPath(candidate, homeDirectory: homeDir) else {
                log.warning("BlobSyncManager: path outside allowed directory, skipping: \(candidate)")
                continue
            }

            let expandedPath = candidate.hasPrefix("~/")
                ? homeDir + candidate.dropFirst(1)
                : candidate
            let fileURL = URL(fileURLWithPath: expandedPath)

            if fm.fileExists(atPath: expandedPath) {
                do {
                    let fileData = try Data(contentsOf: fileURL)
                    let contentType = item.mimeType ?? mimeTypeForPath(expandedPath)
                    return (fileData, contentType)
                } catch {
                    log.warning("BlobSyncManager: could not read \(expandedPath): \(error)")
                }
            } else if let icloudURL = icloudPlaceholderURL(for: fileURL), fm.fileExists(atPath: icloudURL.path) {
                // File is offloaded to iCloud — queue download so next sync pass can upload it
                do {
                    try fm.startDownloadingUbiquitousItem(at: fileURL)
                    queuedCount += 1
                    log.info("BlobSyncManager: queued iCloud download for \(expandedPath)")
                } catch {
                    log.warning("BlobSyncManager: could not queue iCloud download for \(expandedPath): \(error)")
                }
            }
        }

        log.info("BlobSyncManager: attachment file not found on disk: \(item.externalId)")
        return nil
    }

    /// Returns the `.filename.ext.icloud` placeholder URL for a given file URL, or nil if not applicable.
    private func icloudPlaceholderURL(for fileURL: URL) -> URL? {
        let dir = fileURL.deletingLastPathComponent()
        let name = fileURL.lastPathComponent
        let placeholder = dir.appendingPathComponent(".\(name).icloud")
        return placeholder
    }

    private func uploadBlob(externalId: String, data: Data, contentType: String) async throws -> String {
        let encoded = externalId.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? externalId
        let path = "/client/imessage/attachments/\(encoded)/blob"

        let responseData = try await apiClient.authenticatedMultipartUpload(path) { form in
            form.append(data, withName: "blob", fileName: "blob", mimeType: contentType)
        }

        let json = JSON(responseData)
        guard json["success"].boolValue else {
            throw APIError.serverError(statusCode: 0, message: json["error"]["message"].stringValue)
        }
        return json["data"]["storageKind"].stringValue
    }

    private func mimeTypeForPath(_ path: String) -> String {
        let ext = (path as NSString).pathExtension.lowercased()
        return ContentTypeMapping.contentType(forExtension: ext)
    }
}
