// // ScheduleViewModel.swift // SportsTime // import Foundation import SwiftUI import os.log private let logger = Logger(subsystem: "com.sportstime.app", category: "ScheduleViewModel") @MainActor @Observable final class ScheduleViewModel { // MARK: - Filter State var selectedSports: Set = Set(Sport.supported) var startDate: Date = Calendar.current.startOfDay(for: Date()) var endDate: Date = Calendar.current.date(byAdding: .day, value: 14, to: Calendar.current.startOfDay(for: Date())) ?? Date() var searchText: String = "" // MARK: - Data State private(set) var games: [RichGame] = [] private(set) var isLoading = false private(set) var error: Error? private(set) var errorMessage: String? private let dataProvider = AppDataProvider.shared // MARK: - Pre-computed Groupings (avoid computed property overhead) /// Games grouped by sport - pre-computed to avoid re-grouping on every render private(set) var gamesBySport: [(sport: Sport, games: [RichGame])] = [] /// All games matching current filters (before any display limiting) private var filteredGames: [RichGame] = [] // MARK: - Diagnostics /// Debug info for troubleshooting missing games private(set) var diagnostics: ScheduleDiagnostics = ScheduleDiagnostics() var hasFilters: Bool { selectedSports.count < Sport.supported.count || !searchText.isEmpty } // MARK: - Actions func loadGames() async { guard !selectedSports.isEmpty else { games = [] updateFilteredGames() return } isLoading = true error = nil errorMessage = nil // Start diagnostics let queryStartDate = startDate let queryEndDate = endDate let querySports = selectedSports logger.info("📅 Loading games: \(querySports.map(\.rawValue).joined(separator: ", "))") logger.info("📅 Date range: \(queryStartDate.formatted()) to \(queryEndDate.formatted())") do { // Load initial data if needed if dataProvider.teams.isEmpty { logger.info("📅 Teams empty, loading initial data...") await dataProvider.loadInitialData() } // Check if data provider had an error if let providerError = dataProvider.errorMessage { self.errorMessage = providerError self.error = dataProvider.error isLoading = false updateFilteredGames() logger.error("📅 DataProvider error: \(providerError)") return } // Log team/stadium counts for diagnostics logger.info("📅 Loaded \(self.dataProvider.teams.count) teams, \(self.dataProvider.stadiums.count) stadiums") games = try await dataProvider.filterRichGames( sports: selectedSports, startDate: startDate, endDate: endDate ) // Update diagnostics var newDiagnostics = ScheduleDiagnostics() newDiagnostics.lastQueryStartDate = queryStartDate newDiagnostics.lastQueryEndDate = queryEndDate newDiagnostics.lastQuerySports = Array(querySports) newDiagnostics.totalGamesReturned = games.count newDiagnostics.teamsLoaded = dataProvider.teams.count newDiagnostics.stadiumsLoaded = dataProvider.stadiums.count // Count games by sport var sportCounts: [Sport: Int] = [:] for game in games { sportCounts[game.game.sport, default: 0] += 1 } newDiagnostics.gamesBySport = sportCounts self.diagnostics = newDiagnostics logger.info("📅 Returned \(self.games.count) games") for (sport, count) in sportCounts.sorted(by: { $0.key.rawValue < $1.key.rawValue }) { logger.info("📅 \(sport.rawValue): \(count) games") } // Debug: Print all NBA games let nbaGames = games.filter { $0.game.sport == .nba } print("🏀 [DEBUG] All NBA games in schedule (\(nbaGames.count) total):") for game in nbaGames.sorted(by: { $0.game.dateTime < $1.game.dateTime }) { let dateStr = game.game.dateTime.gameDateTimeString(in: game.stadium.timeZone) print("🏀 \(dateStr): \(game.awayTeam.name) @ \(game.homeTeam.name) (\(game.game.id))") } } catch let cloudKitError as CloudKitError { self.error = cloudKitError self.errorMessage = cloudKitError.errorDescription logger.error("📅 CloudKit error: \(cloudKitError.errorDescription ?? "unknown")") } catch { self.error = error self.errorMessage = error.localizedDescription logger.error("📅 Error loading games: \(error.localizedDescription)") } isLoading = false updateFilteredGames() } func clearError() { error = nil errorMessage = nil } func toggleSport(_ sport: Sport) { if selectedSports.contains(sport) { selectedSports.remove(sport) } else { selectedSports.insert(sport) } Task { await loadGames() } } func resetFilters() { selectedSports = Set(Sport.supported) searchText = "" startDate = Date() endDate = Calendar.current.date(byAdding: .day, value: 14, to: Date()) ?? Date() Task { await loadGames() } } func updateDateRange(start: Date, end: Date) { startDate = start endDate = end Task { await loadGames() } } // MARK: - Filtering & Grouping (pre-computed, not computed properties) /// Recomputes filtered games and groupings based on current search text func updateFilteredGames() { // Step 1: Filter by search text if searchText.isEmpty { filteredGames = games } else { let query = searchText.lowercased() filteredGames = games.filter { game in game.homeTeam.name.lowercased().contains(query) || game.homeTeam.city.lowercased().contains(query) || game.awayTeam.name.lowercased().contains(query) || game.awayTeam.city.lowercased().contains(query) || game.stadium.name.lowercased().contains(query) || game.stadium.city.lowercased().contains(query) } } // Step 2: Pre-compute grouping by sport (done once, not per-render) let grouped = Dictionary(grouping: filteredGames) { $0.game.sport } gamesBySport = grouped .sorted { lhs, rhs in let lhsIndex = Sport.allCases.firstIndex(of: lhs.key) ?? 0 let rhsIndex = Sport.allCases.firstIndex(of: rhs.key) ?? 0 return lhsIndex < rhsIndex } .map { (sport: $0.key, games: $0.value.sorted { $0.game.dateTime < $1.game.dateTime }) } } } // MARK: - Diagnostics Model /// Diagnostic information for troubleshooting schedule display issues struct ScheduleDiagnostics { var lastQueryStartDate: Date? var lastQueryEndDate: Date? var lastQuerySports: [Sport] = [] var totalGamesReturned: Int = 0 var teamsLoaded: Int = 0 var stadiumsLoaded: Int = 0 var gamesBySport: [Sport: Int] = [:] var summary: String { guard let start = lastQueryStartDate, let end = lastQueryEndDate else { return "No query executed" } let dateFormatter = DateFormatter() dateFormatter.dateStyle = .short dateFormatter.timeStyle = .short var lines: [String] = [] lines.append("Date Range: \(dateFormatter.string(from: start)) - \(dateFormatter.string(from: end))") lines.append("Sports: \(lastQuerySports.map(\.rawValue).joined(separator: ", "))") lines.append("Teams Loaded: \(teamsLoaded)") lines.append("Stadiums Loaded: \(stadiumsLoaded)") lines.append("Total Games: \(totalGamesReturned)") if !gamesBySport.isEmpty { lines.append("By Sport:") for (sport, count) in gamesBySport.sorted(by: { $0.key.rawValue < $1.key.rawValue }) { lines.append(" \(sport.rawValue): \(count)") } } return lines.joined(separator: "\n") } }