/** * HistoryDAO.swift * * Data Access Object (DAO) for History Core Data entity * Provides helper methods for recording recovery and operation history * * @author Matthew Raymer * @version 1.0.0 * @created 2025-12-08 */ import Foundation import CoreData /** * Extension providing DAO methods for History entity * * This extension adds helper methods for recording operation history * including recovery operations, errors, and metrics. */ extension History { // MARK: - Constants private static let TAG = "DNP-HISTORY-DAO" // MARK: - Create/Insert Methods /** * Create a new History entity in the given context * * @param context Core Data managed object context * @param id Unique history identifier (UUID recommended) * @param refId Reference ID (e.g., notification ID, schedule ID) * @param kind History kind (e.g., "recovery", "fetch", "notify") * @param occurredAt When the event occurred * @param durationMs Duration in milliseconds * @param outcome Outcome string (e.g., "success", "failure", "skipped") * @param diagJson Diagnostic JSON string with additional details * @return Created History entity */ static func create( in context: NSManagedObjectContext, id: String, refId: String? = nil, kind: String, occurredAt: Date, durationMs: Int32 = 0, outcome: String, diagJson: String? = nil ) -> History { let entity = History(context: context) entity.id = id entity.refId = refId entity.kind = kind entity.occurredAt = occurredAt entity.durationMs = durationMs entity.outcome = outcome entity.diagJson = diagJson print("\(Self.TAG): Created History record: kind=\(kind), outcome=\(outcome)") return entity } /** * Create history record from dictionary * * @param context Core Data managed object context * @param dict Dictionary with history data * @return Created History entity or nil */ static func create( in context: NSManagedObjectContext, from dict: [String: Any] ) -> History? { guard let id = dict["id"] as? String, let kind = dict["kind"] as? String, let outcome = dict["outcome"] as? String else { print("\(Self.TAG): Missing required fields") return nil } // Convert occurredAt from epoch milliseconds or Date let occurredAt: Date if let timeMillis = dict["occurredAt"] as? Int64 { occurredAt = DailyNotificationDataConversions.dateFromEpochMillis(timeMillis) } else if let timeDate = dict["occurredAt"] as? Date { occurredAt = timeDate } else { occurredAt = Date() } let entity = History(context: context) entity.id = id entity.refId = dict["refId"] as? String entity.kind = kind entity.occurredAt = occurredAt entity.durationMs = DailyNotificationDataConversions.int32FromInt( dict["durationMs"] as? Int ?? 0 ) entity.outcome = outcome // Convert diagJson from dictionary if needed if let diagDict = dict["diagJson"] as? [String: Any] { entity.diagJson = DailyNotificationDataConversions.jsonStringFromDictionary(diagDict) } else if let diagString = dict["diagJson"] as? String { entity.diagJson = diagString } return entity } /** * Record recovery history with metrics * * @param context Core Data managed object context * @param scenario Recovery scenario * @param missedCount Number of missed notifications * @param rescheduledCount Number of rescheduled notifications * @param verifiedCount Number of verified notifications * @param errors Number of errors * @param startTime When recovery started * @param endTime When recovery ended * @return Created History entity */ static func recordRecovery( in context: NSManagedObjectContext, scenario: String, missedCount: Int, rescheduledCount: Int, verifiedCount: Int, errors: Int, startTime: Date, endTime: Date ) -> History { let durationMs = Int32((endTime.timeIntervalSince(startTime) * 1000).rounded()) let diagJson: [String: Any] = [ "scenario": scenario, "missedCount": missedCount, "rescheduledCount": rescheduledCount, "verifiedCount": verifiedCount, "errors": errors, "durationMs": durationMs ] let diagJsonString = DailyNotificationDataConversions.jsonStringFromDictionary(diagJson) ?? "{}" return create( in: context, id: UUID().uuidString, kind: "recovery", occurredAt: endTime, durationMs: durationMs, outcome: errors > 0 ? "partial_success" : "success", diagJson: diagJsonString ) } /** * Record recovery failure * * @param context Core Data managed object context * @param error Error that occurred * @param scenario Recovery scenario (if known) * @return Created History entity */ static func recordRecoveryFailure( in context: NSManagedObjectContext, error: Error, scenario: String? = nil ) -> History { var errorInfo: [String: Any] = [ "error": error.localizedDescription, "errorType": String(describing: type(of: error)) ] // Add scenario if provided if let scenario = scenario { errorInfo["scenario"] = scenario } // Add error details if available if let nsError = error as NSError? { errorInfo["errorCode"] = nsError.code errorInfo["errorDomain"] = nsError.domain if let userInfo = nsError.userInfo as? [String: Any] { errorInfo["userInfo"] = userInfo } } let diagJsonString = DailyNotificationDataConversions.jsonStringFromDictionary(errorInfo) ?? "{}" return create( in: context, id: UUID().uuidString, kind: "recovery", occurredAt: Date(), outcome: "failure", diagJson: diagJsonString ) } // MARK: - Read/Query Methods /** * Fetch History by ID * * @param context Core Data managed object context * @param id History ID * @return History entity or nil */ static func fetch( by id: String, in context: NSManagedObjectContext ) -> History? { let request: NSFetchRequest = History.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 } } /** * Query by kind * * @param context Core Data managed object context * @param kind History kind * @return Array of History entities */ static func query( by kind: String, in context: NSManagedObjectContext ) -> [History] { let request: NSFetchRequest = History.fetchRequest() request.predicate = NSPredicate(format: "kind == %@", kind) request.sortDescriptors = [NSSortDescriptor(key: "occurredAt", ascending: false)] do { return try context.fetch(request) } catch { print("\(Self.TAG): Error querying by kind: \(error.localizedDescription)") return [] } } /** * Query by refId * * @param context Core Data managed object context * @param refId Reference ID * @return Array of History entities */ static func queryByRefId( _ refId: String, in context: NSManagedObjectContext ) -> [History] { let request: NSFetchRequest = History.fetchRequest() request.predicate = NSPredicate(format: "refId == %@", refId) request.sortDescriptors = [NSSortDescriptor(key: "occurredAt", ascending: false)] do { return try context.fetch(request) } catch { print("\(Self.TAG): Error querying by refId: \(error.localizedDescription)") return [] } } /** * Query by outcome * * @param context Core Data managed object context * @param outcome Outcome string * @return Array of History entities */ static func queryByOutcome( _ outcome: String, in context: NSManagedObjectContext ) -> [History] { let request: NSFetchRequest = History.fetchRequest() request.predicate = NSPredicate(format: "outcome == %@", outcome) request.sortDescriptors = [NSSortDescriptor(key: "occurredAt", ascending: false)] do { return try context.fetch(request) } catch { print("\(Self.TAG): Error querying by outcome: \(error.localizedDescription)") return [] } } }