import Foundation import CoreData import Combine @MainActor class GoalRepository: ObservableObject { private let context: NSManagedObjectContext @Published private(set) var goals: [Goal] = [] private var cancellables = Set() init(context: NSManagedObjectContext = CoreDataStack.shared.viewContext) { self.context = context fetchGoals() setupNotificationObserver() } private func setupNotificationObserver() { NotificationCenter.default.publisher(for: .NSManagedObjectContextObjectsDidChange, object: context) .receive(on: DispatchQueue.main) .sink { [weak self] _ in self?.fetchGoals() } .store(in: &cancellables) NotificationCenter.default.publisher(for: .cloudKitForceReload) .receive(on: DispatchQueue.main) .sink { [weak self] _ in self?.fetchGoals() } .store(in: &cancellables) } // MARK: - Fetch func fetchGoals(for account: Account? = nil) { let request: NSFetchRequest = Goal.fetchRequest() if let account = account { request.predicate = NSPredicate(format: "account == %@", account) } request.sortDescriptors = [ NSSortDescriptor(keyPath: \Goal.createdAt, ascending: true) ] // Performance: Add batch size for fetches request.fetchBatchSize = 50 do { goals = try context.fetch(request) } catch { print("Failed to fetch goals: \(error)") goals = [] } } // MARK: - Create /// Check if a goal with the given name already exists in the account (case-insensitive) func goalExists(name: String, in account: Account?) -> Bool { let trimmed = name.trimmingCharacters(in: .whitespacesAndNewlines) let normalized = trimmed.lowercased() return goals.contains { goal in let nameMatches = goal.name.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() == normalized let accountMatches = goal.account?.id == account?.id return nameMatches && accountMatches } } /// Find an existing goal by name (case-insensitive) within the same account func findGoal(byName name: String, in account: Account?) -> Goal? { let trimmed = name.trimmingCharacters(in: .whitespacesAndNewlines) let normalized = trimmed.lowercased() return goals.first { goal in let nameMatches = goal.name.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() == normalized let accountMatches = goal.account?.id == account?.id return nameMatches && accountMatches } } @discardableResult func createGoal( name: String, targetAmount: Decimal, targetDate: Date? = nil, account: Account? ) -> Goal { // Check if goal with same name already exists in this account if let existing = findGoal(byName: name, in: account) { return existing } let goal = Goal(context: context) goal.name = name.trimmingCharacters(in: .whitespacesAndNewlines) goal.targetAmount = NSDecimalNumber(decimal: targetAmount) goal.targetDate = targetDate goal.account = account save() return goal } // MARK: - Update func updateGoal( _ goal: Goal, name: String? = nil, targetAmount: Decimal? = nil, targetDate: Date? = nil, clearTargetDate: Bool = false, isActive: Bool? = nil ) { if let name = name { goal.name = name } if let targetAmount = targetAmount { goal.targetAmount = NSDecimalNumber(decimal: targetAmount) } if clearTargetDate { goal.targetDate = nil } else if let targetDate = targetDate { goal.targetDate = targetDate } if let isActive = isActive { goal.isActive = isActive } save() } func deleteGoal(_ goal: Goal) { context.delete(goal) save() } // MARK: - Save private func save() { guard context.hasChanges else { return } do { try context.save() // Note: Removed redundant fetchGoals() call - the NotificationCenter observer // in setupNotificationObserver() already handles refetching on context changes } catch { print("Failed to save goals: \(error)") } } }