// // DailyNotificationScheduleHelper.swift // DailyNotificationPlugin // // Created by Matthew Raymer on 2025-12-23 // Copyright © 2025 TimeSafari. All rights reserved. // import Foundation import UserNotifications /** * DailyNotificationScheduleHelper.swift * * Orchestration helper for daily notification scheduling * * This helper encapsulates complex scheduling orchestration logic that combines * multiple services (scheduler, storage, stateActor, background tasks). * Similar to Android's ScheduleHelper.kt pattern. * * Responsibilities: * - Schedule daily notifications with full orchestration (cancel, clear, save, schedule, prefetch) * - Schedule dual notifications (background fetch + user notification) * - Clear rollover state * - Combine status from multiple sources * * @author Matthew Raymer * @version 1.0.0 * @created 2025-12-23 */ enum DailyNotificationScheduleHelper { /** * Schedule daily notification with full orchestration * * Orchestrates: * 1. Cancel all existing notifications * 2. Clear all stored notification content * 3. Clear rollover state * 4. Save notification content (via stateActor if available) * 5. Schedule notification * 6. Schedule background fetch (5 minutes before notification) * * @param content Notification content to schedule * @param scheduledTime Scheduled time in milliseconds * @param scheduler DailyNotificationScheduler instance * @param storage DailyNotificationStorage instance * @param stateActor DailyNotificationStateActor instance (optional, iOS 13+) * @param scheduleBackgroundFetch Closure to schedule background fetch * @return true if scheduling succeeded, false otherwise */ static func scheduleDailyNotification( content: NotificationContent, scheduledTime: Int64, scheduler: DailyNotificationScheduler, storage: DailyNotificationStorage?, stateActor: DailyNotificationStateActor?, scheduleBackgroundFetch: (Int64) -> Void ) async -> Bool { // Step 1: Cancel all existing notifications await scheduler.cancelAllNotifications() // Step 2: Clear all stored notification content storage?.clearAllNotifications() // Step 3: Clear rollover state clearRolloverState(storage: storage) // Step 4: Save notification content (via stateActor if available, otherwise storage) if #available(iOS 13.0, *), let stateActor = stateActor { await stateActor.saveNotificationContent(content) } else { storage?.saveNotificationContent(content) } // Step 5: Schedule notification let scheduled = await scheduler.scheduleNotification(content) // Step 6: Schedule background fetch if notification was scheduled if scheduled { scheduleBackgroundFetch(scheduledTime) } return scheduled } /** * Schedule dual notification (background fetch + user notification) * * Orchestrates both background fetch and user notification scheduling. * * @param contentFetchConfig Background fetch configuration * @param userNotificationConfig User notification configuration * @param scheduleBackgroundFetch Closure to schedule background fetch * @param scheduleUserNotification Closure to schedule user notification * @throws Error if scheduling fails */ static func scheduleDualNotification( contentFetchConfig: [String: Any], userNotificationConfig: [String: Any], scheduleBackgroundFetch: ([String: Any]) throws -> Void, scheduleUserNotification: ([String: Any]) throws -> Void ) throws { // Schedule both background fetch and user notification try scheduleBackgroundFetch(contentFetchConfig) try scheduleUserNotification(userNotificationConfig) } /** * Clear rollover state from storage and UserDefaults * * Clears: * - Global rollover time in storage * - All rollover_* keys from UserDefaults * * @param storage DailyNotificationStorage instance (optional) */ static func clearRolloverState(storage: DailyNotificationStorage?) { // Clear global rollover time storage?.saveLastRolloverTime(0) // Clear per-notification rollover times from UserDefaults let userDefaults = UserDefaults.standard let allKeys = userDefaults.dictionaryRepresentation().keys for key in allKeys { if key.hasPrefix("rollover_") { userDefaults.removeObject(forKey: key) } } userDefaults.synchronize() } /** * Get health status combining multiple sources * * Combines: * - Scheduler status (pending count, permission status) * - Storage/StateActor status (last notification) * * @param scheduler DailyNotificationScheduler instance * @param storage DailyNotificationStorage instance (optional) * @param stateActor DailyNotificationStateActor instance (optional, iOS 13+) * @return Health status dictionary * @throws Error if scheduler not initialized */ static func getHealthStatus( scheduler: DailyNotificationScheduler, storage: DailyNotificationStorage?, stateActor: DailyNotificationStateActor? ) async throws -> [String: Any] { // Delegate to scheduler for pending count and permission status let pendingCount = await scheduler.getPendingNotificationCount() let isEnabled = await scheduler.checkPermissionStatus() == .authorized // Delegate to stateActor if available (thread-safe), otherwise use storage directly let lastNotification: NotificationContent? if #available(iOS 13.0, *), let stateActor = stateActor { lastNotification = await stateActor.getLastNotification() } else { lastNotification = storage?.getLastNotification() } return [ "contentFetch": [ "isEnabled": true, "isScheduled": pendingCount > 0, "lastFetchTime": lastNotification?.fetchedAt ?? 0, "nextFetchTime": 0, "pendingFetches": pendingCount ], "userNotification": [ "isEnabled": isEnabled, "isScheduled": pendingCount > 0, "lastNotificationTime": lastNotification?.scheduledTime ?? 0, "nextNotificationTime": 0, "pendingNotifications": pendingCount ], "relationship": [ "isLinked": true, "contentAvailable": lastNotification != nil, "lastLinkTime": lastNotification?.fetchedAt ?? 0 ], "overall": [ "isActive": isEnabled && pendingCount > 0, "lastActivity": lastNotification?.scheduledTime ?? 0, "errorCount": 0, "successRate": 1.0 ] ] } }