// // CloudKitService.swift // SportsTime // import Foundation import CloudKit // MARK: - CloudKit Errors enum CloudKitError: Error, LocalizedError { case notSignedIn case networkUnavailable case serverError(String) case quotaExceeded case permissionDenied case recordNotFound case unknown(Error) var errorDescription: String? { switch self { case .notSignedIn: return "Please sign in to iCloud in Settings to sync data." case .networkUnavailable: return "Unable to connect to the server. Check your internet connection." case .serverError(let message): return "Server error: \(message)" case .quotaExceeded: return "iCloud storage quota exceeded." case .permissionDenied: return "Permission denied. Check your iCloud settings." case .recordNotFound: return "Data not found." case .unknown(let error): return "An unexpected error occurred: \(error.localizedDescription)" } } static func from(_ error: Error) -> CloudKitError { if let ckError = error as? CKError { switch ckError.code { case .notAuthenticated: return .notSignedIn case .networkUnavailable, .networkFailure: return .networkUnavailable case .serverResponseLost: return .serverError("Connection lost") case .quotaExceeded: return .quotaExceeded case .permissionFailure: return .permissionDenied case .unknownItem: return .recordNotFound default: return .serverError(ckError.localizedDescription) } } return .unknown(error) } } actor CloudKitService { static let shared = CloudKitService() private let container: CKContainer private let publicDatabase: CKDatabase private init() { self.container = CKContainer(identifier: "iCloud.com.sportstime.app") self.publicDatabase = container.publicCloudDatabase } // MARK: - Availability Check func isAvailable() async -> Bool { let status = await checkAccountStatus() return status == .available } func checkAvailabilityWithError() async throws { let status = await checkAccountStatus() switch status { case .available: return case .noAccount: throw CloudKitError.notSignedIn case .restricted: throw CloudKitError.permissionDenied case .couldNotDetermine: throw CloudKitError.networkUnavailable case .temporarilyUnavailable: throw CloudKitError.networkUnavailable @unknown default: throw CloudKitError.networkUnavailable } } // MARK: - Fetch Operations func fetchTeams(for sport: Sport) async throws -> [Team] { let predicate = NSPredicate(format: "sport == %@", sport.rawValue) let query = CKQuery(recordType: CKRecordType.team, predicate: predicate) let (results, _) = try await publicDatabase.records(matching: query) return results.compactMap { result in guard case .success(let record) = result.1 else { return nil } return CKTeam(record: record).team } } func fetchStadiums() async throws -> [Stadium] { let predicate = NSPredicate(value: true) let query = CKQuery(recordType: CKRecordType.stadium, predicate: predicate) let (results, _) = try await publicDatabase.records(matching: query) return results.compactMap { result in guard case .success(let record) = result.1 else { return nil } return CKStadium(record: record).stadium } } func fetchGames( sports: Set, startDate: Date, endDate: Date ) async throws -> [Game] { var allGames: [Game] = [] for sport in sports { let predicate = NSPredicate( format: "sport == %@ AND dateTime >= %@ AND dateTime <= %@", sport.rawValue, startDate as NSDate, endDate as NSDate ) let query = CKQuery(recordType: CKRecordType.game, predicate: predicate) let (results, _) = try await publicDatabase.records(matching: query) let games = results.compactMap { result -> Game? in guard case .success(let record) = result.1 else { return nil } let ckGame = CKGame(record: record) guard let homeRef = record[CKGame.homeTeamRefKey] as? CKRecord.Reference, let awayRef = record[CKGame.awayTeamRefKey] as? CKRecord.Reference, let stadiumRef = record[CKGame.stadiumRefKey] as? CKRecord.Reference, let homeId = UUID(uuidString: homeRef.recordID.recordName), let awayId = UUID(uuidString: awayRef.recordID.recordName), let stadiumId = UUID(uuidString: stadiumRef.recordID.recordName) else { return nil } return ckGame.game(homeTeamId: homeId, awayTeamId: awayId, stadiumId: stadiumId) } allGames.append(contentsOf: games) } return allGames.sorted { $0.dateTime < $1.dateTime } } func fetchGame(by id: UUID) async throws -> Game? { let predicate = NSPredicate(format: "gameId == %@", id.uuidString) let query = CKQuery(recordType: CKRecordType.game, predicate: predicate) let (results, _) = try await publicDatabase.records(matching: query) guard let result = results.first, case .success(let record) = result.1 else { return nil } let ckGame = CKGame(record: record) guard let homeRef = record[CKGame.homeTeamRefKey] as? CKRecord.Reference, let awayRef = record[CKGame.awayTeamRefKey] as? CKRecord.Reference, let stadiumRef = record[CKGame.stadiumRefKey] as? CKRecord.Reference, let homeId = UUID(uuidString: homeRef.recordID.recordName), let awayId = UUID(uuidString: awayRef.recordID.recordName), let stadiumId = UUID(uuidString: stadiumRef.recordID.recordName) else { return nil } return ckGame.game(homeTeamId: homeId, awayTeamId: awayId, stadiumId: stadiumId) } // MARK: - Sync Status func checkAccountStatus() async -> CKAccountStatus { do { return try await container.accountStatus() } catch { return .couldNotDetermine } } // MARK: - Subscription (for schedule updates) func subscribeToScheduleUpdates() async throws { let subscription = CKQuerySubscription( recordType: CKRecordType.game, predicate: NSPredicate(value: true), subscriptionID: "game-updates", options: [.firesOnRecordCreation, .firesOnRecordUpdate] ) let notification = CKSubscription.NotificationInfo() notification.shouldSendContentAvailable = true subscription.notificationInfo = notification try await publicDatabase.save(subscription) } }