import Foundation import Combine import CoreData @MainActor class SourceDetailViewModel: ObservableObject { // MARK: - Published Properties @Published var source: InvestmentSource @Published var snapshots: [Snapshot] = [] @Published var metrics: InvestmentMetrics = .empty @Published var predictions: [Prediction] = [] @Published var predictionResult: PredictionResult? @Published var isLoading = false @Published var showingAddSnapshot = false @Published var showingEditSource = false @Published var showingPaywall = false @Published var errorMessage: String? // MARK: - Chart Data @Published var chartData: [(date: Date, value: Decimal)] = [] // MARK: - Dependencies private let snapshotRepository: SnapshotRepository private let sourceRepository: InvestmentSourceRepository private let calculationService: CalculationService private let predictionEngine: PredictionEngine private let freemiumValidator: FreemiumValidator private var cancellables = Set() // MARK: - Initialization init( source: InvestmentSource, snapshotRepository: SnapshotRepository = SnapshotRepository(), sourceRepository: InvestmentSourceRepository = InvestmentSourceRepository(), calculationService: CalculationService = .shared, predictionEngine: PredictionEngine = .shared, iapService: IAPService ) { self.source = source self.snapshotRepository = snapshotRepository self.sourceRepository = sourceRepository self.calculationService = calculationService self.predictionEngine = predictionEngine self.freemiumValidator = FreemiumValidator(iapService: iapService) loadData() setupObservers() } // MARK: - Setup private func setupObservers() { NotificationCenter.default.publisher(for: .NSManagedObjectContextObjectsDidChange) .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main) .sink { [weak self] _ in self?.refreshData() } .store(in: &cancellables) } // MARK: - Data Loading func loadData() { isLoading = true refreshData() isLoading = false FirebaseService.shared.logScreenView(screenName: "SourceDetail") } func refreshData() { // Fetch snapshots (filtered by freemium limits) let allSnapshots = snapshotRepository.fetchSnapshots(for: source) snapshots = freemiumValidator.filterSnapshots(allSnapshots) // Calculate metrics metrics = calculationService.calculateMetrics(for: snapshots) // Prepare chart data chartData = snapshots .sorted { $0.date < $1.date } .map { (date: $0.date, value: $0.decimalValue) } // Calculate predictions if premium if freemiumValidator.canViewPredictions() && snapshots.count >= 3 { predictionResult = predictionEngine.predict(snapshots: snapshots) predictions = predictionResult?.predictions ?? [] } else { predictions = [] predictionResult = nil } } // MARK: - Snapshot Actions func addSnapshot(date: Date, value: Decimal, contribution: Decimal?, notes: String?) { snapshotRepository.createSnapshot( for: source, date: date, value: value, contribution: contribution, notes: notes ) // Reschedule notification NotificationService.shared.scheduleReminder(for: source) // Log analytics FirebaseService.shared.logSnapshotAdded(sourceName: source.name, value: value) showingAddSnapshot = false refreshData() } func deleteSnapshot(_ snapshot: Snapshot) { snapshotRepository.deleteSnapshot(snapshot) refreshData() } func deleteSnapshot(at offsets: IndexSet) { snapshotRepository.deleteSnapshot(at: offsets, from: snapshots) refreshData() } // MARK: - Source Actions func updateSource( name: String, category: Category, frequency: NotificationFrequency, customMonths: Int ) { sourceRepository.updateSource( source, name: name, category: category, notificationFrequency: frequency, customFrequencyMonths: customMonths ) // Update notification NotificationService.shared.scheduleReminder(for: source) showingEditSource = false } // MARK: - Predictions func showPredictions() { if freemiumValidator.canViewPredictions() { // Already loaded, just navigate FirebaseService.shared.logPredictionViewed( algorithm: predictionResult?.algorithm.rawValue ?? "unknown" ) } else { showingPaywall = true FirebaseService.shared.logPaywallShown(trigger: "predictions") } } // MARK: - Computed Properties var currentValue: Decimal { source.latestValue } var formattedCurrentValue: String { currentValue.currencyString } var totalReturn: Decimal { metrics.absoluteReturn } var formattedTotalReturn: String { metrics.formattedAbsoluteReturn } var percentageReturn: Decimal { metrics.percentageReturn } var formattedPercentageReturn: String { metrics.formattedPercentageReturn } var isPositiveReturn: Bool { totalReturn >= 0 } var categoryName: String { source.category?.name ?? "Uncategorized" } var categoryColor: String { source.category?.colorHex ?? "#3B82F6" } var lastUpdated: String { source.latestSnapshot?.date.friendlyDescription ?? "Never" } var snapshotCount: Int { snapshots.count } var hasEnoughDataForPredictions: Bool { snapshots.count >= 3 } var canViewPredictions: Bool { freemiumValidator.canViewPredictions() } var isHistoryLimited: Bool { !freemiumValidator.isPremium && snapshotRepository.fetchSnapshots(for: source).count > snapshots.count } var hiddenSnapshotCount: Int { let allCount = snapshotRepository.fetchSnapshots(for: source).count return allCount - snapshots.count } }