/**
 * DailyNotificationScheduler.swift
 * 
 * Handles scheduling and timing of daily notifications using UNUserNotificationCenter
 * Implements calendar-based triggers with timing tolerance (±180s) and permission auto-healing
 * 
 * @author Matthew Raymer
 * @version 1.0.0
 */

import Foundation
import UserNotifications

/**
 * Protocol for scheduling background fetches
 */
protocol DailyNotificationFetchScheduling {
    func scheduleFetch(atMillis: Int64)
    func scheduleImmediateFetch()
}

/**
 * No-op implementation for when fetcher is not available
 */
final class NoopFetcherScheduler: DailyNotificationFetchScheduling {
    func scheduleFetch(atMillis: Int64) { /* intentionally noop */ }
    func scheduleImmediateFetch() { /* intentionally noop */ }
}

/**
 * Manages scheduling of daily notifications using UNUserNotificationCenter
 * 
 * This class handles the scheduling aspect of the prefetch → cache → schedule → display pipeline.
 * It supports calendar-based triggers with iOS timing tolerance (±180s).
 */
class DailyNotificationScheduler {
    
    // MARK: - Constants
    
    private static let TAG = "DailyNotificationScheduler"
    private static let NOTIFICATION_CATEGORY_ID = "DAILY_NOTIFICATION"
    private static let TIMING_TOLERANCE_SECONDS: TimeInterval = 180 // ±180 seconds tolerance
    
    // MARK: - Properties
    
    private let notificationCenter: UNUserNotificationCenter
    private var scheduledNotifications: Set<String> = []
    private let schedulerQueue = DispatchQueue(label: "org.timesafari.dailynotification.scheduler", attributes: .concurrent)
    
    // TTL enforcement
    private weak var ttlEnforcer: DailyNotificationTTLEnforcer?
    
    // Fetch scheduling
    private let fetchScheduler: DailyNotificationFetchScheduling
    
    // MARK: - Initialization
    
    /**
     * Initialize scheduler
     * 
     * @param fetchScheduler Optional fetch scheduler (defaults to NoopFetcherScheduler)
     */
    init(fetchScheduler: DailyNotificationFetchScheduling = NoopFetcherScheduler()) {
        self.notificationCenter = UNUserNotificationCenter.current()
        self.fetchScheduler = fetchScheduler
        setupNotificationCategory()
    }
    
    /**
     * Set TTL enforcer for freshness validation
     * 
     * @param ttlEnforcer TTL enforcement instance
     */
    func setTTLEnforcer(_ ttlEnforcer: DailyNotificationTTLEnforcer) {
        self.ttlEnforcer = ttlEnforcer
        print("\(Self.TAG): TTL enforcer set for freshness validation")
    }
    
    // MARK: - Notification Category Setup
    
    /**
     * Setup notification category for actions
     */
    private func setupNotificationCategory() {
        let category = UNNotificationCategory(
            identifier: Self.NOTIFICATION_CATEGORY_ID,
            actions: [],
            intentIdentifiers: [],
            options: []
        )
        
        notificationCenter.setNotificationCategories([category])
        print("\(Self.TAG): Notification category setup complete")
    }
    
    // MARK: - Permission Management
    
    /**
     * Check notification permission status
     * 
     * @return Authorization status
     */
    func checkPermissionStatus() async -> UNAuthorizationStatus {
        let settings = await notificationCenter.notificationSettings()
        return settings.authorizationStatus
    }
    
    /**
     * Request notification permissions
     * 
     * @return true if permissions granted
     */
    func requestPermissions() async -> Bool {
        do {
            let granted = try await notificationCenter.requestAuthorization(options: [.alert, .sound, .badge])
            print("\(Self.TAG): Permission request result: \(granted)")
            return granted
        } catch {
            print("\(Self.TAG): Permission request failed: \(error)")
            return false
        }
    }
    
    /**
     * Auto-heal permissions: Check and request if needed
     * 
     * @return Authorization status after auto-healing
     */
    func autoHealPermissions() async -> UNAuthorizationStatus {
        let status = await checkPermissionStatus()
        
        switch status {
        case .notDetermined:
            // Request permissions
            let granted = await requestPermissions()
            return granted ? .authorized : .denied
        case .denied:
            // Cannot auto-heal denied permissions
            return .denied
        case .authorized, .provisional, .ephemeral:
            return status
        @unknown default:
            return .notDetermined
        }
    }
    
    // MARK: - Scheduling
    
    /**
     * Schedule a notification for delivery
     * 
     * @param content Notification content to schedule
     * @return true if scheduling was successful
     */
    func scheduleNotification(_ content: NotificationContent) async -> Bool {
        do {
            print("\(Self.TAG): Scheduling notification: \(content.id)")
            
            // Permission auto-healing
            let permissionStatus = await autoHealPermissions()
            if permissionStatus != .authorized && permissionStatus != .provisional {
                print("\(Self.TAG): Notifications denied, cannot schedule")
                // Log error code for debugging
                print("\(Self.TAG): Error code: \(DailyNotificationErrorCodes.NOTIFICATIONS_DENIED)")
                return false
            }
            
            // TTL validation before arming
            if let ttlEnforcer = ttlEnforcer {
                let okToArm = ttlEnforcer.validateBeforeArming(content)
                if !okToArm {
                    print("\(Self.TAG): TTL validation failed, skipping schedule for \(content.id)")
                    return false
                }
            }
            
            // Cancel any existing notification for this ID
            await cancelNotification(id: content.id)
            
            // Create notification content
            let notificationContent = UNMutableNotificationContent()
            notificationContent.title = content.title ?? "Daily Update"
            notificationContent.body = content.body ?? "Your daily notification is ready"
            notificationContent.sound = .default
            notificationContent.categoryIdentifier = Self.NOTIFICATION_CATEGORY_ID
            notificationContent.userInfo = [
                "notification_id": content.id,
                "scheduled_time": content.scheduledTime,
                "fetched_at": content.fetchedAt
            ]
            
            // Create calendar trigger for daily scheduling
            let scheduledDate = content.getScheduledTimeAsDate()
            let calendar = Calendar.current
            let dateComponents = calendar.dateComponents([.year, .month, .day, .hour, .minute], from: scheduledDate)
            
            let trigger = UNCalendarNotificationTrigger(
                dateMatching: dateComponents,
                repeats: false
            )
            
            // Create notification request
            let request = UNNotificationRequest(
                identifier: content.id,
                content: notificationContent,
                trigger: trigger
            )
            
            // Schedule notification
            try await notificationCenter.add(request)
            
            schedulerQueue.async(flags: .barrier) {
                self.scheduledNotifications.insert(content.id)
            }
            
            // Log pending count for test scripts (matches Android's alarm count logging)
            // Use NSLog to ensure it appears in system logs (print() may not always be captured)
            let pendingCount = await getPendingNotificationCount()
            NSLog("\(Self.TAG): Notification scheduled successfully for \(scheduledDate), id=\(content.id), pendingCount=\(pendingCount)")
            print("\(Self.TAG): Notification scheduled successfully for \(scheduledDate), id=\(content.id), pendingCount=\(pendingCount)")
            return true
            
        } catch {
            print("\(Self.TAG): Error scheduling notification: \(error)")
            return false
        }
    }
    
    /**
     * Cancel a notification by ID
     * 
     * @param id Notification ID
     */
    func cancelNotification(id: String) async {
        notificationCenter.removePendingNotificationRequests(withIdentifiers: [id])
        
        schedulerQueue.async(flags: .barrier) {
            self.scheduledNotifications.remove(id)
        }
        
        print("\(Self.TAG): Notification cancelled: \(id)")
    }
    
    /**
     * Cancel all scheduled notifications
     */
    func cancelAllNotifications() async {
        notificationCenter.removeAllPendingNotificationRequests()
        
        schedulerQueue.async(flags: .barrier) {
            self.scheduledNotifications.removeAll()
        }
        
        print("\(Self.TAG): All notifications cancelled")
    }
    
    // MARK: - Status Queries
    
    /**
     * Get pending notification requests
     * 
     * @return Array of pending notification identifiers
     */
    func getPendingNotifications() async -> [String] {
        let requests = await notificationCenter.pendingNotificationRequests()
        return requests.map { $0.identifier }
    }
    
    /**
     * Get notification status
     * 
     * @param id Notification ID
     * @return true if notification is scheduled
     */
    func isNotificationScheduled(id: String) async -> Bool {
        let requests = await notificationCenter.pendingNotificationRequests()
        return requests.contains { $0.identifier == id }
    }
    
    /**
     * Get count of pending notifications
     * 
     * @return Count of pending notifications
     */
    func getPendingNotificationCount() async -> Int {
        let requests = await notificationCenter.pendingNotificationRequests()
        return requests.count
    }
    
    // MARK: - Helper Methods
    
    /**
     * 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)
    }
    
    /**
     * Calculate next occurrence of a daily time
     * 
     * Matches Android calculateNextOccurrence() functionality
     * 
     * @param hour Hour of day (0-23)
     * @param minute Minute of hour (0-59)
     * @return Timestamp in milliseconds of next occurrence
     */
    func calculateNextOccurrence(hour: Int, minute: Int) -> Int64 {
        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 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)
    }
    
    /**
     * Get next notification time from pending notifications
     * 
     * @return Timestamp in milliseconds of next notification or nil
     */
    func getNextNotificationTime() async -> Int64? {
        let requests = await notificationCenter.pendingNotificationRequests()
        
        // Find the earliest scheduled notification by checking all requests
        var earliestDate: Date? = nil
        var earliestRequestId: String? = nil
        var allTimes: [(String, String)] = []
        
        for request in requests {
            var requestTime: Date? = nil
            
            if let trigger = request.trigger as? UNCalendarNotificationTrigger,
               let nextDate = trigger.nextTriggerDate() {
                requestTime = nextDate
            } else if let trigger = request.trigger as? UNTimeIntervalNotificationTrigger,
                      let nextDate = trigger.nextTriggerDate() {
                requestTime = nextDate
            }
            
            if let time = requestTime {
                let timeStr = formatTime(Int64(time.timeIntervalSince1970 * 1000))
                allTimes.append((request.identifier, timeStr))
                
                if earliestDate == nil || time < earliestDate! {
                    earliestDate = time
                    earliestRequestId = request.identifier
                }
            }
        }
        
        guard let nextDate = earliestDate else {
            NSLog("DNP-ROLLOVER: GET_NEXT_TIME no_pending_requests")
            print("DNP-ROLLOVER: GET_NEXT_TIME no_pending_requests")
            return nil
        }
        
        let nextTime = Int64(nextDate.timeIntervalSince1970 * 1000)
        let nextTimeStr = formatTime(nextTime)
        let allTimesStr = allTimes.map { "\($0.0):\($0.1)" }.joined(separator: ", ")
        NSLog("DNP-ROLLOVER: GET_NEXT_TIME found=\(nextTimeStr) id=\(earliestRequestId ?? "unknown") from \(requests.count) pending: [\(allTimesStr)]")
        print("DNP-ROLLOVER: GET_NEXT_TIME found=\(nextTimeStr) id=\(earliestRequestId ?? "unknown") from \(requests.count) pending: [\(allTimesStr)]")
        
        return nextTime
    }
    
    /**
     * Calculate next scheduled time from current (24h or rollover interval minutes). DST-safe.
     * When rolloverIntervalMinutes > 0 (dev/testing), adds that many minutes; otherwise adds 24 hours.
     */
    func calculateNextScheduledTime(_ currentScheduledTime: Int64, rolloverIntervalMinutes: Int? = nil) -> Int64 {
        let calendar = Calendar.current
        let currentDate = Date(timeIntervalSince1970: Double(currentScheduledTime) / 1000.0)
        let currentTimeStr = formatTime(currentScheduledTime)
        let addMinutes = (rolloverIntervalMinutes ?? 0) > 0 ? rolloverIntervalMinutes! : (24 * 60)
        guard let nextDate = calendar.date(byAdding: .minute, value: addMinutes, to: currentDate) else {
            let fallbackTime = currentScheduledTime + (Int64(addMinutes) * 60 * 1000)
            NSLog("DNP-ROLLOVER: DST_CALC_FAILED current=\(currentTimeStr) using_fallback add_minutes=\(addMinutes)")
            return fallbackTime
        }
        let nextTime = Int64(nextDate.timeIntervalSince1970 * 1000)
        let nextTimeStr = formatTime(nextTime)
        let timeDiffMs = nextTime - currentScheduledTime
        let timeDiffHours = Double(timeDiffMs) / 1000.0 / 60.0 / 60.0
        NSLog("DNP-ROLLOVER: CALC_NEXT current=\(currentTimeStr) next=\(nextTimeStr) diff_hours=\(String(format: "%.2f", timeDiffHours)) rollover_min=\(rolloverIntervalMinutes ?? 0)")
        return nextTime
    }
    
    /**
     * Schedule next notification after current one fires (rollover)
     * 
     * Matches Android scheduleNextNotification() functionality
     * Implements multi-level duplicate prevention
     * 
     * @param content Current notification content that just fired
     * @param storage Storage instance for duplicate checking
     * @param fetcher Optional fetcher for scheduling prefetch (Phase 2)
     * @return true if next notification was scheduled successfully
     */
    func scheduleNextNotification(
        _ content: NotificationContent,
        storage: DailyNotificationStorage?,
        fetcher: Any? = nil
    ) async -> Bool {
        let currentTime = Int64(Date().timeIntervalSince1970 * 1000)
        let currentTimeStr = formatTime(currentTime)
        let currentScheduledTimeStr = formatTime(content.scheduledTime)
        
        NSLog("DNP-ROLLOVER: START id=\(content.id) current_time=\(currentTimeStr) scheduled_time=\(currentScheduledTimeStr)")
        print("DNP-ROLLOVER: START id=\(content.id) current_time=\(currentTimeStr) scheduled_time=\(currentScheduledTimeStr)")
        
        // Check 1: Rollover state tracking (prevent duplicate rollover attempts)
        if let storage = storage {
            let lastRolloverTime = await storage.getLastRolloverTime(for: content.id)
            
            // If rollover was processed recently (< 1 hour ago), skip
            // TESTING: Change `(60 * 60 * 1000)` to `(60 * 1000)` for 1-minute threshold when testing with 2-minute intervals
            if let lastTime = lastRolloverTime,
               (currentTime - lastTime) < (60 * 60 * 1000) {
                let lastTimeStr = formatTime(lastTime)
                let timeSinceRollover = (currentTime - lastTime) / 1000 / 60 // minutes
                NSLog("DNP-ROLLOVER: SKIP id=\(content.id) already_processed last_rollover=\(lastTimeStr) \(timeSinceRollover)min_ago")
                print("DNP-ROLLOVER: SKIP id=\(content.id) already_processed last_rollover=\(lastTimeStr) \(timeSinceRollover)min_ago")
                return false
            }
        }
        
        // Calculate next occurrence (use stored rollover interval for dev/testing, else 24h)
        let rolloverMin = (content.rolloverIntervalMinutes ?? 0) > 0 ? content.rolloverIntervalMinutes : nil
        var nextScheduledTime = calculateNextScheduledTime(content.scheduledTime, rolloverIntervalMinutes: rolloverMin)
        
        // If next scheduled time is in the past, keep calculating forward until we get a future time
        // This handles cases where the notification fired more than 2 minutes ago
        while nextScheduledTime < currentTime {
            let nextTimeStr = formatTime(nextScheduledTime)
            NSLog("DNP-ROLLOVER: NEXT_IN_PAST id=\(content.id) next_time=\(nextTimeStr) current_time=\(currentTimeStr), calculating forward")
            nextScheduledTime = calculateNextScheduledTime(nextScheduledTime, rolloverIntervalMinutes: rolloverMin)
        }
        
        let nextScheduledTimeStr = formatTime(nextScheduledTime)
        let hoursUntilNext = Double(nextScheduledTime - currentTime) / 1000.0 / 60.0 / 60.0
        
        NSLog("DNP-ROLLOVER: CALCULATED id=\(content.id) next_time=\(nextScheduledTimeStr) hours_until=\(String(format: "%.2f", hoursUntilNext))")
        print("DNP-ROLLOVER: CALCULATED id=\(content.id) next_time=\(nextScheduledTimeStr) hours_until=\(String(format: "%.2f", hoursUntilNext))")
        
        // Check 2: Storage-level duplicate check (prevent duplicate notifications)
        if let storage = storage {
            let existingNotifications = storage.getAllNotifications()
            let toleranceMs: Int64 = 60 * 1000 // 1 minute tolerance for DST shifts
            
            for existing in existingNotifications {
                if abs(existing.scheduledTime - nextScheduledTime) <= toleranceMs {
                    let existingTimeStr = formatTime(existing.scheduledTime)
                    let timeDiffMs = abs(existing.scheduledTime - nextScheduledTime)
                    NSLog("DNP-ROLLOVER: DUPLICATE_STORAGE id=\(content.id) existing_id=\(existing.id) existing_time=\(existingTimeStr) time_diff_ms=\(timeDiffMs)")
                    print("DNP-ROLLOVER: DUPLICATE_STORAGE id=\(content.id) existing_id=\(existing.id) existing_time=\(existingTimeStr) time_diff_ms=\(timeDiffMs)")
                    return false // Skip rescheduling to prevent duplicate
                }
            }
        }
        
        // Check 3: System-level duplicate check (query UNUserNotificationCenter)
        let pendingNotifications = await notificationCenter.pendingNotificationRequests()
        NSLog("DNP-ROLLOVER: CHECK_SYSTEM id=\(content.id) pending_count=\(pendingNotifications.count)")
        print("DNP-ROLLOVER: CHECK_SYSTEM id=\(content.id) pending_count=\(pendingNotifications.count)")
        
        for pending in pendingNotifications {
            if let trigger = pending.trigger as? UNCalendarNotificationTrigger,
               let nextDate = trigger.nextTriggerDate() {
                let pendingTime = Int64(nextDate.timeIntervalSince1970 * 1000)
                let toleranceMs: Int64 = 60 * 1000
                
                if abs(pendingTime - nextScheduledTime) <= toleranceMs {
                    let pendingTimeStr = formatTime(pendingTime)
                    let timeDiffMs = abs(pendingTime - nextScheduledTime)
                    NSLog("DNP-ROLLOVER: DUPLICATE_SYSTEM id=\(content.id) system_pending_id=\(pending.identifier) pending_time=\(pendingTimeStr) time_diff_ms=\(timeDiffMs)")
                    print("DNP-ROLLOVER: DUPLICATE_SYSTEM id=\(content.id) system_pending_id=\(pending.identifier) pending_time=\(pendingTimeStr) time_diff_ms=\(timeDiffMs)")
                    return false
                }
            }
        }
        
        // Extract hour:minute from current scheduled time for logging
        let calendar = Calendar.current
        let scheduledDate = content.getScheduledTimeAsDate()
        let hour = calendar.component(.hour, from: scheduledDate)
        let minute = calendar.component(.minute, from: scheduledDate)
        
        // Create new notification content for next occurrence
        // Note: Content will be refreshed by prefetch, but we need placeholder
        let nextId = "daily_rollover_\(Int64(Date().timeIntervalSince1970 * 1000))"
        let nextContent = NotificationContent(
            id: nextId,
            title: content.title,
            body: content.body,
            scheduledTime: nextScheduledTime,
            fetchedAt: Int64(Date().timeIntervalSince1970 * 1000),
            url: content.url,
            payload: content.payload,
            etag: content.etag,
            rolloverIntervalMinutes: content.rolloverIntervalMinutes
        )
        
        // Schedule the next notification
        NSLog("DNP-ROLLOVER: SCHEDULING id=\(content.id) next_id=\(nextId) next_time=\(nextScheduledTimeStr)")
        print("DNP-ROLLOVER: SCHEDULING id=\(content.id) next_id=\(nextId) next_time=\(nextScheduledTimeStr)")
        
        let scheduled = await scheduleNotification(nextContent)
        
        if scheduled {
            // Save notification content to storage so it can be retrieved when rollover fires
            // This is critical: without saving, processRollover won't find the content
            storage?.saveNotificationContent(nextContent)
            NSLog("DNP-ROLLOVER: SAVED id=\(content.id) next_id=\(nextId) content saved to storage")
            print("DNP-ROLLOVER: SAVED id=\(content.id) next_id=\(nextId) content saved to storage")
            
            // Verify the notification was actually scheduled
            let pendingCount = await getPendingNotificationCount()
            let isScheduled = await isNotificationScheduled(id: nextId)
            
            NSLog("DNP-ROLLOVER: SUCCESS id=\(content.id) next_id=\(nextId) next_time=\(nextScheduledTimeStr) pending_count=\(pendingCount) is_scheduled=\(isScheduled)")
            print("DNP-ROLLOVER: SUCCESS id=\(content.id) next_id=\(nextId) next_time=\(nextScheduledTimeStr) pending_count=\(pendingCount) is_scheduled=\(isScheduled)")
            
            // Log time difference verification
            let timeDiffMs = nextScheduledTime - content.scheduledTime
            let timeDiffHours = Double(timeDiffMs) / 1000.0 / 60.0 / 60.0
            NSLog("DNP-ROLLOVER: TIME_VERIFY id=\(content.id) current=\(currentScheduledTimeStr) next=\(nextScheduledTimeStr) diff_hours=\(String(format: "%.2f", timeDiffHours))")
            print("DNP-ROLLOVER: TIME_VERIFY id=\(content.id) current=\(currentScheduledTimeStr) next=\(nextScheduledTimeStr) diff_hours=\(String(format: "%.2f", timeDiffHours))")
            
            // Schedule background fetch for next notification (5 minutes before scheduled time)
            let fetchTime = nextScheduledTime - (5 * 60 * 1000) // 5 minutes before
            let currentTime = Int64(Date().timeIntervalSince1970 * 1000)
            
            if fetchTime > currentTime {
                print("\(Self.TAG): scheduling fetch at \(fetchTime)")
                fetchScheduler.scheduleFetch(atMillis: fetchTime)
                NSLog("DNP-ROLLOVER: PREFETCH_SCHEDULED id=\(content.id) next_fetch=\(fetchTime) next_notification=\(nextScheduledTime)")
                print("DNP-ROLLOVER: PREFETCH_SCHEDULED id=\(content.id) next_fetch=\(fetchTime) next_notification=\(nextScheduledTime)")
            } else {
                print("\(Self.TAG): scheduling immediate fetch")
                fetchScheduler.scheduleImmediateFetch()
                NSLog("DNP-ROLLOVER: PREFETCH_PAST id=\(content.id) fetch_time=\(fetchTime) current=\(currentTime)")
                print("DNP-ROLLOVER: PREFETCH_PAST id=\(content.id) fetch_time=\(fetchTime) current=\(currentTime)")
            }
            
            // Mark rollover as processed
            let rolloverProcessedTime = Int64(Date().timeIntervalSince1970 * 1000)
            await storage?.saveLastRolloverTime(for: content.id, time: rolloverProcessedTime)
            
            NSLog("DNP-ROLLOVER: COMPLETE id=\(content.id) next_id=\(nextId) next_time=\(nextScheduledTimeStr)")
            print("DNP-ROLLOVER: COMPLETE id=\(content.id) next_id=\(nextId) next_time=\(nextScheduledTimeStr)")
            
            return true
        } else {
            NSLog("DNP-ROLLOVER: ERROR id=\(content.id) scheduling_failed next_time=\(nextScheduledTimeStr)")
            print("DNP-ROLLOVER: ERROR id=\(content.id) scheduling_failed next_time=\(nextScheduledTimeStr)")
            return false
        }
    }
}

