import Foundation

/// Lightweight offline cache wrapper: memory → disk → network fallback.
/// Uses UserDefaults for small values, FileCache for larger blobs.
@MainActor
final class LocalCache {
    static let shared = LocalCache()

    private let defaults = UserDefaults.standard
    private let fileCache = FileCache(suite: "local_cache")

    private init() {}

    // MARK: - Simple values (UserDefaults)

    func get<T>(_ key: String) -> T? where T: Decodable {
        // Try memory/disk via FileCache first, then UserDefaults
        if let cached: T = try? fileCache.load(T.self, forKey: key) {
            return cached
        }
        return nil
    }

    func set<T>(_ value: T, forKey key: String) where T: Encodable {
        try? fileCache.save(value, forKey: key)
    }

    func remove(_ key: String) {
        try? fileCache.remove(forKey: key)
    }

    // MARK: - Array caching (common pattern)

    func getList<T: Decodable>(_ key: String) -> [T] {
        (try? fileCache.load([T].self, forKey: key)) ?? []
    }

    func setList<T: Encodable>(_ items: [T], forKey key: String) {
        try? fileCache.save(items, forKey: key)
    }

    // MARK: - Expiry-based caching

    func getWithExpiry<T: Decodable>(_ key: String, ttl: TimeInterval = 300) -> T? {
        let expiryKey = "\(key)_expiry"
        let expiry = defaults.double(forKey: expiryKey)
        guard expiry == 0 || Date().timeIntervalSince1970 < expiry else {
            remove(key)
            defaults.removeObject(forKey: expiryKey)
            return nil
        }
        return get(key)
    }

    func setWithExpiry<T: Encodable>(_ value: T, forKey key: String, ttl: TimeInterval = 300) {
        set(value, forKey: key)
        defaults.set(Date().timeIntervalSince1970 + ttl, forKey: "\(key)_expiry")
    }

    func clearAll() {
        try? fileCache.clear()
    }
}

// MARK: - ViewModel caching helper

extension LocalCache {
    /// Wrap an API fetch with cache-first strategy.
    /// Returns cached data instantly, then refreshes in background.
    func cacheFirst<T: Codable>(
        key: String,
        ttl: TimeInterval = 300,
        fetch: @Sendable @escaping () async throws -> T
    ) async throws -> T {
        if let cached: T = getWithExpiry(key, ttl: ttl) {
            Task { try? await refreshCache(key: key, ttl: ttl, fetch: fetch) }
            return cached
        }
        let fresh = try await fetch()
        setWithExpiry(fresh, forKey: key, ttl: ttl)
        return fresh
    }

    private func refreshCache<T: Codable>(
        key: String,
        ttl: TimeInterval,
        fetch: @Sendable @escaping () async throws -> T
    ) async throws {
        let fresh = try await fetch()
        setWithExpiry(fresh, forKey: key, ttl: ttl)
    }
}
