jQuery(document).ready(function ($) {
    // --- Selectors ---
    const elements = {
        resetBtn: $("#atml-chat-reset"),
        suggestedQuestions: $("#suggested-questions"),
        inputArea: $("#antimanual-input-container"),
        messageInput: $('#antimanualinput'),
        sendBtn: $('#antimanualsend'),
        liveChatAttachBtn: $('#antimanual-live-chat-attach'),
        liveChatFileInput: $('#antimanual-live-chat-file'),
        liveChatAttachmentPreview: $('#antimanual-live-chat-attachment-preview'),
        chatbox: $('#antimanual-chatbox'),
        chatboxArea: $(".antimanualchatbox"),
        offlineBanner: $('.antimanual-offline-banner'),
        helpButton: $('#antimanual-help-button'),
        helpButtonSm: $('#antimanual-help-button-sm'),
        liveChatCta: $('#antimanual-live-chat-cta'),
        liveChatButton: $('#antimanual-live-chat-button')
    };

    // --- Constants ---
    const STORAGE = {
        MESSAGES: "antimanual-messages",
        LEAD: "antimanual-lead-info",
        CONVERSATION: "antimanual_conversation_id",
        FEEDBACK: "antimanual-feedback",
        LIVE_CHAT_ID: "antimanual-live-chat-id",
        LIVE_CHAT_TOKEN: "antimanual-live-chat-token"
    };
    // Phase A: CONVERSATION moved to localStorage (with 24h TTL via a sibling
    // key) so a tab refresh / cross-tab open keeps history context. Other
    // sensitive keys remain session-scoped.
    const SESSION_STORAGE_KEYS = new Set([
        STORAGE.MESSAGES,
        STORAGE.LEAD,
        STORAGE.FEEDBACK,
        STORAGE.LIVE_CHAT_ID,
        STORAGE.LIVE_CHAT_TOKEN
    ]);
    const CONVERSATION_TTL_MS = 24 * 60 * 60 * 1000;
    const CONVERSATION_EXPIRES_KEY = 'antimanual_conversation_expires_at';
    const MAX_CHAR_LIMIT = 500;
    const MOBILE_BREAKPOINT = 768;
    const SENDER = { USER: "user", BOT: "bot", SYSTEM: "system" };
    const LIVE_CHAT_POLL_INTERVAL_ACTIVE = 5000;
    const LIVE_CHAT_POLL_INTERVAL_IDLE = 15000;
    const FALLBACK_LIVE_CHAT_IMAGE_TYPES = ['image/jpg','image/jpeg', 'image/png', 'image/gif', 'image/webp'];

    // --- Helpers ---
    const toBool = (val, fallback = false) => typeof val === 'undefined' || val === null ? fallback : 
        typeof val === 'boolean' ? val : 
        String(val).toLowerCase() === '1' || String(val).toLowerCase() === 'true';
    
    // --- Configs & State ---
    const config = window.antimanual_chatbot_vars || {};
    const { lead: leadConfig = {}, mobile: mobileConfig = {}, display: displayConfig = {} } = config;
    const liveChatConfig = config.live_chat || {};
    const LIVE_CHAT_ATTACHMENTS_ENABLED = toBool(liveChatConfig.attachments_enabled);
    const LIVE_CHAT_ALLOWED_IMAGE_TYPES = Array.isArray(liveChatConfig.allowed_image_types) && liveChatConfig.allowed_image_types.length
        ? liveChatConfig.allowed_image_types
        : FALLBACK_LIVE_CHAT_IMAGE_TYPES;
    const LIVE_CHAT_MAX_UPLOAD_SIZE = Number(liveChatConfig.max_upload_size) || 0;
    const LIVE_CHAT_ATTACHMENTS_UPGRADE_MESSAGE = liveChatConfig.upgrade_message || 'Image attachments are available in Antimanual Pro.';
    
    let messages = [];
    let messagesLoaded = false;
    let lastFailedMessage = null;
    let isOnline = navigator.onLine;

    // --- Live Chat State ---
    let liveChatId = null;
    let liveChatToken = '';
    let liveChatPollingTimer = null;
    let liveChatPollInFlight = false;
    let liveChatLastMsgId = 0;
    let liveChatActive = false;
    let liveChatRequestPending = false;
    let liveChatAgentName = '';
    let liveChatAgentAvatar = '';
    let liveChatSendPending = false;
    let liveChatPendingAttachment = null;

    const getI18n = (key, defaultText) => wp?.i18n ? wp.i18n.__(key, 'antimanual') : defaultText;
    
    const showToast = (message, type = 'info') => {
        const validTypes = ['info', 'success', 'error'];
        const toastType = validTypes.includes(type) ? type : 'info';
        const $toast = $('<div></div>').addClass(`antimanual-toast antimanual-toast-${toastType}`).text(message).appendTo('body');
        setTimeout(() => $toast.addClass('show'), 10);
        setTimeout(() => { $toast.removeClass('show'); setTimeout(() => $toast.remove(), 300); }, 3000);
    };

    const getAjaxErrorMessage = (xhr, fallback) => {
        const json = xhr?.responseJSON;
        if (json?.message) return json.message;
        if (json?.data?.message) return json.data.message;
        if (typeof xhr?.responseText === 'string' && xhr.responseText.trim()) {
            return xhr.responseText.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
        }
        return fallback;
    };

    const stripHtml = html => {
        const tmp = document.createElement('div');
        tmp.innerHTML = html;
        return tmp.textContent || tmp.innerText || '';
    };

    const escapeHtml = value => String(value ?? '').replace(/[&<>"']/g, char => ({
        '&': '&amp;',
        '<': '&lt;',
        '>': '&gt;',
        '"': '&quot;',
        "'": '&#39;'
    }[char] || char));

    const escapeAttribute = value => escapeHtml(value);

    const isSafeUrl = (value, allowedProtocols = ['http:', 'https:', 'mailto:', 'tel:', 'blob:']) => {
        const raw = String(value || '').trim();
        if (!raw) return false;
        if (raw.startsWith('/') || raw.startsWith('#')) return true;

        try {
            const parsed = new URL(raw, window.location.origin);
            return allowedProtocols.includes(parsed.protocol);
        } catch (error) {
            return false;
        }
    };

    const isSafeEmail = value => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(value || '').trim());

    const formatPlainTextMessageHtml = text => escapeHtml(text).replace(/\n/g, '<br />');

    // Animated "thinking" pill that cycles through several verbs while the AI
    // is composing a reply. Class name `antimanual-typing-indicator` is
    // preserved so the existing isLoadingOrError detection in appendMsgEl
    // still recognises this as a loading state.
    //
    // Pool of 20 phrases; each indicator instance picks 4 at random so the
    // feel stays fresh across sends. The CSS keyframe cycle is built for
    // exactly 4 children (1.8s slices × 4 = 7.2s loop), so we always emit 4.
    const TYPING_PHRASES = [
        'Thinking', 'Searching the docs', 'Connecting the dots', 'Composing reply',
        'Looking it up', 'Cross-referencing sources', 'Almost there', 'Putting it together',
        'Digging deeper', 'Pulling the details', 'Scanning the knowledge base', 'Drafting an answer',
        'Reading the manual', 'Checking the facts', 'Hold tight', 'Working on it',
        'Polishing the reply', 'Just a moment', 'Lining up the answer', 'Tracing the breadcrumbs',
    ];
    const TYPING_PHRASES_PER_INDICATOR = 4;
    const buildTypingIndicator = () => {
        // Fisher-Yates over a copy, then take the first N. Cheap and fair.
        const pool = TYPING_PHRASES.slice();
        for (let i = pool.length - 1; i > 0; i--) {
            const j = Math.floor(Math.random() * (i + 1));
            const t = pool[i]; pool[i] = pool[j]; pool[j] = t;
        }
        const picks = pool.slice(0, TYPING_PHRASES_PER_INDICATOR);
        const items = picks
            .map((p) => `<span>${escapeHtml(getI18n(p, p))}</span>`)
            .join('');
        return `<div class="antimanual-typing-indicator atml-thinking">
            <span class="atml-thinking-shimmer"></span>
            <span class="atml-thinking-text">${items}</span>
            <span class="atml-thinking-dots"><i></i><i></i><i></i></span>
        </div>`;
    };

    const formatBytes = bytes => {
        const value = Number(bytes) || 0;
        if (!value) return '0 B';
        if (value < 1024) return `${value} B`;
        if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} KB`;
        return `${(value / (1024 * 1024)).toFixed(1)} MB`;
    };

    const getLiveChatAttachmentFallbackName = mimeType => {
        const extensionMap = {
            'image/jpeg': 'jpg',
            'image/png': 'png',
            'image/gif': 'gif',
            'image/webp': 'webp'
        };

        return `pasted-image-${Date.now()}.${extensionMap[mimeType] || 'png'}`;
    };

    const buildLiveChatMessageHtml = ({ messageText = '', attachmentUrl = '', attachmentName = '' } = {}) => {
        const trimmedText = String(messageText || '').trim();
        const safeUrl = isSafeUrl(attachmentUrl) ? escapeAttribute(attachmentUrl) : '';
        const safeName = escapeHtml(attachmentName || getI18n('Shared image', 'Shared image'));
        const textHtml = trimmedText
            ? `<div class="antimanual-live-chat-text">${formatPlainTextMessageHtml(trimmedText)}</div>`
            : '';
        const imageHtml = safeUrl
            ? `<div class="antimanual-live-chat-attachment"><a href="${safeUrl}" target="_blank" rel="noopener noreferrer"><img src="${safeUrl}" alt="${safeName}" loading="lazy" /></a><div class="antimanual-live-chat-attachment-name">${safeName}</div></div>`
            : '';

        return imageHtml || textHtml
            ? `<div class="antimanual-live-chat-message-body">${imageHtml}${textHtml}</div>`
            : '';
    };

    const copyTextToClipboard = text => {
        if (!text) {
            return Promise.reject(new Error('No text to copy'));
        }

        if (navigator.clipboard?.writeText && window.isSecureContext) {
            return navigator.clipboard.writeText(text);
        }

        return new Promise((resolve, reject) => {
            const textarea = document.createElement('textarea');
            textarea.value = text;
            textarea.setAttribute('readonly', 'readonly');
            textarea.style.position = 'fixed';
            textarea.style.top = '-9999px';
            textarea.style.left = '-9999px';
            document.body.appendChild(textarea);
            textarea.focus();
            textarea.select();

            try {
                const copied = document.execCommand('copy');
                document.body.removeChild(textarea);
                if (copied) {
                    resolve();
                    return;
                }
            } catch (error) {
                document.body.removeChild(textarea);
                reject(error);
                return;
            }

            reject(new Error('Copy command failed'));
        });
    };

    const normalizeCreatedAt = value => {
        if (!value) return Date.now();
        if (typeof value === 'number') return value;
        const parsed = new Date(value).getTime();
        return Number.isFinite(parsed) ? parsed : Date.now();
    };

    // --- Storage Management ---
    const getStorageBackend = key => SESSION_STORAGE_KEYS.has(key) ? window.sessionStorage : window.localStorage;

    const migrateSensitiveStorageKey = key => {
        if (!SESSION_STORAGE_KEYS.has(key)) return;

        try {
            const existing = window.sessionStorage.getItem(key);
            if (existing !== null) {
                window.localStorage.removeItem(key);
                return;
            }

            const legacy = window.localStorage.getItem(key);
            if (legacy !== null) {
                window.sessionStorage.setItem(key, legacy);
                window.localStorage.removeItem(key);
            }
        } catch (error) {
            // Ignore storage migration failures.
        }
    };

    Object.values(STORAGE).forEach(migrateSensitiveStorageKey);

    const LocalStore = {
        get: (key, defaultValue) => {
            try {
                const raw = getStorageBackend(key).getItem(key);
                return raw ? JSON.parse(raw) : defaultValue;
            } catch {
                return defaultValue;
            }
        },
        set: (key, value) => {
            try { getStorageBackend(key).setItem(key, JSON.stringify(value)); } catch {}
        },
        remove: key => {
            try { getStorageBackend(key).removeItem(key); } catch {}
        },
        getRaw: (key, defaultValue = '') => {
            try {
                const raw = getStorageBackend(key).getItem(key);
                return raw === null ? defaultValue : raw;
            } catch {
                return defaultValue;
            }
        },
        setRaw: (key, value) => {
            try {
                if (value === null || typeof value === 'undefined' || value === '') {
                    getStorageBackend(key).removeItem(key);
                    return;
                }
                getStorageBackend(key).setItem(key, String(value));
            } catch {}
        }
    };

    // Phase A: CONVERSATION TTL helpers — read/write the conversation id in
    // localStorage and silently expire after CONVERSATION_TTL_MS so a stale
    // conversation post id never anchors a brand-new session forever.
    const getConversationId = () => {
        try {
            const id = window.localStorage.getItem(STORAGE.CONVERSATION);
            if (!id) return '';
            const expiresAt = parseInt(window.localStorage.getItem(CONVERSATION_EXPIRES_KEY) || '0', 10);
            if (expiresAt && Date.now() > expiresAt) {
                window.localStorage.removeItem(STORAGE.CONVERSATION);
                window.localStorage.removeItem(CONVERSATION_EXPIRES_KEY);
                return '';
            }
            return id;
        } catch { return ''; }
    };
    const setConversationId = (id) => {
        try {
            if (id === null || typeof id === 'undefined' || id === '') {
                window.localStorage.removeItem(STORAGE.CONVERSATION);
                window.localStorage.removeItem(CONVERSATION_EXPIRES_KEY);
                return;
            }
            window.localStorage.setItem(STORAGE.CONVERSATION, String(id));
            window.localStorage.setItem(CONVERSATION_EXPIRES_KEY, String(Date.now() + CONVERSATION_TTL_MS));
        } catch {}
    };
    // Cross-domain embed scope label. Two sources, last write wins:
    //   1. Server-localized value (read from ?ctx= when chat-interface.php
    //      enqueued the script — works without any JS coordination).
    //   2. Runtime postMessage from the parent embed wrapper:
    //         iframe.contentWindow.postMessage(
    //             { type: 'atml_set_context', value: 'Eazydocs' }, '*'
    //         );
    //      Useful for SPA parents that change product scope without
    //      reloading the iframe.
    let runtimeEmbedContext = '';
    const getEmbedContext = () => runtimeEmbedContext || String(config.embed_context || '').slice(0, 200);

    // One-time bootstrap log so you can verify in the browser console what
    // the iframe received from the server. Look for this line on page load.
    try {
        console.info('[Antimanual] embed_context bootstrap:', {
            from_server: config.embed_context || '(empty)',
            iframe_url: window.location.href,
        });
    } catch (e) {}
    try {
        window.addEventListener('message', (event) => {
            const payload = event && event.data;
            if (!payload || typeof payload !== 'object') return;
            if (payload.type !== 'atml_set_context') return;
            const value = String(payload.value || '').trim();
            if (!value) return;
            runtimeEmbedContext = value.slice(0, 200);
        });
    } catch {}

    // One-time migration: pull any session-scoped conversation id into
    // localStorage so users who already had an active chat keep it.
    try {
        const legacy = window.sessionStorage.getItem(STORAGE.CONVERSATION);
        if (legacy && !window.localStorage.getItem(STORAGE.CONVERSATION)) {
            setConversationId(legacy);
        }
        window.sessionStorage.removeItem(STORAGE.CONVERSATION);
    } catch {}

    liveChatId = parseInt(LocalStore.getRaw(STORAGE.LIVE_CHAT_ID, ''), 10) || null;
    liveChatToken = LocalStore.getRaw(STORAGE.LIVE_CHAT_TOKEN, '');

    const loadMessages = () => messages = LocalStore.get(STORAGE.MESSAGES, []);
    const saveMessages = () => LocalStore.set(STORAGE.MESSAGES, messages);
    const clearMessages = () => { messages = []; LocalStore.remove(STORAGE.MESSAGES); renderMessages(); };
    const syncLiveChatCursorFromMessages = () => {
        liveChatLastMsgId = messages.reduce((max, msg) => Math.max(max, Number(msg.liveChatServerId) || 0), 0);
    };
    
    const addMessage = (messageObj) => {
        if (!messageObj.id) {
            messageObj.id = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
        }
        messageObj.createdAt = normalizeCreatedAt(messageObj.createdAt);
        messages.push(messageObj);
        saveMessages();
        renderMessages();
    };

    const updateMessage = (messageId, updates = {}) => {
        const index = messages.findIndex(message => String(message.id) === String(messageId));
        if (index === -1) return;

        messages[index] = {
            ...messages[index],
            ...updates,
            createdAt: normalizeCreatedAt(updates.createdAt ?? messages[index].createdAt)
        };

        saveMessages();
        renderMessages();
    };

    const removeMessage = messageId => {
        const nextMessages = messages.filter(message => String(message.id) !== String(messageId));
        if (nextMessages.length === messages.length) return;

        messages = nextMessages;
        saveMessages();
        renderMessages();
    };

    const revokeObjectUrl = url => {
        if (url && String(url).startsWith('blob:')) {
            URL.revokeObjectURL(url);
        }
    };

    const clearLiveChatPendingAttachment = ({ preservePreview = false } = {}) => {
        if (!elements.liveChatFileInput.length) {
            liveChatPendingAttachment = null;
            return;
        }

        if (!preservePreview) {
            revokeObjectUrl(liveChatPendingAttachment?.previewUrl);
        }

        liveChatPendingAttachment = null;
        elements.liveChatFileInput.val('');
    };

    const validateLiveChatAttachment = file => {
        if (!LIVE_CHAT_ATTACHMENTS_ENABLED) {
            return LIVE_CHAT_ATTACHMENTS_UPGRADE_MESSAGE;
        }

        if (!file) {
            return getI18n('Please choose an image to upload.', 'Please choose an image to upload.');
        }

        if (!LIVE_CHAT_ALLOWED_IMAGE_TYPES.includes(file.type)) {
            return getI18n('Only JPG, PNG, GIF, or WebP images can be uploaded in live chat.', 'Only JPG, PNG, GIF, or WebP images can be uploaded in live chat.');
        }

        if (LIVE_CHAT_MAX_UPLOAD_SIZE > 0 && file.size > LIVE_CHAT_MAX_UPLOAD_SIZE) {
            return `${getI18n('That image is too large.', 'That image is too large.')} ${getI18n('Maximum size:', 'Maximum size:')} ${formatBytes(LIVE_CHAT_MAX_UPLOAD_SIZE)}`;
        }

        return '';
    };

    const updateSendButtonState = () => {
        const len = String(elements.messageInput.val() || '').length;
        const hasMessage = String(elements.messageInput.val() || '').trim().length > 0;
        const hasAttachment = !!liveChatPendingAttachment;
        const overLimit = len > MAX_CHAR_LIMIT;
        const canSend = isOnline
            && !liveChatSendPending
            && !overLimit
            && (liveChatActive ? (hasMessage || hasAttachment) : hasMessage);

        elements.sendBtn.prop('disabled', !canSend);
    };

    const updateLiveChatAttachmentUi = () => {
        const canAttach = LIVE_CHAT_ATTACHMENTS_ENABLED && liveChatActive && isOnline && !liveChatSendPending;

        // Keep the attachment trigger visible when enabled so users can discover
        // the capability from the chatbot input area.
        elements.liveChatAttachBtn.toggle(LIVE_CHAT_ATTACHMENTS_ENABLED);
        elements.liveChatAttachBtn.prop('disabled', !canAttach);

        if (!elements.liveChatAttachmentPreview.length) {
            updateSendButtonState();
            return;
        }

        if (!LIVE_CHAT_ATTACHMENTS_ENABLED || !liveChatActive || !liveChatPendingAttachment) {
            elements.liveChatAttachmentPreview.hide().empty();
            updateSendButtonState();
            return;
        }

        elements.liveChatAttachmentPreview.html(`
            <div class="antimanual-live-chat-attachment-chip">
                <img src="${escapeAttribute(liveChatPendingAttachment.previewUrl)}" alt="${escapeAttribute(liveChatPendingAttachment.name)}" />
                <div class="antimanual-live-chat-attachment-chip-meta">
                    <strong>${escapeHtml(liveChatPendingAttachment.name)}</strong>
                    <span>${formatBytes(liveChatPendingAttachment.size)}</span>
                </div>
                <button type="button" class="antimanual-live-chat-attachment-remove" aria-label="${escapeAttribute(getI18n('Remove image', 'Remove image'))}">×</button>
            </div>
        `).show();

        updateSendButtonState();
    };

    const setLiveChatPendingAttachment = file => {
        if (!LIVE_CHAT_ATTACHMENTS_ENABLED) {
            showToast(LIVE_CHAT_ATTACHMENTS_UPGRADE_MESSAGE, 'info');
            return false;
        }

        if (!liveChatActive) return false;

        const safeName = file?.name || getLiveChatAttachmentFallbackName(file?.type);
        const validationMessage = validateLiveChatAttachment(file);

        if (validationMessage) {
            showToast(validationMessage, 'error');
            return false;
        }

        clearLiveChatPendingAttachment();

        liveChatPendingAttachment = {
            file,
            previewUrl: URL.createObjectURL(file),
            name: safeName,
            size: Number(file.size) || 0,
            type: file.type || ''
        };

        updateLiveChatAttachmentUi();
        return true;
    };

    const setLiveChatSession = ({ chatId = null, token = '' } = {}) => {
        liveChatId = chatId ? Number(chatId) : null;
        liveChatToken = token || '';
        liveChatAgentName = '';
        liveChatAgentAvatar = '';

        if (liveChatId) {
            LocalStore.setRaw(STORAGE.LIVE_CHAT_ID, liveChatId);
        } else {
            LocalStore.remove(STORAGE.LIVE_CHAT_ID);
        }

        if (liveChatToken) {
            LocalStore.setRaw(STORAGE.LIVE_CHAT_TOKEN, liveChatToken);
        } else {
            LocalStore.remove(STORAGE.LIVE_CHAT_TOKEN);
        }
    };

    const updateLiveChatIndicator = () => {
        const $headerText = $('#antimanual-chatbox-header-text');
        if (!$headerText.length) return;

        $('#antimanual-live-chat-indicator').remove();

        if (liveChatActive) {
            $headerText.append(`
                <div id="antimanual-live-chat-indicator">
                    <span>${getI18n('Live chat in progress', 'Live chat in progress')}</span>
                    <button type="button" class="antimanual-end-live-chat">${getI18n('End', 'End')}</button>
                </div>
            `);
        }

        elements.resetBtn.attr(
            'title',
            liveChatActive
                ? getI18n('End live chat and clear conversation', 'End live chat and clear conversation')
                : getI18n('Reset Conversation', 'Reset Conversation')
        );
    };

    const canShowPersistentLiveChatButton = () => {
        const escalation = config.escalation || {};
        return toBool(escalation.enabled) && escalation.type === 'live_chat' && toBool(escalation.show_button);
    };

    const updatePersistentLiveChatButton = () => {
        if (!elements.liveChatCta.length || !elements.liveChatButton.length) return;

        const shouldShow = canShowPersistentLiveChatButton() && isLeadCollected() && !liveChatActive;

        elements.liveChatCta.toggle(shouldShow);
        elements.liveChatButton
            .prop('disabled', liveChatRequestPending)
            .toggleClass('is-busy', liveChatRequestPending);
    };

    const getLiveChatPollInterval = () => {
        const isChatVisible = elements.chatbox.is(':visible');
        return document.hidden || !isChatVisible
            ? LIVE_CHAT_POLL_INTERVAL_IDLE
            : LIVE_CHAT_POLL_INTERVAL_ACTIVE;
    };

    const scheduleNextLiveChatPoll = (delay = getLiveChatPollInterval()) => {
        if (!liveChatActive || !liveChatId || !liveChatToken) return;
        if (liveChatPollingTimer) {
            clearTimeout(liveChatPollingTimer);
        }
        liveChatPollingTimer = setTimeout(() => {
            liveChatPollingTimer = null;
            pollLiveChat();
        }, delay);
    };

    // --- Availability & UI State ---
    const updateAvailabilityState = () => {
        isOnline = navigator.onLine;
        elements.offlineBanner.find('span').text(getI18n('You are offline', 'You are offline'));
        elements.offlineBanner.toggleClass('show', !isOnline);
        
        elements.inputArea.find('input, button').prop('disabled', !isOnline);
        elements.suggestedQuestions.find('button').prop('disabled', !isOnline);
        $('.antimanual-lead-form input, .antimanual-lead-form button').prop('disabled', !isOnline);
        elements.messageInput.attr(
            'placeholder',
            liveChatActive
                ? getI18n('Type a message...', 'Type a message...')
                : displayConfig.input_placeholder || getI18n('Type your query', 'Type your query')
        );

        updateLiveChatAttachmentUi();
        updateSendButtonState();
    };

    const applyResponsiveSettings = () => {
        const isMobile = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT}px)`).matches;
        const $body = $(document.body);
        $body.removeClass('antimanual-mobile-hidden antimanual-mobile-bottom-left antimanual-mobile-bottom-center antimanual-desktop-bottom-left');

        if (isMobile) {
            if (!toBool(mobileConfig.show_on_mobile, true)) {
                $body.addClass('antimanual-mobile-hidden').removeClass('antimanual_chatbox_expanded');
                elements.helpButton.hide();
                elements.helpButtonSm.hide();
                elements.chatbox.hide();
                return;
            }
            elements.helpButton.show();
            elements.helpButtonSm.show();
            const pos = mobileConfig.position || 'bottom-right';
            if (pos !== 'bottom-right') $body.addClass(`antimanual-mobile-${pos}`);
        } else {
            const pos = displayConfig.desktop_position || 'bottom-right';
            if (pos === 'bottom-left') $body.addClass('antimanual-desktop-bottom-left');
        }
    };

    const applyBrandingSettings = () => {
        if (displayConfig.suggested_label?.trim()) {
            elements.suggestedQuestions.find('p').first().text(displayConfig.suggested_label.trim());
        }
        
        const showAvatar = toBool(displayConfig.show_avatar, true);
        $('.bot-profile-icon').toggle(showAvatar).attr('src', showAvatar && displayConfig.custom_avatar_url?.trim() ? displayConfig.custom_avatar_url.trim() : $('.bot-profile-icon').attr('src'));
        $('.antimanual-msg.bot').toggleClass('no-avatar', !showAvatar);
    };

    // --- Lead Collection ---
    const getLeadInfo = () => {
        const stored = LocalStore.get(STORAGE.LEAD, null);
        if (stored) return stored;
        // Fall back to the logged-in user's email/name so the lead form is skipped.
        if (leadConfig.prefill && leadConfig.prefill.email) {
            return { email: leadConfig.prefill.email, name: leadConfig.prefill.name || '', synced: false };
        }
        return null;
    };
    const isLeadCollected = () => !leadConfig.collect_email || !!getLeadInfo();

    const renderLeadForm = () => {
        elements.chatboxArea.children().hide();
        elements.suggestedQuestions.hide();
        $('#antimanual-chatbox-footer').addClass('antimanual-footer-hidden');
        $('.antimanual-lead-form').remove();

        const prompt = leadConfig.prompt || getI18n('Please enter your details to continue.', 'Please enter your details to continue.');
        const emailLabel = getI18n('Your Email', 'Your Email') + (leadConfig.email_required ? ' *' : '');
        const nameLabel = getI18n('Your Name', 'Your Name') + (leadConfig.name_required ? ' *' : '');

        const formHtml = `
            <div class="antimanual-lead-form">
                <p class="lead-prompt">${prompt}</p>
                <div class="form-group form-group--floating">
                    <input type="email" id="antimanual-lead-email" placeholder=" " ${leadConfig.email_required ? 'required' : ''} />
                    <label for="antimanual-lead-email">${emailLabel}</label>
                </div>
                ${leadConfig.collect_name ? `<div class="form-group form-group--floating">
                    <input type="text" id="antimanual-lead-name" placeholder=" " ${leadConfig.name_required ? 'required' : ''} />
                    <label for="antimanual-lead-name">${nameLabel}</label>
                </div>` : ''}
                <button id="antimanual-lead-submit" class="lead-submit-btn">${getI18n('Start Chatting', 'Start Chatting')}</button>
            </div>
        `;
        elements.chatboxArea.append(formHtml);
        // Auto-focus the email field for better UX.
        setTimeout(() => $('#antimanual-lead-email').trigger('focus'), 100);
    };

    const submitLeadForm = () => {
        const email = $('#antimanual-lead-email').val();
        const name = $('#antimanual-lead-name').val();

        if ((leadConfig.email_required || email) && (!email || !email.includes('@'))) {
            return showToast(getI18n('Please enter a valid email address.', 'Please enter a valid email address.'), 'error');
        }
        if (leadConfig.collect_name && leadConfig.name_required && !name) {
            return showToast(getI18n('Please enter your name.', 'Please enter your name.'), 'error');
        }

        LocalStore.set(STORAGE.LEAD, { email, name, synced: false });
        renderMessages();
    };

    // --- Core Rendering ---
    const createMessageActions = (messageId, isBot) => {
        if (!isBot) return '';
        const feedback = LocalStore.get(STORAGE.FEEDBACK, {});
        const msgFeedback = feedback[messageId];
        
        return `<div class="antimanual-msg-actions">
            <div class="antimanual-feedback-btns">
                <button class="antimanual-feedback-btn antimanual-like-btn ${msgFeedback === 'like' ? 'liked' : ''}" data-message-id="${messageId}" title="${getI18n('Helpful', 'Helpful')}">
                    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"></path></svg>
                </button>
                <button class="antimanual-feedback-btn antimanual-dislike-btn ${msgFeedback === 'dislike' ? 'disliked' : ''}" data-message-id="${messageId}" title="${getI18n('Not helpful', 'Not helpful')}">
                    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17"></path></svg>
                </button>
            </div>
            <button class="antimanual-msg-action-btn antimanual-copy-btn" data-message-id="${messageId}" title="${getI18n('Copy', 'Copy')}">
                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>
            </button></div>`;
    };

    const appendMsgEl = (message, sender = SENDER.BOT, references = [], messageId = null, createdAt = null, messageKind = 'general', agentAvatar = '') => {
        const msgEl = $(`.antimanualchatbox .antimanual-msg.${sender}`)?.first();
        if (!msgEl || !msgEl.length) return null;
        
        const clonedMsgEl = msgEl.clone(true);
        const isBot = sender === SENDER.BOT;
        const isSystem = sender === SENDER.SYSTEM;
        const isLoadingOrError = message.includes('antimanual-typing-indicator') || message.includes('antimanual-error-msg');
        const canShowReferences = isBot && messageKind === 'answer' && !isLoadingOrError;
        const canShowActions = canShowReferences && !!messageId;

        clonedMsgEl.find(".msg-content").html(message);
        msgEl.parent().append(clonedMsgEl);
        clonedMsgEl.removeClass("placeholder").show();

        // Swap bot avatar with the agent avatar for live chat messages.
        if (isBot && agentAvatar) {
            const $avatar = clonedMsgEl.find('.bot-profile-icon');
            if ($avatar.length) {
                $avatar.attr('src', agentAvatar).attr('alt', 'Agent Avatar');
            }
        }

        const $templateSourcesContainer = isBot ? clonedMsgEl.find(".sources-container") : null;
        $templateSourcesContainer?.remove();

        const formatTime = ts => {
            const diff = Date.now() - ts;
            const minutes = Math.floor(diff / 60000);
            if (minutes < 1) return getI18n('Just now', 'Just now');
            if (minutes < 60) return minutes === 1 ? getI18n('1 min ago', '1 min ago') : `${minutes} ${getI18n('mins ago', 'mins ago')}`;
            const hours = Math.floor(minutes / 60);
            if (hours < 24) return hours === 1 ? getI18n('1 hour ago', '1 hour ago') : `${hours} ${getI18n('hours ago', 'hours ago')}`;
            const days = Math.floor(hours / 24);
            if (days < 7) return days === 1 ? getI18n('Yesterday', 'Yesterday') : `${days} ${getI18n('days ago', 'days ago')}`;
            return new Date(ts).toLocaleDateString();
        };

        if (!isBot && !isSystem && createdAt) {
            clonedMsgEl.find(".msg-content").wrap('<div class="antimanual-msg-user-wrapper"></div>');
        }

        if (isBot && !isLoadingOrError) {
            const validRefs = (Array.isArray(references) ? references : []).filter(r => r?.link && isSafeUrl(r.link, ['http:', 'https:']));
            const uniqueRefs = [...new Map(validRefs.map(v => [v.link, v])).values()];
            
            const $footer = $('<div class="antimanual-msg-footer"></div>');
            let $sourcesBtn = null;
            let $referencesEl = null;

            if (canShowReferences && uniqueRefs.length > 0) {
                $sourcesBtn = msgEl.find('.sources').clone(true).show();
                $referencesEl = msgEl.find('.references').clone(true).empty();
                
                uniqueRefs.forEach(ref => {
                    const safeLink = escapeAttribute(ref.link);
                    const safeTitle = escapeHtml(ref.title || getI18n("Source", "Source"));
                    $referencesEl.append($(`<a href="${safeLink}" target="_blank" rel="noopener noreferrer"><span class="antimanual-icon"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 36 36" fill="currentColor"><path d="M17.6,24.32l-2.46,2.44a4,4,0,0,1-5.62,0,3.92,3.92,0,0,1,0-5.55l4.69-4.65a4,4,0,0,1,5.62,0,3.86,3.86,0,0,1,1,1.71A2,2,0,0,0,21.1,18l1.29-1.28a5.89,5.89,0,0,0-1.15-1.62,6,6,0,0,0-8.44,0L8.1,19.79a5.91,5.91,0,0,0,0,8.39,6,6,0,0,0,8.44,0l3.65-3.62c-.17,0-.33,0-.5,0A8,8,0,0,1,17.6,24.32Z"></path><path d="M28.61,7.82a6,6,0,0,0-8.44,0l-3.65,3.62c.17,0,.33,0,.49,0h0a8,8,0,0,1,2.1.28l2.46-2.44a4,4,0,0,1,5.62,0,3.92,3.92,0,0,1,0,5.55l-4.69,4.65a4,4,0,0,1-5.62,0,3.86,3.86,0,0,1-1-1.71,2,2,0,0,0-.28.23l-1.29,1.28a5.89,5.89,0,0,0,1.15,1.62,6,6,0,0,0,8.44,0l4.69-4.65a5.92,5.92,0,0,0,0-8.39Z"></path></svg></span><span class="text">${safeTitle}</span></a>`));
                });
                
                $sourcesBtn.on("click", () => {
                    const isVisible = $referencesEl.css("display") !== "none";
                    $referencesEl.css("display", isVisible ? "none" : "flex");
                    $sourcesBtn.toggleClass("expanded");
                });
                
                $footer.append($sourcesBtn);
            }

            if (canShowActions) $footer.append(createMessageActions(messageId, isBot));
            if (createdAt) $footer.append(`<span class="antimanual-msg-timestamp">${formatTime(createdAt)}</span>`);
            
            if ($footer.children().length > 0) {
                clonedMsgEl.find(".antimanual-response").append($footer);
            }
            if ($referencesEl) {
                clonedMsgEl.find(".antimanual-response").append($referencesEl);
            }
        }

        return clonedMsgEl;
    };

    const renderMessages = () => {
        if (!isLeadCollected()) {
            updatePersistentLiveChatButton();
            renderLeadForm();
            updateAvailabilityState();
            return;
        }

        $('#antimanual-chatbox-footer').removeClass('antimanual-footer-hidden');
        elements.inputArea.find('input, button').prop('disabled', false);
        $('.antimanual-lead-form').remove();
        elements.chatboxArea.children(":not(.placeholder)").remove();

        if (!messagesLoaded) {
            loadMessages();
            messagesLoaded = true;
        }

        if (!messages.length) {
            addMessage({
                message: config.welcome_message || getI18n("What can I help you with?", "What can I help you with?"),
                sender: SENDER.BOT
            });
            return; // addMessage calls renderMessages
        }

        const hasUserMsg = messages.some(m => m.sender === SENDER.USER);
        elements.resetBtn.prop("disabled", !hasUserMsg);
        elements.suggestedQuestions.toggle(!hasUserMsg);

        messages.forEach(m => appendMsgEl(m.message, m.sender, m.references, m.id, m.createdAt, m.kind, m.agentAvatar));
        
        setTimeout(() => elements.chatboxArea[0]?.scrollTo({ top: elements.chatboxArea[0].scrollHeight, behavior: "smooth" }), 300);
        updateAvailabilityState();
        updateLiveChatIndicator();
        updatePersistentLiveChatButton();
    };

    // --- Message Input ---
    const updateCharCounter = () => {
        updateSendButtonState();
        
        // Auto-resize textarea
        const el = elements.messageInput[0];
        el.style.height = 'auto'; // Reset to get natural scroll height
        const newHeight = Math.min(Math.max(el.scrollHeight, 24), 120); // Cap at 120px, min 24px
        el.style.height = newHeight + 'px';
    };

    // --- Data Sending ---
    const finalizeSuccessfulReply = (data = {}) => {
        const convId = data.conversation_id;
        if (convId) setConversationId(convId);

        const lead = getLeadInfo();
        if (lead?.email && !lead.synced && convId) {
            $.post({
                url: `${config.rest_url}/leads/submit`,
                beforeSend: xhr => config.nonce && xhr.setRequestHeader('X-WP-Nonce', config.nonce),
                data: { conversation_id: convId, email: lead.email, name: lead.name },
                success: () => LocalStore.set(STORAGE.LEAD, { ...lead, synced: true })
            });
        }

        lastFailedMessage = null;

        const escalation = config.escalation || {};
        const handoffType = data.handoff_type || escalation.type;
        const liveChatAvailable = typeof data.live_chat_available !== 'undefined'
            ? toBool(data.live_chat_available)
            : toBool(escalation.live_chat_available);
        const shouldOfferHandoff = toBool(data.should_offer_handoff)
            || (toBool(escalation.enabled) && toBool(data.is_irrelevant));

        if (!shouldOfferHandoff) return;

        if (toBool(data.handoff_intent) && handoffType === 'live_chat' && liveChatAvailable) {
            requestLiveChat();
            return;
        }

        showEscalationCard({
            message: toBool(data.handoff_intent) ? data.answer : escalation.message,
            type: handoffType,
            email: escalation.email,
            url: escalation.url,
            liveChatAvailable
        });
    };

    // Blocking path: same behavior the plugin has always had.
    const sendMessageBlocking = (messageText) => {
        $.ajax({
            url: `${config.rest_url}/messages`,
            type: 'POST',
            beforeSend: xhr => config.nonce && xhr.setRequestHeader('X-WP-Nonce', config.nonce),
            data: { message: messageText, conversation_id: getConversationId(), embed_context: getEmbedContext() },
            success: res => {
                $('.loading-message').remove();
                if (res?.success) {
                    addMessage({ message: res.data.answer, sender: SENDER.BOT, references: res.data.references, kind: 'answer' });
                    finalizeSuccessfulReply(res.data);
                } else {
                    handleMsgError(messageText);
                }
            },
            error: () => { $('.loading-message').remove(); handleMsgError(messageText); }
        });
    };

    // Phase 5: streaming path. Uses fetch+ReadableStream against
    // /messages/stream and parses Server-Sent-Events incrementally.
    //
    // Falls back to sendMessageBlocking on:
    //   - HTTP 404 (endpoint missing / streaming disabled server-side)
    //   - HTTP 409 (control errors: handoff matched, custom answer matched,
    //     provider doesn't support streaming) — the blocking endpoint
    //     handles all of these natively.
    //   - any network error before the stream opens
    //   - any SSE error event emitted by the server
    //
    // Falling back means we restore the typing indicator and re-issue the
    // same request to /messages; user just experiences the original
    // (blocking) UX rather than a broken stream.
    const parseSseFrame = (frame) => {
        let event = 'message';
        const dataLines = [];
        for (const raw of frame.split('\n')) {
            const line = raw.replace(/\r$/, '');
            if (line === '' || line.startsWith(':')) continue;
            const colon = line.indexOf(':');
            if (colon === -1) continue;
            const field = line.slice(0, colon);
            const value = line.slice(colon + 1).replace(/^ /, '');
            if (field === 'event') event = value;
            else if (field === 'data') dataLines.push(value);
        }
        if (!dataLines.length) return null;
        try { return { event, data: JSON.parse(dataLines.join('\n')) }; }
        catch { return null; }
    };

    const sendMessageStream = async (messageText) => {
        const fallbackToBlocking = (reason) => {
            // Re-show typing indicator (the old one is gone) and re-issue.
            const reloadEl = appendMsgEl(buildTypingIndicator(), "bot");
            reloadEl?.addClass("loading-message");
            sendMessageBlocking(messageText);
        };

        let response;
        try {
            response = await fetch(`${config.rest_url}/messages/stream`, {
                method: 'POST',
                credentials: 'same-origin',
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded',
                    'X-WP-Nonce': config.nonce || '',
                    'Accept': 'text/event-stream',
                },
                body: new URLSearchParams({
                    message: messageText,
                    conversation_id: getConversationId() || '',
                    embed_context: getEmbedContext() || '',
                }).toString(),
            });
        } catch (e) {
            $('.loading-message').remove();
            return fallbackToBlocking('network');
        }

        // Control errors → fall back. The blocking endpoint handles handoff,
        // custom answers, disabled-streaming, and unsupported providers.
        if (!response.ok || !response.body || response.status === 404 || response.status === 409) {
            $('.loading-message').remove();
            return fallbackToBlocking('control:' + response.status);
        }

        // Replace typing indicator with an empty bot bubble that we'll feed
        // incrementally.
        $('.loading-message').remove();
        const botEl = appendMsgEl('<span class="atml-stream-cursor">▍</span>', SENDER.BOT, [], null, null, 'answer');
        const $content = botEl && botEl.find ? botEl.find('.antimanual-message-content, .antimanual-message-text').first() : null;

        let accumulated = '';
        let finalMeta = null;
        let streamError = null;
        let pendingRaf = false;

        const renderNow = () => {
            pendingRaf = false;
            if ($content && $content.length) {
                $content.html(accumulated + '<span class="atml-stream-cursor">▍</span>');
            } else if (botEl && botEl.html) {
                botEl.html(accumulated + '<span class="atml-stream-cursor">▍</span>');
            }
            elements.chatboxArea[0]?.scrollTo({ top: elements.chatboxArea[0].scrollHeight, behavior: 'smooth' });
        };

        const scheduleRender = () => {
            if (pendingRaf) return;
            pendingRaf = true;
            (window.requestAnimationFrame || window.setTimeout)(renderNow, 16);
        };

        const reader = response.body.getReader();
        const decoder = new TextDecoder();
        let buffer = '';

        try {
            while (true) {
                const { value, done } = await reader.read();
                if (done) break;
                buffer += decoder.decode(value, { stream: true });

                let sep;
                while ((sep = buffer.indexOf('\n\n')) !== -1) {
                    const frame = buffer.slice(0, sep);
                    buffer = buffer.slice(sep + 2);
                    const parsed = parseSseFrame(frame);
                    if (!parsed) continue;

                    if (parsed.event === 'chunk') {
                        accumulated += parsed.data?.delta || '';
                        scheduleRender();
                    } else if (parsed.event === 'meta') {
                        finalMeta = parsed.data;
                    } else if (parsed.event === 'error') {
                        streamError = parsed.data?.message || 'Stream error';
                    } else if (parsed.event === 'done') {
                        // server is finished; outer loop will exit naturally
                    }
                }
            }
        } catch (e) {
            streamError = e?.message || 'Reader error';
        }

        // Tear down the streaming bubble — we're about to render the
        // persisted bot message via addMessage which re-renders the chatbox.
        if (botEl && botEl.remove) botEl.remove();

        if (streamError && !accumulated) {
            // Nothing came through. Fall back so the user still gets a reply.
            return fallbackToBlocking('stream:' + streamError);
        }

        // Persist + render the final accumulated reply through the normal
        // message path (this also writes to localStorage and re-renders
        // with references).
        addMessage({
            message: accumulated,
            sender: SENDER.BOT,
            references: finalMeta?.references || [],
            kind: 'answer'
        });

        if (finalMeta) {
            finalizeSuccessfulReply(Object.assign({ answer: accumulated }, finalMeta));
        }
    };

    const sendMessage = (messageText) => {
        if (!messageText || messageText.length > MAX_CHAR_LIMIT) return;
        if (!navigator.onLine) return showToast(getI18n('You appear to be offline. Please check your connection.', 'You appear to be offline.'), 'error');

        addMessage({ message: formatPlainTextMessageHtml(messageText), sender: SENDER.USER });
        elements.messageInput.val('');
        updateCharCounter();

        const loadingMsgEl = appendMsgEl(buildTypingIndicator(), "bot");
        loadingMsgEl?.addClass("loading-message");
        setTimeout(() => elements.chatboxArea[0]?.scrollTo({ top: elements.chatboxArea[0].scrollHeight, behavior: "smooth" }), 300);

        // Streaming requires fetch + ReadableStream. If those aren't available
        // (old browsers), or the option is off, use the blocking endpoint.
        const canStream = toBool(config.streaming_enabled)
            && typeof window.fetch === 'function'
            && typeof ReadableStream !== 'undefined';

        if (canStream) {
            sendMessageStream(messageText).catch(() => {
                $('.loading-message').remove();
                handleMsgError(messageText);
            });
            return;
        }

        sendMessageBlocking(messageText);
    };

    const handleMsgError = (original) => {
        lastFailedMessage = original;
        addMessage({
            message: `<div class="antimanual-error-msg"><span class="antimanual-error-text">${getI18n("Sorry, I couldn't process your request. Please try again.", "Error")}</span><button class="antimanual-retry-btn"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"></polyline><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path></svg>${getI18n('Retry', 'Retry')}</button></div>`,
            sender: SENDER.BOT
        });
    };

    const showEscalationCard = ({ message, type, email, url, liveChatAvailable }) => {
        const escMsg = escapeHtml(message || getI18n('Would you like to contact a human?', 'Would you like to contact a human?'));
        const safeEmail = isSafeEmail(email) ? escapeAttribute(email) : '';
        const safeUrl = isSafeUrl(url, ['http:', 'https:']) ? escapeAttribute(url) : '';
        let escAction = '';

        if (type === 'live_chat') {
            if (liveChatAvailable) {
                escAction = `<button class="antimanual-escalation-link antimanual-live-chat-btn">${getI18n('Chat with a Human', 'Chat with a Human')}</button>`;
            } else {
                escAction = `<span class="antimanual-escalation-offline">${getI18n('No agents are currently available.', 'No agents are currently available.')}</span>`;
                if (safeEmail) {
                    escAction += ` <a href="mailto:${safeEmail}" class="antimanual-escalation-link">${getI18n('Send Email Instead', 'Send Email Instead')}</a>`;
                }
            }
        } else if (type === 'email' && safeEmail) {
            escAction = `<a href="mailto:${safeEmail}" class="antimanual-escalation-link">${getI18n('Send Email', 'Send Email')}</a>`;
        } else if (type === 'url' && safeUrl) {
            escAction = `<a href="${safeUrl}" target="_blank" rel="noopener noreferrer" class="antimanual-escalation-link">${getI18n('Contact Support', 'Contact Support')}</a>`;
        }

        addMessage({
            message: `<div class="antimanual-escalation-card"><p>${escMsg}</p>${escAction}</div>`,
            sender: SENDER.BOT,
            kind: 'system-card'
        });
    };

    // =========================================================================
    // Live Chat Functions
    // =========================================================================

    /**
     * Request a new live chat session.
     */
    const requestLiveChat = () => {
        if (liveChatActive || liveChatRequestPending) return;

        const convId = getConversationId() || '';
        const lead = getLeadInfo();
        liveChatRequestPending = true;
        updatePersistentLiveChatButton();

        addMessage({
            message: `<div class="antimanual-live-chat-status waiting">${getI18n('Connecting you to an agent...', 'Connecting you to an agent...')}</div>`,
            sender: SENDER.SYSTEM,
            kind: 'live-chat-status'
        });

        $.ajax({
            url: `${config.rest_url}/chatbot/live-chat/request`,
            type: 'POST',
            beforeSend: xhr => config.nonce && xhr.setRequestHeader('X-WP-Nonce', config.nonce),
            data: {
                conversation_id: convId,
                visitor_name: lead?.name || '',
                visitor_email: lead?.email || '',
                page_url: (typeof window !== 'undefined' && window.location) ? window.location.href : '',
                page_title: (typeof document !== 'undefined') ? (document.title || '') : '',
                referrer: (typeof document !== 'undefined') ? (document.referrer || '') : ''
            },
            success: res => {
                if (res?.success) {
                    setLiveChatSession({
                        chatId: res.data.chat_id,
                        token: res.data.visitor_token || ''
                    });
                    enterLiveChatMode();
                } else {
                    addMessage({
                        message: `<div class="antimanual-live-chat-status error">${res?.message || getI18n('Unable to start live chat.', 'Unable to start live chat.')}</div>`,
                        sender: SENDER.SYSTEM,
                        kind: 'live-chat-status'
                    });
                }

                liveChatRequestPending = false;
                updatePersistentLiveChatButton();
            },
            error: (xhr) => {
                addMessage({
                    message: `<div class="antimanual-live-chat-status error">${getAjaxErrorMessage(xhr, getI18n('Unable to start live chat. Please try again.', 'Unable to start live chat. Please try again.'))}</div>`,
                    sender: SENDER.SYSTEM,
                    kind: 'live-chat-status'
                });

                liveChatRequestPending = false;
                updatePersistentLiveChatButton();
            }
        });
    };

    /**
     * Enter live chat mode — switch input to send to human endpoint, start polling.
     */
    const enterLiveChatMode = () => {
        if (!liveChatId || !liveChatToken) {
            exitLiveChatMode();
            return;
        }

        liveChatActive = true;
        syncLiveChatCursorFromMessages();
        updateLiveChatIndicator();
        updateAvailabilityState();
        updatePersistentLiveChatButton();
        updateLiveChatAttachmentUi();
        startLiveChatPolling();
    };

    /**
     * Exit live chat mode — stop polling, reset input.
     */
    const exitLiveChatMode = () => {
        liveChatActive = false;
        liveChatSendPending = false;
        setLiveChatSession();
        stopLiveChatPolling();
        clearLiveChatPendingAttachment();
        updateLiveChatIndicator();
        updateAvailabilityState();
        updatePersistentLiveChatButton();
        updateLiveChatAttachmentUi();
    };

    /**
     * Send a message in live chat mode.
     */
    const sendLiveChatMessage = (messageText) => {
        if (!liveChatId || !liveChatToken || liveChatSendPending) return;

        const trimmedMessage = String(messageText || '').trim();
        const pendingAttachment = liveChatPendingAttachment;

        if (!trimmedMessage && !pendingAttachment) return;

        const localMessageId = `live-chat-local-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
        const localMessageHtml = buildLiveChatMessageHtml({
            messageText: trimmedMessage,
            attachmentUrl: pendingAttachment?.previewUrl || '',
            attachmentName: pendingAttachment?.name || ''
        }) || formatPlainTextMessageHtml(trimmedMessage);

        addMessage({
            id: localMessageId,
            message: localMessageHtml,
            sender: SENDER.USER,
            kind: 'live-chat',
            createdAt: Date.now()
        });

        elements.messageInput.val('');
        clearLiveChatPendingAttachment({ preservePreview: !!pendingAttachment?.previewUrl });
        updateCharCounter();

        const formData = new FormData();
        formData.append('chat_id', String(liveChatId));
        formData.append('visitor_token', liveChatToken);
        formData.append('message', trimmedMessage);
        if (pendingAttachment?.file) {
            formData.append('attachment', pendingAttachment.file, pendingAttachment.name || pendingAttachment.file.name || getLiveChatAttachmentFallbackName(pendingAttachment.file.type));
        }

        liveChatSendPending = true;
        updateLiveChatAttachmentUi();

        $.ajax({
            url: `${config.rest_url}/chatbot/live-chat/send`,
            type: 'POST',
            beforeSend: xhr => config.nonce && xhr.setRequestHeader('X-WP-Nonce', config.nonce),
            data: formData,
            processData: false,
            contentType: false,
            success: res => {
                if (!res?.success) {
                    removeMessage(localMessageId);
                    showToast(res?.message || getI18n('Failed to send message.', 'Failed to send message.'), 'error');
                    return;
                }

                const storedMessage = res?.data?.message;
                if (storedMessage) {
                    revokeObjectUrl(pendingAttachment?.previewUrl);
                    updateMessage(localMessageId, {
                        message: buildLiveChatMessageHtml({
                            messageText: storedMessage.message,
                            attachmentUrl: storedMessage.attachment_url,
                            attachmentName: storedMessage.attachment_name
                        }) || formatPlainTextMessageHtml(storedMessage.message || ''),
                        createdAt: storedMessage.created_at,
                        liveChatServerId: storedMessage.id || res?.data?.message_id || 0
                    });
                } else {
                    revokeObjectUrl(pendingAttachment?.previewUrl);
                    updateMessage(localMessageId, {
                        liveChatServerId: res?.data?.message_id || 0
                    });
                }
            },
            error: (xhr) => {
                revokeObjectUrl(pendingAttachment?.previewUrl);
                removeMessage(localMessageId);
                showToast(getAjaxErrorMessage(xhr, getI18n('Failed to send message.', 'Failed to send message.')), 'error');
            },
            complete: () => {
                liveChatSendPending = false;
                updateLiveChatAttachmentUi();
            }
        });
    };

    /**
     * Poll for new messages from the agent.
     */
    const pollLiveChat = () => {
        if (!liveChatId || !liveChatToken || !liveChatActive) return;

        if (!navigator.onLine) {
            scheduleNextLiveChatPoll();
            return;
        }

        if (liveChatPollInFlight) {
            scheduleNextLiveChatPoll();
            return;
        }

        liveChatPollInFlight = true;

        $.ajax({
            url: `${config.rest_url}/chatbot/live-chat/poll`,
            type: 'GET',
            beforeSend: xhr => config.nonce && xhr.setRequestHeader('X-WP-Nonce', config.nonce),
            data: { chat_id: liveChatId, visitor_token: liveChatToken, after_id: liveChatLastMsgId },
            success: res => {
                if (!res?.success) {
                    addMessage({
                        message: `<div class="antimanual-live-chat-status error">${res?.message || getI18n('Live chat is no longer available.', 'Live chat is no longer available.')}</div>`,
                        sender: SENDER.SYSTEM,
                        kind: 'live-chat-status'
                    });
                    exitLiveChatMode();
                    return;
                }

                const { status, messages: newMsgs, agent_name: agentName, agent_avatar: agentAvatarUrl } = res.data;

                // Store agent info when available.
                if (agentAvatarUrl) liveChatAgentAvatar = agentAvatarUrl;
                if (agentName) liveChatAgentName = agentName;

                if (newMsgs && newMsgs.length > 0) {
                    newMsgs.forEach(m => {
                        liveChatLastMsgId = Math.max(liveChatLastMsgId, m.id);

                        // Skip visitor messages (already rendered locally).
                        if (m.sender === 'visitor') return;

                        if (m.sender === 'system') {
                            addMessage({
                                message: `<div class="antimanual-live-chat-status system">${escapeHtml(m.message)}</div>`,
                                sender: SENDER.SYSTEM,
                                id: `live-chat-system-${m.id}`,
                                createdAt: m.created_at,
                                kind: 'live-chat-status',
                                liveChatServerId: m.id
                            });
                        } else if (m.sender === 'agent') {
                            addMessage({
                                message: buildLiveChatMessageHtml({
                                    messageText: m.message,
                                    attachmentUrl: m.attachment_url,
                                    attachmentName: m.attachment_name
                                }) || formatPlainTextMessageHtml(m.message || ''),
                                sender: SENDER.BOT,
                                id: `live-chat-agent-${m.id}`,
                                createdAt: m.created_at,
                                kind: 'live-chat',
                                liveChatServerId: m.id,
                                agentAvatar: liveChatAgentAvatar
                            });
                        }
                    });
                }

                if (status === 'closed') {
                    addMessage({
                        message: `<div class="antimanual-live-chat-status ended">${getI18n('Chat session has ended.', 'Chat session has ended.')}</div>`,
                        sender: SENDER.SYSTEM,
                        kind: 'live-chat-status'
                    });
                    exitLiveChatMode();
                }
            },
            error: () => {
                if (liveChatActive) {
                    scheduleNextLiveChatPoll(LIVE_CHAT_POLL_INTERVAL_IDLE);
                }
            },
            complete: () => {
                liveChatPollInFlight = false;
                if (liveChatActive && liveChatId && liveChatToken && !liveChatPollingTimer) {
                    scheduleNextLiveChatPoll();
                }
            }
        });
    };

    /**
     * Visitor ends the live chat.
     */
    const endLiveChat = ({ announce = true, clearConversation = false } = {}) => {
        if (!liveChatId || !liveChatToken) {
            if (clearConversation) {
                setConversationId('');
                clearMessages();
                elements.messageInput.val('');
                updateCharCounter();
            }
            return;
        }

        $.ajax({
            url: `${config.rest_url}/chatbot/live-chat/end`,
            type: 'POST',
            beforeSend: xhr => config.nonce && xhr.setRequestHeader('X-WP-Nonce', config.nonce),
            data: { chat_id: liveChatId, visitor_token: liveChatToken }
        });

        if (announce) {
            addMessage({
                message: `<div class="antimanual-live-chat-status ended">${getI18n('You ended the chat.', 'You ended the chat.')}</div>`,
                sender: SENDER.SYSTEM,
                kind: 'live-chat-status'
            });
        }
        exitLiveChatMode();

        if (clearConversation) {
            setConversationId('');
            clearMessages();
            elements.messageInput.val('');
            updateCharCounter();
        }
    };

    const startLiveChatPolling = () => {
        stopLiveChatPolling();
        scheduleNextLiveChatPoll(0);
    };

    const stopLiveChatPolling = () => {
        if (liveChatPollingTimer) {
            clearTimeout(liveChatPollingTimer);
            liveChatPollingTimer = null;
        }
        liveChatPollInFlight = false;
    };

    // --- Events ---
    elements.resetBtn.on('click', () => {
        if (liveChatActive) {
            endLiveChat({ announce: false, clearConversation: true });
            return;
        }
        setConversationId('');
        clearMessages();
        elements.messageInput.val('');
        clearLiveChatPendingAttachment();
        updateCharCounter();
    });
    elements.messageInput.on('input', updateCharCounter).on('keydown', e => { 
        if (e.which === 13 && !e.shiftKey) { 
            e.preventDefault(); 
            elements.sendBtn.click(); 
            return false; 
        } 
    });
    elements.messageInput.on('paste', e => {
        if (!liveChatActive) return;

        const clipboardItems = e.originalEvent?.clipboardData?.items || [];
        const imageItem = Array.from(clipboardItems).find(item => item?.type && item.type.startsWith('image/'));
        if (!imageItem) return;

        if (!LIVE_CHAT_ATTACHMENTS_ENABLED) {
            showToast(LIVE_CHAT_ATTACHMENTS_UPGRADE_MESSAGE, 'info');
            return;
        }

        const file = imageItem.getAsFile();
        if (!file) return;

        e.preventDefault();
        setLiveChatPendingAttachment(file);
    });
    elements.sendBtn.on('click', () => {
        const msg = elements.messageInput.val().trim();
        if (liveChatActive) {
            if (msg || liveChatPendingAttachment) {
                sendLiveChatMessage(msg);
            }
            return;
        }

        // If the user picked an image without an active live chat, route the
        // pending attachment through the live-chat preconditions instead of
        // silently dropping it (which the prior branch did by falling
        // through to sendMessage and ignoring the file).
        if (liveChatPendingAttachment) {
            const escalation = config.escalation || {};
            const liveChatConfigured = toBool(escalation.enabled) && escalation.type === 'live_chat';
            const liveChatAvailable = toBool(escalation.live_chat_available);

            if (!liveChatConfigured) {
                showToast(getI18n('Enable Live Chat handoff in chatbot settings to send images.', 'Enable Live Chat handoff in chatbot settings to send images.'), 'info');
                return;
            }
            if (!isLeadCollected()) {
                renderMessages();
                showToast(getI18n('Enter your details first, then we will start the live chat with your image.', 'Enter your details first, then we will start the live chat with your image.'), 'info');
                return;
            }
            if (!liveChatAvailable) {
                showToast(getI18n('No live agents are currently available to receive your image.', 'No live agents are currently available to receive your image.'), 'error');
                return;
            }

            // Pending attachment is preserved across requestLiveChat() —
            // once liveChatActive flips true the user can hit send again.
            showToast(getI18n('Starting live chat… click send again once connected.', 'Starting live chat… click send again once connected.'), 'info');
            requestLiveChat();
            return;
        }

        if (msg) {
            sendMessage(msg);
        }
    });
    
    elements.liveChatAttachBtn.on('click', () => {
        // The click handler used to refuse opening the file picker until a
        // live-chat session was already active. That made the button feel
        // broken on first interaction. Now we always open the picker when
        // attachments are enabled and a send isn't in-flight — the routing
        // logic (live-chat session request, preconditions toast) runs at
        // send time, when we actually need a destination.

        
        if (!LIVE_CHAT_ATTACHMENTS_ENABLED) {
            showToast(LIVE_CHAT_ATTACHMENTS_UPGRADE_MESSAGE, 'info');
            return;
        }

        if (liveChatSendPending) return;

        if (!elements.liveChatFileInput || !elements.liveChatFileInput.length) {
            showToast(getI18n('Image upload is unavailable right now.', 'Image upload is unavailable right now.'), 'error');
            return;
        }

        // Surface the destination-not-ready hints as a non-blocking nudge so
        // the user still gets to pick a file. If they hit send, the send
        // handler enforces the real preconditions.
        if (!liveChatActive) {
            const escalation = config.escalation || {};
            const liveChatConfigured = toBool(escalation.enabled) && escalation.type === 'live_chat';
            const liveChatAvailable = toBool(escalation.live_chat_available);
            if (!liveChatConfigured) {
                showToast(getI18n('Live Chat handoff is off — pick an image and we will guide you.', 'Live Chat handoff is off — pick an image and we will guide you.'), 'info');
            } else if (!isLeadCollected()) {
                showToast(getI18n('Pick an image — we will ask for your details before sending.', 'Pick an image — we will ask for your details before sending.'), 'info');
            } else if (!liveChatAvailable) {
                showToast(getI18n('No live agents are online right now — your image will queue.', 'No live agents are online right now — your image will queue.'), 'info');
            }
        }

        elements.liveChatFileInput.trigger('click');
    });
    elements.liveChatFileInput.on('change', function() {
        const file = this.files?.[0];
        if (file) {
            setLiveChatPendingAttachment(file);
        }
        $(this).val('');
    });
    $(document).on('click', '.antimanual-live-chat-attachment-remove', function() {
        clearLiveChatPendingAttachment();
        updateLiveChatAttachmentUi();
    });
    $(document).on('click', '#suggested-questions .atml-suggested-question', function() { const q = $(this).text().trim(); elements.messageInput.val(q); sendMessage(q); });
    
    $(document).on('click', '#antimanual-lead-submit', e => { e.preventDefault(); submitLeadForm(); });
    $(document).on('keypress', '#antimanual-lead-email, #antimanual-lead-name', e => { if (e.which === 13) { submitLeadForm(); return false; } });
    $(document).on('click', '.antimanual-retry-btn', function() {
        if (!lastFailedMessage) return;
        $(this).closest('.antimanual-msg').remove();
        messages = messages.filter(m => !m.message.includes('antimanual-error-msg'));
        saveMessages();
        sendMessage(lastFailedMessage);
    });
    
    $(document).on('click', '.antimanual-copy-btn', function(e) {
        e.preventDefault();
        e.stopPropagation();
        const $btn = $(this);
        const messageId = String($btn.data('message-id'));
        const message = messages.find(m => String(m.id) === messageId);
        const text = stripHtml(message?.message || $btn.closest('.antimanual-response').find('.msg-content').html() || "").trim();

        if (text) {
            copyTextToClipboard(text).then(() => {
                const orig = $btn.html();
                $btn.html('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>').addClass('active');
                setTimeout(() => { $btn.removeClass('active').html(orig); }, 1500);
            }).catch(() => showToast(getI18n('Failed to copy', 'Failed to copy'), 'error'));
        }
    });

    $(document).on('click', '.antimanual-feedback-btn', function(e) {
        e.stopPropagation();
        const $btn = $(this);
        const id = $btn.data('message-id');
        const isLike = $btn.hasClass('antimanual-like-btn');
        const activeClass = isLike ? 'liked' : 'disliked';
        const inactiveClass = isLike ? 'disliked' : 'liked';
        const siblingClass = isLike ? '.antimanual-dislike-btn' : '.antimanual-like-btn';
        const feedbackVal = isLike ? 'like' : 'dislike';

        if ($btn.hasClass(activeClass)) {
            $btn.removeClass(activeClass);
            const fb = LocalStore.get(STORAGE.FEEDBACK, {});
            delete fb[id];
            LocalStore.set(STORAGE.FEEDBACK, fb);
        } else {
            $btn.addClass(activeClass);
            $btn.siblings(siblingClass).removeClass(inactiveClass);
            const fb = LocalStore.get(STORAGE.FEEDBACK, {});
            fb[id] = feedbackVal;
            LocalStore.set(STORAGE.FEEDBACK, fb);

            // Send feedback to the REST API.
            const convId = getConversationId();
            if (convId && config.rest_url) {
                const msgIndex = messages.findIndex(m => m.id === id);
                $.ajax({
                    url: `${config.rest_url}/chatbot/feedback`,
                    type: 'POST',
                    beforeSend: xhr => config.nonce && xhr.setRequestHeader('X-WP-Nonce', config.nonce),
                    data: {
                        conversation_id: convId,
                        message_index: msgIndex >= 0 ? msgIndex : 0,
                        is_helpful: isLike ? 1 : 0
                    }
                });
            }
        }
    });

    // Live chat button click handler.
    $(document).on('click', '.antimanual-live-chat-btn', function() {
        requestLiveChat();
    });

    elements.liveChatButton.on('click', () => {
        if (liveChatActive || liveChatRequestPending) return;
        if (!isLeadCollected()) {
            renderMessages();
            return;
        }

        const escalation = config.escalation || {};
        const liveChatAvailable = toBool(escalation.live_chat_available);

        if (!liveChatAvailable) {
            showEscalationCard({
                message: escalation.message,
                type: 'live_chat',
                email: escalation.email,
                url: escalation.url,
                liveChatAvailable: false
            });
            return;
        }

        requestLiveChat();
    });

    // End live chat handler.
    $(document).on('click', '.antimanual-end-live-chat', function() {
        endLiveChat();
    });

    $(window).on('resize orientationchange', () => { applyResponsiveSettings(); });
    window.addEventListener('online', updateAvailabilityState);
    window.addEventListener('offline', updateAvailabilityState);
    document.addEventListener('visibilitychange', () => {
        if (!liveChatActive) return;
        scheduleNextLiveChatPoll(document.hidden ? LIVE_CHAT_POLL_INTERVAL_IDLE : 0);
    });
    $(document).on('keydown', e => { if (e.key === 'Escape' && elements.chatbox.is(':visible')) AntimanualToggleChat(); });
    
    if (!window.localStorage.getItem('antimanual-opened')) elements.helpButton.addClass('pulse');
    elements.helpButton.on('click', () => { elements.helpButton.removeClass('pulse'); window.localStorage.setItem('antimanual-opened', 'true'); });

    // --- Init ---
    applyResponsiveSettings();
    applyBrandingSettings();
    updateAvailabilityState();
    renderMessages();
    elements.messageInput.val('');
    updateCharCounter();
    updateLiveChatAttachmentUi();

    // Resume live chat if session exists.
    if (liveChatId && liveChatToken) {
        enterLiveChatMode();
    } else if (liveChatId || liveChatToken) {
        setLiveChatSession();
    }

    // Auto-open chatbox if it was open before navigation.
    if (window.sessionStorage.getItem('antimanual-chatbox-open') === '1' && !elements.chatbox.is(':visible')) {
        AntimanualToggleChat();
    }
});

function AntimanualToggleChat() {
    const $body = jQuery(document.body);
    const $chatbox = jQuery('#antimanual-chatbox');
    const $helpBtn = jQuery('#antimanual-help-button');
    
    if ($body.hasClass('antimanual-mobile-hidden')) return;

    if ($chatbox.is(':visible')) {
        // Close the chatbox.
        $chatbox.slideUp(300, () => {
            jQuery('#help-icon').show().fadeIn(300);
            jQuery('#close-icon').hide();
            $helpBtn.find('.antimanual-help-btn-label').show();
        });
        $body.removeClass('antimanual_chatbox_expanded');
        window.sessionStorage.setItem('antimanual-chatbox-open', '0');
    } else {
        // Open the chatbox.
        $chatbox.hide().css('display', 'flex').hide().slideDown(300, () => {
            jQuery('#antimanualinput').trigger('focus');
        });
        jQuery('#help-icon').fadeOut(300, () => {
            jQuery('#help-icon').hide();
            jQuery('#close-icon').hide().fadeIn(300);
        });
        $helpBtn.find('.antimanual-help-btn-label').hide();
        $body.addClass('antimanual_chatbox_expanded');
        setTimeout(() => {
            const cb = jQuery('.antimanualchatbox')[0];
            cb?.scrollTo({ top: cb.scrollHeight, behavior: 'smooth' });
        }, 300);
        window.sessionStorage.setItem('antimanual-chatbox-open', '1');
    }
}
