import Foundation import Combine import CoreData import UIKit @MainActor class SettingsViewModel: ObservableObject { // MARK: - Published Properties @Published var isPremium = false @Published var isFamilyShared = false @Published var notificationsEnabled = false @Published var defaultNotificationTime = Date() @Published var analyticsEnabled = true @Published var currencyCode = AppSettings.getOrCreate(in: CoreDataStack.shared.viewContext).currency @Published var inputMode: InputMode = .simple @Published var isLoading = false @Published var showingPaywall = false @Published var showingExportOptions = false @Published var showingImportSheet = false @Published var showingResetConfirmation = false @Published var errorMessage: String? @Published var successMessage: String? // MARK: - Statistics @Published var totalSources = 0 @Published var totalSnapshots = 0 @Published var totalCategories = 0 // MARK: - Dependencies private let iapService: IAPService private let notificationService: NotificationService private let sourceRepository: InvestmentSourceRepository private let categoryRepository: CategoryRepository private let freemiumValidator: FreemiumValidator private var cancellables = Set() // MARK: - Initialization init( iapService: IAPService, notificationService: NotificationService? = nil, sourceRepository: InvestmentSourceRepository? = nil, categoryRepository: CategoryRepository? = nil ) { self.iapService = iapService self.notificationService = notificationService ?? .shared self.sourceRepository = sourceRepository ?? InvestmentSourceRepository() self.categoryRepository = categoryRepository ?? CategoryRepository() self.freemiumValidator = FreemiumValidator(iapService: iapService) setupObservers() loadSettings() } // MARK: - Setup private func setupObservers() { iapService.$isPremium .receive(on: DispatchQueue.main) .assign(to: &$isPremium) iapService.$isFamilyShared .receive(on: DispatchQueue.main) .assign(to: &$isFamilyShared) notificationService.$isAuthorized .receive(on: DispatchQueue.main) .assign(to: &$notificationsEnabled) } // MARK: - Data Loading func loadSettings() { let context = CoreDataStack.shared.viewContext let settings = AppSettings.getOrCreate(in: context) defaultNotificationTime = settings.defaultNotificationTime ?? Date() analyticsEnabled = settings.enableAnalytics currencyCode = settings.currency inputMode = InputMode(rawValue: settings.inputMode) ?? .simple // Load statistics totalSources = sourceRepository.sourceCount totalCategories = categoryRepository.categories.count totalSnapshots = sourceRepository.sources.reduce(0) { $0 + $1.snapshotCount } FirebaseService.shared.logScreenView(screenName: "Settings") } // MARK: - Premium Actions func upgradeToPremium() { showingPaywall = true FirebaseService.shared.logPaywallShown(trigger: "settings_upgrade") } func restorePurchases() async { isLoading = true await iapService.restorePurchases() isLoading = false if isPremium { successMessage = "Purchases restored successfully!" FirebaseService.shared.logRestorePurchases(success: true) } else { errorMessage = "No purchases to restore" FirebaseService.shared.logRestorePurchases(success: false) } } // MARK: - Notification Settings func requestNotificationPermission() async { let granted = await notificationService.requestAuthorization() if !granted { errorMessage = "Please enable notifications in Settings" } } func updateNotificationTime(_ time: Date) { defaultNotificationTime = time let context = CoreDataStack.shared.viewContext let settings = AppSettings.getOrCreate(in: context) settings.defaultNotificationTime = time CoreDataStack.shared.save() // Reschedule all notifications with new time let sources = sourceRepository.fetchActiveSources() notificationService.scheduleAllReminders(for: sources) } func openSystemSettings() { if let url = URL(string: UIApplication.openSettingsURLString) { UIApplication.shared.open(url) } } // MARK: - Export func exportData(format: ExportService.ExportFormat) { guard freemiumValidator.canExport() else { showingPaywall = true FirebaseService.shared.logPaywallShown(trigger: "export") return } guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, let viewController = windowScene.windows.first?.rootViewController else { return } ExportService.shared.share( format: format, sources: sourceRepository.sources, categories: categoryRepository.categories, from: viewController ) } var canExport: Bool { freemiumValidator.canExport() } // MARK: - Analytics func toggleAnalytics(_ enabled: Bool) { analyticsEnabled = enabled let context = CoreDataStack.shared.viewContext let settings = AppSettings.getOrCreate(in: context) settings.enableAnalytics = enabled CoreDataStack.shared.save() // Note: In production, you'd also update Firebase Analytics consent } // MARK: - Currency func updateCurrency(_ code: String) { currencyCode = code let context = CoreDataStack.shared.viewContext let settings = AppSettings.getOrCreate(in: context) settings.currency = code CoreDataStack.shared.save() } // MARK: - Input Mode func updateInputMode(_ mode: InputMode) { inputMode = mode let context = CoreDataStack.shared.viewContext let settings = AppSettings.getOrCreate(in: context) settings.inputMode = mode.rawValue CoreDataStack.shared.save() } // MARK: - Data Management func resetAllData() { let context = CoreDataStack.shared.viewContext // Delete all snapshots, sources, and categories for source in sourceRepository.sources { context.delete(source) } for category in categoryRepository.categories { context.delete(category) } let assetRequest: NSFetchRequest = Asset.fetchRequest() let accountRequest: NSFetchRequest = Account.fetchRequest() let goalRequest: NSFetchRequest = Goal.fetchRequest() if let assets = try? context.fetch(assetRequest) { assets.forEach { context.delete($0) } } if let accounts = try? context.fetch(accountRequest) { accounts.forEach { context.delete($0) } } if let goals = try? context.fetch(goalRequest) { goals.forEach { context.delete($0) } } CoreDataStack.shared.save() // Clear notifications notificationService.cancelAllReminders() // Recreate default categories categoryRepository.createDefaultCategoriesIfNeeded() _ = AccountRepository().createDefaultAccountIfNeeded() // Reload data loadSettings() successMessage = "All data has been reset" } // MARK: - Computed Properties var appVersion: String { "\(AppConstants.appVersion) (\(AppConstants.buildNumber))" } var premiumStatusText: String { if isPremium { return isFamilyShared ? "Premium (Family)" : "Premium" } return "Free" } var sourceLimitText: String { if isPremium { return "Unlimited" } return "\(totalSources)/\(FreemiumLimits.maxSources)" } var historyLimitText: String { if isPremium { return "Full history" } return "Last \(FreemiumLimits.maxHistoricalMonths) months" } var storageUsedText: String { let bytes = calculateStorageUsed() let formatter = ByteCountFormatter() formatter.countStyle = .file return formatter.string(fromByteCount: Int64(bytes)) } private func calculateStorageUsed() -> Int { guard let storeURL = CoreDataStack.sharedStoreURL else { return 0 } do { let attributes = try FileManager.default.attributesOfItem(atPath: storeURL.path) return attributes[.size] as? Int ?? 0 } catch { return 0 } } }