import { Host, h } from "@stencil/core";
import { chatsStore, configStore, messagesStore, selectedChatStore } from "../../store/store";
import { useTranslation } from "../../utils/utils";
import { Icon } from "../internal-component/icon";
import { getSocket, initSocket, joinRoom, leaveRoom } from "../../utils/socket";
import { typingStore, getCurrentAdmin } from "../../store/typing";
import { getOrFetchTranslation, handleGetChatMessages, handleMapReadMessage, serviceReadMessage, initDB, serviceUpdateChatItem, serviceUpdateMessageItems, } from "../../store/message";
import { getReplyTokenInfo } from "../../api";
import { groupBy } from "../../utils/helpers";
import { displayLongDate } from "../../utils/dayjs";
import { ZButton } from "../internal-component/z-button";
import dayjs from "dayjs";
import { ZIconButton } from "../internal-component/z-icon-button";
import { handleError } from "../../store/dialog";
import { persistMessage, updateMessageSyncState, invalidateMessageSyncState } from "../../utils/cache";
export class MessageBox {
    hostElement;
    disposeListener;
    disposeReadsMapListener;
    replyTokenTimer;
    focusRequested;
    handleClick() {
        this.handleReadMessage();
    }
    onSyncMessageComplete(event) {
        const sessionId = event.detail;
        const selectedChat = selectedChatStore.get('selectedChat');
        if (selectedChat && selectedChat.id === sessionId) {
            this.handleResetMessageCache();
            this.loadMessages({ sessionId });
        }
    }
    isInitial = false;
    isLoadingTop = false;
    isLoadingBottom = false;
    isAtBottom = true;
    replyTokenInfo = null;
    showReplyTokenBanner = false;
    numberOfNewMessages = 0;
    scrollContainer;
    prevSelectedChat = null;
    replyTokenSpan;
    lastScrollTop = 0;
    scrollDirection = null;
    isProgrammaticScroll = false;
    // Timestamp of last chat switch, used to debounce stale socket messages
    // from a room that hasn't fully unsubscribed yet.
    chatSwitchedAt = 0;
    // Memoization cache for flattened messages
    cachedFlatList = null;
    cachedMessagesLength = 0;
    cachedMessageIds = '';
    /**
     * Memoized message flattening - only recalculates when messages actually change
     * Performance: O(N) only on content update, O(1) on re-render.
     */
    getFlattenedMessages(messages) {
        // Quick check: If length hasn't changed and IDs match, return cached
        const messageIds = messages.map(m => m.id).join(',');
        if (this.cachedFlatList && messages.length === this.cachedMessagesLength && messageIds === this.cachedMessageIds) {
            return this.cachedFlatList;
        }
        // Recalculate: Sort and flatten
        // ASC-> -1 : 1 DESC -> 1 : -1
        const sorted = [...messages].sort((a, b) => (dayjs(a.message_at).isBefore(dayjs(b.message_at)) ? 1 : -1));
        const flatList = [];
        const grouped = groupBy(sorted, item => displayLongDate(item.message_at));
        // Optimized O(N) approach:
        const idToIndexMap = new Map(messages.map((m, i) => [m.id, i]));
        Object.entries(grouped).forEach(([date, items]) => {
            items.forEach(item => {
                flatList.push({
                    type: 'message',
                    data: item,
                    index: idToIndexMap.get(item.id) ?? 0
                });
            });
            flatList.push({
                type: 'date',
                date: date,
                id: `time-${date}`
            });
        });
        // Update cache
        this.cachedFlatList = flatList;
        this.cachedMessagesLength = messages.length;
        this.cachedMessageIds = messageIds;
        return flatList;
    }
    componentDidLoad() {
        initDB();
    }
    handleReadMessage = async () => {
        const selectedChat = selectedChatStore.get('selectedChat');
        if (!selectedChat)
            return;
        const matchChat = chatsStore.get('chats').find(item => item.id === selectedChat.id);
        if (!matchChat)
            return;
        if ( /*matchChat.unread_count > 0*/matchChat.is_unread) {
            await serviceReadMessage(selectedChat.id);
        }
    };
    scrollToBottom(isSmooth) {
        setTimeout(() => {
            if (this.scrollContainer) {
                this.scrollContainer.scrollTo({
                    top: this.scrollContainer.scrollHeight,
                    behavior: isSmooth ? 'smooth' : 'instant',
                });
            }
        }, 100);
    }
    async fetchReplyTokenInfo(sessionId, channel) {
        if (channel !== 'line')
            return;
        try {
            const info = await getReplyTokenInfo({ sessionId });
            this.replyTokenInfo = info;
            this.startReplyTokenTimer();
        }
        catch (error) {
            handleError(error, 'Failed to get reply token info');
        }
    }
    // private async handleSetChatLanguage(sessionId: string, newChat: TChatItem, newLanguage: string) {
    //   const language = newLanguage;
    //   const { code } = await updateChat({ sessionId, language })
    //   if (code === 'chat-updated') {
    //     const readChats = chatsStore.get('chats').map((chat) => {
    //       if (chat.id === sessionId) {
    //         return { ...chat, language };
    //       }
    //       return chat;
    //     })
    //     chatsStore.set('chats', readChats);
    //     selectedChatStore.set('selectedChat', { ...newChat, language });
    //   }
    // }
    handleSocketEvents(prevChatId) {
        // Leave old chat room first to stop receiving stale messages
        if (prevChatId) {
            leaveRoom(prevChatId);
        }
        const selectedChat = selectedChatStore.get('selectedChat');
        const chatId = selectedChat.id;
        joinRoom(chatId);
    }
    onMessageEvent = async (message) => {
        try {
            // Ignore messages that arrive within 500ms of a chat switch.
            // These are likely stale events from the old socket room that
            // hasn't fully unsubscribed yet.
            if (Date.now() - this.chatSwitchedAt < 500)
                return;
            const messageId = message.id;
            const existChatIdx = messagesStore.get('messages').findIndex(item => item.id === messageId);
            if (existChatIdx !== -1)
                return;
            const selectedChat = selectedChatStore.get('selectedChat');
            if (!selectedChat)
                return;
            const chatId = selectedChat.id;
            let translatedMessage = message;
            const translationSettings = configStore.get('translationSettings');
            if (message.text) {
                const isAutoTranslate = translationSettings.is_auto &&
                    selectedChat.language !== translationSettings.target_language &&
                    !translationSettings.exclude_languages.includes(selectedChat.language);
                const translatedResponse = await getOrFetchTranslation({
                    chat_id: chatId,
                    message_ids: [messageId],
                    referenceMessages: [message],
                    skipFetch: !isAutoTranslate,
                });
                // Guard: if the user switched chats during translation, discard this message
                const currentChat = selectedChatStore.get('selectedChat');
                if (!currentChat || currentChat.id !== chatId)
                    return;
                const translatedObject = translatedResponse?.messages?.[0];
                if (translatedObject) {
                    translatedMessage = {
                        ...message,
                        translatedText: translatedObject.text,
                        isTranslateFromInput: translatedObject.is_translate_from_input,
                    };
                }
                else if (isAutoTranslate) {
                    // If not in cache and auto is on, it was already handled by getOrFetchTranslation
                }
            }
            // Final guard: ensure we're still on the same chat before updating store
            const finalChat = selectedChatStore.get('selectedChat');
            if (!finalChat || finalChat.id !== chatId)
                return;
            serviceUpdateMessageItems({ updateMessages: [translatedMessage], clearSending: true });
            if (translatedMessage.id && !translatedMessage.isSending) {
                persistMessage(chatId, translatedMessage).then(() => updateMessageSyncState(chatId));
            }
            if (message.sender.type !== 'admin') {
                this.numberOfNewMessages += 1;
            }
            this.fetchReplyTokenInfo(chatId, selectedChat.channel);
        }
        catch (error) {
            handleError(error, 'Failed to process message from socket');
        }
    };
    onReadEvent = (chatRead) => {
        try {
            if (chatRead) {
                const chatId = chatRead.id;
                serviceUpdateChatItem({
                    chatId,
                    updateValue: {
                        // unread_count: 0,
                        is_unread: false,
                        updated_at: chatRead.read_at,
                    },
                });
                const reads = messagesStore.get('reads');
                const readIndex = reads.findIndex(item => item.id === chatId);
                if (readIndex !== -1) {
                    reads[readIndex] = chatRead;
                }
                else {
                    reads.push(chatRead);
                }
                messagesStore.set('reads', reads);
                handleMapReadMessage();
            }
        }
        catch (error) {
            handleError(error, 'Failed to process read message event');
        }
    };
    onTypingUpdateEvent = (users) => {
        const currentAdmin = getCurrentAdmin();
        const filtered = currentAdmin
            ? users.filter(u => u.id !== currentAdmin.id)
            : users;
        typingStore.set('typingAdmins', filtered);
    };
    handleResetMessageCache = () => {
        messagesStore.reset();
        this.prevSelectedChat = null;
        // Reset loading flags so the new chat's load isn't blocked
        this.isLoadingTop = false;
        this.isLoadingBottom = false;
        // Clear render immediately
        this.cachedFlatList = null;
        this.cachedMessagesLength = 0;
        this.cachedMessageIds = '';
    };
    async componentWillLoad() {
        const socket = initSocket();
        socket.on('message', this.onMessageEvent);
        socket.on('read', this.onReadEvent);
        socket.on('typing_update', this.onTypingUpdateEvent);
        this.disposeListener = selectedChatStore.onChange('selectedChat', async (newChat) => {
            if (!newChat) {
                this.handleResetMessageCache();
                return;
            }
            const sessionId = newChat.id;
            const prevSessionId = this.prevSelectedChat ? this.prevSelectedChat.id : '';
            if (sessionId !== prevSessionId) {
                // Leave old room and join new room FIRST to prevent stale socket messages
                this.handleSocketEvents(prevSessionId || undefined);
                // Mark the switch time so onMessageEvent can debounce stale events
                this.chatSwitchedAt = Date.now();
                // If the previous chat had pending (sending) messages, invalidate its cache
                // so a fresh API fetch happens when switching back (the socket confirmation
                // will be missed because we just left that chat's room).
                if (prevSessionId) {
                    const hasPendingMessages = messagesStore.get('messages').some(m => m.isSending);
                    if (hasPendingMessages) {
                        invalidateMessageSyncState(prevSessionId);
                    }
                }
                // IMPORTANT: Clear everything FIRST to remove old DOM nodes immediately
                this.handleResetMessageCache();
                // Clear typing state when switching chats
                typingStore.set('typingAdmins', []);
                selectedChatStore.set('isSyncingMessage', false);
                selectedChatStore.set('isSyncingTag', false);
                // reset reply token info when switching chats
                if (this.replyTokenTimer)
                    window.clearInterval(this.replyTokenTimer);
                this.replyTokenInfo = null;
                this.showReplyTokenBanner = false;
                // if (newChat.language === null) {
                //   await this.handleSetChatLanguage(sessionId, newChat, configStore.get('translationSettings').target_language)
                // }
                await this.loadMessages({ sessionId });
                this.prevSelectedChat = newChat;
                this.focusRequested.emit();
                await this.fetchReplyTokenInfo(sessionId, newChat.channel);
                this.isInitial = true;
            }
            //Always read message when click
            // if (newChat.unread_count > 0) {
            // await serviceReadMessage(sessionId);
            // }
        });
    }
    disconnectedCallback() {
        const socket = getSocket();
        socket.off('message', this.onMessageEvent);
        socket.off('read', this.onReadEvent);
        socket.off('typing_update', this.onTypingUpdateEvent);
        if (this.disposeListener)
            this.disposeListener();
        if (this.disposeReadsMapListener)
            this.disposeReadsMapListener();
        if (this.replyTokenTimer)
            window.clearInterval(this.replyTokenTimer);
        typingStore.set('typingAdmins', []);
        const selectedChat = selectedChatStore.get('selectedChat');
        if (selectedChat) {
            leaveRoom(selectedChat.id);
        }
    }
    updateReplyTokenRemainingDOM() {
        if (!(this.replyTokenInfo && this.replyTokenInfo.expired_at))
            return;
        const now = new Date();
        const expiredAt = new Date(this.replyTokenInfo.expired_at);
        const diffMs = expiredAt.getTime() - now.getTime();
        if (diffMs <= 0) {
            if (this.replyTokenTimer)
                window.clearInterval(this.replyTokenTimer);
            this.replyTokenInfo = null; // reset when expired
            this.showReplyTokenBanner = false;
            return;
        }
        const totalSeconds = Math.floor(diffMs / 1000);
        const minutes = Math.floor(totalSeconds / 60);
        const seconds = totalSeconds % 60;
        const minutesStr = minutes.toString().padStart(2, '0');
        const secondsStr = seconds.toString().padStart(2, '0');
        const remaining = `${minutesStr}:${secondsStr}`;
        if (this.replyTokenSpan) {
            this.replyTokenSpan.textContent = remaining;
        }
    }
    startReplyTokenTimer() {
        if (!this.replyTokenInfo)
            return;
        if (this.replyTokenTimer)
            window.clearInterval(this.replyTokenTimer);
        this.showReplyTokenBanner = (this.replyTokenInfo.total || 0) > 0;
        // prime the DOM value once and then tick
        requestAnimationFrame(() => this.updateReplyTokenRemainingDOM());
        this.replyTokenTimer = window.setInterval(() => this.updateReplyTokenRemainingDOM(), 1000);
    }
    async loadMessages({ startMessageId, next, prev, isNewer = false, sessionId }) {
        if (this.isLoadingTop || this.isLoadingBottom)
            return;
        try {
            if (prev) {
                this.isLoadingTop = true;
            }
            else {
                this.isLoadingBottom = true;
            }
            await handleGetChatMessages({
                startMessageId,
                next,
                prev,
                isNewer,
                sessionId,
            });
        }
        catch (error) {
            handleError(error, 'Failed to load chat messages');
        }
        finally {
            if (prev) {
                this.isLoadingTop = false;
            }
            else {
                this.isLoadingBottom = false;
            }
        }
    }
    handleScroll = async () => {
        if (this.isProgrammaticScroll)
            return;
        const element = this.scrollContainer;
        if (!element)
            return;
        const threshold = 50;
        const { scrollTop, scrollHeight, clientHeight } = element;
        //Detect scroll direction
        if (scrollTop > this.lastScrollTop) {
            this.scrollDirection = 'down';
        }
        else if (scrollTop < this.lastScrollTop) {
            this.scrollDirection = 'up';
        }
        this.lastScrollTop = scrollTop;
        const olderMessages = messagesStore.get('older');
        const newerMessages = messagesStore.get('newer');
        //Check bottom
        this.isAtBottom = Math.floor(scrollTop) === 0;
        if (this.isAtBottom) {
            this.numberOfNewMessages = 0;
            if (!newerMessages || !newerMessages.prev) {
                return;
            }
        }
        //Is Scroll up and Near top → Load older messages
        if (this.scrollDirection === 'up') {
            const olderNextKey = olderMessages ? olderMessages.next : null;
            const minScrollTop = -(scrollHeight - clientHeight);
            const isNearTop = scrollTop <= minScrollTop + threshold;
            if (isNearTop && olderNextKey) {
                await this.loadMessages({ next: olderNextKey });
                return;
            }
        }
        //Is Scroll down and Near bottom → Load newer messages
        if (this.scrollDirection === 'down') {
            const newerPrevKey = newerMessages ? newerMessages.prev : null;
            // With flex-col-reverse: scrollTop = 0 is at bottom, negative is up
            const isNearBottom = scrollTop > -threshold;
            if (isNearBottom) {
                if (newerPrevKey) {
                    await this.loadMessages({ prev: newerPrevKey, isNewer: true });
                }
            }
        }
    };
    goToLastMessage() {
        this.handleResetMessageCache();
        this.scrollToBottom();
        this.loadMessages({});
    }
    componentDidUpdate() {
        const jumpMessageId = messagesStore.get('jumpMessageId');
        if (jumpMessageId) {
            const element = document.getElementById(`message-${jumpMessageId}`);
            if (element) {
                this.isProgrammaticScroll = true;
                // Jumping to middle means we're not at bottom
                this.isAtBottom = false;
                element.scrollIntoView({ behavior: 'smooth', block: 'center' });
                setTimeout(() => {
                    this.isProgrammaticScroll = false;
                    // Recalculate isAtBottom after scroll completes
                    if (this.scrollContainer) {
                        this.isAtBottom = Math.floor(this.scrollContainer.scrollTop) === 0;
                    }
                }, 1000);
                messagesStore.set('jumpMessageId', undefined);
            }
        }
    }
    render() {
        const t = useTranslation();
        const messages = messagesStore.get('messages');
        const selectedChat = selectedChatStore.get('selectedChat');
        const isNotConnected = configStore.get('isNotConnected');
        const isZwiz = configStore.get('isZwiz');
        const isShowWarning = String(configStore.get('isShowWarningNotification')) === 'true';
        const flatList = this.getFlattenedMessages(messages);
        const isReplyTokenInfo = this.showReplyTokenBanner;
        return (h(Host, { key: '0691060e9dc6abb90e23d8899be0803da4dc7efe' }, selectedChat === null ? (h("div", { class: `message-box-wrapper ${isZwiz ? 'z-zwiz-max-height' : 'z-max-height'} ${isShowWarning ? 'has-warning-notification' : ''}` }, h("div", { class: "message-box-no-selected-chat-box" }, h("div", { class: "mx-auto" }, h(Icon, { name: "MessageSquareText", addClass: "size-18" })), isNotConnected ? t('chat_tab.you_not_connect_chat') : t('chat_tab.please_choose_chat_to_reply'), isNotConnected && h(ZButton, { startIcon: h(Icon, { name: "Plus" }), label: t('chat_tab.add_connection'), id: "connect-channel" })))) : (h("div", { class: `message-box-wrapper ${isZwiz ? 'z-zwiz-max-height' : 'z-max-height'} ${isShowWarning ? 'has-warning-notification' : ''}` }, h("chat-header", null), selectedChatStore.get('isSearchMessage') && h("search-message", null), (selectedChatStore.get('isSyncingMessage') || selectedChatStore.get('isSyncingTag')) && (h("div", { class: "message-box-syncing-message" }, selectedChatStore.get('isSyncingMessage') && h("status-badge", { status: "loading", label: t('chat_tab.syncing_message') }), selectedChatStore.get('isSyncingTag') && h("status-badge", { status: "loading", label: t('chat_tab.syncing_tag') }))), h("div", { class: "relative" }, !this.isAtBottom &&
            this.numberOfNewMessages > 0 &&
            h("div", { class: "message-box-new-message" }, t('you_have_new_message'), "\u00A0", h("span", { class: "message-box-new-message-link", onClick: () => this.goToLastMessage() }, t('go_to_last_message')))), h("div", { class: `message-box-container scroll-container ${isReplyTokenInfo ? 'pb-8 pt-4 px-4' : 'p-4 pb-0'}`, id: `message-box-${selectedChat.id}`, ref: el => (this.scrollContainer = el), onScroll: this.handleScroll }, this.isLoadingBottom && (h("div", { id: "loading-bottom", class: "message-box-loading mb-10 mt-2" }, h(Icon, { name: "LoaderCircle", addClass: "message-box-loading-icon" }))), flatList.map(item => {
            if (item.type === 'message') {
                const message = item.data;
                return (h("div", { key: message.id }, h("message-item", { item: message, "data-message-id": message.id })));
            }
            if (item.type === 'date') {
                const date = item.date;
                return (h("div", { key: `time-${date}`, class: "message-box-time-wrapper" }, h("span", { class: "message-box-time bg-time-badge" }, dayjs().tz().format('DD MMM YYYY') === date ? t('today') : date)));
            }
            return null;
        }), this.isLoadingTop && (h("div", { id: "loading-top", class: "message-box-loading mt-10 mb-2" }, h(Icon, { name: "LoaderCircle", addClass: "message-box-loading-icon" })))), h("div", { class: "relative" }, !this.isAtBottom && (h("div", { class: `message-box-scroll-to-bottom ${isReplyTokenInfo ? '-top-24' : '-top-12'}` }, h(ZIconButton, { iconName: "ArrowDown", onClick: () => this.goToLastMessage() }))), isReplyTokenInfo &&
            (() => {
                const template = t('chat_tab.reply_token_info');
                const totalStr = String(this.replyTokenInfo ? this.replyTokenInfo.total : 0);
                const replacedTotal = template.replace('{total}', totalStr);
                const parts = replacedTotal.split('{remaining}');
                const before = parts[0] || '';
                const after = parts[1] || '';
                return (h("div", { class: "message-box-token-wrapper", style: { bottom: 'calc(100% + 8px)' } }, h("span", null, before), h("span", { ref: el => {
                        this.replyTokenSpan = el;
                        this.updateReplyTokenRemainingDOM();
                    }, style: { minWidth: '4ch', display: 'inline-block', textAlign: 'center' } }), h("span", null, after), h("div", { class: "relative -bottom-0.5 inline-flex left-1" }, h("z-tooltip", { text: t('chat_tab.reply_token_info_desc'), position: "top" }, h(ZIconButton, { variant: "text", iconName: "Info", noPaddingY: true, noPadding: true, addClassIcon: "size-4 text-white pointer-events-auto" })))));
            })(), h("typing-indicator", null), h("chat-input", null))))));
    }
    static get is() { return "message-box"; }
    static get originalStyleUrls() {
        return {
            "$": ["message-box.css"]
        };
    }
    static get styleUrls() {
        return {
            "$": ["message-box.css"]
        };
    }
    static get states() {
        return {
            "isInitial": {},
            "isLoadingTop": {},
            "isLoadingBottom": {},
            "isAtBottom": {},
            "replyTokenInfo": {},
            "showReplyTokenBanner": {},
            "numberOfNewMessages": {}
        };
    }
    static get events() {
        return [{
                "method": "focusRequested",
                "name": "focusRequested",
                "bubbles": true,
                "cancelable": true,
                "composed": true,
                "docs": {
                    "tags": [],
                    "text": ""
                },
                "complexType": {
                    "original": "any",
                    "resolved": "any",
                    "references": {}
                }
            }];
    }
    static get elementRef() { return "hostElement"; }
    static get listeners() {
        return [{
                "name": "click",
                "method": "handleClick",
                "target": undefined,
                "capture": false,
                "passive": false
            }, {
                "name": "syncMessageComplete",
                "method": "onSyncMessageComplete",
                "target": "window",
                "capture": false,
                "passive": false
            }];
    }
}
