import UIKit
import ImageIO
import os

// MARK: - ImageLoadLimiter

/// Android parity: `ImageLoadSemaphore` in `AttachmentComponents.kt`.
/// Limits concurrent image disk I/O + decode operations to 3,
/// preventing scroll lag when collages (4–5 photos) appear simultaneously.
actor ImageLoadLimiter {
    static let shared = ImageLoadLimiter()
    private let maxConcurrent = 3
    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: - AttachmentCache

/// Local disk cache for downloaded/decrypted attachment images and files.
///
/// Android parity: `AttachmentFileManager.kt` encrypts all files with user's private key.
/// Desktop parity: `readFile("m/...")` and `writeFile(...)` in `DialogProvider.tsx`.
///
/// Cache directory: `Documents/AttachmentCache/`
/// Key format: attachment ID (8-char random string).
final class AttachmentCache: @unchecked Sendable {

    nonisolated static let shared = AttachmentCache()

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

    nonisolated private let cacheDir: URL

    /// In-memory image cache — eliminates disk I/O + crypto on scroll-back.
    /// Android parity: LruCache in AttachmentFileManager.kt.
    /// Same pattern as AvatarRepository.cache (NSCache is thread-safe).
    nonisolated(unsafe) private let imageCache: NSCache<NSString, UIImage> = {
        let cache = NSCache<NSString, UIImage>()
        cache.countLimit = 100
        cache.totalCostLimit = 80 * 1024 * 1024 // 80 MB — auto-evicts under memory pressure
        return cache
    }()

    /// Private key for encrypting files at rest (Android parity).
    /// Set from SessionManager.startSession() after unlocking account.
    nonisolated private let keyLock = NSLock()
    nonisolated(unsafe) private var _privateKey: String?
    nonisolated var privateKey: String? {
        get { keyLock.lock(); defer { keyLock.unlock() }; return _privateKey }
        set {
            keyLock.lock()
            _privateKey = newValue
            keyLock.unlock()
            if newValue == nil { imageCache.removeAllObjects() }
        }
    }

    private init() {
        let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
        cacheDir = docs.appendingPathComponent("AttachmentCache", isDirectory: true)
        try? FileManager.default.createDirectory(at: cacheDir, withIntermediateDirectories: true)
    }

    // MARK: - Downsampled Decode

    /// Android parity: `BitmapFactory.Options.inSampleSize` with max 4096px.
    /// Uses `CGImageSource` for memory-efficient downsampled decoding + EXIF orientation.
    /// `kCGImageSourceShouldCacheImmediately` forces decode now (not lazily on first draw).
    nonisolated static func downsampledImage(from data: Data, maxPixelSize: Int = 4096) -> UIImage? {
        guard let source = CGImageSourceCreateWithData(data as CFData, nil) else {
            return UIImage(data: data)
        }
        let options: [CFString: Any] = [
            kCGImageSourceCreateThumbnailFromImageAlways: true,
            kCGImageSourceShouldCacheImmediately: true,
            kCGImageSourceCreateThumbnailWithTransform: true,
            kCGImageSourceThumbnailMaxPixelSize: maxPixelSize
        ]
        guard let cgImage = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary) else {
            return UIImage(data: data)
        }
        return UIImage(cgImage: cgImage)
    }

    // MARK: - Images

    /// Returns an image only if it's already in in-memory cache.
    /// Never touches disk/crypto — safe for hot UI paths (scrolling/layout).
    nonisolated func cachedImage(forAttachmentId id: String) -> UIImage? {
        imageCache.object(forKey: id as NSString)
    }

    /// Saves a decoded image to cache, encrypted with private key (Android parity).
    nonisolated func saveImage(_ image: UIImage, forAttachmentId id: String) {
        guard let data = image.jpegData(compressionQuality: 0.95) else { return }

        // Warm in-memory cache immediately — next loadImage() returns in O(1)
        imageCache.setObject(image, forKey: id as NSString, cost: data.count)

        let url = cacheDir.appendingPathComponent("img_\(id).enc")

        if let key = privateKey,
           let encrypted = try? CryptoManager.shared.encryptWithPassword(data, password: key),
           let encData = encrypted.data(using: .utf8) {
            try? encData.write(to: url, options: [.atomic, .completeFileProtectionUntilFirstUserAuthentication])
        } else {
            // Fallback: write plaintext (no private key available yet)
            let plainUrl = cacheDir.appendingPathComponent("img_\(id).jpg")
            try? data.write(to: plainUrl, options: .atomic)
        }
    }

    /// Loads a cached image for an attachment ID, or `nil` if not cached.
    nonisolated func loadImage(forAttachmentId id: String) -> UIImage? {
        // Fast path: in-memory cache hit — no disk I/O, no crypto
        if let cached = imageCache.object(forKey: id as NSString) {
            return cached
        }

        // Try encrypted format first
        let encUrl = cacheDir.appendingPathComponent("img_\(id).enc")
        if FileManager.default.fileExists(atPath: encUrl.path),
           let fileData = try? Data(contentsOf: encUrl),
           let encString = String(data: fileData, encoding: .utf8),
           let key = privateKey,
           let decrypted = try? CryptoManager.shared.decryptWithPassword(encString, password: key),
           let image = Self.downsampledImage(from: decrypted) {
            imageCache.setObject(image, forKey: id as NSString, cost: decrypted.count)
            return image
        }

        // Fallback: try plaintext (legacy unencrypted files)
        let plainUrl = cacheDir.appendingPathComponent("img_\(id).jpg")
        if FileManager.default.fileExists(atPath: plainUrl.path),
           let image = UIImage(contentsOfFile: plainUrl.path) {
            // Re-save encrypted for next time
            if privateKey != nil {
                saveImage(image, forAttachmentId: id)
                try? FileManager.default.removeItem(at: plainUrl)
            }
            return image
        }

        return nil
    }

    // MARK: - Files

    /// Saves raw file data to cache (encrypted), returns the file URL.
    @discardableResult
    nonisolated func saveFile(_ data: Data, forAttachmentId id: String, fileName: String) -> URL {
        let safeFileName = fileName.replacingOccurrences(of: "/", with: "_")
        let url = cacheDir.appendingPathComponent("file_\(id)_\(safeFileName).enc")

        if let key = privateKey,
           let encrypted = try? CryptoManager.shared.encryptWithPassword(data, password: key),
           let encData = encrypted.data(using: .utf8) {
            try? encData.write(to: url, options: [.atomic, .completeFileProtectionUntilFirstUserAuthentication])
        } else {
            let plainUrl = cacheDir.appendingPathComponent("file_\(id)_\(safeFileName)")
            try? data.write(to: plainUrl, options: .atomic)
            return plainUrl
        }
        return url
    }

    /// Returns cached file URL, or `nil` if not cached.
    nonisolated func fileURL(forAttachmentId id: String, fileName: String) -> URL? {
        let safeFileName = fileName.replacingOccurrences(of: "/", with: "_")
        // Check encrypted
        let encUrl = cacheDir.appendingPathComponent("file_\(id)_\(safeFileName).enc")
        if FileManager.default.fileExists(atPath: encUrl.path) { return encUrl }
        // Check plaintext (legacy)
        let plainUrl = cacheDir.appendingPathComponent("file_\(id)_\(safeFileName)")
        if FileManager.default.fileExists(atPath: plainUrl.path) { return plainUrl }
        return nil
    }

    /// Load file data, decrypting if needed.
    nonisolated func loadFileData(forAttachmentId id: String, fileName: String) -> Data? {
        let safeFileName = fileName.replacingOccurrences(of: "/", with: "_")
        // Try encrypted
        let encUrl = cacheDir.appendingPathComponent("file_\(id)_\(safeFileName).enc")
        if FileManager.default.fileExists(atPath: encUrl.path),
           let fileData = try? Data(contentsOf: encUrl),
           let encString = String(data: fileData, encoding: .utf8),
           let key = privateKey,
           let decrypted = try? CryptoManager.shared.decryptWithPassword(encString, password: key) {
            return decrypted
        }
        // Try plaintext
        let plainUrl = cacheDir.appendingPathComponent("file_\(id)_\(safeFileName)")
        if FileManager.default.fileExists(atPath: plainUrl.path) {
            return try? Data(contentsOf: plainUrl)
        }
        return nil
    }

    // MARK: - Cleanup

    nonisolated func clearAll() {
        imageCache.removeAllObjects()
        try? FileManager.default.removeItem(at: cacheDir)
        try? FileManager.default.createDirectory(at: cacheDir, withIntermediateDirectories: true)
    }
}
