// // BackgroundSyncManager.swift // SportsTime // // Manages background tasks for nightly CloudKit sync and database cleanup. // Uses BGTaskScheduler to run sync operations even when app is not active. // import Foundation import BackgroundTasks import SwiftData /// Manages background refresh and processing tasks for CloudKit sync. @MainActor final class BackgroundSyncManager { // MARK: - Task Identifiers /// Background app refresh task - runs periodically (system decides frequency) static let refreshTaskIdentifier = "com.sportstime.app.refresh" /// Background processing task - runs during optimal conditions (plugged in, overnight) static let processingTaskIdentifier = "com.sportstime.app.db-cleanup" // MARK: - Singleton static let shared = BackgroundSyncManager() // MARK: - Properties private var modelContainer: ModelContainer? private init() {} // MARK: - Configuration /// Configure the manager with the model container. /// Must be called before registering tasks. func configure(with container: ModelContainer) { self.modelContainer = container } // MARK: - Task Registration /// Register all background tasks with the system. /// Must be called early in app lifecycle, before `applicationDidFinishLaunching` returns. func registerTasks() { // Register refresh task (for periodic CloudKit sync) BGTaskScheduler.shared.register( forTaskWithIdentifier: Self.refreshTaskIdentifier, using: nil ) { task in guard let task = task as? BGAppRefreshTask else { return } Task { @MainActor in await self.handleRefreshTask(task) } } // Register processing task (for overnight heavy sync/cleanup) BGTaskScheduler.shared.register( forTaskWithIdentifier: Self.processingTaskIdentifier, using: nil ) { task in guard let task = task as? BGProcessingTask else { return } Task { @MainActor in await self.handleProcessingTask(task) } } print("Background tasks registered") } // MARK: - Task Scheduling /// Schedule the next background refresh. /// System will run this periodically based on app usage patterns. func scheduleRefresh() { let request = BGAppRefreshTaskRequest(identifier: Self.refreshTaskIdentifier) // Request to run no earlier than 1 hour from now // System will actually schedule based on usage patterns, battery, network, etc. request.earliestBeginDate = Date(timeIntervalSinceNow: 60 * 60) do { try BGTaskScheduler.shared.submit(request) print("Background refresh scheduled") } catch { print("Failed to schedule background refresh: \(error.localizedDescription)") } } /// Schedule overnight processing task for heavy sync/cleanup. /// Runs when device is charging and connected to WiFi. func scheduleProcessingTask() { let request = BGProcessingTaskRequest(identifier: Self.processingTaskIdentifier) // Schedule for overnight - earliest 2 AM let calendar = Calendar.current var components = calendar.dateComponents([.year, .month, .day], from: Date()) components.hour = 2 components.minute = 0 // If it's past 2 AM, schedule for tomorrow var targetDate = calendar.date(from: components) ?? Date() if targetDate < Date() { targetDate = calendar.date(byAdding: .day, value: 1, to: targetDate) ?? Date() } request.earliestBeginDate = targetDate request.requiresNetworkConnectivity = true request.requiresExternalPower = false // Don't require power, but prefer it do { try BGTaskScheduler.shared.submit(request) print("Overnight processing task scheduled for \(targetDate)") } catch { print("Failed to schedule processing task: \(error.localizedDescription)") } } /// Schedule all background tasks. /// Call this after app finishes launching and when app enters background. func scheduleAllTasks() { scheduleRefresh() scheduleProcessingTask() } // MARK: - Task Handlers /// Handle the background refresh task. @MainActor private func handleRefreshTask(_ task: BGAppRefreshTask) async { print("Background refresh task started") // Schedule the next refresh before we start (in case we get terminated) scheduleRefresh() // Set up expiration handler task.expirationHandler = { print("Background refresh task expired") task.setTaskCompleted(success: false) } guard let container = modelContainer else { print("Background refresh: No model container configured") task.setTaskCompleted(success: false) return } do { let success = try await performSync(context: container.mainContext) task.setTaskCompleted(success: success) print("Background refresh completed: \(success ? "success" : "no updates")") } catch { print("Background refresh failed: \(error.localizedDescription)") task.setTaskCompleted(success: false) } } /// Handle the background processing task (overnight heavy sync). @MainActor private func handleProcessingTask(_ task: BGProcessingTask) async { print("Background processing task started") // Schedule the next processing task scheduleProcessingTask() // Set up expiration handler task.expirationHandler = { print("Background processing task expired") task.setTaskCompleted(success: false) } guard let container = modelContainer else { print("Background processing: No model container configured") task.setTaskCompleted(success: false) return } do { // 1. Full sync from CloudKit let syncSuccess = try await performSync(context: container.mainContext) // 2. Clean up old data (games older than 1 year) let cleanupSuccess = await performCleanup(context: container.mainContext) task.setTaskCompleted(success: syncSuccess || cleanupSuccess) print("Background processing completed: sync=\(syncSuccess), cleanup=\(cleanupSuccess)") } catch { print("Background processing failed: \(error.localizedDescription)") task.setTaskCompleted(success: false) } } // MARK: - Sync Operations /// Perform CloudKit sync operation. @MainActor private func performSync(context: ModelContext) async throws -> Bool { let syncService = CanonicalSyncService() do { let result = try await syncService.syncAll(context: context) // Reload DataProvider if data changed if !result.isEmpty { await AppDataProvider.shared.loadInitialData() print("Background sync: \(result.totalUpdated) items updated") return true } return false } catch CanonicalSyncService.SyncError.cloudKitUnavailable { // No network - this is expected, not a failure print("Background sync: CloudKit unavailable (offline)") return false } catch CanonicalSyncService.SyncError.syncAlreadyInProgress { // Another sync is running - that's fine print("Background sync: Already in progress") return false } } /// Clean up old game data (older than 1 year). @MainActor private func performCleanup(context: ModelContext) async -> Bool { let oneYearAgo = Calendar.current.date(byAdding: .year, value: -1, to: Date()) ?? Date() do { // Fetch old games let descriptor = FetchDescriptor( predicate: #Predicate { game in game.dateTime < oneYearAgo && game.deprecatedAt == nil } ) let oldGames = try context.fetch(descriptor) guard !oldGames.isEmpty else { return false } // Soft-delete old games (mark as deprecated) for game in oldGames { game.deprecatedAt = Date() game.deprecationReason = "Auto-archived: game older than 1 year" } try context.save() print("Background cleanup: Archived \(oldGames.count) old games") return true } catch { print("Background cleanup failed: \(error.localizedDescription)") return false } } } // MARK: - Debug Helpers #if DEBUG extension BackgroundSyncManager { /// Manually trigger refresh task for testing. /// Run in debugger: `e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.sportstime.app.refresh"]` func debugTriggerRefresh() { print("Debug: Use lldb command to trigger background refresh:") print("e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@\"com.sportstime.app.refresh\"]") } /// Manually trigger processing task for testing. func debugTriggerProcessing() { print("Debug: Use lldb command to trigger background processing:") print("e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@\"com.sportstime.app.db-cleanup\"]") } } #endif