import Foundation

// MARK: - Task Status

enum TaskStatus: String, Codable {
    case pending       = "pending"
    case uploading     = "uploading"
    case retryWaiting  = "retry_waiting"
    case dead          = "dead"
}

// MARK: - SidecarData (Codable — 磁盘 JSON 结构)

/// sidecar JSON 文件的完整数据结构（替代 SQLite 的核心）
///
/// 文件路径：`<Caches>/material_monitor/{style}/{basename}.json`
/// 写入策略：先写 `*.json.tmp`，再 `FileManager.replaceItem` 原子重命名（满足崩溃安全）
struct SidecarData: Codable {
    let schemaVersion: Int          // 当前版本 = 1
    let uploadTaskId: String        // UUID，一级去重键
    let showId: String              // 广告展示唯一 ID，二级去重键
    let placementId: String
    let networkFirmId: Int
    let style: String               // MaterialStyle.rawValue
    let imagePath: String           // 相对 material_monitor/ 根目录的路径
    let imageHash: String           // pHash 16 字符 hex
    let imageUrl: String?           // 原生自渲染 URL
    let metaJson: String            // 首次入队时序列化，重试时直接复用

    var status: String
    var attemptCount: Int
    var nextAttemptAt: Int64        // ms，调度器过滤"等冷却"任务
    var lastErrorCode: Int?
    var lastErrorMsg: String?

    let createdAt: Int64            // ms，LRU 淘汰参考
    var updatedAt: Int64            // ms
}

// MARK: - TaskMeta (内存)

/// sidecar 在内存中的轻量镜像，供调度器快速筛选
struct TaskMeta {
    let uploadTaskId: String
    let showId: String
    let placementId: String
    let networkFirmId: Int
    let style: MaterialStyle
    let imagePath: String
    let imageHash: String
    let imageUrl: String?
    let metaJson: String

    var status: TaskStatus
    var attemptCount: Int
    var nextAttemptAt: Int64

    let createdAt: Int64
    var updatedAt: Int64
}

// MARK: - TaskStore

/// 纯文件持久化队列
///
/// - 所有文件 / 内存索引操作必须在 `ioQueue` 上执行（外部调用方保证）
/// - 内存索引：`[uploadTaskId: TaskMeta]`
/// - show_id 去重集合：`Set<String>`
final class TaskStore {

    // MARK: Queues

    /// IO 串行队列（所有文件操作 + 内存索引修改均在此执行）
    static let ioQueue = DispatchQueue(
        label: "com.materialmonitor.io",
        qos: .utility
    )

    // MARK: Root directory

    private static var rootDirectory: URL = {
        let caches = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
        mmLog("沙盒路径：\(caches)")
        return caches.appendingPathComponent("material_monitor", isDirectory: true)
    }()

    // MARK: Memory Index

    private(set) var tasks: [String: TaskMeta] = [:]       // key = uploadTaskId
    private(set) var showIdSet: Set<String>     = []

    // MARK: Quota

    /// 3001/3002 当日配额耗尽标记；次日 0 点 UTC+8 重置
    private(set) var quotaExhaustedUntilMidnight = false
    private var quotaResetTimer: Timer?

    // MARK: Init

    init() {
        createDirectoriesIfNeeded()
    }

    // MARK: - Cold Start Scan

    /// 冷启扫描：重建内存索引、重置 uploading→pending、清理孤儿/过期文件
    ///
    /// - Parameter completion: 扫描完成后的回调（在 `ioQueue` 上调用）
    func scanOnColdStart(completion: @escaping () -> Void) {
        TaskStore.ioQueue.async { [weak self] in
            self?.performColdStartScan()
            completion()
        }
    }

    private func performColdStartScan() {
        let fm = FileManager.default
        let now = Int64(Date().timeIntervalSince1970 * 1000)
        let sevenDaysMs: Int64 = 7 * 24 * 60 * 60 * 1000

        for style in MaterialStyle.allCases {
            let styleDir = styleDirectory(for: style)
            guard let enumerator = fm.enumerator(at: styleDir,
                includingPropertiesForKeys: nil,
                options: [.skipsSubdirectoryDescendants]) else { continue }

            var jsonFiles:  [URL] = []
            var imageFiles: Set<String> = []

            for case let url as URL in enumerator {
                if url.pathExtension == "json" { jsonFiles.append(url) }
                if url.pathExtension == "jpg"  { imageFiles.insert(url.lastPathComponent) }
            }

            for jsonURL in jsonFiles {
                let baseName = jsonURL.deletingPathExtension().lastPathComponent
                let jpgExists = imageFiles.contains(baseName + ".jpg")

                guard var sidecar = readSidecar(at: jsonURL) else {
                    // 损坏的 JSON，删除
                    try? fm.removeItem(at: jsonURL)
                    continue
                }

                // 孤儿：json 存在但 jpg 不存在
                if !jpgExists {
                    try? fm.removeItem(at: jsonURL)
                    continue
                }

                // dead + 超 7 天：清理
                if sidecar.status == TaskStatus.dead.rawValue,
                   now - sidecar.createdAt > sevenDaysMs {
                    let jpgURL = jsonURL.deletingPathExtension().appendingPathExtension("jpg")
                    try? fm.removeItem(at: jpgURL)
                    try? fm.removeItem(at: jsonURL)
                    continue
                }

                // uploading → pending（进程被杀时未完成）
                if sidecar.status == TaskStatus.uploading.rawValue {
                    sidecar.status    = TaskStatus.pending.rawValue
                    sidecar.updatedAt = now
                    writeSidecar(sidecar, to: jsonURL)
                }

                // 跳过 dead（保留磁盘，不加入内存索引）
                guard sidecar.status != TaskStatus.dead.rawValue else { continue }

                guard let style = MaterialStyle(rawValue: sidecar.style),
                      let status = TaskStatus(rawValue: sidecar.status) else { continue }

                // show_id 去重：保留 createdAt 较新的
                if showIdSet.contains(sidecar.showId) {
                    if let existing = tasks.values.first(where: { $0.showId == sidecar.showId }),
                       existing.createdAt > sidecar.createdAt {
                        // 现有更新，当前是旧的 → 标 dead
                        var updated = sidecar
                        updated.status    = TaskStatus.dead.rawValue
                        updated.updatedAt = now
                        writeSidecar(updated, to: jsonURL)
                        continue
                    }
                }

                let meta = TaskMeta(
                    uploadTaskId: sidecar.uploadTaskId,
                    showId:        sidecar.showId,
                    placementId:   sidecar.placementId,
                    networkFirmId: sidecar.networkFirmId,
                    style:         style,
                    imagePath:     sidecar.imagePath,
                    imageHash:     sidecar.imageHash,
                    imageUrl:      sidecar.imageUrl,
                    metaJson:      sidecar.metaJson,
                    status:        status,
                    attemptCount:  sidecar.attemptCount,
                    nextAttemptAt: sidecar.nextAttemptAt,
                    createdAt:     sidecar.createdAt,
                    updatedAt:     sidecar.updatedAt
                )
                tasks[meta.uploadTaskId] = meta
                showIdSet.insert(meta.showId)
            }

            // 孤儿：jpg 存在但 json 不存在
            guard let jpgEnumerator = fm.enumerator(at: styleDir,
                includingPropertiesForKeys: nil,
                options: [.skipsSubdirectoryDescendants]) else { continue }
            for case let url as URL in jpgEnumerator {
                if url.pathExtension == "jpg" {
                    let jsonURL = url.deletingPathExtension().appendingPathExtension("json")
                    if !fm.fileExists(atPath: jsonURL.path) {
                        try? fm.removeItem(at: url)
                    }
                }
            }
        }
    }

    // MARK: - Enqueue

    /// 保存图片并写 sidecar，加入内存索引
    ///
    /// - Returns: `(absoluteFilePath, relativePath)` 或 `nil`（写入失败）
    func saveImage(_ data: Data, style: MaterialStyle,
                   showTimeMs: Int64, uploadTaskId: String) -> (absolute: String, relative: String)? {
        let styleDir  = styleDirectory(for: style)
        let baseName  = fileName(style: style, showTimeMs: showTimeMs, uploadTaskId: uploadTaskId)
        let jpgURL    = styleDir.appendingPathComponent(baseName + ".jpg")

        // 碰撞检测
        if FileManager.default.fileExists(atPath: jpgURL.path) {
            return nil
        }

        do {
            try data.write(to: jpgURL, options: .atomic)
            setExcludedFromBackup(jpgURL)
            let relative = style.rawValue + "/" + baseName + ".jpg"
            return (jpgURL.path, relative)
        } catch {
            return nil
        }
    }

    /// 入队：写 sidecar JSON，更新内存索引 + show_id 集合
    func enqueue(_ sidecar: SidecarData) {
        let jsonURL = absoluteURL(for: sidecar.imagePath)
            .deletingPathExtension()
            .appendingPathExtension("json")
        writeSidecar(sidecar, to: jsonURL)

        guard let style  = MaterialStyle(rawValue: sidecar.style),
              let status = TaskStatus(rawValue: sidecar.status) else { return }

        let meta = TaskMeta(
            uploadTaskId: sidecar.uploadTaskId,
            showId:        sidecar.showId,
            placementId:   sidecar.placementId,
            networkFirmId: sidecar.networkFirmId,
            style:         style,
            imagePath:     sidecar.imagePath,
            imageHash:     sidecar.imageHash,
            imageUrl:      sidecar.imageUrl,
            metaJson:      sidecar.metaJson,
            status:        status,
            attemptCount:  sidecar.attemptCount,
            nextAttemptAt: sidecar.nextAttemptAt,
            createdAt:     sidecar.createdAt,
            updatedAt:     sidecar.updatedAt
        )
        tasks[meta.uploadTaskId] = meta
        showIdSet.insert(meta.showId)
    }

    // MARK: - Status Updates

    /// 更新任务状态并重写 sidecar
    func updateStatus(uploadTaskId: String,
                      status: TaskStatus,
                      attemptCount: Int? = nil,
                      nextAttemptAt: Int64? = nil,
                      lastErrorCode: Int? = nil,
                      lastErrorMsg: String? = nil) {
        guard var meta = tasks[uploadTaskId] else { return }
        let now = Int64(Date().timeIntervalSince1970 * 1000)

        meta.status        = status
        meta.updatedAt     = now
        if let a = attemptCount  { meta.attemptCount  = a }
        if let n = nextAttemptAt { meta.nextAttemptAt = n }
        tasks[uploadTaskId] = meta

        let jsonURL = sidecarURL(for: meta.imagePath)
        if var sidecar = readSidecar(at: jsonURL) {
            sidecar.status        = status.rawValue
            sidecar.updatedAt     = now
            if let a = attemptCount   { sidecar.attemptCount  = a }
            if let n = nextAttemptAt  { sidecar.nextAttemptAt = n }
            if let c = lastErrorCode  { sidecar.lastErrorCode = c }
            if let m = lastErrorMsg   { sidecar.lastErrorMsg  = m }
            writeSidecar(sidecar, to: jsonURL)
        }
    }

    // MARK: - Remove

    /// 删除 .jpg + .json，从内存索引和 show_id 集合移除
    func removeTask(uploadTaskId: String) {
        guard let meta = tasks[uploadTaskId] else { return }
        let jpgURL  = absoluteURL(for: meta.imagePath)
        let jsonURL = sidecarURL(for: meta.imagePath)
        try? FileManager.default.removeItem(at: jpgURL)
        try? FileManager.default.removeItem(at: jsonURL)
        tasks.removeValue(forKey: uploadTaskId)
        showIdSet.remove(meta.showId)
    }

    // MARK: - Dedup

    /// show_id 是否已存在于内存集合（客户端二级去重）
    func containsShowId(_ showId: String) -> Bool {
        showIdSet.contains(showId)
    }

    // MARK: - Scheduling Helpers

    /// 取下一个可立即执行的任务（状态 pending 或 retryWaiting 且冷却已过）
    func pickNextRunnable(now: Int64) -> TaskMeta? {
        tasks.values
            .filter { ($0.status == .pending || $0.status == .retryWaiting)
                      && $0.nextAttemptAt <= now }
            .min { ($0.nextAttemptAt, $0.createdAt) < ($1.nextAttemptAt, $1.createdAt) }
    }

    /// 最近一个 retryWaiting 任务的 nextAttemptAt（用于设置定时器唤醒）
    func nextRetryWaitingTime() -> Int64? {
        tasks.values
            .filter { $0.status == .retryWaiting }
            .map    { $0.nextAttemptAt }
            .min()
    }

    // MARK: - Quota

    func markQuotaExhausted() {
        quotaExhaustedUntilMidnight = true
        scheduleQuotaReset()
    }

    /// 释放配额重置定时器（shutdown 调用；磁盘文件不删）
    /// Timer 注册在 RunLoop.main，invalidate 必须回到主线程执行
    func invalidateTimers() {
        let timer = quotaResetTimer
        quotaResetTimer = nil
        if Thread.isMainThread {
            timer?.invalidate()
        } else {
            DispatchQueue.main.async { timer?.invalidate() }
        }
    }

    private func scheduleQuotaReset() {
        quotaResetTimer?.invalidate()
        let next = nextMidnightChinaStandard()
        quotaResetTimer = Timer.scheduledTimer(
            withTimeInterval: next.timeIntervalSinceNow,
            repeats: false
        ) { [weak self] _ in
            self?.quotaExhaustedUntilMidnight = false
        }
        RunLoop.main.add(quotaResetTimer!, forMode: .common)
    }

    private func nextMidnightChinaStandard() -> Date {
        var cal = Calendar(identifier: .gregorian)
        cal.timeZone = TimeZone(identifier: "Asia/Shanghai")!
        var comps = cal.dateComponents([.year, .month, .day], from: Date())
        comps.day! += 1
        comps.hour = 0; comps.minute = 0; comps.second = 0
        return cal.date(from: comps) ?? Date().addingTimeInterval(86400)
    }

    // MARK: - getRecords / deleteRecord

    /// 返回指定样式下所有状态非 dead 的记录（供 `MaterialMonitor.getRecords` 使用）
    func getRecords(style: MaterialStyle) -> [FrameRecord] {
        tasks.values
            .filter { $0.style == style && $0.status != .dead }
            .map { meta in
                FrameRecord(
                    showId:        meta.showId,
                    uploadTaskId:  meta.uploadTaskId,
                    placementId:   meta.placementId,
                    style:         meta.style,
                    requestTimeMs: 0, fillTimeMs: 0,
                    showTimeMs:    meta.nextAttemptAt,
                    imageUrl:      meta.imageUrl,
                    localFilePath: absoluteURL(for: meta.imagePath).path,
                    imageHash:     meta.imageHash,
                    createdAt:     meta.createdAt
                )
            }
    }

    /// 删除记录（pending/retryWaiting/dead 立即删除；uploading 标记为待取消）
    func deleteRecord(uploadTaskId: String, cancelIfUploading: Bool = true) -> Bool {
        guard let meta = tasks[uploadTaskId] else { return false }
        if meta.status == .uploading && cancelIfUploading {
            // 标记为待取消（调度器检查此标记后不触发回调）
            var updated = meta
            updated.status = .dead
            tasks[uploadTaskId] = updated
            return true
        }
        removeTask(uploadTaskId: uploadTaskId)
        return true
    }

    // MARK: - Image data read

    func readImageData(for meta: TaskMeta) -> Data? {
        try? Data(contentsOf: absoluteURL(for: meta.imagePath))
    }

    // MARK: - LRU Cleanup

    /// 检查目录总大小，超出时按 created_at 升序淘汰 dead / 孤儿文件
    func cleanupIfNeeded(maxMb: Int) {
        let maxBytes = maxMb * 1024 * 1024
        let fm = FileManager.default

        // 计算总大小
        var totalSize = 0
        if let enumerator = fm.enumerator(at: TaskStore.rootDirectory,
                                          includingPropertiesForKeys: [.fileSizeKey]) {
            for case let url as URL in enumerator {
                totalSize += (try? url.resourceValues(forKeys: [.fileSizeKey]).fileSize) ?? 0
            }
        }
        guard totalSize > maxBytes else { return }

        // 找出可淘汰的 dead 任务，按 createdAt 升序
        let evictable = tasks.values
            .filter { $0.status == .dead }
            .sorted { $0.createdAt < $1.createdAt }

        for meta in evictable {
            guard totalSize > maxBytes else { break }
            let jpgURL  = absoluteURL(for: meta.imagePath)
            let jsonURL = sidecarURL(for: meta.imagePath)
            let jpgSize = (try? jpgURL.resourceValues(forKeys: [.fileSizeKey]).fileSize)  ?? 0
            let jsonSize = (try? jsonURL.resourceValues(forKeys: [.fileSizeKey]).fileSize) ?? 0
            try? fm.removeItem(at: jpgURL)
            try? fm.removeItem(at: jsonURL)
            tasks.removeValue(forKey: meta.uploadTaskId)
            showIdSet.remove(meta.showId)
            totalSize -= (jpgSize + jsonSize)
        }
    }

    // MARK: - Private file utilities

    private func styleDirectory(for style: MaterialStyle) -> URL {
        TaskStore.rootDirectory.appendingPathComponent(style.rawValue, isDirectory: true)
    }

    private func fileName(style: MaterialStyle, showTimeMs: Int64, uploadTaskId: String) -> String {
        let short = uploadTaskId.replacingOccurrences(of: "-", with: "").lowercased().prefix(8)
        return "\(style.rawValue)_\(showTimeMs)_\(short)"
    }

    private func absoluteURL(for relativePath: String) -> URL {
        TaskStore.rootDirectory.appendingPathComponent(relativePath)
    }

    private func sidecarURL(for relativePath: String) -> URL {
        absoluteURL(for: relativePath)
            .deletingPathExtension()
            .appendingPathExtension("json")
    }

    private func createDirectoriesIfNeeded() {
        let fm = FileManager.default
        for style in MaterialStyle.allCases {
            let dir = styleDirectory(for: style)
            try? fm.createDirectory(at: dir, withIntermediateDirectories: true)
        }
    }

    private func setExcludedFromBackup(_ url: URL) {
        var u = url
        var values = URLResourceValues()
        values.isExcludedFromBackup = true
        try? u.setResourceValues(values)
    }

    // MARK: Sidecar read / write (atomic)

    func readSidecar(at url: URL) -> SidecarData? {
        guard let data = try? Data(contentsOf: url) else { return nil }
        return try? JSONDecoder().decode(SidecarData.self, from: data)
    }

    func writeSidecar(_ sidecar: SidecarData, to url: URL) {
        guard let data = try? JSONEncoder().encode(sidecar) else { return }
        let tmpURL = url.deletingLastPathComponent()
            .appendingPathComponent(url.lastPathComponent + ".tmp")
        do {
            try data.write(to: tmpURL, options: .atomic)
            _ = try FileManager.default.replaceItemAt(url, withItemAt: tmpURL)
            setExcludedFromBackup(url)
        } catch {
            try? data.write(to: url, options: .atomic)
        }
    }
}
