// // DataProvider.swift // SportsTime // // Unified data provider that reads from SwiftData for offline support. // Data is bootstrapped from bundled JSON and can be synced via CloudKit. // import Foundation import SwiftData import Combine /// Environment-aware data provider that reads from SwiftData (offline-first) @MainActor final class AppDataProvider: ObservableObject { static let shared = AppDataProvider() @Published private(set) var teams: [Team] = [] @Published private(set) var stadiums: [Stadium] = [] @Published private(set) var isLoading = false @Published private(set) var error: Error? @Published private(set) var errorMessage: String? // Lookup dictionaries - keyed by canonical ID (String) private var teamsById: [String: Team] = [:] private var stadiumsById: [String: Stadium] = [:] private var modelContext: ModelContext? private init() {} // MARK: - Configuration /// Set the model context for SwiftData access func configure(with context: ModelContext) { self.modelContext = context } // MARK: - Data Loading /// Load all data from SwiftData into memory for fast access func loadInitialData() async { guard let context = modelContext else { errorMessage = "Model context not configured" return } isLoading = true error = nil errorMessage = nil do { // Fetch canonical stadiums from SwiftData let stadiumDescriptor = FetchDescriptor( predicate: #Predicate { $0.deprecatedAt == nil } ) let canonicalStadiums = try context.fetch(stadiumDescriptor) // Convert to domain models and build lookups var loadedStadiums: [Stadium] = [] var stadiumLookup: [String: Stadium] = [:] for canonical in canonicalStadiums { let stadium = canonical.toDomain() loadedStadiums.append(stadium) stadiumLookup[stadium.id] = stadium } // Fetch canonical teams from SwiftData let teamDescriptor = FetchDescriptor( predicate: #Predicate { $0.deprecatedAt == nil } ) let canonicalTeams = try context.fetch(teamDescriptor) // Convert to domain models var loadedTeams: [Team] = [] var teamLookup: [String: Team] = [:] for canonical in canonicalTeams { let team = canonical.toDomain() loadedTeams.append(team) teamLookup[team.id] = team } self.teams = loadedTeams self.stadiums = loadedStadiums self.teamsById = teamLookup self.stadiumsById = stadiumLookup } catch { self.error = error self.errorMessage = error.localizedDescription } isLoading = false } func clearError() { error = nil errorMessage = nil } func retry() async { await loadInitialData() } // MARK: - Data Access func team(for id: String) -> Team? { teamsById[id] } func stadium(for id: String) -> Stadium? { stadiumsById[id] } func teams(for sport: Sport) -> [Team] { teams.filter { $0.sport == sport } } // MARK: - Game Fetching /// Fetch games from SwiftData within date range func fetchGames(sports: Set, startDate: Date, endDate: Date) async throws -> [Game] { guard let context = modelContext else { throw DataProviderError.contextNotConfigured } let sportStrings = sports.map { $0.rawValue } let descriptor = FetchDescriptor( predicate: #Predicate { game in game.deprecatedAt == nil && game.dateTime >= startDate && game.dateTime <= endDate } ) let canonicalGames = try context.fetch(descriptor) // Filter by sport and convert to domain models return canonicalGames.compactMap { canonical -> Game? in guard sportStrings.contains(canonical.sport) else { return nil } return canonical.toDomain() } } /// Fetch a single game by canonical ID func fetchGame(by id: String) async throws -> Game? { guard let context = modelContext else { throw DataProviderError.contextNotConfigured } let descriptor = FetchDescriptor( predicate: #Predicate { $0.canonicalId == id } ) guard let canonical = try context.fetch(descriptor).first else { return nil } return canonical.toDomain() } /// Fetch games with full team and stadium data func fetchRichGames(sports: Set, startDate: Date, endDate: Date) async throws -> [RichGame] { let games = try await fetchGames(sports: sports, startDate: startDate, endDate: endDate) return games.compactMap { game in guard let homeTeam = teamsById[game.homeTeamId], let awayTeam = teamsById[game.awayTeamId], let stadium = stadiumsById[game.stadiumId] else { return nil } return RichGame(game: game, homeTeam: homeTeam, awayTeam: awayTeam, stadium: stadium) } } func richGame(from game: Game) -> RichGame? { guard let homeTeam = teamsById[game.homeTeamId], let awayTeam = teamsById[game.awayTeamId], let stadium = stadiumsById[game.stadiumId] else { return nil } return RichGame(game: game, homeTeam: homeTeam, awayTeam: awayTeam, stadium: stadium) } } // MARK: - Errors enum DataProviderError: Error, LocalizedError { case contextNotConfigured var errorDescription: String? { switch self { case .contextNotConfigured: return "Data provider not configured with model context" } } }