// // TravelOverrideService.swift // SportsTime // // CloudKit service for travel day position overrides // import Foundation import CloudKit actor TravelOverrideService { static let shared = TravelOverrideService() private let container: CKContainer private let publicDatabase: CKDatabase private init() { self.container = CKContainer(identifier: "iCloud.com.sportstime.app") self.publicDatabase = container.publicCloudDatabase } // MARK: - CRUD Operations /// Create or update a travel day override (upserts by tripId + travelAnchorId) func saveOverride(_ override: TravelDayOverride) async throws -> TravelDayOverride { print("☁️ [CloudKit] Saving travel override for: \(override.travelAnchorId)") // Look up existing override by travelAnchorId (not by UUID) let existingOverrides = try await fetchOverrides(forTripId: override.tripId) let existingOverride = existingOverrides.first { $0.travelAnchorId == override.travelAnchorId } let record: CKRecord let now = Date() if let existing = existingOverride { // Fetch the actual CKRecord to update it let recordID = CKRecord.ID(recordName: existing.id.uuidString) do { let existingRecord = try await publicDatabase.record(for: recordID) existingRecord[CKTravelDayOverride.displayDayKey] = override.displayDay existingRecord[CKTravelDayOverride.modifiedAtKey] = now record = existingRecord print("☁️ [CloudKit] Updating existing override record: \(existing.id)") } catch { // Record doesn't exist anymore, create new let ckOverride = CKTravelDayOverride(override: override) record = ckOverride.record } } else { // Create new record let ckOverride = CKTravelDayOverride(override: override) record = ckOverride.record print("☁️ [CloudKit] Creating new override record") } do { try await publicDatabase.save(record) print("☁️ [CloudKit] Travel override saved: \(record.recordID.recordName)") var savedOverride = override savedOverride.modifiedAt = now return savedOverride } catch let error as CKError { print("☁️ [CloudKit] CKError on save: \(error.code.rawValue) - \(error.localizedDescription)") throw mapCloudKitError(error) } catch { print("☁️ [CloudKit] Unknown error on save: \(error)") throw TravelOverrideError.unknown(error) } } /// Delete a travel day override func deleteOverride(_ overrideId: UUID) async throws { let recordID = CKRecord.ID(recordName: overrideId.uuidString) do { try await publicDatabase.deleteRecord(withID: recordID) print("☁️ [CloudKit] Travel override deleted: \(overrideId)") } catch let error as CKError { if error.code != .unknownItem { throw mapCloudKitError(error) } // Already deleted - ignore } catch { throw TravelOverrideError.unknown(error) } } /// Delete override by travel anchor ID (useful when removing by travel segment) func deleteOverride(forTripId tripId: UUID, travelAnchorId: String) async throws { // First fetch to find the record let overrides = try await fetchOverrides(forTripId: tripId) if let existing = overrides.first(where: { $0.travelAnchorId == travelAnchorId }) { try await deleteOverride(existing.id) } } /// Fetch all travel day overrides for a trip func fetchOverrides(forTripId tripId: UUID) async throws -> [TravelDayOverride] { print("☁️ [CloudKit] Fetching travel overrides for tripId: \(tripId.uuidString)") let predicate = NSPredicate( format: "%K == %@", CKTravelDayOverride.tripIdKey, tripId.uuidString ) let query = CKQuery(recordType: CKRecordType.travelDayOverride, predicate: predicate) do { let (results, _) = try await publicDatabase.records(matching: query) print("☁️ [CloudKit] Query returned \(results.count) travel override records") let overrides = results.compactMap { result -> TravelDayOverride? in guard case .success(let record) = result.1 else { print("☁️ [CloudKit] Record fetch failed for: \(result.0.recordName)") return nil } let override = CKTravelDayOverride(record: record).toOverride() if override == nil { print("☁️ [CloudKit] Failed to parse travel override record: \(record.recordID.recordName)") } return override } print("☁️ [CloudKit] Parsed \(overrides.count) valid travel overrides") return overrides } catch let error as CKError { print("☁️ [CloudKit] CKError on fetch: \(error.code.rawValue) - \(error.localizedDescription)") throw mapCloudKitError(error) } catch { print("☁️ [CloudKit] Unknown error on fetch: \(error)") throw TravelOverrideError.unknown(error) } } /// Convert array of overrides to dictionary keyed by travelAnchorId /// If duplicate travelAnchorIds exist (legacy data), takes the most recently modified one func fetchOverridesAsDictionary(forTripId tripId: UUID) async throws -> [String: Int] { let overrides = try await fetchOverrides(forTripId: tripId) // Sort by modifiedAt descending so most recent comes first let sorted = overrides.sorted { $0.modifiedAt > $1.modifiedAt } // Use uniquingKeysWith to keep the first (most recent) value for duplicates return Dictionary(sorted.map { ($0.travelAnchorId, $0.displayDay) }) { first, _ in first } } // MARK: - Error Mapping private func mapCloudKitError(_ error: CKError) -> TravelOverrideError { switch error.code { case .notAuthenticated: return .notSignedIn case .networkUnavailable, .networkFailure: return .networkUnavailable case .unknownItem: return .overrideNotFound default: return .unknown(error) } } } // MARK: - Errors enum TravelOverrideError: Error, LocalizedError { case notSignedIn case overrideNotFound case networkUnavailable case unknown(Error) var errorDescription: String? { switch self { case .notSignedIn: return "Please sign in to iCloud to customize travel days." case .overrideNotFound: return "Travel day setting not found." case .networkUnavailable: return "Unable to connect. Please check your internet connection." case .unknown(let error): return "An error occurred: \(error.localizedDescription)" } } }