import Foundation
import UIKit
import ImageIO

enum FileVaultError: Error {
    case readFailed
    case writeFailed
    case removeFailed
    case decodeFailed
}

/// `@unchecked Sendable`:rootURL 是 let,方法只 I/O 到沙盒目录(线程安全),
/// 可被任意 actor / Task 跨边界访问。实例方法显式 `nonisolated`,见 ModelStore 同款注释。
/// 类级 `nonisolated`:工程开了 `SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor`,默认会把本类
/// 连同 `thumbnailCache`(非 Sendable 的 NSCache)推成 MainActor,令 nonisolated I/O 方法 / 单例
/// 初始化访问不了;本类是纯文件 I/O + 缓存工具,必须脱离 MainActor 供任意 actor 调用,故整类标 nonisolated。
nonisolated final class FileVault: @unchecked Sendable {
    nonisolated static let shared: FileVault = {
        do {
            let appSupport = try FileManager.default.url(
                for: .applicationSupportDirectory,
                in: .userDomainMask,
                appropriateFor: nil,
                create: true
            )
            let vaultURL = appSupport.appendingPathComponent("Vault", isDirectory: true)
            return try FileVault(rootURL: vaultURL)
        } catch {
            fatalError("FileVault.shared init failed: \(error)")
        }
    }()

    let rootURL: URL

    /// 已降采样图片的内存缓存。NSCache 线程安全、内存吃紧时系统自动回收;
    /// key = "相对路径@目标像素",避免 TabView 翻页 / 列表滚动反复读盘解码同一张图。
    /// 只缓存降采样后的小图(几百 KB),不缓存全分辨率原图。
    /// `nonisolated(unsafe)`:本工程默认 MainActor 隔离,非 Sendable 的 NSCache 存储属性即便整类标
    /// nonisolated 仍被推成 MainActor,令各 nonisolated I/O 方法访问不到;NSCache 本身线程安全,故 unsafe 豁免。
    private nonisolated(unsafe) let thumbnailCache: NSCache<NSString, UIImage> = {
        let cache = NSCache<NSString, UIImage>()
        cache.countLimit = 40
        // 关键:按「解码后真实字节数」限制总量,而非只数张数。否则 40 张全屏 2000px 大图
        // (各 ~12-16MB)就能占数百 MB 把 App 推过内存上限;混在同一缓存里的小缩略图也会被错误计权。
        cache.totalCostLimit = 96 * 1024 * 1024   // 96MB
        return cache
    }()

    init(rootURL: URL) throws {
        self.rootURL = rootURL
        try FileManager.default.createDirectory(
            at: rootURL,
            withIntermediateDirectories: true,
            attributes: [.protectionKey: FileProtectionType.complete]
        )
    }

    struct SavedAsset {
        let relativePath: String
        let bytes: Int
    }

    // MARK: - Path Safety

    nonisolated private func resolveSafePath(_ relativePath: String) throws -> URL {
        guard !relativePath.contains("/"),
              !relativePath.contains(".."),
              !relativePath.isEmpty else {
            throw FileVaultError.readFailed
        }
        let url = rootURL.appendingPathComponent(relativePath)
        guard url.path.hasPrefix(rootURL.path) else {
            throw FileVaultError.readFailed
        }
        return url
    }

    // MARK: - Public API

    nonisolated func writeJPEG(_ image: UIImage, quality: CGFloat = 0.85) throws -> SavedAsset {
        guard let data = image.jpegData(compressionQuality: quality) else {
            throw FileVaultError.writeFailed
        }
        let filename = "\(UUID().uuidString).jpg"
        let url = rootURL.appendingPathComponent(filename)
        try data.write(to: url, options: [.atomic, .completeFileProtection])
        return SavedAsset(relativePath: filename, bytes: data.count)
    }

    nonisolated func loadImage(relativePath: String) throws -> UIImage {
        let url = try resolveSafePath(relativePath)
        let data: Data
        do {
            data = try Data(contentsOf: url)
        } catch {
            throw FileVaultError.readFailed
        }
        guard let image = UIImage(data: data) else { throw FileVaultError.decodeFailed }
        return image
    }

    /// 按目标最大边降采样加载。用 ImageIO 直接解出缩略图,**绝不**把全分辨率位图载进内存:
    /// 一张 4000×3000 体检照全量解码是 ~48MB RGBA,翻几页就 jetsam;降到 ≤2000px 后仅几 MB。
    /// 自动尊重 EXIF 方向。结果按尺寸缓存,翻页/滚动回看命中缓存不再读盘。
    /// 失败(锁屏读不到 / 损坏)抛 readFailed,与 loadImage 一致,UI 显示占位即可。
    nonisolated func loadDownsampledImage(relativePath: String, maxPixelSize: CGFloat) throws -> UIImage {
        let cacheKey = "\(relativePath)@\(Int(maxPixelSize))" as NSString
        if let cached = thumbnailCache.object(forKey: cacheKey) { return cached }

        let url = try resolveSafePath(relativePath)
        let srcOptions: [CFString: Any] = [kCGImageSourceShouldCache: false]
        guard let src = CGImageSourceCreateWithURL(url as CFURL, srcOptions as CFDictionary) else {
            throw FileVaultError.readFailed
        }
        let thumbOptions: [CFString: Any] = [
            kCGImageSourceCreateThumbnailFromImageAlways: true,
            kCGImageSourceCreateThumbnailWithTransform: true,   // 应用 EXIF 旋转,免得横竖颠倒
            kCGImageSourceShouldCacheImmediately: true,          // 在后台线程就解码,不拖到主线程绘制时
            kCGImageSourceThumbnailMaxPixelSize: maxPixelSize
        ]
        guard let cg = CGImageSourceCreateThumbnailAtIndex(src, 0, thumbOptions as CFDictionary) else {
            throw FileVaultError.decodeFailed
        }
        let image = UIImage(cgImage: cg)
        // cost = 解码后位图字节数,让 NSCache 按真实内存占用淘汰(大图少留、小图多留)。
        let cost = cg.bytesPerRow * cg.height
        thumbnailCache.setObject(image, forKey: cacheKey, cost: cost)
        return image
    }

    nonisolated func remove(relativePath: String) throws {
        let url = try resolveSafePath(relativePath)
        do {
            try FileManager.default.removeItem(at: url)
        } catch {
            throw FileVaultError.removeFailed
        }
        // 删文件后清掉降采样缓存,避免详情页仍显示已删原图(删除次数极少,整清无虞)。
        thumbnailCache.removeAllObjects()
    }

    /// 清空 Vault 全部文件。单个文件删除失败(被占用/权限)不中断,继续删其余;
    /// 最后复查仍有残留才抛错 ——「永久删除 / 全清」语义下不能因一个文件卡住而留下隐私残留。
    nonisolated func wipe() throws {
        let fm = FileManager.default
        let contents = (try? fm.contentsOfDirectory(at: rootURL, includingPropertiesForKeys: nil)) ?? []
        for url in contents {
            try? fm.removeItem(at: url)
        }
        let remaining = (try? fm.contentsOfDirectory(at: rootURL, includingPropertiesForKeys: nil)) ?? []
        thumbnailCache.removeAllObjects()
        if !remaining.isEmpty {
            throw FileVaultError.removeFailed
        }
    }
}
