// // InsightsViewModel.swift // Feels // // Created by Claude Code on 12/9/24. // import Foundation import SwiftUI struct Insight: Identifiable { let id = UUID() let icon: String let title: String let description: String let mood: Mood? } @MainActor class InsightsViewModel: ObservableObject { @Published var monthInsights: [Insight] = [] @Published var yearInsights: [Insight] = [] @Published var allTimeInsights: [Insight] = [] private let calendar = Calendar.current init() { generateInsights() } func generateInsights() { let now = Date() // Get date ranges let monthStart = calendar.date(from: calendar.dateComponents([.year, .month], from: now))! let yearStart = calendar.date(from: calendar.dateComponents([.year], from: now))! let allTimeStart = Date(timeIntervalSince1970: 0) // Fetch entries for each period let monthEntries = DataController.shared.getData(startDate: monthStart, endDate: now, includedDays: [1,2,3,4,5,6,7]) let yearEntries = DataController.shared.getData(startDate: yearStart, endDate: now, includedDays: [1,2,3,4,5,6,7]) let allTimeEntries = DataController.shared.getData(startDate: allTimeStart, endDate: now, includedDays: [1,2,3,4,5,6,7]) // Generate insights for each period monthInsights = generateRandomInsights(entries: monthEntries, periodName: "this month", count: 5) yearInsights = generateRandomInsights(entries: yearEntries, periodName: "this year", count: 5) allTimeInsights = generateRandomInsights(entries: allTimeEntries, periodName: "all time", count: 5) } private func generateRandomInsights(entries: [MoodEntryModel], periodName: String, count: Int) -> [Insight] { // Filter out missing/placeholder entries let validEntries = entries.filter { ![.missing, .placeholder].contains($0.mood) } guard !validEntries.isEmpty else { return [Insight(icon: "questionmark.circle", title: "No Data Yet", description: "Start logging your moods to see insights for \(periodName).", mood: nil)] } // Generate all possible insights var allInsights: [Insight] = [] // Add all insight generators allInsights.append(contentsOf: generateMostCommonMoodInsights(entries: validEntries, periodName: periodName)) allInsights.append(contentsOf: generateLeastCommonMoodInsights(entries: validEntries, periodName: periodName)) allInsights.append(contentsOf: generateBestDayInsights(entries: validEntries, periodName: periodName)) allInsights.append(contentsOf: generateWorstDayInsights(entries: validEntries, periodName: periodName)) allInsights.append(contentsOf: generateStreakInsights(entries: validEntries, periodName: periodName)) allInsights.append(contentsOf: generateTrendInsights(entries: validEntries, periodName: periodName)) allInsights.append(contentsOf: generatePositivityInsights(entries: validEntries, periodName: periodName)) allInsights.append(contentsOf: generateWeekendVsWeekdayInsights(entries: validEntries, periodName: periodName)) allInsights.append(contentsOf: generateMoodSwingInsights(entries: validEntries, periodName: periodName)) allInsights.append(contentsOf: generateConsistencyInsights(entries: validEntries, periodName: periodName)) allInsights.append(contentsOf: generateMilestoneInsights(entries: validEntries, periodName: periodName)) allInsights.append(contentsOf: generateComparativeInsights(entries: validEntries, periodName: periodName)) allInsights.append(contentsOf: generatePatternInsights(entries: validEntries, periodName: periodName)) allInsights.append(contentsOf: generateFunFactInsights(entries: validEntries, periodName: periodName)) allInsights.append(contentsOf: generateMotivationalInsights(entries: validEntries, periodName: periodName)) allInsights.append(contentsOf: generateRareMoodInsights(entries: validEntries, periodName: periodName)) allInsights.append(contentsOf: generateRecentInsights(entries: validEntries, periodName: periodName)) allInsights.append(contentsOf: generateMonthOfYearInsights(entries: validEntries, periodName: periodName)) allInsights.append(contentsOf: generateLongestMoodRunInsights(entries: validEntries, periodName: periodName)) allInsights.append(contentsOf: generateAverageMoodInsights(entries: validEntries, periodName: periodName)) // Shuffle and pick random insights allInsights.shuffle() return Array(allInsights.prefix(count)) } // MARK: - Most Common Mood private func generateMostCommonMoodInsights(entries: [MoodEntryModel], periodName: String) -> [Insight] { var insights: [Insight] = [] let moodCounts = Dictionary(grouping: entries, by: { $0.mood }).mapValues { $0.count } if let mostCommon = moodCounts.max(by: { $0.value < $1.value }) { let percentage = Int((Float(mostCommon.value) / Float(entries.count)) * 100) let variations = [ "You felt \(mostCommon.key.strValue.lowercased()) \(percentage)% of the time \(periodName).", "\(mostCommon.key.strValue) is your go-to mood \(periodName) with \(mostCommon.value) days.", "Your vibe \(periodName)? Definitely \(mostCommon.key.strValue.lowercased()) (\(percentage)%).", "\(mostCommon.value) days of feeling \(mostCommon.key.strValue.lowercased()) \(periodName)!", ] insights.append(Insight( icon: "star.fill", title: "Dominant Mood", description: variations.randomElement()!, mood: mostCommon.key )) if percentage > 50 { insights.append(Insight( icon: "chart.pie.fill", title: "Mood Majority", description: "More than half your days \(periodName) were \(mostCommon.key.strValue.lowercased())!", mood: mostCommon.key )) } } return insights } // MARK: - Least Common Mood private func generateLeastCommonMoodInsights(entries: [MoodEntryModel], periodName: String) -> [Insight] { var insights: [Insight] = [] let moodCounts = Dictionary(grouping: entries, by: { $0.mood }).mapValues { $0.count } if let leastCommon = moodCounts.filter({ $0.value > 0 }).min(by: { $0.value < $1.value }) { let variations = [ "You rarely felt \(leastCommon.key.strValue.lowercased()) \(periodName) (only \(leastCommon.value) days).", "\(leastCommon.key.strValue) was your rarest mood \(periodName).", "Only \(leastCommon.value) \(leastCommon.key.strValue.lowercased()) days \(periodName) - that's rare!", ] insights.append(Insight( icon: "sparkle", title: "Rare Mood", description: variations.randomElement()!, mood: leastCommon.key )) } return insights } // MARK: - Best Day of Week private func generateBestDayInsights(entries: [MoodEntryModel], periodName: String) -> [Insight] { var insights: [Insight] = [] let weekdayCounts = Dictionary(grouping: entries, by: { Int($0.weekDay) }) var weekdayScores: [Int: Float] = [:] for (weekday, dayEntries) in weekdayCounts { let avgScore = Float(dayEntries.reduce(0) { $0 + Int($1.moodValue) }) / Float(dayEntries.count) weekdayScores[weekday] = avgScore } if let bestWeekday = weekdayScores.max(by: { $0.value < $1.value }) { let weekdayName = weekdayNameFrom(weekday: bestWeekday.key) let avgMood = moodFromScore(bestWeekday.value) let variations = [ "\(weekdayName)s are your happiest days \(periodName)!", "Good vibes on \(weekdayName)s - your best day \(periodName).", "You shine brightest on \(weekdayName)s!", "\(weekdayName) = your power day \(periodName).", ] insights.append(Insight( icon: "calendar.badge.checkmark", title: "Best Day", description: variations.randomElement()!, mood: avgMood )) } return insights } // MARK: - Worst Day of Week private func generateWorstDayInsights(entries: [MoodEntryModel], periodName: String) -> [Insight] { var insights: [Insight] = [] let weekdayCounts = Dictionary(grouping: entries, by: { Int($0.weekDay) }) var weekdayScores: [Int: Float] = [:] for (weekday, dayEntries) in weekdayCounts { let avgScore = Float(dayEntries.reduce(0) { $0 + Int($1.moodValue) }) / Float(dayEntries.count) weekdayScores[weekday] = avgScore } if let worstWeekday = weekdayScores.min(by: { $0.value < $1.value }) { let weekdayName = weekdayNameFrom(weekday: worstWeekday.key) let avgMood = moodFromScore(worstWeekday.value) let variations = [ "\(weekdayName)s tend to be tougher for you \(periodName).", "Watch out for \(weekdayName)s - your challenging day.", "\(weekdayName) blues are real for you \(periodName).", ] insights.append(Insight( icon: "calendar.badge.exclamationmark", title: "Challenging Day", description: variations.randomElement()!, mood: avgMood )) } return insights } // MARK: - Streak Insights private func generateStreakInsights(entries: [MoodEntryModel], periodName: String) -> [Insight] { var insights: [Insight] = [] let streakInfo = calculateStreaks(entries: entries) if streakInfo.currentStreak > 0 { let variations = [ "You've logged \(streakInfo.currentStreak) days in a row! Keep it up!", "\(streakInfo.currentStreak)-day streak and counting!", "On fire! \(streakInfo.currentStreak) consecutive days logged.", ] insights.append(Insight( icon: "flame.fill", title: "Current Streak", description: variations.randomElement()!, mood: .great )) } if streakInfo.longestStreak > 1 { insights.append(Insight( icon: "trophy.fill", title: "Longest Streak", description: "Your record is \(streakInfo.longestStreak) consecutive days \(periodName)!", mood: nil )) } if streakInfo.currentStreak == 0 { insights.append(Insight( icon: "arrow.clockwise", title: "Start Fresh", description: "Log today to start a new streak!", mood: nil )) } // Good mood streaks let goodStreaks = calculateMoodStreaks(entries: entries, moods: [.great, .good]) if goodStreaks.longest >= 3 { insights.append(Insight( icon: "sun.max.fill", title: "Happiness Streak", description: "Your longest good/great streak \(periodName) was \(goodStreaks.longest) days!", mood: .great )) } // Recovery insight let badStreaks = calculateMoodStreaks(entries: entries, moods: [.horrible, .bad]) if badStreaks.longest >= 2 && goodStreaks.longest >= 2 { insights.append(Insight( icon: "arrow.up.heart.fill", title: "Resilient", description: "You bounced back from tough times \(periodName). That takes strength!", mood: .good )) } return insights } // MARK: - Trend Insights private func generateTrendInsights(entries: [MoodEntryModel], periodName: String) -> [Insight] { var insights: [Insight] = [] guard entries.count >= 4 else { return insights } let sortedEntries = entries.sorted { $0.forDate < $1.forDate } let halfCount = sortedEntries.count / 2 let firstHalf = Array(sortedEntries.prefix(halfCount)) let secondHalf = Array(sortedEntries.suffix(halfCount)) let firstAvg = Float(firstHalf.reduce(0) { $0 + Int($1.moodValue) }) / Float(firstHalf.count) let secondAvg = Float(secondHalf.reduce(0) { $0 + Int($1.moodValue) }) / Float(secondHalf.count) let diff = secondAvg - firstAvg if diff > 0.5 { let variations = [ "Your mood has been improving \(periodName)!", "Things are looking up! Positive trend \(periodName).", "You're on an upswing \(periodName)!", "Rising vibes detected \(periodName)!", ] insights.append(Insight( icon: "chart.line.uptrend.xyaxis", title: "Upward Trend", description: variations.randomElement()!, mood: .great )) } else if diff < -0.5 { let variations = [ "Your mood has dipped recently. Hang in there!", "Tough stretch lately. Better days ahead!", "You've been through it \(periodName). Stay strong.", ] insights.append(Insight( icon: "chart.line.downtrend.xyaxis", title: "Downward Trend", description: variations.randomElement()!, mood: .bad )) } else { let variations = [ "Your mood has been steady \(periodName).", "Consistent vibes \(periodName) - stability is good!", "Even keel \(periodName). That's balance!", ] insights.append(Insight( icon: "chart.line.flattrend.xyaxis", title: "Stable Mood", description: variations.randomElement()!, mood: .average )) } return insights } // MARK: - Positivity Insights private func generatePositivityInsights(entries: [MoodEntryModel], periodName: String) -> [Insight] { var insights: [Insight] = [] let positiveDays = entries.filter { [.great, .good].contains($0.mood) }.count let negativeDays = entries.filter { [.horrible, .bad].contains($0.mood) }.count let positiveRatio = Int((Float(positiveDays) / Float(entries.count)) * 100) let negativeRatio = Int((Float(negativeDays) / Float(entries.count)) * 100) if positiveRatio >= 80 { insights.append(Insight( icon: "sparkles", title: "Crushing It", description: "\(positiveRatio)% positive days \(periodName)! You're absolutely killing it!", mood: .great )) } else if positiveRatio >= 60 { insights.append(Insight( icon: "sun.max.fill", title: "Positive Outlook", description: "\(positiveRatio)% of your days were positive \(periodName). Nice!", mood: .good )) } else if positiveRatio >= 40 { insights.append(Insight( icon: "sun.haze.fill", title: "Mixed Bag", description: "About \(positiveRatio)% positive days \(periodName). Life's a balance!", mood: .average )) } if negativeRatio >= 30 { insights.append(Insight( icon: "cloud.rain.fill", title: "Tough Times", description: "\(negativeRatio)% challenging days \(periodName). You're getting through it.", mood: .bad )) } if negativeDays == 0 && entries.count >= 5 { insights.append(Insight( icon: "star.circle.fill", title: "No Bad Days", description: "Zero bad or horrible days \(periodName)! That's amazing!", mood: .great )) } let greatDays = entries.filter { $0.mood == .great }.count if greatDays >= 5 { insights.append(Insight( icon: "hands.clap.fill", title: "Great Days Club", description: "You had \(greatDays) great days \(periodName)!", mood: .great )) } return insights } // MARK: - Weekend vs Weekday private func generateWeekendVsWeekdayInsights(entries: [MoodEntryModel], periodName: String) -> [Insight] { var insights: [Insight] = [] let weekendDays = entries.filter { [1, 7].contains(Int($0.weekDay)) } // Sunday = 1, Saturday = 7 let weekdayEntries = entries.filter { ![1, 7].contains(Int($0.weekDay)) } guard !weekendDays.isEmpty && !weekdayEntries.isEmpty else { return insights } let weekendAvg = Float(weekendDays.reduce(0) { $0 + Int($1.moodValue) }) / Float(weekendDays.count) let weekdayAvg = Float(weekdayEntries.reduce(0) { $0 + Int($1.moodValue) }) / Float(weekdayEntries.count) if weekendAvg > weekdayAvg + 0.5 { insights.append(Insight( icon: "sun.horizon.fill", title: "Weekend Warrior", description: "You're happier on weekends \(periodName). Living for the weekend!", mood: .great )) } else if weekdayAvg > weekendAvg + 0.5 { insights.append(Insight( icon: "briefcase.fill", title: "Weekday Wonder", description: "You're actually happier on weekdays \(periodName)! Work fulfills you.", mood: .good )) } else { insights.append(Insight( icon: "equal.circle.fill", title: "Balanced Week", description: "Weekends and weekdays feel about the same \(periodName).", mood: .average )) } return insights } // MARK: - Mood Swing Insights private func generateMoodSwingInsights(entries: [MoodEntryModel], periodName: String) -> [Insight] { var insights: [Insight] = [] guard entries.count >= 3 else { return insights } let sortedEntries = entries.sorted { $0.forDate < $1.forDate } var swings = 0 for i in 1..= 2 { swings += 1 } } let swingRate = Float(swings) / Float(entries.count - 1) if swingRate > 0.4 { insights.append(Insight( icon: "waveform.path", title: "Emotional Rollercoaster", description: "Your moods fluctuated a lot \(periodName). Quite a ride!", mood: nil )) } else if swingRate < 0.1 && entries.count >= 7 { insights.append(Insight( icon: "minus.circle.fill", title: "Steady Eddie", description: "Very few mood swings \(periodName). You're emotionally steady!", mood: .good )) } // Big jump detection for i in 1..= 3 { insights.append(Insight( icon: "arrow.up.forward.circle.fill", title: "Mood Boost", description: "You had a big mood jump from \(prev.mood.strValue.lowercased()) to \(curr.mood.strValue.lowercased()) \(periodName)!", mood: curr.mood )) break } } return insights } // MARK: - Consistency Insights private func generateConsistencyInsights(entries: [MoodEntryModel], periodName: String) -> [Insight] { var insights: [Insight] = [] let totalDaysInPeriod = calculateDaysInPeriod(periodName: periodName) let loggedDays = entries.count let completionRate = totalDaysInPeriod > 0 ? Int((Float(loggedDays) / Float(totalDaysInPeriod)) * 100) : 0 if completionRate >= 90 { insights.append(Insight( icon: "checkmark.seal.fill", title: "Logging Pro", description: "You logged \(completionRate)% of days \(periodName). Incredible dedication!", mood: .great )) } else if completionRate >= 70 { insights.append(Insight( icon: "checkmark.circle.fill", title: "Consistent Logger", description: "\(completionRate)% logging rate \(periodName). Keep it up!", mood: .good )) } else if completionRate >= 50 { insights.append(Insight( icon: "circle.dotted", title: "Getting There", description: "You've logged about half the days \(periodName). Room to grow!", mood: .average )) } if loggedDays >= 30 { insights.append(Insight( icon: "30.circle.fill", title: "30 Day Club", description: "You've logged at least 30 days \(periodName)!", mood: .great )) } if loggedDays >= 100 { insights.append(Insight( icon: "100.circle.fill", title: "Century Logger", description: "\(loggedDays) days logged \(periodName)! That's commitment!", mood: .great )) } return insights } // MARK: - Milestone Insights private func generateMilestoneInsights(entries: [MoodEntryModel], periodName: String) -> [Insight] { var insights: [Insight] = [] let count = entries.count let milestones = [7, 14, 21, 30, 50, 75, 100, 150, 200, 250, 300, 365, 500, 730, 1000] for milestone in milestones { if count >= milestone && count < milestone + 10 { insights.append(Insight( icon: "flag.fill", title: "\(milestone) Days!", description: "You've hit \(milestone) logged days \(periodName)! Milestone achieved!", mood: .great )) break } } return insights } // MARK: - Comparative Insights private func generateComparativeInsights(entries: [MoodEntryModel], periodName: String) -> [Insight] { var insights: [Insight] = [] let moodCounts = Dictionary(grouping: entries, by: { $0.mood }).mapValues { $0.count } let greatCount = moodCounts[.great] ?? 0 let goodCount = moodCounts[.good] ?? 0 let averageCount = moodCounts[.average] ?? 0 let badCount = moodCounts[.bad] ?? 0 let horribleCount = moodCounts[.horrible] ?? 0 if greatCount > goodCount + averageCount + badCount + horribleCount { insights.append(Insight( icon: "crown.fill", title: "Great Majority", description: "More great days than all other moods combined \(periodName)!", mood: .great )) } if greatCount > 0 && horribleCount > 0 { let ratio = Float(greatCount) / Float(horribleCount) if ratio >= 5 { insights.append(Insight( icon: "scale.3d", title: "Positive Ratio", description: "\(Int(ratio))x more great days than horrible days \(periodName)!", mood: .great )) } } if goodCount + greatCount > badCount + horribleCount { let positive = goodCount + greatCount let negative = badCount + horribleCount if negative > 0 { insights.append(Insight( icon: "hand.thumbsup.fill", title: "Net Positive", description: "\(positive) positive days vs \(negative) negative days \(periodName).", mood: .good )) } } return insights } // MARK: - Pattern Insights private func generatePatternInsights(entries: [MoodEntryModel], periodName: String) -> [Insight] { var insights: [Insight] = [] // Monday blues check let mondays = entries.filter { Int($0.weekDay) == 2 } if !mondays.isEmpty { let mondayAvg = Float(mondays.reduce(0) { $0 + Int($1.moodValue) }) / Float(mondays.count) if mondayAvg < 2 { insights.append(Insight( icon: "moon.zzz.fill", title: "Monday Blues", description: "Mondays are rough for you \(periodName). Hang in there!", mood: .bad )) } else if mondayAvg >= 3.5 { insights.append(Insight( icon: "sun.and.horizon.fill", title: "Monday Motivation", description: "You actually like Mondays! Starting the week strong \(periodName).", mood: .good )) } } // Friday feels let fridays = entries.filter { Int($0.weekDay) == 6 } if !fridays.isEmpty { let fridayAvg = Float(fridays.reduce(0) { $0 + Int($1.moodValue) }) / Float(fridays.count) if fridayAvg >= 3.5 { insights.append(Insight( icon: "party.popper.fill", title: "TGIF Energy", description: "Fridays are your happy place \(periodName)!", mood: .great )) } } // Hump day let wednesdays = entries.filter { Int($0.weekDay) == 4 } if !wednesdays.isEmpty { let wedAvg = Float(wednesdays.reduce(0) { $0 + Int($1.moodValue) }) / Float(wednesdays.count) if wedAvg >= 3 { insights.append(Insight( icon: "figure.walk", title: "Hump Day Hero", description: "You crush Wednesdays \(periodName). Midweek champion!", mood: .good )) } } return insights } // MARK: - Fun Fact Insights private func generateFunFactInsights(entries: [MoodEntryModel], periodName: String) -> [Insight] { var insights: [Insight] = [] let greatDays = entries.filter { $0.mood == .great }.count let goodDays = entries.filter { $0.mood == .good }.count let averageDays = entries.filter { $0.mood == .average }.count if greatDays >= 7 { insights.append(Insight( icon: "face.smiling.inverse", title: "Week of Great", description: "You've had at least a full week's worth of great days \(periodName)!", mood: .great )) } if averageDays >= entries.count / 2 && entries.count >= 10 { insights.append(Insight( icon: "figure.stand", title: "The Average Joe", description: "Most of your days \(periodName) were... average. And that's okay!", mood: .average )) } // Calculate "great day streak potential" let recentEntries = entries.sorted { $0.forDate > $1.forDate }.prefix(7) let recentGreat = recentEntries.filter { $0.mood == .great }.count if recentGreat >= 3 { insights.append(Insight( icon: "bolt.fill", title: "On a Roll", description: "\(recentGreat) great days in your last 7 entries! You're vibing!", mood: .great )) } if goodDays + greatDays >= 20 { insights.append(Insight( icon: "heart.fill", title: "20+ Happy Days", description: "Over 20 good or great days \(periodName). Life is good!", mood: .great )) } return insights } // MARK: - Motivational Insights private func generateMotivationalInsights(entries: [MoodEntryModel], periodName: String) -> [Insight] { var insights: [Insight] = [] let avgMood = Float(entries.reduce(0) { $0 + Int($1.moodValue) }) / Float(entries.count) if avgMood >= 3.5 { let messages = [ "You're doing amazing! Keep spreading those good vibes.", "Your positivity \(periodName) is inspiring!", "Whatever you're doing, keep doing it!", ] insights.append(Insight( icon: "hands.sparkles.fill", title: "Keep Shining", description: messages.randomElement()!, mood: .great )) } else if avgMood >= 2.5 { let messages = [ "You're handling life well \(periodName). Stay balanced!", "Steady progress \(periodName). You've got this!", "Balance is key, and you're finding it.", ] insights.append(Insight( icon: "figure.mind.and.body", title: "Stay Centered", description: messages.randomElement()!, mood: .good )) } else { let messages = [ "Tough times don't last, but tough people do.", "Every day is a new chance. You've got this!", "Tracking is the first step to understanding. Keep going.", "Better days are coming. Believe in yourself!", ] insights.append(Insight( icon: "heart.circle.fill", title: "Hang In There", description: messages.randomElement()!, mood: .average )) } return insights } // MARK: - Rare Mood Insights private func generateRareMoodInsights(entries: [MoodEntryModel], periodName: String) -> [Insight] { var insights: [Insight] = [] let moodCounts = Dictionary(grouping: entries, by: { $0.mood }).mapValues { $0.count } // Check for missing moods let allMoods: [Mood] = [.great, .good, .average, .bad, .horrible] let missingMoods = allMoods.filter { moodCounts[$0] == nil || moodCounts[$0] == 0 } if missingMoods.contains(.horrible) && entries.count >= 10 { insights.append(Insight( icon: "xmark.shield.fill", title: "No Horrible Days", description: "Not a single horrible day \(periodName)! That's worth celebrating!", mood: .great )) } if missingMoods.contains(.bad) && entries.count >= 10 { insights.append(Insight( icon: "shield.fill", title: "Bad-Free Zone", description: "Zero bad days \(periodName). You're in a good place!", mood: .great )) } if missingMoods.count == 0 && entries.count >= 10 { insights.append(Insight( icon: "paintpalette.fill", title: "Full Spectrum", description: "You've experienced every mood \(periodName). That's being human!", mood: nil )) } return insights } // MARK: - Recent Insights private func generateRecentInsights(entries: [MoodEntryModel], periodName: String) -> [Insight] { var insights: [Insight] = [] let sortedEntries = entries.sorted { $0.forDate > $1.forDate } guard let mostRecent = sortedEntries.first else { return insights } let daysSinceLastEntry = calendar.dateComponents([.day], from: mostRecent.forDate, to: Date()).day ?? 0 if daysSinceLastEntry == 0 { insights.append(Insight( icon: "clock.badge.checkmark.fill", title: "Logged Today", description: "You already logged today! Great job staying on top of it.", mood: mostRecent.mood )) } else if daysSinceLastEntry == 1 { insights.append(Insight( icon: "clock.fill", title: "Yesterday's Log", description: "Last entry was yesterday. Don't forget today!", mood: nil )) } else if daysSinceLastEntry <= 3 { insights.append(Insight( icon: "clock.arrow.circlepath", title: "Recent Logger", description: "Last entry was \(daysSinceLastEntry) days ago. Time to catch up!", mood: nil )) } // Last 3 entries mood if sortedEntries.count >= 3 { let last3 = Array(sortedEntries.prefix(3)) let last3Avg = Float(last3.reduce(0) { $0 + Int($1.moodValue) }) / 3.0 let avgMood = moodFromScore(last3Avg) if last3Avg >= 3.5 { insights.append(Insight( icon: "arrow.up.circle.fill", title: "Recent High", description: "Your last 3 entries have been great! Momentum is building.", mood: avgMood )) } else if last3Avg <= 1.5 { insights.append(Insight( icon: "arrow.down.circle.fill", title: "Recent Low", description: "Last few entries have been tough. Tomorrow's a new day!", mood: avgMood )) } } return insights } // MARK: - Month of Year Insights private func generateMonthOfYearInsights(entries: [MoodEntryModel], periodName: String) -> [Insight] { var insights: [Insight] = [] guard periodName == "this year" || periodName == "all time" else { return insights } let monthGroups = Dictionary(grouping: entries) { entry -> Int in calendar.component(.month, from: entry.forDate) } var monthScores: [Int: Float] = [:] for (month, monthEntries) in monthGroups { if monthEntries.count >= 3 { let avgScore = Float(monthEntries.reduce(0) { $0 + Int($1.moodValue) }) / Float(monthEntries.count) monthScores[month] = avgScore } } if let bestMonth = monthScores.max(by: { $0.value < $1.value }) { let monthName = DateFormatter().monthSymbols[bestMonth.key - 1] insights.append(Insight( icon: "calendar.circle.fill", title: "Best Month", description: "\(monthName) is your happiest month \(periodName)!", mood: moodFromScore(bestMonth.value) )) } if let worstMonth = monthScores.min(by: { $0.value < $1.value }) { let monthName = DateFormatter().monthSymbols[worstMonth.key - 1] insights.append(Insight( icon: "calendar.badge.minus", title: "Challenging Month", description: "\(monthName) tends to be tougher for you \(periodName).", mood: moodFromScore(worstMonth.value) )) } return insights } // MARK: - Longest Mood Run private func generateLongestMoodRunInsights(entries: [MoodEntryModel], periodName: String) -> [Insight] { var insights: [Insight] = [] for mood in [Mood.great, .good, .average, .bad, .horrible] { let streaks = calculateMoodStreaks(entries: entries, moods: [mood]) if streaks.longest >= 3 { let adjective: String switch mood { case .great: adjective = "amazing" case .good: adjective = "solid" case .average: adjective = "steady" case .bad: adjective = "rough" case .horrible: adjective = "tough" default: adjective = "" } insights.append(Insight( icon: "repeat.circle.fill", title: "\(mood.strValue) Streak", description: "Longest \(mood.strValue.lowercased()) streak \(periodName): \(streaks.longest) \(adjective) days in a row.", mood: mood )) } } return insights } // MARK: - Average Mood Insights private func generateAverageMoodInsights(entries: [MoodEntryModel], periodName: String) -> [Insight] { var insights: [Insight] = [] let avgMood = Float(entries.reduce(0) { $0 + Int($1.moodValue) }) / Float(entries.count) let roundedAvg = moodFromScore(avgMood) let avgDescriptions: [Mood: [String]] = [ .great: [ "Your average mood \(periodName) is great! Living your best life.", "On average, you're feeling great \(periodName). Amazing!", ], .good: [ "Your average mood \(periodName) is good. Solid vibes!", "Generally good vibes \(periodName). Keep it up!", ], .average: [ "Your average mood \(periodName) is... average. Balanced!", "Right in the middle \(periodName). That's life!", ], .bad: [ "Your average mood \(periodName) has been lower. Hang in there.", "Tougher times \(periodName), but tracking helps.", ], .horrible: [ "It's been a hard \(periodName). You're brave for tracking.", "Rough \(periodName). Remember: this too shall pass.", ], ] if let descriptions = avgDescriptions[roundedAvg] { insights.append(Insight( icon: "number.circle.fill", title: "Average Mood", description: descriptions.randomElement()!, mood: roundedAvg )) } // Mood score as a number let score = String(format: "%.1f", avgMood) insights.append(Insight( icon: "gauge.with.needle.fill", title: "Mood Score", description: "Your mood score \(periodName): \(score) out of 4.0", mood: roundedAvg )) return insights } // MARK: - Helper Functions private func calculateStreaks(entries: [MoodEntryModel]) -> (currentStreak: Int, longestStreak: Int) { let sortedEntries = entries.sorted { $0.forDate > $1.forDate } guard !sortedEntries.isEmpty else { return (0, 0) } var currentStreak = 0 var longestStreak = 0 var tempStreak = 1 let today = calendar.startOfDay(for: Date()) let mostRecent = sortedEntries.first!.forDate if calendar.isDate(mostRecent, inSameDayAs: today) || calendar.isDate(mostRecent, inSameDayAs: calendar.date(byAdding: .day, value: -1, to: today)!) { currentStreak = 1 var checkDate = calendar.date(byAdding: .day, value: -1, to: mostRecent)! for entry in sortedEntries.dropFirst() { let entryDate = entry.forDate if calendar.isDate(entryDate, inSameDayAs: checkDate) { currentStreak += 1 checkDate = calendar.date(byAdding: .day, value: -1, to: checkDate)! } else { break } } } for i in 1.. (current: Int, longest: Int) { let sortedEntries = entries.sorted { $0.forDate < $1.forDate } guard !sortedEntries.isEmpty else { return (0, 0) } var currentStreak = 0 var longestStreak = 0 var tempStreak = 0 for entry in sortedEntries { if moods.contains(entry.mood) { tempStreak += 1 longestStreak = max(longestStreak, tempStreak) } else { tempStreak = 0 } } // Check if current streak is at the end let reversedEntries = sortedEntries.reversed() for entry in reversedEntries { if moods.contains(entry.mood) { currentStreak += 1 } else { break } } return (currentStreak, longestStreak) } private func calculateDaysInPeriod(periodName: String) -> Int { let now = Date() switch periodName { case "this month": let monthStart = calendar.date(from: calendar.dateComponents([.year, .month], from: now))! return calendar.dateComponents([.day], from: monthStart, to: now).day ?? 0 + 1 case "this year": let yearStart = calendar.date(from: calendar.dateComponents([.year], from: now))! return calendar.dateComponents([.day], from: yearStart, to: now).day ?? 0 + 1 default: return 0 } } private func weekdayNameFrom(weekday: Int) -> String { let weekdays = ["", "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"] return weekdays[min(max(weekday, 1), 7)] } private func moodFromScore(_ score: Float) -> Mood { switch score { case 3.5...: return .great case 2.5..<3.5: return .good case 1.5..<2.5: return .average case 0.5..<1.5: return .bad default: return .horrible } } }