// ── Mood / Energy Check-In Manager ─────────────────────────── // Quick post-timer energy check-in with weekly insights // All data stored locally — never sent to any server import Foundation import Combine @MainActor final class MoodCheckInManager: ObservableObject { static let shared = MoodCheckInManager() @Published var checkIns: [MoodCheckIn] = [] @Published var enabled: Bool { didSet { UserDefaults.standard.set(enabled, forKey: enabledKey) } } private let storageKey = "chronomind-mood-checkins" private let enabledKey = "chronomind-mood-enabled" private init() { enabled = UserDefaults.standard.bool(forKey: enabledKey) loadCheckIns() } // MARK: - Record Check-In func record(energy: EnergyLevel, timerId: String?, timerLabel: String?, context: CheckInContext = .postTimer) { let checkIn = MoodCheckIn( energy: energy, timerId: timerId, timerLabel: timerLabel, context: context, timestamp: Date(), hourOfDay: Calendar.current.component(.hour, from: Date()), dayOfWeek: Calendar.current.component(.weekday, from: Date()) ) checkIns.append(checkIn) // Keep last 500 check-ins (~2 months at 8/day) if checkIns.count > 500 { checkIns = Array(checkIns.suffix(500)) } saveCheckIns() } // MARK: - Insights /// Weekly insight: best productivity hours func weeklyInsight() -> WeeklyMoodInsight? { let calendar = Calendar.current let weekAgo = calendar.date(byAdding: .day, value: -7, to: Date())! let recentCheckIns = checkIns.filter { $0.timestamp >= weekAgo } guard recentCheckIns.count >= 5 else { return nil } // Group by hour of day let byHour = Dictionary(grouping: recentCheckIns, by: \.hourOfDay) let hourAverages = byHour.mapValues { entries -> Double in let sum = entries.reduce(0.0) { $0 + Double($1.energy.numericValue) } return sum / Double(entries.count) } // Find peak productivity window let sortedHours = hourAverages.sorted { $0.value > $1.value } let peakHours = sortedHours.prefix(3).map(\.key).sorted() // Average energy let totalEnergy = recentCheckIns.reduce(0.0) { $0 + Double($1.energy.numericValue) } let avgEnergy = totalEnergy / Double(recentCheckIns.count) // Energy distribution let distribution = Dictionary(grouping: recentCheckIns, by: \.energy) .mapValues(\.count) // Best day of week let byDay = Dictionary(grouping: recentCheckIns, by: \.dayOfWeek) let dayAverages = byDay.mapValues { entries -> Double in let sum = entries.reduce(0.0) { $0 + Double($1.energy.numericValue) } return sum / Double(entries.count) } let bestDay = dayAverages.max(by: { $0.value < $1.value })?.key return WeeklyMoodInsight( checkInCount: recentCheckIns.count, averageEnergy: avgEnergy, peakHours: peakHours, bestDayOfWeek: bestDay, distribution: distribution, insightText: generateInsightText(peakHours: peakHours, avgEnergy: avgEnergy) ) } /// Generate human-readable insight private func generateInsightText(peakHours: [Int], avgEnergy: Double) -> String { guard !peakHours.isEmpty else { return "Keep checking in to see patterns!" } let formatter = DateFormatter() formatter.dateFormat = "ha" let peakRange: String if peakHours.count >= 2 { let startHour = peakHours.first! let endHour = peakHours.last! + 1 let startDate = Calendar.current.date(from: DateComponents(hour: startHour))! let endDate = Calendar.current.date(from: DateComponents(hour: endHour))! peakRange = "\(formatter.string(from: startDate))-\(formatter.string(from: endDate))" } else { let startDate = Calendar.current.date(from: DateComponents(hour: peakHours[0]))! peakRange = "around \(formatter.string(from: startDate))" } return "You're most productive \(peakRange)" } // MARK: - Check-In Prompts /// Determine if we should prompt for a check-in func shouldPrompt(for context: CheckInContext) -> Bool { guard enabled else { return false } let calendar = Calendar.current let today = calendar.startOfDay(for: Date()) // Limit to max 8 check-ins per day let todayCheckIns = checkIns.filter { calendar.startOfDay(for: $0.timestamp) == today } guard todayCheckIns.count < 8 else { return false } // Don't prompt more than once per 30 minutes if let lastCheckIn = checkIns.last { let timeSince = Date().timeIntervalSince(lastCheckIn.timestamp) guard timeSince > 1800 else { return false } } return true } // MARK: - Persistence private func loadCheckIns() { guard let data = UserDefaults.standard.data(forKey: storageKey), let decoded = try? JSONDecoder().decode([MoodCheckIn].self, from: data) else { return } checkIns = decoded } private func saveCheckIns() { if let data = try? JSONEncoder().encode(checkIns) { UserDefaults.standard.set(data, forKey: storageKey) } } } // MARK: - Models enum EnergyLevel: String, Codable, CaseIterable, Identifiable { case exhausted = "exhausted" case low = "low" case okay = "okay" case energized = "energized" var id: String { rawValue } var emoji: String { switch self { case .exhausted: return "😴" case .low: return "😐" case .okay: return "😊" case .energized: return "🔥" } } var label: String { switch self { case .exhausted: return "Exhausted" case .low: return "Low" case .okay: return "Good" case .energized: return "Energized" } } var numericValue: Int { switch self { case .exhausted: return 1 case .low: return 2 case .okay: return 3 case .energized: return 4 } } } enum CheckInContext: String, Codable { case postTimer = "post_timer" case morning = "morning" case postLunch = "post_lunch" case evening = "evening" case manual = "manual" } struct MoodCheckIn: Codable, Identifiable { let id: String let energy: EnergyLevel let timerId: String? let timerLabel: String? let context: CheckInContext let timestamp: Date let hourOfDay: Int let dayOfWeek: Int init(energy: EnergyLevel, timerId: String?, timerLabel: String?, context: CheckInContext, timestamp: Date, hourOfDay: Int, dayOfWeek: Int) { self.id = UUID().uuidString self.energy = energy self.timerId = timerId self.timerLabel = timerLabel self.context = context self.timestamp = timestamp self.hourOfDay = hourOfDay self.dayOfWeek = dayOfWeek } } struct WeeklyMoodInsight { let checkInCount: Int let averageEnergy: Double let peakHours: [Int] let bestDayOfWeek: Int? let distribution: [EnergyLevel: Int] let insightText: String var averageEnergyLabel: String { switch averageEnergy { case 3.5...: return "High" case 2.5..<3.5: return "Moderate" case 1.5..<2.5: return "Low" default: return "Very Low" } } }