// // DailyNotificationCallbacks.swift // DailyNotificationPlugin // // Created by Matthew Raymer on 2025-09-22 // Copyright © 2025 TimeSafari. All rights reserved. // import Foundation import CoreData /** * Callback management for iOS Daily Notification Plugin * Implements HTTP and local callback delivery with error handling * * @author Matthew Raymer * @version 1.1.0 * @created 2025-09-22 09:22:32 UTC */ extension DailyNotificationPlugin { // MARK: - Callback Management @objc func registerCallback(_ call: CAPPluginCall) { guard let name = call.getString("name"), let callbackConfig = call.getObject("callback") else { call.reject("Callback name and config required") return } print("DNP-PLUGIN: Registering callback: \(name)") do { try registerCallback(name: name, config: callbackConfig) call.resolve() } catch { print("DNP-PLUGIN: Failed to register callback: \(error)") call.reject("Callback registration failed: \(error.localizedDescription)") } } @objc func unregisterCallback(_ call: CAPPluginCall) { guard let name = call.getString("name") else { call.reject("Callback name required") return } print("DNP-PLUGIN: Unregistering callback: \(name)") do { try unregisterCallback(name: name) call.resolve() } catch { print("DNP-PLUGIN: Failed to unregister callback: \(error)") call.reject("Callback unregistration failed: \(error.localizedDescription)") } } @objc func getRegisteredCallbacks(_ call: CAPPluginCall) { Task { do { let callbacks = try await getRegisteredCallbacks() call.resolve(["callbacks": callbacks]) } catch { print("DNP-PLUGIN: Failed to get registered callbacks: \(error)") call.reject("Callback retrieval failed: \(error.localizedDescription)") } } } // MARK: - Content Management @objc func getContentCache(_ call: CAPPluginCall) { Task { do { let cache = try await getContentCache() call.resolve(cache) } catch { print("DNP-PLUGIN: Failed to get content cache: \(error)") call.reject("Content cache retrieval failed: \(error.localizedDescription)") } } } @objc func clearContentCache(_ call: CAPPluginCall) { Task { do { try await clearContentCache() call.resolve() } catch { print("DNP-PLUGIN: Failed to clear content cache: \(error)") call.reject("Content cache clearing failed: \(error.localizedDescription)") } } } @objc func getContentHistory(_ call: CAPPluginCall) { Task { do { let history = try await getContentHistory() call.resolve(["history": history]) } catch { print("DNP-PLUGIN: Failed to get content history: \(error)") call.reject("Content history retrieval failed: \(error.localizedDescription)") } } } // MARK: - Private Callback Implementation private func fireCallbacks(eventType: String, payload: [String: Any]) async throws { // Get registered callbacks from Core Data let context = persistenceController.container.viewContext let request: NSFetchRequest = Callback.fetchRequest() request.predicate = NSPredicate(format: "enabled == YES") let callbacks = try context.fetch(request) for callback in callbacks { do { try await deliverCallback(callback: callback, eventType: eventType, payload: payload) } catch { print("DNP-CB-FAILURE: Callback \(callback.id ?? "unknown") failed: \(error)") } } } private func deliverCallback(callback: Callback, eventType: String, payload: [String: Any]) async throws { guard let callbackId = callback.id, let kind = callback.kind else { return } let event = [ "id": callbackId, "at": Date().timeIntervalSince1970, "type": eventType, "payload": payload ] as [String: Any] switch kind { case "http": try await deliverHttpCallback(callback: callback, event: event) case "local": try await deliverLocalCallback(callback: callback, event: event) default: print("DNP-CB-UNKNOWN: Unknown callback kind: \(kind)") } } private func deliverHttpCallback(callback: Callback, event: [String: Any]) async throws { guard let target = callback.target, let url = URL(string: target) else { throw NSError(domain: "DailyNotificationPlugin", code: 1, userInfo: [NSLocalizedDescriptionKey: "Invalid callback target"]) } var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.httpBody = try JSONSerialization.data(withJSONObject: event) let (_, response) = try await URLSession.shared.data(for: request) guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { throw NSError(domain: "DailyNotificationPlugin", code: 2, userInfo: [NSLocalizedDescriptionKey: "HTTP callback failed"]) } print("DNP-CB-HTTP-SUCCESS: HTTP callback delivered to \(target)") } private func deliverLocalCallback(callback: Callback, event: [String: Any]) async throws { // Local callback implementation would go here print("DNP-CB-LOCAL: Local callback delivered for \(callback.id ?? "unknown")") } private func registerCallback(name: String, config: [String: Any]) throws { let context = persistenceController.container.viewContext let callback = Callback(context: context) callback.id = name callback.kind = config["kind"] as? String ?? "local" callback.target = config["target"] as? String ?? "" callback.enabled = true callback.createdAt = Date() try context.save() print("DNP-CB-REGISTER: Callback \(name) registered") } private func unregisterCallback(name: String) throws { let context = persistenceController.container.viewContext let request: NSFetchRequest = Callback.fetchRequest() request.predicate = NSPredicate(format: "id == %@", name) let callbacks = try context.fetch(request) for callback in callbacks { context.delete(callback) } try context.save() print("DNP-CB-UNREGISTER: Callback \(name) unregistered") } private func getRegisteredCallbacks() async throws -> [String] { let context = persistenceController.container.viewContext let request: NSFetchRequest = Callback.fetchRequest() let callbacks = try context.fetch(request) return callbacks.compactMap { $0.id } } private func getContentCache() async throws -> [String: Any] { guard let latestContent = try await getLatestContent() else { return [:] } return latestContent } private func clearContentCache() async throws { let context = persistenceController.container.viewContext let request: NSFetchRequest = ContentCache.fetchRequest() let results = try context.fetch(request) for content in results { context.delete(content) } try context.save() print("DNP-CACHE-CLEAR: Content cache cleared") } private func getContentHistory() async throws -> [[String: Any]] { let context = persistenceController.container.viewContext let request: NSFetchRequest = History.fetchRequest() request.sortDescriptors = [NSSortDescriptor(keyPath: \History.occurredAt, ascending: false)] request.fetchLimit = 100 let results = try context.fetch(request) return results.map { history in [ "id": history.id ?? "", "kind": history.kind ?? "", "occurredAt": history.occurredAt?.timeIntervalSince1970 ?? 0, "outcome": history.outcome ?? "", "durationMs": history.durationMs ] } } private func getHealthStatus() async throws -> [String: Any] { let context = persistenceController.container.viewContext // Get next runs (simplified) let nextRuns = [Date().addingTimeInterval(3600).timeIntervalSince1970, Date().addingTimeInterval(86400).timeIntervalSince1970] // Get recent history let historyRequest: NSFetchRequest = History.fetchRequest() historyRequest.predicate = NSPredicate(format: "occurredAt >= %@", Date().addingTimeInterval(-86400) as NSDate) historyRequest.sortDescriptors = [NSSortDescriptor(keyPath: \History.occurredAt, ascending: false)] historyRequest.fetchLimit = 10 let recentHistory = try context.fetch(historyRequest) let lastOutcomes = recentHistory.map { $0.outcome ?? "" } // Get cache age let cacheRequest: NSFetchRequest = ContentCache.fetchRequest() cacheRequest.sortDescriptors = [NSSortDescriptor(keyPath: \ContentCache.fetchedAt, ascending: false)] cacheRequest.fetchLimit = 1 let latestCache = try context.fetch(cacheRequest).first let cacheAgeMs = latestCache?.fetchedAt?.timeIntervalSinceNow ?? 0 return [ "nextRuns": nextRuns, "lastOutcomes": lastOutcomes, "cacheAgeMs": abs(cacheAgeMs * 1000), "staleArmed": abs(cacheAgeMs) > 3600, "queueDepth": recentHistory.count, "circuitBreakers": [ "total": 0, "open": 0, "failures": 0 ], "performance": [ "avgFetchTime": 0, "avgNotifyTime": 0, "successRate": 1.0 ] ] } }