package org.timesafari.dailynotification

import android.app.AlarmManager
import android.app.AlarmManager.AlarmClockInfo
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
import android.util.Log
import androidx.core.app.NotificationCompat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

/**
 * AlarmManager implementation for user notifications
 * Implements TTL-at-fire logic and notification delivery
 * 
 * @author Matthew Raymer
 * @version 2.0.0
 */
/**
 * Source of schedule request - tracks which code path triggered scheduling
 * Used for debugging duplicate alarm issues
 */
enum class ScheduleSource {
    INITIAL_SETUP,        // User schedules initial daily notification
    ROLLOVER_ON_FIRE,     // Notification fired, scheduling next day
    APP_LAUNCH_RECOVERY,  // App launched, recovering from DB
    BOOT_RECOVERY,        // Device booted, recovering from DB
    APP_RESUME_INIT,      // App resumed, initialization/ensure-schedule path
    MANUAL_RESCHEDULE,    // Manual reschedule (e.g., time change)
    TEST_NOTIFICATION     // Test notification scheduling
}

class NotifyReceiver : BroadcastReceiver() {
    
    companion object {
        private const val TAG = "DNP-NOTIFY"
        private const val SCHEDULE_TAG = "DNP-SCHEDULE"
        private const val CHANNEL_ID = "daily_notifications"
        private const val NOTIFICATION_ID = 1001
        
        /**
         * Generate stable request code from scheduleId
         * Uses scheduleId hash to ensure same schedule always gets same requestCode
         * This prevents duplicate alarms when same schedule is scheduled multiple times
         * 
         * @param scheduleId Stable identifier for the schedule (e.g., "daily_reminder_1")
         * @return Request code for PendingIntent (uses lower 16 bits of hash)
         */
        private fun getRequestCode(scheduleId: String): Int {
            // Use scheduleId hash for stability - same schedule = same requestCode
            // This ensures FLAG_UPDATE_CURRENT works correctly to replace existing alarms
            return (scheduleId.hashCode() and 0xFFFF).toInt()
        }
        
        /**
         * Legacy: Generate request code from trigger time (for backward compatibility)
         * @deprecated Use getRequestCode(scheduleId) instead for stable request codes
         */
        @Deprecated("Use getRequestCode(scheduleId) for stable request codes")
        private fun getRequestCodeFromTime(triggerAtMillis: Long): Int {
            return (triggerAtMillis and 0xFFFF).toInt()
        }
        
        /**
         * Get launch intent for the host app
         * Uses package launcher intent to avoid hardcoding MainActivity class name
         * This works across all host apps regardless of their MainActivity package/class
         * 
         * @param context Application context
         * @return Intent to launch the app, or null if not available
         */
        private fun getLaunchIntent(context: Context): Intent? {
            return try {
                // Use package launcher intent - works for any host app
                context.packageManager.getLaunchIntentForPackage(context.packageName)?.apply {
                    flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
                }
            } catch (e: Exception) {
                Log.w(TAG, "Failed to get launch intent for package: ${context.packageName}", e)
                null
            }
        }
        
        /**
         * Check if exact alarm permission is granted
         * On Android 12+ (API 31+), SCHEDULE_EXACT_ALARM must be granted at runtime
         * 
         * @param context Application context
         * @return true if exact alarms can be scheduled, false otherwise
         */
        private fun canScheduleExactAlarms(context: Context): Boolean {
            return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
                val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as? AlarmManager
                alarmManager?.canScheduleExactAlarms() ?: false
            } else {
                // Pre-Android 12: exact alarms are always allowed
                true
            }
        }
        
        /**
         * Schedule an exact notification using AlarmManager
         * Uses setAlarmClock() for Android 5.0+ for better reliability
         * Falls back to setExactAndAllowWhileIdle for older versions
         * 
         * FIX: Uses DailyNotificationReceiver (registered in manifest) instead of NotifyReceiver
         * Stores notification content in database and passes notification ID to receiver
         * 
         * Includes idempotence check to prevent duplicate alarms for same schedule
         * 
         * @param context Application context
         * @param triggerAtMillis When to trigger the notification (UTC milliseconds)
         * @param config Notification configuration
         * @param isStaticReminder Whether this is a static reminder (no content dependency)
         * @param reminderId Optional reminder ID for tracking (used as scheduleId if provided)
         * @param scheduleId Stable identifier for the schedule (used for requestCode stability)
         * @param source Source of the scheduling request (for debugging duplicate alarms)
         * @param skipPendingIntentIdempotence If true, skip PendingIntent-based idempotence checks.
         *   Use when the caller has just cancelled this scheduleId (cancel-then-schedule path).
         *   Android may still return the cancelled PendingIntent from cache briefly, which would
         *   incorrectly cause the new schedule to be skipped.
         */
        @JvmStatic
        fun scheduleExactNotification(
            context: Context,
            triggerAtMillis: Long,
            config: UserNotificationConfig,
            isStaticReminder: Boolean = false,
            reminderId: String? = null,
            scheduleId: String? = null,
            source: ScheduleSource = ScheduleSource.MANUAL_RESCHEDULE,
            skipPendingIntentIdempotence: Boolean = false
        ) {
            val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager

            // Generate stable scheduleId - prefer provided scheduleId, then reminderId, then generate from time
            // This ensures same schedule always uses same ID for idempotence checks
            val stableScheduleId = scheduleId ?: reminderId ?: "daily_${triggerAtMillis}"

            // Generate notification ID (use reminderId if provided, otherwise generate from trigger time)
            val notificationId = reminderId ?: "notify_${triggerAtMillis}"

            val requestCode = getRequestCode(stableScheduleId)
            val checkIntent = Intent(context, org.timesafari.dailynotification.DailyNotificationReceiver::class.java).apply {
                setPackage(context.packageName)
                action = "org.timesafari.daily.NOTIFICATION"
            }

            // IDEMPOTENCE CHECK: Verify no existing alarm for this trigger time before scheduling.
            // Skip PendingIntent checks when caller just cancelled this schedule (Android may still
            // return the cancelled PendingIntent from cache and cause the new schedule to be skipped).
            if (!skipPendingIntentIdempotence) {
                // Check 1: Same scheduleId (stable requestCode) - most reliable
                var existingPendingIntent = PendingIntent.getBroadcast(
                    context,
                    requestCode,
                    checkIntent,
                    PendingIntent.FLAG_NO_CREATE or PendingIntent.FLAG_IMMUTABLE
                )

                // Check 2: If no match by scheduleId, check by trigger time (within 1 minute tolerance)
                if (existingPendingIntent == null) {
                    val timeBasedRequestCode = getRequestCodeFromTime(triggerAtMillis)
                    existingPendingIntent = PendingIntent.getBroadcast(
                        context,
                        timeBasedRequestCode,
                        checkIntent,
                        PendingIntent.FLAG_NO_CREATE or PendingIntent.FLAG_IMMUTABLE
                    )
                }

                // Check 3: AlarmManager next alarm (Android 5.0+)
                if (existingPendingIntent == null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                    val nextAlarm = alarmManager.nextAlarmClock
                    if (nextAlarm != null) {
                        val nextAlarmTime = nextAlarm.triggerTime
                        val timeDiff = Math.abs(nextAlarmTime - triggerAtMillis)
                        if (timeDiff < 60000) {
                            val triggerTimeStr = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.US)
                                .format(java.util.Date(triggerAtMillis))
                            Log.w(SCHEDULE_TAG, "Skipping duplicate schedule: id=$stableScheduleId, nextRun=$triggerTimeStr, source=$source")
                            Log.w(SCHEDULE_TAG, "Existing alarm found in AlarmManager at $nextAlarmTime (diff=${timeDiff}ms) - alarm already scheduled")
                            return
                        }
                    }
                }

                if (existingPendingIntent != null) {
                    if (source == ScheduleSource.ROLLOVER_ON_FIRE) {
                        // Rollover chain: same schedule id, new trigger time — treat as update: cancel then set
                        Log.d(SCHEDULE_TAG, "ROLLOVER_ON_FIRE: cancelling existing alarm for id=$stableScheduleId to set new trigger")
                        alarmManager.cancel(existingPendingIntent)
                    } else {
                        val triggerTimeStr = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.US)
                            .format(java.util.Date(triggerAtMillis))
                        Log.w(SCHEDULE_TAG, "Skipping duplicate schedule: id=$stableScheduleId, nextRun=$triggerTimeStr, source=$source")
                        Log.w(SCHEDULE_TAG, "Existing PendingIntent found for requestCode=$requestCode - alarm already scheduled")
                        return
                    }
                }
            } else {
                Log.d(SCHEDULE_TAG, "Skipping PendingIntent idempotence (caller just cancelled scheduleId=$stableScheduleId)")
            }

            // DB-LEVEL IDEMPOTENCE CHECK: Verify no existing schedule for this scheduleId and nextRun
            // When skipPendingIntentIdempotence is true (e.g. "re-set" flow), skip this check so we don't
            // cancel the alarm and then skip re-scheduling, resulting in no alarm.
            if (!skipPendingIntentIdempotence) {
                try {
                    runBlocking {
                        val db = DailyNotificationDatabase.getDatabase(context)
                        val existingSchedule = db.scheduleDao().getById(stableScheduleId)
                        
                        if (existingSchedule != null && existingSchedule.nextRunAt != null) {
                            val timeDiff = Math.abs(existingSchedule.nextRunAt - triggerAtMillis)
                            // If we already have a schedule for this ID with the same nextRun (within 1 minute), skip
                            if (timeDiff < 60000) {
                                val triggerTimeStr = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.US)
                                    .format(java.util.Date(triggerAtMillis))
                                Log.w(SCHEDULE_TAG, "Skipping duplicate schedule for id=$stableScheduleId at $triggerTimeStr from source=$source")
                                Log.w(SCHEDULE_TAG, "Existing schedule found in DB: nextRunAt=${existingSchedule.nextRunAt}, diff=${timeDiff}ms")
                                return@runBlocking
                            }
                        }
                    }
                } catch (e: Exception) {
                    Log.w(SCHEDULE_TAG, "DB idempotence check failed, continuing with schedule: $stableScheduleId", e)
                }
            } else {
                Log.d(SCHEDULE_TAG, "Skipping DB idempotence (skipPendingIntentIdempotence=true) for scheduleId=$stableScheduleId")
            }
            
            val triggerTimeStr = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.US)
                .format(java.util.Date(triggerAtMillis))
            Log.i(SCHEDULE_TAG, "Scheduling next daily alarm: id=$stableScheduleId, nextRun=$triggerTimeStr, source=$source")
            
            // Store notification content in database before scheduling alarm
            // Phase 1: Always create NotificationContentEntity for recovery tracking
            // This allows recovery to detect missed notifications even for static reminders
            // Use runBlocking to call suspend function from non-suspend context
            // This is acceptable here because we're not in a UI thread and need to ensure
            // content is stored before scheduling the alarm
            try {
                runBlocking {
                    val db = DailyNotificationDatabase.getDatabase(context)
                    val contentCache = db.contentCacheDao().getLatestByScope(ContentCacheScope.DAILY)
                        ?: db.contentCacheDao().getLatestByScope(ContentCacheScope.LEGACY)
                        ?: db.contentCacheDao().getLatest()
                    
                    // Always create a notification content entity for recovery tracking
                    // Phase 1: Recovery needs NotificationContentEntity to detect missed notifications
                    val roomStorage = org.timesafari.dailynotification.storage.DailyNotificationStorageRoom(context)
                    val entity = org.timesafari.dailynotification.entities.NotificationContentEntity(
                        notificationId,
                        "2.1.0", // Plugin version
                        null, // timesafariDid - can be set if available
                        "daily",
                        config.title,
                        config.body ?: (if (contentCache != null) String(contentCache.payload) else ""),
                        triggerAtMillis,
                        java.time.ZoneId.systemDefault().id
                    )
                    entity.priority = when (config.priority) {
                        "high", "max" -> 2
                        "low", "min" -> -1
                        else -> 0
                    }
                    entity.vibrationEnabled = config.vibration ?: true
                    entity.soundEnabled = config.sound ?: true
                    entity.deliveryStatus = "pending"
                    entity.createdAt = System.currentTimeMillis()
                    entity.updatedAt = System.currentTimeMillis()
                    entity.ttlSeconds = contentCache?.ttlSeconds?.toLong() ?: (7 * 24 * 60 * 60).toLong() // Default 7 days if no cache
                    
                    // saveNotificationContent returns CompletableFuture, so we need to wait for it
                    roomStorage.saveNotificationContent(entity).get()
                    Log.d(TAG, "Stored notification content in database: id=$notificationId (for recovery tracking)")
                }
            } catch (e: Exception) {
                Log.w(TAG, "Failed to store notification content in database, continuing with alarm scheduling", e)
            }
            
            // FIX: Use DailyNotificationReceiver (registered in manifest) instead of NotifyReceiver
            // FIX: Set action to match manifest registration; setPackage() ensures AlarmManager
            // delivery reaches this app on all OEMs (see daily-notification-plugin-android-receiver-issue)
            val intent = Intent(context, org.timesafari.dailynotification.DailyNotificationReceiver::class.java).apply {
                setPackage(context.packageName)
                action = "org.timesafari.daily.NOTIFICATION" // Must match manifest intent-filter action
                putExtra("notification_id", notificationId) // DailyNotificationReceiver expects this extra
                putExtra("schedule_id", stableScheduleId) // Add stable scheduleId for tracking
                // Also preserve original extras for backward compatibility if needed
                putExtra("title", config.title)
                putExtra("body", config.body)
                putExtra("sound", config.sound ?: true)
                putExtra("vibration", config.vibration ?: true)
                putExtra("priority", config.priority ?: "normal")
                putExtra("is_static_reminder", isStaticReminder)
                putExtra("trigger_time", triggerAtMillis) // Store trigger time for debugging
                if (reminderId != null) {
                    putExtra("reminder_id", reminderId)
                }
            }
            
            // requestCode already computed above for idempotence check
            val pendingIntent = PendingIntent.getBroadcast(
                context,
                requestCode,
                intent,
                PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
            )
            
            // CRITICAL: Cancel any existing alarm for this requestCode BEFORE scheduling
            // This ensures we don't create duplicate alarms if this function is called multiple times
            // The idempotence check above should prevent this, but this is a safety net
            try {
                val existingPendingIntent = PendingIntent.getBroadcast(
                    context,
                    requestCode,
                    intent,
                    PendingIntent.FLAG_NO_CREATE or PendingIntent.FLAG_IMMUTABLE
                )
                if (existingPendingIntent != null) {
                    Log.w(SCHEDULE_TAG, "Cancelling existing alarm before rescheduling: requestCode=$requestCode, scheduleId=$stableScheduleId, source=$source")
                    alarmManager.cancel(existingPendingIntent)
                    // Do not call existingPendingIntent.cancel(): the cached PendingIntent may be the same
                    // object we pass to setAlarmClock below; cancelling it can prevent the new alarm from firing.
                }
            } catch (e: Exception) {
                Log.w(SCHEDULE_TAG, "Failed to cancel existing alarm before scheduling: $stableScheduleId", e)
            }
            
            val currentTime = System.currentTimeMillis()
            val delayMs = triggerAtMillis - currentTime
            
            Log.i(TAG, "Scheduling alarm: triggerTime=$triggerTimeStr, delayMs=$delayMs, requestCode=$requestCode, scheduleId=$stableScheduleId")
            
            // Check exact alarm permission before scheduling (Android 12+)
            val canScheduleExact = canScheduleExactAlarms(context)
            if (!canScheduleExact && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
                Log.w(TAG, "Exact alarm permission not granted. Cannot schedule exact alarm. User must grant SCHEDULE_EXACT_ALARM permission in settings.")
                // Fall back to inexact alarm
                alarmManager.set(
                    AlarmManager.RTC_WAKEUP,
                    triggerAtMillis,
                    pendingIntent
                )
                Log.i(TAG, "Inexact alarm scheduled (exact permission denied): triggerAt=$triggerAtMillis, requestCode=$requestCode")
                return
            }
            
            try {
                // ONE-ALARM POLICY: Use only setAlarmClock() for Android 5.0+ (API 21+)
                // This is the most reliable method and shows alarm icon in status bar
                // Do NOT also call setExactAndAllowWhileIdle or setExact for the same event
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                    // Create show intent for alarm clock (opens app when alarm fires)
                    // Use package launcher intent to avoid hardcoding MainActivity class name
                    val showIntent = getLaunchIntent(context)
                    
                    val showPendingIntent = if (showIntent != null) {
                        PendingIntent.getActivity(
                            context,
                            requestCode + 1, // Different request code for show intent
                            showIntent,
                            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
                        )
                    } else {
                        null
                    }
                    
                    val alarmClockInfo = AlarmClockInfo(triggerAtMillis, showPendingIntent)
                    
                    // Deep logging to identify this specific AlarmManager call
                    Log.i(SCHEDULE_TAG, "Scheduling OS alarm: variant=ALARM_CLOCK, action=${intent.action}, triggerTime=$triggerAtMillis, requestCode=$requestCode, scheduleId=$stableScheduleId, source=$source, pendingIntentHash=${pendingIntent.hashCode()}, showIntentHash=${showPendingIntent?.hashCode() ?: 0}")
                    
                    alarmManager.setAlarmClock(alarmClockInfo, pendingIntent)
                    
                    Log.i(TAG, "Alarm clock scheduled (setAlarmClock): triggerAt=$triggerAtMillis, requestCode=$requestCode")
                } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                    // Fallback to setExactAndAllowWhileIdle for Android 6.0-4.4 (pre-LOLLIPOP)
                    // Deep logging to identify this specific AlarmManager call
                    Log.i(SCHEDULE_TAG, "Scheduling OS alarm: variant=EXACT_ALLOW_WHILE_IDLE, action=${intent.action}, triggerTime=$triggerAtMillis, requestCode=$requestCode, scheduleId=$stableScheduleId, source=$source, pendingIntentHash=${pendingIntent.hashCode()}")
                    
                    alarmManager.setExactAndAllowWhileIdle(
                        AlarmManager.RTC_WAKEUP,
                        triggerAtMillis,
                        pendingIntent
                    )
                    
                    Log.i(TAG, "Exact alarm scheduled (setExactAndAllowWhileIdle): triggerAt=$triggerAtMillis, requestCode=$requestCode")
                } else {
                    // Fallback to setExact for older versions (pre-M)
                    // Deep logging to identify this specific AlarmManager call
                    Log.i(SCHEDULE_TAG, "Scheduling OS alarm: variant=EXACT, action=${intent.action}, triggerTime=$triggerAtMillis, requestCode=$requestCode, scheduleId=$stableScheduleId, source=$source, pendingIntentHash=${pendingIntent.hashCode()}")
                    
                    alarmManager.setExact(
                        AlarmManager.RTC_WAKEUP,
                        triggerAtMillis,
                        pendingIntent
                    )
                    
                    Log.i(TAG, "Exact alarm scheduled (setExact): triggerAt=$triggerAtMillis, requestCode=$requestCode")
                }
            } catch (e: SecurityException) {
                Log.w(TAG, "Cannot schedule exact alarm, falling back to inexact", e)
                alarmManager.set(
                    AlarmManager.RTC_WAKEUP,
                    triggerAtMillis,
                    pendingIntent
                )
                Log.i(TAG, "Inexact alarm scheduled (fallback): triggerAt=$triggerAtMillis, requestCode=$requestCode")
            }
            
            // Update database schedule with new nextRunAt so getNotificationStatus() returns correct value
            // This is critical for rollover scenarios where the UI needs to show the updated time
            // Strategy: Find existing enabled notify schedule and update it (there should only be one)
            // This ensures getNotificationStatus() finds the updated schedule, not a stale one
            try {
                runBlocking {
                    val db = DailyNotificationDatabase.getDatabase(context)
                    
                    // First, try to find schedule by the provided stableScheduleId
                    var scheduleToUpdate = db.scheduleDao().getById(stableScheduleId)
                    
                    // If not found by ID, only use "first enabled notify" fallback when this is NOT
                    // a rollover id (daily_rollover_*). Rollover work may use a different notification_id
                    // (e.g. from recovery); updating the app's schedule row here would overwrite
                    // nextRunAt with the rollover time and can leave the app's alarm in a bad state.
                    if (scheduleToUpdate == null && !stableScheduleId.startsWith("daily_rollover_")) {
                        val allSchedules = db.scheduleDao().getAll()
                        // Includes "static_reminder" so this fallback can find the real static
                        // reminder row (now isolated from "notify") instead of falling through to
                        // the "create new" branch below and spawning a duplicate row.
                        scheduleToUpdate = allSchedules.firstOrNull {
                            (it.kind == "notify" || it.kind == "static_reminder") && it.enabled
                        }
                    }
                    
                    // Calculate cron expression from trigger time (HH:mm format)
                    val calendar = java.util.Calendar.getInstance().apply {
                        timeInMillis = triggerAtMillis
                    }
                    val hour = calendar.get(java.util.Calendar.HOUR_OF_DAY)
                    val minute = calendar.get(java.util.Calendar.MINUTE)
                    val cronExpression = "${minute} ${hour} * * *"
                    val clockTime = String.format("%02d:%02d", hour, minute)
                    
                    if (scheduleToUpdate != null) {
                        // Update existing schedule with new nextRunAt
                        // Use the existing schedule's ID (not stableScheduleId) to ensure we update the right one
                        db.scheduleDao().updateRunTimes(scheduleToUpdate.id, scheduleToUpdate.lastRunAt, triggerAtMillis)
                        Log.d(SCHEDULE_TAG, "Updated schedule in database: id=${scheduleToUpdate.id}, nextRunAt=$triggerAtMillis (rollover)")
                    } else {
                        // No existing schedule found - create new one (shouldn't happen in normal flow)
                        val newSchedule = Schedule(
                            id = stableScheduleId,
                            kind = "notify",
                            cron = cronExpression,
                            clockTime = clockTime,
                            enabled = true,
                            lastRunAt = null,
                            nextRunAt = triggerAtMillis,
                            jitterMs = 0,
                            backoffPolicy = "exp",
                            stateJson = null
                        )
                        db.scheduleDao().upsert(newSchedule)
                        Log.d(SCHEDULE_TAG, "Created new schedule in database: id=$stableScheduleId, nextRunAt=$triggerAtMillis")
                    }
                }
            } catch (e: Exception) {
                // Log but don't fail - alarm is already scheduled, DB update is best-effort
                Log.w(SCHEDULE_TAG, "Failed to update schedule in database: $stableScheduleId (alarm still scheduled)", e)
            }
        }
        
        /**
         * Cancel a scheduled notification alarm
         * FIX: Uses DailyNotificationReceiver to match alarm scheduling
         * @param context Application context
         * @param scheduleId The schedule ID of the alarm to cancel (preferred - uses stable request code)
         * @param triggerAtMillis The trigger time of the alarm to cancel (fallback - for backward compatibility)
         */
        fun cancelNotification(context: Context, scheduleId: String? = null, triggerAtMillis: Long? = null) {
            val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
            // FIX: Use DailyNotificationReceiver to match what was scheduled
            val intent = Intent(context, org.timesafari.dailynotification.DailyNotificationReceiver::class.java).apply {
                setPackage(context.packageName)
                action = "org.timesafari.daily.NOTIFICATION"
            }
            val requestCode = when {
                scheduleId != null -> getRequestCode(scheduleId)
                triggerAtMillis != null -> getRequestCodeFromTime(triggerAtMillis)
                else -> {
                    Log.e(TAG, "cancelNotification: Must provide either scheduleId or triggerAtMillis")
                    return
                }
            }
            
            // CRITICAL: Use FLAG_NO_CREATE to get existing PendingIntent, don't create new one
            // This matches the pattern used in scheduleExactNotification for proper cancellation
            val existingPendingIntent = PendingIntent.getBroadcast(
                context,
                requestCode,
                intent,
                PendingIntent.FLAG_NO_CREATE or PendingIntent.FLAG_IMMUTABLE
            )
            
            if (existingPendingIntent != null) {
                // Cancel both the alarm in AlarmManager AND the PendingIntent itself
                // This matches the pattern in scheduleExactNotification (lines 311-312)
                alarmManager.cancel(existingPendingIntent)
                existingPendingIntent.cancel()
                Log.i(TAG, "DNP-CANCEL: Notification alarm cancelled: scheduleId=$scheduleId, triggerAt=$triggerAtMillis, requestCode=$requestCode")
                
                // Verify cancellation by checking if alarm still exists
                val verifyIntent = PendingIntent.getBroadcast(
                    context,
                    requestCode,
                    intent,
                    PendingIntent.FLAG_NO_CREATE or PendingIntent.FLAG_IMMUTABLE
                )
                if (verifyIntent == null) {
                    Log.d(TAG, "DNP-CANCEL: ✅ Cancellation verified - no PendingIntent found for requestCode=$requestCode")
                } else {
                    Log.w(TAG, "DNP-CANCEL: ⚠️ Cancellation may have failed - PendingIntent still exists for requestCode=$requestCode")
                }
            } else {
                Log.d(TAG, "DNP-CANCEL: No existing PendingIntent found to cancel: scheduleId=$scheduleId, requestCode=$requestCode")
            }
        }
        
        /**
         * Check if an alarm is scheduled for the given schedule
         * FIX: Uses DailyNotificationReceiver to match alarm scheduling
         * @param context Application context
         * @param scheduleId The schedule ID to check (preferred - uses stable request code)
         * @param triggerAtMillis The trigger time to check (fallback - for backward compatibility)
         * @return true if alarm is scheduled, false otherwise
         */
        fun isAlarmScheduled(context: Context, scheduleId: String? = null, triggerAtMillis: Long? = null): Boolean {
            // FIX: Use DailyNotificationReceiver to match what was scheduled
            val intent = Intent(context, org.timesafari.dailynotification.DailyNotificationReceiver::class.java).apply {
                setPackage(context.packageName)
                action = "org.timesafari.daily.NOTIFICATION"
            }
            val requestCode = when {
                scheduleId != null -> getRequestCode(scheduleId)
                triggerAtMillis != null -> getRequestCodeFromTime(triggerAtMillis)
                else -> {
                    Log.e(TAG, "isAlarmScheduled: Must provide either scheduleId or triggerAtMillis")
                    return false
                }
            }
            val pendingIntent = PendingIntent.getBroadcast(
                context,
                requestCode,
                intent,
                PendingIntent.FLAG_NO_CREATE or PendingIntent.FLAG_IMMUTABLE
            )
            val isScheduled = pendingIntent != null
            
            val triggerTimeStr = when {
                triggerAtMillis != null -> java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.US)
                    .format(java.util.Date(triggerAtMillis))
                else -> "scheduleId=$scheduleId"
            }
            Log.d(TAG, "Alarm check for $triggerTimeStr: scheduled=$isScheduled, requestCode=$requestCode")
            
            return isScheduled
        }
        
        /**
         * Get the next scheduled alarm time from AlarmManager
         * @param context Application context
         * @return Next alarm time in milliseconds, or null if no alarm is scheduled
         */
        fun getNextAlarmTime(context: Context): Long? {
            return try {
                val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                    val nextAlarm = alarmManager.nextAlarmClock
                    if (nextAlarm != null) {
                        val triggerTime = nextAlarm.triggerTime
                        val triggerTimeStr = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.US)
                            .format(java.util.Date(triggerTime))
                        Log.d(TAG, "Next alarm clock: $triggerTimeStr")
                        triggerTime
                    } else {
                        Log.d(TAG, "No alarm clock scheduled")
                        null
                    }
                } else {
                    Log.d(TAG, "getNextAlarmTime() requires Android 5.0+")
                    null
                }
            } catch (e: Exception) {
                Log.e(TAG, "Error getting next alarm time", e)
                null
            }
        }
        
        /**
         * Test method: Schedule an alarm to fire in a few seconds
         * Useful for verifying alarm delivery works correctly
         * @param context Application context
         * @param secondsFromNow How many seconds from now to fire (default: 5)
         */
        fun testAlarm(context: Context, secondsFromNow: Int = 5) {
            val triggerTime = System.currentTimeMillis() + (secondsFromNow * 1000L)
            val config = UserNotificationConfig(
                enabled = true,
                schedule = "",
                title = "Test Notification",
                body = "This is a test notification scheduled $secondsFromNow seconds from now",
                sound = true,
                vibration = true,
                priority = "high"
            )
            
            val triggerTimeStr = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.US)
                .format(java.util.Date(triggerTime))
            Log.i(TAG, "TEST: Scheduling test alarm for $triggerTimeStr (in $secondsFromNow seconds)")
            
            scheduleExactNotification(
                context,
                triggerTime,
                config,
                isStaticReminder = true,
                reminderId = "test_${System.currentTimeMillis()}"
            )
            
            Log.i(TAG, "TEST: Alarm scheduled. Check logs in $secondsFromNow seconds for 'Notification receiver triggered'")
        }
    }

    override fun onReceive(context: Context, intent: Intent?) {
        val triggerTime = intent?.getLongExtra("trigger_time", 0L) ?: 0L
        val triggerTimeStr = if (triggerTime > 0) {
            java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.US)
                .format(java.util.Date(triggerTime))
        } else {
            "unknown"
        }
        val currentTime = System.currentTimeMillis()
        val delayMs = if (triggerTime > 0) currentTime - triggerTime else 0L
        
        Log.i(TAG, "Notification receiver triggered: triggerTime=$triggerTimeStr, currentTime=${java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.US).format(java.util.Date(currentTime))}, delayMs=$delayMs")
        
        CoroutineScope(Dispatchers.IO).launch {
            try {
                // Check if this is a static reminder (no content dependency)
                val isStaticReminder = intent?.getBooleanExtra("is_static_reminder", false) ?: false
                
                if (isStaticReminder) {
                    // Handle static reminder without content cache
                    val title = intent?.getStringExtra("title") ?: "Daily Reminder"
                    val body = intent?.getStringExtra("body") ?: "Don't forget your daily check-in!"
                    val sound = intent?.getBooleanExtra("sound", true) ?: true
                    val vibration = intent?.getBooleanExtra("vibration", true) ?: true
                    val priority = intent?.getStringExtra("priority") ?: "normal"
                    val reminderId = intent?.getStringExtra("reminder_id") ?: "unknown"
                    
                    showStaticReminderNotification(context, title, body, sound, vibration, priority, reminderId)
                    
                    // Record reminder trigger in database
                    recordReminderTrigger(context, reminderId)
                    return@launch
                }
                
                // Existing cached content logic for regular notifications
                val db = DailyNotificationDatabase.getDatabase(context)
                val latestCache = db.contentCacheDao().getLatestByScope(ContentCacheScope.DAILY)
                    ?: db.contentCacheDao().getLatestByScope(ContentCacheScope.LEGACY)
                    ?: db.contentCacheDao().getLatest()
                
                if (latestCache == null) {
                    Log.w(TAG, "No cached content available for notification")
                    recordHistory(db, "notify", "no_content")
                    return@launch
                }
                
                // TTL-at-fire check
                val now = System.currentTimeMillis()
                val ttlExpiry = latestCache.fetchedAt + (latestCache.ttlSeconds * 1000L)
                
                if (now > ttlExpiry) {
                    Log.i(TAG, "Content TTL expired, skipping notification")
                    recordHistory(db, "notify", "skipped_ttl")
                    return@launch
                }
                
                // Show notification
                val title = intent?.getStringExtra("title") ?: "Daily Notification"
                val body = intent?.getStringExtra("body") ?: String(latestCache.payload)
                val sound = intent?.getBooleanExtra("sound", true) ?: true
                val vibration = intent?.getBooleanExtra("vibration", true) ?: true
                val priority = intent?.getStringExtra("priority") ?: "normal"
                
                showNotification(context, title, body, sound, vibration, priority)
                recordHistory(db, "notify", "success")
                
                // Fire callbacks
                fireCallbacks(context, db, "onNotifyDelivered", latestCache)
                
            } catch (e: Exception) {
                Log.e(TAG, "Error in notification receiver", e)
                try {
                    val db = DailyNotificationDatabase.getDatabase(context)
                    recordHistory(db, "notify", "failure", e.message)
                } catch (dbError: Exception) {
                    Log.e(TAG, "Failed to record notification failure", dbError)
                }
            }
        }
    }
    
    private fun showNotification(
        context: Context,
        title: String,
        body: String,
        sound: Boolean,
        vibration: Boolean,
        priority: String
    ) {
        val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        
        // Create notification channel for Android 8.0+
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel = NotificationChannel(
                CHANNEL_ID,
                "Daily Notifications",
                when (priority) {
                    "high" -> NotificationManager.IMPORTANCE_HIGH
                    "low" -> NotificationManager.IMPORTANCE_LOW
                    else -> NotificationManager.IMPORTANCE_DEFAULT
                }
            ).apply {
                enableVibration(vibration)
                if (!sound) {
                    setSound(null, null)
                }
            }
            notificationManager.createNotificationChannel(channel)
        }
        
        // Create intent to launch app when notification is clicked
        // Use package launcher intent to avoid hardcoding MainActivity class name
        val intent = getLaunchIntent(context) ?: return
        val pendingIntent = PendingIntent.getActivity(
            context,
            0,
            intent,
            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
        )
        
        val notification = NotificationCompat.Builder(context, CHANNEL_ID)
            .setContentTitle(title)
            .setContentText(body)
            .setSmallIcon(android.R.drawable.ic_dialog_info)
            .setPriority(
                when (priority) {
                    "high" -> NotificationCompat.PRIORITY_HIGH
                    "low" -> NotificationCompat.PRIORITY_LOW
                    else -> NotificationCompat.PRIORITY_DEFAULT
                }
            )
            .setAutoCancel(true) // Dismissible when user swipes it away
            .setContentIntent(pendingIntent) // Launch app when clicked
            .setVibrate(if (vibration) longArrayOf(0, 250, 250, 250) else null)
            .build()
            
        notificationManager.notify(NOTIFICATION_ID, notification)
        Log.i(TAG, "Notification displayed: $title")
    }
    
    private suspend fun recordHistory(
        db: DailyNotificationDatabase,
        kind: String,
        outcome: String,
        diagJson: String? = null
    ) {
        try {
            db.historyDao().insert(
                History(
                    refId = "notify_${System.currentTimeMillis()}",
                    kind = kind,
                    occurredAt = System.currentTimeMillis(),
                    outcome = outcome,
                    diagJson = diagJson
                )
            )
        } catch (e: Exception) {
            Log.e(TAG, "Failed to record history", e)
        }
    }
    
    private suspend fun fireCallbacks(
        context: Context,
        db: DailyNotificationDatabase,
        eventType: String,
        contentCache: ContentCache
    ) {
        try {
            val callbacks = db.callbackDao().getEnabled()
            callbacks.forEach { callback ->
                try {
                    when (callback.kind) {
                        "http" -> fireHttpCallback(callback, eventType, contentCache)
                        "local" -> fireLocalCallback(context, callback, eventType, contentCache)
                        else -> Log.w(TAG, "Unknown callback kind: ${callback.kind}")
                    }
                } catch (e: Exception) {
                    Log.e(TAG, "Failed to fire callback ${callback.id}", e)
                    recordHistory(db, "callback", "failure", "{\"callback_id\": \"${callback.id}\", \"error\": \"${e.message}\"}")
                }
            }
        } catch (e: Exception) {
            Log.e(TAG, "Failed to fire callbacks", e)
        }
    }
    
    private suspend fun fireHttpCallback(
        callback: Callback,
        eventType: String,
        contentCache: ContentCache
    ) {
        // HTTP callback implementation would go here
        Log.i(TAG, "HTTP callback fired: ${callback.id} for event: $eventType")
    }
    
    private suspend fun fireLocalCallback(
        context: Context,
        callback: Callback,
        eventType: String,
        contentCache: ContentCache
    ) {
        // Local callback implementation would go here
        Log.i(TAG, "Local callback fired: ${callback.id} for event: $eventType")
    }

    // Static Reminder Helper Methods
    private fun showStaticReminderNotification(
        context: Context,
        title: String,
        body: String,
        sound: Boolean,
        vibration: Boolean,
        priority: String,
        reminderId: String
    ) {
        val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        
        // Create notification channel for reminders
        createReminderNotificationChannel(context, notificationManager)
        
        // Create intent to launch app when notification is clicked
        // Use package launcher intent to avoid hardcoding MainActivity class name
        val intent = getLaunchIntent(context) ?: return
        val pendingIntent = PendingIntent.getActivity(
            context,
            reminderId.hashCode(),
            intent,
            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
        )
        
        val notification = NotificationCompat.Builder(context, "daily_reminders")
            .setSmallIcon(android.R.drawable.ic_dialog_info)
            .setContentTitle(title)
            .setContentText(body)
            .setPriority(
                when (priority) {
                    "high" -> NotificationCompat.PRIORITY_HIGH
                    "low" -> NotificationCompat.PRIORITY_LOW
                    else -> NotificationCompat.PRIORITY_DEFAULT
                }
            )
            .setSound(if (sound) null else null) // Use default sound if enabled
            .setAutoCancel(true) // Dismissible when user swipes it away
            .setContentIntent(pendingIntent) // Launch app when clicked
            .setVibrate(if (vibration) longArrayOf(0, 250, 250, 250) else null)
            .setCategory(NotificationCompat.CATEGORY_REMINDER)
            .build()
            
        notificationManager.notify(reminderId.hashCode(), notification)
        Log.i(TAG, "Static reminder displayed: $title (ID: $reminderId)")
    }

    private fun createReminderNotificationChannel(context: Context, notificationManager: NotificationManager) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel = NotificationChannel(
                "daily_reminders",
                "Daily Reminders",
                NotificationManager.IMPORTANCE_DEFAULT
            ).apply {
                description = "Daily reminder notifications"
                enableVibration(true)
                setShowBadge(true)
            }
            notificationManager.createNotificationChannel(channel)
        }
    }

    private fun recordReminderTrigger(context: Context, reminderId: String) {
        try {
            val prefs = context.getSharedPreferences("daily_reminders", Context.MODE_PRIVATE)
            val editor = prefs.edit()
            editor.putLong("${reminderId}_lastTriggered", System.currentTimeMillis())
            editor.apply()
            Log.d(TAG, "Reminder trigger recorded: $reminderId")
        } catch (e: Exception) {
            Log.e(TAG, "Error recording reminder trigger", e)
        }
    }
}
