import Foundation

nonisolated enum ModelKind: String, CaseIterable {
    /// 也是沙盒 Models/ 下的子目录名 / CDN 路径段。
    /// 同一个 Qwen3.5-2B,两种格式两种引擎:
    /// - mnnLLM:MNN(CPU/SME2,考核路径)文本+视觉一肩挑,taobao-mnn 预转换。iPhone17+(A19/SME2)主用,只露它。
    /// - llm:MLX(GPU)兜底,Qwen3.5-2B-4bit 多模态(同时兜底文本与视觉,走 qwen3_5)。
    /// - vl:已废弃(MLX VL 改走 .llm 多模态),保留枚举避免动一圈穷举 switch,不再下载/展示。
    case llm    = "Qwen3.5-2B-4bit"
    case vl     = "Qwen3-VL-4B-Instruct-4bit"
    case mnnLLM = "Qwen3.5-2B-MNN"

    var displayName: String {
        switch self {
        case .llm:    return "Qwen3.5-2B (MLX)"
        case .vl:     return "Qwen3-VL-4B"
        case .mnnLLM: return "Qwen3.5-2B (MNN/SME2)"
        }
    }

    /// HuggingFace 仓库 ID(org/name),用于下载
    var huggingFaceRepo: String { "mlx-community/\(rawValue)" }

    /// 用于判定该模型是否已就绪的最小标志文件
    var sentinelFilename: String { "config.json" }

    /// 面向用户的模型集合:模型管理页 / 下载全部 / 就绪计数对外只暴露统一的
    /// Qwen3.5-2B(MNN,文本+视觉全包,iPhone17+ 走它)。
    /// MLX 的 .llm/.vl 仅作模拟器与兜底路径,保留枚举与下载能力(旁路导入仍可单独导),
    /// 但不在「我的 · 模型管理」展示,也不计入「下载全部」与就绪计数。
    static let userFacing: [ModelKind] = [.mnnLLM]
}

/// `@unchecked Sendable`:rootURL 是 let,方法只读 filesystem(线程安全),
/// 可被任意 actor / Task 跨边界访问。
/// 实例方法显式 `nonisolated`:项目开了 `SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor`,
/// 默认会把方法推到 MainActor,导致从 `AIRuntime` 等 actor 调用报错。
final class ModelStore: @unchecked Sendable {
    nonisolated static let shared: ModelStore = {
        do {
            let appSupport = try FileManager.default.url(
                for: .applicationSupportDirectory,
                in: .userDomainMask,
                appropriateFor: nil,
                create: true
            )
            let root = appSupport.appendingPathComponent("Models", isDirectory: true)
            return try ModelStore(rootURL: root)
        } catch {
            fatalError("ModelStore.shared init failed: \(error)")
        }
    }()

    let rootURL: URL

    init(rootURL: URL) throws {
        self.rootURL = rootURL
        try FileManager.default.createDirectory(at: rootURL, withIntermediateDirectories: true)
    }

    nonisolated func localURL(for kind: ModelKind) -> URL {
        rootURL.appendingPathComponent(kind.rawValue, isDirectory: true)
    }

    nonisolated func isReady(_ kind: ModelKind) -> Bool {
        let sentinel = localURL(for: kind).appendingPathComponent(kind.sentinelFilename)
        return FileManager.default.fileExists(atPath: sentinel.path)
    }

    nonisolated func totalBytes(for kind: ModelKind) -> Int {
        let folder = localURL(for: kind)
        guard let enumerator = FileManager.default.enumerator(
            at: folder,
            includingPropertiesForKeys: [.fileSizeKey]
        ) else { return 0 }
        var sum = 0
        for case let url as URL in enumerator {
            if let size = try? url.resourceValues(forKeys: [.fileSizeKey]).fileSize {
                sum += size
            }
        }
        return sum
    }

    /// Demo 现场旁路:从 Bundle 拷贝预装模型(W6 才真正使用,本周占位)
    nonisolated func seedFromBundle(_ kind: ModelKind) throws {
        guard let bundleURL = Bundle.main.url(forResource: kind.rawValue, withExtension: nil) else {
            #if DEBUG
            assertionFailure("Bundle 缺少 \(kind.rawValue),检查资源是否加入 target")
            #endif
            return
        }
        let target = localURL(for: kind)
        if FileManager.default.fileExists(atPath: target.path) {
            try FileManager.default.removeItem(at: target)
        }
        try FileManager.default.copyItem(at: bundleURL, to: target)
    }

    // MARK: - 下载 / 导入支撑

    /// 模型目录下某个相对路径文件的本地 URL。
    nonisolated func fileURL(for kind: ModelKind, relativePath: String) -> URL {
        localURL(for: kind).appendingPathComponent(relativePath)
    }

    /// 本地该文件当前字节数,不存在返回 0(用于断点续传偏移与跳过判断)。
    nonisolated func localBytes(for kind: ModelKind, relativePath: String) -> Int {
        let url = fileURL(for: kind, relativePath: relativePath)
        guard let size = try? url.resourceValues(forKeys: [.fileSizeKey]).fileSize else { return 0 }
        return size
    }

    /// 按清单校验模型是否完整:每个文件都存在且大小等于预期。
    /// `files` 默认取 `ModelManifest`;测试可注入小清单。
    nonisolated func isComplete(for kind: ModelKind, files: [ModelFile]? = nil) -> Bool {
        let manifest = files ?? ModelManifest.files(for: kind)
        guard !manifest.isEmpty else { return false }
        for file in manifest where localBytes(for: kind, relativePath: file.path) != file.bytes {
            return false
        }
        return true
    }

    /// 旁路导入:把一个含 config.json 的模型文件夹整体拷入沙盒(现场重装兜底)。
    nonisolated func importModel(_ kind: ModelKind, from sourceFolder: URL) throws {
        let configPath = sourceFolder.appendingPathComponent(kind.sentinelFilename).path
        guard FileManager.default.fileExists(atPath: configPath) else {
            throw ModelStoreError.missingConfig
        }
        let target = localURL(for: kind)
        if FileManager.default.fileExists(atPath: target.path) {
            try FileManager.default.removeItem(at: target)
        }
        try FileManager.default.createDirectory(
            at: target.deletingLastPathComponent(), withIntermediateDirectories: true)
        try FileManager.default.copyItem(at: sourceFolder, to: target)
    }
}

enum ModelStoreError: Error, LocalizedError {
    case missingConfig

    var errorDescription: String? {
        switch self {
        case .missingConfig:
            return String(appLoc: "所选文件夹缺少 config.json,不是有效的模型目录")
        }
    }
}
