// ── Sleep Manager ───────────────────────────────────────────── // Bedtime routines, smart alarm, HealthKit sleep data integration // Ties into AI reschedule for sleep-aware morning adjustments import Foundation import HealthKit import Combine @MainActor final class SleepManager: ObservableObject { static let shared = SleepManager() @Published var bedtimeRoutine: BedtimeRoutine? @Published var smartAlarmConfig: SmartAlarmConfig? @Published var lastSleepData: SleepSummary? @Published var healthKitAuthorized = false private let healthStore = HKHealthStore() private let routineKey = "chronomind-bedtime-routine" private let alarmKey = "chronomind-smart-alarm" private let sleepKey = "chronomind-last-sleep" private init() { loadData() } // MARK: - HealthKit Authorization func requestHealthKitAccess() async -> Bool { guard HKHealthStore.isHealthDataAvailable() else { return false } let sleepType = HKObjectType.categoryType(forIdentifier: .sleepAnalysis)! let readTypes: Set = [sleepType] do { try await healthStore.requestAuthorization(toShare: [], read: readTypes) healthKitAuthorized = true return true } catch { return false } } // MARK: - Sleep Data (Read-Only from HealthKit) func fetchLastNightSleep() async -> SleepSummary? { guard healthKitAuthorized else { return nil } let sleepType = HKObjectType.categoryType(forIdentifier: .sleepAnalysis)! let calendar = Calendar.current let now = Date() let yesterday = calendar.date(byAdding: .day, value: -1, to: calendar.startOfDay(for: now))! let predicate = HKQuery.predicateForSamples(withStart: yesterday, end: now, options: .strictStartDate) let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false) return await withCheckedContinuation { continuation in let query = HKSampleQuery( sampleType: sleepType, predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: [sortDescriptor] ) { _, samples, error in guard let samples = samples as? [HKCategorySample], !samples.isEmpty else { continuation.resume(returning: nil) return } let summary = self.processSleepSamples(samples) Task { @MainActor in self.lastSleepData = summary self.saveSleepData(summary) } continuation.resume(returning: summary) } healthStore.execute(query) } } private func processSleepSamples(_ samples: [HKCategorySample]) -> SleepSummary { var totalAsleep: TimeInterval = 0 var totalInBed: TimeInterval = 0 var sleepStart: Date? var sleepEnd: Date? for sample in samples { let duration = sample.endDate.timeIntervalSince(sample.startDate) switch sample.value { case HKCategoryValueSleepAnalysis.asleepCore.rawValue, HKCategoryValueSleepAnalysis.asleepDeep.rawValue, HKCategoryValueSleepAnalysis.asleepREM.rawValue: totalAsleep += duration case HKCategoryValueSleepAnalysis.inBed.rawValue: totalInBed += duration default: break } if sleepStart == nil || sample.startDate < sleepStart! { sleepStart = sample.startDate } if sleepEnd == nil || sample.endDate > sleepEnd! { sleepEnd = sample.endDate } } return SleepSummary( totalSleepHours: totalAsleep / 3600, totalInBedHours: max(totalInBed, totalAsleep) / 3600, sleepEfficiency: totalInBed > 0 ? (totalAsleep / totalInBed) : 0, bedtime: sleepStart, wakeTime: sleepEnd, date: Date() ) } // MARK: - Smart Alarm /// Generate a reschedule suggestion based on sleep data func sleepAwareRescheduleSuggestion(sleepSummary: SleepSummary) -> String? { guard let wakeTime = sleepSummary.wakeTime else { return nil } let sleepHours = sleepSummary.totalSleepHours if sleepHours < 6 { let deficit = Int((7 - sleepHours) * 15) // 15 min per hour of deficit return "You slept \(String(format: "%.0f", sleepHours))h \(Int((sleepHours.truncatingRemainder(dividingBy: 1)) * 60))m. Shift your morning by \(deficit) minutes?" } else if sleepHours < 7 { return "You slept \(String(format: "%.0f", sleepHours))h \(Int((sleepHours.truncatingRemainder(dividingBy: 1)) * 60))m. Consider a gentler morning — shift timers by 10 minutes?" } return nil } // MARK: - Bedtime Routine func saveBedtimeRoutine(_ routine: BedtimeRoutine) { bedtimeRoutine = routine if let data = try? JSONEncoder().encode(routine) { UserDefaults.standard.set(data, forKey: routineKey) } } func saveSmartAlarmConfig(_ config: SmartAlarmConfig) { smartAlarmConfig = config if let data = try? JSONEncoder().encode(config) { UserDefaults.standard.set(data, forKey: alarmKey) } } // MARK: - Wind-Down Notifications func scheduleWindDownNotification(bedtime: Date) { let content = UNMutableNotificationContent() content.title = "Time to wind down" content.body = "Bedtime in 30 minutes — start your wind-down routine" content.sound = .default content.categoryIdentifier = "WIND_DOWN" let triggerDate = bedtime.addingTimeInterval(-1800) // 30 min before guard triggerDate > Date() else { return } let components = Calendar.current.dateComponents([.hour, .minute], from: triggerDate) let trigger = UNCalendarNotificationTrigger(dateMatching: components, repeats: true) let request = UNNotificationRequest(identifier: "wind-down", content: content, trigger: trigger) UNUserNotificationCenter.current().add(request) } // MARK: - Persistence private func loadData() { if let data = UserDefaults.standard.data(forKey: routineKey), let decoded = try? JSONDecoder().decode(BedtimeRoutine.self, from: data) { bedtimeRoutine = decoded } if let data = UserDefaults.standard.data(forKey: alarmKey), let decoded = try? JSONDecoder().decode(SmartAlarmConfig.self, from: data) { smartAlarmConfig = decoded } if let data = UserDefaults.standard.data(forKey: sleepKey), let decoded = try? JSONDecoder().decode(SleepSummary.self, from: data) { lastSleepData = decoded } } private func saveSleepData(_ summary: SleepSummary) { if let data = try? JSONEncoder().encode(summary) { UserDefaults.standard.set(data, forKey: sleepKey) } } } // MARK: - Models struct SleepSummary: Codable { let totalSleepHours: Double let totalInBedHours: Double let sleepEfficiency: Double // 0-1 let bedtime: Date? let wakeTime: Date? let date: Date var formattedSleep: String { let hours = Int(totalSleepHours) let minutes = Int((totalSleepHours - Double(hours)) * 60) return "\(hours)h \(minutes)m" } var qualityLabel: String { switch totalSleepHours { case 8...: return "Excellent" case 7..<8: return "Good" case 6..<7: return "Fair" default: return "Poor" } } var qualityColor: String { switch totalSleepHours { case 8...: return "success" case 7..<8: return "accent" case 6..<7: return "important" default: return "error" } } } struct BedtimeRoutine: Codable { var targetBedtime: Date // Time component only var windDownMinutes: Int // Default 30 var steps: [RoutineStep] var enabled: Bool struct RoutineStep: Codable, Identifiable { let id: String var label: String var durationMinutes: Int var icon: String init(label: String, durationMinutes: Int, icon: String) { self.id = UUID().uuidString self.label = label self.durationMinutes = durationMinutes self.icon = icon } } static let `default` = BedtimeRoutine( targetBedtime: Calendar.current.date(from: DateComponents(hour: 22, minute: 30))!, windDownMinutes: 30, steps: [ RoutineStep(label: "Put devices away", durationMinutes: 5, icon: "iphone.slash"), RoutineStep(label: "Read or journal", durationMinutes: 15, icon: "book.fill"), RoutineStep(label: "Breathing exercise", durationMinutes: 5, icon: "wind"), RoutineStep(label: "Lights out", durationMinutes: 5, icon: "moon.fill"), ], enabled: false ) } struct SmartAlarmConfig: Codable { var windowStart: Date // e.g. 6:30 AM var windowEnd: Date // e.g. 7:00 AM var enabled: Bool var useSleepData: Bool // Use HealthKit to optimize wake time static let `default` = SmartAlarmConfig( windowStart: Calendar.current.date(from: DateComponents(hour: 6, minute: 30))!, windowEnd: Calendar.current.date(from: DateComponents(hour: 7, minute: 0))!, enabled: false, useSleepData: true ) }