import {useCallback, useContext, useEffect, useMemo, useRef, useState} from '@wordpress/element';
import {__} from "@wordpress/i18n";
import {SIZES} from '../../includes/constants';
import Header from '../../components/header';
import Body from '../../components/body';
import Footer from '../../components/footer';
import useTheme from '../../hooks/use-theme';
import useScrollIsolation from '../../hooks/use-scroll-isolation';
import useSoundNotification, {clearUnseenMessagesCount} from '../../hooks/use-sound-notification';
import {hexToRgb} from "../../includes/helpers";
import ChatbotUtility from "../../../../utilities/chatbot";
import {
    prepareUserMessage,
    scrollMCToTheBottom,
    dateStringToUtc,
    generateUniqueId,
    getCookie,
    checkIsMobile,
    showWidget
} from "../../../../helpers";
import ChatsHistory from "../../components/body/chats-history";
import Messages from "../../components/body/messages";
import Home from "../../components/home";
import Loading from "../../components/loading";
import {CHAT_ANIMATION_TYPE, CHAT_ANIMATIONS} from "./_data";
import {ChatbotPreviewContext} from "../../../../admin/components/contexts/chatbot-preview";
import {createHooks} from "@wordpress/hooks";
import {ConfirmContext, createConfirm} from "../../components/confirm";
import {convertLinksToHTML} from "../../../../admin/helpers";

if (!LimbChatbot.Hooks) {
    LimbChatbot.Hooks = createHooks();
}

const MESSAGES_DEFAULT_PARAMS = {
    perPage: 100,
    orderBy: 'id',
    order: 'desc',
};

export const IS_MOBILE_DEVICE = checkIsMobile();

export default function Chat({chatbot, close, sizeChanged, viewMode, wpBlockId, onCloseStart, showChatbot, capturedState}) {
    const Chatbot = useRef(null);
    const chatbotPreview = useContext(ChatbotPreviewContext);
    // Get user_uuid from cookies
    const userUuid = getCookie('lbaic_chatbot_user_uuid') || '';
    // Build localStorageKey with user_uuid if available
    const baseKey = viewMode === 'wp-block' ? `lbaic.chatbot-block.${wpBlockId}` : 'lbaic.chatbots';
    const localStorageKey = useRef(userUuid ? `${baseKey}.${userUuid}` : baseKey);
    // States related with chatbot utility
    const [size, setSize] = useState(IS_MOBILE_DEVICE ? 'full' : (chatbot.utility.size || 'md'));
    const [sound, setSound] = useState(Boolean(chatbot.utility.sound));
    const [themeMode, setThemeMode] = useState(chatbot.utility.theme || 'system');
    const theme = useTheme(themeMode);

    const [loading, setLoading] = useState(0);
    const [loadingBar, setLoadingBar] = useState(0);
    const [closing, setClosing] = useState(false);
    const [isSending, setIsSending] = useState(false);
    const [isStreaming, setIsStreaming] = useState(false);
    const [isStreamTyping, setIsStreamTyping] = useState(false);
    const chatInRef = useRef(null);
    const messageListRef = useRef(null);
    const lastScrolledHeight = useRef(0);
    const lastScrollTop = useRef(0);
    const currentChatUuid = useRef(null);
    const [messageList, setMessageList] = useState([]);
    const messageListStateRef = useRef(messageList);
    const [messagesPagination, setMessagesPagination] = useState({
        page: 1,
        per_page: MESSAGES_DEFAULT_PARAMS.perPage,
        total: 0,
    });
    const [isLoadingMore, setIsLoadingMore] = useState(false);
    const [previousMessageCount, setPreviousMessageCount] = useState(0);
    const [isMessagesFetched, setIsMessagesFetched] = useState(false);
    const [chatScreen, setChatScreen] = useState('home');
    const chatScreenRef = useRef('home');
    const widgetsMessagesAddedRef = useRef(false);
    const [lastMessageIsBotError, setLastMessageIsBotError] = useState(false);
    const [lastMessageParameter, setLastMessageParameter] = useState(null);
    const [widgetsLoading, setWidgetsLoading] = useState(false);
    const lastMessageParameterRef = useRef(null);
    const scrolledToTheBottomRef = useRef(false);
    const reloadingMessageUuidRef = useRef(null);
    const conversationStateInProgress = useRef(false);
    // Live agent mode states
    const [isLiveAgentMode, setIsLiveAgentMode] = useState(false);
    const liveAgentSinceRef = useRef(null);
    const liveAgentIntervalRef = useRef(null);
    const liveAgentFetchInProgressRef = useRef(false);
    const liveAgentPollingActiveRef = useRef(false);
    const [shouldShowLiveAgentTyping, setShouldShowLiveAgentTyping] = useState(false);
    const liveAgentTypingTimeoutRef = useRef(null);

    const [showScrollToBottom, setShowScrollToBottom] = useState(false);
    const scrollToBottomDebounceRef = useRef(null);

    const messageSpacerCalculatingRef = useRef(false);

    const messageWidgetsInChatRef = useRef(null);
    const notifyMessageWidgetsCountInChatRef = useRef(0);

    useEffect(() => {
        if (chatbot.utility?.widgets?.length > 0) {
            messageWidgetsInChatRef.current = chatbot.utility.widgets.filter(widget =>
                widget.published
                && widget.type === 'message'
                && showWidget(widget.locations, {screen: 'new_chat'})
            );
            notifyMessageWidgetsCountInChatRef.current = messageWidgetsInChatRef.current.filter(widget => widget.notify).length;
        } else {
            messageWidgetsInChatRef.current = null;
            notifyMessageWidgetsCountInChatRef.current = 0;
        }
    }, [chatbot.utility?.widgets]);

    const messageWidgetsCountToNotify = useMemo(() => {
        if (chatbot.utility?.widgets?.length > 0) {
            return chatbot.utility.widgets.filter(widget =>
                widget.published
                && widget.type === 'message'
                && showWidget(widget.locations, {screen: 'new_chat'})
                && widget.notify
            ).length;
        } else {
            return 0;
        }
    }, [chatbot.uuid, chatbot.utility?.widgets]);

    // Is in preview mode
    const isInPreviewMode = chatbotPreview?.uuid === chatbot?.uuid;

    const [showLeadCaptureWidget, setShowLeadCaptureWidget] = useState(false);
    const sendMessageAfterLeadCaptureRef = useRef(null);
    const leadCaptureWidget = useMemo(() => chatbot.utility?.widgets?.find(widget => widget.published && widget.type === 'lead_capture' && showWidget(widget.locations, {screen: 'new_chat'})), [chatbot.utility?.widgets]);

    useEffect(() => {
        if (!capturedState || isInPreviewMode) {
            if (!messageList.length && leadCaptureWidget) {
                setShowLeadCaptureWidget(true);
            } else {
                setShowLeadCaptureWidget(false);
            }
        } else {
            setShowLeadCaptureWidget(false);
        }
    }, [capturedState, messageList, leadCaptureWidget, isInPreviewMode])

    useEffect(() => {
        messageListStateRef.current = messageList;
    }, [messageList]);

    // Create a confirm function specific to this Chat instance
    const confirm = useCallback(createConfirm(chatInRef), []);

    /**
     * Update chat screen state, ref, and local storage
     * @param {string} v New chat screen value: 'home', 'chat', or 'chat-history'
     */
    const updateChatScreen = useCallback((v) => {
        setChatScreen(v);
        chatScreenRef.current = v;

        // Update local storage (only save 'home' or 'chat', not 'chat-history')
        const chatbotUuid = chatbot?.uuid;
        if (chatbotUuid) {
            if (v === 'home' || v === 'chat') {
                const chatbotsState = JSON.parse(localStorage.getItem(localStorageKey.current) || '{}');
                localStorage.setItem(localStorageKey.current, JSON.stringify({
                    ...chatbotsState,
                    [chatbotUuid]: {
                        ...(chatbotUuid in chatbotsState ? chatbotsState[chatbotUuid] : {}),
                        screen: v
                    }
                }));
            }
        }
    }, [chatbot?.uuid]);

    // Use sound notification hook
    // Sound is enabled based on the sound state (which is initialized from chatbot.utility.sound)
    // isOnChatScreen: true when user is viewing chat (not on home or history screen)
    const isOnChatScreen = showChatbot && chatScreen === 'chat';

    const playNotificationSound = useSoundNotification(chatbot.uuid, sound, isOnChatScreen);

    /**
     * Check if message content has slack_connection with success status
     *
     * @param {Array} content Message content array
     * @return {boolean}
     */
    const hasSlackConnectionSuccess = useCallback((content) => {
        if (!Array.isArray(content)) {
            return false;
        }
        return content.some(item =>
            item.type === 'slack_connection' &&
            item.slack_connection?.value === 'success'
        );
    }, []);

    /**
     * Check if message content has live_agent_disconnection
     *
     * @param {Array} content Message content array
     * @return {boolean}
     */
    const hasLiveAgentDisconnection = useCallback((content) => {
        if (!Array.isArray(content)) {
            return false;
        }
        return content.some(item => item.type === 'live_agent_disconnection');
    }, []);

    /**
     * Stop live agent polling
     */
    const stopLiveAgentPolling = useCallback(() => {
        // Mark polling as inactive first to prevent new timeouts from being scheduled
        liveAgentPollingActiveRef.current = false;
        liveAgentFetchInProgressRef.current = false;

        if (liveAgentIntervalRef.current) {
            clearTimeout(liveAgentIntervalRef.current);
            liveAgentIntervalRef.current = null;
        }
        liveAgentSinceRef.current = null;
        setIsLiveAgentMode(false);
    }, []);

    /**
     * Check scroll position and update button visibility
     * Debounce showing the button (600ms) to prevent it from appearing immediately when new messages load
     */
    const checkScrollPosition = useCallback(() => {
        const container = messageListRef.current;
        if (!container) {
            return;
        }

        const {scrollTop, scrollHeight, clientHeight} = container;

        // Calculate distance from bottom
        const distanceFromBottom = scrollHeight - scrollTop - clientHeight;

        // Clear any existing debounce timeout
        if (scrollToBottomDebounceRef.current) {
            clearTimeout(scrollToBottomDebounceRef.current);
            scrollToBottomDebounceRef.current = null;
        }

        // Show scroll-to-bottom button when scrolled up 60px or more from bottom
        const shouldShow = distanceFromBottom >= 60;

        if (shouldShow) {
            // Debounce showing the button (600ms delay)
            scrollToBottomDebounceRef.current = setTimeout(() => {
                if (!messageSpacerCalculatingRef.current) {
                    setShowScrollToBottom(true);
                }
                scrollToBottomDebounceRef.current = null;
            }, 0);
        } else {
            // Hide immediately when scrolled to bottom
            setShowScrollToBottom(false);
        }
    }, []);

    /**
     * Fetch live agent messages
     * Prevents parallel requests and skips if waiting for AI response
     */
    const fetchLiveAgentMessages = useCallback(async () => {
        // Skip if no chat, no since datetime, fetch already in progress, or waiting for AI response
        const isWaitingForResponse = isSending || isStreaming || isStreamTyping;
        if (!Chatbot.current?.hasChat() || !liveAgentSinceRef.current || liveAgentFetchInProgressRef.current || isWaitingForResponse) {
            // Schedule next poll if polling is still active
            if (liveAgentPollingActiveRef.current) {
                liveAgentIntervalRef.current = setTimeout(fetchLiveAgentMessages, 1000);
            }
            return;
        }

        // Mark fetch as in progress
        liveAgentFetchInProgressRef.current = true;

        let shouldDisconnect = false;

        try {
            const params = {};
            if (isInPreviewMode) {
                params.preview = true;
            }
            const result = await Chatbot.current.getLiveAgentMessages(liveAgentSinceRef.current, params);

            if (result && result.items && result.items.length > 0) {
                // Get existing message UUIDs to avoid duplicates
                const existingUuids = new Set(messageListStateRef.current.map(m => m.uuid).filter(Boolean));

                // Filter out messages that already exist
                const newMessages = result.items.filter(msg => msg.uuid && !existingUuids.has(msg.uuid));

                if (newMessages.length > 0) {
                    // Add new messages to the list
                    setMessageList(prev => [...prev, ...newMessages]);

                    // Update since to the latest message's created_at time
                    const latestMessage = newMessages[newMessages.length - 1];
                    if (latestMessage.created_at) {
                        liveAgentSinceRef.current = latestMessage.created_at;
                    }

                    // Check if any new message contains live_agent_disconnection
                    shouldDisconnect = newMessages.some(msg => hasLiveAgentDisconnection(msg.content));

                    // Play notification sound when live agent messages are received
                    // Only play for messages from the agent (not user messages, not errors)
                    // Check for agent property first (definitive indicator of live agent message),
                    // then fallback to assistant role (all live agent messages should be assistant role)
                    const hasAgentMessages = newMessages.some(msg => {
                        // Skip user messages and error messages
                        if (msg.role === 'user' || msg.hasError) {
                            return false;
                        }
                        // Prioritize checking for agent property (definitive indicator of live agent message)
                        if (msg.agent) {
                            return true;
                        }
                        // Fallback: check if it's an assistant message (all live agent messages are assistant role)
                        return msg.role === 'assistant';
                    });
                    if (hasAgentMessages) {
                        playNotificationSound();
                    }

                    // In live agent mode, scroll to bottom on every new message (user or agent)
                    window.requestAnimationFrame(() => {
                        scrollMCToTheBottom(messageListRef.current, scrolledToTheBottomRef);
                        // Check scroll position after scrolling animation completes
                        setTimeout(() => {
                            checkScrollPosition();
                        }, 200);
                    });
                }
            }
        } finally {
            // Mark fetch as complete
            liveAgentFetchInProgressRef.current = false;

            // If disconnection message received, stop polling
            if (shouldDisconnect) {
                stopLiveAgentPolling();
            } else if (liveAgentPollingActiveRef.current) {
                // Schedule next poll if polling is still active
                liveAgentIntervalRef.current = setTimeout(fetchLiveAgentMessages, 1000);
            }
        }
    }, [isSending, isStreaming, isStreamTyping, hasLiveAgentDisconnection, stopLiveAgentPolling, playNotificationSound, checkScrollPosition, isInPreviewMode]);

    /**
     * Start live agent polling
     *
     * @param {string} sinceDateTime The initial since datetime
     */
    const startLiveAgentPolling = useCallback((sinceDateTime) => {
        // Stop any existing polling first
        if (liveAgentIntervalRef.current) {
            clearTimeout(liveAgentIntervalRef.current);
            liveAgentIntervalRef.current = null;
        }

        // Set the initial since datetime
        liveAgentSinceRef.current = sinceDateTime;
        liveAgentPollingActiveRef.current = true;
        liveAgentFetchInProgressRef.current = false;
        setIsLiveAgentMode(true);

        // Start polling (uses setTimeout for serial requests - next request only after previous completes)
        liveAgentIntervalRef.current = setTimeout(fetchLiveAgentMessages, 1000);
    }, [fetchLiveAgentMessages]);

    // Cleanup live agent polling on unmount or chat change
    useEffect(() => {
        return () => {
            liveAgentPollingActiveRef.current = false;
            if (liveAgentIntervalRef.current) {
                clearTimeout(liveAgentIntervalRef.current);
                liveAgentIntervalRef.current = null;
            }
            // Cleanup scroll-to-bottom button debounce
            if (scrollToBottomDebounceRef.current) {
                clearTimeout(scrollToBottomDebounceRef.current);
                scrollToBottomDebounceRef.current = null;
            }
        };
    }, [chatbot.uuid]);

    // Isolate scroll events from external scroll libraries
    useScrollIsolation(messageListRef);

    // Calculate if there are more messages to load
    const hasMoreMessages = messagesPagination.page * messagesPagination.per_page < messagesPagination.total;

    /**
     * Load more messages for infinite scroll
     */
    const loadMoreMessages = async () => {
        if (hasMoreMessages && !isLoadingMore && !loading) {
            setIsLoadingMore(true);
            await getChatMessages({
                page: messagesPagination.page + 1,
                per_page: messagesPagination.per_page,
                orderby: MESSAGES_DEFAULT_PARAMS.orderBy,
                order: MESSAGES_DEFAULT_PARAMS.order
            });
        }
    };

    /**
     * Get messages
     */
    useEffect(() => {
        // Setup chatbot object
        Chatbot.current = new ChatbotUtility(chatbot.uuid, localStorageKey.current);
        // Reset messages fetched state when chatbot changes
        setIsMessagesFetched(false);
        scrolledToTheBottomRef.current = false;
        // Check chat last state from the local storage
        const chatbotsState = JSON.parse(localStorage.getItem(localStorageKey.current) || '{}');
        // Get active chat
        setActiveChat(chatbotsState[chatbot.uuid]?.chat);
        // UI related
        if (chatbot.uuid in chatbotsState) {
            // Screen - convert old screen value to chatScreen
            const savedScreen = chatbotsState[chatbot.uuid].screen || 'home';
            const initialChatScreen = savedScreen === 'home' ? 'home' : 'chat';
            updateChatScreen(initialChatScreen);
            // Set size
            handleSizeChange(chatbotsState[chatbot.uuid].size);
            // Set theme
            handleThemeChange(chatbotsState[chatbot.uuid].theme);
            // Set sound
            handleSoundChange(chatbotsState[chatbot.uuid].sound);
        } else {
            // Default to home if no saved state
            updateChatScreen('home');
            // Set size
            handleSizeChange(chatbot.utility.size);
            // Set theme
            handleThemeChange(chatbot.utility.theme);
            // Set sound
            handleSoundChange(chatbot.utility.sound);
        }
    }, [chatbot.uuid, updateChatScreen]);

    useEffect(() => {
        // Clear unseen messages count when chatbot is opened and user is on chat screen
        if (isOnChatScreen && chatbot.uuid) {
            clearUnseenMessagesCount(chatbot.uuid);
        }
    }, [chatbot.uuid, isOnChatScreen]);

    /**
     * Init sends message hook
     */
    useEffect(() => {
        const callback = async (result, message, chatbotUuid = null) => {
            // If it is the selected chatbot
            if (chatbotUuid && chatbotUuid !== chatbot.uuid) {
                return result;
            }
            // Send the message if it is valid
            if (typeof message === 'string' && message.trim().length > 0) {
                result = await sendUserRequestRef.current(
                    prepareUserMessage({
                        text: message,
                        images: [],
                        files: [],
                        voice: null
                    }),
                    false,
                    {
                        cancelCurrentAction: true
                    }
                );
            }

            return result;
        };

        LimbChatbot.Hooks.addFilter('lbaic.chatbot.sendMessage', 'lbaic/chatbot/send-message', callback, 11);

        return () => {
            LimbChatbot.Hooks.removeFilter('lbaic.chatbot.sendMessage', 'lbaic/chatbot/send-message');
        }
    }, [chatbot?.uuid]);

    /**
     * Handle size change
     */
    useEffect(() => {
        handleSizeChange(chatbot?.utility?.size || 'md');
    }, [chatbot?.utility?.size]);

    /**
     * Handle theme change
     */
    useEffect(() => {
        if (chatbot?.utility?.theme) {
            handleThemeChange(chatbot.utility.theme);
        }
    }, [chatbot?.utility?.theme]);

    /**
     * Scroll bottom when messages are initially fetched
     */
    useEffect(() => {
        let timeout;
        if (isMessagesFetched && !isStreamTyping) {
            timeout = setTimeout(() => {
                // Ensure animation by setting ref to true before scrolling
                scrolledToTheBottomRef.current = true;
                window.requestAnimationFrame(() => {
                    scrollMCToTheBottom(messageListRef.current, scrolledToTheBottomRef);
                    // Check scroll position after scrolling animation completes
                    setTimeout(() => {
                        checkScrollPosition();
                    }, 200);
                });
            }, 500); // It is for the thinking indicator animation
        }

        return () => {
            if (timeout) {
                clearTimeout(timeout);
            }
        }
    }, [isMessagesFetched, isStreamTyping, isSending, isStreaming, checkScrollPosition]);

    /**
     * Update scroll position check when footer layout changes (e.g., input type changes)
     * No auto-scrolling - user can scroll manually
     */
    useEffect(() => {
        if (isMessagesFetched && messageList.length > 0) {
            // Just update scroll position check, no auto-scrolling
            setTimeout(() => {
                checkScrollPosition();
            }, 50);
        }
    }, [lastMessageParameter, checkScrollPosition]);

    /**
     * Scroll to bottom when chatbot opens and user is on chat screen
     */
    useEffect(() => {
        if (isOnChatScreen && isMessagesFetched && messageListRef.current) {
            // Use requestAnimationFrame to wait for DOM updates after chatbot opens
            window.requestAnimationFrame(() => {
                scrollMCToTheBottom(messageListRef.current, scrolledToTheBottomRef);
                // Check scroll position after scrolling animation completes
                setTimeout(() => {
                    checkScrollPosition();
                }, 200);
            });
        }
    }, [isOnChatScreen, isMessagesFetched, checkScrollPosition]);

    /**
     * Preserve scroll position when loading more messages
     */
    useEffect(() => {
        const container = messageListRef.current;
        if (!container) {
            return;
        }

        const currentMessageCount = messageList.length;

        // If we have more messages than before, we're loading more
        if (currentMessageCount > previousMessageCount && previousMessageCount > 0 && isLoadingMore) {
            // Adjust scroll position to maintain the same relative position
            container.scrollTop = container.scrollHeight - lastScrolledHeight.current;
            setIsLoadingMore(false);
        }

        setPreviousMessageCount(currentMessageCount);
    }, [messageList.length, previousMessageCount, isLoadingMore]);

    /**
     * Handle scroll to load more messages and show/hide scroll-to-bottom button
     */
    const handleScroll = useCallback(() => {
        const container = messageListRef.current;
        if (!container) {
            return;
        }

        // Update button visibility based on scroll position
        checkScrollPosition();

        // Handle loading more messages (only if conditions are met)
        if (!hasMoreMessages || loading > 0 || isLoadingMore) {
            return;
        }

        // Determine scroll direction
        const isScrollingUp = scrollTop < lastScrollTop.current;

        // Update last scroll position
        lastScrollTop.current = scrollTop;
        // Update last scrolled height
        lastScrolledHeight.current = scrollHeight - scrollTop;

        // Only trigger if scrolling up and near the top (within 50px)
        if (isScrollingUp && scrollTop < 50) {
            loadMoreMessages();
        }
    }, [hasMoreMessages, loading, loadMoreMessages, checkScrollPosition]);

    // Add scroll event listener and polling fallback
    useEffect(() => {
        const container = messageListRef.current;
        if (!container) {
            return;
        }

        // Method 1: Try scroll event listener (may be blocked by useScrollIsolation)
        const scrollHandler = () => {
            handleScroll();
        };

        // Try both capture and bubble phases
        container.addEventListener('scroll', scrollHandler, {passive: true, capture: false});
        container.addEventListener('scroll', scrollHandler, {passive: true, capture: true});

        // Method 2: Polling fallback to detect scroll changes (in case scroll events are blocked)
        // This ensures we detect scroll even if useScrollIsolation blocks events
        let lastKnownScrollTop = container.scrollTop;
        let lastKnownScrollHeight = container.scrollHeight;
        let lastKnownClientHeight = container.clientHeight;

        const pollInterval = setInterval(() => {
            if (!container || chatScreenRef.current !== 'chat') {
                return;
            }

            const currentScrollTop = container.scrollTop;
            const currentScrollHeight = container.scrollHeight;
            const currentClientHeight = container.clientHeight;

            // Check if scroll position changed
            if (currentScrollTop !== lastKnownScrollTop ||
                currentScrollHeight !== lastKnownScrollHeight ||
                currentClientHeight !== lastKnownClientHeight) {

                lastKnownScrollTop = currentScrollTop;
                lastKnownScrollHeight = currentScrollHeight;
                lastKnownClientHeight = currentClientHeight;

                handleScroll();
            }
        }, 50); // Check every 50ms for responsive detection

        // Check initial scroll position
        handleScroll();

        // Store for cleanup
        container._scrollHandler = scrollHandler;
        container._scrollPollInterval = pollInterval;

        return () => {
            if (container) {
                container.removeEventListener('scroll', scrollHandler, {passive: true, capture: false});
                container.removeEventListener('scroll', scrollHandler, {passive: true, capture: true});
                if (container._scrollPollInterval) {
                    clearInterval(container._scrollPollInterval);
                }
                delete container._scrollHandler;
                delete container._scrollPollInterval;
            }
        };
    }, [handleScroll, isMessagesFetched, chatScreen, messageList.length]);

    useEffect(() => {
        LimbChatbot.Hooks.addAction('lbaic.chatbot.actions.close', 'lbaic/chatbot/actions', (uuid) => {
            if (uuid === chatbot.uuid) {
                closeChat(uuid);
            }
        });

        LimbChatbot.Hooks.addAction('lbaic.chatbot.actions.changeTheme', 'lbaic/chatbot/actions/theme', (theme, uuid = false) => {
            if (uuid === false || uuid === chatbot.uuid) {
                handleThemeChange(theme);
            }
        });

        LimbChatbot.Hooks.addAction('lbaic.chatbot.open', 'lbaic/chatbot/open', (uuid, screen) => {
            if (uuid === chatbot.uuid) {
                updateChatScreen(screen);
            }
        }, 11);

        return () => {
            LimbChatbot.Hooks.removeAction('lbaic.chatbot.actions.close', 'lbaic/chatbot/actions');
            LimbChatbot.Hooks.removeAction('lbaic.chatbot.actions.changeTheme', 'lbaic/chatbot/actions/theme');
            LimbChatbot.Hooks.removeAction('lbaic.chatbot.open', 'lbaic/chatbot/open');
        }
    }, [chatbot?.uuid]);

    /**
     * Close chat with animation
     */
    const closeChat = (uuid) => {
        if (typeof onCloseStart === 'function') {
            onCloseStart(uuid);
        }
        setClosing(true);
        setTimeout(() => {
            close(uuid);
            setClosing(false);
        }, CHAT_ANIMATIONS[CHAT_ANIMATION_TYPE].duration);
    }

    /**
     * Get chat messages
     * For initial load (reset=true), uses getChatWithMessages to also get live agent status
     *
     * @return {Promise<void>}
     */
    const getChatMessages = async (queryParams = {}, reset = false) => {
        // Mark messages as fetched for initial load
        if (reset) {
            setLoading(prev => prev + 1);
            scrolledToTheBottomRef.current = false;
            setIsMessagesFetched(false);
        }

        let res;
        let messagesData;

        if (reset) {
            // For initial load, use getChatWithMessages to get messages and live agent status
            res = await Chatbot.current.getChatWithMessages();
            if (res === false) {
                // Chat doesn't exist
                setChat(false);
                setIsMessagesFetched(true);
                setLoading(prev => prev - 1);
                return res;
            }

            // Extract messages from the new response structure
            messagesData = res?.messages || {};

            // Check if live agent is active and start polling
            if (res?.is_live_agent_active === true) {
                // Get the latest message's created_at for the since parameter
                // Note: messages are returned with orderby=id&order=desc, so first item is the latest
                const messages = messagesData?.items || [];
                const latestMessage = messages.length > 0 ? messages[0] : null;
                const sinceDateTime = latestMessage?.created_at || dateStringToUtc(new Date().toISOString());
                startLiveAgentPolling(sinceDateTime);
            }
        } else {
            // For pagination, use regular getChatMessages
            res = await Chatbot.current.getChatMessages(queryParams);
            if (res === false) {
                // Chat doesn't exist
                setChat(false);
                return res;
            }
            messagesData = res;
        }

        const messages = messagesData?.items?.length ? messagesData.items : [];
        // For reset (initial load), getChatWithMessages returns messages with order=desc (newest first)
        // For pagination, getChatMessages also uses order=desc
        // So we always reverse when order is desc to show oldest first
        const shouldReverse = reset || queryParams?.order === 'desc';
        const orderedMessages = shouldReverse ? messages.reverse() : messages;

        if (reset) {
            setMessageList(orderedMessages);
        } else {
            setMessageList(prevState => [...orderedMessages, ...prevState]);
        }
        // Update pagination state
        setMessagesPagination(prevState => ({
            ...prevState,
            page: queryParams.page || 1,
            total: messagesData.total || 0
        }));

        // Mark messages as fetched for initial load
        if (reset) {
            setIsMessagesFetched(true);
            setLoading(prev => prev - 1);
        }
        return res;
    }

    /**
     * Get chats
     *
     * @param {object} params Query params
     * @return {Promise<Array|*>}
     */
    const getChats = async (params) => {
        setLoading(prev => prev + 1);
        const res = await Chatbot.current.getChats(params);
        setLoading(prev => prev - 1);
        return res;
    }

    /**
     * Edit chat
     *
     * @param {string} uuid Chat UUID
     * @param {object} data Chat data
     * @return {Promise<*|boolean>}
     */
    const editChat = async (uuid, data) => {
        setLoadingBar(prev => prev + 1);
        const res = await Chatbot.current.updateChat(uuid, data);
        setLoadingBar(prev => prev - 1);
        return res;
    }

    /**
     * Delete chat
     *
     * @param {string} uuid Chat UUID
     * @return {Promise<boolean|undefined|*>}
     */
    const deleteChat = async (uuid) => {
        const confirmed = await confirm(__("Are you sure you want to delete the chat?", 'limb-chatbot'));
        if (!confirmed) {
            return false;
        }

        // Check if deleting the current chat before the API call
        const isDeletingCurrentChat = uuid === currentChatUuid.current;

        setLoadingBar(prev => prev + 1);
        const params = isInPreviewMode ? {preview: true} : {};
        const res = await Chatbot.current.deleteChat(uuid, params);
        setLoadingBar(prev => prev - 1);

        // If the deleted chat was the current chat, reset the UI state
        if (res && isDeletingCurrentChat) {
            // Reset all running states
            setIsSending(false);
            setIsStreaming(false);
            setIsStreamTyping(false);
            // Stop live agent polling if active
            stopLiveAgentPolling();
            // Clear messages
            setMessageList([]);
            // Reset current chat tracking (use false for consistency with setActiveChat)
            currentChatUuid.current = false;
            // Reset scroll and reload refs
            scrolledToTheBottomRef.current = false;
            reloadingMessageUuidRef.current = null;
            // Reset widget messages added state
            widgetsMessagesAddedRef.current = false;
            // Reset message-related states
            setLastMessageIsBotError(false);
            setLastMessageParameter(null);
            lastMessageParameterRef.current = null;
            // Reset pagination and loading states
            setMessagesPagination({
                page: 1,
                per_page: MESSAGES_DEFAULT_PARAMS.perPage,
                total: 0,
            });
            setIsLoadingMore(false);
            // Reset messages fetched state for new chat empty state
            setIsMessagesFetched(true);
        }

        return res;
    }

    /**
     * Set active chat
     *
     * @param {string|*} chatUuid Chat UUID
     * @return {Promise<boolean>}
     */
    const setActiveChat = async (chatUuid) => {
        // Track the current chat UUID
        currentChatUuid.current = chatUuid;
        Chatbot.current.changeChatTo(chatUuid);
        if (chatUuid) {
            const res = await getChatMessages({
                per_page: messagesPagination.per_page,
                orderby: MESSAGES_DEFAULT_PARAMS.orderBy,
                order: MESSAGES_DEFAULT_PARAMS.order
            }, true); // Reset messages when switching chats
            return Boolean(res);
        } else {
            // Play sound for
            if (notifyMessageWidgetsCountInChatRef.current > 0) {
                playNotificationSound(notifyMessageWidgetsCountInChatRef.current);
            }
            widgetsMessagesAddedRef.current = false;
            setIsMessagesFetched(true);
            return false;
        }
    }

    /**
     * Chat selected
     * @param {string|false} chatUuid Chat UUID
     * @param {boolean} goToHome Go to home screen
     */
    const setChat = async (chatUuid, goToHome = true) => {
        // Reset all running states when switching chats
        setIsSending(false);
        setIsStreaming(false);
        setIsStreamTyping(false);
        // Stop live agent polling when switching chats
        stopLiveAgentPolling();

        const res = await setActiveChat(chatUuid);
        if (res) {
            handleChatScreenChange('chat');
        } else {
            if (!chatUuid) {
                setMessageList([]);
                handleChatScreenChange(goToHome ? 'home' : 'chat');
            }
        }
    }

    /**
     * Get AI response
     */
    const runChat = () => {
        // Disable streaming when in live agent mode (messages come from live agent service, not AI streaming)
        if (chatbot.utility?.stream && !isLiveAgentMode) {
            runChatWithStream();
        } else {
            runChatWithSimpleWay();
        }
    }

    /**
     * Get AI response getting chunk by chunk
     *
     * @return {Promise<void>}
     */
    const runChatWithStream = async () => {
        // Wait for a conversation state request to complete if it's in progress
        while (conversationStateInProgress.current) {
            await new Promise(resolve => setTimeout(resolve, 100));
        }

        const operationChatUuid = currentChatUuid.current;
        try {
            setIsSending(true);
            const params = isInPreviewMode ? {preview: true} : {};
            const response = await Chatbot.current.runChat(params, true);
            setIsSending(false);

            if (currentChatUuid.current !== operationChatUuid) {
                return;
            }

            const contentType = response.headers.get('Content-Type') || '';
            const isStreamResponse = contentType.includes('text/event-stream');

            if (response.ok && isStreamResponse) {
                await handleRunStreamingResponse(response, operationChatUuid, undefined);
                return;
            }

            if (!response.ok) {
                const newMessage = {
                    _clientId: `assistant-${generateUniqueId()}`,
                    role: 'assistant',
                    content: [{type: 'text', text: {value: ''}}],
                    created_at: new Date().toISOString(),
                    hasError: true,
                    errorMessage: __("Failed to reply", 'limb-chatbot'),
                };
                try {
                    const result = await response.json();
                    newMessage.errorMessage = result.message ? __(result.message, 'limb-chatbot') : __("Failed to reply", 'limb-chatbot');
                } catch (_) {
                    // ignore parse error
                }
                setMessageList(prevState => [...prevState, newMessage]);
                return;
            }

            // Non-stream JSON response (fallback)
            const data = await response.json();
            const newMessage = {
                _clientId: `assistant-${generateUniqueId()}`,
                role: 'assistant',
                content: data.content || [{type: 'text', text: {value: ''}}],
                created_at: data.created_at || new Date().toISOString(),
                uuid: data.uuid,
                chat_uuid: data.chat_uuid,
                updated_at: data.updated_at,
            };
            setMessageList(prevState => [...prevState, newMessage]);
            playNotificationSound();
            if (!liveAgentSinceRef.current) {
                sendConversationState();
            }
        } catch (e) {
            if (currentChatUuid.current !== operationChatUuid) {
                return;
            }
            const newMessage = {
                _clientId: `assistant-${generateUniqueId()}`,
                role: 'assistant',
                content: [{type: 'text', text: {value: ''}}],
                created_at: new Date().toISOString(),
                hasError: true,
                errorMessage: e.message ? __(e.message, 'limb-chatbot') : __("Failed to reply", 'limb-chatbot'),
            };
            setMessageList(prevState => [...prevState, newMessage]);
        }
    }

    /**
     * Get AI response waiting full response
     */
    const runChatWithSimpleWay = async () => {
        // Wait for a conversation state request to complete if it's in progress
        while (conversationStateInProgress.current) {
            await new Promise(resolve => setTimeout(resolve, 100));
        }

        // Capture the chat UUID at the start of the operation
        const operationChatUuid = currentChatUuid.current;
        setIsSending(true);
        const params = isInPreviewMode ? {preview: true} : {};
        Chatbot.current.runChat(params).then(aiMessage => {
            // Check if chat has changed before adding message
            if (currentChatUuid.current !== operationChatUuid) {
                return;
            }
            // Add AI message in the chatbot UI
            setMessageList(prev => [...prev, aiMessage]);
            setIsSending(false);
            // Play notification sound when AI message is received
            playNotificationSound();
            // Send conversation state update after successful AI response (non-blocking)
            // Skip if in live agent mode
            if (!liveAgentSinceRef.current) {
                sendConversationState();
            }
        }).catch(e => {
            // Check if chat has changed before adding error message
            if (currentChatUuid.current !== operationChatUuid) {
                return;
            }
            const errorMessage = __(e.message, 'limb-chatbot');

            setMessageList(prev => [...prev, {
                _clientId: `error-${generateUniqueId()}`,
                role: 'assistant',
                content: [{
                    type: 'text',
                    text: {
                        value: '',
                    },
                }],
                hasError: true,
                errorMessage: convertLinksToHTML(errorMessage),
            }]);
            setIsSending(false);
        });
    }

    /**
     * Send conversation state request
     * This is called after an added message to update the conversation state
     */
    const sendConversationState = useCallback(async () => {
        if (!Chatbot.current?.hasChat()) {
            return;
        }

        // Don't start a new request if one is already in progress
        if (conversationStateInProgress.current) {
            return;
        }

        conversationStateInProgress.current = true;
        try {
            const params = isInPreviewMode ? {preview: true} : {};
            // await Chatbot.current.updateConversationState(params);
        } catch (e) {
            // Silently fail - this is a non-critical background operation
            console.error('Failed to update conversation state:', e);
        } finally {
            conversationStateInProgress.current = false;
        }
    }, [isInPreviewMode]);

    /**
     * Reload the last AI response
     *
     * @param {string|null} messageUuid Message UUID to reload
     * @return {Promise<void>}
     */
    const reloadChatResponse = useCallback(async (messageUuid = null) => {
        if (!messageUuid || !Chatbot.current?.hasChat()) {
            return;
        }

        const messageIndex = messageListStateRef.current.findIndex(message => message.uuid === messageUuid);
        if (messageIndex === -1) {
            return;
        }

        if (reloadingMessageUuidRef.current) {
            return;
        }

        reloadingMessageUuidRef.current = messageUuid;
        setIsSending(true);

        setMessageList(prev => prev.filter((_, index) => index !== messageIndex));

        while (conversationStateInProgress.current) {
            await new Promise(resolve => setTimeout(resolve, 100));
        }

        const operationChatUuid = currentChatUuid.current;
        const params = {
            reload: 1,
        };
        if (isInPreviewMode) {
            params.preview = true;
        }

        const isStreamingEnabled = chatbot.utility?.stream === true && !isLiveAgentMode;

        try {
            if (isStreamingEnabled) {
                const response = await Chatbot.current.runChat(params, true);

                if (currentChatUuid.current !== operationChatUuid) {
                    return;
                }

                const contentType = response.headers.get('Content-Type') || '';
                const isStreamResponse = contentType.includes('text/event-stream');

                if (response.ok && isStreamResponse) {
                    await handleRunStreamingResponse(response, operationChatUuid, messageIndex);
                    return;
                }

                if (!response.ok) {
                    let errorMessage = __("Failed to reply", 'limb-chatbot');
                    try {
                        const result = await response.json();
                        errorMessage = result.message ? __(result.message, 'limb-chatbot') : errorMessage;
                    } catch (_) {
                        // ignore
                    }
                    setMessageList(prev => {
                        const updated = [...prev];
                        updated.splice(messageIndex, 0, {
                            _clientId: `reload-error-${generateUniqueId()}`,
                            role: 'assistant',
                            content: [{type: 'text', text: {value: ''}}],
                            hasError: true,
                            errorMessage: convertLinksToHTML(errorMessage),
                        });
                        return updated;
                    });
                    return;
                }

                // response.ok but not SSE (e.g. server returned JSON): use body once, no second request
                try {
                    const data = await response.json();
                    const newMessage = {
                        _clientId: `assistant-${generateUniqueId()}`,
                        role: 'assistant',
                        content: data.content || [{type: 'text', text: {value: ''}}],
                        created_at: data.created_at || new Date().toISOString(),
                        uuid: data.uuid,
                        chat_uuid: data.chat_uuid,
                        updated_at: data.updated_at,
                    };
                    setMessageList(prev => {
                        const updated = [...prev];
                        updated.splice(messageIndex, 0, newMessage);
                        return updated;
                    });
                    playNotificationSound();
                    if (!liveAgentSinceRef.current) {
                        sendConversationState();
                    }
                    return;
                } catch (_) {
                    // fall through to non-streaming path if parse fails
                }
            }

            const newMessage = await Chatbot.current.runChat(params);

            if (currentChatUuid.current !== operationChatUuid) {
                return;
            }

            setMessageList(prev => {
                const updated = [...prev];
                const insertIndex = Math.min(messageIndex, updated.length);
                updated.splice(insertIndex, 0, newMessage);
                return updated;
            });

            playNotificationSound();
            if (!liveAgentSinceRef.current) {
                sendConversationState();
            }
        } catch (e) {
            if (currentChatUuid.current !== operationChatUuid) {
                return;
            }

            const errorMessage = convertLinksToHTML(__(e.message, 'limb-chatbot'));

            setMessageList(prev => {
                const updated = [...prev];
                const insertIndex = Math.min(messageIndex, updated.length);
                updated.splice(insertIndex, 0, {
                    _clientId: `reload-error-${generateUniqueId()}`,
                    role: 'assistant',
                    content: [{
                        type: 'text',
                        text: {
                            value: '',
                        },
                    }],
                    hasError: true,
                    errorMessage,
                });
                return updated;
            });
        } finally {
            reloadingMessageUuidRef.current = null;
            setIsSending(false);
        }
    }, [isInPreviewMode, sendConversationState, chatbot.utility?.stream, isLiveAgentMode]);

    const handleAssistantReload = useCallback((message) => {
        if (!message?.uuid) {
            return;
        }
        reloadChatResponse(message.uuid);
    }, [reloadChatResponse]);

    /**
     * Handle streaming response from addMessage
     *
     * @param {Response} response The streaming response
     * @param {string} operationChatUuid The chat UUID at the start of the operation
     * @param {object} tempUserMessage The temporary user message object
     * @return {Promise<{success: boolean, data?: object, message?: string}>}
     */
    const handleStreamingResponse = async (response, operationChatUuid, tempUserMessage) => {
        const result = {
            success: true,
        };

        // Initialize assistant message for streaming
        const assistantMessage = {
            _clientId: `assistant-${generateUniqueId()}`,
            role: 'assistant',
            content: [{
                type: 'text',
                text: {
                    value: '',
                },
            }],
            created_at: new Date().toISOString(),
        };

        let assistantMessageAdded = false;
        let receivedChat = null;
        let receivedUserMessage = null;

        /**
         * Helper function to find the last user message without uuid (temp message)
         * @param {Array} messages The message list
         * @return {number} The index of the last user message without uuid, or -1 if not found
         */
        const findLastTempUserMessageIndex = (messages) => {
            for (let i = messages.length - 1; i >= 0; i--) {
                if (messages[i].role === 'user' && !messages[i].uuid) {
                    return i;
                }
            }
            return -1;
        };

        /**
         * Helper function to ensure assistant message is added to the list
         */
        const ensureAssistantMessageAdded = () => {
            if (!assistantMessageAdded) {
                setMessageList(prev => {
                    const messages = [...prev];
                    const lastUserIndex = findLastTempUserMessageIndex(messages);

                    if (lastUserIndex >= 0) {
                        // Temp user message still exists
                        // If we have received user_message_saved, use it
                        if (receivedUserMessage) {
                            const updatedUserMessage = {
                                ...receivedUserMessage,
                                _clientId: messages[lastUserIndex]._clientId,
                                showCheckIcon: messages[lastUserIndex].showCheckIcon,
                            };
                            return [...messages.slice(0, lastUserIndex), updatedUserMessage, assistantMessage];
                        }
                        // Otherwise keep temp user message and add assistant
                        return [...messages, assistantMessage];
                    }
                    // No temp user message found (already replaced by user_message_saved)
                    // Just append assistant message after the last message
                    return [...messages, assistantMessage];
                });
                assistantMessageAdded = true;
            }
        };

        /**
         * Helper function to update assistant message in the list
         */
        const updateAssistantMessage = () => {
            if (assistantMessageAdded) {
                setMessageList(prev => {
                    const messages = [...prev];
                    // Find the assistant message by _clientId (it might not be at the last index)
                    const messageIndex = messages.findIndex(msg => msg._clientId === assistantMessage._clientId);
                    if (messageIndex >= 0) {
                        return [...messages.slice(0, messageIndex), {...assistantMessage}, ...messages.slice(messageIndex + 1)];
                    }
                    return messages;
                });
            }
        };

        // Track if this is a disconnection message
        const isDisconnectionMessage = hasLiveAgentDisconnection(tempUserMessage.content);

        try {
            setIsStreaming(true);

            // Parse SSE stream
            await parseSSEStream(response.body, (eventType, data) => {
                // Check if chat has changed during streaming
                // Allow transition from false (no chat) to a UUID (chat created) - this is expected for first message
                // Only skip if both are truthy and different (user switched to different chat)
                if (operationChatUuid && currentChatUuid.current && currentChatUuid.current !== operationChatUuid) {
                    return;
                }

                switch (eventType) {
                    case 'text':
                        // Append content to assistant message
                        if (data && typeof data === 'object' && data.content) {
                            assistantMessage.content[0].text.value += data.content;
                            ensureAssistantMessageAdded();
                            updateAssistantMessage();
                        }
                        break;

                    case 'chat':
                        // Save chat info and update currentChatUuid
                        receivedChat = data;
                        if (data && data.uuid) {
                            currentChatUuid.current = data.uuid;
                            // Update Chatbot utility's internal chat UUID so subsequent messages use existing chat
                            if (Chatbot.current) {
                                Chatbot.current.changeChatTo(data.uuid);
                            }
                        }
                        break;

                    case 'chat_created':
                        // Save newly created chat info and update currentChatUuid
                        // This is critical: update the utility immediately so subsequent messages use existing chat
                        receivedChat = data;
                        if (data && data.uuid) {
                            // Update currentChatUuid first
                            currentChatUuid.current = data.uuid;
                            // Update Chatbot utility's internal chat UUID immediately and synchronously
                            // This ensures that if another message is sent during streaming, it uses the existing chat
                            // This must happen before any other processing to prevent creating a new chat
                            if (Chatbot.current) {
                                Chatbot.current.changeChatTo(data.uuid);
                            }
                        }
                        break;

                    case 'user_message_saved':
                        // Save user message info and replace temp user message
                        receivedUserMessage = data;

                        setMessageList(prev => {
                            const messages = [...prev];
                            const lastUserIndex = findLastTempUserMessageIndex(messages);

                            if (lastUserIndex >= 0) {
                                const updatedUserMessage = {
                                    ...data,
                                    _clientId: messages[lastUserIndex]._clientId,
                                    showCheckIcon: messages[lastUserIndex].showCheckIcon,
                                };
                                // Replace temp user message with saved one
                                return [...messages.slice(0, lastUserIndex), updatedUserMessage, ...messages.slice(lastUserIndex + 1)];
                            }
                            return messages;
                        });

                        break;

                    case 'chatbot_message_saved':
                        // Update assistant message with saved data from database
                        // This includes uuid, timestamps, sources, and any other server-side data
                        if (data && typeof data === 'object') {
                            // Use saved content as source of truth (it has the complete content)
                            // Merge saved data with existing assistant message, preserving _clientId
                            Object.assign(assistantMessage, {
                                uuid: data.uuid,
                                chat_uuid: data.chat_uuid,
                                created_at: data.created_at,
                                updated_at: data.updated_at,
                                streamed: data.streamed,
                                sources: data.sources,
                                // Use saved content (complete and correct from server)
                                content: data.content || assistantMessage.content,
                            });

                            // Always ensure message is added (in case text events didn't trigger it)
                            ensureAssistantMessageAdded();

                            // Update the message in the list
                            updateAssistantMessage();
                        }
                        break;

                    case 'parameter':
                        // Handle parameter event - update assistant message content
                        if (data && typeof data === 'object') {
                            // Add parameter to assistant message content
                            if (!assistantMessage.content.some(item => item.type === 'parameter' && item.parameter?.id === data.id)) {
                                assistantMessage.content.push({
                                    type: 'parameter',
                                    parameter: data,
                                });
                                ensureAssistantMessageAdded();
                                updateAssistantMessage();
                            }
                        }
                        break;

                    case 'parameter_value':
                        // Handle parameter value update
                        if (data && typeof data === 'object' && data.parameter_id) {
                            // Update parameter value in assistant message content
                            const paramIndex = assistantMessage.content.findIndex(
                                item => item.type === 'parameter' && item.parameter?.id === data.parameter_id
                            );
                            if (paramIndex >= 0) {
                                assistantMessage.content[paramIndex] = {
                                    ...assistantMessage.content[paramIndex],
                                    parameter: {
                                        ...assistantMessage.content[paramIndex].parameter,
                                        value: data.value,
                                    },
                                };
                                ensureAssistantMessageAdded();
                                updateAssistantMessage();
                            }
                        }
                        break;

                    case 'action_submission':
                        // Handle action submission
                        // Support both formats:
                        // - New format: {action_submission: {message, data_fields, success, ...}}
                        // - Old format: {id, ...} (direct action_submission object)
                        if (data && typeof data === 'object') {
                            // Extract action_submission object (new format has it nested, old format is direct)
                            const actionSubmissionData = data.action_submission || data;

                            // Check if this action submission already exists
                            // Use id if available, otherwise check by message or data_fields to avoid duplicates
                            const alreadyExists = assistantMessage.content.some(item => {
                                if (item.type === 'action_submission' && item.action_submission) {
                                    // If both have id, compare by id
                                    if (item.action_submission.id && actionSubmissionData.id) {
                                        return item.action_submission.id === actionSubmissionData.id;
                                    }
                                    // Otherwise, check if message and data_fields match (for new format)
                                    if (actionSubmissionData.message && item.action_submission.message) {
                                        return item.action_submission.message === actionSubmissionData.message &&
                                            JSON.stringify(item.action_submission.data_fields) === JSON.stringify(actionSubmissionData.data_fields);
                                    }
                                }
                                return false;
                            });

                            if (!alreadyExists) {
                                assistantMessage.content.push({
                                    type: 'action_submission',
                                    action_submission: actionSubmissionData,
                                });
                                ensureAssistantMessageAdded();
                                updateAssistantMessage();
                            }
                        }
                        break;

                    case 'action_cancellation':
                        // Handle action cancellation
                        if (data && typeof data === 'object' && data.action_id) {
                            // Remove or update action in assistant message content
                            assistantMessage.content = assistantMessage.content.filter(
                                item => !(item.type === 'action_submission' && item.action_submission?.id === data.action_id)
                            );
                            ensureAssistantMessageAdded();
                            updateAssistantMessage();
                        }
                        break;

                    case 'live_agent_disconnection':
                        // Handle live agent disconnection
                        if (data && typeof data === 'object') {
                            // Add live agent disconnection to assistant message content
                            assistantMessage.content.push({
                                type: 'live_agent_disconnection',
                                live_agent_disconnection: data,
                            });
                            ensureAssistantMessageAdded();
                            updateAssistantMessage();

                            // Stop live agent polling
                            if (isLiveAgentMode) {
                                stopLiveAgentPolling();
                            }
                        }
                        break;

                    case 'error':
                        // Handle error event (non-fatal)
                        if (data && typeof data === 'object') {
                            assistantMessage.hasError = true;
                            assistantMessage.errorMessage = data.message ? __(data.message, 'limb-chatbot') : __("An error occurred", 'limb-chatbot');
                            ensureAssistantMessageAdded();
                            updateAssistantMessage();
                        }
                        break;

                    case 'fatal_error':
                        // Handle fatal error event
                        if (data && typeof data === 'object') {
                            assistantMessage.hasError = true;
                            assistantMessage.errorMessage = data.message ? __(data.message, 'limb-chatbot') : __("A fatal error occurred", 'limb-chatbot');
                            ensureAssistantMessageAdded();
                            updateAssistantMessage();

                            // Mark result as failed
                            result.success = false;
                            result.message = data.message || __("A fatal error occurred", 'limb-chatbot');
                        }
                        break;

                    case 'slack_connection':
                        // Handle slack connection event
                        if (data && typeof data === 'object') {
                            // Check if slack_connection already exists in content
                            const existingIndex = assistantMessage.content.findIndex(
                                item => item.type === 'slack_connection'
                            );

                            if (existingIndex >= 0) {
                                // Update existing slack_connection
                                assistantMessage.content[existingIndex] = {
                                    type: 'slack_connection',
                                    slack_connection: data,
                                };
                            } else {
                                // Add new slack_connection
                                assistantMessage.content.push({
                                    type: 'slack_connection',
                                    slack_connection: data,
                                });
                            }

                            ensureAssistantMessageAdded();
                            updateAssistantMessage();
                        }
                        break;

                    case 'done':
                        // Handle done event - streaming is complete
                        // This event indicates the stream has finished successfully
                        // No specific action needed here as the stream will naturally end
                        // But we can use this to mark that streaming completed cleanly
                        break;
                }
            });

            setIsStreaming(false);

            // Check if chat has changed before finalizing
            // Allow transition from false (no chat) to a UUID (chat created) - this is expected for first message
            // Only skip if both are truthy and different (user switched to different chat)
            if (operationChatUuid && currentChatUuid.current && currentChatUuid.current !== operationChatUuid) {
                return result;
            }

            // Play notification sound when AI message is complete (if no error)
            if (assistantMessageAdded && !assistantMessage.hasError) {
                playNotificationSound();
            }

            // Send conversation state update after successful AI response (non-blocking)
            // Skip if in live agent mode
            if (!liveAgentSinceRef.current && !assistantMessage.hasError) {
                sendConversationState();
            }

            // Stop live agent polling if disconnection message was successfully sent
            if (isDisconnectionMessage && isLiveAgentMode) {
                stopLiveAgentPolling();
            }

            // Check if response contains slack_connection success - start live agent polling
            if (assistantMessageAdded && hasSlackConnectionSuccess(assistantMessage.content)) {
                const sinceDateTime = assistantMessage.created_at || dateStringToUtc(new Date().toISOString());
                startLiveAgentPolling(sinceDateTime);
            }

            // Set result data
            result.data = {
                chat: receivedChat,
                user_message: receivedUserMessage,
                assistant_message: assistantMessage,
            };

            // Ensure result is marked as successful if we got content (unless there was a fatal error)
            if (assistantMessageAdded && !assistantMessage.hasError) {
                result.success = true;
            }

        } catch (streamError) {
            // Handle streaming error
            setIsStreaming(false);

            // Check if chat has changed before finalizing
            // Allow transition from false (no chat) to a UUID (chat created) - this is expected for first message
            // Only skip if both are truthy and different (user switched to different chat)
            if (operationChatUuid && currentChatUuid.current && currentChatUuid.current !== operationChatUuid) {
                return result;
            }

            // Show error for assistant message
            assistantMessage.hasError = true;
            assistantMessage.errorMessage = streamError.message ? __(streamError.message, 'limb-chatbot') : __("Failed to receive response", 'limb-chatbot');

            if (!assistantMessageAdded) {
                setMessageList(prev => [...prev, assistantMessage]);
            } else {
                setMessageList(prev => {
                    const messages = [...prev];
                    const lastIndex = messages.length - 1;
                    if (lastIndex >= 0 && messages[lastIndex]._clientId === assistantMessage._clientId) {
                        return [...messages.slice(0, lastIndex), {...assistantMessage}];
                    }
                    return messages;
                });
            }

            result.success = false;
            result.message = streamError.message || __("Failed to receive response", 'limb-chatbot');
        }

        return result;
    };

    /**
     * Parse Server-Sent Events (SSE) from a ReadableStream
     *
     * @param {ReadableStream} stream The response stream
     * @param {Function} onEvent Callback function called for each event: (eventType, data) => void
     * @return {Promise<void>}
     */
    const parseSSEStream = async (stream, onEvent) => {
        const reader = stream.getReader();
        const decoder = new TextDecoder();
        let buffer = '';
        let currentEvent = null;
        let currentData = '';

        try {
            while (true) {
                const {done, value} = await reader.read();
                if (done) break;

                // Decode the chunk and add to buffer
                buffer += decoder.decode(value, {stream: true});

                // Process complete lines
                const lines = buffer.split('\n');
                // Keep the last incomplete line in buffer
                buffer = lines.pop() || '';

                for (const line of lines) {
                    if (line.trim() === '') {
                        // Empty line indicates end of event
                        if (currentEvent && currentData) {
                            try {
                                const parsedData = JSON.parse(currentData);
                                onEvent(currentEvent, parsedData);
                            } catch (e) {
                                // If data is not JSON, pass as string
                                onEvent(currentEvent, currentData);
                            }
                        }
                        currentEvent = null;
                        currentData = '';
                    } else if (line.startsWith('event:')) {
                        currentEvent = line.substring(6).trim();
                    } else if (line.startsWith('data:')) {
                        const data = line.substring(5).trim();
                        if (currentData) {
                            currentData += '\n' + data;
                        } else {
                            currentData = data;
                        }
                    }
                }
            }

            // Handle any remaining event in buffer
            if (currentEvent && currentData) {
                try {
                    const parsedData = JSON.parse(currentData);
                    onEvent(currentEvent, parsedData);
                } catch (e) {
                    onEvent(currentEvent, currentData);
                }
            }
        } finally {
            reader.releaseLock();
        }
    };

    /**
     * Handle SSE stream from run chat (initial run or reload).
     * Adds assistant message at the end (insertIndex undefined) or at insertIndex (reload).
     *
     * @param {Response} response The streaming response
     * @param {string} operationChatUuid The chat UUID at the start of the operation
     * @param {number|undefined} insertIndex Index to insert the assistant message (undefined = append at end)
     * @return {Promise<void>}
     */
    const handleRunStreamingResponse = async (response, operationChatUuid, insertIndex) => {
        const assistantMessage = {
            _clientId: `assistant-${generateUniqueId()}`,
            role: 'assistant',
            content: [{
                type: 'text',
                text: {
                    value: '',
                },
            }],
            created_at: new Date().toISOString(),
        };

        let assistantMessageAdded = false;

        const ensureAssistantMessageAdded = () => {
            if (!assistantMessageAdded) {
                setMessageList(prev => {
                    const messages = [...prev];
                    if (insertIndex !== undefined) {
                        messages.splice(insertIndex, 0, assistantMessage);
                        return messages;
                    }
                    return [...messages, assistantMessage];
                });
                assistantMessageAdded = true;
            }
        };

        const updateAssistantMessage = () => {
            if (assistantMessageAdded) {
                setMessageList(prev => {
                    const messages = [...prev];
                    const messageIndex = messages.findIndex(msg => msg._clientId === assistantMessage._clientId);
                    if (messageIndex >= 0) {
                        return [...messages.slice(0, messageIndex), {...assistantMessage}, ...messages.slice(messageIndex + 1)];
                    }
                    return messages;
                });
            }
        };

        try {
            setIsStreaming(true);
            await parseSSEStream(response.body, (eventType, data) => {
                if (operationChatUuid && currentChatUuid.current && currentChatUuid.current !== operationChatUuid) {
                    return;
                }

                switch (eventType) {
                    case 'text':
                        if (data && typeof data === 'object' && data.content) {
                            assistantMessage.content[0].text.value += data.content;
                            ensureAssistantMessageAdded();
                            updateAssistantMessage();
                        }
                        break;

                    case 'chat':
                    case 'chat_created':
                        if (data && data.uuid) {
                            currentChatUuid.current = data.uuid;
                            if (Chatbot.current) {
                                Chatbot.current.changeChatTo(data.uuid);
                            }
                        }
                        break;

                    case 'chatbot_message_saved':
                        if (data && typeof data === 'object') {
                            Object.assign(assistantMessage, {
                                uuid: data.uuid,
                                chat_uuid: data.chat_uuid,
                                created_at: data.created_at,
                                updated_at: data.updated_at,
                                streamed: data.streamed,
                                sources: data.sources,
                                content: data.content || assistantMessage.content,
                            });
                            ensureAssistantMessageAdded();
                            updateAssistantMessage();
                        }
                        break;

                    case 'parameter':
                        if (data && typeof data === 'object') {
                            if (!assistantMessage.content.some(item => item.type === 'parameter' && item.parameter?.id === data.id)) {
                                assistantMessage.content.push({
                                    type: 'parameter',
                                    parameter: data,
                                });
                                ensureAssistantMessageAdded();
                                updateAssistantMessage();
                            }
                        }
                        break;

                    case 'parameter_value':
                        if (data && typeof data === 'object' && data.parameter_id) {
                            const paramIndex = assistantMessage.content.findIndex(
                                item => item.type === 'parameter' && item.parameter?.id === data.parameter_id
                            );
                            if (paramIndex >= 0) {
                                assistantMessage.content[paramIndex] = {
                                    ...assistantMessage.content[paramIndex],
                                    parameter: {
                                        ...assistantMessage.content[paramIndex].parameter,
                                        value: data.value,
                                    },
                                };
                                ensureAssistantMessageAdded();
                                updateAssistantMessage();
                            }
                        }
                        break;

                    case 'action_submission':
                        if (data && typeof data === 'object') {
                            const actionSubmissionData = data.action_submission || data;
                            if (!assistantMessage.content.some(item =>
                                item.type === 'action_submission' && item.action_submission?.id === actionSubmissionData.id
                            )) {
                                assistantMessage.content.push({
                                    type: 'action_submission',
                                    action_submission: actionSubmissionData,
                                });
                                ensureAssistantMessageAdded();
                                updateAssistantMessage();
                            }
                        }
                        break;

                    case 'action_cancellation':
                        if (data && typeof data === 'object' && data.action_id) {
                            assistantMessage.content = assistantMessage.content.filter(
                                item => !(item.type === 'action_submission' && item.action_submission?.id === data.action_id)
                            );
                            ensureAssistantMessageAdded();
                            updateAssistantMessage();
                        }
                        break;

                    case 'live_agent_disconnection':
                        if (data && typeof data === 'object') {
                            assistantMessage.content.push({
                                type: 'live_agent_disconnection',
                                live_agent_disconnection: data,
                            });
                            ensureAssistantMessageAdded();
                            updateAssistantMessage();
                            if (isLiveAgentMode) {
                                stopLiveAgentPolling();
                            }
                        }
                        break;

                    case 'slack_connection':
                        if (data && typeof data === 'object') {
                            const existingIndex = assistantMessage.content.findIndex(item => item.type === 'slack_connection');
                            if (existingIndex >= 0) {
                                assistantMessage.content[existingIndex] = {
                                    type: 'slack_connection',
                                    slack_connection: data,
                                };
                            } else {
                                assistantMessage.content.push({
                                    type: 'slack_connection',
                                    slack_connection: data,
                                });
                            }
                            ensureAssistantMessageAdded();
                            updateAssistantMessage();
                        }
                        break;

                    case 'error':
                    case 'fatal_error':
                        if (data && typeof data === 'object') {
                            assistantMessage.hasError = true;
                            assistantMessage.errorMessage = data.message ? __(data.message, 'limb-chatbot') : __("An error occurred", 'limb-chatbot');
                            ensureAssistantMessageAdded();
                            updateAssistantMessage();
                        }
                        break;
                }
            });

            if (operationChatUuid && currentChatUuid.current && currentChatUuid.current !== operationChatUuid) {
                return;
            }

            if (assistantMessageAdded && !assistantMessage.hasError) {
                playNotificationSound();
            }
            if (!liveAgentSinceRef.current && !assistantMessage.hasError) {
                sendConversationState();
            }
            if (assistantMessageAdded && hasSlackConnectionSuccess(assistantMessage.content)) {
                const sinceDateTime = assistantMessage.created_at || dateStringToUtc(new Date().toISOString());
                startLiveAgentPolling(sinceDateTime);
            }
        } catch (streamError) {
            if (operationChatUuid && currentChatUuid.current && currentChatUuid.current !== operationChatUuid) {
                return;
            }
            assistantMessage.hasError = true;
            assistantMessage.errorMessage = streamError.message ? __(streamError.message, 'limb-chatbot') : __("Failed to receive response", 'limb-chatbot');
            if (!assistantMessageAdded) {
                setMessageList(prev => {
                    const messages = [...prev];
                    if (insertIndex !== undefined) {
                        messages.splice(insertIndex, 0, assistantMessage);
                        return messages;
                    }
                    return [...prev, assistantMessage];
                });
            } else {
                setMessageList(prev => {
                    const messages = [...prev];
                    const messageIndex = messages.findIndex(msg => msg._clientId === assistantMessage._clientId);
                    if (messageIndex >= 0) {
                        return [...messages.slice(0, messageIndex), {...assistantMessage}, ...messages.slice(messageIndex + 1)];
                    }
                    return messages;
                });
            }
        } finally {
            setIsStreaming(false);
        }
    };

    /**
     * Send user request
     *
     * @param {{role: 'user',content: object[]}} message User new request message content
     * @param {boolean} newChat Whether to start a new chat
     * @param {object} info Message send additional information
     */
    const sendUserRequest = async (message, newChat = false, info = {}) => {
        const {cancelCurrentAction, ignoreLeadCapture} = info;

        // Cancel current action
        if (cancelCurrentAction && lastMessageParameterRef.current) {
            if (!message.content.find(item => item.type === 'action_cancellation')) {
                message.content.unshift({
                    type: "action_cancellation",
                });
            }
        }

        handleChatScreenChange('chat');

        // Start a new chat
        if (newChat) {
            await setChat(false, false);
        }

        /**
         * Show the lead capture widget on the new chat
         */
        if (!ignoreLeadCapture && (newChat || !Chatbot.current?.hasChat()) && showLeadCaptureWidget && !capturedState) {
            setShowLeadCaptureWidget(true);
            sendMessageAfterLeadCaptureRef.current = [
                message,
                newChat,
                info
            ];
            return {
                success: true,
                message: __('Lead captured', 'limb-chatbot'),
            };
        }

        // Track if this is a disconnection message
        const isDisconnectionMessage = hasLiveAgentDisconnection(message.content);

        // Add the user message immediately to the UI (without uuid, as a temporary message)
        // Add _clientId to maintain consistent React key and avoid flicker when uuid is added
        const tempUserMessage = {
            ...message,
            _clientId: `temp-${generateUniqueId()}`,
            created_at: new Date().toISOString(),
        };

        // Remove the last AI error message or not saved message if exists before adding a user message
        setMessageList(prev => {
            const lastIndex = prev.length - 1;

            const widgetMessages = [];

            // If sent the first message
            if (newChat || !prev.filter(item => !item.hasError).length) {
                if (!widgetsMessagesAddedRef.current) {
                    // Convert widget messages to assistant message format
                    if (messageWidgetsInChatRef.current?.length) {
                        const content = messageWidgetsInChatRef.current.reduce((acc, widget) => [
                            ...acc,
                            ...widget.data.content
                        ], []);

                        widgetMessages.push(
                            {
                                _clientId: `widget-${generateUniqueId()}`,
                                role: 'assistant',
                                content: content,
                                created_at: new Date().toISOString(),
                                isWidgetMessage: true, // Flag to identify widget-originated messages
                            }
                        );
                    }
                    widgetsMessagesAddedRef.current = true;
                }
            }

            // Check needs to show the check icon
            if (prev.length) {
                const lastAIMessage = prev.findLast(item => item.role === 'assistant');
                if (Array.isArray(lastAIMessage?.content)) {
                    tempUserMessage.showCheckIcon = lastAIMessage?.content.some(item => item && typeof item === 'object' && item.type === 'parameter');
                }
            }

            if (lastIndex >= 0 && ((prev[lastIndex].role === 'assistant' && prev[lastIndex].hasError) || !prev[lastIndex].uuid)) {
                return [...widgetMessages, ...prev.slice(0, lastIndex), tempUserMessage];
            }

            return [...widgetMessages, ...prev, tempUserMessage];
        });

        // Show a typing indicator while waiting for a response
        setIsSending(true);

        // Wait for a conversation state request to complete if it's in progress
        while (conversationStateInProgress.current) {
            await new Promise(resolve => setTimeout(resolve, 100));
        }

        // Check if streaming is enabled
        // Disable streaming when in live agent mode (messages come from live agent service, not AI streaming)
        const isStreamingEnabled = chatbot.utility?.stream === true && !isLiveAgentMode;

        // Capture the chat UUID at the start of the operation
        const operationChatUuid = currentChatUuid.current;

        // Add a new message
        const result = {
            success: true,
        };

        try {
            // Send a message
            const params = isInPreviewMode ? {preview: true} : {};
            const response = await Chatbot.current.addMessage(message.content, params, isStreamingEnabled);

            // Check if response is a Response object (streaming)
            if (response instanceof Response) {
                // Check Content-Type header to confirm it's a stream
                const contentType = response.headers.get('Content-Type') || '';
                const isStreamResponse = contentType.includes('text/event-stream');

                if (isStreamResponse && response.ok) {
                    // Handle streaming response using dedicated function
                    const streamResult = await handleStreamingResponse(response, operationChatUuid, tempUserMessage);
                    Object.assign(result, streamResult);
                    // Mark as handled to prevent falling through to error handling
                    result.streamingHandled = true;
                } else if (!response.ok) {
                    // Stream response but not OK - try to parse error
                    try {
                        const errorData = await response.json();
                        result.success = false;
                        result.message = errorData.message ? __(errorData.message, 'limb-chatbot') : __("Failed to send message", 'limb-chatbot');
                        result.code = errorData.code;
                    } catch (e) {
                        result.success = false;
                        result.message = __("Failed to send message", 'limb-chatbot');
                    }
                } else {
                    // Response is a Response object but not a stream - fallback to normal handling
                    // This shouldn't happen if streaming is properly configured, but handle gracefully
                    try {
                        result.data = await response.json();
                    } catch (e) {
                        result.success = false;
                        result.message = __("Failed to parse response", 'limb-chatbot');
                    }
                }
            } else {
                // Non-streaming response (existing behavior)
                result.data = response;
            }
        } catch (e) {
            result.success = false;
            result.message = __(e.message, 'limb-chatbot');
            result.code = e.code;
            // Delete chat if no messages yet (only for user message errors)
            const isUserMessageError = e.code >= 11000 && e.code <= 11099;
            if (isUserMessageError && !messageList.filter(item => !item.hasError).length) {
                const deleteParams = isInPreviewMode ? {preview: true} : {};
                await Chatbot.current.deleteChat(false, deleteParams);
            }
        }

        // Handle the response (only for non-streaming responses, or streaming that wasn't handled)
        // Skip if streaming was already handled (message list already updated in handleStreamingResponse)
        if (result.success && result.data && !isStreamingEnabled && !result.streamingHandled) {
            // Check if response is from chat creation (new format with nested message)
            // New format: {uuid: chatUuid, message: {previous_message, ...assistantData}}
            // Old format: {previous_message, ...assistantData}
            let previous_message;
            let assistantMessageData;

            if (result.data.message && typeof result.data.message === 'object') {
                // New format: chat was just created, response includes chat details
                // Update current chat UUID (ChatbotUtility already set its internal chat_uuid in createChat)
                if (result.data.uuid) {
                    currentChatUuid.current = result.data.uuid;
                }
                // Extract previous_message and assistant message from nested structure
                const {previous_message: prevMsg, ...assistantData} = result.data.message;
                previous_message = prevMsg;
                assistantMessageData = assistantData;
            } else {
                // Old format: chat already exists, extract from top level
                const {previous_message: prevMsg, ...assistantData} = result.data;
                previous_message = prevMsg;
                assistantMessageData = assistantData;
            }

            // Check if a user message was saved successfully
            if (previous_message && previous_message.uuid) {
                // Replace a temp user message with the saved one and add an assistant message
                setMessageList(prev => {
                    // Find and replace the temp user message
                    const messages = [...prev];
                    // Check if need to show check icon
                    let showCheckIcon = false;
                    if (messages.length) {
                        const lastAIMessage = messages.findLast(item => item.role === 'assistant');
                        if (Array.isArray(lastAIMessage?.content)) {
                            showCheckIcon = lastAIMessage?.content.some(item => item && typeof item === 'object' && item.type === 'parameter');
                        }
                    }

                    const lastUserIndex = messages.length - 1;

                    if (lastUserIndex >= 0 && messages[lastUserIndex].role === 'user' && !messages[lastUserIndex].uuid) {
                        // Preserve _clientId from temp message to prevent flicker, merge with server data
                        const updatedUserMessage = {
                            ...previous_message,
                            _clientId: messages[lastUserIndex]._clientId,
                            showCheckIcon,
                        };
                        // Add _clientId to assistant message
                        const updatedAssistantMessage = {
                            ...assistantMessageData,
                            _clientId: `assistant-${generateUniqueId()}`,
                        };
                        return [...messages.slice(0, lastUserIndex), updatedUserMessage, updatedAssistantMessage];
                    }
                    // Fallback: just append both messages with _clientId for assistant
                    return [...messages, {
                        ...previous_message,
                        showCheckIcon,
                    }, {
                        ...assistantMessageData,
                        _clientId: `assistant-${generateUniqueId()}`,
                    }];
                });

                // Play notification sound when AI message is received
                playNotificationSound();

                // Check if response contains slack_connection success - start live agent polling
                if (hasSlackConnectionSuccess(assistantMessageData.content)) {
                    // Start polling from the assistant message's created_at time
                    // Use dateStringToUtc to get current UTC time as fallback
                    const sinceDateTime = assistantMessageData.created_at || dateStringToUtc(new Date().toISOString());
                    startLiveAgentPolling(sinceDateTime);
                } else if (!liveAgentSinceRef.current) {
                    // Send conversation state update (non-blocking) - only when not in live agent mode
                    sendConversationState();
                }

                // Stop live agent polling if disconnection message was successfully sent
                if (isDisconnectionMessage && isLiveAgentMode) {
                    stopLiveAgentPolling();
                }
            } else if (result.data.role === 'user' && result.data.uuid) {
                // Live agent mode: response is the saved user message directly
                // Update the temp user message with uuid and timestamps (merge to avoid re-render flicker)
                setMessageList(prev => {
                    const messages = [...prev];
                    const lastUserIndex = messages.length - 1;

                    if (lastUserIndex >= 0 && messages[lastUserIndex].role === 'user' && !messages[lastUserIndex].uuid) {
                        // Merge uuid and timestamps into existing message to avoid visual flicker
                        const updatedUserMessage = {
                            ...messages[lastUserIndex],
                            uuid: result.data.uuid,
                            created_at: result.data.created_at,
                            updated_at: result.data.updated_at,
                        };
                        return [...messages.slice(0, lastUserIndex), updatedUserMessage];
                    }
                    return messages;
                });

                // In live agent mode, scroll to bottom when user message is saved
                if (isLiveAgentMode) {
                    window.requestAnimationFrame(() => {
                        scrollMCToTheBottom(messageListRef.current, scrolledToTheBottomRef);
                        // Check scroll position after scrolling animation completes
                        setTimeout(() => {
                            checkScrollPosition();
                        }, 200);
                    });
                }

                // Stop live agent polling if disconnection message was successfully sent
                if (isDisconnectionMessage && isLiveAgentMode) {
                    stopLiveAgentPolling();
                }
            } else {
                // User message failed to save, mark it with error
                const errorUserMessage = {
                    ...tempUserMessage,
                    hasError: true,
                    errorMessage: __("Failed to send message", 'limb-chatbot'),
                };
                setMessageList(prev => {
                    const messages = [...prev];
                    const lastUserIndex = messages.length - 1;

                    if (lastUserIndex >= 0 && messages[lastUserIndex].role === 'user' && !messages[lastUserIndex].uuid) {
                        return [...messages.slice(0, lastUserIndex), errorUserMessage];
                    }
                    return [...messages, errorUserMessage];
                });
            }
        } else if (!result.streamingHandled) {
            // Request failed, check error code to determine an error type
            // Skip error handling if streaming was already handled
            const isUserMessageError = result.code >= 11000 && result.code <= 11099;

            if (isUserMessageError) {
                // User message error (code 11000-11099): mark temp user message with error
                const errorUserMessage = {
                    ...tempUserMessage,
                    hasError: true,
                    errorMessage: result.message || __("Failed to send message", 'limb-chatbot'),
                };
                setMessageList(prev => {
                    const messages = [...prev];
                    const lastUserIndex = messages.length - 1;

                    if (lastUserIndex >= 0 && messages[lastUserIndex].role === 'user' && !messages[lastUserIndex].uuid) {
                        return [...messages.slice(0, lastUserIndex), errorUserMessage];
                    }
                    return [...messages, errorUserMessage];
                });
            } else {
                // Assistant error (code not in 11000-11099): user message saved but assistant failed
                // to Keep the temp user message (it was saved) and add an assistant error message
                const assistantErrorMessage = {
                    _clientId: `assistant-error-${generateUniqueId()}`,
                    role: 'assistant',
                    content: [{
                        type: 'text',
                        text: {
                            value: '',
                        },
                    }],
                    hasError: true,
                    errorMessage: convertLinksToHTML(result.message || __("Failed to reply", 'limb-chatbot')),
                };
                setMessageList(prev => [...prev, assistantErrorMessage]);
            }
        }

        // Stop typing indicator (will be set to false when response completes or errors)
        setIsSending(false);

        return result;
    }

    const sendUserRequestRef = useRef(sendUserRequest);

    useEffect(() => {
        sendUserRequestRef.current = sendUserRequest;
    }, [sendUserRequest]);

    /**
     * Clear chat button handler
     */
    const clearChat = async () => {
        const confirmed = await confirm(__("Are you sure you want to clear the chat?", 'limb-chatbot'));
        if (!confirmed) {
            return false;
        }

        setLoadingBar(prev => prev + 1);
        const params = isInPreviewMode ? {preview: true} : {};
        const res = await Chatbot.current.clearChat(false, params);
        if (res) {
            setMessageList([]);
        }
        setLoadingBar(prev => prev - 1);
        return res;
    }

    const chatbotRequestFailedCallback = () => {
        // Remove the last error message before retrying
        setMessageList(prev => {
            // Find and remove the last assistant message with error
            const lastIndex = prev.length - 1;
            if (lastIndex >= 0 && prev[lastIndex].role === 'assistant' && prev[lastIndex].hasError) {
                return prev.slice(0, lastIndex);
            }
            return prev;
        });

        // Check if chat was auto-deleted (no chat UUID exists)
        if (!Chatbot.current?.hasChat()) {
            // Chat was auto-deleted, need to resend the original user message to create a new chat
            // Find the last user message
            const lastUserMessage = messageListStateRef.current.findLast(msg => msg.role === 'user' && msg.content);
            if (lastUserMessage && lastUserMessage.content) {
                // Resend the user message (this will create a new chat)
                sendUserRequest({
                    role: 'user',
                    content: lastUserMessage.content
                });
            }
        } else {
            // Chat exists, retry the chat normally
            runChat();
        }
    }

    const userRequestFailedCallback = (data) => {
        // Remove the failed user message before retrying
        setMessageList(prev => {
            // Find and remove the last assistant message with error
            const lastIndex = prev.length - 1;
            if (lastIndex >= 0 && prev[lastIndex].role === 'user' && prev[lastIndex].hasError) {
                return prev.slice(0, lastIndex);
            }
            return prev;
        });
        // Resend the user message
        sendUserRequest(data);
    }

    /**
     * Handle chat screen change
     * @param {string} newChatScreen New chat screen value: 'home', 'chat', or 'chat-history'
     */
    const handleChatScreenChange = useCallback((newChatScreen) => {
        if (newChatScreen === chatScreenRef.current) {
            return;
        }

        // Validate the screen value
        const validScreens = ['home', 'chat', 'chat-history'];
        if (!validScreens.includes(newChatScreen)) {
            return;
        }

        const previousChatScreen = chatScreenRef.current;

        // Update state, ref, and local storage
        updateChatScreen(newChatScreen);

        const chatbotUuid = chatbot?.uuid;
        if (chatbotUuid) {
            // Clear unseen messages count when navigating to chat screen
            if (newChatScreen === 'chat') {
                clearUnseenMessagesCount(chatbotUuid);
            }
        }

        // Scroll to bottom when returning from home or history screen to chat
        if ((previousChatScreen === 'home' || previousChatScreen === 'chat-history') &&
            newChatScreen === 'chat' &&
            messageListRef.current) {
            window.requestAnimationFrame(() => {
                scrollMCToTheBottom(messageListRef.current, scrolledToTheBottomRef);
                // Check scroll position after scrolling animation completes
                setTimeout(() => {
                    checkScrollPosition();
                }, 200);
            });
        }
    }, [chatbot?.uuid, checkScrollPosition]);

    const handleSizeChange = useCallback((newSize) => {
        if (IS_MOBILE_DEVICE) {
            return;
        }
        if (newSize === size || !SIZES.includes(newSize)) {
            return;
        }
        // Update size state
        setSize(newSize);

        // Add scroll event handler in messages container
        if (messageListRef.current) {
            // Scroll to the bottom
            messageListRef.current.scrollTop = messageListRef.current.scrollHeight;
        }

        const chatbotUuid = chatbot?.uuid;
        if (chatbotUuid) {
            // Keep the size state in local storage
            if (!IS_MOBILE_DEVICE) {
                const prevState = JSON.parse(localStorage.getItem(localStorageKey.current) || '{}');
                localStorage.setItem(localStorageKey.current, JSON.stringify({
                    ...prevState,
                    [chatbotUuid]: {
                        ...(prevState[chatbotUuid] || {}),
                        size: newSize,
                    }
                }));
            }
            // Size change callback
            typeof sizeChanged === 'function' && sizeChanged(chatbotUuid, newSize);
        }
    }, [size, chatbot?.uuid]);

    const toggleSize = useCallback(() => {
        handleSizeChange(SIZES[(SIZES.indexOf(size) + 1) % SIZES.length]);
    }, [size]);

    const handleThemeChange = useCallback((newTheme) => {
        if (!newTheme || newTheme === themeMode) {
            return;
        }
        // Validate theme value
        const validThemes = ['light', 'dark', 'system'];
        if (!validThemes.includes(newTheme)) {
            return;
        }
        // Update theme state
        setThemeMode(newTheme);

        const chatbotUuid = chatbot?.uuid;
        if (chatbotUuid) {
            // Keep the theme state in local storage
            const prevState = JSON.parse(localStorage.getItem(localStorageKey.current) || '{}');
            localStorage.setItem(localStorageKey.current, JSON.stringify({
                ...prevState,
                [chatbotUuid]: {
                    ...(prevState[chatbotUuid] || {}),
                    theme: newTheme,
                }
            }));
        }
    }, [themeMode, chatbot?.uuid]);

    const handleSoundChange = useCallback((newSound) => {
        // Don't do anything when not defined properly
        if (typeof newSound !== 'boolean') {
            return;
        }
        if (!chatbot?.utility?.sound) {
            if (newSound) {
                // Don't allow turning ON
                return;
            }
        }
        // If there is nothing to change then return
        if (newSound === sound) {
            return;
        }
        // Update sound state
        setSound(newSound);

        const chatbotUuid = chatbot?.uuid;
        if (chatbotUuid) {
            // Keep the sound state in local storage
            const prevState = JSON.parse(localStorage.getItem(localStorageKey.current) || '{}');
            localStorage.setItem(localStorageKey.current, JSON.stringify({
                ...prevState,
                [chatbotUuid]: {
                    ...(prevState[chatbotUuid] || {}),
                    sound: newSound,
                }
            }));
        }
    }, [sound, chatbot?.uuid, chatbot?.utility?.sound]);

    const handleLastMessageBotError = (isBotError) => {
        setLastMessageIsBotError(isBotError);
    }

    const handleLastMessageParameter = (parameter) => {
        setLastMessageParameter(parameter);
        lastMessageParameterRef.current = parameter;
    }

    /**
     * Handle scroll to bottom button click
     */
    const handleScrollToBottom = useCallback(() => {
        if (messageListRef.current) {
            // Set ref to enable animation
            scrolledToTheBottomRef.current = true;
            scrollMCToTheBottom(messageListRef.current, scrolledToTheBottomRef);
            // Check scroll position after scrolling animation completes
            setTimeout(() => {
                checkScrollPosition();
            }, 200);
        }
    }, [checkScrollPosition]);

    /**
     * Determine if we should show live agent typing indicator
     * Show when: in live agent mode, last message is from user (successfully sent with uuid), and no agent response received yet
     * Show with 500ms delay after message is sent successfully
     */
    useEffect(() => {
        // Clear any existing timeout
        if (liveAgentTypingTimeoutRef.current) {
            clearTimeout(liveAgentTypingTimeoutRef.current);
            liveAgentTypingTimeoutRef.current = null;
        }

        if (!isLiveAgentMode || messageList.length === 0) {
            setShouldShowLiveAgentTyping(false);
            return;
        }

        const lastMessage = messageList[messageList.length - 1];
        // Check if last message is from user and has uuid (successfully sent)
        const lastMessageIsFromUser = lastMessage.role === 'user' && Boolean(lastMessage.uuid);

        if (lastMessageIsFromUser) {
            // Show typing indicator after 500ms delay
            liveAgentTypingTimeoutRef.current = setTimeout(() => {
                setShouldShowLiveAgentTyping(true);
            }, 500);
        } else {
            // Hide immediately if last message is not from user or doesn't have uuid
            setShouldShowLiveAgentTyping(false);
        }

        // Cleanup timeout on unmount or when dependencies change
        return () => {
            if (liveAgentTypingTimeoutRef.current) {
                clearTimeout(liveAgentTypingTimeoutRef.current);
                liveAgentTypingTimeoutRef.current = null;
            }
        };
    }, [isLiveAgentMode, messageList]);

    return (
        <div
            className={`lbaic-chat lbaic-${theme} lbaic-${size} ${CHAT_ANIMATIONS[CHAT_ANIMATION_TYPE].open}${closing ? ' ' + CHAT_ANIMATIONS[CHAT_ANIMATION_TYPE].close : ''}`}
            style={{
                'display': !showChatbot ? 'none' : '',
                '--lbaic-main': chatbot.utility.color,
                '--lbaic-main-rgb': hexToRgb(chatbot.utility.color),
            }}
        >
            <div ref={chatInRef} className='lbaic-chat-in'>
                <ConfirmContext.Provider value={confirm}>
                    <Home
                        chatbot={chatbot}
                        close={closeChat}
                        newChat={() => setChat(false, false)}
                        getChats={async (params) => await Chatbot.current.getChats(params)}
                        chatSelected={setChat}
                        sendMessage={sendUserRequest}
                        size={{
                            get: size,
                            set: toggleSize,
                        }}
                        chatScreen={chatScreen}
                        setChatScreen={handleChatScreenChange}
                        loading={loading}
                        setLoading={setLoading}
                        viewMode={viewMode}
                    />
                    <div style={{display: chatScreen === 'home' ? 'none' : undefined}} className='lbaic-chat-inner'>
                        <Header
                            chatbot={chatbot}
                            close={closeChat}
                            clearChat={clearChat}
                            hasMessages={messageList.length > 0}
                            viewMode={viewMode}
                            size={{
                                get: size,
                                set: toggleSize,
                            }}
                            chatScreen={chatScreen}
                            setChatScreen={handleChatScreenChange}
                            sound={{
                                get: sound,
                                set: handleSoundChange
                            }}
                        />
                        {loading > 0 && <Loading className="lbaic-loading-chat"/>}
                        <Body bodyInRef={messageListRef} loading={loadingBar} scrollToBottomButton={
                            chatScreen === 'chat' && messageList.length > 0 ? (
                                <div className={`lbaic-scroll-to-bottom${showScrollToBottom ? ' visible' : ''}`}>
                                    <div className="lbaic-scroll-to-bottom-in">
                                        <button
                                            className="lbaic-button lbaic-scroll-to-bottom-button"
                                            onClick={handleScrollToBottom}
                                        >
                                            <svg xmlns='http://www.w3.org/2000/svg'>
                                                <use href="#lbaic-trigger-arrow"/>
                                            </svg>
                                        </button>
                                    </div>
                                </div>
                            ) : null
                        }>
                            {chatScreen === 'chat-history' ? (
                                <ChatsHistory
                                    getChats={getChats}
                                    chatSelected={setChat}
                                    editChat={editChat}
                                    deleteChat={deleteChat}
                                    loading={loading}
                                    setLoading={setLoading}
                                    loadingBar={loadingBar}
                                    setLoadingBar={setLoadingBar}
                                />
                            ) : (
                                <Messages
                                    chatbot={chatbot}
                                    messages={messageList}
                                    sendMessage={sendUserRequest}
                                    isTyping={isSending || isStreaming || isStreamTyping}
                                    setIsStreamTyping={setIsStreamTyping}
                                    chatbotErrorCallback={chatbotRequestFailedCallback}
                                    userErrorCallback={userRequestFailedCallback}
                                    assistantReloadCallback={handleAssistantReload}
                                    messageListRef={messageListRef}
                                    onLastMessageBotError={handleLastMessageBotError}
                                    onLastMessageParameter={handleLastMessageParameter}
                                    hasMore={hasMoreMessages}
                                    isLoadingMore={isLoadingMore}
                                    isLiveAgentMode={isLiveAgentMode}
                                    messageSpacerCalculatingRef={messageSpacerCalculatingRef}
                                    chatScreen={showChatbot ? chatScreen : undefined}
                                    onWidgetsLoadingChange={setWidgetsLoading}
                                />
                            )}
                        </Body>
                        <Footer
                            chatbot={chatbot}
                            chatScreen={chatScreen}
                            send={sendUserRequest}
                            isChatRunning={isSending && !isLiveAgentMode}
                            isChatStreaming={isStreaming}
                            isChatStreamTyping={isStreamTyping}
                            isPreviewSaving={isInPreviewMode && chatbotPreview.saving}
                            lastMessageIsBotError={lastMessageIsBotError}
                            lastMessageParameter={lastMessageParameter}
                            viewMode={viewMode}
                            isLiveAgentMode={isLiveAgentMode}
                            isInPreviewMode={isInPreviewMode}
                            shouldShowLiveAgentTyping={shouldShowLiveAgentTyping}
                            chatbotUtility={Chatbot.current}
                            leadCaptureWidget={showLeadCaptureWidget && leadCaptureWidget}
                            setShowLeadCaptureWidget={setShowLeadCaptureWidget}
                            sendMessageAfterLeadCaptureRef={sendMessageAfterLeadCaptureRef}
                            isWidgetsLoading={widgetsLoading}
                        />
                    </div>
                </ConfirmContext.Provider>
            </div>
        </div>
    )
}