// ── Timer Store ──────────────────────────────────────────────── // Observable store managing all timers — equivalent of Zustand store // Persists to UserDefaults (SwiftData migration in future) import Foundation import Combine import ActivityKit @MainActor final class TimerStore: ObservableObject { // MARK: - Published State @Published var timers: [CMTimer] = [] @Published var now: Date = Date() // MARK: - Private private var tickTimer: Timer? private let persistenceKey = "chronomind-timers" private let notifications = CMNotificationManager.shared private let sharedData = SharedTimerDataManager.shared private let liveActivity = LiveActivityManager.shared private let syncManager = PlatformSyncManager.shared // MARK: - Init init() { loadTimers() startTicking() syncToSharedData() } deinit { tickTimer?.invalidate() } // MARK: - CRUD func addAlarm(_ params: CreateAlarmParams) -> CMTimer { let timer = createAlarm(params) timers.append(timer) notifications.scheduleNotifications(for: timer) saveTimers() liveActivity.startActivity(for: timer) syncManager.enqueueChange(timer, action: .create) return timer } func addCountdown(_ params: CreateCountdownParams) -> CMTimer { let timer = createCountdown(params) timers.append(timer) notifications.scheduleNotifications(for: timer) saveTimers() liveActivity.startActivity(for: timer) syncManager.enqueueChange(timer, action: .create) return timer } func addPomodoro(_ params: CreatePomodoroParams = CreatePomodoroParams()) -> CMTimer { let timer = createPomodoro(params) timers.append(timer) notifications.scheduleNotifications(for: timer) saveTimers() liveActivity.startActivity(for: timer) syncManager.enqueueChange(timer, action: .create) return timer } func removeTimer(_ id: String) { liveActivity.endActivity(for: id) timers.removeAll { $0.id == id } notifications.removeNotifications(for: id) saveTimers() syncManager.enqueueDelete(timerId: id) } // MARK: - State Transitions func pause(_ id: String) { updateTimer(id) { pauseTimer($0) } if let updated = getTimer(id) { syncManager.enqueueChange(updated, action: .update) } } func resume(_ id: String) { updateTimer(id) { t in let resumed = resumeTimer(t) self.notifications.scheduleNotifications(for: resumed) return resumed } if let updated = getTimer(id) { syncManager.enqueueChange(updated, action: .update) } } func fire(_ id: String) { updateTimer(id) { fireTimer($0) } } func snooze(_ id: String, minutes: Int) { updateTimer(id) { t in let snoozed = snoozeTimer(t, snoozeMinutes: minutes) self.notifications.scheduleNotifications(for: snoozed) return snoozed } if let updated = getTimer(id) { syncManager.enqueueChange(updated, action: .update) } } func dismiss(_ id: String) { liveActivity.endActivity(for: id) // Check for late dismissal before updating state if let timer = getTimer(id) { checkForRescheduleSuggestion(dismissedTimer: timer) } updateTimer(id) { dismissTimer($0) } if let updated = getTimer(id) { syncManager.enqueueChange(updated, action: .update) } } func complete(_ id: String) { liveActivity.endActivity(for: id) updateTimer(id) { completeTimer($0) } if let updated = getTimer(id) { syncManager.enqueueChange(updated, action: .update) } // Record completion for streak tracking GamificationStore.shared.recordCompletion() } func advancePom(_ id: String) { guard let index = timers.firstIndex(where: { $0.id == id }) else { return } if let next = advancePomodoro(timers[index]) { timers[index] = next notifications.scheduleNotifications(for: next) saveTimers() syncManager.enqueueChange(next, action: .update) } } // MARK: - Tick func tick() { let currentTime = Date() now = currentTime var changed = false for i in timers.indices { // Check if timer should fire if shouldTimerFire(timers[i], now: currentTime) { timers[i] = fireTimer(timers[i]) changed = true // Haptic feedback HapticEngine.fire(urgency: timers[i].urgency) continue } // Check cascade warnings let newlyFired = checkWarnings(&timers[i].warnings, now: currentTime) if !newlyFired.isEmpty { changed = true // Update state to warning if still active if timers[i].state == .active { timers[i].state = .warning } // Haptic feedback for warning HapticEngine.warning(urgency: timers[i].urgency) } } if changed { saveTimers() } // Update Live Activity for the next firing timer if let next = nextFiringTimer { liveActivity.updateActivity(for: next) } } // MARK: - Reschedule @Published var lastReschedule: RescheduleResult? @Published var rescheduleSuggestions: [RescheduleSuggestion] = [] /// Apply a reschedule action func applyReschedule(_ action: RescheduleAction) { let result = RescheduleEngine.apply(action: action, to: timers) lastReschedule = result timers = result.updatedTimers // Reschedule notifications for affected timers for id in result.affectedTimerIds { if let timer = getTimer(id) { notifications.scheduleNotifications(for: timer) } } saveTimers() // Auto-clear undo after 1 hour DispatchQueue.main.asyncAfter(deadline: .now() + 3600) { [weak self] in if self?.lastReschedule?.timestamp == result.timestamp { self?.lastReschedule = nil } } } /// Undo the last reschedule func undoReschedule() { guard let result = lastReschedule else { return } timers = RescheduleEngine.undo(result: result, in: timers) // Reschedule notifications for restored timers for id in result.affectedTimerIds { if let timer = getTimer(id) { notifications.scheduleNotifications(for: timer) } } lastReschedule = nil saveTimers() } /// Check for late dismissal and generate suggestions func checkForRescheduleSuggestion(dismissedTimer: CMTimer) { let now = Date() if let delay = RescheduleEngine.detectLateDismissal(timer: dismissedTimer, dismissedAt: now) { rescheduleSuggestions = RescheduleSuggestion.suggestionsForLateDismissal( timer: dismissedTimer, dismissDelay: delay ) } } /// Clear current suggestions func clearRescheduleSuggestions() { rescheduleSuggestions = [] } // MARK: - Queries func getTimer(_ id: String) -> CMTimer? { timers.first { $0.id == id } } var activeTimers: [CMTimer] { timers.filter { [.active, .warning, .snoozed, .paused, .firing].contains($0.state) } } var nextFiringTimer: CMTimer? { timers .filter { [.active, .warning].contains($0.state) } .sorted { $0.targetTime < $1.targetTime } .first } // MARK: - Private Helpers private func updateTimer(_ id: String, updater: (CMTimer) -> CMTimer) { guard let index = timers.firstIndex(where: { $0.id == id }) else { return } timers[index] = updater(timers[index]) saveTimers() } private func startTicking() { tickTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in Task { @MainActor in self?.tick() } } } // MARK: - Persistence (UserDefaults for now, SwiftData later) private func saveTimers() { guard let data = try? JSONEncoder().encode(timers) else { return } UserDefaults.standard.set(data, forKey: persistenceKey) syncToSharedData() } private func loadTimers() { guard let data = UserDefaults.standard.data(forKey: persistenceKey), let saved = try? JSONDecoder().decode([CMTimer].self, from: data) else { return } timers = saved } // MARK: - App Group Sync /// Sync timer data to App Group for widgets and watchOS private func syncToSharedData() { sharedData.writeTimers(timers) sharedData.writeActiveTimer(nextFiringTimer) reloadWidgets() } /// Reload all WidgetKit timelines when data changes private func reloadWidgets() { // Post notification that widget-aware code can observe NotificationCenter.default.post(name: .chronoMindTimersDidChange, object: nil) } } // MARK: - Notification Names extension Notification.Name { static let chronoMindTimersDidChange = Notification.Name("chronoMindTimersDidChange") }