/** * DailyNotificationStorage.swift * * Storage management for notification content and settings * Implements tiered storage: UserDefaults (quick) + CoreData (structured) * * @author Matthew Raymer * @version 1.0.0 */ import Foundation import CoreData /** * Manages storage for notification content and settings * * This class implements the tiered storage approach: * - Tier 1: UserDefaults for quick access to settings and recent data * - Tier 2: CoreData for structured notification content * - Tier 3: File system for large assets (future use) */ class DailyNotificationStorage { // MARK: - Constants private static let TAG = "DailyNotificationStorage" private static let PREFS_NAME = "DailyNotificationPrefs" private static let KEY_NOTIFICATIONS = "notifications" private static let KEY_SETTINGS = "settings" private static let KEY_LAST_FETCH = "last_fetch" private static let KEY_ADAPTIVE_SCHEDULING = "adaptive_scheduling" private static let KEY_LAST_SUCCESSFUL_RUN = "last_successful_run" private static let KEY_BGTASK_EARLIEST_BEGIN = "bgtask_earliest_begin" private static let MAX_CACHE_SIZE = 100 // Maximum notifications to keep private static let CACHE_CLEANUP_INTERVAL: TimeInterval = 24 * 60 * 60 // 24 hours // MARK: - Properties private let userDefaults: UserDefaults private let database: DailyNotificationDatabase private var notificationCache: [String: NotificationContent] = [:] private var notificationList: [NotificationContent] = [] private let cacheQueue = DispatchQueue(label: "com.timesafari.dailynotification.storage.cache", attributes: .concurrent) // MARK: - Initialization /** * Initialize storage with database path * * @param databasePath Path to SQLite database */ init(databasePath: String? = nil) { self.userDefaults = UserDefaults.standard let path = databasePath ?? Self.getDefaultDatabasePath() self.database = DailyNotificationDatabase(path: path) loadNotificationsFromStorage() cleanupOldNotifications() } /** * Get default database path */ private static func getDefaultDatabasePath() -> String { let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] return documentsPath.appendingPathComponent("daily_notifications.db").path } /** * Get current database path * * @return Database path */ func getDatabasePath() -> String { return database.getPath() } // MARK: - Notification Content Management /** * Save notification content to storage * * @param content Notification content to save */ func saveNotificationContent(_ content: NotificationContent) { cacheQueue.async(flags: .barrier) { print("\(Self.TAG): Saving notification: \(content.id)") // Add to cache self.notificationCache[content.id] = content // Add to list and sort by scheduled time self.notificationList.removeAll { $0.id == content.id } self.notificationList.append(content) self.notificationList.sort { $0.scheduledTime < $1.scheduledTime } // Persist to UserDefaults self.saveNotificationsToStorage() // Persist to CoreData self.database.saveNotificationContent(content) print("\(Self.TAG): Notification saved successfully") } } /** * Get notification content by ID * * @param id Notification ID * @return Notification content or nil if not found */ func getNotificationContent(id: String) -> NotificationContent? { return cacheQueue.sync { return notificationCache[id] } } /** * Get the last notification that was delivered * * @return Last notification or nil if none exists */ func getLastNotification() -> NotificationContent? { return cacheQueue.sync { if notificationList.isEmpty { return nil } // Find the most recent delivered notification let currentTime = Int64(Date().timeIntervalSince1970 * 1000) // milliseconds for notification in notificationList.reversed() { if notification.scheduledTime <= currentTime { return notification } } return nil } } /** * Get all notifications * * @return Array of all notifications */ func getAllNotifications() -> [NotificationContent] { return cacheQueue.sync { return Array(notificationList) } } /** * Get notifications that are ready to be displayed * * @return Array of ready notifications */ func getReadyNotifications() -> [NotificationContent] { return cacheQueue.sync { let currentTime = Int64(Date().timeIntervalSince1970 * 1000) // milliseconds return notificationList.filter { $0.scheduledTime <= currentTime } } } /** * Delete notification content by ID * * @param id Notification ID */ func deleteNotificationContent(id: String) { cacheQueue.async(flags: .barrier) { print("\(Self.TAG): Deleting notification: \(id)") self.notificationCache.removeValue(forKey: id) self.notificationList.removeAll { $0.id == id } self.saveNotificationsToStorage() self.database.deleteNotificationContent(id: id) print("\(Self.TAG): Notification deleted successfully") } } /** * Clear all notification content */ func clearAllNotifications() { cacheQueue.async(flags: .barrier) { print("\(Self.TAG): Clearing all notifications") self.notificationCache.removeAll() self.notificationList.removeAll() self.userDefaults.removeObject(forKey: Self.KEY_NOTIFICATIONS) self.database.clearAllNotifications() print("\(Self.TAG): All notifications cleared") } } // MARK: - Settings Management /** * Save settings * * @param settings Settings dictionary */ func saveSettings(_ settings: [String: Any]) { if let data = try? JSONSerialization.data(withJSONObject: settings) { userDefaults.set(data, forKey: Self.KEY_SETTINGS) print("\(Self.TAG): Settings saved") } } /** * Get settings * * @return Settings dictionary or empty dictionary */ func getSettings() -> [String: Any] { guard let data = userDefaults.data(forKey: Self.KEY_SETTINGS), let settings = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { return [:] } return settings } // MARK: - Background Task Tracking /** * Save last successful BGTask run timestamp * * @param timestamp Timestamp in milliseconds */ func saveLastSuccessfulRun(timestamp: Int64) { userDefaults.set(timestamp, forKey: Self.KEY_LAST_SUCCESSFUL_RUN) print("\(Self.TAG): Last successful run saved: \(timestamp)") } /** * Get last successful BGTask run timestamp * * @return Timestamp in milliseconds or nil */ func getLastSuccessfulRun() -> Int64? { let timestamp = userDefaults.object(forKey: Self.KEY_LAST_SUCCESSFUL_RUN) as? Int64 return timestamp } /** * Save BGTask earliest begin date * * @param timestamp Timestamp in milliseconds */ func saveBGTaskEarliestBegin(timestamp: Int64) { userDefaults.set(timestamp, forKey: Self.KEY_BGTASK_EARLIEST_BEGIN) print("\(Self.TAG): BGTask earliest begin saved: \(timestamp)") } /** * Get BGTask earliest begin date * * @return Timestamp in milliseconds or nil */ func getBGTaskEarliestBegin() -> Int64? { let timestamp = userDefaults.object(forKey: Self.KEY_BGTASK_EARLIEST_BEGIN) as? Int64 return timestamp } // MARK: - Private Helper Methods /** * Load notifications from UserDefaults */ private func loadNotificationsFromStorage() { guard let data = userDefaults.data(forKey: Self.KEY_NOTIFICATIONS), let notifications = try? JSONDecoder().decode([NotificationContent].self, from: data) else { print("\(Self.TAG): No notifications found in storage") return } cacheQueue.async(flags: .barrier) { self.notificationList = notifications for notification in notifications { self.notificationCache[notification.id] = notification } print("\(Self.TAG): Loaded \(notifications.count) notifications from storage") } } /** * Save notifications to UserDefaults */ private func saveNotificationsToStorage() { guard let data = try? JSONEncoder().encode(notificationList) else { print("\(Self.TAG): Failed to encode notifications") return } userDefaults.set(data, forKey: Self.KEY_NOTIFICATIONS) } /** * Cleanup old notifications */ private func cleanupOldNotifications() { cacheQueue.async(flags: .barrier) { let currentTime = Int64(Date().timeIntervalSince1970 * 1000) // milliseconds let cutoffTime = currentTime - Int64(Self.CACHE_CLEANUP_INTERVAL * 1000) self.notificationList.removeAll { notification in let isOld = notification.scheduledTime < cutoffTime if isOld { self.notificationCache.removeValue(forKey: notification.id) } return isOld } // Limit cache size if self.notificationList.count > Self.MAX_CACHE_SIZE { let excess = self.notificationList.count - Self.MAX_CACHE_SIZE for i in 0..