import Foundation import Combine import CoreData @MainActor class MonthlyCheckInViewModel: ObservableObject { @Published var sourcesNeedingUpdate: [InvestmentSource] = [] @Published var sources: [InvestmentSource] = [] @Published var monthlySummary: MonthlySummary = .empty @Published var recentNotes: [Snapshot] = [] @Published var isLoading = false @Published var selectedAccount: Account? @Published var showAllAccounts = true @Published var selectedRange: DateRange = .thisMonth @Published var checkInStats: MonthlyCheckInStats = .empty private let sourceRepository: InvestmentSourceRepository private let snapshotRepository: SnapshotRepository private let calculationService: CalculationService private var cancellables = Set() @MainActor init( sourceRepository: InvestmentSourceRepository? = nil, snapshotRepository: SnapshotRepository? = nil, calculationService: CalculationService? = nil ) { let context = CoreDataStack.shared.viewContext self.sourceRepository = sourceRepository ?? InvestmentSourceRepository(context: context) self.snapshotRepository = snapshotRepository ?? SnapshotRepository(context: context) self.calculationService = calculationService ?? .shared setupObservers() refresh() } private func setupObservers() { sourceRepository.$sources .receive(on: DispatchQueue.main) .sink { [weak self] _ in self?.refresh() } .store(in: &cancellables) NotificationCenter.default.publisher(for: .NSManagedObjectContextObjectsDidChange) .debounce(for: .milliseconds(500), scheduler: DispatchQueue.main) .sink { [weak self] _ in self?.refresh() } .store(in: &cancellables) } func refresh() { isLoading = true let filtered = filteredSources() let snapshots = filteredSnapshots(for: filtered) let accountFilter = showAllAccounts ? nil : selectedAccount sourcesNeedingUpdate = sourceRepository.fetchSourcesNeedingUpdate(for: accountFilter) sources = filtered monthlySummary = calculationService.calculateMonthlySummary( sources: filtered, snapshots: snapshots, range: selectedRange ) checkInStats = MonthlyCheckInStore.stats(referenceDate: selectedRange.end) recentNotes = snapshots .filter { ($0.notes?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false) && selectedRange.contains($0.date) } .sorted { $0.date > $1.date } .prefix(5) .map { $0 } isLoading = false } func duplicatePreviousMonthSnapshots(referenceDate: Date) { let targetRange = DateRange.month(containing: referenceDate) let previousRange = DateRange.month(containing: referenceDate.adding(months: -1)) let sources = filteredSources() guard !sources.isEmpty else { return } let targetSnapshots = snapshotRepository.fetchSnapshots( from: targetRange.start, to: targetRange.end ) let targetSourceIds = Set(targetSnapshots.compactMap { $0.source?.id }) let now = Date() let targetDate = targetRange.contains(now) ? now : targetRange.end for source in sources { let sourceId = source.id if targetSourceIds.contains(sourceId) { continue } let previousSnapshots = snapshotRepository.fetchSnapshots(for: source) guard let previousSnapshot = previousSnapshots.first(where: { previousRange.contains($0.date) }) else { continue } snapshotRepository.createSnapshot( for: source, date: targetDate, value: previousSnapshot.decimalValue, contribution: previousSnapshot.contribution?.decimalValue, notes: previousSnapshot.notes ) } } private func filteredSources() -> [InvestmentSource] { if showAllAccounts || selectedAccount == nil { return sourceRepository.sources } return sourceRepository.sources.filter { $0.account?.id == selectedAccount?.id } } private func filteredSnapshots(for sources: [InvestmentSource]) -> [Snapshot] { let sourceIds = Set(sources.compactMap { $0.id }) return snapshotRepository.fetchAllSnapshots().filter { snapshot in let sourceId = snapshot.source?.id return sourceId.map(sourceIds.contains) ?? false } } }