//
//  DailyNotificationPlugin.swift
//  DailyNotificationPlugin
//
//  Created by Matthew Raymer on 2025-09-22
//  Copyright © 2025 TimeSafari. All rights reserved.
//

import Foundation
import UIKit
import Capacitor
import UserNotifications
import BackgroundTasks
import CoreData
import ObjectiveC

/**
 * iOS implementation of Daily Notification Plugin
 * Implements BGTaskScheduler + UNUserNotificationCenter for dual scheduling
 * 
 * @author Matthew Raymer
 * @version 1.1.0
 * @created 2025-09-22 09:22:32 UTC
 */
@objc(DailyNotificationPlugin)
public class DailyNotificationPlugin: CAPPlugin {
    
    let notificationCenter = UNUserNotificationCenter.current()
    private let backgroundTaskScheduler = BGTaskScheduler.shared
    // Note: PersistenceController available for Phase 2+ CoreData integration if needed
    
    // Background task identifiers
    private let fetchTaskIdentifier = "org.timesafari.dailynotification.fetch"
    private let notifyTaskIdentifier = "org.timesafari.dailynotification.notify"
    /// Prefix for API-managed batch notification identifiers: `api_\(epochMillis)`.
    private let apiNotificationPrefix = "api_"
    /// Prefix for Daily Reminder UNNotificationRequest identifiers: `daily_\(logicalReminderId)`.
    private let dailyReminderNotificationPrefix = "daily_"
    /// Stable identifier for the dual (New Activity) user notification. Used for replace and cancelDualSchedule; must not conflict with Daily Reminder (`daily_*`).
    private let dualNotificationRequestIdentifier = "org.timesafari.dailynotification.dual"
    /// UserDefaults key for persisted dual schedule config (userNotification + relationship) for relationship resolution when fetch completes.
    private let dualScheduleConfigKey = "dual_schedule_config"
    /// Matches Android [DailyNotificationConstants.DUAL_NOTIFY_SCHEDULE_ID_KEY] — stable id for chained dual notify.
    private let dualNotifyScheduleIdStorageKey = "dual_notify_schedule_id"
    /// Max slip after nominal notify time before showing fallback (parity with product discussion).
    private let dualChainedMaxSlipSeconds: TimeInterval = 15 * 60
    /// Parity with Android SharedPreferences `daily_notification_timesafari` + `starredPlanIds`.
    private let starredPlanIdsStorageKey = "daily_notification_timesafari.starredPlanIds"
    
    // Phase 1: Storage and Scheduler components
    var storage: DailyNotificationStorage?
    var scheduler: DailyNotificationScheduler?
    
    // Phase 1: Reactivation manager for recovery
    var reactivationManager: DailyNotificationReactivationManager?
    
    // Phase 1: Concurrency actor for thread-safe state access
    @available(iOS 13.0, *)
    var stateActor: DailyNotificationStateActor?
    
    override public func load() {
        NSLog("DNP-DEBUG: DailyNotificationPlugin.load() called - Capacitor discovered the plugin!")
        super.load()
        setupBackgroundTasks()
        
        // Initialize Phase 1 components
        let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
        let defaultPath = documentsPath.appendingPathComponent("daily_notifications.db").path
        let database = DailyNotificationDatabase(path: defaultPath)
        storage = DailyNotificationStorage(databasePath: database.getPath())
        scheduler = DailyNotificationScheduler()
        
        // Initialize reactivation manager for recovery
        reactivationManager = DailyNotificationReactivationManager(
            database: database,
            storage: storage!,
            scheduler: scheduler!
        )
        
        // Initialize state actor for thread-safe access
        if #available(iOS 13.0, *) {
            stateActor = DailyNotificationStateActor(
                database: database,
                storage: storage!
            )
        }
        
        // Perform recovery on app launch (async, non-blocking)
        reactivationManager?.performRecovery()
        
        // Register for notification delivery events (Notification Center pattern)
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(handleNotificationDelivery(_:)),
            name: NSNotification.Name("DailyNotificationDelivered"),
            object: nil
        )
        
        NSLog("DNP-ROLLOVER: Observer registered for DailyNotificationDelivered notification")
        print("DNP-ROLLOVER: Observer registered for DailyNotificationDelivered notification")
        
        // Register for app becoming active to check for missed rollovers
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(handleAppBecameActive(_:)),
            name: UIApplication.didBecomeActiveNotification,
            object: nil
        )
        
        NSLog("DNP-ROLLOVER: Observer registered for app becoming active")
        print("DNP-ROLLOVER: Observer registered for app becoming active")
        
        NSLog("DNP-DEBUG: DailyNotificationPlugin.load() completed - initialization done")
        print("DNP-PLUGIN: Daily Notification Plugin loaded on iOS")
        
        // Debug: Log all available @objc methods for Capacitor discovery
        let methods = getObjCMethods()
        NSLog("DNP-DEBUG: Available @objc methods: \(methods.joined(separator: ", "))")
        print("DNP-DEBUG: Available @objc methods: \(methods.joined(separator: ", "))")
    }
    
    /**
     * Debug helper: Get all @objc methods for this class
     */
    private func getObjCMethods() -> [String] {
        var methods: [String] = []
        var methodCount: UInt32 = 0
        let methodList = class_copyMethodList(type(of: self), &methodCount)
        
        for i in 0..<Int(methodCount) {
            if let method = methodList?[i] {
                let selector = method_getName(method)
                let methodName = NSStringFromSelector(selector)
                // Filter for methods that look like plugin methods (take CAPPluginCall)
                if methodName.contains(":") && !methodName.hasPrefix("_") {
                    methods.append(methodName)
                }
            }
        }
        
        free(methodList)
        return methods.sorted()
    }
    
    // MARK: - Configuration Methods
    
    /**
     * Configure the plugin with database and storage options
     * 
     * Matches Android configure() functionality:
     * - dbPath: Custom database path (optional)
     * - storage: "shared" or "tiered" (default: "tiered")
     * - ttlSeconds: Time-to-live for cached content (optional)
     * - prefetchLeadMinutes: Minutes before notification to prefetch (optional)
     * - maxNotificationsPerDay: Maximum notifications per day (optional)
     * - retentionDays: Days to retain notification history (optional)
     * - activeDidIntegration: Phase 1 activeDid configuration (optional)
     * 
     * @param call Plugin call containing configuration parameters
     */
    @objc func configure(_ call: CAPPluginCall) {
        // Validate and extract configuration parameters
        let dbPath = call.getString("dbPath")
        let storageMode = call.getString("storage") ?? "tiered"
        let ttlSeconds = call.getInt("ttlSeconds")
        let prefetchLeadMinutes = call.getInt("prefetchLeadMinutes")
        let maxNotificationsPerDay = call.getInt("maxNotificationsPerDay")
        let retentionDays = call.getInt("retentionDays")
        
        // Phase 3: Process activeDidIntegration configuration
        if let activeDidConfig = call.getObject("activeDidIntegration") {
            // Extract and store activeDidIntegration configuration
            // This enables TimeSafari-specific DID-based authentication and API integration
            if let platform = activeDidConfig["platform"] as? String {
                UserDefaults.standard.set(platform, forKey: "activeDidIntegration_platform")
            }
            if let storageType = activeDidConfig["storageType"] as? String {
                UserDefaults.standard.set(storageType, forKey: "activeDidIntegration_storageType")
            }
            if let jwtExpirationSeconds = activeDidConfig["jwtExpirationSeconds"] as? Int {
                UserDefaults.standard.set(jwtExpirationSeconds, forKey: "activeDidIntegration_jwtExpirationSeconds")
            }
            if let apiServer = activeDidConfig["apiServer"] as? String {
                UserDefaults.standard.set(apiServer, forKey: "activeDidIntegration_apiServer")
            }
            if let activeDid = activeDidConfig["activeDid"] as? String {
                UserDefaults.standard.set(activeDid, forKey: "activeDidIntegration_activeDid")
            }
            if let autoSync = activeDidConfig["autoSync"] as? Bool {
                UserDefaults.standard.set(autoSync, forKey: "activeDidIntegration_autoSync")
            }
            if let identityChangeGraceSeconds = activeDidConfig["identityChangeGraceSeconds"] as? Int {
                UserDefaults.standard.set(identityChangeGraceSeconds, forKey: "activeDidIntegration_identityChangeGraceSeconds")
            }
            print("DNP-PLUGIN: activeDidIntegration configuration stored")
        }
        
        // Determine database path (use provided or default)
        let finalDbPath: String
        if let dbPath = dbPath, !dbPath.isEmpty {
            finalDbPath = dbPath
        } else {
            let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
            finalDbPath = documentsPath.appendingPathComponent("daily_notifications.db").path
        }
        
        // Reinitialize storage with new database path if needed
        if let currentStorage = storage {
            if currentStorage.getDatabasePath() != finalDbPath {
                storage = DailyNotificationStorage(databasePath: finalDbPath)
            }
        } else {
            storage = DailyNotificationStorage(databasePath: finalDbPath)
        }
        
        // Delegate to storage to store configuration
        storeConfiguration(
            ttlSeconds: ttlSeconds,
            prefetchLeadMinutes: prefetchLeadMinutes,
            maxNotificationsPerDay: maxNotificationsPerDay,
            retentionDays: retentionDays,
            storageMode: storageMode,
            dbPath: finalDbPath
        )
        
        call.resolve()
    }
    
    /**
     * Configure native fetcher with API credentials (cross-platform)
     * 
     * Matches Android configureNativeFetcher() functionality:
     * - Stores configuration in database for persistence
     * - Supports both jwtToken and jwtSecret for backward compatibility
     * - Note: iOS native fetcher interface not yet implemented, but configuration is stored
     * 
     * @param call Plugin call containing configuration parameters
     */
    @objc func configureNativeFetcher(_ call: CAPPluginCall) {
        guard let options = call.options else {
            call.reject("Options are required")
            return
        }
        
        guard NativeNotificationFetcherRegistry.shared.fetcher != nil else {
            call.reject("No native fetcher registered. Host app must call DailyNotificationPlugin.registerNativeFetcher(_:) before configureNativeFetcher.")
            return
        }
        
        guard let apiBaseUrl = options["apiBaseUrl"] as? String else {
            call.reject("apiBaseUrl is required")
            return
        }
        
        guard let activeDid = options["activeDid"] as? String else {
            call.reject("activeDid is required")
            return
        }
        
        // Support both jwtToken and jwtSecret for backward compatibility
        let jwtToken = (options["jwtToken"] as? String) ?? (options["jwtSecret"] as? String)
        guard let jwtToken = jwtToken else {
            call.reject("jwtToken or jwtSecret is required")
            return
        }

        let jwtTokenPoolMax = 128
        var jwtTokenPool: [String]? = nil
        if let rawTokens = options["jwtTokens"] as? [Any] {
            let parsed = rawTokens.compactMap { $0 as? String }.filter { !$0.isEmpty }
            if parsed.count > jwtTokenPoolMax {
                call.reject("jwtTokens must have at most \(jwtTokenPoolMax) entries")
                return
            }
            jwtTokenPool = parsed.isEmpty ? nil : parsed
        } else if let jsonStr = options["jwtTokenPoolJson"] as? String, !jsonStr.isEmpty {
            guard let data = jsonStr.data(using: .utf8),
                  let parsed = try? JSONSerialization.jsonObject(with: data) as? [String] else {
                call.reject("jwtTokenPoolJson must be a JSON array of strings")
                return
            }
            let filtered = parsed.filter { !$0.isEmpty }
            if filtered.count > jwtTokenPoolMax {
                call.reject("jwtTokens must have at most \(jwtTokenPoolMax) entries")
                return
            }
            jwtTokenPool = filtered.isEmpty ? nil : filtered
        }
        
        print("DNP-PLUGIN: Configuring native fetcher: apiBaseUrl=\(apiBaseUrl), activeDid=\(activeDid.prefix(30))...")
        if let pool = jwtTokenPool {
            print("DNP-PLUGIN: jwtTokenPool size=\(pool.count)")
        }
        
        NativeNotificationFetcherRegistry.shared.fetcher?.configure(
            apiBaseUrl: apiBaseUrl,
            activeDid: activeDid,
            jwtToken: jwtToken,
            jwtTokenPool: jwtTokenPool
        )
        
        var configValue: [String: Any] = [
            "apiBaseUrl": apiBaseUrl,
            "activeDid": activeDid,
            "jwtToken": jwtToken
        ]
        if let pool = jwtTokenPool {
            configValue["jwtTokenPool"] = pool
        }
        
        guard let jsonData = try? JSONSerialization.data(withJSONObject: configValue, options: []),
              let jsonString = String(data: jsonData, encoding: .utf8) else {
            call.reject("Failed to serialize configuration")
            return
        }
        
        let configKey = "native_fetcher_config"
        UserDefaults.standard.set(jsonString, forKey: configKey)
        print("DNP-PLUGIN: Native fetcher configuration stored successfully")
        call.resolve()
    }
    
    /**
     * Store configuration values
     * 
     * @param ttlSeconds TTL in seconds
     * @param prefetchLeadMinutes Prefetch lead time in minutes
     * @param maxNotificationsPerDay Maximum notifications per day
     * @param retentionDays Retention period in days
     * @param storageMode Storage mode ("shared" or "tiered")
     * @param dbPath Database path
     */
    private func storeConfiguration(
        ttlSeconds: Int?,
        prefetchLeadMinutes: Int?,
        maxNotificationsPerDay: Int?,
        retentionDays: Int?,
        storageMode: String,
        dbPath: String
    ) {
        var config: [String: Any] = [
            "storageMode": storageMode,
            "dbPath": dbPath
        ]
        
        if let ttlSeconds = ttlSeconds {
            config["ttlSeconds"] = ttlSeconds
        }
        
        if let prefetchLeadMinutes = prefetchLeadMinutes {
            config["prefetchLeadMinutes"] = prefetchLeadMinutes
        }
        
        if let maxNotificationsPerDay = maxNotificationsPerDay {
            config["maxNotificationsPerDay"] = maxNotificationsPerDay
        }
        
        if let retentionDays = retentionDays {
            config["retentionDays"] = retentionDays
        }
        
        storage?.saveSettings(config)
        print("DNP-PLUGIN: Configuration stored successfully")
    }
    
    // MARK: - Dual Scheduling Methods
    
    @objc func scheduleContentFetch(_ call: CAPPluginCall) {
        guard let config = call.getObject("config") else {
            call.reject("Content fetch config required")
            return
        }
        
        do {
            // Delegate to background fetch scheduler
            try scheduleBackgroundFetch(config: config)
            call.resolve()
        } catch {
            call.reject("Content fetch scheduling failed: \(error.localizedDescription)")
        }
    }
    
    @objc func scheduleUserNotification(_ call: CAPPluginCall) {
        guard let config = call.options as? [String: Any] else {
            call.reject("User notification config required")
            return
        }

        do {
            // Delegate to user notification scheduler
            try scheduleUserNotification(config: config)
            call.resolve()
        } catch {
            call.reject("User notification scheduling failed: \(error.localizedDescription)")
        }
    }
    
    @objc func scheduleDualNotification(_ call: CAPPluginCall) {
        guard let config = call.getObject("config"),
              let contentFetchConfig = config["contentFetch"] as? [String: Any],
              let userNotificationConfig = config["userNotification"] as? [String: Any] else {
            call.reject("Dual notification config required")
            return
        }
        
        do {
            // Chained dual: prefetch BG task only; user notification is armed after prefetch completes.
            try scheduleBackgroundFetch(config: contentFetchConfig)
            saveDualScheduleConfig(config)
            let sid = "dual_notify_\(Int64(Date().timeIntervalSince1970 * 1000))"
            UserDefaults.standard.set(sid, forKey: dualNotifyScheduleIdStorageKey)
            call.resolve()
        } catch {
            call.reject("Dual notification scheduling failed: \(error.localizedDescription)")
        }
    }
    
    @objc func getDualScheduleStatus(_ call: CAPPluginCall) {
        Task {
            do {
                // Delegate to private helper (will be moved to service in future batch)
                let status = try await getHealthStatus()
                DispatchQueue.main.async {
                    call.resolve(status)
                }
            } catch {
                DispatchQueue.main.async {
                    print("DNP-PLUGIN: Failed to get dual schedule status: \(error)")
                    call.reject("Status retrieval failed: \(error.localizedDescription)")
                }
            }
        }
    }
    
    /// Cancel the dual (New Activity) schedule only. Does not affect Daily Reminder (`daily_*`).
    @objc func cancelDualSchedule(_ call: CAPPluginCall) {
        do {
            performCancelDualSchedule()
            call.resolve()
        } catch {
            call.reject("Cancel dual schedule failed: \(error.localizedDescription)")
        }
    }
    
    /// Cancel only the dual content-fetch task and dual user notification. Used by cancelDualSchedule() and updateDualScheduleConfig().
    private func performCancelDualSchedule() {
        backgroundTaskScheduler.cancel(taskRequestWithIdentifier: fetchTaskIdentifier)
        notificationCenter.removePendingNotificationRequests(withIdentifiers: [dualNotificationRequestIdentifier])
        UserDefaults.standard.removeObject(forKey: dualScheduleConfigKey)
        UserDefaults.standard.removeObject(forKey: dualNotifyScheduleIdStorageKey)
        print("DNP-PLUGIN: Canceled dual schedule (fetch task + user notification)")
    }
    
    @objc func updateDualScheduleConfig(_ call: CAPPluginCall) {
        guard let config = call.getObject("config"),
              let contentFetchConfig = config["contentFetch"] as? [String: Any],
              let userNotificationConfig = config["userNotification"] as? [String: Any] else {
            call.reject("Dual notification config required")
            return
        }
        do {
            performCancelDualSchedule()
            try scheduleBackgroundFetch(config: contentFetchConfig)
            saveDualScheduleConfig(config)
            let sid = "dual_notify_\(Int64(Date().timeIntervalSince1970 * 1000))"
            UserDefaults.standard.set(sid, forKey: dualNotifyScheduleIdStorageKey)
            call.resolve()
        } catch {
            call.reject("Update dual schedule failed: \(error.localizedDescription)")
        }
    }
    
    @objc func updateStarredPlans(_ call: CAPPluginCall) {
        guard let raw = call.options?["planIds"] else {
            call.reject("planIds is required and must be a string array")
            return
        }
        var planIds: [String] = []
        if let a = raw as? [String] {
            planIds = a
        } else if let anyArr = raw as? [Any] {
            for (index, item) in anyArr.enumerated() {
                guard let s = item as? String else {
                    call.reject("planIds must be an array of strings (non-string at index \(index))")
                    return
                }
                planIds.append(s)
            }
        } else {
            call.reject("planIds must be a string array")
            return
        }
        for (index, id) in planIds.enumerated() {
            if id.trimmingCharacters(in: .whitespaces).isEmpty {
                call.reject("planIds[\(index)] must be a non-empty string")
                return
            }
        }
        do {
            let jsonData = try JSONSerialization.data(withJSONObject: planIds, options: [])
            guard let jsonStr = String(data: jsonData, encoding: .utf8) else {
                call.reject("Failed to serialize planIds")
                return
            }
            UserDefaults.standard.set(jsonStr, forKey: starredPlanIdsStorageKey)
            let updatedAt = Int64(Date().timeIntervalSince1970 * 1000)
            UserDefaults.standard.set(updatedAt, forKey: "\(starredPlanIdsStorageKey).updatedAt")
            call.resolve([
                "success": true,
                "planIdsCount": planIds.count,
                "updatedAt": updatedAt
            ])
        } catch {
            call.reject("Failed to update starred plans: \(error.localizedDescription)")
        }
    }
    
    @objc func getStarredPlans(_ call: CAPPluginCall) {
        let jsonStr = UserDefaults.standard.string(forKey: starredPlanIdsStorageKey) ?? "[]"
        let planIds: [String]
        if let data = jsonStr.data(using: .utf8),
           let arr = try? JSONSerialization.jsonObject(with: data) as? [String] {
            planIds = arr
        } else {
            planIds = []
        }
        let updatedKey = "\(starredPlanIdsStorageKey).updatedAt"
        let updatedAt: Int64
        if let n = UserDefaults.standard.object(forKey: updatedKey) as? Int64 {
            updatedAt = n
        } else if let num = UserDefaults.standard.object(forKey: updatedKey) as? NSNumber {
            updatedAt = num.int64Value
        } else {
            updatedAt = 0
        }
        call.resolve([
            "planIds": planIds,
            "count": planIds.count,
            "updatedAt": updatedAt
        ])
    }
    
    /// Persist dual schedule config (userNotification + relationship) for relationship resolution when fetch completes.
    private func saveDualScheduleConfig(_ config: [String: Any]) {
        guard config["userNotification"] != nil,
              let jsonData = try? JSONSerialization.data(withJSONObject: config),
              let jsonString = String(data: jsonData, encoding: .utf8) else { return }
        UserDefaults.standard.set(jsonString, forKey: dualScheduleConfigKey)
    }
    
    /// Chained dual: arm a **one-shot** user notification at `max(T, prefetchCompletedAt)` (capped by slip), never before prefetch completes.
    private func armChainedDualNotificationAfterPrefetch(fetchedContent: NotificationContent, prefetchCompletedAt: Date) {
        guard let configJson = UserDefaults.standard.string(forKey: dualScheduleConfigKey),
              let configData = configJson.data(using: .utf8),
              let config = try? JSONSerialization.jsonObject(with: configData) as? [String: Any],
              let userNotification = config["userNotification"] as? [String: Any] else {
            return
        }
        let relationship = config["relationship"] as? [String: Any]
        let contentTimeoutMs = (relationship?["contentTimeout"] as? NSNumber)?.intValue ?? 300_000
        let fallbackBehavior = relationship?["fallbackBehavior"] as? String ?? "show_default"
        
        let nowMs = Int64(Date().timeIntervalSince1970 * 1000)
        let useFetched = (nowMs - fetchedContent.fetchedAt) <= contentTimeoutMs
        let scheduleStr = userNotification["schedule"] as? String ?? "0 9 * * *"
        guard let nominalT = dualNextWallClockDate(scheduleStr: scheduleStr, after: prefetchCompletedAt.addingTimeInterval(-1)) else {
            print("DNP-FETCH: Dual chained: invalid notify cron")
            return
        }
        let slippedLate = prefetchCompletedAt.timeIntervalSince(nominalT) > dualChainedMaxSlipSeconds
        var fireDate = max(nominalT, prefetchCompletedAt)
        if slippedLate {
            fireDate = Date().addingTimeInterval(2)
        }
        
        let title: String
        let body: String
        if slippedLate && fallbackBehavior == "show_default" {
            title = userNotification["title"] as? String ?? "Daily Notification"
            body = userNotification["body"] as? String ?? "Your daily update is ready"
        } else if useFetched {
            title = fetchedContent.title ?? "Daily Notification"
            body = fetchedContent.body ?? "Your daily update is ready"
        } else if fallbackBehavior == "show_default" {
            title = userNotification["title"] as? String ?? "Daily Notification"
            body = userNotification["body"] as? String ?? "Your daily update is ready"
        } else {
            return
        }
        
        let content = UNMutableNotificationContent()
        content.title = title
        content.body = body
        content.sound = (userNotification["sound"] as? Bool ?? true) ? .default : nil
        let scheduledMs = Int64(fireDate.timeIntervalSince1970 * 1000)
        content.userInfo = [
            "notification_id": dualNotificationRequestIdentifier,
            "scheduled_time": NSNumber(value: scheduledMs)
        ]
        let cal = Calendar.current
        let comps = cal.dateComponents([.year, .month, .day, .hour, .minute, .second], from: fireDate)
        let trigger = UNCalendarNotificationTrigger(dateMatching: comps, repeats: false)
        notificationCenter.removePendingNotificationRequests(withIdentifiers: [dualNotificationRequestIdentifier])
        let request = UNNotificationRequest(identifier: dualNotificationRequestIdentifier, content: content, trigger: trigger)
        notificationCenter.add(request) { err in
            if let e = err {
                print("DNP-FETCH: Failed to arm chained dual notification: \(e.localizedDescription)")
            } else {
                print("DNP-FETCH: Armed chained dual notification at \(fireDate) (useFetched=\(useFetched))")
            }
        }
    }
    
    private func dualNextWallClockDate(scheduleStr: String, after date: Date) -> Date? {
        let parts = scheduleStr.trimmingCharacters(in: .whitespaces).split(separator: " ").map(String.init)
        guard parts.count >= 2,
              let minute = Int(parts[0]), minute >= 0, minute <= 59,
              let hour = Int(parts[1]), hour >= 0, hour <= 23 else {
            return nil
        }
        var comp = DateComponents()
        comp.hour = hour
        comp.minute = minute
        comp.second = 0
        return Calendar.current.nextDate(after: date, matching: comp, matchingPolicy: .nextTime)
    }
    
    /**
     * Get health status for dual scheduling system
     * 
     * @return Health status dictionary
     */
    private func getHealthStatus() async throws -> [String: Any] {
        guard let scheduler = scheduler else {
            throw NSError(domain: "DailyNotificationPlugin", code: -1, userInfo: [NSLocalizedDescriptionKey: "Scheduler not initialized"])
        }
        
        // Delegate to ScheduleHelper for health status (combines multiple sources)
        return try await DailyNotificationScheduleHelper.getHealthStatus(
            scheduler: scheduler,
            storage: self.storage,
            stateActor: await self.stateActor
        )
    }
    
    // MARK: - Private Implementation Methods
    
    private func setupBackgroundTasks() {
        // Register background fetch task
        backgroundTaskScheduler.register(forTaskWithIdentifier: fetchTaskIdentifier, using: nil) { task in
            self.handleBackgroundFetch(task: task as! BGAppRefreshTask)
        }
        
        // Register background processing task
        backgroundTaskScheduler.register(forTaskWithIdentifier: notifyTaskIdentifier, using: nil) { task in
            self.handleBackgroundNotify(task: task as! BGProcessingTask)
        }
        
        // Phase 1: Check for missed BGTask on app launch
        checkForMissedBGTask()
    }
    
    /**
     * Handle background fetch task
     * 
     * Phase 1: Dummy fetcher - returns static content
     * Phase 3: Will be replaced with JWT-signed fetcher
     * 
     * Enhanced with:
     * - Recovery logic (verify scheduled notifications)
     * - Next task scheduling
     * - Graceful expiration handling
     * 
     * @param task BGAppRefreshTask
     */
    private func handleBackgroundFetch(task: BGAppRefreshTask) {
        print("DNP-FETCH: Background fetch task started")
        
        // Enhanced expiration handler with graceful cleanup
        var taskCompleted = false
        task.expirationHandler = {
            guard !taskCompleted else { return }
            print("DNP-FETCH: Background fetch task expired - performing graceful cleanup")
            
            // Cancel any ongoing operations
            // Note: In production, you might want to cancel URLSession tasks here
            
            task.setTaskCompleted(success: false)
            taskCompleted = true
        }
        
        let nativeFetcherConfig = UserDefaults.standard.string(forKey: "native_fetcher_config")
        
        Task {
            let prefetchCompletedAt = Date()
            let fetchTimeMs = Int64(prefetchCompletedAt.timeIntervalSince1970 * 1000)
            let content: NotificationContent
            
            if let reg = NativeNotificationFetcherRegistry.shared.fetcher {
                let ctx = FetchContext(
                    trigger: "prefetch",
                    scheduledTimeMillis: nil,
                    fetchTimeMillis: fetchTimeMs,
                    metadata: [:]
                )
                do {
                    let list = try await withTimeout(milliseconds: 30_000) {
                        try await reg.fetchContent(context: ctx)
                    }
                    if let first = list.first {
                        content = first
                        print("DNP-FETCH: Native fetcher returned content id=\(first.id)")
                    } else {
                        print("DNP-FETCH: Native fetcher returned empty list; using placeholder")
                        content = NotificationContent(
                            id: "empty_\(Date().timeIntervalSince1970)",
                            title: "Daily Update",
                            body: "Your daily notification is ready",
                            scheduledTime: fetchTimeMs + (5 * 60 * 1000),
                            fetchedAt: fetchTimeMs,
                            url: nil,
                            payload: ["empty": true],
                            etag: nil
                        )
                    }
                } catch {
                    print("DNP-FETCH: Native fetcher failed (\(error.localizedDescription)), using fallback content")
                    content = NotificationContent(
                        id: "fallback_\(Date().timeIntervalSince1970)",
                        title: "Daily Update",
                        body: "Your daily notification is ready",
                        scheduledTime: fetchTimeMs + (5 * 60 * 1000),
                        fetchedAt: fetchTimeMs,
                        url: nil,
                        payload: ["fetchError": error.localizedDescription],
                        etag: nil
                    )
                }
            } else if let configJson = nativeFetcherConfig,
                      let configData = configJson.data(using: .utf8),
                      let config = try? JSONSerialization.jsonObject(with: configData) as? [String: Any],
                      let apiBaseUrl = config["apiBaseUrl"] as? String,
                      let activeDid = config["activeDid"] as? String {
                let jwtFromPrimary = (config["jwtToken"] as? String).flatMap { $0.isEmpty ? nil : $0 }
                let jwtFromPool = (config["jwtTokenPool"] as? [String])?.first { !$0.isEmpty }
                let bearerToken = jwtFromPrimary ?? jwtFromPool
                if let jwtToken = bearerToken {
                    print("DNP-FETCH: Legacy in-plugin HTTP (no registered native fetcher)")
                    do {
                        content = try await fetchContentFromAPI(
                            apiBaseUrl: apiBaseUrl,
                            activeDid: activeDid,
                            jwtToken: jwtToken
                        )
                    } catch {
                        content = NotificationContent(
                            id: "fallback_\(Date().timeIntervalSince1970)",
                            title: "Daily Update",
                            body: "Your daily notification is ready",
                            scheduledTime: fetchTimeMs + (5 * 60 * 1000),
                            fetchedAt: fetchTimeMs,
                            url: nil,
                            payload: ["fetchError": error.localizedDescription],
                            etag: nil
                        )
                    }
                } else {
                    content = NotificationContent(
                        id: "dummy_\(Date().timeIntervalSince1970)",
                        title: "Daily Update",
                        body: "Your daily notification is ready",
                        scheduledTime: fetchTimeMs + (5 * 60 * 1000),
                        fetchedAt: fetchTimeMs,
                        url: nil,
                        payload: nil,
                        etag: nil
                    )
                }
            } else {
                print("DNP-FETCH: Using dummy content (no native fetcher, no config)")
                content = NotificationContent(
                    id: "dummy_\(Date().timeIntervalSince1970)",
                    title: "Daily Update",
                    body: "Your daily notification is ready",
                    scheduledTime: fetchTimeMs + (5 * 60 * 1000),
                    fetchedAt: fetchTimeMs,
                    url: nil,
                    payload: nil,
                    etag: nil
                )
            }
            
            do {
                if #available(iOS 13.0, *) {
                    if let stateActor = await self.stateActor {
                        await stateActor.saveNotificationContent(content)
                        let currentTime = Int64(Date().timeIntervalSince1970 * 1000)
                        await stateActor.saveLastSuccessfulRun(timestamp: currentTime)
                    } else {
                        self.storage?.saveNotificationContent(content)
                        let currentTime = Int64(Date().timeIntervalSince1970 * 1000)
                        self.storage?.saveLastSuccessfulRun(timestamp: currentTime)
                    }
                } else {
                    self.storage?.saveNotificationContent(content)
                    let currentTime = Int64(Date().timeIntervalSince1970 * 1000)
                    self.storage?.saveLastSuccessfulRun(timestamp: currentTime)
                }
                
                if UserDefaults.standard.string(forKey: self.dualScheduleConfigKey) != nil {
                    self.armChainedDualNotificationAfterPrefetch(fetchedContent: content, prefetchCompletedAt: prefetchCompletedAt)
                    do {
                        try self.scheduleNextDualPrefetchFromPersistedConfig()
                    } catch {
                        print("DNP-FETCH: Next dual prefetch schedule failed: \(error.localizedDescription)")
                    }
                }
                
                if let reactivationManager = self.reactivationManager {
                    Task {
                        do {
                            let verificationResult = try await reactivationManager.verifyFutureNotifications()
                            if verificationResult.notificationsMissing > 0 {
                                print("DNP-FETCH: Recovery - found \(verificationResult.notificationsMissing) missing notifications, will reschedule on next app launch")
                            }
                        } catch {
                            print("DNP-FETCH: Recovery verification failed (non-fatal): \(error.localizedDescription)")
                        }
                    }
                }
                
                if UserDefaults.standard.string(forKey: self.dualScheduleConfigKey) == nil {
                    if let scheduler = self.scheduler {
                        let nextScheduledTime = await scheduler.getNextNotificationTime()
                        if let nextTime = nextScheduledTime {
                            self.scheduleBackgroundFetch(scheduledTime: nextTime)
                            print("DNP-FETCH: Next background fetch scheduled")
                        } else {
                            print("DNP-FETCH: No future notifications found, skipping next task schedule")
                        }
                    } else {
                        print("DNP-FETCH: Scheduler not available, skipping next task schedule")
                    }
                }
                
                guard !taskCompleted else { return }
                task.setTaskCompleted(success: true)
                taskCompleted = true
                print("DNP-FETCH: Background fetch task completed successfully")
                
            } catch {
                print("DNP-FETCH: Background fetch task failed: \(error.localizedDescription)")
                guard !taskCompleted else { return }
                task.setTaskCompleted(success: false)
                taskCompleted = true
            }
        }
    }
    
    private func scheduleNextDualPrefetchFromPersistedConfig() throws {
        guard let json = UserDefaults.standard.string(forKey: dualScheduleConfigKey),
              let data = json.data(using: .utf8),
              let root = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
              let cf = root["contentFetch"] as? [String: Any] else {
            return
        }
        try scheduleBackgroundFetch(config: cf)
    }
    
    /**
     * Handle background notification task
     * 
     * Enhanced with:
     * - Recovery logic (verify scheduled notifications)
     * - Next task scheduling
     * - Graceful expiration handling
     * 
     * @param task BGProcessingTask
     */
    private func handleBackgroundNotify(task: BGProcessingTask) {
        print("DNP-NOTIFY: Background notify task started")
        
        // Enhanced expiration handler with graceful cleanup
        var taskCompleted = false
        task.expirationHandler = {
            guard !taskCompleted else { return }
            print("DNP-NOTIFY: Background notify task expired - performing graceful cleanup")
            task.setTaskCompleted(success: false)
            taskCompleted = true
        }
        
        Task {
            do {
                // Phase 3.3: Recovery logic - verify scheduled notifications
                // Check if notifications are still scheduled
                if let reactivationManager = self.reactivationManager {
                    // Perform lightweight verification (non-blocking)
                    let verificationResult = try await reactivationManager.verifyFutureNotifications()
                    if verificationResult.notificationsMissing > 0 {
                        print("DNP-NOTIFY: Recovery - found \(verificationResult.notificationsMissing) missing notifications, will reschedule on next app launch")
                        // Note: Full recovery happens on app launch, not in background task
                    }
                }
                
                // Phase 1: Not used for single daily schedule
                // This will be used in Phase 2+ for rolling window maintenance
                // For now, just verify state
                
                // Phase 3.3: Schedule next background task if needed
                // For notify task, schedule next occurrence if applicable
                if let scheduler = self.scheduler {
                    let nextScheduledTime = await scheduler.getNextNotificationTime()
                    if let nextTime = nextScheduledTime {
                        // Calculate next notify task time (if applicable)
                        // Note: Notify tasks are typically scheduled less frequently than fetch tasks
                        print("DNP-NOTIFY: Next notification scheduled at \(nextTime)")
                    }
                }
                
                guard !taskCompleted else { return }
                task.setTaskCompleted(success: true)
                taskCompleted = true
                print("DNP-NOTIFY: Background notify task completed successfully")
                
            } catch {
                print("DNP-NOTIFY: Background notify task failed: \(error.localizedDescription)")
                guard !taskCompleted else { return }
                task.setTaskCompleted(success: false)
                taskCompleted = true
            }
        }
    }
    
    /**
     * Check for missed BGTask and reschedule if needed
     * 
     * Phase 1: BGTask Miss Detection
     * - Checks if BGTask was scheduled but not run within 15 min window
     * - Reschedules if missed
     */
    private func checkForMissedBGTask() {
        Task {
            var earliestBeginTimestamp: Int64?
            var lastSuccessfulRun: Int64?
            
            // Get BGTask tracking info via state actor (thread-safe)
            if #available(iOS 13.0, *) {
                if let stateActor = await self.stateActor {
                    earliestBeginTimestamp = await stateActor.getBGTaskEarliestBegin()
                    lastSuccessfulRun = await stateActor.getLastSuccessfulRun()
                } else {
                    // Fallback to direct storage access
                    earliestBeginTimestamp = self.storage?.getBGTaskEarliestBegin()
                    lastSuccessfulRun = self.storage?.getLastSuccessfulRun()
                }
            } else {
                // Fallback for iOS < 13
                earliestBeginTimestamp = self.storage?.getBGTaskEarliestBegin()
                lastSuccessfulRun = self.storage?.getLastSuccessfulRun()
            }
            
            guard let earliestBeginTime = earliestBeginTimestamp else {
                // No BGTask scheduled
                return
            }
            
            let currentTime = Int64(Date().timeIntervalSince1970 * 1000)
            let missWindow = Int64(15 * 60 * 1000) // 15 minutes in milliseconds
            
            // Check if task was missed (current time > earliestBeginTime + 15 min)
            if currentTime > earliestBeginTime + missWindow {
                // Check if there was a successful run
                if let lastRun = lastSuccessfulRun {
                    // If last successful run was after earliestBeginTime, task was not missed
                    if lastRun >= earliestBeginTime {
                        print("DNP-FETCH: BGTask completed successfully, no reschedule needed")
                        return
                    }
                }
                
                // Task was missed - reschedule
                print("DNP-FETCH: BGTask missed window; rescheduling")
                
                // Reschedule for 1 minute from now
                let rescheduleTime = currentTime + (1 * 60 * 1000) // 1 minute from now
                let rescheduleDate = Date(timeIntervalSince1970: Double(rescheduleTime) / 1000.0)
                
                let request = BGAppRefreshTaskRequest(identifier: fetchTaskIdentifier)
                request.earliestBeginDate = rescheduleDate
                
                do {
                    try backgroundTaskScheduler.submit(request)
                    
                    // Save rescheduled time via state actor (thread-safe)
                    if #available(iOS 13.0, *) {
                        if let stateActor = await self.stateActor {
                            await stateActor.saveBGTaskEarliestBegin(timestamp: rescheduleTime)
                        } else {
                            // Fallback to direct storage access
                            self.storage?.saveBGTaskEarliestBegin(timestamp: rescheduleTime)
                        }
                    } else {
                        // Fallback for iOS < 13
                        self.storage?.saveBGTaskEarliestBegin(timestamp: rescheduleTime)
                    }
                    
                    print("DNP-FETCH: BGTask rescheduled for \(rescheduleDate)")
                } catch {
                    print("DNP-FETCH: Failed to reschedule BGTask: \(error)")
                }
            }
        }
    }
    
    private func scheduleBackgroundFetch(config: [String: Any]) throws {
        let request = BGAppRefreshTaskRequest(identifier: fetchTaskIdentifier)
        
        // Calculate next run time (simplified - would use proper cron parsing in production)
        let nextRunTime = calculateNextRunTime(from: config["schedule"] as? String ?? "0 9 * * *")
        request.earliestBeginDate = Date(timeIntervalSinceNow: nextRunTime)
        
        try backgroundTaskScheduler.submit(request)
        print("DNP-FETCH-SCHEDULE: Background fetch scheduled for \(request.earliestBeginDate!)")
    }
    
    private func scheduleUserNotification(config: [String: Any]) throws {
        let content = UNMutableNotificationContent()
        content.title = config["title"] as? String ?? "Daily Notification"
        content.body = config["body"] as? String ?? "Your daily update is ready"
        content.sound = (config["sound"] as? Bool ?? true) ? .default : nil
        
        // Parse cron "minute hour * * *" for daily at local time (replace semantics: one dual notification)
        let scheduleStr = config["schedule"] as? String ?? "0 9 * * *"
        let parts = scheduleStr.trimmingCharacters(in: .whitespaces).split(separator: " ").map(String.init)
        let hour: Int
        let minute: Int
        if parts.count >= 2, let m = Int(parts[0]), let h = Int(parts[1]), m >= 0, m <= 59, h >= 0, h <= 23 {
            minute = m
            hour = h
        } else {
            minute = 0
            hour = 9
        }
        var dateComp = DateComponents()
        dateComp.hour = hour
        dateComp.minute = minute
        dateComp.second = 0
        let trigger = UNCalendarNotificationTrigger(dateMatching: dateComp, repeats: true)
        
        // Replace any existing dual notification (stable id for cancelDualSchedule and updateDualScheduleConfig)
        notificationCenter.removePendingNotificationRequests(withIdentifiers: [dualNotificationRequestIdentifier])
        
        let request = UNNotificationRequest(
            identifier: dualNotificationRequestIdentifier,
            content: content,
            trigger: trigger
        )
        
        notificationCenter.add(request) { error in
            if let error = error {
                print("DNP-NOTIFY-SCHEDULE: Failed to schedule notification: \(error)")
            } else {
                print("DNP-NOTIFY-SCHEDULE: Notification scheduled successfully (id: \(self.dualNotificationRequestIdentifier))")
            }
        }
    }
    
    /// Parse cron "minute hour * * *" (daily) and return seconds from now until next occurrence (device local time).
    /// Matches Android calculateNextRunTime semantics for parity.
    private func calculateNextRunTime(from schedule: String) -> TimeInterval {
        let parts = schedule.trimmingCharacters(in: .whitespaces).split(separator: " ").map(String.init)
        guard parts.count >= 2,
              let minute = Int(parts[0]), minute >= 0, minute <= 59,
              let hour = Int(parts[1]), hour >= 0, hour <= 23 else {
            print("DNP-SCHEDULE: Invalid cron format: \(schedule), defaulting to 24h from now")
            return 24 * 60 * 60
        }
        var comp = DateComponents()
        comp.hour = hour
        comp.minute = minute
        comp.second = 0
        let cal = Calendar.current
        guard let next = cal.nextDate(after: Date(), matching: comp, matchingPolicy: .nextTime) else {
            return 24 * 60 * 60
        }
        let interval = next.timeIntervalSinceNow
        return interval > 0 ? interval : (24 * 60 * 60)
    }

    /// Epoch milliseconds for the next local occurrence of `hour`:`minute` (persisted as `predictiveEpochMillis` for reminder alignment).
    private func epochMillisNextDailyOccurrence(hour: Int, minute: Int) -> Int64 {
        var comp = DateComponents()
        comp.hour = hour
        comp.minute = minute
        comp.second = 0
        guard let next = Calendar.current.nextDate(after: Date(), matching: comp, matchingPolicy: .nextTime) else {
            return Int64(Date().timeIntervalSince1970 * 1000)
        }
        return Int64(next.timeIntervalSince1970 * 1000)
    }

    private func apiNotificationId(epochMillis: Int64) -> String {
        "\(apiNotificationPrefix)\(epochMillis)"
    }

    private func dailyReminderNotificationId(reminderId: String) -> String {
        "\(dailyReminderNotificationPrefix)\(reminderId)"
    }

    /// Pre-separation Daily Reminders used `api_<epochMillis>`. Used only to cancel stale pending requests.
    private func legacyApiReminderNotificationId(from reminder: [String: Any]) -> String? {
        guard let n = reminder["predictiveEpochMillis"] as? NSNumber else { return nil }
        return apiNotificationId(epochMillis: n.int64Value)
    }

    private func cancelDailyReminderNotificationRequests(reminderId: String, storedReminder: [String: Any]?) {
        var ids = [dailyReminderNotificationId(reminderId: reminderId)]
        if let storedReminder = storedReminder,
           let legacyId = legacyApiReminderNotificationId(from: storedReminder),
           !ids.contains(legacyId) {
            ids.append(legacyId)
        }
        notificationCenter.removePendingNotificationRequests(withIdentifiers: ids)
    }

    // MARK: - API batch notifications (replace-all UNUserNotificationCenter)

    /// Removes only pending and delivered API-managed notifications (`api_*`). Does not touch Daily Reminders (`daily_*`), dual/org IDs, or other stacks.
    @objc func clearApiNotifications(_ call: CAPPluginCall) {
        NSLog("DNP-BATCH: clearApiNotifications — removing api_* pending and delivered only")
        print("DNP-BATCH: clearApiNotifications — removing api_* pending and delivered only")
        let center = notificationCenter
        let prefix = apiNotificationPrefix
        let group = DispatchGroup()

        group.enter()
        center.getPendingNotificationRequests { requests in
            let ids = requests.map { $0.identifier }.filter { $0.hasPrefix(prefix) }
            center.removePendingNotificationRequests(withIdentifiers: ids)
            NSLog("DNP-BATCH: cleared \(ids.count) pending API notification id(s)")
            print("DNP-BATCH: cleared \(ids.count) pending API notification id(s)")
            group.leave()
        }

        group.enter()
        center.getDeliveredNotifications { notifications in
            let ids = notifications.map { $0.request.identifier }.filter { $0.hasPrefix(prefix) }
            center.removeDeliveredNotifications(withIdentifiers: ids)
            NSLog("DNP-BATCH: cleared \(ids.count) delivered API notification id(s)")
            print("DNP-BATCH: cleared \(ids.count) delivered API notification id(s)")
            group.leave()
        }

        group.notify(queue: .main) {
            call.resolve()
        }
    }

    /// Add one-shot reminders at epoch-ms timestamps. Does not remove other requests; identical IDs replace pending entries. Caller should clear first if needed.
    /// Adds/overwrites API-managed notifications using deterministic IDs.
    /// Does NOT clear existing notifications. Caller is responsible for lifecycle.
    @objc func scheduleApiNotifications(_ call: CAPPluginCall) {
        guard let timestamps = call.getArray("timestamps", Double.self) else {
            call.reject("Missing timestamps")
            return
        }

        NSLog("DNP-BATCH: scheduleApiNotifications — additive scheduling for \(timestamps.count) timestamp(s)")
        print("DNP-BATCH: scheduleApiNotifications — additive scheduling for \(timestamps.count) timestamp(s)")

        for ts in timestamps {
            let date = Date(timeIntervalSince1970: ts / 1000)
            guard date.timeIntervalSinceNow > 0 else {
                NSLog("DNP-BATCH: skip stale timestamp ts=\(ts) (not in the future)")
                print("DNP-BATCH: skip stale timestamp ts=\(ts) (not in the future)")
                continue
            }

            let interval = date.timeIntervalSinceNow

            let trigger = UNTimeIntervalNotificationTrigger(
                timeInterval: interval,
                repeats: false
            )

            let content = UNMutableNotificationContent()
            content.title = "Reminder"
            content.body = "You have a scheduled notification"

            let id = "\(apiNotificationPrefix)\(Int(ts))"

            NSLog("DNP-BATCH: scheduling ts=\(ts) interval=\(interval)s id=\(id)")
            print("DNP-BATCH: scheduling ts=\(ts) interval=\(interval)s id=\(id)")

            let request = UNNotificationRequest(
                identifier: id,
                content: content,
                trigger: trigger
            )

            notificationCenter.add(request) { error in
                if let error = error {
                    NSLog("DNP-BATCH: add failed id=\(id) error=\(error.localizedDescription)")
                    print("DNP-BATCH: add failed id=\(id) error=\(error.localizedDescription)")
                }
            }
        }

        call.resolve()
    }

    // MARK: - Static Daily Reminder Methods

    @objc func scheduleDailyReminder(_ call: CAPPluginCall) {
        guard let id = call.getString("id"),
              let title = call.getString("title"),
              let body = call.getString("body"),
              let time = call.getString("time") else {
            call.reject("Missing required parameters: id, title, body, time")
            return
        }

        let sound = call.getBool("sound", true)
        let vibration = call.getBool("vibration", true)
        let priority = call.getString("priority", "normal")
        let repeatDaily = call.getBool("repeatDaily", true)
        let timezone = call.getString("timezone")

        // Validate and parse time (HH:mm format)
        let timeComponents = time.components(separatedBy: ":")
        guard timeComponents.count == 2,
              let hour = Int(timeComponents[0]),
              let minute = Int(timeComponents[1]),
              hour >= 0 && hour <= 23,
              minute >= 0 && minute <= 59 else {
            call.reject("Invalid time format. Use HH:mm (e.g., 09:00)")
            return
        }

        // Create notification content
        let content = UNMutableNotificationContent()
        content.title = title
        content.body = body
        content.sound = sound ? .default : nil
        content.categoryIdentifier = "DAILY_REMINDER"

        // Set priority
        if #available(iOS 15.0, *) {
            switch priority {
            case "high":
                content.interruptionLevel = .critical
            case "low":
                content.interruptionLevel = .passive
            default:
                content.interruptionLevel = .active
            }
        }

        // Create date components for daily trigger
        var dateComponents = DateComponents()
        dateComponents.hour = hour
        dateComponents.minute = minute

        let trigger = UNCalendarNotificationTrigger(
            dateMatching: dateComponents,
            repeats: repeatDaily
        )

        let epochMillis = epochMillisNextDailyOccurrence(hour: hour, minute: minute)
        let requestId = dailyReminderNotificationId(reminderId: id)

        // Cancel any pre-separation `api_*` pending request for this logical reminder id.
        if let existing = getRemindersFromUserDefaults().first(where: { ($0["id"] as? String) == id }),
           let legacyId = legacyApiReminderNotificationId(from: existing) {
            notificationCenter.removePendingNotificationRequests(withIdentifiers: [legacyId])
        }

        let request = UNNotificationRequest(
            identifier: requestId,
            content: content,
            trigger: trigger
        )

        // Store reminder in UserDefaults
        storeReminderInUserDefaults(
            id: id,
            title: title,
            body: body,
            time: time,
            sound: sound,
            vibration: vibration,
            priority: priority,
            repeatDaily: repeatDaily,
            timezone: timezone,
            predictiveEpochMillis: epochMillis
        )

        // Delegate to UNUserNotificationCenter to schedule notification
        notificationCenter.add(request) { error in
            DispatchQueue.main.async {
                if let error = error {
                    call.reject("Failed to schedule daily reminder: \(error.localizedDescription)")
                } else {
                    call.resolve()
                }
            }
        }
    }

    @objc func cancelDailyReminder(_ call: CAPPluginCall) {
        guard let reminderId = call.getString("reminderId") else {
            call.reject("Missing reminderId parameter")
            return
        }

        // Cancel pending Daily Reminder notification(s) for this logical id.
        let reminders = getRemindersFromUserDefaults()
        let stored = reminders.first(where: { ($0["id"] as? String) == reminderId })
        cancelDailyReminderNotificationRequests(reminderId: reminderId, storedReminder: stored)

        // Delegate to storage for reminder removal
        removeReminderFromUserDefaults(id: reminderId)

        call.resolve()
    }

    @objc func getScheduledReminders(_ call: CAPPluginCall) {
        print("DNP-REMINDER: Getting scheduled reminders")

        // Get pending notifications
        notificationCenter.getPendingNotificationRequests { requests in
            let pendingIds = Set(requests.map { $0.identifier })

            // Get stored reminder data from UserDefaults
            let reminders = self.getRemindersFromUserDefaults()

            var result: [[String: Any]] = []
            for reminder in reminders {
                guard let logicalId = reminder["id"] as? String else {
                    var reminderInfo = reminder
                    reminderInfo["isScheduled"] = false
                    result.append(reminderInfo)
                    continue
                }
                let expectedId = self.dailyReminderNotificationId(reminderId: logicalId)
                let legacyId = self.legacyApiReminderNotificationId(from: reminder)
                let isScheduled = pendingIds.contains(expectedId)
                    || (legacyId != nil && pendingIds.contains(legacyId!))
                var reminderInfo = reminder
                reminderInfo["isScheduled"] = isScheduled
                result.append(reminderInfo)
            }

            DispatchQueue.main.async {
                call.resolve(["reminders": result])
            }
        }
    }

    @objc func updateDailyReminder(_ call: CAPPluginCall) {
        guard let reminderId = call.getString("reminderId") else {
            call.reject("Missing reminderId parameter")
            return
        }

        // Cancel existing reminder (before UserDefaults update)
        let remindersBeforeUpdate = getRemindersFromUserDefaults()
        let storedBeforeUpdate = remindersBeforeUpdate.first(where: { ($0["id"] as? String) == reminderId })
        cancelDailyReminderNotificationRequests(reminderId: reminderId, storedReminder: storedBeforeUpdate)

        // Update in UserDefaults
        let title = call.getString("title")
        let body = call.getString("body")
        let time = call.getString("time")
        let sound = call.getBool("sound")
        let vibration = call.getBool("vibration")
        let priority = call.getString("priority")
        let repeatDaily = call.getBool("repeatDaily")
        let timezone = call.getString("timezone")

        updateReminderInUserDefaults(
            id: reminderId,
            title: title,
            body: body,
            time: time,
            sound: sound,
            vibration: vibration,
            priority: priority,
            repeatDaily: repeatDaily,
            timezone: timezone
        )

        // Reschedule with new settings if all required fields are provided
        if let title = title, let body = body, let time = time {
            // Parse time
            let timeComponents = time.components(separatedBy: ":")
            guard timeComponents.count == 2,
                  let hour = Int(timeComponents[0]),
                  let minute = Int(timeComponents[1]) else {
                call.reject("Invalid time format")
                return
            }

            // Create new notification content
            let content = UNMutableNotificationContent()
            content.title = title
            content.body = body
            content.sound = (sound ?? true) ? .default : nil
            content.categoryIdentifier = "DAILY_REMINDER"

            // Set priority
            let finalPriority = priority ?? "normal"
            if #available(iOS 15.0, *) {
                switch finalPriority {
                case "high":
                    content.interruptionLevel = .critical
                case "low":
                    content.interruptionLevel = .passive
                default:
                    content.interruptionLevel = .active
                }
            }

            // Create date components for daily trigger
            var dateComponents = DateComponents()
            dateComponents.hour = hour
            dateComponents.minute = minute

            let trigger = UNCalendarNotificationTrigger(
                dateMatching: dateComponents,
                repeats: repeatDaily ?? true
            )

            let epochMillis = epochMillisNextDailyOccurrence(hour: hour, minute: minute)
            let requestId = dailyReminderNotificationId(reminderId: reminderId)

            let request = UNNotificationRequest(
                identifier: requestId,
                content: content,
                trigger: trigger
            )

            // Schedule the updated notification
            notificationCenter.add(request) { error in
                DispatchQueue.main.async {
                    if let error = error {
                        call.reject("Failed to reschedule updated reminder: \(error.localizedDescription)")
                    } else {
                        self.updateReminderInUserDefaults(
                            id: reminderId,
                            title: nil,
                            body: nil,
                            time: nil,
                            sound: nil,
                            vibration: nil,
                            priority: nil,
                            repeatDaily: nil,
                            timezone: nil,
                            predictiveEpochMillis: epochMillis
                        )
                        call.resolve()
                    }
                }
            }
        } else {
            call.resolve()
        }
    }

    // MARK: - Helper Methods for Reminder Storage

    private func storeReminderInUserDefaults(
        id: String,
        title: String,
        body: String,
        time: String,
        sound: Bool,
        vibration: Bool,
        priority: String,
        repeatDaily: Bool,
        timezone: String?,
        predictiveEpochMillis: Int64
    ) {
        let reminderData: [String: Any] = [
            "id": id,
            "title": title,
            "body": body,
            "time": time,
            "sound": sound,
            "vibration": vibration,
            "priority": priority,
            "repeatDaily": repeatDaily,
            "timezone": timezone ?? "",
            "predictiveEpochMillis": predictiveEpochMillis,
            "createdAt": Date().timeIntervalSince1970,
            "lastTriggered": 0
        ]

        var reminders = UserDefaults.standard.array(forKey: "daily_reminders") as? [[String: Any]] ?? []
        reminders.append(reminderData)
        UserDefaults.standard.set(reminders, forKey: "daily_reminders")

        print("DNP-REMINDER: Reminder stored: \(id)")
    }

    private func removeReminderFromUserDefaults(id: String) {
        var reminders = UserDefaults.standard.array(forKey: "daily_reminders") as? [[String: Any]] ?? []
        reminders.removeAll { ($0["id"] as? String) == id }
        UserDefaults.standard.set(reminders, forKey: "daily_reminders")

        print("DNP-REMINDER: Reminder removed: \(id)")
    }

    private func getRemindersFromUserDefaults() -> [[String: Any]] {
        return UserDefaults.standard.array(forKey: "daily_reminders") as? [[String: Any]] ?? []
    }

    private func updateReminderInUserDefaults(
        id: String,
        title: String?,
        body: String?,
        time: String?,
        sound: Bool?,
        vibration: Bool?,
        priority: String?,
        repeatDaily: Bool?,
        timezone: String?,
        predictiveEpochMillis: Int64? = nil
    ) {
        var reminders = UserDefaults.standard.array(forKey: "daily_reminders") as? [[String: Any]] ?? []
        
        for i in 0..<reminders.count {
            if reminders[i]["id"] as? String == id {
                if let title = title { reminders[i]["title"] = title }
                if let body = body { reminders[i]["body"] = body }
                if let time = time { reminders[i]["time"] = time }
                if let sound = sound { reminders[i]["sound"] = sound }
                if let vibration = vibration { reminders[i]["vibration"] = vibration }
                if let priority = priority { reminders[i]["priority"] = priority }
                if let repeatDaily = repeatDaily { reminders[i]["repeatDaily"] = repeatDaily }
                if let timezone = timezone { reminders[i]["timezone"] = timezone }
                if let predictiveEpochMillis = predictiveEpochMillis {
                    reminders[i]["predictiveEpochMillis"] = predictiveEpochMillis
                }
                break
            }
        }
        
        UserDefaults.standard.set(reminders, forKey: "daily_reminders")
        print("DNP-REMINDER: Reminder updated: \(id)")
    }
    
    // MARK: - Phase 1: Core Notification Methods
    
    /**
     * Schedule a daily notification with the specified options
     * 
     * Phase 1: Single daily schedule (one prefetch 5 min before + one notification)
     * 
     * @param call Plugin call containing notification parameters
     */
    @objc func scheduleDailyNotification(_ call: CAPPluginCall) {
        guard let scheduler = scheduler else {
            let error = DailyNotificationErrorCodes.createErrorResponse(
                code: DailyNotificationErrorCodes.PLUGIN_NOT_INITIALIZED,
                message: "Plugin not initialized"
            )
            let errorMessage = error["message"] as? String ?? "Unknown error"
            let errorCode = error["error"] as? String ?? "unknown_error"
            call.reject(errorMessage, errorCode)
            return
        }
        
        // Capacitor passes the options object directly as call data
        // Read parameters directly from call (matching Android implementation)
        guard let time = call.getString("time"), !time.isEmpty else {
            let error = DailyNotificationErrorCodes.missingParameter("time")
            let errorMessage = error["message"] as? String ?? "Unknown error"
            let errorCode = error["error"] as? String ?? "unknown_error"
            call.reject(errorMessage, errorCode)
            return
        }
        
        // Parse time (HH:mm format)
        let timeComponents = time.components(separatedBy: ":")
        guard timeComponents.count == 2,
              let hour = Int(timeComponents[0]),
              let minute = Int(timeComponents[1]),
              hour >= 0 && hour <= 23,
              minute >= 0 && minute <= 59 else {
            let error = DailyNotificationErrorCodes.invalidTimeFormat()
            let errorMessage = error["message"] as? String ?? "Unknown error"
            let errorCode = error["error"] as? String ?? "unknown_error"
            call.reject(errorMessage, errorCode)
            return
        }
        
        // Extract other parameters (with defaults matching Android)
        let title = call.getString("title") ?? "Daily Update"
        let body = call.getString("body") ?? "Your daily notification is ready"
        let sound = call.getBool("sound", true)
        let url = call.getString("url")
        let priority = call.getString("priority") ?? "default"
        let rawRollover = call.getInt("rolloverIntervalMinutes") ?? 0
        let rolloverIntervalMinutes = rawRollover > 0 ? rawRollover : nil
        
        // Calculate scheduled time (next occurrence at specified hour:minute)
        let scheduledTime = calculateNextScheduledTime(hour: hour, minute: minute)
        let fetchedAt = Int64(Date().timeIntervalSince1970 * 1000) // Current time in milliseconds
        
        // Create notification content (persist rollover interval for dev/testing; survives app restart)
        let content = NotificationContent(
            id: "daily_\(Date().timeIntervalSince1970)",
            title: title,
            body: body,
            scheduledTime: scheduledTime,
            fetchedAt: fetchedAt,
            url: url,
            payload: nil,
            etag: nil,
            rolloverIntervalMinutes: rolloverIntervalMinutes
        )
        
        // Delegate to ScheduleHelper for orchestration
        Task {
            let scheduled = await DailyNotificationScheduleHelper.scheduleDailyNotification(
                content: content,
                scheduledTime: scheduledTime,
                scheduler: scheduler,
                storage: self.storage,
                stateActor: await self.stateActor,
                scheduleBackgroundFetch: { [weak self] (scheduledTime: Int64) -> Void in
                    self?.scheduleBackgroundFetch(scheduledTime: scheduledTime)
                }
            )
            
            DispatchQueue.main.async {
                if scheduled {
                    call.resolve()
                } else {
                    let error = DailyNotificationErrorCodes.createErrorResponse(
                        code: DailyNotificationErrorCodes.SCHEDULING_FAILED,
                        message: "Failed to schedule notification"
                    )
                    let errorMessage = error["message"] as? String ?? "Unknown error"
                    let errorCode = error["error"] as? String ?? "unknown_error"
                    call.reject(errorMessage, errorCode)
                }
            }
        }
    }
    
    /**
     * Test method: Schedule an alarm to fire in a few seconds
     * Useful for verifying alarm delivery works correctly
     * 
     * @param call Plugin call with optional secondsFromNow (default: 5)
     * @returns Object with scheduled (boolean), secondsFromNow (number), and triggerAtMillis (number)
     */
    @objc func testAlarm(_ call: CAPPluginCall) {
        NSLog("DNP-DEBUG: testAlarm() method CALLED - method is being invoked!")
        print("DNP-DEBUG: testAlarm() method CALLED - method is being invoked!")
        print("DNP-DEBUG: testAlarm call data: \(call.jsObjectRepresentation)")
        
        guard let scheduler = scheduler else {
            NSLog("DNP-DEBUG: testAlarm() - scheduler is nil, rejecting")
            print("DNP-DEBUG: testAlarm() - scheduler is nil, rejecting")
            let error = DailyNotificationErrorCodes.createErrorResponse(
                code: DailyNotificationErrorCodes.PLUGIN_NOT_INITIALIZED,
                message: "Plugin not initialized"
            )
            let errorMessage = error["message"] as? String ?? "Unknown error"
            let errorCode = error["error"] as? String ?? "unknown_error"
            call.reject(errorMessage, errorCode)
            return
        }
        
        // Get secondsFromNow parameter (default: 5)
        let secondsFromNow = call.getInt("secondsFromNow") ?? 5
        
        // Ensure minimum of 1 second (iOS requirement)
        let validSeconds = max(1, secondsFromNow)
        
        Task {
            do {
                // Check permissions first
                let permissionStatus = await notificationCenter.notificationSettings()
                if permissionStatus.authorizationStatus != .authorized && permissionStatus.authorizationStatus != .provisional {
                    let error = DailyNotificationErrorCodes.createErrorResponse(
                        code: DailyNotificationErrorCodes.NOTIFICATIONS_DENIED,
                        message: "Notification permissions not granted"
                    )
                    let errorMessage = error["message"] as? String ?? "Unknown error"
                    let errorCode = error["error"] as? String ?? "unknown_error"
                    call.reject(errorMessage, errorCode)
                    return
                }
                
                // Create test notification content
                let notificationContent = UNMutableNotificationContent()
                notificationContent.title = "Test Notification"
                notificationContent.body = "This is a test notification scheduled \(validSeconds) seconds from now"
                notificationContent.sound = .default
                notificationContent.categoryIdentifier = "DAILY_NOTIFICATION"
                notificationContent.userInfo = [
                    "notification_id": "test_\(Date().timeIntervalSince1970)",
                    "is_test": true
                ]
                
                // Create time interval trigger (fires in X seconds)
                let trigger = UNTimeIntervalNotificationTrigger(timeInterval: TimeInterval(validSeconds), repeats: false)
                
                let fireDate = Date().addingTimeInterval(TimeInterval(validSeconds))
                let epochMillis = Int64(fireDate.timeIntervalSince1970 * 1000)
                let requestId = apiNotificationId(epochMillis: epochMillis)
                let request = UNNotificationRequest(
                    identifier: requestId,
                    content: notificationContent,
                    trigger: trigger
                )
                
                // Schedule notification
                try await notificationCenter.add(request)
                
                // Calculate trigger time in milliseconds
                let triggerAtMillis = Int64((Date().timeIntervalSince1970 + Double(validSeconds)) * 1000)
                
                let result: [String: Any] = [
                    "scheduled": true,
                    "secondsFromNow": validSeconds,
                    "triggerAtMillis": triggerAtMillis
                ]
                
                print("DNP-PLUGIN: Test alarm scheduled for \(validSeconds) seconds from now (triggerAtMillis=\(triggerAtMillis))")
                NSLog("DNP-DEBUG: testAlarm() - Successfully scheduled, resolving with result: \(result)")
                
                DispatchQueue.main.async {
                    NSLog("DNP-DEBUG: testAlarm() - Resolving call with result")
                    call.resolve(result)
                }
                
            } catch {
                NSLog("DNP-DEBUG: testAlarm() - Error caught: \(error)")
                print("DNP-PLUGIN: Error scheduling test alarm: \(error)")
                let errorResponse = DailyNotificationErrorCodes.createErrorResponse(
                    code: DailyNotificationErrorCodes.SCHEDULING_FAILED,
                    message: "Failed to schedule test alarm: \(error.localizedDescription)"
                )
                let errorMessage = errorResponse["message"] as? String ?? "Unknown error"
                let errorCode = errorResponse["error"] as? String ?? "unknown_error"
                NSLog("DNP-DEBUG: testAlarm() - Rejecting with error: \(errorMessage) (\(errorCode))")
                DispatchQueue.main.async {
                    call.reject(errorMessage, errorCode)
                }
            }
        }
    }
    
    /**
     * Debug method: List all available plugin methods
     * Useful for verifying Capacitor method discovery
     * 
     * @param call Plugin call
     */
    @objc func listAvailableMethods(_ call: CAPPluginCall) {
        let methods = getObjCMethods()
        let result: [String: Any] = [
            "methods": methods,
            "count": methods.count,
            "testAlarmFound": methods.contains("testAlarm:")
        ]
        NSLog("DNP-DEBUG: listAvailableMethods() - Found \(methods.count) methods")
        NSLog("DNP-DEBUG: testAlarm: found: \(methods.contains("testAlarm:"))")
        call.resolve(result)
    }
    
    /**
     * Get the last notification that was delivered
     * 
     * @param call Plugin call
     */
    @objc func getLastNotification(_ call: CAPPluginCall) {
        Task {
            let lastNotification: NotificationContent?
            
            // Delegate to stateActor if available (thread-safe), otherwise use storage directly
            if #available(iOS 13.0, *), let stateActor = await self.stateActor {
                lastNotification = await stateActor.getLastNotification()
            } else {
                lastNotification = self.storage?.getLastNotification()
            }
            
            DispatchQueue.main.async {
                if let notification = lastNotification {
                    let result: [String: Any] = [
                        "id": notification.id,
                        "title": notification.title ?? "",
                        "body": notification.body ?? "",
                        "timestamp": notification.scheduledTime,
                        "url": notification.url ?? ""
                    ]
                    call.resolve(result)
                } else {
                    call.resolve([:])
                }
            }
        }
    }
    
    /**
     * Cancel all scheduled notifications
     * 
     * @param call Plugin call
     */
    @objc func cancelAllNotifications(_ call: CAPPluginCall) {
        guard let scheduler = scheduler else {
            let error = DailyNotificationErrorCodes.createErrorResponse(
                code: DailyNotificationErrorCodes.PLUGIN_NOT_INITIALIZED,
                message: "Plugin not initialized"
            )
            let errorMessage = error["message"] as? String ?? "Unknown error"
            let errorCode = error["error"] as? String ?? "unknown_error"
            call.reject(errorMessage, errorCode)
            return
        }
        
        Task {
            // Delegate cancellation to scheduler
            await scheduler.cancelAllNotifications()
            
            // Clear storage via stateActor if available (thread-safe), otherwise use storage directly
            if #available(iOS 13.0, *), let stateActor = await self.stateActor {
                await stateActor.clearAllNotifications()
            } else {
                self.storage?.clearAllNotifications()
            }
            
            DispatchQueue.main.async {
                print("DNP-PLUGIN: All notifications cancelled successfully")
                call.resolve()
            }
        }
    }
    
    /**
     * Get the current status of notifications
     * 
     * @param call Plugin call
     */
    @objc func getNotificationStatus(_ call: CAPPluginCall) {
        guard let scheduler = scheduler else {
            let error = DailyNotificationErrorCodes.createErrorResponse(
                code: DailyNotificationErrorCodes.PLUGIN_NOT_INITIALIZED,
                message: "Plugin not initialized"
            )
            let errorMessage = error["message"] as? String ?? "Unknown error"
            let errorCode = error["error"] as? String ?? "unknown_error"
            call.reject(errorMessage, errorCode)
            return
        }
        
        Task {
            // Delegate to scheduler for permission status and pending count
            let isEnabled = await scheduler.checkPermissionStatus() == .authorized
            let pendingCount = await scheduler.getPendingNotificationCount()
            
            // Delegate to stateActor if available (thread-safe), otherwise use storage directly
            let lastNotification: NotificationContent?
            let settings: [String: Any]
            if #available(iOS 13.0, *), let stateActor = await self.stateActor {
                lastNotification = await stateActor.getLastNotification()
                settings = await stateActor.getSettings()
            } else {
                lastNotification = self.storage?.getLastNotification()
                settings = self.storage?.getSettings() ?? [:]
            }
            
            // Delegate to scheduler for next notification time
            let nextNotificationTime = await scheduler.getNextNotificationTime() ?? 0
            
            // Delegate to storage for rollover status
            let lastRolloverTime = storage?.getLastRolloverTime() ?? 0
            
            var result: [String: Any] = [
                "isEnabled": isEnabled,
                "isScheduled": pendingCount > 0,
                "lastNotificationTime": lastNotification?.scheduledTime ?? 0,
                "nextNotificationTime": nextNotificationTime,
                "pending": pendingCount,
                "rolloverEnabled": true, // Indicate rollover is active
                "lastRolloverTime": lastRolloverTime, // When last rollover occurred
                "settings": settings
            ]
            
            DispatchQueue.main.async {
                call.resolve(result)
            }
        }
    }
    
    /**
     * Handle notification delivery event (from Notification Center)
     * 
     * This is called when AppDelegate posts notification delivery event
     * Matches Android's scheduleNextNotification() behavior
     * 
     * @param notification NSNotification with userInfo containing notification_id and scheduled_time
     */
    @objc private func handleNotificationDelivery(_ notification: Notification) {
        NSLog("DNP-ROLLOVER: handleNotificationDelivery called")
        print("DNP-ROLLOVER: handleNotificationDelivery called")
        
        // Extract notification data from userInfo
        guard let userInfo = notification.userInfo,
              let notificationId = userInfo["notification_id"] as? String,
              let scheduledTime = userInfo["scheduled_time"] as? Int64 else {
            NSLog("DNP-ROLLOVER: ERROR handleNotificationDelivery missing required data userInfo=%@", notification.userInfo ?? "nil")
            print("DNP-ROLLOVER: ERROR handleNotificationDelivery missing required data userInfo=\(notification.userInfo ?? [:])")
            return
        }
        
        let scheduledTimeStr = formatTime(scheduledTime)
        NSLog("DNP-ROLLOVER: handleNotificationDelivery processing id=%@ scheduled_time=%@", notificationId, scheduledTimeStr)
        print("DNP-ROLLOVER: handleNotificationDelivery processing id=\(notificationId) scheduled_time=\(scheduledTimeStr)")
        
        // Track notify execution
        let currentTime = Int64(Date().timeIntervalSince1970 * 1000)
        storage?.saveLastNotifyExecution(timestamp: currentTime)
        
        // Delegate rollover processing (glue logic - will be moved to service in future)
        Task {
            await processRollover(notificationId: notificationId, scheduledTime: scheduledTime)
        }
    }
    
    /**
     * Handle app becoming active (foreground)
     * 
     * This is called when the app becomes active to check for missed rollovers
     * that occurred while the app was backgrounded.
     * 
     * @param notification NSNotification for app becoming active
     */
    @objc private func handleAppBecameActive(_ notification: Notification) {
        NSLog("DNP-ROLLOVER: handleAppBecameActive called")
        print("DNP-ROLLOVER: handleAppBecameActive called")
        
        // Perform lightweight rollover check when app becomes active
        // This handles cases where notifications fired while app was backgrounded
        reactivationManager?.performActiveRolloverCheck()
    }
    
    /**
     * Process rollover for delivered notification
     * 
     * @param notificationId ID of notification that was delivered
     * @param scheduledTime Scheduled time of delivered notification
     */
    private func processRollover(notificationId: String, scheduledTime: Int64) async {
        let scheduledTimeStr = formatTime(scheduledTime)
        NSLog("DNP-ROLLOVER: processRollover START id=%@ scheduled_time=%@", notificationId, scheduledTimeStr)
        print("DNP-ROLLOVER: processRollover START id=\(notificationId) scheduled_time=\(scheduledTimeStr)")
        
        guard let scheduler = scheduler, let storage = storage else {
            NSLog("DNP-ROLLOVER: ERROR processRollover missing scheduler or storage scheduler=%@ storage=%@", 
                  scheduler != nil ? "present" : "nil", storage != nil ? "present" : "nil")
            print("DNP-ROLLOVER: ERROR processRollover missing scheduler or storage scheduler=\(scheduler != nil ? "present" : "nil") storage=\(storage != nil ? "present" : "nil")")
            return
        }
        
        // Get the notification content that was delivered
        guard let content = storage.getNotificationContent(id: notificationId) else {
            NSLog("DNP-ROLLOVER: ERROR processRollover content not found in storage id=%@", notificationId)
            print("DNP-ROLLOVER: ERROR processRollover content not found in storage id=\(notificationId)")
            
            // Log available notification IDs for debugging
            let allNotifications = storage.getAllNotifications()
            let availableIds = allNotifications.map { $0.id }.joined(separator: ", ")
            NSLog("DNP-ROLLOVER: Available notification IDs in storage: [%@]", availableIds)
            print("DNP-ROLLOVER: Available notification IDs in storage: [\(availableIds)]")
            return
        }
        
        NSLog("DNP-ROLLOVER: processRollover found content id=%@ calling scheduleNextNotification", notificationId)
        print("DNP-ROLLOVER: processRollover found content id=\(notificationId) calling scheduleNextNotification")
        
        // Delegate to scheduler to schedule next notification (glue logic - will be moved to service)
        // 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: processRollover SUCCESS id=%@ next notification scheduled", notificationId)
            print("DNP-ROLLOVER: processRollover SUCCESS id=\(notificationId) next notification scheduled")
        } else {
            NSLog("DNP-ROLLOVER: processRollover FAILED id=%@ scheduleNextNotification returned false", notificationId)
            print("DNP-ROLLOVER: processRollover FAILED id=\(notificationId) scheduleNextNotification returned false")
        }
        
        // Rollover processing is non-fatal - recovery will handle on next launch if needed
    }
    
    /**
     * 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)
    }
    
    /**
     * Check permission status
     * Returns boolean flags for each permission type
     * 
     * @param call Plugin call
     */
    @objc func checkPermissionStatus(_ call: CAPPluginCall) {
        guard let scheduler = scheduler else {
            let error = DailyNotificationErrorCodes.createErrorResponse(
                code: DailyNotificationErrorCodes.PLUGIN_NOT_INITIALIZED,
                message: "Plugin not initialized"
            )
            let errorMessage = error["message"] as? String ?? "Unknown error"
            let errorCode = error["error"] as? String ?? "unknown_error"
            call.reject(errorMessage, errorCode)
            return
        }
        
        Task {
            do {
                // Delegate to scheduler for permission status check
                let notificationStatus = await scheduler.checkPermissionStatus()
                let notificationsEnabled = notificationStatus == .authorized
                
                // Format result (iOS-specific: exactAlarm and wakeLock map to notification permission)
                let result: [String: Any] = [
                    "notificationsEnabled": notificationsEnabled,
                    "exactAlarmEnabled": notificationsEnabled, // iOS equivalent
                    "wakeLockEnabled": notificationsEnabled, // iOS equivalent (Background App Refresh)
                    "allPermissionsGranted": notificationsEnabled
                ]
                
                DispatchQueue.main.async {
                    call.resolve(result)
                }
            } catch {
                DispatchQueue.main.async {
                    call.reject("Failed to check permission status: \(error.localizedDescription)", "permission_check_failed")
                }
            }
        }
    }
    
    /**
     * Request notification permissions
     * Shows system permission dialog if permissions haven't been determined yet
     * 
     * @param call Plugin call
     */
    @objc func requestNotificationPermissions(_ call: CAPPluginCall) {
        guard let scheduler = scheduler else {
            let error = DailyNotificationErrorCodes.createErrorResponse(
                code: DailyNotificationErrorCodes.PLUGIN_NOT_INITIALIZED,
                message: "Plugin not initialized"
            )
            let errorMessage = error["message"] as? String ?? "Unknown error"
            let errorCode = error["error"] as? String ?? "unknown_error"
            call.reject(errorMessage, errorCode)
            return
        }
        
        Task {
            do {
                // Delegate to scheduler for permission request
                let granted = await scheduler.requestPermissions()
                
                let result: [String: Any] = [
                    "granted": granted
                ]
                
                DispatchQueue.main.async {
                    call.resolve(result)
                }
            } catch {
                DispatchQueue.main.async {
                    call.reject("Failed to request permissions: \(error.localizedDescription)", "permission_request_failed")
                }
            }
        }
    }
    
    // MARK: - iOS-Specific Methods
    
    /**
     * Get notification permission status (iOS-specific)
     * 
     * Returns detailed permission status matching API.md specification
     * 
     * @param call Plugin call
     */
    @objc func getNotificationPermissionStatus(_ call: CAPPluginCall) {
        guard let scheduler = scheduler else {
            let error = DailyNotificationErrorCodes.createErrorResponse(
                code: DailyNotificationErrorCodes.PLUGIN_NOT_INITIALIZED,
                message: "Plugin not initialized"
            )
            let errorMessage = error["message"] as? String ?? "Unknown error"
            let errorCode = error["error"] as? String ?? "unknown_error"
            call.reject(errorMessage, errorCode)
            return
        }
        
        Task {
            do {
                // Delegate to scheduler for permission status check
                let status = await scheduler.checkPermissionStatus()
                
                // Format result with all status flags
                let result: [String: Any] = [
                    "authorized": status == .authorized,
                    "denied": status == .denied,
                    "notDetermined": status == .notDetermined,
                    "provisional": status == .provisional
                ]
                
                DispatchQueue.main.async {
                    call.resolve(result)
                }
            } catch {
                DispatchQueue.main.async {
                    call.reject("Failed to get permission status: \(error.localizedDescription)", "permission_status_failed")
                }
            }
        }
    }
    
    /**
     * Request notification permission (iOS-specific)
     * 
     * @param call Plugin call
     */
    @objc func requestNotificationPermission(_ call: CAPPluginCall) {
        guard let scheduler = scheduler else {
            let error = DailyNotificationErrorCodes.createErrorResponse(
                code: DailyNotificationErrorCodes.PLUGIN_NOT_INITIALIZED,
                message: "Plugin not initialized"
            )
            let errorMessage = error["message"] as? String ?? "Unknown error"
            let errorCode = error["error"] as? String ?? "unknown_error"
            call.reject(errorMessage, errorCode)
            return
        }
        
        Task {
            do {
                // Delegate to scheduler for permission request
                let granted = await scheduler.requestPermissions()
                
                let result: [String: Any] = [
                    "granted": granted
                ]
                
                DispatchQueue.main.async {
                    call.resolve(result)
                }
            } catch {
                DispatchQueue.main.async {
                    call.reject("Failed to request permission: \(error.localizedDescription)", "permission_request_failed")
                }
            }
        }
    }
    
    /**
     * Get pending notifications (iOS diagnostics)
     *
     * Exposes UNUserNotificationCenter pending requests with bridge-safe fields only.
     *
     * @param call Plugin call
     */
    @objc func getPendingNotifications(_ call: CAPPluginCall) {
        diagnosticLog("getPendingNotifications called")
        notificationCenter.getPendingNotificationRequests { requests in
            var notifications: [[String: Any]] = []
            for request in requests {
                let content = request.content
                let (triggerTimestamp, triggerDateIso) = Self.pendingTriggerFields(from: request.trigger)
                var item: [String: Any] = [
                    "identifier": request.identifier,
                    "title": content.title,
                    "body": content.body,
                    "triggerType": Self.pendingTriggerTypeString(from: request.trigger),
                    "repeats": request.trigger?.repeats ?? false
                ]
                if let triggerTimestamp = triggerTimestamp {
                    item["triggerTimestamp"] = triggerTimestamp
                    item["triggerDate"] = triggerTimestamp
                } else {
                    item["triggerTimestamp"] = NSNull()
                    item["triggerDate"] = NSNull()
                }
                item["triggerDateIso"] = triggerDateIso ?? NSNull()
                notifications.append(item)
            }
            self.diagnosticLog("getPendingNotifications resolved count=\(notifications.count)")
            call.resolve([
                "count": notifications.count,
                "notifications": notifications
            ])
        }
    }

    /**
     * Get delivered notification metadata (iOS diagnostics)
     *
     * Lightweight delivery snapshot; omits userInfo and attachment payloads.
     *
     * @param call Plugin call
     */
    @objc func getDeliveredNotifications(_ call: CAPPluginCall) {
        diagnosticLog("getDeliveredNotifications called")
        notificationCenter.getDeliveredNotifications { delivered in
            var notifications: [[String: Any]] = []
            for notification in delivered {
                let request = notification.request
                let content = request.content
                let deliveredTimestamp = Int64(notification.date.timeIntervalSince1970 * 1000)
                notifications.append([
                    "identifier": request.identifier,
                    "deliveredTimestamp": deliveredTimestamp,
                    "deliveredDateIso": Self.iso8601String(from: notification.date) ?? NSNull(),
                    "title": content.title,
                    "body": content.body,
                    "categoryIdentifier": content.categoryIdentifier.isEmpty ? NSNull() : content.categoryIdentifier
                ])
            }
            self.diagnosticLog("getDeliveredNotifications resolved count=\(notifications.count)")
            call.resolve([
                "count": notifications.count,
                "notifications": notifications
            ])
        }
    }

    /**
     * Get notification authorization and presentation settings (iOS diagnostics)
     *
     * Always resolves with serializable settings; does not require plugin scheduler init.
     *
     * @param call Plugin call
     */
    @objc func getNotificationSettings(_ call: CAPPluginCall) {
        diagnosticLog("getNotificationSettings called")
        notificationCenter.getNotificationSettings { settings in
            let result = Self.serializeNotificationSettings(settings)
            self.diagnosticLog("getNotificationSettings resolved authorization=\(result["authorizationStatus"] ?? "unknown")")
            call.resolve(result)
        }
    }
    
    /**
     * Get background task status (iOS-specific)
     * 
     * @param call Plugin call
     */
    @objc func getBackgroundTaskStatus(_ call: CAPPluginCall) {
        // Note: BGTaskScheduler doesn't provide a way to query registered task identifiers
        // We assume tasks are registered if setupBackgroundTasks() was called
        // Background App Refresh status cannot be checked programmatically
        // User must check in Settings app
        
        // Delegate storage access to storage service
        let lastFetchExecution: Any = storage?.getLastSuccessfulRun() ?? NSNull()
        let lastNotifyExecution: Any = storage?.getLastNotifyExecution() ?? NSNull()
        
        let result: [String: Any] = [
            "fetchTaskRegistered": true, // Assumed registered if setupBackgroundTasks() was called
            "notifyTaskRegistered": true, // Assumed registered if setupBackgroundTasks() was called
            "lastFetchExecution": lastFetchExecution,
            "lastNotifyExecution": lastNotifyExecution,
            "backgroundRefreshEnabled": NSNull() // Cannot check programmatically
        ]
        
        call.resolve(result)
    }
    
    /**
     * Open notification settings (iOS-specific)
     * 
     * @param call Plugin call
     */
    @objc func openNotificationSettings(_ call: CAPPluginCall) {
        // Delegate to UIApplication to open settings
        guard let settingsUrl = URL(string: UIApplication.openSettingsURLString) else {
            call.reject("Invalid settings URL", "open_settings_failed")
            return
        }
        
        guard UIApplication.shared.canOpenURL(settingsUrl) else {
            call.reject("Cannot open settings URL", "open_settings_failed")
            return
        }
        
        UIApplication.shared.open(settingsUrl) { success in
            DispatchQueue.main.async {
                if success {
                    call.resolve()
                } else {
                    call.reject("Failed to open notification settings", "open_settings_failed")
                }
            }
        }
    }
    
    /**
     * Open Background App Refresh settings (iOS-specific)
     * 
     * Note: iOS doesn't provide a direct URL to Background App Refresh settings.
     * This opens the app's settings page where user can find Background App Refresh.
     * 
     * @param call Plugin call
     */
    @objc func openBackgroundAppRefreshSettings(_ call: CAPPluginCall) {
        // iOS doesn't have a direct URL to Background App Refresh settings
        // Open app settings instead, where user can find Background App Refresh
        guard let settingsUrl = URL(string: UIApplication.openSettingsURLString) else {
            call.reject("Invalid settings URL", "open_settings_failed")
            return
        }
        
        guard UIApplication.shared.canOpenURL(settingsUrl) else {
            call.reject("Cannot open settings URL", "open_settings_failed")
            return
        }
        
        UIApplication.shared.open(settingsUrl) { success in
            DispatchQueue.main.async {
                if success {
                    call.resolve()
                } else {
                    call.reject("Failed to open settings", "open_settings_failed")
                }
            }
        }
    }
    
    // MARK: - Channel Methods (iOS Parity with Android)
    
    /**
     * Check if notification channel is enabled
     * 
     * iOS Note: iOS doesn't have per-channel control like Android. This method
     * checks if notifications are authorized app-wide, which is the iOS equivalent.
     * 
     * @param call Plugin call with optional channelId parameter
     */
    @objc func isChannelEnabled(_ call: CAPPluginCall) {
        guard let scheduler = scheduler else {
            let error = DailyNotificationErrorCodes.createErrorResponse(
                code: DailyNotificationErrorCodes.PLUGIN_NOT_INITIALIZED,
                message: "Plugin not initialized"
            )
            let errorMessage = error["message"] as? String ?? "Unknown error"
            let errorCode = error["error"] as? String ?? "unknown_error"
            call.reject(errorMessage, errorCode)
            return
        }
        
        // Get channelId from call (optional, for API parity with Android)
        // iOS doesn't have per-channel control, so check app-wide notification authorization
        let channelId = call.getString("channelId") ?? "default"
        Task {
            // Delegate to scheduler for permission status check
            let status = await scheduler.checkPermissionStatus()
            let enabled = (status == .authorized || status == .provisional)
            
            let result: [String: Any] = [
                "enabled": enabled,
                "channelId": channelId
            ]
            
            DispatchQueue.main.async {
                call.resolve(result)
            }
        }
    }
    
    /**
     * Open notification channel settings
     * 
     * iOS Note: iOS doesn't have per-channel settings. This method opens
     * the app's notification settings in iOS Settings, which is the iOS equivalent.
     * 
     * @param call Plugin call with optional channelId parameter
     */
    @objc func openChannelSettings(_ call: CAPPluginCall) {
        // Get channelId from call (optional, for API parity with Android)
        // iOS doesn't have per-channel settings, so open app-wide notification settings
        guard let settingsUrl = URL(string: UIApplication.openSettingsURLString) else {
            call.reject("Invalid settings URL", "open_settings_failed")
            return
        }
        
        guard UIApplication.shared.canOpenURL(settingsUrl) else {
            call.reject("Cannot open settings URL", "open_settings_failed")
            return
        }
        
        UIApplication.shared.open(settingsUrl) { success in
            DispatchQueue.main.async {
                if success {
                    call.resolve()
                } else {
                    call.reject("Failed to open settings", "open_settings_failed")
                }
            }
        }
    }
    
    /**
     * Update notification settings
     * 
     * @param call Plugin call containing new settings
     */
    @objc func updateSettings(_ call: CAPPluginCall) {
        guard let settings = call.getObject("settings") else {
            let error = DailyNotificationErrorCodes.missingParameter("settings")
            let errorMessage = error["message"] as? String ?? "Unknown error"
            let errorCode = error["error"] as? String ?? "unknown_error"
            call.reject(errorMessage, errorCode)
            return
        }
        
        Task {
            // Delegate to stateActor if available (thread-safe), otherwise use storage directly
            if #available(iOS 13.0, *), let stateActor = await self.stateActor {
                await stateActor.saveSettings(settings)
            } else {
                self.storage?.saveSettings(settings)
            }
            
            DispatchQueue.main.async {
                call.resolve()
            }
        }
    }
    
    // MARK: - Phase 3: JWT Fetcher HTTP Implementation
    
    /**
     * Fetch notification content from TimeSafari API using JWT authentication
     * 
     * Phase 3: Complete HTTP implementation for JWT-signed fetcher
     * 
     * This method:
     * - Makes authenticated HTTP request to TimeSafari API
     * - Uses JWT token in Authorization header
     * - Parses response and converts to NotificationContent
     * - Handles errors gracefully with fallback
     * 
     * @param apiBaseUrl Base URL for TimeSafari API server
     * @param activeDid Active DID for authentication
     * @param jwtToken JWT token for Authorization header
     * @return NotificationContent from API or throws error
     */
    private func fetchContentFromAPI(
        apiBaseUrl: String,
        activeDid: String,
        jwtToken: String
    ) async throws -> NotificationContent {
        // Construct API endpoint URL
        // Note: This is a minimal implementation - can be extended with full endpoint support
        let endpoint = "/api/v2/report/offers"
        guard let baseURL = URL(string: apiBaseUrl),
              let url = URL(string: endpoint, relativeTo: baseURL) else {
            throw NSError(
                domain: "DailyNotification",
                code: -1,
                userInfo: [NSLocalizedDescriptionKey: "Invalid API URL"]
            )
        }
        
        // Create HTTP request with JWT authentication
        var request = URLRequest(url: url)
        request.httpMethod = "GET"
        request.setValue("Bearer \(jwtToken)", forHTTPHeaderField: "Authorization")
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.timeoutInterval = 30.0 // 30 second timeout
        
        print("DNP-FETCH-HTTP: Making request to \(url.absoluteString)")
        
        // Execute HTTP request
        let (data, response) = try await URLSession.shared.data(for: request)
        
        // Validate HTTP response
        guard let httpResponse = response as? HTTPURLResponse else {
            throw NSError(
                domain: "DailyNotification",
                code: -2,
                userInfo: [NSLocalizedDescriptionKey: "Invalid HTTP response"]
            )
        }
        
        print("DNP-FETCH-HTTP: Response status code: \(httpResponse.statusCode)")
        
        // Check for successful response
        guard httpResponse.statusCode == 200 else {
            throw NSError(
                domain: "DailyNotification",
                code: httpResponse.statusCode,
                userInfo: [NSLocalizedDescriptionKey: "HTTP error: \(httpResponse.statusCode)"]
            )
        }
        
        // Parse JSON response
        guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
            throw NSError(
                domain: "DailyNotification",
                code: -3,
                userInfo: [NSLocalizedDescriptionKey: "Failed to parse JSON response"]
            )
        }
        
        // Convert API response to NotificationContent
        // Note: This is a minimal conversion - can be extended to handle full TimeSafari API response structure
        let currentTime = Int64(Date().timeIntervalSince1970 * 1000)
        let content = NotificationContent(
            id: "api_\(currentTime)",
            title: json["title"] as? String ?? "Daily Update",
            body: json["body"] as? String ?? "Your daily notification is ready",
            scheduledTime: currentTime + (5 * 60 * 1000), // 5 min from now
            fetchedAt: currentTime,
            url: apiBaseUrl,
            payload: json,
            etag: httpResponse.value(forHTTPHeaderField: "ETag")
        )
        
        print("DNP-FETCH-HTTP: Successfully converted API response to NotificationContent")
        return content
    }
    
    // MARK: - Phase 1: Helper Methods
    
    /**
     * Get next scheduled notification time
     * 
     * Helper method to get the next scheduled notification time for
     * scheduling background tasks. Uses async/await internally.
     * 
     * @return Next scheduled notification time in milliseconds (Int64), or nil if none
     */
    private func getNextScheduledNotificationTime() -> Int64? {
        guard let scheduler = scheduler else {
            return nil
        }
        
        // Use async helper to get next notification time
        // Note: This is called from background task handlers which are already async
        var nextTime: Int64? = nil
        let semaphore = DispatchSemaphore(value: 0)
        
        Task {
            nextTime = await scheduler.getNextNotificationTime()
            semaphore.signal()
        }
        
        // Wait with timeout (2 seconds - background tasks have limited time)
        _ = semaphore.wait(timeout: .now() + 2.0)
        return nextTime
    }
    
    /**
     * Calculate next scheduled time for given hour and minute
     * 
     * Uses scheduler's calculateNextOccurrence for consistency
     * 
     * @param hour Hour (0-23)
     * @param minute Minute (0-59)
     * @return Timestamp in milliseconds
     */
    private func calculateNextScheduledTime(hour: Int, minute: Int) -> Int64 {
        guard let scheduler = scheduler else {
            // Fallback calculation if scheduler not available
            let calendar = Calendar.current
            let now = Date()
            
            var components = calendar.dateComponents([.year, .month, .day], from: now)
            components.hour = hour
            components.minute = minute
            components.second = 0
            
            var scheduledDate = calendar.date(from: components) ?? now
            
            // If scheduled time has passed today, schedule for tomorrow
            if scheduledDate <= now {
                scheduledDate = calendar.date(byAdding: .day, value: 1, to: scheduledDate) ?? scheduledDate
            }
            
            return Int64(scheduledDate.timeIntervalSince1970 * 1000)
        }
        
        return scheduler.calculateNextOccurrence(hour: hour, minute: minute)
    }
    
    /**
     * Schedule background fetch 5 minutes before notification time
     * 
     * @param scheduledTime Notification scheduled time in milliseconds
     */
    private func scheduleBackgroundFetch(scheduledTime: Int64) {
        // Calculate fetch time (5 minutes before notification)
        let fetchTime = scheduledTime - (5 * 60 * 1000) // 5 minutes in milliseconds
        let fetchDate = Date(timeIntervalSince1970: Double(fetchTime) / 1000.0)
        
        // Schedule BGTaskScheduler task
        let request = BGAppRefreshTaskRequest(identifier: fetchTaskIdentifier)
        request.earliestBeginDate = fetchDate
        
        do {
            try backgroundTaskScheduler.submit(request)
            
            // Store earliest begin date for miss detection via state actor (thread-safe)
            Task {
                if #available(iOS 13.0, *) {
                    if let stateActor = await self.stateActor {
                        await stateActor.saveBGTaskEarliestBegin(timestamp: fetchTime)
                    } else {
                        // Fallback to direct storage access
                        self.storage?.saveBGTaskEarliestBegin(timestamp: fetchTime)
                    }
                } else {
                    // Fallback for iOS < 13
                    self.storage?.saveBGTaskEarliestBegin(timestamp: fetchTime)
                }
            }
            
            print("DNP-FETCH-SCHEDULE: Background fetch scheduled for \(fetchDate)")
        } catch {
            // BGTaskScheduler errors are common on simulator (Code=1: notPermitted)
            // This is expected behavior - simulators don't reliably support background tasks
            // On real devices, this should work if Background App Refresh is enabled
            let errorDescription = error.localizedDescription
            if errorDescription.contains("BGTaskSchedulerErrorDomain") || 
               errorDescription.contains("Code=1") ||
               (error as NSError).domain == "BGTaskSchedulerErrorDomain" {
                print("DNP-FETCH-SCHEDULE: Background fetch scheduling failed (expected on simulator): \(errorDescription)")
                print("DNP-FETCH-SCHEDULE: Note: BGTaskScheduler requires real device or Background App Refresh enabled")
            } else {
                print("DNP-FETCH-SCHEDULE: Failed to schedule background fetch: \(error)")
            }
            // Don't fail notification scheduling if background fetch fails
            // Notification will still be delivered, just without prefetch
        }
    }
}

// MARK: - Diagnostics helpers
extension DailyNotificationPlugin {
    private func diagnosticLog(_ message: String) {
        NSLog("[DailyNotificationPlugin] %@", message)
    }

    private static let iso8601Formatter: ISO8601DateFormatter = {
        let formatter = ISO8601DateFormatter()
        formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
        return formatter
    }()

    static func iso8601String(from date: Date) -> String? {
        iso8601Formatter.string(from: date)
    }

    static func pendingTriggerFields(from trigger: UNNotificationTrigger?) -> (Int64?, String?) {
        guard let trigger = trigger else { return (nil, nil) }
        var nextDate: Date?
        if let calendarTrigger = trigger as? UNCalendarNotificationTrigger {
            nextDate = calendarTrigger.nextTriggerDate()
        } else if let timeIntervalTrigger = trigger as? UNTimeIntervalNotificationTrigger {
            nextDate = timeIntervalTrigger.nextTriggerDate()
        }
        guard let date = nextDate else { return (nil, nil) }
        let timestamp = Int64(date.timeIntervalSince1970 * 1000)
        return (timestamp, iso8601String(from: date))
    }

    static func pendingTriggerTypeString(from trigger: UNNotificationTrigger?) -> String {
        guard let trigger = trigger else { return "unknown" }
        if trigger is UNCalendarNotificationTrigger { return "calendar" }
        if trigger is UNTimeIntervalNotificationTrigger { return "timeInterval" }
        if trigger is UNLocationNotificationTrigger { return "location" }
        return "unknown"
    }

    static func authorizationStatusString(_ status: UNAuthorizationStatus) -> String {
        if #available(iOS 14.0, *), status == .ephemeral {
            return "ephemeral"
        }
        switch status {
        case .notDetermined: return "notDetermined"
        case .denied: return "denied"
        case .authorized: return "authorized"
        case .provisional: return "provisional"
        @unknown default: return "unknown"
        }
    }

    static func notificationSettingString(_ setting: UNNotificationSetting) -> String {
        switch setting {
        case .notSupported: return "notSupported"
        case .disabled: return "disabled"
        case .enabled: return "enabled"
        @unknown default: return "unknown"
        }
    }

    static func showPreviewsSettingString(_ setting: UNShowPreviewsSetting) -> String {
        switch setting {
        case .always: return "always"
        case .whenAuthenticated: return "whenAuthenticated"
        case .never: return "never"
        @unknown default: return "unknown"
        }
    }

    static func serializeNotificationSettings(_ settings: UNNotificationSettings) -> [String: Any] {
        var result: [String: Any] = [
            "authorizationStatus": authorizationStatusString(settings.authorizationStatus),
            "notificationCenterSetting": notificationSettingString(settings.notificationCenterSetting),
            "lockScreenSetting": notificationSettingString(settings.lockScreenSetting),
            "carPlaySetting": notificationSettingString(settings.carPlaySetting),
            "alertSetting": notificationSettingString(settings.alertSetting),
            "badgeSetting": notificationSettingString(settings.badgeSetting),
            "soundSetting": notificationSettingString(settings.soundSetting),
            "criticalAlertSetting": notificationSettingString(settings.criticalAlertSetting),
            "showPreviewsSetting": showPreviewsSettingString(settings.showPreviewsSetting),
        ]
        if #available(iOS 13.0, *) {
            result["announcementSetting"] = notificationSettingString(settings.announcementSetting)
        }
        if #available(iOS 15.0, *) {
            result["scheduledDeliverySetting"] = notificationSettingString(settings.scheduledDeliverySetting)
            result["timeSensitiveSetting"] = notificationSettingString(settings.timeSensitiveSetting)
        }
        return result
    }
}

// MARK: - CAPBridgedPlugin Conformance
// This extension makes the plugin conform to CAPBridgedPlugin protocol
// which is required for Capacitor to discover and register the plugin
@objc extension DailyNotificationPlugin: CAPBridgedPlugin {
    @objc public var identifier: String {
        return "org.timesafari.dailynotification"
    }
    
    @objc public var jsName: String {
        return "DailyNotification"
    }
    
    @objc public var pluginMethods: [CAPPluginMethod] {
        var methods: [CAPPluginMethod] = []
        
        // Core methods
        methods.append(CAPPluginMethod(name: "configure", returnType: CAPPluginReturnPromise))
        methods.append(CAPPluginMethod(name: "configureNativeFetcher", returnType: CAPPluginReturnPromise))
        methods.append(CAPPluginMethod(name: "scheduleDailyNotification", returnType: CAPPluginReturnPromise))
        methods.append(CAPPluginMethod(name: "testAlarm", returnType: CAPPluginReturnPromise))
        methods.append(CAPPluginMethod(name: "getLastNotification", returnType: CAPPluginReturnPromise))
        methods.append(CAPPluginMethod(name: "cancelAllNotifications", returnType: CAPPluginReturnPromise))
        methods.append(CAPPluginMethod(name: "getNotificationStatus", returnType: CAPPluginReturnPromise))
        methods.append(CAPPluginMethod(name: "updateSettings", returnType: CAPPluginReturnPromise))
        
        // Permission methods
        methods.append(CAPPluginMethod(name: "checkPermissionStatus", returnType: CAPPluginReturnPromise))
        methods.append(CAPPluginMethod(name: "requestNotificationPermissions", returnType: CAPPluginReturnPromise))
        
        // iOS-specific methods
        methods.append(CAPPluginMethod(name: "getNotificationPermissionStatus", returnType: CAPPluginReturnPromise))
        methods.append(CAPPluginMethod(name: "requestNotificationPermission", returnType: CAPPluginReturnPromise))
        methods.append(CAPPluginMethod(name: "getPendingNotifications", returnType: CAPPluginReturnPromise))
        methods.append(CAPPluginMethod(name: "getDeliveredNotifications", returnType: CAPPluginReturnPromise))
        methods.append(CAPPluginMethod(name: "getNotificationSettings", returnType: CAPPluginReturnPromise))
        methods.append(CAPPluginMethod(name: "getBackgroundTaskStatus", returnType: CAPPluginReturnPromise))
        methods.append(CAPPluginMethod(name: "openNotificationSettings", returnType: CAPPluginReturnPromise))
        methods.append(CAPPluginMethod(name: "openBackgroundAppRefreshSettings", returnType: CAPPluginReturnPromise))
        
        // Channel methods (iOS parity with Android)
        methods.append(CAPPluginMethod(name: "isChannelEnabled", returnType: CAPPluginReturnPromise))
        methods.append(CAPPluginMethod(name: "openChannelSettings", returnType: CAPPluginReturnPromise))
        
        // Reminder methods
        methods.append(CAPPluginMethod(name: "scheduleDailyReminder", returnType: CAPPluginReturnPromise))
        methods.append(CAPPluginMethod(name: "cancelDailyReminder", returnType: CAPPluginReturnPromise))
        methods.append(CAPPluginMethod(name: "getScheduledReminders", returnType: CAPPluginReturnPromise))
        methods.append(CAPPluginMethod(name: "updateDailyReminder", returnType: CAPPluginReturnPromise))
        
        // API batch notifications (replace-all UNUserNotificationCenter)
        methods.append(CAPPluginMethod(name: "clearApiNotifications", returnType: CAPPluginReturnPromise))
        methods.append(CAPPluginMethod(name: "scheduleApiNotifications", returnType: CAPPluginReturnPromise))
        
        // Dual scheduling methods
        methods.append(CAPPluginMethod(name: "scheduleContentFetch", returnType: CAPPluginReturnPromise))
        methods.append(CAPPluginMethod(name: "scheduleUserNotification", returnType: CAPPluginReturnPromise))
        methods.append(CAPPluginMethod(name: "scheduleDualNotification", returnType: CAPPluginReturnPromise))
        methods.append(CAPPluginMethod(name: "getDualScheduleStatus", returnType: CAPPluginReturnPromise))
        methods.append(CAPPluginMethod(name: "updateDualScheduleConfig", returnType: CAPPluginReturnPromise))
        methods.append(CAPPluginMethod(name: "cancelDualSchedule", returnType: CAPPluginReturnPromise))
        methods.append(CAPPluginMethod(name: "updateStarredPlans", returnType: CAPPluginReturnPromise))
        methods.append(CAPPluginMethod(name: "getStarredPlans", returnType: CAPPluginReturnPromise))
        
        return methods
    }
}

extension DailyNotificationPlugin {
    /// Register the host app’s `NativeNotificationContentFetcher` (call before `configureNativeFetcher`, typically from `AppDelegate`).
    public static func registerNativeFetcher(_ fetcher: NativeNotificationContentFetcher?) {
        NativeNotificationFetcherRegistry.shared.set(fetcher)
    }
}