// WCSession manager for bidirectional iPhone ↔ Watch sync import Foundation import WatchConnectivity import os // MARK: - Watch Timer Command enum WatchTimerCommand: Codable { case snooze(id: String, minutes: Int) case dismiss(id: String) case pause(id: String) case resume(id: String) case complete(id: String) var messageDict: [String: Any] { switch self { case .snooze(let id, let minutes): return ["command": "snooze", "id": id, "minutes": minutes] case .dismiss(let id): return ["command": "dismiss", "id": id] case .pause(let id): return ["command": "pause", "id": id] case .resume(let id): return ["command": "resume", "id": id] case .complete(let id): return ["command": "complete", "id": id] } } } // MARK: - Watch Session Manager @MainActor final class WatchSessionManager: NSObject, ObservableObject { static let shared = WatchSessionManager() @Published var isReachable = false private let session: WCSession private let logger = Logger(subsystem: "com.chronomind.watch", category: "WatchSessionManager") private var pendingCommands: [[String: Any]] = [] private override init() { session = WCSession.default super.init() } // MARK: - Activation func activate() { guard WCSession.isSupported() else { logger.warning("WCSession not supported on this device") return } session.delegate = self session.activate() logger.info("WCSession activation requested") } // MARK: - Send Command func sendCommand(_ command: WatchTimerCommand) { let message = command.messageDict guard session.activationState == .activated else { logger.warning("Session not activated, queuing command") pendingCommands.append(message) return } if session.isReachable { session.sendMessage(message, replyHandler: { reply in Task { @MainActor in self.logger.info("Command acknowledged: \(reply.description)") } }, errorHandler: { error in Task { @MainActor in self.logger.error("Failed to send command: \(error.localizedDescription)") self.pendingCommands.append(message) } }) } else { // Fallback: use application context for non-urgent delivery do { try session.updateApplicationContext(["pendingCommand": message]) logger.info("Command sent via application context") } catch { logger.error("Failed to update application context: \(error.localizedDescription)") pendingCommands.append(message) } } } // MARK: - Flush Pending private func flushPendingCommands() { guard session.isReachable, !pendingCommands.isEmpty else { return } let commands = pendingCommands pendingCommands.removeAll() for command in commands { session.sendMessage(command, replyHandler: nil) { [weak self] error in Task { @MainActor in self?.logger.error("Failed to flush command: \(error.localizedDescription)") } } } logger.info("Flushed \(commands.count) pending commands") } } // MARK: - WCSessionDelegate extension WatchSessionManager: WCSessionDelegate { nonisolated func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { Task { @MainActor in if let error = error { logger.error("WCSession activation failed: \(error.localizedDescription)") return } logger.info("WCSession activated: \(activationState.rawValue)") isReachable = session.isReachable flushPendingCommands() } } nonisolated func sessionReachabilityDidChange(_ session: WCSession) { Task { @MainActor in isReachable = session.isReachable logger.info("Reachability changed: \(session.isReachable)") if session.isReachable { flushPendingCommands() } } } nonisolated func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String: Any]) { Task { @MainActor in logger.info("Received application context update") // iPhone pushed updated timer data — refresh from App Group NotificationCenter.default.post(name: .watchTimerDataUpdated, object: nil) } } nonisolated func session(_ session: WCSession, didReceiveMessage message: [String: Any]) { Task { @MainActor in logger.info("Received message: \(message.keys.joined(separator: ", "))") // iPhone pushed a real-time update — refresh from App Group NotificationCenter.default.post(name: .watchTimerDataUpdated, object: nil) } } nonisolated func session(_ session: WCSession, didReceiveMessage message: [String: Any], replyHandler: @escaping ([String: Any]) -> Void) { Task { @MainActor in logger.info("Received message with reply: \(message.keys.joined(separator: ", "))") NotificationCenter.default.post(name: .watchTimerDataUpdated, object: nil) replyHandler(["status": "received"]) } } } // MARK: - Notification Name extension Notification.Name { static let watchTimerDataUpdated = Notification.Name("watchTimerDataUpdated") }