import Foundation
import Observation
import UIKit

/// Manages avatar image storage on disk with in-memory cache.
///
/// Android parity: `AvatarFileManager.kt` encrypts avatars with password "rosetta-a".
/// Desktop parity: `AvatarProvider.tsx` uses `AVATAR_PASSWORD_TO_ENCODE = "rosetta-a"`.
///
/// Storage: `Application Support/Rosetta/Avatars/{normalizedKey}.enc`
@Observable
@MainActor
final class AvatarRepository {

    static let shared = AvatarRepository()
    private init() {}

    /// Android/Desktop parity: fixed password for avatar encryption at rest.
    private static let avatarPassword = "rosetta-a"
    /// Shared container used by Notification Service Extension.
    private static let appGroupID = "group.com.rosetta.dev"

    /// Incremented on every avatar save/remove — views that read this property
    /// will re-render and pick up the latest avatar from cache.
    private(set) var avatarVersion: UInt = 0

    /// In-memory cache for decoded UIImages — keyed by normalized public key.
    /// Bounded: 50MB / 200 images max to prevent memory pressure on low-end devices.
    private let cache: NSCache<NSString, UIImage> = {
        let c = NSCache<NSString, UIImage>()
        c.totalCostLimit = 50 * 1024 * 1024  // 50MB
        c.countLimit = 200
        return c
    }()

    private let compressionQuality: CGFloat = 0.8

    // MARK: - Public API

    /// Saves an avatar image for the given public key.
    /// Android parity: encrypts with "rosetta-a" password before writing to disk.
    func saveAvatar(publicKey: String, image: UIImage) {
        let key = normalizedKey(publicKey)
        guard let jpegData = image.jpegData(compressionQuality: compressionQuality) else { return }
        let url = avatarURL(for: key)
        ensureStorageDirectoriesExist()

        // Encrypt with "rosetta-a" (Android/Desktop parity)
        if let encrypted = try? CryptoManager.shared.encryptWithPassword(jpegData, password: Self.avatarPassword),
           let encData = encrypted.data(using: .utf8) {
            try? encData.write(to: url, options: [.atomic, .completeFileProtectionUntilFirstUserAuthentication])
        } else {
            // Fallback: plaintext (should never happen)
            try? jpegData.write(to: url, options: [.atomic, .completeFileProtectionUntilFirstUserAuthentication])
        }
        cache.setObject(image, forKey: key as NSString, cost: jpegData.count)
        syncAvatarToNotificationStoreIfNeeded(image, normalizedKey: key)
        avatarVersion += 1
    }

    /// Saves an avatar from a base64-encoded image string (used when receiving from network).
    func saveAvatarFromBase64(_ base64: String, publicKey: String) {
        let rawBase64: String
        if base64.hasPrefix("data:") {
            rawBase64 = String(base64.drop(while: { $0 != "," }).dropFirst())
        } else {
            rawBase64 = base64
        }
        guard let data = Data(base64Encoded: rawBase64),
              let image = UIImage(data: data) else { return }
        saveAvatar(publicKey: publicKey, image: image)
    }

    /// Loads avatar for the given public key.
    /// NOTE: This does synchronous disk I/O + crypto decrypt on cache miss.
    /// For hot paths (cell configure, layoutSubviews), use `cachedAvatar()` + `loadAvatarAsync()`.
    func loadAvatar(publicKey: String) -> UIImage? {
        if let systemAvatar = systemAccountAvatar(for: publicKey) {
            return systemAvatar
        }
        let key = normalizedKey(publicKey)
        if let cached = cache.object(forKey: key as NSString) {
            // PERF: Don't sync to notification store on cache hit — sync only
            // happens on disk load (below). Cache hits fire 50+/sec during scroll.
            return cached
        }
        return loadFromDiskSync(normalizedKey: key, publicKey: publicKey)
    }

    /// Cache-only avatar lookup — O(1), no disk I/O, no crypto.
    /// Use this in hot paths (cell configure, layoutSubviews) paired with `loadAvatarAsync()`.
    func cachedAvatar(publicKey: String) -> UIImage? {
        if let systemAvatar = systemAccountAvatar(for: publicKey) {
            return systemAvatar
        }
        let key = normalizedKey(publicKey)
        return cache.object(forKey: key as NSString)
    }

    /// Keys currently being loaded from disk — prevents duplicate background loads.
    private var pendingAsyncLoads: Set<String> = []

    /// Async avatar load: disk I/O + crypto decrypt on background queue.
    /// Completion called on MainActor with the decoded image (or nil).
    /// Deduplicates: concurrent calls for the same key are coalesced.
    func loadAvatarAsync(publicKey: String, completion: @escaping (UIImage?) -> Void) {
        if systemAccountAvatar(for: publicKey) != nil { return }
        let key = normalizedKey(publicKey)
        if cache.object(forKey: key as NSString) != nil { return } // Already cached
        guard !pendingAsyncLoads.contains(key) else { return } // Already loading
        pendingAsyncLoads.insert(key)

        let url = avatarURL(for: key)
        let password = Self.avatarPassword
        DispatchQueue.global(qos: .utility).async { [weak self] in
            let image = Self.decryptAvatarFromDisk(url: url, password: password)
            DispatchQueue.main.async {
                guard let self else { return }
                self.pendingAsyncLoads.remove(key)
                if let image {
                    self.cache.setObject(image, forKey: key as NSString,
                                         cost: Int(image.size.width * image.size.height * 4))
                    self.syncAvatarToNotificationStoreIfNeeded(image, normalizedKey: key)
                }
                completion(image)
            }
        }
    }

    /// Disk load + decrypt (runs on ANY thread — no MainActor dependency).
    private nonisolated static func decryptAvatarFromDisk(url: URL, password: String) -> UIImage? {
        guard let fileData = try? Data(contentsOf: url) else { return nil }
        // Try encrypted format
        if let encryptedString = String(data: fileData, encoding: .utf8),
           let decrypted = try? CryptoManager.shared.decryptWithPassword(encryptedString, password: password),
           let image = UIImage(data: decrypted) {
            return image
        }
        // Fallback: plaintext JPEG (legacy)
        return UIImage(data: fileData)
    }

    /// Synchronous disk load — used by non-hot-path callers (loadAvatar).
    private func loadFromDiskSync(normalizedKey key: String, publicKey: String) -> UIImage? {
        let url = avatarURL(for: key)
        guard let fileData = try? Data(contentsOf: url) else { return nil }

        // Try decrypting (new encrypted format)
        if let encryptedString = String(data: fileData, encoding: .utf8),
           let decrypted = try? CryptoManager.shared.decryptWithPassword(encryptedString, password: Self.avatarPassword),
           let image = UIImage(data: decrypted) {
            cache.setObject(image, forKey: key as NSString, cost: decrypted.count)
            syncAvatarToNotificationStoreIfNeeded(image, normalizedKey: key)
            return image
        }

        // Fallback: try as plaintext JPEG (legacy unencrypted files)
        if let image = UIImage(data: fileData) {
            cache.setObject(image, forKey: key as NSString, cost: fileData.count)
            // Re-save encrypted for next time
            saveAvatar(publicKey: publicKey, image: image)
            return image
        }

        return nil
    }

    private func systemAccountAvatar(for publicKey: String) -> UIImage? {
        if publicKey == SystemAccounts.safePublicKey {
            return UIImage(named: "safe-avatar")
        }
        if publicKey == SystemAccounts.updatesPublicKey {
            return UIImage(named: "updates-avatar")
        }
        return nil
    }

    func loadAvatarBase64(publicKey: String) -> String? {
        guard let image = loadAvatar(publicKey: publicKey),
              let data = image.jpegData(compressionQuality: compressionQuality) else {
            return nil
        }
        return data.base64EncodedString()
    }

    func removeAvatar(publicKey: String) {
        let key = normalizedKey(publicKey)
        cache.removeObject(forKey: key as NSString)
        let url = avatarURL(for: key)
        try? FileManager.default.removeItem(at: url)
        if let notificationURL = notificationAvatarURL(for: key) {
            try? FileManager.default.removeItem(at: notificationURL)
        }
        avatarVersion += 1
    }

    func clearCache() {
        cache.removeAllObjects()
    }

    func clearAll() {
        cache.removeAllObjects()
        if let directory = avatarsDirectory {
            try? FileManager.default.removeItem(at: directory)
        }
        if let directory = notificationAvatarsDirectory {
            try? FileManager.default.removeItem(at: directory)
        }
        avatarVersion += 1
    }

    // MARK: - Private

    private var avatarsDirectory: URL? {
        FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)
            .first?
            .appendingPathComponent("Rosetta/Avatars", isDirectory: true)
    }

    private var notificationAvatarsDirectory: URL? {
        FileManager.default
            .containerURL(forSecurityApplicationGroupIdentifier: Self.appGroupID)?
            .appendingPathComponent("NotificationAvatars", isDirectory: true)
    }

    private func avatarURL(for normalizedKey: String) -> URL {
        avatarsDirectory!
            .appendingPathComponent("\(normalizedKey).enc")
    }

    private func notificationAvatarURL(for normalizedKey: String) -> URL? {
        notificationAvatarsDirectory?
            .appendingPathComponent("\(normalizedKey).jpg")
    }

    private func normalizedKey(_ publicKey: String) -> String {
        publicKey
            .replacingOccurrences(of: "0x", with: "")
            .lowercased()
    }

    private func ensureStorageDirectoriesExist() {
        ensureDirectoryExists(avatarsDirectory)
        ensureDirectoryExists(notificationAvatarsDirectory)
    }

    private func ensureDirectoryExists(_ directory: URL?) {
        guard let directory else { return }
        if FileManager.default.fileExists(atPath: directory.path) { return }
        try? FileManager.default.createDirectory(
            at: directory,
            withIntermediateDirectories: true
        )
    }

    /// PERF: Moved to background queue — synchronous JPEG encode + disk write
    /// was blocking main thread during cell configuration.
    /// Only called on disk load (not cache hit), so runs once per avatar per session.
    private func syncAvatarToNotificationStoreIfNeeded(_ image: UIImage, normalizedKey: String) {
        guard let notificationURL = notificationAvatarURL(for: normalizedKey) else { return }
        let dir = notificationAvatarsDirectory
        DispatchQueue.global(qos: .utility).async {
            self.ensureDirectoryExists(dir)
            guard let jpegData = image.jpegData(compressionQuality: 0.72) else { return }
            try? jpegData.write(to: notificationURL, options: [.atomic])
        }
    }
}
