/** * NotificationContentDAO.swift * * Data Access Object (DAO) for NotificationContent Core Data entity * Provides CRUD operations and query helpers for notification content * * @author Matthew Raymer * @version 1.0.0 * @created 2025-12-08 */ import Foundation import CoreData /** * Extension providing DAO methods for NotificationContent entity * * This extension adds CRUD operations and query helpers to the * auto-generated NotificationContent Core Data class. */ extension NotificationContentEntity { // MARK: - Constants private static let TAG = "DNP-NOTIFICATION-CONTENT-DAO" // MARK: - Create/Insert Methods /** * Create a new NotificationContent entity in the given context * * @param context Core Data managed object context * @param id Unique notification identifier * @param pluginVersion Plugin version string * @param timesafariDid TimeSafari device ID * @param notificationType Type of notification * @param title Notification title * @param body Notification body * @param scheduledTime Scheduled delivery time (Date) * @param timezone Timezone string * @param priority Notification priority (0-10) * @param vibrationEnabled Whether vibration is enabled * @param soundEnabled Whether sound is enabled * @param mediaUrl URL to media content * @param encryptedContent Encrypted content string * @param encryptionKeyId Encryption key identifier * @param ttlSeconds Time-to-live in seconds * @param deliveryStatus Current delivery status * @param deliveryAttempts Number of delivery attempts * @param metadata Additional metadata (JSON string) * @return Created NotificationContent entity */ static func create( in context: NSManagedObjectContext, id: String, pluginVersion: String? = nil, timesafariDid: String? = nil, notificationType: String? = nil, title: String? = nil, body: String? = nil, scheduledTime: Date, timezone: String? = nil, priority: Int32 = 0, vibrationEnabled: Bool = false, soundEnabled: Bool = true, mediaUrl: String? = nil, encryptedContent: String? = nil, encryptionKeyId: String? = nil, ttlSeconds: Int64 = 604800, // 7 days default deliveryStatus: String? = nil, deliveryAttempts: Int32 = 0, metadata: String? = nil ) -> NotificationContentEntity { let entity = NotificationContentEntity(context: context) let now = Date() entity.id = id entity.pluginVersion = pluginVersion entity.timesafariDid = timesafariDid entity.notificationType = notificationType entity.title = title entity.body = body entity.scheduledTime = scheduledTime entity.timezone = timezone entity.priority = priority entity.vibrationEnabled = vibrationEnabled entity.soundEnabled = soundEnabled entity.mediaUrl = mediaUrl entity.encryptedContent = encryptedContent entity.encryptionKeyId = encryptionKeyId entity.createdAt = now entity.updatedAt = now entity.ttlSeconds = ttlSeconds entity.deliveryStatus = deliveryStatus entity.deliveryAttempts = deliveryAttempts entity.lastDeliveryAttempt = nil entity.userInteractionCount = 0 entity.lastUserInteraction = nil entity.metadata = metadata print("\(Self.TAG): Created NotificationContent with id: \(id)") return entity } /** * Create from dictionary representation * * @param context Core Data managed object context * @param dict Dictionary with notification data * @return Created NotificationContent entity or nil */ static func create( in context: NSManagedObjectContext, from dict: [String: Any] ) -> NotificationContentEntity? { guard let id = dict["id"] as? String else { print("\(Self.TAG): Missing required 'id' field") return nil } // Convert scheduledTime from epoch milliseconds or Date let scheduledTime: Date if let timeMillis = dict["scheduledTime"] as? Int64 { scheduledTime = DailyNotificationDataConversions.dateFromEpochMillis(timeMillis) } else if let timeDate = dict["scheduledTime"] as? Date { scheduledTime = timeDate } else { print("\(Self.TAG): Missing or invalid 'scheduledTime' field") return nil } // Convert createdAt/updatedAt if present let createdAt: Date if let createdMillis = dict["createdAt"] as? Int64 { createdAt = DailyNotificationDataConversions.dateFromEpochMillis(createdMillis) } else if let createdDate = dict["createdAt"] as? Date { createdAt = createdDate } else { createdAt = Date() } let updatedAt: Date if let updatedMillis = dict["updatedAt"] as? Int64 { updatedAt = DailyNotificationDataConversions.dateFromEpochMillis(updatedMillis) } else if let updatedDate = dict["updatedAt"] as? Date { updatedAt = updatedDate } else { updatedAt = Date() } let entity = NotificationContentEntity(context: context) entity.id = id entity.pluginVersion = dict["pluginVersion"] as? String entity.timesafariDid = dict["timesafariDid"] as? String entity.notificationType = dict["notificationType"] as? String entity.title = dict["title"] as? String entity.body = dict["body"] as? String entity.scheduledTime = scheduledTime entity.timezone = dict["timezone"] as? String entity.priority = DailyNotificationDataConversions.int32FromInt( dict["priority"] as? Int ?? 0 ) entity.vibrationEnabled = dict["vibrationEnabled"] as? Bool ?? false entity.soundEnabled = dict["soundEnabled"] as? Bool ?? true entity.mediaUrl = dict["mediaUrl"] as? String entity.encryptedContent = dict["encryptedContent"] as? String entity.encryptionKeyId = dict["encryptionKeyId"] as? String entity.createdAt = createdAt entity.updatedAt = updatedAt entity.ttlSeconds = dict["ttlSeconds"] as? Int64 ?? 604800 entity.deliveryStatus = dict["deliveryStatus"] as? String entity.deliveryAttempts = DailyNotificationDataConversions.int32FromInt( dict["deliveryAttempts"] as? Int ?? 0 ) if let lastAttemptMillis = dict["lastDeliveryAttempt"] as? Int64 { entity.lastDeliveryAttempt = DailyNotificationDataConversions.dateFromEpochMillis(lastAttemptMillis) } entity.userInteractionCount = DailyNotificationDataConversions.int32FromInt( dict["userInteractionCount"] as? Int ?? 0 ) if let lastInteractionMillis = dict["lastUserInteraction"] as? Int64 { entity.lastUserInteraction = DailyNotificationDataConversions.dateFromEpochMillis(lastInteractionMillis) } entity.metadata = dict["metadata"] as? String return entity } // MARK: - Read/Query Methods /** * Fetch NotificationContent by ID * * @param context Core Data managed object context * @param id Notification ID * @return NotificationContent entity or nil */ static func fetch( by id: String, in context: NSManagedObjectContext ) -> NotificationContentEntity? { let request: NSFetchRequest = NotificationContentEntity.fetchRequest() request.predicate = NSPredicate(format: "id == %@", id) request.fetchLimit = 1 do { let results = try context.fetch(request) return results.first } catch { print("\(Self.TAG): Error fetching by id: \(error.localizedDescription)") return nil } } /** * Fetch all NotificationContent entities * * @param context Core Data managed object context * @return Array of NotificationContent entities */ static func fetchAll( in context: NSManagedObjectContext ) -> [NotificationContentEntity] { let request: NSFetchRequest = NotificationContentEntity.fetchRequest() do { return try context.fetch(request) } catch { print("\(Self.TAG): Error fetching all: \(error.localizedDescription)") return [] } } /** * Query by timesafariDid * * @param context Core Data managed object context * @param timesafariDid TimeSafari device ID * @return Array of NotificationContent entities */ static func query( by timesafariDid: String, in context: NSManagedObjectContext ) -> [NotificationContentEntity] { let request: NSFetchRequest = NotificationContentEntity.fetchRequest() request.predicate = NSPredicate(format: "timesafariDid == %@", timesafariDid) request.sortDescriptors = [NSSortDescriptor(key: "scheduledTime", ascending: true)] do { return try context.fetch(request) } catch { print("\(Self.TAG): Error querying by timesafariDid: \(error.localizedDescription)") return [] } } /** * Query by notificationType * * @param context Core Data managed object context * @param notificationType Notification type string * @return Array of NotificationContent entities */ static func queryByNotificationType( _ notificationType: String, in context: NSManagedObjectContext ) -> [NotificationContentEntity] { let request: NSFetchRequest = NotificationContentEntity.fetchRequest() request.predicate = NSPredicate(format: "notificationType == %@", notificationType) request.sortDescriptors = [NSSortDescriptor(key: "scheduledTime", ascending: true)] do { return try context.fetch(request) } catch { print("\(Self.TAG): Error querying by notificationType: \(error.localizedDescription)") return [] } } /** * Query by scheduledTime range * * @param context Core Data managed object context * @param startDate Start date (inclusive) * @param endDate End date (inclusive) * @return Array of NotificationContent entities */ static func query( scheduledTimeBetween startDate: Date, and endDate: Date, in context: NSManagedObjectContext ) -> [NotificationContentEntity] { let request: NSFetchRequest = NotificationContentEntity.fetchRequest() request.predicate = NSPredicate( format: "scheduledTime >= %@ AND scheduledTime <= %@", startDate as NSDate, endDate as NSDate ) request.sortDescriptors = [NSSortDescriptor(key: "scheduledTime", ascending: true)] do { return try context.fetch(request) } catch { print("\(Self.TAG): Error querying by scheduledTime range: \(error.localizedDescription)") return [] } } /** * Query by deliveryStatus * * @param context Core Data managed object context * @param deliveryStatus Delivery status string * @return Array of NotificationContent entities */ static func queryByDeliveryStatus( _ deliveryStatus: String, in context: NSManagedObjectContext ) -> [NotificationContentEntity] { let request: NSFetchRequest = NotificationContentEntity.fetchRequest() request.predicate = NSPredicate(format: "deliveryStatus == %@", deliveryStatus) request.sortDescriptors = [NSSortDescriptor(key: "scheduledTime", ascending: true)] do { return try context.fetch(request) } catch { print("\(Self.TAG): Error querying by deliveryStatus: \(error.localizedDescription)") return [] } } /** * Query notifications ready for delivery (scheduledTime <= currentTime) * * @param context Core Data managed object context * @param currentTime Current time for comparison * @return Array of NotificationContent entities */ static func queryReadyForDelivery( currentTime: Date, in context: NSManagedObjectContext ) -> [NotificationContentEntity] { let request: NSFetchRequest = NotificationContentEntity.fetchRequest() request.predicate = NSPredicate(format: "scheduledTime <= %@", currentTime as NSDate) request.sortDescriptors = [NSSortDescriptor(key: "scheduledTime", ascending: true)] do { return try context.fetch(request) } catch { print("\(Self.TAG): Error querying ready for delivery: \(error.localizedDescription)") return [] } } // MARK: - Update Methods /** * Update this entity's updatedAt timestamp */ func touch() { self.updatedAt = Date() } /** * Update delivery status and increment attempts * * @param status New delivery status */ func updateDeliveryStatus(_ status: String) { self.deliveryStatus = status self.deliveryAttempts += 1 self.lastDeliveryAttempt = Date() self.touch() } /** * Record user interaction */ func recordUserInteraction() { self.userInteractionCount += 1 self.lastUserInteraction = Date() self.touch() } // MARK: - Delete Methods /** * Delete NotificationContent by ID * * @param context Core Data managed object context * @param id Notification ID * @return true if deleted, false otherwise */ static func delete( by id: String, in context: NSManagedObjectContext ) -> Bool { guard let entity = fetch(by: id, in: context) else { return false } context.delete(entity) do { try context.save() print("\(Self.TAG): Deleted NotificationContent with id: \(id)") return true } catch { print("\(Self.TAG): Error deleting: \(error.localizedDescription)") context.rollback() return false } } /** * Delete all NotificationContent entities * * @param context Core Data managed object context * @return Number of entities deleted */ static func deleteAll( in context: NSManagedObjectContext ) -> Int { let request: NSFetchRequest = NotificationContentEntity.fetchRequest() let deleteRequest = NSBatchDeleteRequest(fetchRequest: request) do { let result = try context.execute(deleteRequest) as? NSBatchDeleteResult try context.save() let count = result?.result as? Int ?? 0 print("\(Self.TAG): Deleted \(count) NotificationContent entities") return count } catch { print("\(Self.TAG): Error deleting all: \(error.localizedDescription)") context.rollback() return 0 } } }