import { chacha20Decrypt, decodeWithPassword, decrypt, generateMd5 } from '@/app/workers/crypto/crypto';
import { useDatabase } from '@/app/providers/DatabaseProvider/useDatabase';
import { createContext, useEffect, useRef, useState } from 'react';
import { Attachment, AttachmentTransport, AttachmentType, PacketMessage } from '@/app/providers/ProtocolProvider/protocol/packets/packet.message';
import { usePrivatePlain } from '../AccountProvider/usePrivatePlain';
import { usePublicKey } from '../AccountProvider/usePublicKey';
import { PacketRead } from '@/app/providers/ProtocolProvider/protocol/packets/packet.read';
import { usePrivateKeyHash } from '../AccountProvider/usePrivateKeyHash';
import { useMemory } from '../MemoryProvider/useMemory';
import { useBlacklist } from '../BlacklistProvider/useBlacklist';
import { useLogger } from '@/app/hooks/useLogger';
import { useSender } from '../ProtocolProvider/useSender';
import { usePacket } from '../ProtocolProvider/usePacket';
import { MAX_MESSAGES_LOAD, MESSAGE_MAX_LOADED, TIME_TO_INACTIVE_FOR_MESSAGES_UNREAD } from '@/app/constants';
import { PacketDelivery } from '@/app/providers/ProtocolProvider/protocol/packets/packet.delivery';
import { useIdle } from '@mantine/hooks';
import { useWindowFocus } from '@/app/hooks/useWindowFocus';
import { useDialogsCache } from './useDialogsCache';
import { useConsoleLogger } from '@/app/hooks/useConsoleLogger';
import { useViewPanelsState, ViewPanelsState } from '@/app/hooks/useViewPanelsState';
import { useFileStorage } from '@/app/hooks/useFileStorage';
import { useSystemAccounts } from '../SystemAccountsProvider/useSystemAccounts';
import { useDialogsList } from '../DialogListProvider/useDialogsList';
import { useGroups } from './useGroups';
import { useMentions } from '../DialogStateProvider.tsx/useMentions';

export interface DialogContextValue {
    loading: boolean;
    messages: Message[];
    setMessages: (messages: React.SetStateAction<Message[]>) => void;
    dialog: string;
    clearDialogCache: () => void;
    loadMessagesToTop: () => Promise<void>;
    loadMessagesToMessageId: (messageId: string) => Promise<void>;
}

export const DialogContext = createContext<DialogContextValue | null>(null);

export enum DeliveredMessageState {
    WAITING,
    DELIVERED,
    ERROR
}

export interface AttachmentMeta {
    id: string;
    type: AttachmentType;
    preview: string;
    transport: AttachmentTransport;
}

export interface Message {
    from_public_key: string;
    to_public_key: string;
    content: string;
    timestamp: number;
    readed: number;
    chacha_key: string;
    from_me: number;
    plain_message: string;
    delivered: DeliveredMessageState;
    message_id: string;
    attachments: Attachment[];
}

interface DialogProviderProps {
    children: React.ReactNode;
    dialog: string;
}

type DialogMessageEvent = {
  dialogId: string;
  message: Message;
};

const bus = new EventTarget();

export const emitDialogMessage = (payload: DialogMessageEvent) => {
  bus.dispatchEvent(new CustomEvent<DialogMessageEvent>("dialog:message", { detail: payload }));
};

export const onDialogMessage = (handler: (payload: DialogMessageEvent) => void) => {
  const listener = (e: Event) => handler((e as CustomEvent<DialogMessageEvent>).detail);
  bus.addEventListener("dialog:message", listener);
  return () => bus.removeEventListener("dialog:message", listener);
};

export function DialogProvider(props: DialogProviderProps) {
    const [messages, setMessages] = useState<Message[]>([]);
    const {allQuery, runQuery} = useDatabase();
    const privatePlain = usePrivatePlain();
    const publicKey = usePublicKey();
    const privateKey = usePrivateKeyHash();
    const send = useSender();
    const [__, setCurrentDialogPublicKeyView] = useMemory("current-dialog-public-key-view", "", true);
    const log = useLogger('DialogProvider');
    const [blocked] = useBlacklist(props.dialog)
    const lastMessageTimeRef = useRef(0);
    const idle = useIdle(TIME_TO_INACTIVE_FOR_MESSAGES_UNREAD * 1000);
    const focus = useWindowFocus();
    const [loading, setLoading] = useState(false);
    const {getDialogCache, addOrUpdateDialogCache, dialogsCache, setDialogsCache} = useDialogsCache();
    const {info, warn, error} = useConsoleLogger('DialogProvider');
    const [viewState] = useViewPanelsState();
    const {readFile} = useFileStorage();
    const systemAccounts = useSystemAccounts();
    const {updateDialog} = useDialogsList();
    const {hasGroup, getGroupKey} = useGroups();
    const {popMention, isMentioned} = useMentions();


    useEffect(() => {
        const unsub = onDialogMessage(({ dialogId, message }) => {
        if (dialogId !== props.dialog) return;
            setMessages((prev) => [...prev, message]);
        });
        return unsub;
    }, [props.dialog]);

    useEffect(() => {
        setCurrentDialogPublicKeyView(props.dialog);
        return () => {
            setCurrentDialogPublicKeyView("");
        }
    }, [props.dialog]);

    useEffect(() => {
        if(props.dialog == "demo"){
            /**
             * Это нужно для демонстрации работы сообщений на странице оформления. Так как там нет хуков и 
             * других инструментов для загрузки сообщений, то мы просто не загружаем сообщения, так как это режим
             * демонстрации
             */
            return;
        }
        if(idle){
           return; 
        }
        (async () => {
            if(hasGroup(props.dialog)){
                await runQuery(`UPDATE messages SET read = 1 WHERE to_public_key = ? AND account = ? AND read != 1 AND from_public_key != ?`, [props.dialog, publicKey, publicKey]);
            }else{
                await runQuery(`UPDATE messages SET read = 1 WHERE ((from_public_key = ? AND to_public_key = ?) OR (from_public_key = ? AND to_public_key = ?)) AND account = ? AND read != 1 AND from_me = 0`, [props.dialog, publicKey, publicKey, props.dialog, publicKey]);
            }
            updateDialog(props.dialog);
        })();
    }, [idle, props.dialog]);

    useEffect(() => {
        if(props.dialog == "demo"){
            /**
             * Это нужно для демонстрации работы сообщений на странице оформления. Так как там нет хуков и 
             * других инструментов для загрузки сообщений, то мы просто не загружаем сообщения, так как это режим
             * демонстрации
             */            
            return;
        }
        setMessages([]);
        if(props.dialog == "" 
                || privatePlain == "") {
            return;
        }
        (async () => {
            let dialogCacheEntry = getDialogCache(props.dialog);
            if(dialogCacheEntry.length > 0){
                const messagesToLoadFromCache = dialogCacheEntry.slice(-MESSAGE_MAX_LOADED);
                setMessages(messagesToLoadFromCache);
                info("Loading messages for " + props.dialog + " from cache");
                setLoading(false);
                if(hasGroup(props.dialog)){
                    await runQuery(`UPDATE messages SET read = 1 WHERE to_public_key = ? AND account = ? AND read != 1 AND from_public_key != ?`, [props.dialog, publicKey, publicKey]);
                }else{
                    await runQuery(`UPDATE messages SET read = 1 WHERE ((from_public_key = ? AND to_public_key = ?) OR (from_public_key = ? AND to_public_key = ?)) AND account = ? AND read != 1 AND from_me = 0`, [props.dialog, publicKey, publicKey, props.dialog, publicKey]);
                }
                if(isMentioned(props.dialog)){
                    /**
                     * Удаляем упоминания потому что мы только что загрузили
                     * диалог из кэша, может быть в нем есть упоминания
                     */
                    for(let i = 0; i < messagesToLoadFromCache.length; i++){
                        const message = messagesToLoadFromCache[i];
                        popMention({
                            dialog_id: props.dialog,
                            message_id: message.message_id
                        });
                    }
                }
                updateDialog(props.dialog);
                return;
            }
            info("Loading messages for " + props.dialog + " from database");
            setLoading(true);
            let result: any[] = [];
            if (props.dialog != publicKey) {
                if(hasGroup(props.dialog)){
                    result = await allQuery(`
                    SELECT * FROM (SELECT * FROM messages WHERE (to_public_key = ?) AND account = ? ORDER BY timestamp DESC LIMIT ?) ORDER BY timestamp ASC
                `, [props.dialog, publicKey, MAX_MESSAGES_LOAD]);
                }else{
                    result = await allQuery(`
                    SELECT * FROM (SELECT * FROM messages WHERE (from_public_key = ? OR to_public_key = ?) AND (from_public_key = ? OR to_public_key = ?) AND account = ? ORDER BY timestamp DESC LIMIT ?) ORDER BY timestamp ASC
                `, [props.dialog, props.dialog, publicKey, publicKey, publicKey, MAX_MESSAGES_LOAD]);
                }
            } else {
                result = await allQuery(`
                    SELECT * FROM (SELECT * FROM messages WHERE from_public_key = ? AND to_public_key = ? AND account = ? ORDER BY timestamp DESC LIMIT ?) ORDER BY timestamp ASC
                `, [publicKey, publicKey, publicKey, MAX_MESSAGES_LOAD]);
            }
            await runQuery(`UPDATE messages SET read = 1 WHERE ((from_public_key = ? AND to_public_key = ?) OR (from_public_key = ? AND to_public_key = ?)) AND account = ? AND read != 1 AND from_me = 0`, [props.dialog, publicKey, publicKey, props.dialog, publicKey]);
            const finalMessages : Message[] = [];
            let readUpdated = false;
            for(let i = 0; i < result.length; i++){
                const message = result[i];
                if(message.read != 1 && !readUpdated){
                    readUpdated = true;
                } 
                let decryptKey = '';
                if(message.from_me && message.chacha_key != "" && !message.chacha_key.startsWith("sync:")){
                    /**
                     * Если это сообщение от нас, то проверяем, есть ли внутри chacha_key
                     */
                    try{
                        decryptKey = Buffer.from(await decodeWithPassword(privatePlain, message.chacha_key), 'binary').toString('hex');
                    }catch(e) {
                        decryptKey = "";
                    }
                }
                if(message.from_me && message.chacha_key != "" && message.chacha_key.startsWith("sync:")){
                    /**
                     * Если это сообщение от нас, то проверяем, есть ли внутри chacha_key, если есть, значит это
                     * сообщение пришло нам в результате синхронизации и его нужно расшифровать, если chacha_key нет,
                     * значит сообщение отправлено с нашего устройства, и зашифровано на стороне отправки (plain_message)
                     */
                    decryptKey = Buffer.from(await decodeWithPassword(privatePlain, message.chacha_key.replace("sync:", "")), 'binary').toString('hex');
                }
                if(hasGroup(props.dialog)){
                    /**
                     * Если это групповое сообщение, то получаем ключ группы
                     */
                    decryptKey = await getGroupKey(props.dialog);
                    /**
                     * Приводим к HEX так как этого требует формат расшифровки вложений в приложении
                     */
                    decryptKey = Buffer.from(decryptKey).toString('hex');
                }
                if(!message.from_me && !hasGroup(props.dialog)){
                    /**
                     * Если сообщение не от меня и не групповое,
                     * расшифровываем ключ чачи своим приватным ключом
                     */
                    decryptKey = Buffer.from(await decrypt(message.chacha_key, privatePlain), "binary").toString('hex');
                }   
                finalMessages.push({
                    from_public_key: message.from_public_key,
                    to_public_key: message.to_public_key,
                    content: "__ENCRYPTED__",
                    timestamp: message.timestamp,
                    readed: message.read || message.from_public_key == message.to_public_key,
                    chacha_key: decryptKey,
                    from_me: message.from_me,
                    plain_message: await loadMessage(message.plain_message),
                    delivered: message.delivered,
                    message_id: message.message_id,
                    attachments: await loadAttachments(message.attachments)
                });
               
                if(isMentioned(props.dialog)){
                    /**
                     * Если мы были упомянуты в этом диалоге, то убираем упоминание,
                     * так как мы только что загрузили это сообщение
                     */
                    popMention({
                        dialog_id: props.dialog,
                        message_id: message.message_id
                    });
                }
                
            }
            if(readUpdated){
                updateDialog(props.dialog);
            }
            setMessages(finalMessages);
            setLoading(false);
        })();
    }, [props.dialog]);

    useEffect(() => {
        if(props.dialog == "demo"){
            return;
        }
        if(!messages || messages.length == 0){
            return;
        }
        addOrUpdateDialogCache(props.dialog, messages);
    }, [props.dialog, messages])

    useEffect(() => {
        if(props.dialog == publicKey || messages.length == 0
            || blocked 
            || idle
            || lastMessageTimeRef.current == messages[messages.length - 1].timestamp
            || !focus){
            return;
        }
        if(viewState == ViewPanelsState.DIALOGS_PANEL_ONLY){
            /**
             * Если мы сейчас видим только диалоги
             * то сообщение мы не читаем
             */
            return;
        }
        if(systemAccounts.find(acc => acc.publicKey == props.dialog)){
            /**
             * Системные аккаунты не отмечаем как прочитанные
             */
            return;
        }
        const readPacket = new PacketRead();
        readPacket.setFromPublicKey(publicKey);
        readPacket.setToPublicKey(props.dialog);
        readPacket.setPrivateKey(privateKey);
        send(readPacket);
        log("Send read packet to " + props.dialog);
        info("Send read packet");
        lastMessageTimeRef.current = messages[messages.length - 1].timestamp;
    }, [props.dialog, viewState, focus, messages, blocked, idle]);

    /**
     * Обработчик чтения для личных сообщений
     */
    usePacket(0x07, async (packet : PacketRead) => {
        const fromPublicKey = packet.getFromPublicKey();
        if(fromPublicKey == publicKey){
            /**
             * Если это пакет синхронизации прочтения то игнорируем его здесь, для него есть другой обработчик
             */
            return;
        }
        if(hasGroup(props.dialog)){
            /**
             * Для групп обработка чтения есть ниже
             */
            return;
        }
        if(idle){
            return;
        }
        if(fromPublicKey != props.dialog){
            return;
        }
        setMessages((prev) => prev.map((msg) => {
            if(msg.from_public_key == publicKey && !msg.readed){
                return {
                    ...msg,
                    readed: 1
                }
            }
            return msg;
        }));
        //updateDialog(props.dialog);
    }, [idle, props.dialog]);

    /**
     * Обработчик чтения групповых сообщений
     */
    usePacket(0x07, async (packet : PacketRead) => {
        const fromPublicKey = packet.getFromPublicKey();
        if(fromPublicKey == publicKey){
            /**
             * Если это пакет синхронизации прочтения то игнорируем его здесь, для него есть другой обработчик
             */
            return;
        }
        const toPublicKey = packet.getToPublicKey();
        if(!hasGroup(props.dialog)){
            /**
             * Для личных сообщений обработка чтения выше
             */
            return;
        }
        if(idle){
            return;
        }
        if(toPublicKey != props.dialog){
            return;
        }
        setMessages((prev) => prev.map((msg) => {
            if(msg.from_public_key == publicKey && !msg.readed){
                return {
                    ...msg,
                    readed: 1
                }
            }
            return msg;
        }));
        //updateDialog(props.dialog);
    }, [idle, props.dialog]);

    /**
     * Обработчик синхронизации чтения
     */
    usePacket(0x07, async (packet : PacketRead) => {
        const fromPublicKey = packet.getFromPublicKey();
        const toPublicKey = packet.getToPublicKey();
        if(fromPublicKey != publicKey){
            /**
             * Это не пакет синхронизации, игнорируем
             */
            return;
        }
        if(toPublicKey != props.dialog){
            /**
             * Относится не к этому диалогу
             */
            return;
        }
        setMessages((prev) => prev.map((msg) => {
            if(msg.from_public_key == toPublicKey && !msg.readed){
                return {
                    ...msg,
                    readed: 1
                }
            }
            return msg;
        }));
    }, [publicKey]);

    usePacket(0x08, async (packet : PacketDelivery) => {
        const fromPublicKey = packet.getToPublicKey();
        const messageId = packet.getMessageId();
        if(fromPublicKey != props.dialog){
            return;
        }
        setMessages((prev) => prev.map((msg) => {
            if(msg.message_id == messageId && msg.delivered != DeliveredMessageState.DELIVERED){
                return {
                    ...msg,
                    delivered: DeliveredMessageState.DELIVERED,
                    timestamp: Date.now()
                }
            }
            return msg;
        }));
    }, [props.dialog]);    

    /**
     * Нам приходят сообщения от себя самих же при синхронизации
     * нужно обрабатывать их особым образом соотвественно
     * 
     * Метод нужен для синхронизации своих сообщений
     */
    usePacket(0x06, async (packet: PacketMessage) => {
        const fromPublicKey = packet.getFromPublicKey();
        const toPublicKey = packet.getToPublicKey();
        const aesChachaKey = packet.getAesChachaKey();
        const content = packet.getContent();
        const timestamp = packet.getTimestamp();
        const messageId = packet.getMessageId();
        
        if(fromPublicKey != publicKey){
            /**
             * Игнорируем если это не сообщение от нас
             */
            return;
        }
        if(hasGroup(toPublicKey)){
            /**
             * Есть другой обработчик для синхронизации групп
             */
            return;
        }
        if(toPublicKey != props.dialog) {
            /**
             * Игнорируем если это не сообщение для этого диалога
             */
            return;
        }
        const chachaDecryptedKey = Buffer.from(await decodeWithPassword(privatePlain, aesChachaKey), "binary");
        const key = chachaDecryptedKey.slice(0, 32);
        const nonce = chachaDecryptedKey.slice(32);
        const decryptedContent = await chacha20Decrypt(content, nonce.toString('hex'), key.toString('hex'));

        let attachments: Attachment[] = [];
        for(let i = 0; i < packet.getAttachments().length; i++) {
            const attachment = packet.getAttachments()[i];
            attachments.push({
                ...attachment,
                blob: attachment.type == AttachmentType.MESSAGES ? await decodeWithPassword(chachaDecryptedKey.toString('hex'), attachment.blob) : ""
            });
        }

        const newMessage: Message = {
            from_public_key: fromPublicKey,
            to_public_key: toPublicKey,
            content: content,
            timestamp: timestamp,
            readed: 0, //сообщение прочитано
            chacha_key: chachaDecryptedKey.toString('hex'),
            from_me: 1, //сообщение от нас
            plain_message: (decryptedContent as string),
            delivered: DeliveredMessageState.DELIVERED,
            message_id: messageId,
            attachments: attachments
        };

        setMessages((prev) => ([...prev, newMessage]));
    }, [privatePlain]);    

    /**
     * Обработчик сообщений для синхронизации своих же сообщений в группе
     */
    usePacket(0x06, async (packet: PacketMessage) => {
        const fromPublicKey = packet.getFromPublicKey();
        const toPublicKey = packet.getToPublicKey();

        if(fromPublicKey != publicKey){
            /**
             * Это не синхронизация, игнорируем ее в этом обработчике
             */
            return;
        }        

        if(toPublicKey != props.dialog){
            /**
             * Исправление кросс диалогового сообщения
             */
            return;
        }

        if(!hasGroup(props.dialog)){
            /**
             * Если это не групповое сообщение, то для него есть
             * другой обработчик выше
             */
            return;
        }
        const content = packet.getContent();
        const timestamp = packet.getTimestamp();
        /**
         * Генерация рандомного ID сообщения по SEED нужна для того, 
         * чтобы сообщение записанное здесь в стек сообщений совпадало
         * с тем что записывается в БД в файле useDialogFiber.ts
         */
        const messageId = packet.getMessageId();

        const groupKey = await getGroupKey(toPublicKey);
        if(!groupKey){
            log("Group key not found for group " + toPublicKey);
            error("Message dropped because group key not found for group " + toPublicKey);
            return;
        }

        let decryptedContent = '';

        try{
            decryptedContent = await decodeWithPassword(groupKey, content);
        }catch(e) {
            decryptedContent = '';
        }

        let attachments: Attachment[] = [];
        for(let i = 0; i < packet.getAttachments().length; i++) {
            const attachment = packet.getAttachments()[i];
            attachments.push({
                ...attachment,
                blob: attachment.type == AttachmentType.MESSAGES ? await decodeWithPassword(groupKey, attachment.blob) : ""
            });
        }

        const newMessage : Message = {
            from_public_key: fromPublicKey,
            to_public_key: toPublicKey,
            content: content,
            timestamp: timestamp,
            readed: 0,
            chacha_key: Buffer.from(groupKey).toString('hex'),
            from_me: 1,
            plain_message: decryptedContent,
            delivered: DeliveredMessageState.DELIVERED,
            message_id: messageId,
            attachments: attachments
        };

        setMessages((prev) => ([...prev, newMessage]));
    }, [messages, idle, props.dialog]);        

    /**
     * Обработчик для личных сообщений
     */
    usePacket(0x06, async (packet: PacketMessage) => {
        const fromPublicKey = packet.getFromPublicKey();
        const toPublicKey = packet.getToPublicKey();

        if(fromPublicKey == publicKey){
            /**
             * Это синхронизация, игнорируем ее в этом обработчике
             */
            return;
        }

        if(hasGroup(props.dialog)){
            /**
             * Если это групповое сообщение, то для него есть
             * другой обработчик ниже
             */
            return;
        }
        if(fromPublicKey != props.dialog || toPublicKey != publicKey){
            console.info("From " + fromPublicKey + " to " + props.dialog + " ignore");
            return;
        }        
        if(blocked){
            warn("Message from blocked user, ignore " + fromPublicKey);
            log("Message from blocked user, ignore " + fromPublicKey);
            return;
        }


        const content = packet.getContent();
        const chachaKey = packet.getChachaKey();
        const timestamp = packet.getTimestamp();

        /**
         * Генерация рандомного ID сообщения по SEED нужна для того, 
         * чтобы сообщение записанное здесь в стек сообщений совпадало
         * с тем что записывается в БД в файле useDialogFiber.ts
         */
        const messageId = packet.getMessageId();
        const chachaDecryptedKey = Buffer.from(
            await decrypt(chachaKey, privatePlain), 
            "binary");
        const key = chachaDecryptedKey.slice(0, 32);
        const nonce = chachaDecryptedKey.slice(32);
        const decryptedContent = await chacha20Decrypt(content, nonce.toString('hex'), key.toString('hex'));
        

        let attachments: Attachment[] = [];
        for(let i = 0; i < packet.getAttachments().length; i++) {
            const attachment = packet.getAttachments()[i];
            attachments.push({
                ...attachment,
                blob: attachment.type == AttachmentType.MESSAGES ? await decodeWithPassword(chachaDecryptedKey.toString('hex'), attachment.blob) : ""
            });
        }
        console.info(attachments);
        const newMessage : Message = {
            from_public_key: fromPublicKey,
            to_public_key: toPublicKey,
            content: content,
            timestamp: timestamp,
            readed: idle ? 0 : 1,
            chacha_key: chachaDecryptedKey.toString('hex'),
            from_me: fromPublicKey == publicKey ? 1 : 0,
            plain_message: (decryptedContent as string),
            delivered: DeliveredMessageState.DELIVERED,
            message_id: messageId,
            attachments: attachments
        };

        setMessages((prev) => ([...prev, newMessage]));
    }, [blocked, messages, idle, props.dialog]);

    /**
     * Обработчик для групповых сообщений
     */
    usePacket(0x06, async (packet: PacketMessage) => {
        const fromPublicKey = packet.getFromPublicKey();
        const toPublicKey = packet.getToPublicKey();

        if(fromPublicKey == publicKey){
            /**
             * Это синхронизация, игнорируем ее в этом обработчике
             */
            return;
        }        

        if(toPublicKey != props.dialog){
            /**
             * Исправление кросс диалогового сообщения
             */
            return;
        }

        if(!hasGroup(props.dialog)){
            /**
             * Если это не групповое сообщение, то для него есть
             * другой обработчик выше
             */
            return;
        }
        const content = packet.getContent();
        const timestamp = packet.getTimestamp();
        /**
         * Генерация рандомного ID сообщения по SEED нужна для того, 
         * чтобы сообщение записанное здесь в стек сообщений совпадало
         * с тем что записывается в БД в файле useDialogFiber.ts
         */
        const messageId = packet.getMessageId();

        const groupKey = await getGroupKey(toPublicKey);
        if(!groupKey){
            log("Group key not found for group " + toPublicKey);
            error("Message dropped because group key not found for group " + toPublicKey);
            return;
        }

        let decryptedContent = '';

        try{
            decryptedContent = await decodeWithPassword(groupKey, content);
        }catch(e) {
            decryptedContent = '';
        }

        let attachments: Attachment[] = [];
        for(let i = 0; i < packet.getAttachments().length; i++) {
            const attachment = packet.getAttachments()[i];
            attachments.push({
                ...attachment,
                blob: attachment.type == AttachmentType.MESSAGES ? await decodeWithPassword(groupKey, attachment.blob) : ""
            });
        }

        const newMessage : Message = {
            from_public_key: fromPublicKey,
            to_public_key: toPublicKey,
            content: content,
            timestamp: timestamp,
            readed: idle ? 0 : 1,
            chacha_key: Buffer.from(groupKey).toString('hex'),
            from_me: fromPublicKey == publicKey ? 1 : 0,
            plain_message: decryptedContent,
            delivered: DeliveredMessageState.DELIVERED,
            message_id: messageId,
            attachments: attachments
        };

        setMessages((prev) => ([...prev, newMessage]));
    }, [messages, idle, props.dialog]);    

    /**
     * Расшифровывает сообщение
     * @param message Зашифрованное сообщение
     * @returns Расшифрованное сообщение
     */
    const loadMessage = async (message : string) => {
        try{
            return await decodeWithPassword(privatePlain, message);
        }catch(e){
            return "";
        }
    }

    /**
     * Загружает часть диалога где есть определенный message_id
     */
    const loadMessagesToMessageId = async (messageId: string) => {
        warn("Load messages to message ID " + messageId + " for " + props.dialog);
        if(props.dialog == "DELETED" 
                || privatePlain == "") {
            return;
        }
        let result : any[] = [];
        if(props.dialog != publicKey){
            if(hasGroup(props.dialog)){
                result = await allQuery(`
                    SELECT * FROM (SELECT * FROM messages WHERE (to_public_key = ?) AND account = ? AND timestamp <= (SELECT timestamp FROM messages WHERE message_id = ? AND account = ?) ORDER BY timestamp DESC LIMIT ?) ORDER BY timestamp ASC
                `, [props.dialog, publicKey, messageId, publicKey, MAX_MESSAGES_LOAD]);
            }else{
                result = await allQuery(`
                    SELECT * FROM (SELECT * FROM messages WHERE (from_public_key = ? OR to_public_key = ?) AND (from_public_key = ? OR to_public_key = ?) AND account = ? AND timestamp <= (SELECT timestamp FROM messages WHERE message_id = ? AND account = ?) ORDER BY timestamp DESC LIMIT ?) ORDER BY timestamp ASC
                `, [props.dialog, props.dialog, publicKey, publicKey, publicKey, messageId, publicKey, MAX_MESSAGES_LOAD]);
            }
        }else{
            result = await allQuery(`
                SELECT * FROM (SELECT * FROM messages WHERE from_public_key = ? AND to_public_key = ? AND account = ? AND timestamp <= (SELECT timestamp FROM messages WHERE message_id = ? AND account = ?) ORDER BY timestamp DESC LIMIT ?) ORDER BY timestamp ASC
            `, [publicKey, publicKey, publicKey, messageId, publicKey, MAX_MESSAGES_LOAD]);
        }
        const finalMessages : Message[] = [];
        for(let i = 0; i < result.length; i++){
            const message = result[i];
            let decryptKey = '';
            if(message.from_me){
                /**
                 * Если сообщение от меня, то ключ расшифровки для вложений
                 * не нужен, передаем пустую строку, так как под капотом
                 * в MessageAttachment.tsx при расшифровке вложений используется
                 * локальный ключ, а не тот что в сообщении, так как файл и так находится
                 * у нас локально
                 */
                decryptKey = '';
            }
            if(hasGroup(props.dialog)){
                /**
                 * Если это групповое сообщение, то получаем ключ группы
                 */
                decryptKey = await getGroupKey(props.dialog);
            }
            if(!message.from_me && !hasGroup(props.dialog)){
                /**
                 * Если сообщение не от меня и не групповое,
                 * расшифровываем ключ чачи своим приватным ключом
                 */
                decryptKey = Buffer.from(await decrypt(message.chacha_key, privatePlain), "binary").toString('hex');
            }               
            finalMessages.push({    
                from_public_key: message.from_public_key,
                to_public_key: message.to_public_key,
                content: "__ENCRYPTED__",
                timestamp: message.timestamp,
                readed: message.read || message.from_public_key == message.to_public_key || !message.from_me,
                chacha_key: decryptKey,
                from_me: message.from_me,
                plain_message: await loadMessage(message.plain_message),
                delivered: message.delivered,
                message_id: message.message_id,
                attachments: await loadAttachments(message.attachments)
            });

            if(isMentioned(props.dialog)){
                /**
                 * Если мы были упомянуты в этом диалоге, то убираем упоминание,
                 * так как мы только что загрузили это сообщение
                 */
                popMention({
                    dialog_id: props.dialog,
                    message_id: message.message_id
                });
            }
        }
        if(finalMessages.length == 0) {
            return;
        }
        setMessages([...finalMessages]);
    }

    /**
     * Загружает сообщения в верх диалога, когда пользователь 
     * скроллит вверх и доскроллил до конца
     * @returns 
     */
    const loadMessagesToTop = async () => {
        warn("Load messages to top for " + props.dialog);
        if(props.dialog == "DELETED" 
                || privatePlain == "") {
            return;
        }
        let result : any[] = [];
        if(props.dialog != publicKey){
            if(hasGroup(props.dialog)){
                result = await allQuery(`
                    SELECT * FROM (SELECT * FROM messages WHERE (to_public_key = ?) AND account = ? AND timestamp < ? ORDER BY timestamp DESC LIMIT ?) ORDER BY timestamp ASC
                `, [props.dialog, publicKey, messages.length > 0 ? messages[0].timestamp : Math.floor(Date.now() / 1000), MAX_MESSAGES_LOAD]);
            }else{
                result = await allQuery(`
                    SELECT * FROM (SELECT * FROM messages WHERE (from_public_key = ? OR to_public_key = ?) AND (from_public_key = ? OR to_public_key = ?) AND account = ? AND timestamp < ? ORDER BY timestamp DESC LIMIT ?) ORDER BY timestamp ASC
                `, [props.dialog, props.dialog, publicKey, publicKey, publicKey, messages.length > 0 ? messages[0].timestamp : Math.floor(Date.now() / 1000), MAX_MESSAGES_LOAD]);
            }
        }else{
            result = await allQuery(`
                SELECT * FROM (SELECT * FROM messages WHERE from_public_key = ? AND to_public_key = ? AND account = ? AND timestamp < ? ORDER BY timestamp DESC LIMIT ?) ORDER BY timestamp ASC
            `, [publicKey, publicKey, publicKey, messages.length > 0 ? messages[0].timestamp : Math.floor(Date.now() / 1000), MAX_MESSAGES_LOAD]);
        }
        const finalMessages : Message[] = [];
        for(let i = 0; i < result.length; i++){
            const message = result[i];
            let decryptKey = '';
            if(message.from_me){
                /**
                 * Если сообщение от меня, то ключ расшифровки для вложений
                 * не нужен, передаем пустую строку, так как под капотом
                 * в MessageAttachment.tsx при расшифровке вложений используется
                 * локальный ключ, а не тот что в сообщении, так как файл и так находится
                 * у нас локально
                 */
                decryptKey = '';
            }
            if(hasGroup(props.dialog)){
                /**
                 * Если это групповое сообщение, то получаем ключ группы
                 */
                decryptKey = await getGroupKey(props.dialog);
            }
            if(!message.from_me && !hasGroup(props.dialog)){
                /**
                 * Если сообщение не от меня и не групповое,
                 * расшифровываем ключ чачи своим приватным ключом
                 */
                decryptKey = Buffer.from(await decrypt(message.chacha_key, privatePlain), "binary").toString('hex');
            }               
            finalMessages.push({    
                from_public_key: message.from_public_key,
                to_public_key: message.to_public_key,
                content: "__ENCRYPTED__",
                timestamp: message.timestamp,
                readed: message.read || message.from_public_key == message.to_public_key || !message.from_me,
                chacha_key: decryptKey,
                from_me: message.from_me,
                plain_message: await loadMessage(message.plain_message),
                delivered: message.delivered,
                message_id: message.message_id,
                attachments: await loadAttachments(message.attachments)
            });

            if(isMentioned(props.dialog)){
                /**
                 * Если мы были упомянуты в этом диалоге, то убираем упоминание,
                 * так как мы только что загрузили это сообщение
                 */
                popMention({
                    dialog_id: props.dialog,
                    message_id: message.message_id
                });
            }
        }
        if(finalMessages.length == 0) {
            return;
        }
        setMessages([...finalMessages, ...messages]);
    }

    /**
     * Загружает вложения из JSON строки. 
     * Если вложение не было загружено (то есть его нет на диске),
     * то в blob кладется пустая строка, далее в useAttachment 
     * это отработается (загрузится превью вложение и появится возможность скачать и тд)
     * @param jsonAttachments JSON вложений AttachmentMeta формат
     * @returns Вложения
     */
    const loadAttachments = async (jsonAttachments : string) : Promise<Attachment[]> => {
        if(jsonAttachments == "[]") {
            return [];
        }
        try {
            const attachmentsMeta : AttachmentMeta[] = JSON.parse(jsonAttachments);
            const attachments : Attachment[] = [];
            for(const meta of attachmentsMeta) {
                if(meta.type == AttachmentType.FILE){
                    /**
                     * Все кроме файлов декодируем заранее
                     */
                    attachments.push({
                        ...meta,
                        blob: ""
                    });
                    continue;
                }
                if(meta.type == AttachmentType.CALL){
                    /**
                     * Если это звонок
                     */
                    attachments.push({
                        ...meta,
                        blob: ""
                    });
                    continue;
                }
                const fileData = await readFile(`m/${await generateMd5(meta.id + publicKey)}`);
                if(!fileData) {
                    attachments.push({
                        ...meta,
                        blob: ""
                    });                    
                    continue;
                } 
                let blob = "";
                if(meta.type != AttachmentType.IMAGE){
                    blob = await decodeWithPassword(privatePlain, Buffer.from(fileData, 'binary').toString());
                }
                attachments.push({
                    id: meta.id,
                    blob: blob,
                    type: meta.type,
                    preview: meta.preview,
                    transport: meta.transport
                });
            }
            return attachments;
        }catch(e) {
            console.info(e);
            error("Failed to parse attachments");
        }
        return [];
    }

    /**
     * Дедубликация сообщений по message_id, так как может возникать ситуация, что одно и то же сообщение
     * может загрузиться несколько раз при накладках сети, отставании часов, при синхронизации
     * @param messages массив сообщений
     * @returns массив уникальных сообщений
     */
    const deduplicate = (messages: Message[]) => {
        const map = new Map<string, Message>();
        for(let i = 0; i < messages.length; i++){
            const message = messages[i];
            if(map.has(message.message_id)){
                continue;
            }
            map.set(message.message_id, message);
        }
        return Array.from(map.values());
    }

    return (
        <DialogContext.Provider value={{
            loading,
            messages: deduplicate(messages), 
            setMessages,
            clearDialogCache: () => {
                setDialogsCache(dialogsCache.filter((cache) => cache.publicKey != props.dialog));
            },
            dialog: props.dialog,
            loadMessagesToTop,
            loadMessagesToMessageId
        }}>
            {props.children}
        </DialogContext.Provider> 
    )
}