import Foundation import CoreData final class ChatCacheWriter { static let shared = ChatCacheWriter() private let persistenceController: PersistenceController private let userDefaults: UserDefaults private let keychainService: KeychainService init( persistenceController: PersistenceController = .shared, userDefaults: UserDefaults = .standard, keychainService: KeychainService = .shared ) { self.persistenceController = persistenceController self.userDefaults = userDefaults self.keychainService = keychainService } func storePrivateChatList(_ data: PrivateChatListData) { guard !AppConfig.DISABLE_DB else { return } guard let accountUserId = resolveCurrentAccountUserId() else { return } let context = persistenceController.newBackgroundContext() context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy context.perform { [weak self] in guard let self else { return } do { try self.sync(chatList: data, accountUserId: accountUserId, context: context) if context.hasChanges { try context.save() } } catch { if AppConfig.DEBUG { print("[ChatCacheWriter] Failed to store private chat list: \(error)") } } } } private func resolveCurrentAccountUserId() -> String? { guard let login = userDefaults.string(forKey: "currentUser") else { return nil } return keychainService.get(forKey: "userId", service: login) } private func sync(chatList: PrivateChatListData, accountUserId: String, context: NSManagedObjectContext) throws { var profileCache: [String: CDProfile] = [:] var chatCache: [String: CDPrivateChat] = [:] var messageCache: [String: CDMessage] = [:] for item in chatList.items { let chat = try upsertChat(item, accountUserId: accountUserId, context: context, cache: &chatCache) let previousLastMessage = chat.lastMessage if let profileData = item.chatData { let profile = try upsertProfile(profileData, accountUserId: accountUserId, context: context, cache: &profileCache) chat.chatData = profile } else { chat.chatData = nil } if let lastMessageData = item.lastMessage { let message = try upsertMessage( lastMessageData, accountUserId: accountUserId, chat: chat, context: context, profileCache: &profileCache, messageCache: &messageCache ) if let previous = previousLastMessage, previous.objectID != message.objectID { previous.chatAsLastMessage = nil } chat.lastMessage = message } else { previousLastMessage?.chatAsLastMessage = nil chat.lastMessage = nil } } } private func upsertChat( _ chatItem: PrivateChatListItem, accountUserId: String, context: NSManagedObjectContext, cache: inout [String: CDPrivateChat] ) throws -> CDPrivateChat { if let cached = cache[chatItem.chatId] { apply(chatItem: chatItem, to: cached, accountUserId: accountUserId) return cached } let chat = try fetchChat(chatId: chatItem.chatId, accountUserId: accountUserId, context: context) ?? CDPrivateChat(context: context) apply(chatItem: chatItem, to: chat, accountUserId: accountUserId) cache[chatItem.chatId] = chat return chat } private func apply(chatItem: PrivateChatListItem, to chat: CDPrivateChat, accountUserId: String) { chat.accountUserId = accountUserId chat.chatId = chatItem.chatId chat.chatType = chatItem.chatType.rawValue chat.createdAt = chatItem.createdAt chat.unreadCount = Int32(chatItem.unreadCount) } private func upsertProfile( _ profileData: ChatProfile, accountUserId: String, context: NSManagedObjectContext, cache: inout [String: CDProfile] ) throws -> CDProfile { if let cached = cache[profileData.userId] { apply(profile: profileData, to: cached, accountUserId: accountUserId) return cached } let profile = try fetchProfile(userId: profileData.userId, accountUserId: accountUserId, context: context) ?? CDProfile(context: context) apply(profile: profileData, to: profile, accountUserId: accountUserId) cache[profileData.userId] = profile return profile } private func apply(profile profileData: ChatProfile, to profile: CDProfile, accountUserId: String) { profile.accountUserId = accountUserId profile.userId = profileData.userId profile.login = profileData.login profile.fullName = profileData.fullName profile.customName = profileData.customName profile.bio = profileData.bio if let lastSeen = profileData.lastSeen { profile.setValue(NSNumber(value: lastSeen), forKey: "lastSeen") } else { profile.setValue(nil, forKey: "lastSeen") } profile.createdAt = profileData.createdAt profile.isOfficial = profileData.isOfficial } private func upsertMessage( _ messageData: MessageItem, accountUserId: String, chat: CDPrivateChat, context: NSManagedObjectContext, profileCache: inout [String: CDProfile], messageCache: inout [String: CDMessage] ) throws -> CDMessage { if let cached = messageCache[messageData.messageId] { apply(message: messageData, to: cached, accountUserId: accountUserId, chat: chat, context: context, profileCache: &profileCache) return cached } let message = try fetchMessage(messageId: messageData.messageId, accountUserId: accountUserId, context: context) ?? CDMessage(context: context) apply(message: messageData, to: message, accountUserId: accountUserId, chat: chat, context: context, profileCache: &profileCache) messageCache[messageData.messageId] = message return message } private func apply( message messageData: MessageItem, to message: CDMessage, accountUserId: String, chat: CDPrivateChat, context: NSManagedObjectContext, profileCache: inout [String: CDProfile] ) { message.accountUserId = accountUserId message.messageId = messageData.messageId message.chatId = messageData.chatId message.messageType = messageData.messageType message.content = messageData.content message.mediaLink = messageData.mediaLink message.createdAt = messageData.createdAt message.updatedAt = messageData.updatedAt message.senderId = messageData.senderId message.chat = chat message.chatAsLastMessage = chat if let isViewed = messageData.isViewed { message.setValue(NSNumber(value: isViewed), forKey: "isViewed") } else { message.setValue(nil, forKey: "isViewed") } if let forward = messageData.forwardMetadata { message.forwardType = forward.forwardType message.forwardSenderId = forward.forwardSenderId message.forwardMessageId = forward.forwardMessageId } else { message.forwardType = nil message.forwardSenderId = nil message.forwardMessageId = nil } if let senderProfileData = messageData.senderData { if let profile = try? upsertProfile( senderProfileData, accountUserId: accountUserId, context: context, cache: &profileCache ) { message.sender = profile } } else { message.sender = nil } } private func fetchChat(chatId: String, accountUserId: String, context: NSManagedObjectContext) throws -> CDPrivateChat? { let request = NSFetchRequest(entityName: "CDPrivateChat") request.fetchLimit = 1 request.predicate = NSPredicate(format: "accountUserId == %@ AND chatId == %@", accountUserId, chatId) return try context.fetch(request).first } private func fetchProfile(userId: String, accountUserId: String, context: NSManagedObjectContext) throws -> CDProfile? { let request = NSFetchRequest(entityName: "CDProfile") request.fetchLimit = 1 request.predicate = NSPredicate(format: "accountUserId == %@ AND userId == %@", accountUserId, userId) return try context.fetch(request).first } private func fetchMessage(messageId: String, accountUserId: String, context: NSManagedObjectContext) throws -> CDMessage? { let request = NSFetchRequest(entityName: "CDMessage") request.fetchLimit = 1 request.predicate = NSPredicate(format: "accountUserId == %@ AND messageId == %@", accountUserId, messageId) return try context.fetch(request).first } }