import Foundation import UserNotifications @MainActor final class NotificationService { static let shared = NotificationService() static let remindersEnabledKey = "notifications_reminders_enabled" private static let allReminderIdentifiers = [ "mealmood.next-week-planning", "mealmood.sunday-planning" ] private init() {} /// User-level preference for weekly planning reminders (independent of OS permission). var remindersEnabled: Bool { get { UserDefaults.standard.object(forKey: Self.remindersEnabledKey) as? Bool ?? true } set { UserDefaults.standard.set(newValue, forKey: Self.remindersEnabledKey) if !newValue { cancelAllReminders() } } } func authorizationStatus() async -> UNAuthorizationStatus { await UNUserNotificationCenter.current().notificationSettings().authorizationStatus } /// Explicit permission request for contextual prompts (onboarding, settings toggle). /// Returns whether notifications are authorized after the request. @discardableResult func requestPermission() async -> Bool { let center = UNUserNotificationCenter.current() let settings = await center.notificationSettings() switch settings.authorizationStatus { case .notDetermined: return (try? await center.requestAuthorization(options: [.alert, .sound, .badge])) ?? false case .authorized, .provisional, .ephemeral: return true default: return false } } func requestPermissionIfNeeded() async { let center = UNUserNotificationCenter.current() let settings = await center.notificationSettings() guard settings.authorizationStatus == .notDetermined else { return } _ = try? await center.requestAuthorization(options: [.alert, .sound, .badge]) } func cancelAllReminders() { UNUserNotificationCenter.current() .removePendingNotificationRequests(withIdentifiers: Self.allReminderIdentifiers) } func schedulePlanningReminderIfNeeded(nextWeekPlan: WeekPlan?, language: AppLanguage) { guard remindersEnabled else { cancelAllReminders() return } let center = UNUserNotificationCenter.current() let identifier = "mealmood.next-week-planning" let isComplete = nextWeekPlan?.slotList.allSatisfy { $0.dishId != nil || $0.isEatingOut } ?? false if isComplete { center.removePendingNotificationRequests(withIdentifiers: [identifier]) } else { let nextWeekStart = Date().startOfWeek().addingDays(7) let reminderDay = nextWeekStart.addingDays(-1) let calendar = Calendar.current var components = calendar.dateComponents([.year, .month, .day], from: reminderDay) components.hour = 19 components.minute = 0 let trigger = UNCalendarNotificationTrigger(dateMatching: components, repeats: false) let content = UNMutableNotificationContent() content.title = localizedString("notification_planning_title", language: language) content.body = localizedString("notification_planning_body", language: language) content.sound = .default let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger) center.removePendingNotificationRequests(withIdentifiers: [identifier]) center.add(request) } scheduleSundayReminderIfNeeded(nextWeekPlan: nextWeekPlan, language: language) } private func scheduleSundayReminderIfNeeded(nextWeekPlan: WeekPlan?, language: AppLanguage) { let center = UNUserNotificationCenter.current() let identifier = "mealmood.sunday-planning" // Cancel if next week is already planned let hasAnySlot = nextWeekPlan?.slotList.contains { $0.dishId != nil || $0.isEatingOut } ?? false if hasAnySlot { center.removePendingNotificationRequests(withIdentifiers: [identifier]) return } // Schedule for next Sunday at 17:00 var components = DateComponents() components.weekday = 1 // Sunday components.hour = 17 components.minute = 0 let trigger = UNCalendarNotificationTrigger(dateMatching: components, repeats: true) let content = UNMutableNotificationContent() content.title = localizedString("notification_sunday_title", language: language) content.body = localizedString("notification_sunday_body", language: language) content.sound = .default let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger) center.removePendingNotificationRequests(withIdentifiers: [identifier]) center.add(request) } }