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<CDPrivateChat>(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<CDProfile>(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<CDMessage>(entityName: "CDMessage")
        request.fetchLimit = 1
        request.predicate = NSPredicate(format: "accountUserId == %@ AND messageId == %@", accountUserId, messageId)
        return try context.fetch(request).first
    }
}
