import Foundation
import Observation
import SwiftData
import Bedrock

@MainActor
@Observable
final class RitualStore: RitualStoreProviding {
    @ObservationIgnored private let modelContext: ModelContext
    @ObservationIgnored private let seedService: RitualSeedProviding
    @ObservationIgnored private let settingsStore: SettingsStore
    @ObservationIgnored private let calendar: Calendar
    @ObservationIgnored private let dayFormatter: DateFormatter
    @ObservationIgnored private let displayFormatter: DateFormatter

    private(set) var rituals: [Ritual] = []
    private(set) var currentRituals: [Ritual] = []
    private(set) var pastRituals: [Ritual] = []
    private(set) var lastErrorMessage: String?
    private var analyticsNeedsRefresh = true
    private var cachedDatesWithActivity: Set<Date> = []
    private var cachedPerfectDayIDs: Set<String> = []
    private var pendingReminderTask: Task<Void, Never>?
    private var insightCardsNeedRefresh = true
    private var cachedInsightCards: [InsightCard] = []
    
    /// Reminder scheduler for time-slot based notifications
    let reminderScheduler = ReminderScheduler()
    
    /// Ritual that needs renewal prompt (arc just completed)
    var ritualNeedingRenewal: Ritual?

    init(
        modelContext: ModelContext,
        seedService: RitualSeedProviding,
        settingsStore: SettingsStore,
        calendar: Calendar = .current
    ) {
        self.modelContext = modelContext
        self.seedService = seedService
        self.settingsStore = settingsStore
        self.calendar = calendar
        self.dayFormatter = DateFormatter()
        self.displayFormatter = DateFormatter()
        dayFormatter.calendar = calendar
        dayFormatter.dateFormat = "yyyy-MM-dd"
        displayFormatter.calendar = calendar
        displayFormatter.dateStyle = .full
        displayFormatter.timeStyle = .none
        loadRitualsIfNeeded()
    }

    var activeRitual: Ritual? {
        // Return the first ritual with an active arc that covers today
        currentRituals.first { ritual in
            guard let arc = ritual.currentArc else { return false }
            return arc.contains(date: Date())
        }
    }

    var todayDisplayString: String {
        displayFormatter.string(from: Date())
    }

    var activeRitualProgress: Double {
        guard let ritual = activeRitual else { return 0 }
        let habits = ritual.habits
        guard !habits.isEmpty else { return 0 }
        let completed = habits.filter { isHabitCompletedToday($0) }.count
        return Double(completed) / Double(habits.count)
    }

    /// Refreshes rituals and derived state for current date/time.
    func refresh() {
        reloadRituals()
        checkForCompletedArcs()
    }

    func ritualProgress(for ritual: Ritual) -> Double {
        let habits = ritual.habits
        guard !habits.isEmpty else { return 0 }
        let completed = habits.filter { isHabitCompletedToday($0) }.count
        return Double(completed) / Double(habits.count)
    }

    func habits(for ritual: Ritual) -> [ArcHabit] {
        ritual.habits
    }

    func isHabitCompletedToday(_ habit: ArcHabit) -> Bool {
        let dayID = dayIdentifier(for: Date())
        return habit.completedDayIDs.contains(dayID)
    }

    func toggleHabitCompletion(_ habit: ArcHabit) {
        let dayID = dayIdentifier(for: Date())
        let wasCompleted = habit.completedDayIDs.contains(dayID)
        
        if wasCompleted {
            habit.completedDayIDs.removeAll { $0 == dayID }
        } else {
            habit.completedDayIDs.append(dayID)
            // Play feedback on check-in (not on uncheck)
            if settingsStore.hapticsEnabled {
                SoundManager.shared.playHaptic(.success)
            }
            if settingsStore.soundEnabled {
                SoundManager.shared.playSystemSound(SystemSound.success)
            }
        }
        saveContext()
    }

    func ritualDayIndex(for ritual: Ritual) -> Int {
        guard let arc = ritual.currentArc else { return 0 }
        return arc.dayIndex(for: Date())
    }

    func ritualDayLabel(for ritual: Ritual) -> String {
        let format = String(localized: "Day %lld of %lld")
        return String.localizedStringWithFormat(
            format,
            ritualDayIndex(for: ritual),
            ritual.durationDays
        )
    }

    func completionSummary(for ritual: Ritual) -> String {
        let completed = ritual.habits.filter { isHabitCompletedToday($0) }.count
        let format = String(localized: "%lld of %lld habits complete")
        return String.localizedStringWithFormat(
            format,
            completed,
            ritual.habits.count
        )
    }

    // MARK: - Ritual Management
    
    /// Returns rituals appropriate for the current time of day that have active arcs covering today.
    /// Filters based on time of day: morning (before 11am), midday (11am-2pm), afternoon (2pm-5pm),
    /// evening (5pm-9pm), night (after 9pm). Anytime rituals are always shown.
    func ritualsForToday() -> [Ritual] {
        let hour = calendar.component(.hour, from: Date())
        let currentPeriod: TimeOfDay = {
            switch hour {
            case 0..<11: return .morning
            case 11..<14: return .midday
            case 14..<17: return .afternoon
            case 17..<21: return .evening
            default: return .night
            }
        }()
        
        return currentRituals.filter { ritual in
            guard let arc = ritual.currentArc, arc.contains(date: Date()) else { return false }
            return ritual.timeOfDay == .anytime || ritual.timeOfDay == currentPeriod
        }
    }
    
    /// Groups current rituals by time of day for display
    func currentRitualsGroupedByTime() -> [(timeOfDay: TimeOfDay, rituals: [Ritual])] {
        let grouped = Dictionary(grouping: currentRituals) { $0.timeOfDay }
        return TimeOfDay.allCases
            .compactMap { time in
                guard let rituals = grouped[time], !rituals.isEmpty else { return nil }
                return (timeOfDay: time, rituals: rituals)
            }
    }

    // MARK: - Arc Management
    
    /// Returns all arcs that were active on a specific date.
    func arcsActive(on date: Date) -> [RitualArc] {
        rituals.flatMap { $0.arcs ?? [] }.filter { $0.contains(date: date) }
    }
    
    /// Returns habits from all arcs that were active on a specific date.
    func habitsActive(on date: Date) -> [ArcHabit] {
        arcsActive(on: date).flatMap { $0.habits ?? [] }
    }
    
    /// Checks if a ritual's current arc has completed (past end date).
    func isArcCompleted(_ ritual: Ritual) -> Bool {
        guard let arc = ritual.currentArc else { return false }
        let today = calendar.startOfDay(for: Date())
        let endDate = calendar.startOfDay(for: arc.endDate)
        return today > endDate
    }
    
    /// Checks for rituals that need renewal and triggers the prompt.
    func checkForCompletedArcs() {
        for ritual in currentRituals {
            if isArcCompleted(ritual) {
                ritualNeedingRenewal = ritual
                break
            }
        }
    }
    
    /// Renews a ritual by creating a new arc, optionally copying habits from the previous arc.
    /// - Parameters:
    ///   - ritual: The ritual to renew
    ///   - durationDays: Duration for the new arc (defaults to ritual's default)
    ///   - copyHabits: Whether to copy habits from the previous arc
    func renewArc(for ritual: Ritual, durationDays: Int? = nil, copyHabits: Bool = true) {
        // Mark current arc as inactive
        if let currentArc = ritual.currentArc {
            currentArc.isActive = false
        }
        
        // Create new arc
        let duration = durationDays ?? ritual.defaultDurationDays
        let newArcNumber = (ritual.latestArc?.arcNumber ?? 0) + 1
        
        let newHabits: [ArcHabit]
        if copyHabits, let previousArc = ritual.latestArc {
            newHabits = (previousArc.habits ?? []).map { $0.copyForNewArc() }
        } else {
            newHabits = []
        }
        
        let newArc = RitualArc(
            startDate: Date(),
            durationDays: duration,
            arcNumber: newArcNumber,
            isActive: true,
            habits: newHabits
        )
        
        var arcs = ritual.arcs ?? []
        arcs.append(newArc)
        ritual.arcs = arcs
        saveContext()
    }
    
    /// Starts a new arc for a past ritual (one without an active arc).
    /// - Parameters:
    ///   - ritual: The ritual to start
    ///   - durationDays: Duration for the new arc (defaults to ritual's default)
    func startNewArc(for ritual: Ritual, durationDays: Int? = nil) {
        renewArc(for: ritual, durationDays: durationDays, copyHabits: true)
    }
    
    /// Ends a ritual without renewal (marks it as having no active arc).
    func endArc(for ritual: Ritual) {
        if let currentArc = ritual.currentArc {
            currentArc.isActive = false
            saveContext()
        }
    }
    
    /// Dismisses the renewal prompt without taking action.
    func dismissRenewalPrompt() {
        ritualNeedingRenewal = nil
    }

    // MARK: - Streak Tracking
    
    /// Returns the set of all day IDs that had 100% completion across all active arcs for that day
    private func perfectDays() -> Set<String> {
        refreshAnalyticsCacheIfNeeded()
        return cachedPerfectDayIDs
    }
    
    /// Calculates the current streak (consecutive perfect days ending today or yesterday)
    func currentStreak() -> Int {
        let perfect = perfectDays()
        guard !perfect.isEmpty else { return 0 }
        
        var streak = 0
        var checkDate = calendar.startOfDay(for: Date())
        
        // Check if today is a perfect day
        if perfect.contains(dayIdentifier(for: checkDate)) {
            streak = 1
            checkDate = calendar.date(byAdding: .day, value: -1, to: checkDate) ?? checkDate
        }
        
        // Count consecutive days backwards
        while perfect.contains(dayIdentifier(for: checkDate)) {
            streak += 1
            checkDate = calendar.date(byAdding: .day, value: -1, to: checkDate) ?? checkDate
        }
        
        return streak
    }
    
    /// Calculates the longest streak of consecutive perfect days
    func longestStreak() -> Int {
        let perfect = perfectDays()
        guard !perfect.isEmpty else { return 0 }
        
        // Sort the perfect days chronologically
        let sortedDates = perfect.compactMap { dayID -> Date? in
            dayFormatter.date(from: dayID)
        }.sorted()
        
        guard !sortedDates.isEmpty else { return 0 }
        
        var longest = 1
        var current = 1
        
        for i in 1..<sortedDates.count {
            let prev = sortedDates[i - 1]
            let curr = sortedDates[i]
            
            if let nextDay = calendar.date(byAdding: .day, value: 1, to: prev),
               calendar.isDate(nextDay, inSameDayAs: curr) {
                current += 1
                longest = max(longest, current)
            } else {
                current = 1
            }
        }
        
        return longest
    }
    
    /// Returns completion data for the last 7 days for a trend chart
    func weeklyTrendData() -> [TrendDataPoint] {
        let today = calendar.startOfDay(for: Date())
        let shortWeekdayFormatter = DateFormatter()
        shortWeekdayFormatter.calendar = calendar
        shortWeekdayFormatter.dateFormat = "EEE"
        
        var dataPoints: [TrendDataPoint] = []
        
        for daysAgo in (0..<7).reversed() {
            guard let date = calendar.date(byAdding: .day, value: -daysAgo, to: today) else { continue }
            
            let rate = completionRate(for: date)
            
            dataPoints.append(TrendDataPoint(
                date: date,
                value: rate,
                label: shortWeekdayFormatter.string(from: date)
            ))
        }
        
        return dataPoints
    }
    
    /// Returns the average completion rate for the last 7 days
    func weeklyAverageCompletion() -> Int {
        let trend = weeklyTrendData()
        guard !trend.isEmpty else { return 0 }
        let sum = trend.reduce(0.0) { $0 + $1.value }
        return Int((sum / Double(trend.count)) * 100)
    }
    
    /// Returns a breakdown showing how Days Active is calculated
    private func daysActiveBreakdown() -> [BreakdownItem] {
        let activeDates = datesWithActivity()
        let totalDays = activeDates.count
        
        // Get first and last active dates
        let sortedDates = activeDates.sorted()
        
        var breakdown: [BreakdownItem] = []
        
        // Total check-ins
        let totalCheckIns = rituals
            .flatMap { $0.arcs ?? [] }
            .flatMap { $0.habits ?? [] }
            .reduce(0) { $0 + $1.completedDayIDs.count }
        breakdown.append(BreakdownItem(
            label: String(localized: "Total check-ins"),
            value: "\(totalCheckIns)"
        ))
        
        // Unique days
        breakdown.append(BreakdownItem(
            label: String(localized: "Unique days with activity"),
            value: "\(totalDays)"
        ))
        
        // Date range
        if let first = sortedDates.first, let last = sortedDates.last {
            let dateFormatter = DateFormatter()
            dateFormatter.dateStyle = .medium
            
            breakdown.append(BreakdownItem(
                label: String(localized: "First check-in"),
                value: dateFormatter.string(from: first)
            ))
            
            if first != last {
                breakdown.append(BreakdownItem(
                    label: String(localized: "Most recent"),
                    value: dateFormatter.string(from: last)
                ))
            }
        }
        
        // Per-ritual breakdown
        for ritual in rituals {
            let ritualDays = Set((ritual.arcs ?? []).flatMap { $0.habits ?? [] }.flatMap { $0.completedDayIDs }).count
            breakdown.append(BreakdownItem(
                label: ritual.title,
                value: String(localized: "\(ritualDays) days")
            ))
        }
        
        return breakdown
    }

    func insightCards() -> [InsightCard] {
        refreshInsightCardsIfNeeded()
        return cachedInsightCards
    }

    func refreshInsightCardsIfNeeded() {
        guard insightCardsNeedRefresh else { return }
        cachedInsightCards = computeInsightCards()
        insightCardsNeedRefresh = false
    }

    private func computeInsightCards() -> [InsightCard] {
        // Only count habits from active arcs for today's stats
        let activeHabitsToday = habitsActive(on: Date())
        let totalHabits = activeHabitsToday.count
        let completedToday = activeHabitsToday.filter { isHabitCompletedToday($0) }.count
        let completionRateValue = totalHabits == 0 ? 0 : Int((Double(completedToday) / Double(totalHabits)) * 100)
        
        // Days active = unique calendar days with at least one check-in
        let daysActiveCount = datesWithActivity().count
        
        // Count rituals with active arcs
        let activeRitualCount = currentRituals.count
        
        // Build per-ritual progress breakdown
        let habitsBreakdown = currentRituals.map { ritual in
            let completed = ritual.habits.filter { isHabitCompletedToday($0) }.count
            return BreakdownItem(
                label: ritual.title,
                value: "\(completed) of \(ritual.habits.count)"
            )
        }
        
        // Streak tracking
        let current = currentStreak()
        let longest = longestStreak()
        let streakBreakdown = [
            BreakdownItem(label: String(localized: "Current streak"), value: "\(current) days"),
            BreakdownItem(label: String(localized: "Longest streak"), value: "\(longest) days")
        ]
        
        // Weekly trend
        let trendData = weeklyTrendData()
        let trendBreakdown = trendData.map { point in
            BreakdownItem(label: point.label, value: "\(Int(point.value * 100))%")
        }
        
        // 7-day average
        let weeklyAvg = weeklyAverageCompletion()
        
        // Total habits completed all-time
        let totalHabitsAllTime = rituals
            .flatMap { $0.arcs ?? [] }
            .flatMap { $0.habits ?? [] }
            .reduce(0) { $0 + $1.completedDayIDs.count }
        
        // Best ritual by completion rate
        let bestRitualInfo: (title: String, rate: Int)? = {
            var best: (title: String, rate: Int)?
            for ritual in currentRituals {
                guard let arc = ritual.currentArc else { continue }
                let habits = arc.habits ?? []
                guard !habits.isEmpty else { continue }
                let totalCheckIns = habits.reduce(0) { $0 + $1.completedDayIDs.count }
                let possibleCheckIns = habits.count * arc.dayIndex(for: Date())
                guard possibleCheckIns > 0 else { continue }
                let rate = Int(Double(totalCheckIns) / Double(possibleCheckIns) * 100)
                if best == nil || rate > best!.rate {
                    best = (ritual.title, rate)
                }
            }
            return best
        }()

        return [
            InsightCard(
                title: String(localized: "Active"),
                value: "\(activeRitualCount)",
                caption: String(localized: "In progress now"),
                explanation: String(localized: "Ritual arcs you're currently working on. Each ritual is a focused journey lasting 2-8 weeks, helping you build lasting habits through consistency."),
                symbolName: "sparkles",
                breakdown: currentRituals.map { BreakdownItem(label: $0.title, value: $0.theme) }
            ),
            InsightCard(
                title: String(localized: "Streak"),
                value: "\(current)",
                caption: String(localized: "Consecutive perfect days"),
                explanation: String(localized: "Your current streak of consecutive days with 100% habit completion. Complete all your habits today to keep the streak going!"),
                symbolName: "flame.fill",
                breakdown: streakBreakdown
            ),
            InsightCard(
                title: String(localized: "Habits today"),
                value: "\(completedToday)",
                caption: String(localized: "Completed today"),
                explanation: String(localized: "The number of habits you've checked off today across all your active rituals. Each check-in builds momentum toward your goals."),
                symbolName: "checkmark.circle.fill",
                breakdown: habitsBreakdown
            ),
            InsightCard(
                title: String(localized: "Completion"),
                value: "\(completionRateValue)%",
                caption: String(localized: "Today's progress"),
                explanation: String(localized: "Your completion percentage for today across all rituals. The chart shows your last 7 days—this helps you spot patterns and stay consistent."),
                symbolName: "chart.bar.fill",
                breakdown: trendBreakdown,
                trendData: trendData
            ),
            InsightCard(
                title: String(localized: "Days Active"),
                value: "\(daysActiveCount)",
                caption: String(localized: "Days you checked in"),
                explanation: String(localized: "This counts every unique calendar day where you completed at least one habit. It's calculated by scanning all your habit check-ins across all rituals and counting the distinct days. For example, if you checked in on Monday, skipped Tuesday, then checked in Wednesday and Thursday, your Days Active would be 3."),
                symbolName: "calendar",
                breakdown: daysActiveBreakdown()
            ),
            InsightCard(
                title: String(localized: "7-Day Avg"),
                value: "\(weeklyAvg)%",
                caption: String(localized: "Weekly average"),
                explanation: String(localized: "Your average completion rate over the last 7 days. This smooths out daily fluctuations and shows your typical consistency."),
                symbolName: "chart.line.uptrend.xyaxis",
                breakdown: trendBreakdown,
                trendData: trendData
            ),
            InsightCard(
                title: String(localized: "Total Check-ins"),
                value: "\(totalHabitsAllTime)",
                caption: String(localized: "All-time habits completed"),
                explanation: String(localized: "The total number of habit check-ins you've made since you started using Rituals. Every check-in counts toward building lasting change."),
                symbolName: "checkmark.seal.fill"
            ),
            {
                if let best = bestRitualInfo {
                    return InsightCard(
                        title: String(localized: "Best Ritual"),
                        value: "\(best.rate)%",
                        caption: best.title,
                        explanation: String(localized: "Your highest-performing ritual by completion rate in the current arc. Keep it up!"),
                        symbolName: "star.fill"
                    )
                } else {
                    return InsightCard(
                        title: String(localized: "Best Ritual"),
                        value: "—",
                        caption: String(localized: "No active rituals"),
                        explanation: String(localized: "Start a ritual to see which one you complete most consistently."),
                        symbolName: "star.fill"
                    )
                }
            }()
        ]
    }

    func createQuickRitual() {
        let defaultDuration = 28
        let habits = [
            ArcHabit(title: String(localized: "Hydrate"), symbolName: "drop.fill"),
            ArcHabit(title: String(localized: "Move"), symbolName: "figure.walk"),
            ArcHabit(title: String(localized: "Reflect"), symbolName: "pencil.and.list.clipboard")
        ]
        let arc = RitualArc(
            startDate: Date(),
            durationDays: defaultDuration,
            arcNumber: 1,
            isActive: true,
            habits: habits
        )
        let ritual = Ritual(
            title: String(localized: "Custom Ritual"),
            theme: String(localized: "Your next chapter"),
            defaultDurationDays: defaultDuration,
            notes: String(localized: "A fresh ritual created from your focus today."),
            arcs: [arc]
        )
        modelContext.insert(ritual)
        saveContext()
    }
    
    /// Creates a new ritual with the given properties
    func createRitual(
        title: String,
        theme: String,
        notes: String = "",
        durationDays: Int = 28,
        timeOfDay: TimeOfDay = .anytime,
        iconName: String = "sparkles",
        category: String = "",
        habits: [ArcHabit] = []
    ) {
        let arc = RitualArc(
            startDate: Date(),
            durationDays: durationDays,
            arcNumber: 1,
            isActive: true,
            habits: habits
        )
        let ritual = Ritual(
            title: title,
            theme: theme,
            defaultDurationDays: durationDays,
            notes: notes,
            timeOfDay: timeOfDay,
            iconName: iconName,
            category: category,
            arcs: [arc]
        )
        modelContext.insert(ritual)
        saveContext()
    }
    
    /// Creates a ritual from a preset template
    func createRitualFromPreset(_ preset: RitualPreset) {
        let habits = preset.habits.map { habitPreset in
            ArcHabit(title: habitPreset.title, symbolName: habitPreset.symbolName)
        }
        createRitual(
            title: preset.title,
            theme: preset.theme,
            notes: preset.notes,
            durationDays: preset.durationDays,
            timeOfDay: preset.timeOfDay,
            iconName: preset.iconName,
            category: preset.category,
            habits: habits
        )
    }
    
    /// Creates a ritual from a preset template and returns it.
    /// Used during onboarding to immediately show the created ritual.
    @discardableResult
    func createRitual(from preset: RitualPreset) -> Ritual {
        let habits = preset.habits.map { habitPreset in
            ArcHabit(title: habitPreset.title, symbolName: habitPreset.symbolName)
        }
        let arc = RitualArc(
            startDate: Date(),
            durationDays: preset.durationDays,
            arcNumber: 1,
            isActive: true,
            habits: habits
        )
        let ritual = Ritual(
            title: preset.title,
            theme: preset.theme,
            defaultDurationDays: preset.durationDays,
            notes: preset.notes,
            timeOfDay: preset.timeOfDay,
            iconName: preset.iconName,
            category: preset.category,
            arcs: [arc]
        )
        modelContext.insert(ritual)
        saveContext()
        return ritual
    }
    
    /// Updates an existing ritual's properties
    func updateRitual(
        _ ritual: Ritual,
        title: String,
        theme: String,
        notes: String,
        durationDays: Int,
        timeOfDay: TimeOfDay,
        iconName: String,
        category: String
    ) {
        ritual.title = title
        ritual.theme = theme
        ritual.notes = notes
        ritual.defaultDurationDays = durationDays
        ritual.timeOfDay = timeOfDay
        ritual.iconName = iconName
        ritual.category = category
        saveContext()
    }
    
    /// Permanently deletes a ritual and all its history
    func deleteRitual(_ ritual: Ritual) {
        modelContext.delete(ritual)
        saveContext()
    }
    
    /// Adds a habit to the current arc of a ritual
    func addHabit(to ritual: Ritual, title: String, symbolName: String) {
        guard let arc = ritual.currentArc else { return }
        let habit = ArcHabit(title: title, symbolName: symbolName)
        var habits = arc.habits ?? []
        habits.append(habit)
        arc.habits = habits
        saveContext()
    }
    
    /// Removes a habit from the current arc of a ritual
    func removeHabit(_ habit: ArcHabit, from ritual: Ritual) {
        guard let arc = ritual.currentArc else { return }
        var habits = arc.habits ?? []
        habits.removeAll { $0.id == habit.id }
        arc.habits = habits
        modelContext.delete(habit)
        saveContext()
    }

    private func loadRitualsIfNeeded() {
        reloadRituals()
        // No longer auto-seed rituals on fresh install
        // Users start with empty state and create their own rituals
    }

    private func reloadRituals() {
        do {
            rituals = try modelContext.fetch(FetchDescriptor<Ritual>())
            updateDerivedData()
            invalidateAnalyticsCache()
            scheduleReminderUpdate()
        } catch {
            lastErrorMessage = error.localizedDescription
        }
    }

    private func saveContext() {
        do {
            try modelContext.save()
            reloadRituals()
        } catch {
            lastErrorMessage = error.localizedDescription
        }
    }

    private func updateDerivedData() {
        currentRituals = rituals
            .filter { $0.hasActiveArc }
            .sorted { $0.timeOfDay < $1.timeOfDay }
        pastRituals = rituals
            .filter { !$0.hasActiveArc }
            .sorted { ($0.lastCompletedDate ?? .distantPast) > ($1.lastCompletedDate ?? .distantPast) }
    }

    private func refreshAnalyticsCacheIfNeeded() {
        guard analyticsNeedsRefresh else { return }
        cachedDatesWithActivity = computeDatesWithActivity()
        cachedPerfectDayIDs = computePerfectDays(from: cachedDatesWithActivity)
        analyticsNeedsRefresh = false
    }

    private func invalidateAnalyticsCache() {
        analyticsNeedsRefresh = true
        insightCardsNeedRefresh = true
    }

    private func computeDatesWithActivity() -> Set<Date> {
        var dates: Set<Date> = []

        for ritual in rituals {
            for arc in ritual.arcs ?? [] {
                for habit in arc.habits ?? [] {
                    for dayID in habit.completedDayIDs {
                        if let date = dayFormatter.date(from: dayID) {
                            dates.insert(calendar.startOfDay(for: date))
                        }
                    }
                }
            }
        }
        
        return dates
    }

    private func computePerfectDays(from activeDates: Set<Date>) -> Set<String> {
        guard !activeDates.isEmpty else { return [] }

        var perfectDayIDs: Set<String> = []

        for date in activeDates {
            let dayID = dayIdentifier(for: date)
            let activeHabits = habitsActive(on: date)

            guard !activeHabits.isEmpty else { continue }

            let allCompleted = activeHabits.allSatisfy { $0.completedDayIDs.contains(dayID) }
            if allCompleted {
                perfectDayIDs.insert(dayID)
            }
        }

        return perfectDayIDs
    }

    private func scheduleReminderUpdate() {
        pendingReminderTask?.cancel()
        let ritualsSnapshot = rituals
        pendingReminderTask = Task {
            try? await Task.sleep(for: .milliseconds(300))
            guard !Task.isCancelled else { return }
            await reminderScheduler.updateReminders(for: ritualsSnapshot)
        }
    }

    private func dayIdentifier(for date: Date) -> String {
        dayFormatter.string(from: date)
    }
    
    // MARK: - History / Calendar Support
    
    /// Returns the completion rate for a specific date, optionally filtered by ritual.
    /// This correctly queries habits from arcs that were active on that date.
    func completionRate(for date: Date, ritual: Ritual? = nil) -> Double {
        let dayID = dayIdentifier(for: date)
        let habits: [ArcHabit]
        
        if let ritual = ritual {
            // Get habits from the arc that was active on this date
            if let arc = ritual.arc(for: date) {
                habits = arc.habits ?? []
            } else {
                return 0
            }
        } else {
            // Get all habits from all arcs that were active on this date
            habits = habitsActive(on: date)
        }
        
        guard !habits.isEmpty else { return 0 }
        
        let completed = habits.filter { $0.completedDayIDs.contains(dayID) }.count
        return Double(completed) / Double(habits.count)
    }
    
    /// Returns all dates that have any habit activity.
    func datesWithActivity() -> Set<Date> {
        refreshAnalyticsCacheIfNeeded()
        return cachedDatesWithActivity
    }
    
    /// Returns the earliest date with any ritual activity.
    func earliestActivityDate() -> Date? {
        datesWithActivity().min()
    }
    
    /// Returns habit completion details for a specific date.
    /// This correctly queries habits from arcs that were active on that date.
    func habitCompletions(for date: Date, ritual: Ritual? = nil) -> [HabitCompletion] {
        let dayID = dayIdentifier(for: date)
        
        var completions: [HabitCompletion] = []
        
        if let ritual = ritual {
            // Get habits from the arc that was active on this date
            if let arc = ritual.arc(for: date) {
                for habit in arc.habits ?? [] {
                    completions.append(HabitCompletion(
                        habit: habit,
                        ritualTitle: ritual.title,
                        isCompleted: habit.completedDayIDs.contains(dayID)
                    ))
                }
            }
        } else {
            // Get all habits from all arcs that were active on this date
            for r in rituals {
                if let arc = r.arc(for: date) {
                    for habit in arc.habits ?? [] {
                        completions.append(HabitCompletion(
                            habit: habit,
                            ritualTitle: r.title,
                            isCompleted: habit.completedDayIDs.contains(dayID)
                        ))
                    }
                }
            }
        }
        
        return completions
    }
    
    /// Checks if a habit was completed on a specific date.
    func isHabitCompleted(_ habit: ArcHabit, on date: Date) -> Bool {
        let dayID = dayIdentifier(for: date)
        return habit.completedDayIDs.contains(dayID)
    }
    
    // MARK: - Enhanced Analytics
    
    /// Returns the weekly average completion rate for the week containing the given date.
    func weeklyAverageForDate(_ date: Date) -> Double {
        guard let weekStart = calendar.date(from: calendar.dateComponents([.yearForWeekOfYear, .weekOfYear], from: date)) else {
            return 0
        }
        
        var totalRate = 0.0
        var daysWithData = 0
        
        for dayOffset in 0..<7 {
            guard let day = calendar.date(byAdding: .day, value: dayOffset, to: weekStart) else { continue }
            let rate = completionRate(for: day)
            if rate > 0 || !habitsActive(on: day).isEmpty {
                totalRate += rate
                daysWithData += 1
            }
        }
        
        return daysWithData > 0 ? totalRate / Double(daysWithData) : 0
    }
    
    /// Returns the streak length that includes the given date, or nil if the date wasn't a perfect day.
    func streakIncluding(_ date: Date) -> Int? {
        let dayID = dayIdentifier(for: date)
        let perfect = perfectDays()
        
        guard perfect.contains(dayID) else { return nil }
        
        // Count backwards from the date
        var streakBefore = 0
        var checkDate = calendar.date(byAdding: .day, value: -1, to: calendar.startOfDay(for: date)) ?? date
        while perfect.contains(dayIdentifier(for: checkDate)) {
            streakBefore += 1
            checkDate = calendar.date(byAdding: .day, value: -1, to: checkDate) ?? checkDate
        }
        
        // Count forwards from the date
        var streakAfter = 0
        checkDate = calendar.date(byAdding: .day, value: 1, to: calendar.startOfDay(for: date)) ?? date
        while perfect.contains(dayIdentifier(for: checkDate)) {
            streakAfter += 1
            checkDate = calendar.date(byAdding: .day, value: 1, to: checkDate) ?? checkDate
        }
        
        return streakBefore + 1 + streakAfter
    }
    
    /// Returns the number of days remaining in the ritual's current arc.
    func daysRemaining(for ritual: Ritual) -> Int {
        guard let arc = ritual.currentArc else { return 0 }
        let today = calendar.startOfDay(for: Date())
        let endDate = calendar.startOfDay(for: arc.endDate)
        
        let days = calendar.dateComponents([.day], from: today, to: endDate).day ?? 0
        return max(0, days)
    }
    
    /// Returns the current streak for a specific ritual's current arc.
    func streakForRitual(_ ritual: Ritual) -> Int {
        guard let arc = ritual.currentArc else { return 0 }
        let habits = arc.habits ?? []
        guard !habits.isEmpty else { return 0 }
        
        var streak = 0
        var checkDate = calendar.startOfDay(for: Date())
        
        while arc.contains(date: checkDate) {
            let dayID = dayIdentifier(for: checkDate)
            let allCompleted = habits.allSatisfy { $0.completedDayIDs.contains(dayID) }
            
            if allCompleted {
                streak += 1
                checkDate = calendar.date(byAdding: .day, value: -1, to: checkDate) ?? checkDate
            } else {
                break
            }
        }
        
        return streak
    }
    
    /// Compares current arc completion rate to the previous arc at the same day index.
    /// Returns nil if there's no previous arc to compare against.
    /// - Returns: Tuple of (percentageDelta, previousArcNumber, currentDayIndex)
    func arcComparison(for ritual: Ritual) -> (delta: Int, previousArcNumber: Int, dayIndex: Int)? {
        guard let currentArc = ritual.currentArc else { return nil }
        
        // Find the previous arc (most recent completed arc)
        let completedArcs = (ritual.arcs ?? [])
            .filter { !$0.isActive }
            .sorted { $0.arcNumber > $1.arcNumber }
        
        guard let previousArc = completedArcs.first else { return nil }
        
        let currentDayIndex = ritualDayIndex(for: ritual)
        guard currentDayIndex > 0 else { return nil }
        
        // Calculate current arc's completion rate up to today
        let currentHabits = currentArc.habits ?? []
        guard !currentHabits.isEmpty else { return nil }
        
        let currentCheckIns = currentHabits.reduce(0) { $0 + $1.completedDayIDs.count }
        let currentPossible = currentHabits.count * currentDayIndex
        let currentRate = currentPossible > 0 ? Double(currentCheckIns) / Double(currentPossible) : 0
        
        // Calculate previous arc's completion rate at the same day index
        let previousHabits = previousArc.habits ?? []
        guard !previousHabits.isEmpty else { return nil }
        
        // Count check-ins from previous arc up to the same day index
        let previousDayIDs = generateDayIDs(from: previousArc.startDate, days: currentDayIndex)
        let previousCheckIns = previousHabits.reduce(0) { total, habit in
            total + habit.completedDayIDs.filter { previousDayIDs.contains($0) }.count
        }
        let previousPossible = previousHabits.count * currentDayIndex
        let previousRate = previousPossible > 0 ? Double(previousCheckIns) / Double(previousPossible) : 0
        
        let delta = Int((currentRate - previousRate) * 100)
        return (delta, previousArc.arcNumber, currentDayIndex)
    }
    
    /// Generates day ID strings for a range of days starting from a date.
    private func generateDayIDs(from startDate: Date, days: Int) -> Set<String> {
        var ids: Set<String> = []
        let start = calendar.startOfDay(for: startDate)
        for i in 0..<days {
            if let date = calendar.date(byAdding: .day, value: i, to: start) {
                ids.insert(dayIdentifier(for: date))
            }
        }
        return ids
    }
    
    /// Returns completion rates for each habit in a ritual's current arc.
    func habitCompletionRates(for ritual: Ritual) -> [(habit: ArcHabit, rate: Double)] {
        let currentDay = ritualDayIndex(for: ritual)
        guard currentDay > 0 else { return ritual.habits.map { ($0, 0.0) } }
        
        return ritual.habits.map { habit in
            let completedDays = habit.completedDayIDs.count
            let rate = Double(completedDays) / Double(currentDay)
            return (habit, min(rate, 1.0))
        }
    }
    
    // MARK: - Arc-Specific Analytics
    
    /// Returns completion rates for each habit in a specific arc.
    func habitCompletionRates(for arc: RitualArc) -> [(habit: ArcHabit, rate: Double)] {
        let habits = arc.habits ?? []
        let totalDays = arc.durationDays
        guard totalDays > 0 else { return habits.map { ($0, 0.0) } }
        
        return habits.map { habit in
            let completedDays = habit.completedDayIDs.count
            let rate = Double(completedDays) / Double(totalDays)
            return (habit, min(rate, 1.0))
        }
    }
    
    /// Returns the longest streak of consecutive perfect days within a specific arc.
    func longestStreak(for arc: RitualArc) -> Int {
        let habits = arc.habits ?? []
        guard !habits.isEmpty else { return 0 }
        
        let arcDayIDs = generateDayIDs(from: arc.startDate, days: arc.durationDays)
        
        // Find which days had all habits completed
        var perfectDayIDs: Set<String> = []
        for dayID in arcDayIDs {
            let allCompleted = habits.allSatisfy { $0.completedDayIDs.contains(dayID) }
            if allCompleted {
                perfectDayIDs.insert(dayID)
            }
        }
        
        guard !perfectDayIDs.isEmpty else { return 0 }
        
        // Convert to dates and sort
        let sortedDates = perfectDayIDs.compactMap { dayFormatter.date(from: $0) }.sorted()
        guard !sortedDates.isEmpty else { return 0 }
        
        var longest = 1
        var current = 1
        
        for i in 1..<sortedDates.count {
            let prev = sortedDates[i - 1]
            let curr = sortedDates[i]
            
            if let nextDay = calendar.date(byAdding: .day, value: 1, to: prev),
               calendar.isDate(nextDay, inSameDayAs: curr) {
                current += 1
                longest = max(longest, current)
            } else {
                current = 1
            }
        }
        
        return longest
    }
    
    /// Returns the count of perfect days (100% completion) within a specific arc.
    func perfectDaysCount(for arc: RitualArc) -> Int {
        let habits = arc.habits ?? []
        guard !habits.isEmpty else { return 0 }
        
        let arcDayIDs = generateDayIDs(from: arc.startDate, days: arc.durationDays)
        
        var count = 0
        for dayID in arcDayIDs {
            let allCompleted = habits.allSatisfy { $0.completedDayIDs.contains(dayID) }
            if allCompleted {
                count += 1
            }
        }
        
        return count
    }
    
    /// Returns completion rate for each day in a specific arc (for heatmap display).
    func dailyCompletionRates(for arc: RitualArc) -> [(date: Date, rate: Double)] {
        let habits = arc.habits ?? []
        guard !habits.isEmpty else { return [] }
        
        var results: [(date: Date, rate: Double)] = []
        let start = calendar.startOfDay(for: arc.startDate)
        
        for dayOffset in 0..<arc.durationDays {
            guard let date = calendar.date(byAdding: .day, value: dayOffset, to: start) else { continue }
            let dayID = dayIdentifier(for: date)
            
            let completed = habits.filter { $0.completedDayIDs.contains(dayID) }.count
            let rate = Double(completed) / Double(habits.count)
            results.append((date, rate))
        }
        
        return results
    }
    
    /// Returns milestones for a ritual with achievement status.
    func milestonesAchieved(for ritual: Ritual) -> [Milestone] {
        let currentDay = ritualDayIndex(for: ritual)
        return Milestone.standardMilestones(currentDay: currentDay, totalDays: ritual.durationDays)
    }
    
    /// Returns the week-over-week change in completion rate.
    func weekOverWeekChange() -> Double {
        let today = calendar.startOfDay(for: Date())
        guard let lastWeekStart = calendar.date(byAdding: .day, value: -7, to: today) else {
            return 0
        }
        
        let thisWeekAvg = weeklyAverageForDate(today)
        let lastWeekAvg = weeklyAverageForDate(lastWeekStart)
        
        guard lastWeekAvg > 0 else { return thisWeekAvg > 0 ? 1.0 : 0 }
        return (thisWeekAvg - lastWeekAvg) / lastWeekAvg
    }
    
    /// Returns a motivational message based on the completion rate.
    func motivationalMessage(for rate: Double) -> String {
        switch rate {
        case 1.0:
            return String(localized: "Perfect day! You crushed it.")
        case 0.75..<1.0:
            return String(localized: "Great progress! Almost there.")
        case 0.5..<0.75:
            return String(localized: "Solid effort. Keep building momentum.")
        case 0.25..<0.5:
            return String(localized: "Every habit counts. You've got this.")
        case 0.01..<0.25:
            return String(localized: "Small steps lead to big changes.")
        default:
            return String(localized: "Tomorrow is a fresh start.")
        }
    }
    
    /// Returns the insight context for tips generation.
    func insightContext() -> InsightContext {
        let activeHabitsToday = habitsActive(on: Date())
        let totalHabits = activeHabitsToday.count
        let completedToday = activeHabitsToday.filter { isHabitCompletedToday($0) }.count
        let rate = totalHabits > 0 ? Double(completedToday) / Double(totalHabits) : 0
        
        return InsightContext(
            completionRate: rate,
            currentStreak: currentStreak(),
            longestStreak: longestStreak(),
            daysActive: datesWithActivity().count,
            totalHabits: totalHabits,
            completedToday: completedToday
        )
    }

    /// Precomputes analytics data if it is invalidated.
    func refreshAnalyticsIfNeeded() {
        refreshAnalyticsCacheIfNeeded()
    }
    
    // MARK: - Debug / Demo Data
    
    #if DEBUG
    /// Preloads 6 months of random completion data for testing the history view.
    func preloadDemoData() {
        let today = calendar.startOfDay(for: Date())
        
        // Go back 6 months
        guard let sixMonthsAgo = calendar.date(byAdding: .month, value: -6, to: today) else { return }
        
        // If no rituals exist, nothing to preload
        guard !rituals.isEmpty else { return }
        
        // Update each ritual's arcs to cover a longer period
        for ritual in rituals {
            // For each arc (active or not), extend it to cover the demo period
            for arc in ritual.arcs ?? [] {
                // Set the arc to start 6 months ago and be active
                arc.startDate = sixMonthsAgo
                arc.endDate = calendar.date(byAdding: .day, value: 180 + 28 - 1, to: sixMonthsAgo) ?? today
                arc.isActive = true
            }
            
            // If no arcs exist, create one
            if (ritual.arcs ?? []).isEmpty {
                let demoHabits = [
                    ArcHabit(title: "Demo Habit 1", symbolName: "star.fill"),
                    ArcHabit(title: "Demo Habit 2", symbolName: "heart.fill")
                ]
                let demoArc = RitualArc(
                    startDate: sixMonthsAgo,
                    durationDays: 180 + 28,
                    arcNumber: 1,
                    isActive: true,
                    habits: demoHabits
                )
                var arcs = ritual.arcs ?? []
                arcs.append(demoArc)
                ritual.arcs = arcs
            }
        }
        
        // Generate completions for each day from 6 months ago to yesterday
        var currentDate = sixMonthsAgo
        
        while currentDate < today {
            let dayID = dayIdentifier(for: currentDate)
            
            for ritual in rituals {
                for arc in ritual.arcs ?? [] {
                    // Only generate completions if the arc covers this date
                    guard arc.contains(date: currentDate) else { continue }
                    
                    for habit in arc.habits ?? [] {
                        // Random completion with ~70% average success rate
                        let threshold = Double.random(in: 0.5...0.9)
                        let shouldComplete = Double.random(in: 0...1) < threshold
                        
                        if shouldComplete && !habit.completedDayIDs.contains(dayID) {
                            habit.completedDayIDs.append(dayID)
                        }
                    }
                }
            }
            
            currentDate = calendar.date(byAdding: .day, value: 1, to: currentDate) ?? currentDate
        }
        
        saveContext()
    }
    
    /// Clears all completion data (for testing).
    func clearAllCompletions() {
        for ritual in rituals {
            for arc in ritual.arcs ?? [] {
                for habit in arc.habits ?? [] {
                    habit.completedDayIDs.removeAll()
                }
            }
        }
        saveContext()
    }
    
    /// Simulates arc completion by setting the first active arc's end date to yesterday.
    /// This triggers the renewal workflow when the user navigates to the Today tab.
    func simulateArcCompletion() {
        // Find the first ritual with an active arc
        guard let ritual = currentRituals.first,
              let arc = ritual.currentArc else {
            print("No active arcs to complete")
            return
        }
        
        // Set the end date to yesterday so the arc appears completed
        let yesterday = calendar.date(byAdding: .day, value: -1, to: calendar.startOfDay(for: Date())) ?? Date()
        arc.endDate = yesterday
        
        // Also backdate the start date so the arc looks like it ran for a reasonable period
        let arcDuration = arc.durationDays
        arc.startDate = calendar.date(byAdding: .day, value: -arcDuration, to: yesterday) ?? yesterday
        
        saveContext()
        
        // Trigger the completion check - this will set ritualNeedingRenewal
        checkForCompletedArcs()
        
        print("Arc '\(ritual.title)' marked as completed. Navigate to Today tab to see renewal prompt.")
    }
    #endif
}
