import Combine import Foundation @MainActor class SyncService: ObservableObject { @Published var isSyncing = false @Published var lastSyncTime: Date? @Published var syncError: String? @Published var isConnected = false @Published var isTesting = false @Published var isOfflineMode = false @Published var providerType: SyncProviderType = .server { didSet { updateActiveProvider() userDefaults.set(providerType.rawValue, forKey: Keys.providerType) } } private var activeProvider: SyncProvider? private let userDefaults = UserDefaults.standard private let logTag = "SyncService" private var syncTask: Task? private enum Keys { static let serverURL = "sync_server_url" static let authToken = "sync_auth_token" static let lastSyncTime = "last_sync_time" static let isConnected = "is_connected" static let autoSyncEnabled = "auto_sync_enabled" static let offlineMode = "offline_mode" static let providerType = "sync_provider_type" } // Legacy properties for compatibility with SettingsView var serverURL: String { get { userDefaults.string(forKey: Keys.serverURL) ?? "" } set { userDefaults.set(newValue, forKey: Keys.serverURL) // If active provider is server, it will pick up the change from UserDefaults } } var authToken: String { get { userDefaults.string(forKey: Keys.authToken) ?? "" } set { userDefaults.set(newValue, forKey: Keys.authToken) } } var isConfigured: Bool { return activeProvider?.isConfigured ?? false } var isAutoSyncEnabled: Bool { get { userDefaults.bool(forKey: Keys.autoSyncEnabled) } set { userDefaults.set(newValue, forKey: Keys.autoSyncEnabled) } } init() { isConnected = userDefaults.bool(forKey: Keys.isConnected) isAutoSyncEnabled = userDefaults.object(forKey: Keys.autoSyncEnabled) as? Bool ?? true isOfflineMode = userDefaults.bool(forKey: Keys.offlineMode) if let savedType = userDefaults.string(forKey: Keys.providerType), let type = SyncProviderType(rawValue: savedType) { self.providerType = type } else { self.providerType = .server // Default } updateActiveProvider() } private func updateActiveProvider() { switch providerType { case .server: activeProvider = ServerSyncProvider() case .iCloud: activeProvider = ICloudSyncProvider() case .none: activeProvider = nil } // Update status based on new provider if let provider = activeProvider { isConnected = provider.isConnected lastSyncTime = provider.lastSyncTime } else { isConnected = false lastSyncTime = nil } } func syncWithServer(dataManager: ClimbingDataManager) async throws { if isOfflineMode { AppLogger.info("Sync skipped: Offline mode is enabled.", tag: logTag) return } guard let provider = activeProvider else { if providerType == .none { return } throw SyncError.notConfigured } guard provider.isConfigured else { throw SyncError.notConfigured } // For server provider, we check connection. For others, maybe not needed or different check. if providerType == .server && !provider.isConnected { throw SyncError.notConnected } isSyncing = true syncError = nil defer { isSyncing = false } do { try await provider.sync(dataManager: dataManager) // Update last sync time self.lastSyncTime = provider.lastSyncTime } catch { syncError = error.localizedDescription throw error } } func testConnection() async throws { guard let provider = activeProvider else { AppLogger.error("Test connection failed: No active provider", tag: logTag) throw SyncError.notConfigured } isTesting = true defer { isTesting = false } try await provider.testConnection() isConnected = provider.isConnected userDefaults.set(isConnected, forKey: Keys.isConnected) } func triggerAutoSync(dataManager: ClimbingDataManager) { guard isConnected && isConfigured && isAutoSyncEnabled else { if isSyncing { isSyncing = false } return } guard !isSyncing else { return } syncTask?.cancel() syncTask = Task { try? await Task.sleep(for: .seconds(2)) guard !Task.isCancelled else { return } do { try await syncWithServer(dataManager: dataManager) } catch { self.isSyncing = false } } } func forceSyncNow(dataManager: ClimbingDataManager) { guard isConnected && isConfigured else { return } syncTask?.cancel() syncTask = nil Task { do { try await syncWithServer(dataManager: dataManager) } catch { self.isSyncing = false } } } func disconnect() { activeProvider?.disconnect() syncTask?.cancel() syncTask = nil isSyncing = false isConnected = false lastSyncTime = nil syncError = nil // These are shared keys, so clearing them affects all providers if they use them // But disconnect() is usually user initiated action userDefaults.set(false, forKey: Keys.isConnected) } func clearConfiguration() { serverURL = "" authToken = "" lastSyncTime = nil isConnected = false isAutoSyncEnabled = true userDefaults.removeObject(forKey: Keys.lastSyncTime) userDefaults.removeObject(forKey: Keys.isConnected) userDefaults.removeObject(forKey: Keys.autoSyncEnabled) syncTask?.cancel() syncTask = nil activeProvider?.disconnect() } deinit { syncTask?.cancel() } }