//
//  DailyNotificationReactivationManager.swift
//  DailyNotificationPlugin
//
//  Created by Matthew Raymer on 2025-12-08
//  Copyright © 2025 TimeSafari. All rights reserved.
//

import Foundation
import UserNotifications
import BackgroundTasks
import CoreData

/**
 * DailyNotificationReactivationManager.swift
 * 
 * Manages recovery of notifications on app launch
 * 
 * This class implements comprehensive recovery logic for iOS app lifecycle scenarios:
 * - Cold Start Recovery: Detects and recovers missed notifications after app termination
 * - Termination Recovery: Full recovery when app was terminated by system
 * - Boot Recovery: Recovery after device reboot
 * - Warm Start: Optimized path when no recovery needed
 * 
 * Features:
 * - Scenario detection (none, cold start, warm start, termination, boot)
 * - Missed notification detection and marking
 * - Future notification verification and rescheduling
 * - Comprehensive error handling (non-fatal, graceful degradation)
 * - Execution time tracking and metrics recording
 * - History persistence via Core Data
 * 
 * Implements:
 * - [Plugin Requirements §3.1.2 - App Cold Start](../../doc/alarms/03-plugin-requirements.md#312-app-cold-start) (iOS equivalent)
 * - [Plugin Requirements §3.1.3 - App Termination](../../doc/alarms/03-plugin-requirements.md#313-app-termination) (iOS equivalent)
 * - [Plugin Requirements §3.1.4 - Device Boot](../../doc/alarms/03-plugin-requirements.md#314-device-boot) (iOS equivalent)
 * 
 * Platform Reference: 
 * - [iOS §3.1.1](../../doc/alarms/01-platform-capability-reference.md#311-notifications-survive-app-termination)
 * - [iOS Recovery Scenario Mapping](../../doc/ios-recovery-scenario-mapping.md)
 * 
 * Error Handling:
 * - All database errors are caught and handled gracefully (non-fatal)
 * - All notification center errors are caught and handled gracefully (non-fatal)
 * - All scheduling errors are caught and handled gracefully (non-fatal)
 * - Partial results returned when some operations fail
 * - App never crashes due to recovery errors
 * 
 * Thread Safety:
 * - All operations are async/await based
 * - Recovery runs in background Task to avoid blocking app startup
 * - Timeout protection (2 seconds default) prevents hanging
 * 
 * @author Matthew Raymer
 * @version 1.0.0
 * @created 2025-12-08
 * @lastUpdated 2025-12-08
 */
class DailyNotificationReactivationManager {
    
    // MARK: - Constants
    
    private static let TAG = "DNP-REACTIVATION"
    private static let RECOVERY_TIMEOUT_SECONDS: TimeInterval = 2.0
    private static let LAST_LAUNCH_TIME_KEY = "DNP_LAST_LAUNCH_TIME"
    private static let BOOT_DETECTION_THRESHOLD_SECONDS: TimeInterval = 60.0 // 1 minute
    
    // MARK: - Properties
    
    private let notificationCenter: UNUserNotificationCenter
    private let database: DailyNotificationDatabase
    private let storage: DailyNotificationStorage
    private let scheduler: DailyNotificationScheduler
    
    // MARK: - Initialization
    
    /**
     * Initialize reactivation manager
     * 
     * @param database Database instance for querying schedules and notifications
     * @param storage Storage instance for accessing notification content
     * @param scheduler Scheduler instance for rescheduling notifications
     */
    init(database: DailyNotificationDatabase, 
         storage: DailyNotificationStorage,
         scheduler: DailyNotificationScheduler) {
        self.notificationCenter = UNUserNotificationCenter.current()
        self.database = database
        self.storage = storage
        self.scheduler = scheduler
        
        NSLog("\(Self.TAG): ReactivationManager initialized")
    }
    
    // MARK: - Recovery Execution
    
    /**
     * Perform lightweight rollover check when app becomes active
     * 
     * This is called when the app becomes active (foreground) to check for
     * missed rollovers that occurred while the app was backgrounded.
     * 
     * This is a lightweight check that only:
     * 1. Checks for delivered notifications and triggers rollover
     * 2. Detects and processes missed rollovers
     * 
     * It does NOT perform full recovery (missed notification marking, rescheduling, etc.)
     * Full recovery only happens on app launch.
     * 
     * This handles the "inactive app" scenario where notifications fire while
     * the app is backgrounded and rollover doesn't happen.
     */
    func performActiveRolloverCheck() {
        Task {
            NSLog("\(Self.TAG): Performing active rollover check (app became active)")
            
            // Check for delivered notifications and trigger rollover
            await checkAndProcessDeliveredNotifications()
            
            // Check for missed rollovers (notifications that should have rolled over)
            let rolloverResult = await detectAndProcessMissedRollovers()
            NSLog("\(Self.TAG): Active rollover check completed: processed=\(rolloverResult.processedCount), failed=\(rolloverResult.failedCount), checked=\(rolloverResult.totalChecked)")
        }
    }
    
    /**
     * Perform recovery on app launch
     * 
     * This is the main entry point for recovery operations. Called automatically
     * when the plugin loads via DailyNotificationPlugin.load().
     * 
     * Recovery Process:
     * 1. Detects boot scenario (if device rebooted)
     * 2. Detects recovery scenario (none, cold start, warm start, termination)
     * 3. Performs appropriate recovery actions based on scenario
     * 4. Records recovery metrics in Core Data history
     * 
     * Scenario Detection:
     * - `.none`: Empty database (first launch) - no recovery needed
     * - `.coldStart`: Notifications exist, may need verification - performs recovery
     * - `.warmStart`: Notifications match DB state - no recovery needed (optimization)
     * - `.termination`: App terminated, notifications cleared - full recovery
     * - `.boot`: Device rebooted - full recovery
     * 
     * Error Handling:
     * - All errors are caught and logged (non-fatal)
     * - Recovery failures are recorded in history
     * - App continues normally even if recovery fails
     * - Partial results returned when some operations fail
     * 
     * Performance:
     * - Runs asynchronously in background Task
     * - Timeout protection (2 seconds default) prevents hanging
     * - Non-blocking: does not delay app startup
     * 
     * Thread Safety:
     * - Safe to call from any thread
     * - All operations are async/await based
     * 
     * @note This method is called automatically on app launch. Manual calls are
     *       generally not needed unless testing recovery scenarios.
     * 
     * @throws Never throws - all errors are caught and handled internally
     * 
     * @see detectScenario() for scenario detection logic
     * @see performColdStartRecovery() for cold start recovery
     * @see handleTerminationRecovery() for termination recovery
     * @see performBootRecovery() for boot recovery
     */
    func performRecovery() {
        Task {
            let startTime = Date()
            do {
                try await withTimeout(seconds: Self.RECOVERY_TIMEOUT_SECONDS) { [self] in
                    NSLog("\(Self.TAG): Starting app launch recovery")
                    
                    // Phase 3: Check for boot scenario first
                    let isBoot = self.detectBootScenario()
                    if isBoot {
                        NSLog("\(Self.TAG): Boot scenario detected - performing boot recovery")
                        let bootStartTime = Date()
                        let result = try await self.performBootRecovery()
                        let bootEndTime = Date()
                        NSLog("\(Self.TAG): Boot recovery completed: missed=\(result.missedCount), rescheduled=\(result.rescheduledCount), verified=\(result.verifiedCount), errors=\(result.errors)")
                        // Record in history
                        try await self.recordRecoveryHistory(result, scenario: .boot, startTime: bootStartTime, endTime: bootEndTime)
                        // Update last launch time after boot recovery
                        self.updateLastLaunchTime()
                        return
                    }
                    
                    // Step 1: Detect scenario
                    let scenario = try await self.detectScenario()
                    NSLog("\(Self.TAG): Detected scenario: \(scenario.rawValue)")
                    
                    // Step 2: Handle based on scenario
                    switch scenario {
                    case .none:
                        NSLog("\(Self.TAG): No recovery needed (first launch or no notifications)")
                        self.updateLastLaunchTime()
                        return
                    case .warmStart:
                        NSLog("\(Self.TAG): Warm start detected - checking for missed rollovers")
                        // Even in warm start, we need to check for missed rollovers
                        // This handles cases where notifications fired while app was backgrounded
                        let warmStartTime = Date()
                        
                        // Check for delivered notifications and trigger rollover
                        await self.checkAndProcessDeliveredNotifications()
                        
                        // Check for missed rollovers (notifications that should have rolled over)
                        let rolloverResult = await self.detectAndProcessMissedRollovers()
                        NSLog("\(Self.TAG): Warm start rollover check: processed=\(rolloverResult.processedCount), failed=\(rolloverResult.failedCount), checked=\(rolloverResult.totalChecked)")
                        
                        let warmEndTime = Date()
                        let duration = warmEndTime.timeIntervalSince(warmStartTime) * 1000 // ms
                        NSLog("\(Self.TAG): Warm start rollover check completed: duration=%.0fms", duration)
                        
                        self.updateLastLaunchTime()
                        return
                    case .coldStart:
                        NSLog("\(Self.TAG): Cold start scenario - performing recovery")
                        let coldStartTime = Date()
                        let result = try await self.performColdStartRecovery()
                        let coldEndTime = Date()
                        NSLog("\(Self.TAG): Cold start recovery completed: missed=\(result.missedCount), rescheduled=\(result.rescheduledCount), verified=\(result.verifiedCount), errors=\(result.errors)")
                        // Record in history
                        try await self.recordRecoveryHistory(result, scenario: .coldStart, startTime: coldStartTime, endTime: coldEndTime)
                        self.updateLastLaunchTime()
                    case .termination:
                        // Phase 2: Termination recovery
                        NSLog("\(Self.TAG): Termination scenario detected - performing full recovery")
                        let termStartTime = Date()
                        let result = try await self.handleTerminationRecovery()
                        let termEndTime = Date()
                        NSLog("\(Self.TAG): Termination recovery completed: missed=\(result.missedCount), rescheduled=\(result.rescheduledCount), verified=\(result.verifiedCount), errors=\(result.errors)")
                        // Record in history
                        try await self.recordRecoveryHistory(result, scenario: .termination, startTime: termStartTime, endTime: termEndTime)
                        self.updateLastLaunchTime()
                    case .boot:
                        // Should be handled by initial boot detection
                        break
                    }
                }
            } catch is TimeoutError {
                let endTime = Date()
                let duration = endTime.timeIntervalSince(startTime)
                NSLog("\(Self.TAG): Recovery timed out after \(Self.RECOVERY_TIMEOUT_SECONDS) seconds (non-fatal) - actual duration: \(String(format: "%.2f", duration))s")
                // Record timeout in history
                do {
                    try await recordRecoveryFailure(TimeoutError(), scenario: "TIMEOUT")
                } catch {
                    NSLog("\(Self.TAG): Failed to record recovery timeout in history")
                }
            } catch {
                // Rollback: Log error but don't crash
                let endTime = Date()
                let duration = endTime.timeIntervalSince(startTime)
                NSLog("\(Self.TAG): Recovery failed (non-fatal): \(error.localizedDescription) - duration: \(String(format: "%.2f", duration))s")
                
                // Enhanced error logging with stack trace
                if let nsError = error as NSError? {
                    NSLog("\(Self.TAG): Error details - domain: \(nsError.domain), code: \(nsError.code)")
                    if let userInfo = nsError.userInfo as? [String: Any] {
                        NSLog("\(Self.TAG): Error userInfo: \(userInfo)")
                    }
                }
                NSLog("\(Self.TAG): Error type: \(type(of: error))")
                
                // Record failure in history (best effort, don't fail if this fails)
                do {
                    try await recordRecoveryFailure(error)
                } catch {
                    NSLog("\(Self.TAG): Failed to record recovery failure in history: \(error.localizedDescription)")
                }
            }
        }
    }
    
    // MARK: - Scenario Detection
    
    /**
     * Detect recovery scenario
     * 
     * Phase 1: Basic scenario detection
     * - .none: Empty database (first launch)
     * - .coldStart: Notifications exist, may need verification
     * - .warmStart: Notifications match DB state
     * 
     * Phase 2: Will add termination detection
     * 
     * @return RecoveryScenario
     * 
     * Note: Internal for testing
     */
    internal func detectScenario() async throws -> RecoveryScenario {
        // Step 1: Check if database has notifications
        // Handle storage errors gracefully (non-fatal)
        let allNotifications: [NotificationContent]
        do {
            allNotifications = storage.getAllNotifications()
        } catch {
            // Non-fatal: Log error and assume empty storage
            NSLog("\(Self.TAG): Error getting notifications from storage (non-fatal): \(error.localizedDescription)")
            return .none
        }
        
        if allNotifications.isEmpty {
            return .none  // First launch
        }
        
        // Step 2: Get pending notifications from UNUserNotificationCenter
        // Handle notification center errors gracefully (non-fatal)
        let pendingRequests: [UNNotificationRequest]
        do {
            pendingRequests = try await notificationCenter.pendingNotificationRequests()
        } catch {
            // Non-fatal: Log error and assume no pending notifications
            NSLog("\(Self.TAG): Error getting pending notifications (non-fatal): \(error.localizedDescription)")
            // Return cold start as safe default - will trigger recovery
            return .coldStart
        }
        
        let pendingIds = Set(pendingRequests.map { $0.identifier })
        
        // Step 3: Get notification IDs from storage
        let dbIds = Set(allNotifications.map { $0.id })
        
        // Step 4: Determine scenario
        if pendingIds.isEmpty && !dbIds.isEmpty {
            // DB has notifications but no notifications scheduled
            // Phase 2: This indicates termination (system cleared notifications)
            return .termination
        } else if !pendingIds.isEmpty && !dbIds.isEmpty {
            // Both have data - check if they match
            if dbIds == pendingIds {
                return .warmStart  // Match indicates warm resume
            } else {
                return .coldStart  // Mismatch indicates recovery needed
            }
        }
        
        // Default: no recovery needed
        return .none
    }
    
    // MARK: - Cold Start Recovery
    
    /**
     * Perform cold start recovery
     * 
     * Handles recovery when app was terminated but notifications may still exist
     * in UNUserNotificationCenter. This is the most common recovery scenario.
     * 
     * Recovery Steps:
     * 1. Detect missed notifications (scheduled_time < now, not delivered)
     * 2. Mark missed notifications in database (update delivery status)
     * 3. Verify future notifications are scheduled in UNUserNotificationCenter
     * 4. Reschedule any missing future notifications
     * 
     * Error Handling:
     * - Individual notification errors are caught and counted
     * - Partial results returned if some operations fail
     * - All errors logged but don't stop recovery process
     * 
     * Performance:
     * - Processes notifications in batches
     * - Non-blocking async operations
     * 
     * @return RecoveryResult containing:
     *   - missedCount: Number of missed notifications marked
     *   - rescheduledCount: Number of notifications rescheduled
     *   - verifiedCount: Number of notifications verified as scheduled
     *   - errors: Number of errors encountered during recovery
     * 
     * @throws Never throws - all errors are caught and counted in result
     * 
     * @see detectMissedNotifications() for missed notification detection
     * @see verifyFutureNotifications() for future notification verification
     * @see RecoveryResult for result structure
     */
    private func performColdStartRecovery() async throws -> RecoveryResult {
        let startTime = Date()
        let currentTime = Date()
        
        NSLog("\(Self.TAG): Cold start recovery: checking for missed notifications")
        
        // Step 1: Detect missed notifications
        let missedNotifications = try await detectMissedNotifications(currentTime: currentTime)
        NSLog("\(Self.TAG): Missed notifications detected: \(missedNotifications.count)")
        
        var missedCount = 0
        var missedErrors = 0
        
        // Step 2: Mark missed notifications
        for notification in missedNotifications {
            do {
                // Data integrity check: verify notification is valid
                if notification.id.isEmpty {
                    NSLog("\(Self.TAG): Skipping invalid notification: empty ID")
                    continue
                }
                
                try await markMissedNotification(notification)
                missedCount += 1
                
                NSLog("\(Self.TAG): Marked missed notification: \(notification.id)")
            } catch {
                missedErrors += 1
                NSLog("\(Self.TAG): Failed to mark missed notification \(notification.id): \(error.localizedDescription)")
            }
        }
        
        // Step 3: Verify future notifications
        let verificationResult = try await verifyFutureNotifications()
        NSLog("\(Self.TAG): Future notifications verified: found=\(verificationResult.notificationsFound), missing=\(verificationResult.notificationsMissing)")
        
        var rescheduledCount = 0
        var rescheduleErrors = 0
        
        // Step 4: Reschedule missing notifications
        if !verificationResult.missingIds.isEmpty {
            NSLog("\(Self.TAG): Found \(verificationResult.missingIds.count) missing notifications, rescheduling...")
            
            for missingId in verificationResult.missingIds {
                do {
                    // Reschedule using scheduler
                    // Note: For Phase 1, we'll need to get the notification content from storage
                    // and reschedule it. This may need to be enhanced in Phase 2.
                    try await rescheduleMissingNotification(id: missingId)
                    rescheduledCount += 1
                    
                    NSLog("\(Self.TAG): Rescheduled missing notification: \(missingId)")
                } catch {
                    rescheduleErrors += 1
                    NSLog("\(Self.TAG): Failed to reschedule notification \(missingId): \(error.localizedDescription)")
                }
            }
        }
        
        // Step 4.5: Check for delivered notifications and trigger rollover
        // This handles notifications that were delivered while app was not running
        await checkAndProcessDeliveredNotifications()
        
        // Step 4.6: Check for missed rollovers (notifications that should have rolled over)
        // This handles notifications that fired but rollover didn't happen (app was terminated)
        let rolloverResult = await detectAndProcessMissedRollovers()
        NSLog("\(Self.TAG): Missed rollover recovery: processed=\(rolloverResult.processedCount), failed=\(rolloverResult.failedCount), checked=\(rolloverResult.totalChecked)")
        
        // Record recovery in history
        let result = RecoveryResult(
            missedCount: missedCount,
            rescheduledCount: rescheduledCount,
            verifiedCount: verificationResult.notificationsFound,
            errors: missedErrors + rescheduleErrors
        )
        
        // Note: History recording is done at performRecovery level with timing
        // This method is called from performRecovery which tracks timing
        
        let duration = Date().timeIntervalSince(startTime) * 1000 // ms
        NSLog("\(Self.TAG): Cold start recovery completed: duration=%.0fms, missed=%d, rescheduled=%d, verified=%d, errors=%d", 
              duration, missedCount, rescheduledCount, verificationResult.notificationsFound, missedErrors + rescheduleErrors)
        
        return result
    }
    
    // MARK: - Missed Notification Detection
    
    /**
     * Detect missed notifications
     * 
     * Identifies notifications that were scheduled to fire but haven't been delivered.
     * A notification is considered "missed" if:
     * - scheduledTime < currentTime (notification time has passed)
     * - deliveryStatus != 'delivered' (not yet marked as delivered)
     * 
     * Error Handling:
     * - Storage errors: Returns empty array (non-fatal)
     * - All errors logged but don't crash app
     * 
     * @param currentTime Current time for comparison (typically Date())
     * @return Array of NotificationContent that are considered missed
     * 
     * @throws Never throws - all errors are caught and handled internally
     * 
     * @note Internal visibility for unit testing. External code should use
     *       performRecovery() which calls this method internally.
     * 
     * @see NotificationContent for notification structure
     */
    internal func detectMissedNotifications(currentTime: Date) async throws -> [NotificationContent] {
        // Get all notifications from storage
        // Handle database/storage errors gracefully (non-fatal)
        let allNotifications: [NotificationContent]
        do {
            allNotifications = storage.getAllNotifications()
        } catch {
            // Non-fatal: Log error and return empty array
            NSLog("\(Self.TAG): Error getting notifications from storage (non-fatal): \(error.localizedDescription)")
            return []
        }
        
        // Convert currentTime to milliseconds (Int64) for comparison
        let currentTimeMs = Int64(currentTime.timeIntervalSince1970 * 1000)
        
        // Filter for missed notifications:
        // - scheduled_time < currentTime
        // - delivery_status != 'delivered' (if deliveryStatus property exists)
        let missed = allNotifications.filter { notification in
            let isPastScheduledTime = notification.scheduledTime < currentTimeMs
            let isNotDelivered = notification.deliveryStatus != "delivered"
            return isPastScheduledTime && isNotDelivered
        }
        
        NSLog("\(Self.TAG): Detected \(missed.count) missed notifications")
        return missed
    }
    
    /**
     * Mark notification as missed
     * 
     * @param notification Notification to mark as missed
     */
    private func markMissedNotification(_ notification: NotificationContent) async throws {
        // Update delivery status and last delivery attempt
        notification.deliveryStatus = "missed"
        notification.lastDeliveryAttempt = Int64(Date().timeIntervalSince1970 * 1000)
        
        // Save to storage (notification already exists, this updates it)
        storage.saveNotificationContent(notification)
        
        // Record in history (if history table exists)
        NSLog("\(Self.TAG): Marked notification \(notification.id) as missed")
    }
    
    // MARK: - Future Notification Verification
    
    /**
     * Verify future notifications are scheduled
     * 
     * Compares notifications in storage (scheduled for future) with pending
     * notifications in UNUserNotificationCenter to identify any missing ones.
     * 
     * Verification Process:
     * 1. Get all pending notifications from UNUserNotificationCenter
     * 2. Get all future notifications from storage (scheduledTime >= now)
     * 3. Compare IDs to find missing notifications
     * 4. Return verification result with counts and missing IDs
     * 
     * Error Handling:
     * - Notification center errors: Returns partial result (assumes all missing)
     * - Storage errors: Returns partial result (assumes none found)
     * - All errors logged but don't crash app
     * 
     * @return VerificationResult containing:
     *   - totalSchedules: Total future notifications in storage
     *   - notificationsFound: Number found in UNUserNotificationCenter
     *   - notificationsMissing: Number missing from UNUserNotificationCenter
     *   - missingIds: Array of notification IDs that need rescheduling
     * 
     * @throws Never throws - all errors are caught and handled internally
     * 
     * @note Internal visibility for unit testing. External code should use
     *       performRecovery() which calls this method internally.
     * 
     * @see VerificationResult for result structure
     */
    internal func verifyFutureNotifications() async throws -> VerificationResult {
        // Get pending notifications from UNUserNotificationCenter
        // Handle notification center errors gracefully (non-fatal)
        let pendingRequests: [UNNotificationRequest]
        do {
            pendingRequests = try await notificationCenter.pendingNotificationRequests()
        } catch {
            // Non-fatal: Log error and assume no pending notifications
            NSLog("\(Self.TAG): Error getting pending notifications (non-fatal): \(error.localizedDescription)")
            // Return verification result indicating all are missing
            let currentTimeMs = Int64(Date().timeIntervalSince1970 * 1000)
            let allNotifications = storage.getAllNotifications()
            let futureNotifications = allNotifications.filter { $0.scheduledTime >= currentTimeMs }
            let futureIds = Set(futureNotifications.map { $0.id })
            
            return VerificationResult(
                totalSchedules: futureNotifications.count,
                notificationsFound: 0,
                notificationsMissing: futureIds.count,
                missingIds: Array(futureIds)
            )
        }
        
        let pendingIds = Set(pendingRequests.map { $0.identifier })
        
        // Get all notifications from storage that are scheduled for future
        // Handle storage errors gracefully (non-fatal)
        let currentTimeMs = Int64(Date().timeIntervalSince1970 * 1000)
        let allNotifications: [NotificationContent]
        do {
            allNotifications = storage.getAllNotifications()
        } catch {
            // Non-fatal: Log error and return empty verification result
            NSLog("\(Self.TAG): Error getting notifications from storage (non-fatal): \(error.localizedDescription)")
            return VerificationResult(
                totalSchedules: 0,
                notificationsFound: pendingIds.count,
                notificationsMissing: 0,
                missingIds: []
            )
        }
        
        let futureNotifications = allNotifications.filter { $0.scheduledTime >= currentTimeMs }
        let futureIds = Set(futureNotifications.map { $0.id })
        
        // Compare and find missing
        let missingIds = Array(futureIds.subtracting(pendingIds))
        
        NSLog("\(Self.TAG): Verification: total=\(futureNotifications.count), found=\(pendingIds.count), missing=\(missingIds.count)")
        
        return VerificationResult(
            totalSchedules: futureNotifications.count,
            notificationsFound: pendingIds.count,
            notificationsMissing: missingIds.count,
            missingIds: missingIds
        )
    }
    
    /**
     * Reschedule missing notification
     * 
     * Retrieves notification content from storage and reschedules it using
     * the scheduler. This is called when verifyFutureNotifications() identifies
     * a notification that should be scheduled but isn't in UNUserNotificationCenter.
     * 
     * Error Handling:
     * - Storage errors: Throws ReactivationError.notificationNotFound
     * - Scheduling errors: Throws ReactivationError.rescheduleFailed
     * - Errors are caught by caller and counted in RecoveryResult.errors
     * 
     * @param id Notification ID to reschedule
     * 
     * @throws ReactivationError.notificationNotFound if notification not found in storage
     * @throws ReactivationError.rescheduleFailed if scheduling fails
     * 
     * @see verifyFutureNotifications() for identification of missing notifications
     * @see DailyNotificationScheduler.scheduleNotification() for scheduling logic
     */
    private func rescheduleMissingNotification(id: String) async throws {
        // Get notification content from storage
        // Handle storage errors gracefully (non-fatal)
        let notification: NotificationContent?
        do {
            notification = storage.getNotificationContent(id: id)
        } catch {
            // Non-fatal: Log error and throw to be caught by caller
            NSLog("\(Self.TAG): Error getting notification from storage (non-fatal): \(error.localizedDescription)")
            throw ReactivationError.notificationNotFound(id: id)
        }
        
        guard let notification = notification else {
            throw ReactivationError.notificationNotFound(id: id)
        }
        
        // Reschedule using scheduler
        // Handle scheduling errors gracefully (non-fatal)
        let success = await scheduler.scheduleNotification(notification)
        
        if !success {
            // Non-fatal: Log error and throw to be caught by caller
            NSLog("\(Self.TAG): Failed to reschedule notification \(id) (non-fatal)")
            throw ReactivationError.rescheduleFailed(id: id)
        }
    }
    
    // MARK: - Phase 2: Termination Recovery
    
    /**
     * Handle termination recovery
     * 
     * Phase 2: Comprehensive recovery when app was terminated by system
     * and notifications were cleared.
     * 
     * Steps:
     * 1. Detect all missed notifications (past scheduled times)
     * 2. Mark all as missed
     * 3. Reschedule all future notifications
     * 4. Reschedule all fetch schedules (if applicable)
     * 
     * @return RecoveryResult with counts
     */
    private func handleTerminationRecovery() async throws -> RecoveryResult {
        NSLog("\(Self.TAG): Handling termination recovery - comprehensive recovery")
        
        // Use full recovery which handles both notify and fetch schedules
        return try await performFullRecovery()
    }
    
    /**
     * Perform full recovery
     * 
     * Phase 2: Comprehensive recovery that handles:
     * - All missed notifications (past scheduled times)
     * - All future notifications (reschedule if missing)
     * - All fetch schedules (reschedule if needed)
     * - Multiple schedules with batch operations
     * 
     * @return RecoveryResult with comprehensive counts
     */
    private func performFullRecovery() async throws -> RecoveryResult {
        let currentTime = Date()
        let currentTimeMs = Int64(currentTime.timeIntervalSince1970 * 1000)
        
        NSLog("\(Self.TAG): Performing full recovery")
        
        // Step 1: Get all notifications from storage
        let allNotifications = storage.getAllNotifications()
        
        if allNotifications.isEmpty {
            NSLog("\(Self.TAG): No notifications to recover")
            return RecoveryResult(missedCount: 0, rescheduledCount: 0, verifiedCount: 0, errors: 0)
        }
        
        NSLog("\(Self.TAG): Processing \(allNotifications.count) notifications")
        
        // Step 2: Get pending notifications once (batch operation)
        let pendingRequests = try await notificationCenter.pendingNotificationRequests()
        let pendingIds = Set(pendingRequests.map { $0.identifier })
        
        // Step 3: Separate missed and future notifications (batch processing)
        var missedNotifications: [NotificationContent] = []
        var futureNotifications: [NotificationContent] = []
        
        for notification in allNotifications {
            if notification.scheduledTime < currentTimeMs {
                missedNotifications.append(notification)
            } else {
                futureNotifications.append(notification)
            }
        }
        
        NSLog("\(Self.TAG): Found \(missedNotifications.count) missed and \(futureNotifications.count) future notifications")
        
        // Step 4: Process missed notifications (batch)
        var missedCount = 0
        var missedErrors = 0
        
        for notification in missedNotifications {
            do {
                try await markMissedNotification(notification)
                missedCount += 1
            } catch {
                missedErrors += 1
                NSLog("\(Self.TAG): Failed to mark missed notification \(notification.id): \(error.localizedDescription)")
            }
        }
        
        // Step 5: Process future notifications (batch verification)
        var rescheduledCount = 0
        var rescheduleErrors = 0
        var missingFutureIds: [String] = []
        
        for notification in futureNotifications {
            if !pendingIds.contains(notification.id) {
                missingFutureIds.append(notification.id)
            }
        }
        
        // Step 6: Reschedule missing future notifications (batch)
        if !missingFutureIds.isEmpty {
            NSLog("\(Self.TAG): Rescheduling \(missingFutureIds.count) missing future notifications...")
            
            for missingId in missingFutureIds {
                do {
                    try await rescheduleMissingNotification(id: missingId)
                    rescheduledCount += 1
                } catch {
                    rescheduleErrors += 1
                    NSLog("\(Self.TAG): Failed to reschedule notification \(missingId): \(error.localizedDescription)")
                }
            }
        }
        
        // Step 7: Verify final state
        let verificationResult = try await verifyFutureNotifications()
        NSLog("\(Self.TAG): Final verification: found=\(verificationResult.notificationsFound), missing=\(verificationResult.notificationsMissing)")
        
        // Step 8: Check for missed rollovers (notifications that should have rolled over)
        // This handles notifications that fired but rollover didn't happen (app was terminated)
        let rolloverResult = await detectAndProcessMissedRollovers()
        NSLog("\(Self.TAG): Missed rollover recovery: processed=\(rolloverResult.processedCount), failed=\(rolloverResult.failedCount), checked=\(rolloverResult.totalChecked)")
        
        // Record recovery in history
        let result = RecoveryResult(
            missedCount: missedCount,
            rescheduledCount: rescheduledCount,
            verifiedCount: verificationResult.notificationsFound,
            errors: missedErrors + rescheduleErrors
        )
        
        // Note: History recording is done at performRecovery level with timing
        // This method is called from performRecovery which tracks timing
        
        NSLog("\(Self.TAG): Full recovery completed: missed=\(result.missedCount), rescheduled=\(result.rescheduledCount), verified=\(result.verifiedCount), errors=\(result.errors)")
        return result
    }
    
    // MARK: - Phase 3: Boot Detection & Recovery
    
    /**
     * Detect boot scenario
     * 
     * Phase 3: Detects if device was rebooted since last app launch
     * 
     * Detection method:
     * 1. Get system uptime (time since last boot)
     * 2. Get last launch time from UserDefaults
     * 3. If system uptime < last launch time, device was rebooted
     * 
     * @return true if boot scenario detected
     * 
     * Note: Internal for testing
     */
    internal func detectBootScenario() -> Bool {
        let systemUptime = ProcessInfo.processInfo.systemUptime
        let lastLaunchTime = getLastLaunchTime()
        
        // If no last launch time recorded, this is first launch (not boot)
        guard let lastLaunch = lastLaunchTime else {
            NSLog("\(Self.TAG): No last launch time recorded - first launch")
            return false
        }
        
        // Calculate time since last launch
        let timeSinceLastLaunch = Date().timeIntervalSince1970 - lastLaunch
        
        // If system uptime is less than time since last launch, device was rebooted
        // Also check if system uptime is very small (just booted)
        let isBoot = systemUptime < timeSinceLastLaunch || systemUptime < Self.BOOT_DETECTION_THRESHOLD_SECONDS
        
        if isBoot {
            NSLog("\(Self.TAG): Boot detected - systemUptime=\(systemUptime)s, timeSinceLastLaunch=\(timeSinceLastLaunch)s")
        }
        
        return isBoot
    }
    
    /**
     * Get last launch time from UserDefaults
     * 
     * @return Last launch timestamp or nil if not set
     */
    private func getLastLaunchTime() -> TimeInterval? {
        let lastLaunch = UserDefaults.standard.double(forKey: Self.LAST_LAUNCH_TIME_KEY)
        return lastLaunch > 0 ? lastLaunch : nil
    }
    
    /**
     * Update last launch time in UserDefaults
     */
    private func updateLastLaunchTime() {
        let currentTime = Date().timeIntervalSince1970
        UserDefaults.standard.set(currentTime, forKey: Self.LAST_LAUNCH_TIME_KEY)
        NSLog("\(Self.TAG): Updated last launch time: \(currentTime)")
    }
    
    /**
     * Perform boot recovery
     * 
     * Phase 3: Comprehensive recovery after device reboot
     * 
     * Steps:
     * 1. Detect all missed notifications (past scheduled times)
     * 2. Mark all as missed
     * 3. Reschedule all future notifications
     * 4. Reschedule all fetch schedules (if applicable)
     * 
     * Similar to termination recovery, but triggered by boot detection
     * 
     * Note: BGTaskScheduler may also trigger boot recovery, but this
     * method provides immediate recovery on app launch after boot.
     * 
     * @return RecoveryResult with counts
     */
    private func performBootRecovery() async throws -> RecoveryResult {
        NSLog("\(Self.TAG): Performing boot recovery - comprehensive recovery after device reboot")
        
        // Boot recovery is similar to termination recovery
        // Use full recovery which handles all notifications
        let result = try await performFullRecovery()
        
        // Note: History recording is done at performRecovery level with timing
        // This method is called from performRecovery which tracks timing
        
        NSLog("\(Self.TAG): Boot recovery completed: missed=\(result.missedCount), rescheduled=\(result.rescheduledCount), verified=\(result.verifiedCount), errors=\(result.errors)")
        return result
    }
    
    /**
     * Verify BGTaskScheduler registration
     * 
     * Phase 3: Verifies that background tasks are properly registered
     * 
     * This is a diagnostic method to check registration status.
     * Actual registration happens in DailyNotificationPlugin.setupBackgroundTasks()
     * 
     * @return Dictionary with registration status
     */
    func verifyBGTaskRegistration() -> [String: Any] {
        guard #available(iOS 13.0, *) else {
            return [
                "available": false,
                "message": "Background tasks not available on this iOS version"
            ]
        }
        
        // Note: BGTaskScheduler doesn't provide a way to query registered task identifiers
        // We can only verify by attempting to schedule or by tracking registration ourselves
        // For now, we'll return that registration status cannot be verified programmatically
        let fetchTaskIdentifier = "org.timesafari.dailynotification.fetch"
        let notifyTaskIdentifier = "org.timesafari.dailynotification.notify"
        
        return [
            "available": true,
            "fetchTaskRegistered": true, // Assumed registered if this method is called
            "notifyTaskRegistered": true, // Assumed registered if this method is called
            "message": "Registration status cannot be verified programmatically. Tasks should be registered in AppDelegate."
        ]
    }
    
    // MARK: - History Recording
    
    /**
     * Record recovery history in Core Data
     * 
     * Persists recovery metrics to Core Data History entity for observability
     * and debugging. Records execution time, counts, and scenario information.
     * 
     * History Record Contains:
     * - Scenario type (cold start, termination, boot)
     * - Missed notification count
     * - Rescheduled notification count
     * - Verified notification count
     * - Error count
     * - Execution duration (milliseconds)
     * 
     * Error Handling:
     * - Core Data errors are logged but don't fail recovery
     * - Best effort: if history recording fails, recovery still succeeds
     * 
     * @param result Recovery result with metrics
     * @param scenario Recovery scenario that was executed
     * @param startTime When recovery started (for duration calculation)
     * @param endTime When recovery ended (for duration calculation)
     * 
     * @throws Never throws - all errors are caught and logged internally
     * 
     * @see HistoryDAO.recordRecovery() for Core Data persistence
     * @see RecoveryResult for result structure
     */
    private func recordRecoveryHistory(_ result: RecoveryResult, scenario: RecoveryScenario, startTime: Date, endTime: Date) async throws {
        // Log recovery metrics
        NSLog("\(Self.TAG): Recovery history - scenario: \(scenario.rawValue), missed: \(result.missedCount), rescheduled: \(result.rescheduledCount), verified: \(result.verifiedCount), errors: \(result.errors)")
        
        // Record in Core Data history table
        guard let context = PersistenceController.shared.viewContext else {
            NSLog("\(Self.TAG): Cannot record history - CoreData not available")
            return
        }
        
        // Create history record
        let history = History.recordRecovery(
            in: context,
            scenario: scenario.rawValue,
            missedCount: result.missedCount,
            rescheduledCount: result.rescheduledCount,
            verifiedCount: result.verifiedCount,
            errors: result.errors,
            startTime: startTime,
            endTime: endTime
        )
        
        // Save context
        do {
            if context.hasChanges {
                try context.save()
                NSLog("\(Self.TAG): Recovery history recorded successfully")
            }
        } catch {
            NSLog("\(Self.TAG): Failed to save recovery history: \(error.localizedDescription)")
            context.rollback()
            throw error
        }
    }
    
    /**
     * Record recovery failure in Core Data
     * 
     * Persists error information to Core Data History entity when recovery
     * fails. Records error details, type, and optional scenario information.
     * 
     * Error Record Contains:
     * - Error message (localizedDescription)
     * - Error type (Swift type name)
     * - NSError domain and code (if applicable)
     * - NSError userInfo (if applicable)
     * - Scenario (if known)
     * 
     * Error Handling:
     * - Core Data errors are logged but don't fail recovery
     * - Best effort: if history recording fails, error is still logged
     * 
     * @param error Error that occurred during recovery
     * @param scenario Optional recovery scenario (if known before failure)
     * 
     * @throws Never throws - all errors are caught and logged internally
     * 
     * @see HistoryDAO.recordRecoveryFailure() for Core Data persistence
     */
    private func recordRecoveryFailure(_ error: Error, scenario: String? = nil) async throws {
        // Enhanced error logging
        var errorDetails: [String: Any] = [
            "error": error.localizedDescription,
            "errorType": String(describing: type(of: error))
        ]
        
        if let nsError = error as NSError? {
            errorDetails["errorCode"] = nsError.code
            errorDetails["errorDomain"] = nsError.domain
            if let userInfo = nsError.userInfo as? [String: Any] {
                errorDetails["userInfo"] = userInfo
            }
        }
        
        if let scenario = scenario {
            errorDetails["scenario"] = scenario
        }
        
        let diagJson = DailyNotificationDataConversions.jsonStringFromDictionary(errorDetails) ?? "{}"
        NSLog("\(Self.TAG): Recovery failure: \(diagJson)")
        
        // Record in Core Data history table
        guard let context = PersistenceController.shared.viewContext else {
            NSLog("\(Self.TAG): Cannot record failure - CoreData not available")
            return
        }
        
        // Create failure history record
        let history = History.recordRecoveryFailure(
            in: context,
            error: error,
            scenario: scenario
        )
        
        // Save context
        do {
            if context.hasChanges {
                try context.save()
                NSLog("\(Self.TAG): Recovery failure recorded successfully")
            }
        } catch {
            NSLog("\(Self.TAG): Failed to save recovery failure: \(error.localizedDescription)")
            context.rollback()
            // Don't throw - this is best effort
        }
    }
    
    /**
     * Check for delivered notifications and trigger rollover
     * 
     * This ensures rollover happens on app launch if notifications were delivered
     * while the app was not running
     */
    private func checkAndProcessDeliveredNotifications() async {
        NSLog("DNP-ROLLOVER: RECOVERY_CHECK_START")
        print("DNP-ROLLOVER: RECOVERY_CHECK_START")
        
        // Get delivered notifications from system
        let deliveredNotifications = await notificationCenter.deliveredNotifications()
        NSLog("DNP-ROLLOVER: RECOVERY_FOUND delivered_count=\(deliveredNotifications.count)")
        print("DNP-ROLLOVER: RECOVERY_FOUND delivered_count=\(deliveredNotifications.count)")
        
        // Get last processed rollover time from storage
        let lastProcessedTime = storage.getLastRolloverTime()
        let lastProcessedTimeStr = formatTime(lastProcessedTime)
        NSLog("DNP-ROLLOVER: RECOVERY_LAST_PROCESSED time=\(lastProcessedTimeStr)")
        print("DNP-ROLLOVER: RECOVERY_LAST_PROCESSED time=\(lastProcessedTimeStr)")
        
        var processedCount = 0
        var skippedCount = 0
        
        for notification in deliveredNotifications {
            let userInfo = notification.request.content.userInfo
            
            guard let notificationId = userInfo["notification_id"] as? String,
                  let scheduledTime = userInfo["scheduled_time"] as? Int64 else {
                continue
            }
            
            let scheduledTimeStr = formatTime(scheduledTime)
            
            // Only process if this notification hasn't been processed yet
            if scheduledTime > lastProcessedTime {
                NSLog("DNP-ROLLOVER: RECOVERY_PROCESS id=\(notificationId) scheduled_time=\(scheduledTimeStr)")
                print("DNP-ROLLOVER: RECOVERY_PROCESS id=\(notificationId) scheduled_time=\(scheduledTimeStr)")
                
                // Get notification content
                guard let content = storage.getNotificationContent(id: notificationId) else {
                    NSLog("DNP-ROLLOVER: RECOVERY_ERROR id=\(notificationId) content_not_found")
                    print("DNP-ROLLOVER: RECOVERY_ERROR id=\(notificationId) content_not_found")
                    continue
                }
                
                // Trigger rollover
                // Note: fetcher parameter is unused - scheduler uses fetchScheduler instead (already implemented)
                let scheduled = await scheduler.scheduleNextNotification(
                    content,
                    storage: storage,
                    fetcher: nil // Unused - fetchScheduler handles prefetch scheduling
                )
                
                if scheduled {
                    NSLog("DNP-ROLLOVER: RECOVERY_SUCCESS id=\(notificationId)")
                    print("DNP-ROLLOVER: RECOVERY_SUCCESS id=\(notificationId)")
                    // Update last processed time
                    storage.saveLastRolloverTime(scheduledTime)
                    processedCount += 1
                } else {
                    NSLog("DNP-ROLLOVER: RECOVERY_FAILED id=\(notificationId)")
                    print("DNP-ROLLOVER: RECOVERY_FAILED id=\(notificationId)")
                }
            } else {
                skippedCount += 1
                NSLog("DNP-ROLLOVER: RECOVERY_SKIP id=\(notificationId) already_processed scheduled_time=\(scheduledTimeStr)")
                print("DNP-ROLLOVER: RECOVERY_SKIP id=\(notificationId) already_processed scheduled_time=\(scheduledTimeStr)")
            }
        }
        
        NSLog("DNP-ROLLOVER: RECOVERY_COMPLETE processed=\(processedCount) skipped=\(skippedCount)")
        print("DNP-ROLLOVER: RECOVERY_COMPLETE processed=\(processedCount) skipped=\(skippedCount)")
    }
    
    /**
     * Detect and process missed rollovers on app launch
     * 
     * This method identifies notifications that should have rolled over but didn't,
     * and schedules the next notification(s) for them.
     * 
     * Detection Logic:
     * 1. Find notifications where scheduledTime < currentTime (should have fired)
     * 2. Check if next notification exists (in storage or pending)
     * 3. Check if rollover was already processed (via lastRolloverTime)
     * 4. If no next notification and rollover not processed, schedule it
     * 
     * This handles cases where:
     * - Notification fired while app was terminated
     * - Notification was dismissed before app launched
     * - Rollover didn't happen because app wasn't active
     * 
     * Error Handling:
     * - Individual notification errors are caught and counted
     * - Partial results returned if some operations fail
     * - All errors logged but don't stop recovery process
     * 
     * @return RolloverRecoveryResult with counts of processed rollovers
     */
    private func detectAndProcessMissedRollovers() async -> RolloverRecoveryResult {
        NSLog("DNP-ROLLOVER: MISSED_ROLLOVER_CHECK_START")
        print("DNP-ROLLOVER: MISSED_ROLLOVER_CHECK_START")
        
        let currentTime = Int64(Date().timeIntervalSince1970 * 1000)
        let currentTimeStr = formatTime(currentTime)
        
        // Step 1: Get all notifications from storage
        let allNotifications: [NotificationContent]
        do {
            allNotifications = storage.getAllNotifications()
        } catch {
            // Non-fatal: Log error and return empty result
            NSLog("\(Self.TAG): Error getting notifications from storage (non-fatal): \(error.localizedDescription)")
            return RolloverRecoveryResult(processedCount: 0, failedCount: 0, totalChecked: 0)
        }
        
        // Step 2: Get pending notifications from system
        let pendingRequests: [UNNotificationRequest]
        do {
            pendingRequests = try await notificationCenter.pendingNotificationRequests()
        } catch {
            // Non-fatal: Log error and continue with empty pending list
            NSLog("\(Self.TAG): Error getting pending notifications (non-fatal): \(error.localizedDescription)")
            return RolloverRecoveryResult(processedCount: 0, failedCount: 0, totalChecked: allNotifications.count)
        }
        
        let pendingIds = Set(pendingRequests.map { $0.identifier })
        
        // Step 3: Find notifications that should have rolled over
        var missedRollovers: [NotificationContent] = []
        
        for notification in allNotifications {
            // Check if notification should have fired (scheduledTime < currentTime)
            if notification.scheduledTime >= currentTime {
                continue // Future notification, skip
            }
            
            // Check if rollover was already processed
            // Only skip if rollover was processed AND next notification exists
            // This handles cases where rollover was attempted but failed
            let lastRolloverTime = await storage.getLastRolloverTime(for: notification.id)
            
            // Calculate next scheduled time first to check if it exists
            var nextScheduledTime = scheduler.calculateNextScheduledTime(notification.scheduledTime)
            
            // If next scheduled time is in the past, keep calculating forward until we get a future time
            // This handles cases where the notification fired more than 2 minutes ago
            while nextScheduledTime < currentTime {
                let nextTimeStr = formatTime(nextScheduledTime)
                NSLog("DNP-ROLLOVER: MISSED_ROLLOVER_NEXT_IN_PAST id=\(notification.id) next_time=\(nextTimeStr) current_time=\(currentTimeStr), calculating forward")
                print("DNP-ROLLOVER: MISSED_ROLLOVER_NEXT_IN_PAST id=\(notification.id) next_time=\(nextTimeStr) current_time=\(currentTimeStr), calculating forward")
                nextScheduledTime = scheduler.calculateNextScheduledTime(nextScheduledTime)
            }
            
            // Check if next notification actually exists
            let toleranceMs: Int64 = 60 * 1000 // 1 minute tolerance
            var nextNotificationExists = false
            
            // Quick check in storage (exclude original)
            for existing in allNotifications {
                if existing.id == notification.id {
                    continue
                }
                if abs(existing.scheduledTime - nextScheduledTime) <= toleranceMs {
                    nextNotificationExists = true
                    break
                }
            }
            
            // Quick check in pending
            if !nextNotificationExists {
                for pending in pendingRequests {
                    if pending.identifier == notification.id {
                        continue
                    }
                    if let trigger = pending.trigger as? UNCalendarNotificationTrigger,
                       let nextDate = trigger.nextTriggerDate() {
                        let pendingTime = Int64(nextDate.timeIntervalSince1970 * 1000)
                        if abs(pendingTime - nextScheduledTime) <= toleranceMs {
                            nextNotificationExists = true
                            break
                        }
                    }
                }
            }
            
            // If rollover was processed AND next notification exists, skip
            // Otherwise, process it (either rollover wasn't attempted, or it failed)
            if let lastTime = lastRolloverTime, lastTime >= notification.scheduledTime, nextNotificationExists {
                let lastTimeStr = formatTime(lastTime)
                NSLog("DNP-ROLLOVER: MISSED_ROLLOVER_SKIP id=\(notification.id) already_processed last_rollover=\(lastTimeStr) next_exists=true")
                print("DNP-ROLLOVER: MISSED_ROLLOVER_SKIP id=\(notification.id) already_processed last_rollover=\(lastTimeStr) next_exists=true")
                continue // Already processed and next notification exists
            }
            
            // If rollover was attempted but next notification doesn't exist, log and continue processing
            if let lastTime = lastRolloverTime, lastTime >= notification.scheduledTime, !nextNotificationExists {
                let lastTimeStr = formatTime(lastTime)
                NSLog("DNP-ROLLOVER: MISSED_ROLLOVER_RETRY id=\(notification.id) rollover_attempted=\(lastTimeStr) but_next_missing, will_retry")
                print("DNP-ROLLOVER: MISSED_ROLLOVER_RETRY id=\(notification.id) rollover_attempted=\(lastTimeStr) but_next_missing, will_retry")
                // Continue to process - rollover was attempted but failed
            }
            
            // Re-check if next notification exists (we already calculated nextScheduledTime above)
            // This is the final check before adding to missed rollovers list
            if !nextNotificationExists {
                let nextTimeStr = formatTime(nextScheduledTime)
                NSLog("DNP-ROLLOVER: MISSED_ROLLOVER_DETECTED id=\(notification.id) scheduled_time=\(formatTime(notification.scheduledTime)) next_time=\(nextTimeStr)")
                print("DNP-ROLLOVER: MISSED_ROLLOVER_DETECTED id=\(notification.id) scheduled_time=\(formatTime(notification.scheduledTime)) next_time=\(nextTimeStr)")
                missedRollovers.append(notification)
            }
        }
        
        NSLog("DNP-ROLLOVER: MISSED_ROLLOVER_FOUND count=\(missedRollovers.count) current_time=\(currentTimeStr)")
        print("DNP-ROLLOVER: MISSED_ROLLOVER_FOUND count=\(missedRollovers.count) current_time=\(currentTimeStr)")
        
        // Step 4: Process missed rollovers
        var processedCount = 0
        var failedCount = 0
        
        for notification in missedRollovers {
            let scheduledTimeStr = formatTime(notification.scheduledTime)
            NSLog("DNP-ROLLOVER: MISSED_ROLLOVER_PROCESS id=\(notification.id) scheduled_time=\(scheduledTimeStr)")
            print("DNP-ROLLOVER: MISSED_ROLLOVER_PROCESS id=\(notification.id) scheduled_time=\(scheduledTimeStr)")
            
            // Schedule next notification using existing rollover logic
            // Note: fetcher parameter is unused - scheduler uses fetchScheduler instead
            let scheduled = await scheduler.scheduleNextNotification(
                notification,
                storage: storage,
                fetcher: nil // Unused - fetchScheduler handles prefetch scheduling
            )
            
            if scheduled {
                processedCount += 1
                NSLog("DNP-ROLLOVER: MISSED_ROLLOVER_SUCCESS id=\(notification.id)")
                print("DNP-ROLLOVER: MISSED_ROLLOVER_SUCCESS id=\(notification.id)")
            } else {
                failedCount += 1
                NSLog("DNP-ROLLOVER: MISSED_ROLLOVER_FAILED id=\(notification.id)")
                print("DNP-ROLLOVER: MISSED_ROLLOVER_FAILED id=\(notification.id)")
            }
        }
        
        NSLog("DNP-ROLLOVER: MISSED_ROLLOVER_COMPLETE processed=\(processedCount) failed=\(failedCount) total_checked=\(allNotifications.count)")
        print("DNP-ROLLOVER: MISSED_ROLLOVER_COMPLETE processed=\(processedCount) failed=\(failedCount) total_checked=\(allNotifications.count)")
        
        return RolloverRecoveryResult(
            processedCount: processedCount,
            failedCount: failedCount,
            totalChecked: allNotifications.count
        )
    }
    
    /**
     * Format time for logging
     * 
     * @param timestamp Timestamp in milliseconds
     * @return Formatted time string
     */
    private func formatTime(_ timestamp: Int64) -> String {
        let date = Date(timeIntervalSince1970: Double(timestamp) / 1000.0)
        let formatter = DateFormatter()
        formatter.dateStyle = .medium
        formatter.timeStyle = .short
        return formatter.string(from: date)
    }
}

// MARK: - Supporting Types

/**
 * Recovery scenario enum
 */
enum RecoveryScenario: String {
    case none = "NONE"
    case coldStart = "COLD_START"
    case termination = "TERMINATION"
    case warmStart = "WARM_START"
    case boot = "BOOT"  // Phase 3: Boot recovery
}

/**
 * Recovery result
 */
struct RecoveryResult {
    let missedCount: Int
    let rescheduledCount: Int
    let verifiedCount: Int
    let errors: Int
}

/**
 * Verification result
 */
struct VerificationResult {
    let totalSchedules: Int
    let notificationsFound: Int
    let notificationsMissing: Int
    let missingIds: [String]
}

/**
 * Rollover recovery result
 */
struct RolloverRecoveryResult {
    let processedCount: Int
    let failedCount: Int
    let totalChecked: Int
}

/**
 * Reactivation errors
 */
enum ReactivationError: LocalizedError {
    case notificationNotFound(id: String)
    case rescheduleFailed(id: String)
    
    var errorDescription: String? {
        switch self {
        case .notificationNotFound(let id):
            return "Notification not found: \(id)"
        case .rescheduleFailed(let id):
            return "Failed to reschedule notification: \(id)"
        }
    }
}

// MARK: - Timeout Helper

/**
 * Timeout error
 */
struct TimeoutError: Error {}

/**
 * Execute async code with timeout
 */
func withTimeout<T>(seconds: TimeInterval, operation: @escaping () async throws -> T) async throws -> T {
    return try await withThrowingTaskGroup(of: T.self) { group in
        group.addTask {
            try await operation()
        }
        
        group.addTask {
            try await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000))
            throw TimeoutError()
        }
        
        let result = try await group.next()!
        group.cancelAll()
        return result
    }
}

