import Foundation import SwiftUI @MainActor final class FavoritesStore: ObservableObject { @Published private(set) var items: [MediaItem] = [] private var profileId: String? private let defaults = UserDefaults.standard private func cacheKey(for profileId: String) -> String { "vmc.tv.favorites.cache.\(profileId)" } func bind(profileId: String?) { self.profileId = profileId guard let profileId, !profileId.isEmpty else { items = [] return } items = loadCache(profileId: profileId) } func contains(type: String, id: String) -> Bool { let want = normalizedType(type) return items.contains { $0.id == id && normalizedType($0.type) == want } } private func normalizedType(_ type: String) -> String { let t = type.lowercased() return (t == "series" || t == "show") ? "series" : "movie" } func replaceAll(_ next: [MediaItem]) { items = next persistCache() } @discardableResult func toggle( type: String, id: String, title: String, year: Int?, posterUrl: String?, backdropUrl: String?, logoUrl: String?, api: ApiClient ) async -> Bool { let normalized = normalizedType(type) let wasOn = contains(type: normalized, id: id) if wasOn { items.removeAll { $0.id == id && normalizedType($0.type) == normalized } } else { let item = MediaItem( type: normalized.uppercased() == "SERIES" ? "SERIES" : "MOVIE", id: id, title: title, subtitle: nil, year: year, posterUrl: posterUrl, backdropUrl: backdropUrl, logoUrl: logoUrl, mediaFileId: nil, seriesId: nil, positionMs: nil, durationMs: nil, progressId: nil, badge: nil ) items.insert(item, at: 0) } persistCache() do { let result = try await api.toggleFavorite(type: normalized, id: id) if let remote = result.items { replaceAll(remote) } else { await refresh(using: api) } return result.favorited } catch { if wasOn { let item = MediaItem( type: normalized.uppercased() == "SERIES" ? "SERIES" : "MOVIE", id: id, title: title, subtitle: nil, year: year, posterUrl: posterUrl, backdropUrl: backdropUrl, logoUrl: logoUrl, mediaFileId: nil, seriesId: nil, positionMs: nil, durationMs: nil, progressId: nil, badge: nil ) items.insert(item, at: 0) } else { items.removeAll { $0.id == id && normalizedType($0.type) == normalized } } persistCache() return wasOn } } func refresh(using api: ApiClient) async { guard let profileId, !profileId.isEmpty else { items = [] return } do { let remote = try await api.listFavorites() replaceAll(remote) } catch { if items.isEmpty { items = loadCache(profileId: profileId) } } } var mediaItems: [MediaItem] { items } private func loadCache(profileId: String) -> [MediaItem] { guard let data = defaults.data(forKey: cacheKey(for: profileId)), let decoded = try? JSONDecoder().decode([MediaItem].self, from: data) else { return [] } return decoded } private func persistCache() { guard let profileId, !profileId.isEmpty else { return } if let data = try? JSONEncoder().encode(items) { defaults.set(data, forKey: cacheKey(for: profileId)) } } }