//
//  LNVoiceResourceManager.swift
//  Lanu
//
//  Created by OneeChan on 2026/1/4.
//

import Foundation
import AVFoundation


private class LNVoiceLocalSourceInfo: Codable {
    @LNVisitedTimeWrapper
    var path: String
    
    init(path: String) {
        self.path = path
    }
}


class LNVoiceResourceManager {
    static let shared = LNVoiceResourceManager()
    private let lock = NSLock()
    private let maxFileCount = 100
    private var voiceFileCache: [LNVoiceSource: [String: LNVoiceLocalSourceInfo]] = LNUserDefaults[.voiceCache, [:]]
    
    private var loadValueAssets: [String: AVURLAsset] = [:]
    private var voiceDurationMap: [String: Double] = [:]
    
    func voicePath(_ name: String, type: LNVoiceSource) -> URL {
        return URL.voiceCacheFolder
            .appendingPathComponent(type.folderName)
            .appendingPathComponent(name)
    }
    
    func voiceResourceFor(url: String, type: LNVoiceSource,
                          cachePath: URL? = nil,
                          completion: ((String) -> Void)?) {
        if let cachePath, FileManager.default.fileExists(atPath: cachePath.path) {
            completion?(cachePath.path)
            return
        }
        
        lock.lock()
        let cache = voiceFileCache[type]?[url]
        lock.unlock()
        
        if let cache {
            if FileManager.default.fileExists(atPath: cache.path) {
                saveCache()
                completion?(cache.path)
                return
            } else {
                lock.lock()
                voiceFileCache[type]?.removeValue(forKey: url)
                lock.unlock()
                saveCache()
            }
        }
        
        downloadVoice(url: url, type: type, customPath: cachePath, completion: completion)
    }
    
    func downloadVoice(url: String, type: LNVoiceSource,
                       customPath: URL? = nil, completion: ((String) -> Void)? = nil) {
        let path = customPath ?? voicePath(url.md5, type: type)
        LNFileDownloader.shared.startDownload(
            from: url, destinationPath: path, completionHandler: { [weak self] result in
                guard let self else { return }
                guard case .success(let sourcePath) = result else {
                    return
                }
                
                completion?(sourcePath.path)
                
                DispatchQueue.global().async { [weak self] in
                    guard let self else { return }
                    lock.lock()
                    var cach = voiceFileCache[type] ?? [:]
                    cach[url] = LNVoiceLocalSourceInfo(path: sourcePath.path)
                    
                    // 缓存文件数超过限制
                    if cach.count > maxFileCount {
                        let keys = cach.sorted { $0.value.$path.visited < $1.value.$path.visited }.map { $0.key }
                        // LRU 规则， 移除一半旧缓存
                        keys.prefix(maxFileCount / 2).forEach {
                            if let path = cach[$0]?.path, FileManager.default.fileExists(atPath: path) {
                                try? FileManager.default.removeItem(atPath: path)
                            }
                            cach.removeValue(forKey: $0)
                        }
                    }
                    
                    voiceFileCache[type] = cach
                    lock.unlock()
                    saveCache()
                }
            })
    }
    
    func getRemoteAudioDuration(urlStr: String?, completion: @escaping (TimeInterval?, Error?) -> Void) {
        guard let urlStr else {
            completion(nil, nil)
            return
        }
        
        lock.lock()
        let cache = voiceDurationMap[urlStr]
        lock.unlock()
        
        if let cache {
            completion(cache, nil)
            return
        }
        
        guard let url = URL(string: urlStr) else {
            completion(nil, nil)
            return
        }
        
        let options: [String: Any] = [
            AVURLAssetPreferPreciseDurationAndTimingKey: true
        ]
        
        let asset = AVURLAsset(url: url, options: options)
        loadValueAssets[urlStr] = asset
        
        asset.loadValuesAsynchronously(forKeys: ["duration"]) { [weak self] in
            guard let self else { return }
            var error: NSError?
            let status = asset.statusOfValue(forKey: "duration", error: &error)
            
            runOnMain { [weak self] in
                guard let self else { return }
                switch status {
                case .loaded:
                    let duration = asset.duration.seconds
                    lock.lock()
                    voiceDurationMap[urlStr] = duration
                    lock.unlock()
                    completion(duration, nil)
                case .failed:
                    completion(nil, error)
                case .cancelled:
                    completion(nil, NSError(domain: "AudioDuration", code: -1, userInfo: [NSLocalizedDescriptionKey: "加载取消"]))
                default:
                    completion(nil, NSError(domain: "AudioDuration", code: -2, userInfo: [NSLocalizedDescriptionKey: "加载失败"]))
                }
                loadValueAssets.removeValue(forKey: urlStr)
            }
        }
    }
    
    func cancelLoadingAsset(urlStr: String) {
        guard let asset = loadValueAssets.removeValue(forKey: urlStr) else { return }
        asset.cancelLoading()
    }
}

extension LNVoiceResourceManager {
    private func saveCache() {
        lock.lock()
        LNUserDefaults[.voiceCache] = voiceFileCache
        lock.unlock()
    }
}
