/** * DailyNotificationRollingWindow.swift * * iOS Rolling window safety for notification scheduling * Ensures today's notifications are always armed and tomorrow's are armed within iOS caps * * @author Matthew Raymer * @version 1.0.0 */ import Foundation import UserNotifications /** * Manages rolling window safety for notification scheduling on iOS * * This class implements the critical rolling window logic: * - Today's remaining notifications are always armed * - Tomorrow's notifications are armed only if within iOS capacity limits * - Automatic window maintenance as time progresses * - iOS-specific capacity management */ class DailyNotificationRollingWindow { // MARK: - Constants private static let TAG = "DailyNotificationRollingWindow" // iOS notification limits private static let IOS_MAX_PENDING_NOTIFICATIONS = 64 private static let IOS_MAX_DAILY_NOTIFICATIONS = 20 // Window maintenance intervals private static let WINDOW_MAINTENANCE_INTERVAL_SECONDS: TimeInterval = 15 * 60 // 15 minutes // MARK: - Properties private let ttlEnforcer: DailyNotificationTTLEnforcer private let database: DailyNotificationDatabase? private let useSharedStorage: Bool // Window state private var lastMaintenanceTime: Date = Date.distantPast private var currentPendingCount: Int = 0 private var currentDailyCount: Int = 0 // MARK: - Initialization /** * Initialize rolling window manager * * @param ttlEnforcer TTL enforcement instance * @param database SQLite database (nil if using UserDefaults) * @param useSharedStorage Whether to use SQLite or UserDefaults */ init(ttlEnforcer: DailyNotificationTTLEnforcer, database: DailyNotificationDatabase?, useSharedStorage: Bool) { self.ttlEnforcer = ttlEnforcer self.database = database self.useSharedStorage = useSharedStorage print("\(Self.TAG): Rolling window initialized for iOS") } // MARK: - Window Maintenance /** * Maintain the rolling window by ensuring proper notification coverage * * This method should be called periodically to maintain the rolling window: * - Arms today's remaining notifications * - Arms tomorrow's notifications if within capacity limits * - Updates window state and statistics */ func maintainRollingWindow() { do { let currentTime = Date() // Check if maintenance is needed if currentTime.timeIntervalSince(lastMaintenanceTime) < Self.WINDOW_MAINTENANCE_INTERVAL_SECONDS { print("\(Self.TAG): Window maintenance not needed yet") return } print("\(Self.TAG): Starting rolling window maintenance") // Update current state updateWindowState() // Arm today's remaining notifications armTodaysRemainingNotifications() // Arm tomorrow's notifications if within capacity armTomorrowsNotificationsIfWithinCapacity() // Update maintenance time lastMaintenanceTime = currentTime print("\(Self.TAG): Rolling window maintenance completed: pending=\(currentPendingCount), daily=\(currentDailyCount)") } catch { print("\(Self.TAG): Error during rolling window maintenance: \(error)") } } /** * Arm today's remaining notifications * * Ensures all notifications for today that haven't fired yet are armed */ private func armTodaysRemainingNotifications() { do { print("\(Self.TAG): Arming today's remaining notifications") // Get today's date let today = Date() let todayDate = formatDate(today) // Get all notifications for today let todaysNotifications = getNotificationsForDate(todayDate) var armedCount = 0 var skippedCount = 0 for notification in todaysNotifications { // Check if notification is in the future let scheduledTime = Date(timeIntervalSince1970: notification.scheduledTime / 1000) if scheduledTime > Date() { // Check TTL before arming if !ttlEnforcer.validateBeforeArming(notification) { print("\(Self.TAG): Skipping today's notification due to TTL: \(notification.id)") skippedCount += 1 continue } // Arm the notification let armed = armNotification(notification) if armed { armedCount += 1 currentPendingCount += 1 } else { print("\(Self.TAG): Failed to arm today's notification: \(notification.id)") } } } print("\(Self.TAG): Today's notifications: armed=\(armedCount), skipped=\(skippedCount)") } catch { print("\(Self.TAG): Error arming today's remaining notifications: \(error)") } } /** * Arm tomorrow's notifications if within capacity limits * * Only arms tomorrow's notifications if we're within iOS capacity limits */ private func armTomorrowsNotificationsIfWithinCapacity() { do { print("\(Self.TAG): Checking capacity for tomorrow's notifications") // Check if we're within capacity limits if !isWithinCapacityLimits() { print("\(Self.TAG): At capacity limit, skipping tomorrow's notifications") return } // Get tomorrow's date let tomorrow = Calendar.current.date(byAdding: .day, value: 1, to: Date()) ?? Date() let tomorrowDate = formatDate(tomorrow) // Get all notifications for tomorrow let tomorrowsNotifications = getNotificationsForDate(tomorrowDate) var armedCount = 0 var skippedCount = 0 for notification in tomorrowsNotifications { // Check TTL before arming if !ttlEnforcer.validateBeforeArming(notification) { print("\(Self.TAG): Skipping tomorrow's notification due to TTL: \(notification.id)") skippedCount += 1 continue } // Arm the notification let armed = armNotification(notification) if armed { armedCount += 1 currentPendingCount += 1 currentDailyCount += 1 } else { print("\(Self.TAG): Failed to arm tomorrow's notification: \(notification.id)") } // Check capacity after each arm if !isWithinCapacityLimits() { print("\(Self.TAG): Reached capacity limit while arming tomorrow's notifications") break } } print("\(Self.TAG): Tomorrow's notifications: armed=\(armedCount), skipped=\(skippedCount)") } catch { print("\(Self.TAG): Error arming tomorrow's notifications: \(error)") } } /** * Check if we're within iOS capacity limits * * @return true if within limits */ private func isWithinCapacityLimits() -> Bool { let withinPendingLimit = currentPendingCount < Self.IOS_MAX_PENDING_NOTIFICATIONS let withinDailyLimit = currentDailyCount < Self.IOS_MAX_DAILY_NOTIFICATIONS print("\(Self.TAG): Capacity check: pending=\(currentPendingCount)/\(Self.IOS_MAX_PENDING_NOTIFICATIONS), daily=\(currentDailyCount)/\(Self.IOS_MAX_DAILY_NOTIFICATIONS), within=\(withinPendingLimit && withinDailyLimit)") return withinPendingLimit && withinDailyLimit } /** * Update window state by counting current notifications */ private func updateWindowState() { do { print("\(Self.TAG): Updating window state") // Count pending notifications currentPendingCount = countPendingNotifications() // Count today's notifications let today = Date() let todayDate = formatDate(today) currentDailyCount = countNotificationsForDate(todayDate) print("\(Self.TAG): Window state updated: pending=\(currentPendingCount), daily=\(currentDailyCount)") } catch { print("\(Self.TAG): Error updating window state: \(error)") } } // MARK: - Notification Management /** * Arm a notification using UNUserNotificationCenter * * @param notification Notification to arm * @return true if successfully armed */ private func armNotification(_ notification: NotificationContent) -> Bool { do { let content = UNMutableNotificationContent() content.title = notification.title ?? "Daily Notification" content.body = notification.body ?? "Your daily notification is ready" content.sound = UNNotificationSound.default // Create trigger for scheduled time let scheduledTime = Date(timeIntervalSince1970: notification.scheduledTime / 1000) let trigger = UNCalendarNotificationTrigger(dateMatching: Calendar.current.dateComponents([.year, .month, .day, .hour, .minute], from: scheduledTime), repeats: false) // Create request let request = UNNotificationRequest(identifier: notification.id, content: content, trigger: trigger) // Schedule notification UNUserNotificationCenter.current().add(request) { error in if let error = error { print("\(Self.TAG): Failed to arm notification \(notification.id): \(error)") } else { print("\(Self.TAG): Successfully armed notification: \(notification.id)") } } return true } catch { print("\(Self.TAG): Error arming notification \(notification.id): \(error)") return false } } // MARK: - Data Access /** * Count pending notifications * * @return Number of pending notifications */ private func countPendingNotifications() -> Int { do { // This would typically query the storage for pending notifications // For now, we'll use a placeholder implementation return 0 // TODO: Implement actual counting logic } catch { print("\(Self.TAG): Error counting pending notifications: \(error)") return 0 } } /** * Count notifications for a specific date * * @param date Date in YYYY-MM-DD format * @return Number of notifications for the date */ private func countNotificationsForDate(_ date: String) -> Int { do { // This would typically query the storage for notifications on a specific date // For now, we'll use a placeholder implementation return 0 // TODO: Implement actual counting logic } catch { print("\(Self.TAG): Error counting notifications for date: \(date), error: \(error)") return 0 } } /** * Get notifications for a specific date * * @param date Date in YYYY-MM-DD format * @return List of notifications for the date */ private func getNotificationsForDate(_ date: String) -> [NotificationContent] { do { // This would typically query the storage for notifications on a specific date // For now, we'll return an empty array return [] // TODO: Implement actual retrieval logic } catch { print("\(Self.TAG): Error getting notifications for date: \(date), error: \(error)") return [] } } /** * Format date as YYYY-MM-DD * * @param date Date to format * @return Formatted date string */ private func formatDate(_ date: Date) -> String { let formatter = DateFormatter() formatter.dateFormat = "yyyy-MM-dd" return formatter.string(from: date) } // MARK: - Public Methods /** * Get rolling window statistics * * @return Statistics string */ func getRollingWindowStats() -> String { do { return String(format: "Rolling window stats: pending=%d/%d, daily=%d/%d, platform=iOS", currentPendingCount, Self.IOS_MAX_PENDING_NOTIFICATIONS, currentDailyCount, Self.IOS_MAX_DAILY_NOTIFICATIONS) } catch { print("\(Self.TAG): Error getting rolling window stats: \(error)") return "Error retrieving rolling window statistics" } } /** * Force window maintenance (for testing or manual triggers) */ func forceMaintenance() { print("\(Self.TAG): Forcing rolling window maintenance") lastMaintenanceTime = Date.distantPast // Reset maintenance time maintainRollingWindow() } /** * Check if window maintenance is needed * * @return true if maintenance is needed */ func isMaintenanceNeeded() -> Bool { let currentTime = Date() return currentTime.timeIntervalSince(lastMaintenanceTime) >= Self.WINDOW_MAINTENANCE_INTERVAL_SECONDS } /** * Get time until next maintenance * * @return Seconds until next maintenance */ func getTimeUntilNextMaintenance() -> TimeInterval { let currentTime = Date() let nextMaintenanceTime = lastMaintenanceTime.addingTimeInterval(Self.WINDOW_MAINTENANCE_INTERVAL_SECONDS) return max(0, nextMaintenanceTime.timeIntervalSince(currentTime)) } }