import Foundation import CoreData import Combine @MainActor class AccountRepository: ObservableObject { private let context: NSManagedObjectContext @Published private(set) var accounts: [Account] = [] private var cancellables = Set() init(context: NSManagedObjectContext = CoreDataStack.shared.viewContext) { self.context = context fetchAccounts() setupNotificationObserver() } private func setupNotificationObserver() { NotificationCenter.default.publisher(for: .NSManagedObjectContextObjectsDidChange, object: context) .receive(on: DispatchQueue.main) .sink { [weak self] _ in self?.fetchAccounts() } .store(in: &cancellables) NotificationCenter.default.publisher(for: .cloudKitForceReload) .receive(on: DispatchQueue.main) .sink { [weak self] _ in self?.fetchAccounts() } .store(in: &cancellables) } // MARK: - Fetch func fetchAccounts() { let request: NSFetchRequest = Account.fetchRequest() request.sortDescriptors = [ NSSortDescriptor(keyPath: \Account.sortOrder, ascending: true), NSSortDescriptor(keyPath: \Account.createdAt, ascending: true) ] // Performance: Add batch size for fetches request.fetchBatchSize = 50 do { accounts = try context.fetch(request) } catch { print("Failed to fetch accounts: \(error)") accounts = [] } } func fetchAccount(by id: UUID) -> Account? { let request: NSFetchRequest = Account.fetchRequest() request.predicate = NSPredicate(format: "id == %@", id as CVarArg) request.fetchLimit = 1 return try? context.fetch(request).first } // MARK: - Create @discardableResult func createAccount( name: String, currency: String?, inputMode: InputMode, notificationFrequency: NotificationFrequency, customFrequencyMonths: Int = 1 ) -> Account { let account = Account(context: context) account.name = name account.currency = currency account.inputMode = inputMode.rawValue account.notificationFrequency = notificationFrequency.rawValue account.customFrequencyMonths = Int16(customFrequencyMonths) account.sortOrder = Int16(accounts.count) save() return account } func createDefaultAccountIfNeeded() -> Account { // Fetch accounts directly from database to avoid race condition with async fetch let request: NSFetchRequest = Account.fetchRequest() request.sortDescriptors = [ NSSortDescriptor(keyPath: \Account.sortOrder, ascending: true), NSSortDescriptor(keyPath: \Account.createdAt, ascending: true) ] let existingAccounts = (try? context.fetch(request)) ?? [] // Check if Default account already exists if let defaultAccount = existingAccounts.first(where: { $0.isDefaultAccount }) { return defaultAccount } // If no Default account but other accounts exist, return first one if let existing = existingAccounts.first { return existing } // No accounts exist, create Default account let defaultCurrency = AppSettings.getOrCreate(in: context).currency let account = Account(context: context) account.name = Account.defaultAccountName account.currency = defaultCurrency account.inputMode = InputMode.simple.rawValue account.notificationFrequency = NotificationFrequency.monthly.rawValue account.customFrequencyMonths = 1 account.sortOrder = 0 // Attach existing sources to the default account let sourceRequest: NSFetchRequest = InvestmentSource.fetchRequest() if let sources = try? context.fetch(sourceRequest) { for source in sources where source.account == nil { source.account = account } } save() return account } /// Removes duplicate Default accounts, keeping only the oldest one. /// Call this once to clean up any duplicates created by the race condition bug. func cleanupDuplicateDefaultAccounts() { let request: NSFetchRequest = Account.fetchRequest() request.predicate = NSPredicate(format: "name == %@", Account.defaultAccountName) request.sortDescriptors = [NSSortDescriptor(keyPath: \Account.createdAt, ascending: true)] guard let defaultAccounts = try? context.fetch(request), defaultAccounts.count > 1 else { return } // Keep the first (oldest) Default account, delete the rest let accountsToDelete = defaultAccounts.dropFirst() for account in accountsToDelete { // Move sources to the kept Default account before deleting if let keptAccount = defaultAccounts.first { for source in account.sourcesArray { source.account = keptAccount } } context.delete(account) } save() } // MARK: - Validation func isNameAvailable(_ name: String, excludingAccountId: UUID? = nil) -> Bool { let trimmed = name.trimmingCharacters(in: .whitespacesAndNewlines) let normalized = trimmed.lowercased() return !accounts.contains { account in if let excludeId = excludingAccountId, account.safeId == excludeId { return false } return account.name.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() == normalized } } // MARK: - Update func updateAccount( _ account: Account, name: String? = nil, currency: String? = nil, inputMode: InputMode? = nil, notificationFrequency: NotificationFrequency? = nil, customFrequencyMonths: Int? = nil ) { if let name = name { account.name = name } if let currency = currency { account.currency = currency } if let inputMode = inputMode { account.inputMode = inputMode.rawValue } if let notificationFrequency = notificationFrequency { account.notificationFrequency = notificationFrequency.rawValue } if let customMonths = customFrequencyMonths { account.customFrequencyMonths = Int16(customMonths) } save() } // MARK: - Delete func canDeleteAccount(_ account: Account) -> Bool { // Cannot delete the Default account guard !account.isDefaultAccount else { return false } // Must keep at least one account return accounts.count > 1 } func deleteAccount(_ account: Account) { guard canDeleteAccount(account) else { return } context.delete(account) save() } // MARK: - Save private func save() { guard context.hasChanges else { return } do { try context.save() // Note: Removed redundant fetchAccounts() call - the NotificationCenter observer // in setupNotificationObserver() already handles refetching on context changes } catch { print("Failed to save accounts: \(error)") } } }