/**
 * LolaCore Chat Interface — 1.0.0 (AJAX Request-Response)
 *
 * Replaces the legacy SSE (Server-Sent Events) architecture with a standard
 * fetch() POST → JSON response flow, using the WordPress 7.0 AI Client backend.
 */
document.addEventListener('DOMContentLoaded', function() {
    if (typeof window.lolacore_widget_state === 'undefined') return;

    const state    = window.lolacore_widget_state;
    const ajaxData = window.lolacore_ajax;
    const i18n     = (typeof window.lolacore_chat_i18n !== 'undefined') ? window.lolacore_chat_i18n : {};

    /**
     * @param {string} key
     * @param {string} fallback
     * @returns {string}
     */
    function lcT(key, fallback) {
        const value = i18n[key];
        return (typeof value === 'string' && value.length > 0) ? value : fallback;
    }

    /**
     * @param {string} key
     * @param {string[]} fallback
     * @returns {string[]}
     */
    function lcTArray(key, fallback) {
        return Array.isArray(i18n[key]) && i18n[key].length > 0 ? i18n[key] : fallback;
    }

    // --- DOM Elements ---
    const trigger      = document.getElementById('lolacore-widget-trigger');
    const panel        = document.getElementById('lolacore-widget-panel');
    const btnMinimize  = document.getElementById('lolacore-btn-minimize');
    const btnClose     = document.getElementById('lolacore-btn-close');
    const dragHandle   = document.getElementById('lolacore-drag-handle');
    const messagesBox  = document.getElementById('lolacore-messages');
    const input        = document.getElementById('lolacore-input');
    const btnSend      = document.getElementById('lolacore-send');
    const statusBar    = document.getElementById('lolacore-status');
    const onboardingBar = document.getElementById('lolacore-onboarding-status');
    const onboardingText = document.getElementById('lolacore-onboarding-text');

    // --- UUID generator (works without HTTPS) ---
    function generateUUID() {
        if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
            return crypto.randomUUID();
        }
        return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c =>
            (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
        );
    }

    // --- State ---
    let activeSessionId = sessionStorage.getItem('lolacore_active_session_id');
    // Guard: sessionStorage may contain the literal string "null" from an older JS version
    if (!activeSessionId || activeSessionId === 'null' || activeSessionId === 'undefined') {
        activeSessionId = null;
        sessionStorage.removeItem('lolacore_active_session_id');
    }

    let lastActivity    = Date.now();
    let isBusy          = false; // Replaces isStreaming — true while awaiting AJAX response
    let pendingSummary  = '';    // Summary from the last closed session, consumed on next open

    // Tracks tool action confirmations currently in flight. Prevents the same
    // action_id from being POSTed twice (re-click after page reload re-renders
    // the card, accessibility tools firing double events, etc.) which would
    // make the second call hit a consumed pending row and surface a misleading
    // "That confirmation has expired" error after the action already succeeded.
    const inFlightActionIds = new Set();

    // Internal trigger that asks the backend for a conversational recovery message
    // after a background build fails. Must match WPAIClientAdapter::BUILDER_FAILURE_SENTINEL.
    const BUILDER_FAILED_SENTINEL = '__LOLA_BUILDER_FAILED__';
    const FIRST_REAL_GREETING_SENTINEL = '__LOLA_FIRST_REAL_GREETING__';

    // --- Thinking Messages (progressive rotation) ---
    const _thinkingProfessional = lcTArray('thinkingProfessional', [
        'Thinking…',
        'Looking into it…',
        'Let me check a few things…',
        'Going through the details…',
        'Processing your request…',
        'Connecting a few pieces…',
        'Give me a second on this one…',
        'On it…',
    ]);

    const _thinkingPersonality = lcTArray('thinkingPersonality', [
        'Hmm, trickier than I thought…',
        'Hold on — I want to get this right.',
        'OK, I see what\'s going on here…',
        'Don\'t rush me.',
        'This deserves a proper answer…',
        'I have opinions about this. One sec.',
        'Let me approach this differently…',
        'OK let me focus a bit harder…',
    ]);

    let _thinkingInterval   = null;
    let _thinkingStart      = 0;
    let _thinkingUsed       = [];

    console.log('[Lola] Init 1.0.0 (AJAX):', { activeSessionId });

    let widgetStateSyncInFlight     = null;
    let widgetStateSyncDebounce     = null;
    let firstRealGreetingInFlight   = false;

    // =========================================================================
    // A. Widget Toggle & Drag
    // =========================================================================

    function isChatEmpty() {
        return messagesBox.children.length === 0;
    }

    function resetSessionForFreshStart() {
        activeSessionId = null;
        sessionStorage.removeItem('lolacore_active_session_id');
        messagesBox.innerHTML = '';
    }

    /**
     * Merge server widget state into the in-memory snapshot.
     *
     * @param {object} fresh
     * @returns {{providerBecameConfigured: boolean, shouldRunFirstGreeting: boolean}}
     */
    function applyWidgetStatePatch(fresh) {
        const prevConfigured = !!state.is_configured;
        const prevTrigger    = !!state.trigger_first_real_greeting;

        state.is_configured                   = !!fresh.is_configured;
        state.onboarding_complete             = !!fresh.onboarding_complete;
        state.trigger_first_real_greeting     = !!fresh.trigger_first_real_greeting;
        state.should_auto_open_first_greeting = !!fresh.should_auto_open_first_greeting;

        if (typeof fresh.has_scan_data === 'boolean') {
            state.has_scan_data = fresh.has_scan_data;
        }
        if (fresh.scan_data) {
            state.scan_data = fresh.scan_data;
        }
        if (typeof fresh.last_session_summary === 'string') {
            state.last_session_summary = fresh.last_session_summary;
        }

        return {
            providerBecameConfigured: !prevConfigured && state.is_configured,
            shouldRunFirstGreeting:   !prevTrigger && state.trigger_first_real_greeting,
        };
    }

    /**
     * Fetch authoritative widget state from the server (provider transitions without reload).
     *
     * @param {{autoOpenOnFirstGreeting?: boolean}} options
     * @returns {Promise<boolean>}
     */
    async function syncWidgetStateFromServer(options = {}) {
        const autoOpenOnFirstGreeting = !!options.autoOpenOnFirstGreeting;
        const openingPanel            = !!options.openingPanel;

        if (widgetStateSyncInFlight) {
            return widgetStateSyncInFlight;
        }

        widgetStateSyncInFlight = (async () => {
            try {
                const response = await fetch(ajaxData.ajax_url, {
                    method:  'POST',
                    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                    body: new URLSearchParams({
                        action:          'lolacore_get_widget_state',
                        _lolacore_nonce: ajaxData.chat_nonce,
                    }),
                });

                if (!response.ok) {
                    console.warn('[Lola] sync widget state HTTP', response.status);
                    return false;
                }

                const result = await response.json();
                if (!result.success || !result.data) {
                    console.warn('[Lola] sync widget state rejected', result);
                    return false;
                }

                const transition = applyWidgetStatePatch(result.data);
                const panelOpen  = !panel.classList.contains('lolacore-hidden');

                if (transition.shouldRunFirstGreeting && !firstRealGreetingInFlight) {
                    resetSessionForFreshStart();

                    const shouldOpen = autoOpenOnFirstGreeting || panelOpen || openingPanel;
                    if (shouldOpen) {
                        panel.classList.remove('lolacore-hidden');
                        applyProviderGateToInput();
                        input.focus();
                        scrollToBottom();
                        startNewSession(pendingSummary);
                        pendingSummary = '';
                    }

                    return true;
                }

                applyProviderGateToInput();

                if (transition.providerBecameConfigured) {
                    const staleContinuitySession = activeSessionId && isChatEmpty();
                    const shouldStart = staleContinuitySession
                        || (openingPanel && isChatEmpty())
                        || (!activeSessionId && panelOpen);

                    if (shouldStart) {
                        resetSessionForFreshStart();
                        if (panelOpen || openingPanel) {
                            panel.classList.remove('lolacore-hidden');
                            applyProviderGateToInput();
                            startNewSession(pendingSummary);
                            pendingSummary = '';
                        }
                    }
                }

                return true;
            } catch (error) {
                console.error('[Lola] sync widget state error:', error);
                return false;
            }
        })();

        try {
            return await widgetStateSyncInFlight;
        } finally {
            widgetStateSyncInFlight = null;
        }
    }

    function scheduleWidgetStateSync(autoOpenOnFirstGreeting) {
        clearTimeout(widgetStateSyncDebounce);
        widgetStateSyncDebounce = setTimeout(() => {
            syncWidgetStateFromServer({ autoOpenOnFirstGreeting });
        }, 300);
    }

    async function togglePanel() {
        if (isDragging || dragOccurred) return;

        if (panel.classList.contains('lolacore-hidden')) {
            await syncWidgetStateFromServer({ autoOpenOnFirstGreeting: false, openingPanel: true });

            if (!panel.classList.contains('lolacore-hidden') && !isChatEmpty()) {
                return;
            }

            panel.classList.remove('lolacore-hidden');
            applyProviderGateToInput();
            input.focus();
            scrollToBottom();

            const needsFreshSession = !activeSessionId
                || state.trigger_first_real_greeting
                || (activeSessionId && isChatEmpty() && state.is_configured);

            if (needsFreshSession) {
                if (activeSessionId) {
                    resetSessionForFreshStart();
                }
                startNewSession(pendingSummary);
                pendingSummary = '';
            }
        } else {
            panel.classList.add('lolacore-hidden');
        }
    }

    trigger.addEventListener('click', togglePanel);
    btnMinimize.addEventListener('click', () => panel.classList.add('lolacore-hidden'));
    btnClose.addEventListener('click', closeAndResetSession);

    // Drag Logic
    let isDragging  = false;
    let dragOccurred = false;
    let currentX, currentY, initialX, initialY;
    let xOffset = 0, yOffset = 0;

    // Restore saved position
    const savedPos = localStorage.getItem('lolacore_widget_position_v2');
    if (savedPos) {
        try {
            const pos = JSON.parse(savedPos);
            xOffset = pos.x || 0;
            yOffset = pos.y || 0;
            // Stop the pulse animation before applying transform — they share
            // the transform property and CSS animations override inline styles.
            trigger.style.animation = 'none';
            setTranslate(xOffset, yOffset, panel);
            setTranslate(xOffset, yOffset, trigger);
        } catch (e) { /* ignore */ }
    }

    trigger.addEventListener('mousedown', dragStart);
    dragHandle.addEventListener('mousedown', dragStart);
    trigger.addEventListener('dragstart', e => e.preventDefault());
    dragHandle.addEventListener('dragstart', e => e.preventDefault());
    trigger.addEventListener('touchstart', dragStart, { passive: false });
    dragHandle.addEventListener('touchstart', dragStart, { passive: false });
    document.addEventListener('mouseup', dragEnd);
    document.addEventListener('touchend', dragEnd);
    document.addEventListener('mousemove', drag, { passive: false });
    document.addEventListener('touchmove', drag, { passive: false });

    function dragStart(e) {
        if (e.type === 'touchstart') {
            initialX = e.touches[0].clientX - xOffset;
            initialY = e.touches[0].clientY - yOffset;
        } else {
            initialX = e.clientX - xOffset;
            initialY = e.clientY - yOffset;
        }
        if (e.target === dragHandle || dragHandle.contains(e.target) ||
            e.target === trigger    || trigger.contains(e.target)) {
            isDragging   = true;
            dragOccurred = false;
        }
    }

    function dragEnd() {
        if (!isDragging) return;
        initialX  = currentX;
        initialY  = currentY;
        isDragging = false;
        if (dragOccurred) {
            localStorage.setItem('lolacore_widget_position_v2', JSON.stringify({ x: xOffset, y: yOffset }));
            setTimeout(() => { dragOccurred = false; }, 100);
        }
    }

    function drag(e) {
        if (!isDragging) return;
        if (e.type !== 'touchmove') e.preventDefault();

        const clientX = e.type === 'touchmove' ? e.touches[0].clientX : e.clientX;
        const clientY = e.type === 'touchmove' ? e.touches[0].clientY : e.clientY;

        if (!dragOccurred &&
            Math.abs(clientX - xOffset - initialX) < 3 &&
            Math.abs(clientY - yOffset - initialY) < 3) {
            return;
        }
        dragOccurred = true;
        currentX     = clientX - initialX;
        currentY     = clientY - initialY;
        xOffset      = currentX;
        yOffset      = currentY;
        setTranslate(currentX, currentY, panel);
        setTranslate(currentX, currentY, trigger);
    }

    function setTranslate(xPos, yPos, el) {
        el.style.transform = `translate3d(${xPos}px, ${yPos}px, 0)`;
    }

    // =========================================================================
    // B. UI Helpers
    // =========================================================================

    function scrollToBottom() {
        messagesBox.scrollTop = messagesBox.scrollHeight;
    }

    /**
     * Append a chat message bubble to the messages list.
     * Supports basic Markdown: **bold**, [link](url), newlines.
     *
     * @param {string} role    'user' | 'assistant'
     * @param {string} content Raw text / Markdown content.
     * @returns {HTMLElement}  The created message div.
     */
    function appendMessage(role, content, isError = false) {
        const msgDiv     = document.createElement('div');
        msgDiv.className = `lolacore-message lolacore-msg-${role}`;

        if (isError) {
            msgDiv.classList.add('lola-error-message');
        }

        const avatarUrl = role === 'assistant' ? state.avatar_url : state.user_avatar_url;

        let processContent = content;
        if (role === 'user') {
            // Escape HTML entities for user input so tags like <script> are visible and not parsed as HTML
            processContent = processContent.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
        }

        const htmlContent = renderMarkdown(processContent);

        msgDiv.innerHTML = `
            <img src="${avatarUrl}" class="lolacore-msg-avatar" alt="${role}">
            <div class="lolacore-msg-content">
                <div class="lolacore-msg-text">${htmlContent}</div>
            </div>
        `;
        messagesBox.appendChild(msgDiv);
        scrollToBottom();
        return msgDiv;
    }

    /**
     * Markdown → HTML renderer.
     * Uses marked.js + DOMPurify if available, otherwise fallback.
     *
     * @param {string} text
     * @returns {string}
     */
    function renderMarkdown(text) {
        // Auto-link plain URLs that are NOT already inside a markdown link or HTML tag.
        // Matches http:// and https:// URLs, stops at whitespace or common punctuation.
        // Negative lookbehind: skip if preceded by ]( (markdown link) or " or ' (HTML attr).
        const autolinked = text.replace(
            /(?<!\]\()(?<!")(?<!')(?<!=)(https?:\/\/[^\s<>"'`)\]]+)/gi,
            '[$1]($1)'
        );

        if (typeof marked !== 'undefined') {
            let html = marked.parse(autolinked, { breaks: true });
            if (typeof DOMPurify !== 'undefined') {
                html = DOMPurify.sanitize(html, { ADD_ATTR: ['target', 'download'] });
            }
            // Ensure all links open in new tab and export files have download attribute
            html = html.replace(/<a\s+([^>]*?)href=["']([^"']+)["']([^>]*)>/gi, (match, before, url, after) => {
                let attrs = 'target="_blank" rel="noopener noreferrer"';
                if (url.match(/\.(xml|json|csv|zip|txt)$/i)) {
                    const filename = url.split('/').pop().split('?')[0] || '';
                    attrs += ` download="${filename}"`;
                }
                return `<a ${before}href="${url}"${after} ${attrs}>`;
            });
            return html;
        }

        // Fallback
        return autolinked
            .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
            .replace(/\[(.*?)\]\((.*?)\)/g, (match, linkText, url) => {
                let attrs = 'target="_blank" rel="noopener noreferrer"';
                if (url.match(/\.(xml|json|csv|zip|txt)$/i)) {
                    const filename = url.split('/').pop().split('?')[0] || '';
                    attrs += ` download="${filename}"`;
                }
                return `<a href="${url}" ${attrs}>${linkText}</a>`;
            })
            .replace(/\n/g, '<br>');
    }

    /**
     * Append a small memory-saved indicator below a message bubble.
     *
     * @param {HTMLElement} msgDiv The parent assistant message div.
     * @param {string}      text   The fact text.
     */
    function appendMemoryIndicator(msgDiv, text) {
        const contentDiv  = msgDiv.querySelector('.lolacore-msg-content');
        const indicator   = document.createElement('div');
        indicator.className   = 'lolacore-memory-indicator';
        indicator.textContent = `💾 ${text}`;
        contentDiv.appendChild(indicator);
        scrollToBottom();
    }

    /**
     * Pick a thinking message based on elapsed time.
     * 0-6s: professional only. 6-12s: mix in personality. 12s+: static fallback.
     *
     * @returns {string}
     */
    function pickThinkingMessage() {
        const elapsed = Date.now() - _thinkingStart;

        // Before 6s: professional only.
        // 6-12s: professional + personality.
        // 12s+: everything + long-wait messages (never stops rotating).
        let pool;
        if (elapsed < 6000) {
            pool = _thinkingProfessional;
        } else if (elapsed < 12000) {
            pool = [..._thinkingProfessional, ..._thinkingPersonality];
        } else {
            pool = [
                ..._thinkingProfessional,
                ..._thinkingPersonality,
                ...lcTArray('thinkingLongWait', [
                    'Still working — this one needs extra attention.',
                    'Taking a bit longer than usual…',
                    'Bear with me, almost cracked it…',
                ]),
            ];
        }

        // Avoid repeating the last 3 messages shown
        const available = pool.filter(m => !_thinkingUsed.includes(m));
        const source    = available.length > 0 ? available : pool;
        const msg       = source[Math.floor(Math.random() * source.length)];

        _thinkingUsed.push(msg);
        if (_thinkingUsed.length > 3) _thinkingUsed.shift();

        return msg;
    }

    /**
     * Show the thinking status bar with rotating messages.
     */
    function showTypingIndicator() {
        isBusy         = true;
        _thinkingStart = Date.now();
        _thinkingUsed  = [];

        const indicator = statusBar.querySelector('.lolacore-typing-indicator');
        indicator.textContent = pickThinkingMessage();
        statusBar.classList.remove('lolacore-hidden');

        _thinkingInterval = setInterval(() => {
            indicator.textContent = pickThinkingMessage();
        }, 3000);
    }

    /**
     * Hide the status bar, clear rotation, and re-enable input.
     */
    function hideTypingIndicator() {
        isBusy = false;
        statusBar.classList.add('lolacore-hidden');
        if (_thinkingInterval) {
            clearInterval(_thinkingInterval);
            _thinkingInterval = null;
        }
    }

    /**
     * Whether the confirmation payload is a Plugin Builder delivery card (pre-save).
     *
     * @param {Object} data
     * @returns {boolean}
     */
    function isPluginBuilderDelivery(data) {
        return data.card_variant === 'plugin_builder_delivery'
            || data.tool_name === 'createPlugin'
            || data.tool_name === 'updatePluginCode';
    }

    /**
     * @param {Object} data
     * @returns {boolean}
     */
    function isPluginRollbackConfirmation(data) {
        return data.card_variant === 'plugin_rollback';
    }

    /**
     * Render a calm, human rollback confirmation card (no technical dump).
     *
     * @param {Object} data
     */
    function renderPluginRollbackConfirmation(data) {
        const msgDiv = appendMessage('assistant', data.message || lcT('approvalRequired', 'I need your approval to proceed:'));
        const contentDiv = msgDiv.querySelector('.lolacore-msg-content');

        const pluginName = data.plugin_name ? String(data.plugin_name) : '';
        const current = data.current_version ? String(data.current_version) : '';
        const target = data.target_version ? String(data.target_version) : '';
        const wasActive = Boolean(data.was_active);

        const nameHtml = pluginName
            ? `<p class="lolacore-tc-rollback-name"><strong>${escapeHtml(pluginName)}</strong></p>`
            : '';

        let versionHtml = '';
        if (current && target) {
            versionHtml = `
                <p class="lolacore-tc-rollback-version">
                    <span class="lolacore-tc-rollback-from">v${escapeHtml(current)}</span>
                    <span class="lolacore-tc-rollback-arrow" aria-hidden="true">→</span>
                    <span class="lolacore-tc-rollback-to">v${escapeHtml(target)}</span>
                </p>
            `;
        }

        const reassuranceHtml = `<p class="lolacore-tc-rollback-reassure">${escapeHtml(lcT('rollbackReassurance', 'Same plugin from WordPress.org — you\'re switching to an earlier release, not removing anything.'))}</p>`;
        const activeHtml = wasActive
            ? `<p class="lolacore-tc-rollback-active">${escapeHtml(lcT('rollbackActiveNote', 'It\'s active — I\'ll turn it off briefly during the swap, then turn it back on.'))}</p>`
            : '';

        const card = document.createElement('div');
        card.className = 'lolacore-tool-confirmation lolacore-tc-rollback';
        card.innerHTML = `
            <div class="lolacore-tc-header">
                <span class="lolacore-tc-icon" aria-hidden="true">⚠️</span>
                <strong>${escapeHtml(lcT('rollbackCardTitle', 'Roll back plugin version'))}</strong>
            </div>
            <div class="lolacore-tc-body">
                ${nameHtml}
                ${versionHtml}
                ${reassuranceHtml}
                ${activeHtml}
            </div>
            <div class="lolacore-tc-actions">
                <button class="lolacore-tc-btn lolacore-tc-approve"
                        data-action-id="${data.action_id}"
                        data-decision="approve">${escapeHtml(lcT('approveExecute', 'Approve and execute'))}</button>
                <button class="lolacore-tc-btn lolacore-tc-cancel"
                        data-action-id="${data.action_id}"
                        data-decision="cancel">${escapeHtml(lcT('cancel', 'Cancel'))}</button>
            </div>
        `;
        contentDiv.appendChild(card);
        scrollToBottom();

        card.querySelector('.lolacore-tc-approve').addEventListener('click', function() {
            handleToolDecision(this.dataset.actionId, 'approve', card);
        });
        card.querySelector('.lolacore-tc-cancel').addEventListener('click', function() {
            handleToolDecision(this.dataset.actionId, 'cancel', card);
        });
    }

    /**
     * Load-test and code-review badges for Plugin Builder delivery cards (QG-3).
     *
     * @param {Object} data
     * @returns {string}
     */
    function buildPluginBuilderQualityHtml(data) {
        const parts = [];
        const loadSafe = data.load_safe && typeof data.load_safe === 'object' ? data.load_safe : null;
        const loadStatus = loadSafe && loadSafe.status ? String(loadSafe.status) : '';
        const loadLabel = loadStatus === 'ok'
            ? lcT('loadTestPassed', 'Passed')
            : loadStatus === 'fatal'
                ? lcT('loadTestFailed', 'Failed')
                : loadStatus === 'skipped'
                    ? lcT('loadTestSkipped', 'Skipped')
                    : '';

        if (loadLabel) {
            parts.push(`<span class="lolacore-tc-pb-load-test">${escapeHtml(lcT('loadTestLabel', 'Load test:'))} ${escapeHtml(loadLabel)}</span>`);
        }

        const reviewScore = typeof data.code_review_score === 'number'
            ? data.code_review_score
            : (typeof data.confidence === 'number' ? data.confidence : 0);

        if (reviewScore > 0) {
            parts.push(`<span class="lolacore-tc-pb-code-review">${escapeHtml(lcT('codeReviewLabel', 'Code review:'))} ${escapeHtml(String(reviewScore))}%</span>`);
        }

        if (!parts.length) {
            return '';
        }

        return `<p class="lolacore-tc-pb-quality">${parts.join(' · ')}</p>`;
    }

    /**
     * Build the collapsible suggestions block for Plugin Builder delivery cards.
     *
     * @param {Object} data
     * @returns {string}
     */
    function buildPluginBuilderSuggestionsHtml(data) {
        const suggestions = Array.isArray(data.suggestions) ? data.suggestions : [];
        if (!suggestions.length) {
            return '';
        }

        const countLabel = suggestions.length === 1
            ? lcT('suggestionsOne', '1 improvement we can make next')
            : lcT('suggestionsMany', '%d improvements we can make next').replace('%d', String(suggestions.length));
        const note = data.note || data.confidence_note || '';
        const suggestionsList = `<ul class="lolacore-tc-pb-suggestion-list">${suggestions.map((s) => `<li>${escapeHtml(String(s))}</li>`).join('')}</ul>`;
        const technicalDetails = note
            ? `<details class="lolacore-tc-pb-technical"><summary>${escapeHtml(lcT('technicalDetails', 'Technical details'))}</summary><p>${escapeHtml(note)}</p></details>`
            : '';

        return `
            <details class="lolacore-tc-pb-suggestions">
                <summary>${escapeHtmlAttr(countLabel)}</summary>
                ${suggestionsList}
                ${technicalDetails}
            </details>
        `;
    }

    /**
     * Educational copy for Plugin Builder delivery cards (pre-save).
     *
     * @param {boolean} isUpdate
     * @returns {string}
     */
    function buildPluginBuilderEducationalBodyHtml(isUpdate) {
        if (isUpdate) {
            return `
                <p class="lolacore-tc-pb-edu">${escapeHtml(lcT('eduUpdateLead', "Here's the next iteration of your plugin. Test the changes in Playground before installing on your live site."))}</p>
                <p class="lolacore-tc-pb-guide">${escapeHtml(lcT('eduUpdateGuide', "Save when you're happy, then tell me what still needs to change."))}</p>
            `;
        }

        return `
            <p class="lolacore-tc-pb-edu">${escapeHtml(lcT('eduCreateLead', 'This is your working first version — it should activate cleanly and include the settings and features you described. Fine-tuning how it behaves on your store is the normal next step in plugin development.'))}</p>
            <p class="lolacore-tc-pb-guide">${escapeHtml(lcT('eduCreateGuide', "Save it to your library, test it in Playground, then tell me what to adjust. You set the direction; I'll read the code and your site and we'll refine it together."))}</p>
            <details class="lolacore-tc-pb-edu-details">
                <summary>${escapeHtml(lcT('eduHowTitle', 'How plugin building works'))}</summary>
                <p>${escapeHtml(lcT('eduHowBody', "Most plugins start as a working base, then improve with each round of testing and feedback. You're the architect — you decide what the plugin should do. I'm your engineer on this site: I review the code, spot gaps, and help you iterate until it fits how you run your store. Behind the scenes, our development team turns that plan into PHP, JavaScript, and CSS."))}</p>
            </details>
        `;
    }

    /**
     * Render a Plugin Builder delivery confirmation (pre-save, celebratory framing).
     *
     * @param {Object} data
     */
    function renderPluginBuilderDeliveryConfirmation(data) {
        const isUpdate = data.tool_name === 'updatePluginCode';
        const defaultMessage = isUpdate
            ? lcT('deliveryUpdateReady', "Your update is ready. Save it when you're happy to continue.")
            : lcT('deliveryStarterReady', 'Your starter plugin is ready. It should activate and show the settings you asked for — save it, try it in Playground, and tell me what to refine.');
        const msgDiv = appendMessage('assistant', data.message || defaultMessage);
        const contentDiv = msgDiv.querySelector('.lolacore-msg-content');

        const name = data.name ? `<p class="lolacore-tc-pb-name"><strong>${escapeHtml(String(data.name))}</strong></p>` : '';
        const slug = data.slug ? String(data.slug) : '';
        const version = data.next_version;
        let metaLine = '';
        if (slug && version) {
            metaLine = `${escapeHtml(slug)} · v${escapeHtml(String(version))}`;
        } else if (slug) {
            metaLine = escapeHtml(slug);
        } else if (version) {
            metaLine = `v${escapeHtml(String(version))}`;
        }
        const metaHtml = metaLine ? `<p class="lolacore-tc-pb-meta">${metaLine}</p>` : '';
        const qualityHtml = buildPluginBuilderQualityHtml(data);
        const educationalHtml = buildPluginBuilderEducationalBodyHtml(isUpdate);
        const suggestionsHtml = buildPluginBuilderSuggestionsHtml(data);
        const headerTitle = isUpdate
            ? lcT('deliveryUpdateTitle', 'Your update is ready to test')
            : lcT('deliveryStarterTitle', 'Your starter plugin is ready');

        const card = document.createElement('div');
        card.className = 'lolacore-tool-confirmation lolacore-tc-pb-delivery';
        card.innerHTML = `
            <div class="lolacore-tc-header">
                <span class="lolacore-tc-icon lolacore-tc-icon-positive" aria-hidden="true"></span>
                <strong>${escapeHtml(headerTitle)}</strong>
            </div>
            <div class="lolacore-tc-body">
                ${name}
                ${metaHtml}
                ${qualityHtml}
                ${educationalHtml}
                ${suggestionsHtml}
            </div>
            <div class="lolacore-tc-actions">
                <button class="lolacore-tc-btn lolacore-tc-approve"
                        data-action-id="${data.action_id}"
                        data-decision="approve">${escapeHtml(lcT('saveToLibrary', 'Save to library'))}</button>
                <button class="lolacore-tc-btn lolacore-tc-cancel"
                        data-action-id="${data.action_id}"
                        data-decision="cancel">${escapeHtml(lcT('notYet', 'Not yet'))}</button>
            </div>
        `;
        contentDiv.appendChild(card);
        scrollToBottom();

        card.querySelector('.lolacore-tc-approve').addEventListener('click', function() {
            handleToolDecision(this.dataset.actionId, 'approve', card);
        });
        card.querySelector('.lolacore-tc-cancel').addEventListener('click', function() {
            handleToolDecision(this.dataset.actionId, 'cancel', card);
        });
    }

    /**
     * Render a tool-confirmation card as an inline assistant message.
     * Shows what action Lola wants to perform; the user approves or cancels.
     * Only the action_id travels to the backend to prevent parameter injection.
     *
     * @param {Object} data  Backend payload: { action_id, tool_name, params, preview, message }
     */
    function renderToolConfirmation(data) {
        const previewFailed = Boolean(data.preview_failed);
        if (isPluginBuilderDelivery(data) && !previewFailed) {
            renderPluginBuilderDeliveryConfirmation(data);
            return;
        }
        if (isPluginRollbackConfirmation(data) && !previewFailed) {
            renderPluginRollbackConfirmation(data);
            return;
        }

        const msgDiv = appendMessage('assistant', data.message || lcT('approvalRequired', 'I need your approval to proceed:'));
        const contentDiv = msgDiv.querySelector('.lolacore-msg-content');

        // Format params as a readable list.
        // Keys and values originate from LLM tool_calls, which can echo
        // attacker-influenced site content (prompt injection). Escape both
        // before inserting into innerHTML to prevent DOM-based XSS.
        const paramsHtml = data.params
            ? Object.entries(data.params)
                .map(([k, v]) => `<li><strong>${escapeHtml(String(k))}:</strong> ${escapeHtml(String(v))}</li>`)
                .join('')
            : '';

        const previewHtml = data.preview
            ? `<div class="lolacore-tc-preview${previewFailed ? ' lolacore-tc-preview-failed' : ''}">${renderMarkdown(data.preview)}</div>`
            : '';

        const card = document.createElement('div');
        card.className = 'lolacore-tool-confirmation' + (previewFailed ? ' lolacore-tool-confirmation-failed' : '');
        card.innerHTML = `
            <div class="lolacore-tc-header">
                <span class="lolacore-tc-icon">${previewFailed ? '⛔' : '⚠️'}</span>
                <strong>${escapeHtml(previewFailed ? lcT('previewFailed', 'Preview failed') : lcT('approvalRequiredTitle', 'Approval required'))}</strong>
            </div>
            <div class="lolacore-tc-body">
                <p class="lolacore-tc-action"><strong>${escapeHtml(String(data.tool_name || lcT('actionLabel', 'Action')))}</strong></p>
                ${paramsHtml ? `<ul class="lolacore-tc-params">${paramsHtml}</ul>` : ''}
                ${previewHtml}
            </div>
            <div class="lolacore-tc-actions">
                <button class="lolacore-tc-btn lolacore-tc-approve"
                        data-action-id="${data.action_id}"
                        data-decision="approve"
                        ${previewFailed ? `disabled title="${escapeHtmlAttr(lcT('previewFixTitle', 'Fix the preview error before executing.'))}"` : ''}>${escapeHtml(lcT('approveExecute', 'Approve and execute'))}</button>
                <button class="lolacore-tc-btn lolacore-tc-cancel"
                        data-action-id="${data.action_id}"
                        data-decision="cancel">${escapeHtml(previewFailed ? lcT('dismiss', 'Dismiss') : lcT('cancel', 'Cancel'))}</button>
            </div>
        `;
        contentDiv.appendChild(card);
        scrollToBottom();

        // Bind button events — delegates to AJAX handler
        card.querySelector('.lolacore-tc-approve').addEventListener('click', function() {
            handleToolDecision(this.dataset.actionId, 'approve', card);
        });
        card.querySelector('.lolacore-tc-cancel').addEventListener('click', function() {
            handleToolDecision(this.dataset.actionId, 'cancel', card);
        });
    }

    /**
     * Render the progress UI for an asynchronous Plugin Builder job.
     */
    function renderBuilderProgress(data) {
        const jobId = data.job_id;
        const msgDiv = appendMessage('assistant', data.message || lcT('builderDrafting', 'The engineering team is drafting the plugin…'));
        const contentDiv = msgDiv.querySelector('.lolacore-msg-content');

        const card = document.createElement('div');
        resetProgressCard(card, lcT('builderStarting', 'Starting job...'));
        contentDiv.appendChild(card);
        scrollToBottom();

        pollBuilderProgress(jobId, card);
    }

    /**
     * Render (or re-render) the active-progress body inside a builder card.
     *
     * Used both for a fresh build and when a failed card transitions back into
     * progress on retry — so a single card animates through the whole lifecycle
     * and the user never sees two cards at once.
     */
    function resetProgressCard(card, statusText) {
        card.className = 'lolacore-builder-progress';
        card.innerHTML = `
            <div class="lolacore-bp-header">
                <span class="lolacore-bp-spinner">⏳</span>
                <strong class="lolacore-bp-title">${escapeHtml(lcT('builderBackground', 'Building in background...'))}</strong>
            </div>
            <div class="lolacore-bp-status">${escapeHtmlAttr(statusText || lcT('builderWorking', 'Working...'))}</div>
            <div class="lolacore-bp-bar-container" style="background:#f0f0f1; height:6px; border-radius:3px; margin-top:8px; overflow:hidden;">
                <div class="lolacore-bp-bar-fill" style="background:#00a32a; height:100%; width:0%; transition: width 0.5s ease-in-out;"></div>
            </div>
        `;
    }

    /**
     * Render the result of a completed async Plugin Builder job.
     *
     * `result` is the preview payload produced by the worker (name, slug,
     * note, suggestions, preview_cache_key, …). Rendered defensively:
     * every field is optional so a missing one can never throw and re-drive polling.
     */
    function renderBuilderResult(result) {
        const data = result || {};
        const name = data.name || lcT('builtPlugin', 'Built plugin');
        const note = data.note || data.confidence_note || '';
        const suggestions = Array.isArray(data.suggestions) ? data.suggestions : [];
        const isUpdate = Boolean(data.post_id && data.next_version);

        const msgDiv = appendMessage('assistant', data.message || lcT('builderReadyReview', 'Your starter plugin is ready for review.'));
        const contentDiv = msgDiv.querySelector('.lolacore-msg-content');

        const suggestionsHtml = suggestions.length
            ? `<ul class="lolacore-br-suggestions">${suggestions.map(s => `<li>${escapeHtmlAttr(String(s))}</li>`).join('')}</ul>`
            : '';
        const educationalHtml = buildPluginBuilderEducationalBodyHtml(isUpdate);
        const qualityHtml = buildPluginBuilderQualityHtml(data);

        const card = document.createElement('div');
        card.className = 'lolacore-builder-result';
        card.innerHTML = `
            <div class="lolacore-br-header">
                <span>📦</span>
                <strong>${escapeHtmlAttr(name)}</strong>
            </div>
            ${qualityHtml}
            ${educationalHtml}
            ${note ? `<details class="lolacore-tc-pb-technical"><summary>${escapeHtml(lcT('technicalDetails', 'Technical details'))}</summary><p>${escapeHtmlAttr(note)}</p></details>` : ''}
            ${suggestionsHtml}
        `;
        contentDiv.appendChild(card);
        scrollToBottom();
    }

    /**
     * Poll the REST API for builder progress until completion or failure.
     */
    async function pollBuilderProgress(jobId, cardEl) {
        const statusEl = cardEl.querySelector('.lolacore-bp-status');
        const fillEl   = cardEl.querySelector('.lolacore-bp-bar-fill');
        const spinnerEl = cardEl.querySelector('.lolacore-bp-spinner');
        const titleEl  = cardEl.querySelector('.lolacore-bp-title');

        let attempts = 0;
        // `terminated` is set the moment a terminal state (completed/failed/timeout)
        // is detected, BEFORE any rendering that could throw. The reschedule below is
        // guarded by it, so a render error can never re-drive the poll into a loop.
        let terminated = false;
        // Absolute safety cap only. The build can legitimately take several minutes
        // with a large token budget, so we keep polling while the backend reports
        // progress; this cap just prevents an infinite loop if the worker dies.
        // 240 × 5s = 20 min, well under the 30-min job transient TTL.
        const maxAttempts = 240;

        const markTerminal = (cssClass, title, status) => {
            terminated = true;
            cardEl.classList.add(cssClass);
            if (spinnerEl) spinnerEl.textContent = '';
            if (titleEl) titleEl.textContent = title;
            statusEl.textContent = status;
        };

        const poll = async () => {
            if (terminated) return;
            attempts++;
            try {
                const response = await fetch(`${ajaxData.rest_url}/builder/job/${jobId}`, {
                    method: 'GET',
                    headers: {
                        'X-WP-Nonce': ajaxData.rest_nonce
                    }
                });

                if (!response.ok) {
                    if (response.status === 404) {
                        throw new Error('Job expired or not found.');
                    }
                    throw new Error(`HTTP ${response.status}`);
                }

                const job = await response.json();

                if (job.status === 'completed') {
                    markTerminal('is-done', lcT('buildComplete', 'Build complete'), lcT('readyForReview', 'Ready for review.'));
                    fillEl.style.width = '100%';

                    // Render the result in its own guard so a render error cannot
                    // re-enter the poll cycle (terminated is already true above).
                    if (job.result) {
                        try {
                            if (job.result.action_id) {
                                // Real, approvable confirmation wired to the existing
                                // confirm handler (Approve → execute from cache → save).
                                handleBackendResponse({ success: true, data: { type: 'tool_confirmation', ...job.result } });
                            } else {
                                renderBuilderResult(job.result);
                            }
                        } catch (e) {
                            console.error('[Lola] Builder result render error:', e);
                            appendMessage('assistant', lcT('renderCardFailed', "The plugin was generated, but I couldn't render the review card. Open the Built Plugins library to find it."), true);
                        }
                    }
                    return;
                } else if (job.status === 'failed') {
                    markTerminal(
                        'is-failed',
                        lcT('builderFailedTitle', "Couldn't finish this build"),
                        job.error || lcT('builderFailedStatus', "I couldn't finish this build. Try again or simplify what you're asking for.")
                    );
                    const canRetry = (job.max_retries || 0) > (job.retry_count || 0);
                    handleBuilderFailure(jobId, cardEl, canRetry);
                    return;
                } else {
                    // processing / queued
                    const progress = job.progress || 0;
                    const step = job.step || lcT('builderWorking', 'Working...');
                    fillEl.style.width = progress + '%';
                    statusEl.textContent = step;
                }

            } catch (err) {
                console.warn('[Lola] Polling error:', err);
                // Keep polling, might be temporary network glitch
            }

            if (!terminated && attempts < maxAttempts) {
                setTimeout(poll, 5000);
            } else if (!terminated) {
                markTerminal(
                    'is-timeout',
                    lcT('stillWorkingTitle', 'Still working on it'),
                    lcT('stillWorkingStatus', 'This is taking longer than expected. It may still finish in the background — check the library shortly.')
                );
            }
        };

        // Initial delay before first poll
        setTimeout(poll, 2000);
    }

    /**
     * Handle a failed build: ask Lola for a warm, in-language recovery message and,
     * when retries remain, surface a "Try again" button on the failed card.
     *
     * The recovery message (chat flow) and the button (card) are independent UI in
     * two places. The button appears immediately — a user who just wants to retry
     * shouldn't wait for Lola to finish writing. If they hit it while her message is
     * still in flight, retryBuild cancels that request cleanly.
     */
    function handleBuilderFailure(jobId, cardEl, canRetry) {
        // Ask Lola to speak. Hold the controller on the card so a retry can cancel it.
        const controller = new AbortController();
        cardEl._lolaRecoveryAbort = controller;
        sendMessage(BUILDER_FAILED_SENTINEL + jobId, { signal: controller.signal });

        if (!canRetry) {
            return;
        }

        const actions = document.createElement('div');
        actions.className = 'lolacore-bp-actions';
        actions.style.cssText = 'margin-top:10px;';

        const retryBtn = document.createElement('button');
        retryBtn.type = 'button';
        retryBtn.className = 'lolacore-bp-retry';
        retryBtn.textContent = lcT('tryAgain', 'Try again');
        retryBtn.addEventListener('click', () => retryBuild(jobId, cardEl, retryBtn));

        actions.appendChild(retryBtn);
        cardEl.appendChild(actions);
        scrollToBottom();
    }

    /**
     * Re-dispatch a failed build. The same card transitions back into progress —
     * one active card, never two. Lola's now-moot recovery message is cancelled if
     * it's still loading.
     */
    async function retryBuild(jobId, cardEl, retryBtn) {
        // Guard against double clicks.
        if (retryBtn) {
            retryBtn.disabled = true;
            retryBtn.style.opacity = '0.5';
        }

        // Cancel Lola's recovery message if it hasn't arrived — the user already decided.
        if (cardEl._lolaRecoveryAbort) {
            cardEl._lolaRecoveryAbort.abort();
            cardEl._lolaRecoveryAbort = null;
        }

        // Transition the failed card back into the progress state (single card).
        resetProgressCard(cardEl, lcT('tryingAgain', 'Trying again...'));
        scrollToBottom();

        try {
            const response = await fetch(`${ajaxData.rest_url}/builder/job/${jobId}/retry`, {
                method: 'POST',
                headers: { 'X-WP-Nonce': ajaxData.rest_nonce }
            });

            const data = await response.json().catch(() => ({}));

            if (!response.ok || !data.job_id) {
                // Restore the failed state with a plain explanation; no jargon.
                cardEl.className = 'lolacore-builder-progress is-failed';
                const statusEl = cardEl.querySelector('.lolacore-bp-status');
                const spinnerEl = cardEl.querySelector('.lolacore-bp-spinner');
                const titleEl = cardEl.querySelector('.lolacore-bp-title');
                if (spinnerEl) spinnerEl.textContent = '';
                if (titleEl) titleEl.textContent = lcT('builderFailedTitle', "Couldn't finish this build");
                if (statusEl) {
                    statusEl.textContent = data.status === 'limit_reached'
                        ? lcT('buildLimitReached', "I've tried this a few times already. Let's try a smaller request, or switch to a stronger model in Settings.")
                        : lcT('builderRetryFailed', "I couldn't start the build again. Refresh the page and try once more.");
                }
                return;
            }

            // Re-drive polling on the SAME card with the new job id.
            pollBuilderProgress(data.job_id, cardEl);

        } catch (e) {
            console.error('[Lola] retryBuild error:', e);
            cardEl.className = 'lolacore-builder-progress is-failed';
            const statusEl = cardEl.querySelector('.lolacore-bp-status');
            if (statusEl) statusEl.textContent = lcT('builderRetryFailed', "I couldn't start the build again. Refresh the page and try once more.");
        }
    }

    /**
     * Lazy singleton modal for Playground install confirmations (overwrite, etc.).
     */
    let pbChatModalEl      = null;
    let pbChatModalCloseFn = null;

    function pbChatModalOnKeydown(event) {
        if (event.key === 'Escape' && typeof pbChatModalCloseFn === 'function') {
            pbChatModalCloseFn(false);
        }
    }

    function pbHideChatModal() {
        if (! pbChatModalEl) {
            return;
        }
        pbChatModalEl.style.display = 'none';
        document.removeEventListener('keydown', pbChatModalOnKeydown);
        pbChatModalCloseFn = null;
    }

    function pbEnsureChatModal() {
        if (pbChatModalEl) {
            return pbChatModalEl;
        }

        pbChatModalEl = document.createElement('div');
        pbChatModalEl.id = 'lolacore-chat-modal';
        pbChatModalEl.className = 'lc-modal-overlay';
        pbChatModalEl.style.display = 'none';
        pbChatModalEl.setAttribute('role', 'dialog');
        pbChatModalEl.setAttribute('aria-modal', 'true');
        pbChatModalEl.setAttribute('aria-labelledby', 'lolacore-chat-modal-title');
        pbChatModalEl.innerHTML = `
            <div class="lc-modal-card" role="document">
                <div class="lc-modal-header lc-modal-header--neutral">
                    <h3 id="lolacore-chat-modal-title"></h3>
                </div>
                <div class="lc-modal-body">
                    <p id="lolacore-chat-modal-message"></p>
                </div>
                <div class="lc-modal-footer" id="lolacore-chat-modal-footer"></div>
            </div>`;

        pbChatModalEl.addEventListener('click', function(event) {
            if (event.target === pbChatModalEl && typeof pbChatModalCloseFn === 'function') {
                pbChatModalCloseFn(false);
            }
        });

        document.body.appendChild(pbChatModalEl);
        return pbChatModalEl;
    }

    /**
     * @param {{ title: string, message: string, buttons: Array<{label: string, primary?: boolean, onClick: function}> }} opts
     * @returns {Promise<boolean>} Resolves true when the primary action runs; false on cancel/backdrop/Escape.
     */
    function pbShowChatModal(opts) {
        const modal     = pbEnsureChatModal();
        const titleEl   = modal.querySelector('#lolacore-chat-modal-title');
        const messageEl = modal.querySelector('#lolacore-chat-modal-message');
        const footerEl  = modal.querySelector('#lolacore-chat-modal-footer');

        if (! titleEl || ! messageEl || ! footerEl) {
            return Promise.resolve(false);
        }

        titleEl.textContent   = opts.title || '';
        messageEl.textContent = opts.message || '';
        footerEl.innerHTML    = '';

        return new Promise(function(resolve) {
            let settled = false;

            function finish(result) {
                if (settled) {
                    return;
                }
                settled = true;
                pbHideChatModal();
                resolve(Boolean(result));
            }

            pbChatModalCloseFn = finish;

            (opts.buttons || []).forEach(function(btn) {
                const el = document.createElement('button');
                el.type = 'button';
                el.className = 'lolacore-artifact-btn ' + (btn.primary ? 'lolacore-artifact-btn-primary' : 'lolacore-artifact-btn-secondary');
                el.textContent = btn.label || '';
                el.addEventListener('click', function() {
                    if (typeof btn.onClick === 'function') {
                        btn.onClick(finish);
                        return;
                    }
                    finish(btn.primary === true);
                });
                footerEl.appendChild(el);
            });

            document.addEventListener('keydown', pbChatModalOnKeydown);
            modal.style.display = 'flex';
        });
    }

    /**
     * Ask the user to confirm overwriting an on-disk builder artifact (plugin or block).
     *
     * @param {'plugin'|'block'} kind
     * @returns {Promise<boolean>}
     */
    function pbConfirmOverwrite(kind) {
        const isBlock = kind === 'block';

        return pbShowChatModal({
            title: lcT('overwriteConfirmTitle', 'Overwrite installed copy'),
            message: isBlock
                ? lcT('overwriteConfirmMessageBlock', 'A version is already on this site. I will replace those files.')
                : lcT('overwriteConfirmMessagePlugin', 'A version is already on this site. I will replace those files. Any active copy will be deactivated first.'),
            buttons: [
                {
                    label: lcT('cancel', 'Cancel'),
                    primary: false,
                    onClick: function(finish) { finish(false); }
                },
                {
                    label: lcT('overwrite', 'Overwrite'),
                    primary: true,
                    onClick: function(finish) { finish(true); }
                }
            ]
        });
    }

    /**
     * Playground gate AJAX + copy for Plugin Builder and Block Builder.
     *
     * @param {'plugin'|'block'} kind
     */
    function getPlaygroundBuilderConfig(kind) {
        if (kind === 'block') {
            return {
                defaultName: lcT('builtBlock', 'Built block'),
                ajax: {
                    playground: 'lolacore_bb_playground',
                    opened: 'lolacore_bb_playground_opened',
                    attest: 'lolacore_bb_playground_attest',
                    install: 'lolacore_bb_install',
                },
                attestCopy: lcT('bbAttestCopy', 'Insert the block in the editor and check the front end. When it looks right, install it on your site.'),
                issuePrompt: pbIssueReportPrompt,
                installErrorDefault: lcT('bbInstallError', 'I could not install the block. Try again from Built Blocks.'),
                installErrorNetwork: lcT('bbInstallErrorNetwork', 'I could not install the block. Check your connection and try again.'),
                installedDefault: lcT('bbInstalled', 'I installed the block files on this site.'),
                overwriteConfirm: lcT('overwriteConfirm', 'A copy is already installed. Overwrite it with this library version?'),
                supportsActivate: false,
            };
        }

        return {
            defaultName: lcT('builtPlugin', 'Built plugin'),
            ajax: {
                playground: 'lolacore_pb_playground',
                opened: 'lolacore_pb_playground_opened',
                attest: 'lolacore_pb_playground_attest',
                install: 'lolacore_pb_install',
            },
            attestCopy: lcT('pbAttestCopy', 'Check the settings page and the front end. When it looks right, install it on your site.'),
            issuePrompt: pbIssueReportPrompt,
            installErrorDefault: lcT('pbInstallError', 'I could not install the plugin. Try again from Built Plugins.'),
            installErrorNetwork: lcT('pbInstallErrorNetwork', 'I could not install the plugin. Check your connection and try again.'),
            installedDefault: lcT('pbInstalled', 'I installed the plugin files on this site.'),
            overwriteConfirm: lcT('overwriteConfirm', 'A copy is already installed. Overwrite it with this library version?'),
            supportsActivate: true,
        };
    }

    /**
     * POST to builder admin AJAX endpoints (Playground gate).
     */
    function pbAjaxPost(action, postId, extra) {
        const body = new URLSearchParams({
            action:      action,
            post_id:     postId,
            _ajax_nonce: ajaxData.nonce
        });
        if (extra) {
            Object.keys(extra).forEach(function(key) {
                body.set(key, extra[key]);
            });
        }
        return fetch(ajaxData.ajax_url, {
            method:  'POST',
            headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
            body
        }).then(function(r) { return r.json(); });
    }

    function pbIssueReportPrompt(pluginName) {
        return lcT('issuePrompt', 'I tested "%s" in Playground and something\'s not right: ').replace('%s', pluginName);
    }

    /**
     * Collapse Playground actions after the user commits to install.
     *
     * @param {HTMLElement} containerEl
     */
    function pbDemotePlaygroundRow(containerEl) {
        const openRow = containerEl.querySelector('.lolacore-pg-open-row');
        if (openRow) {
            openRow.classList.add('lolacore-pg-open-row--demoted');
        }
        const openLink = containerEl.querySelector('.lolacore-pg-open-link');
        if (openLink && openLink.dataset.lcDemoted !== '1') {
            openLink.dataset.lcDemoted = '1';
            openLink.textContent = lcT('openPlaygroundAgain', 'Open in Playground again');
        }
    }

    /**
     * Hide pre-install controls once install succeeds or is in progress.
     *
     * @param {HTMLElement} containerEl
     */
    function pbHidePreInstallUI(containerEl) {
        const attestPanel = containerEl.querySelector('.lolacore-pg-attest-panel');
        const pendingRow  = containerEl.querySelector('.lolacore-pg-pending-install-row');
        if (attestPanel) {
            attestPanel.style.display = 'none';
        }
        if (pendingRow) {
            pendingRow.style.display = 'none';
        }
        pbDemotePlaygroundRow(containerEl);
    }

    /**
     * Show installed state with optional activation prompt.
     *
     * @param {HTMLElement} containerEl
     * @param {string|number} postId
     * @param {Object} data Install AJAX payload.
     */
    function pbRenderInstalledState(containerEl, postId, data, kind) {
        const config = getPlaygroundBuilderConfig(kind || 'plugin');
        pbHidePreInstallUI(containerEl);
        containerEl.classList.add('lolacore-pg-is-installed');

        const msg         = (data && data.message) || config.installedDefault;
        const canActivate = config.supportsActivate && Boolean(data && data.can_activate);
        const pluginsUrl  = (data && data.plugins_url) || '';

        let panel = containerEl.querySelector('.lolacore-pg-installed-panel');
        if (!panel) {
            panel = document.createElement('div');
            panel.className = 'lolacore-pg-installed-panel';
            containerEl.appendChild(panel);
        }

        const pluginsLink = pluginsUrl
            ? ' <a href="' + escapeHtmlAttr(pluginsUrl) + '" target="_blank" rel="noopener noreferrer">' + escapeHtml(lcT('openPlugins', 'Open Plugins')) + '</a>'
            : '';

        let activateHtml = '';
        if (canActivate) {
            activateHtml = `
                <p class="lolacore-pg-activate-prompt">${escapeHtml(lcT('activatePrompt', 'Want me to activate it now?'))}</p>
                <div class="lolacore-artifact-actions lolacore-pg-activate-actions">
                    <button type="button"
                            class="lolacore-artifact-btn lolacore-artifact-btn-primary lolacore-pg-activate-btn">
                        ${escapeHtml(lcT('activateNow', 'Activate now'))}
                    </button>
                    <button type="button"
                            class="lolacore-artifact-btn lolacore-artifact-btn-secondary lolacore-pg-activate-skip">
                        ${escapeHtml(lcT('notYet', 'Not yet'))}
                    </button>
                </div>`;
        }

        panel.innerHTML = `
            <p class="lolacore-pg-install-status lolacore-pg-install-status--success">${escapeHtml(msg)}${pluginsLink}</p>
            ${activateHtml}`;
        panel.style.display = '';

        const activateBtn = panel.querySelector('.lolacore-pg-activate-btn');
        if (activateBtn) {
            activateBtn.addEventListener('click', function() {
                pbRunActivate(postId, containerEl, activateBtn);
            });
        }

        const skipBtn = panel.querySelector('.lolacore-pg-activate-skip');
        if (skipBtn) {
            skipBtn.addEventListener('click', function() {
                const promptEl = panel.querySelector('.lolacore-pg-activate-prompt');
                const actionsEl = panel.querySelector('.lolacore-pg-activate-actions');
                if (promptEl) {
                    promptEl.style.display = 'none';
                }
                if (actionsEl) {
                    actionsEl.style.display = 'none';
                }
            });
        }
    }

    /**
     * Show a recoverable install error without losing Playground attestation.
     *
     * @param {HTMLElement} containerEl
     * @param {string} message
     * @param {{ showInstallRetry?: boolean, postId?: string|number }} [options]
     */
    function pbShowInstallError(containerEl, message, options) {
        const opts = options || {};
        let statusEl = containerEl.querySelector('.lolacore-pg-install-error');
        if (!statusEl) {
            statusEl = document.createElement('p');
            statusEl.className = 'lolacore-pg-install-error';
            containerEl.appendChild(statusEl);
        }
        statusEl.textContent = message;

        if (opts.showInstallRetry && opts.postId) {
            const pendingRow = containerEl.querySelector('.lolacore-pg-pending-install-row');
            if (pendingRow) {
                pendingRow.style.display = '';
            }
            bindPlaygroundInstallButton(containerEl, opts.postId, opts.kind || 'plugin');
        }
    }

    /**
     * Run local install via admin AJAX (no LLM roundtrip).
     *
     * @param {string|number} postId
     * @param {HTMLElement} containerEl
     * @param {HTMLButtonElement|null} btn
     * @param {boolean} overwrite
     * @returns {Promise<boolean>} Resolves true when install succeeded.
     */
    function pbRunInstall(postId, containerEl, btn, overwrite, kind) {
        const config = getPlaygroundBuilderConfig(kind || 'plugin');
        if (btn) {
            btn.disabled = true;
        }

        const extra = overwrite ? { overwrite: '1' } : null;

        return pbAjaxPost(config.ajax.install, postId, extra)
            .then(function(result) {
                if (result.success) {
                    pbRenderInstalledState(containerEl, postId, result.data || {}, kind);
                    const errEl = containerEl.querySelector('.lolacore-pg-install-error');
                    if (errEl) {
                        errEl.remove();
                    }
                    return true;
                }

                if (result.data && result.data.needs_overwrite) {
                    return pbConfirmOverwrite(kind || 'plugin').then(function(confirmed) {
                        if (! confirmed) {
                            return false;
                        }
                        return pbRunInstall(postId, containerEl, btn, true, kind);
                    });
                }

                pbShowInstallError(
                    containerEl,
                    (result.data && result.data.message) || config.installErrorDefault,
                    { showInstallRetry: true, postId: postId, kind: kind || 'plugin' }
                );
                return false;
            })
            .catch(function() {
                pbShowInstallError(
                    containerEl,
                    config.installErrorNetwork,
                    { showInstallRetry: true, postId: postId, kind: kind || 'plugin' }
                );
                return false;
            })
            .finally(function() {
                if (btn) {
                    btn.disabled = false;
                }
            });
    }

    /**
     * Show an activation error inside the installed-state panel.
     *
     * @param {HTMLElement|null} panel
     * @param {string} message
     */
    function pbShowActivateError(panel, message) {
        if (! panel) {
            return;
        }

        let errEl = panel.querySelector('.lolacore-pg-activate-error');
        if (! errEl) {
            errEl = document.createElement('p');
            errEl.className = 'lolacore-pg-activate-error';
            panel.appendChild(errEl);
        }
        errEl.textContent = message;
    }

    /**
     * Activate a plugin that was installed via the Playground gate.
     *
     * @param {string|number} postId
     * @param {HTMLElement} containerEl
     * @param {HTMLButtonElement|null} btn
     */
    function pbRunActivate(postId, containerEl, btn) {
        if (btn) {
            btn.disabled = true;
        }

        return pbAjaxPost('lolacore_pb_activate', postId)
            .then(function(result) {
                const panel = containerEl.querySelector('.lolacore-pg-installed-panel');
                if (!result.success) {
                    const errMsg = (result.data && result.data.message) || lcT('activateFailed', 'I could not activate the plugin. Open Plugins and try from there.');
                    pbShowActivateError(panel, errMsg);
                    return;
                }

                const msg = (result.data && result.data.message) || lcT('activatedDone', 'Done. The plugin is active on this site.');
                if (panel) {
                    panel.innerHTML = '<p class="lolacore-pg-install-status lolacore-pg-install-status--success">' + escapeHtml(msg) + '</p>';
                }
            })
            .catch(function() {
                const panel = containerEl.querySelector('.lolacore-pg-installed-panel');
                pbShowActivateError(
                    panel,
                    lcT('activateFailed', 'I could not activate the plugin. Open Plugins and try from there.')
                );
            })
            .finally(function() {
                if (btn) {
                    btn.disabled = false;
                }
            });
    }

    /**
     * Attest Playground testing, then install on the live site in one step.
     *
     * @param {string|number} postId
     * @param {HTMLElement} containerEl
     * @param {HTMLButtonElement|null} btn
     */
    function pbAttestAndInstall(postId, containerEl, btn, kind) {
        const config = getPlaygroundBuilderConfig(kind || 'plugin');
        if (btn) {
            btn.disabled = true;
        }

        return pbAjaxPost(config.ajax.attest, postId, { outcome: 'attested' })
            .then(function(result) {
                if (!result.success) {
                    pbShowInstallError(containerEl, lcT('attestSaveFailed', 'I could not save your Playground check. Try again.'));
                    return false;
                }
                return pbRunInstall(postId, containerEl, btn, false, kind);
            })
            .finally(function() {
                if (btn) {
                    btn.disabled = false;
                }
            });
    }

    function bindPlaygroundInstallButton(containerEl, postId, kind) {
        const installBtn = containerEl.querySelector('.lolacore-pg-install-btn');
        if (!installBtn || installBtn.dataset.lolacorePgBound === '1') {
            return;
        }
        installBtn.dataset.lolacorePgBound = '1';
        installBtn.addEventListener('click', function() {
            pbRunInstall(postId, containerEl, installBtn, false, kind || 'plugin');
        });
    }

    /**
     * Mount Playground-first gate UI: open → attest+install → optional activate.
     *
     * @param {HTMLElement} containerEl
     * @param {{ postId: string|number, name: string, url: string, installAllowed: boolean, gateStatus?: string }} opts
     */
    function mountPlaygroundGateUI(containerEl, opts) {
        const kind         = opts.kind || 'plugin';
        const config       = getPlaygroundBuilderConfig(kind);
        const postId       = opts.postId;
        const name         = opts.name || config.defaultName;
        const url          = opts.url || '';
        const installAllowed = Boolean(opts.installAllowed);
        const gateStatus   = opts.gateStatus || 'untested';
        const failedHint   = gateStatus === 'failed'
            ? `<p class="lolacore-pg-gate-hint lolacore-pg-gate-hint--failed">${escapeHtml(lcT('gateFailedHint', 'Something did not look right last time. Open Playground again, or tell me what you saw.'))}</p>`
            : '';
        const gateHint     = installAllowed
            ? lcT('gateHintAttested', 'You already checked this in Playground. Install it on your live site when you are ready.')
            : lcT('gateHintUntested', 'Test in Playground before installing on your live site.');

        containerEl.innerHTML = `
            ${failedHint}
            <p class="lolacore-pg-gate-hint">${escapeHtml(gateHint)}</p>
            <div class="lolacore-artifact-actions lolacore-pg-open-row">
                <a class="lolacore-artifact-btn lolacore-artifact-btn-primary lolacore-pg-open-link"
                   href="${escapeHtmlAttr(url)}"
                   target="_blank"
                   rel="noopener noreferrer">${escapeHtml(lcT('openInPlayground', 'Open in Playground'))}</a>
                <button type="button"
                        class="lolacore-artifact-btn lolacore-artifact-btn-secondary lolacore-bp-copy-url"
                        data-copy-url="${escapeHtmlAttr(url)}">${escapeHtml(lcT('copyLink', 'Copy link'))}</button>
            </div>
            <div class="lolacore-pg-attest-panel" style="display:none;">
                <p class="lolacore-pg-attest-copy">${escapeHtml(config.attestCopy)}</p>
                <div class="lolacore-artifact-actions">
                    <button type="button"
                            class="lolacore-artifact-btn lolacore-artifact-btn-primary lolacore-pg-attest-ready">
                        ${escapeHtml(lcT('installOnMySite', 'Install on my site'))}
                    </button>
                    <button type="button"
                            class="lolacore-artifact-btn lolacore-artifact-btn-secondary lolacore-pg-attest-issue">
                        ${escapeHtml(lcT('somethingNotRight', "Something's not right"))}
                    </button>
                </div>
            </div>
            <div class="lolacore-pg-pending-install-row"${installAllowed ? '' : ' style="display:none;"'}>
                <div class="lolacore-artifact-actions">
                    <button type="button"
                            class="lolacore-artifact-btn lolacore-artifact-btn-primary lolacore-pg-install-btn">
                        ${escapeHtml(lcT('installOnThisSite', 'Install on this site'))}
                    </button>
                </div>
            </div>`;

        containerEl.querySelectorAll('.lolacore-bp-copy-url').forEach(function(btn) {
            btn.addEventListener('click', function() {
                const u = btn.getAttribute('data-copy-url') || '';
                if (navigator.clipboard && u) {
                    navigator.clipboard.writeText(u).then(function() {
                        btn.textContent = lcT('copied', 'Copied');
                        setTimeout(function() { btn.textContent = lcT('copyLink', 'Copy link'); }, 2000);
                    }).catch(function() { window.prompt(lcT('copyThisLink', 'Copy this link:'), u); });
                } else if (u) {
                    window.prompt(lcT('copyThisLink', 'Copy this link:'), u);
                }
            });
        });

        const openLink = containerEl.querySelector('.lolacore-pg-open-link');
        if (openLink) {
            openLink.addEventListener('click', function() {
                pbAjaxPost(config.ajax.opened, postId).catch(function() { /* non-blocking */ });
                if (installAllowed || containerEl.classList.contains('lolacore-pg-is-installed')) {
                    return;
                }
                const panel = containerEl.querySelector('.lolacore-pg-attest-panel');
                if (panel) {
                    panel.style.display = '';
                }
            });
        }

        const readyBtn = containerEl.querySelector('.lolacore-pg-attest-ready');
        if (readyBtn) {
            readyBtn.addEventListener('click', function() {
                pbAttestAndInstall(postId, containerEl, readyBtn, kind);
            });
        }

        const issueBtn = containerEl.querySelector('.lolacore-pg-attest-issue');
        if (issueBtn) {
            issueBtn.addEventListener('click', function() {
                pbAjaxPost(config.ajax.attest, postId, { outcome: 'failed' }).catch(function() { /* non-blocking */ });
                sendMessage(config.issuePrompt(name));
            });
        }

        if (installAllowed) {
            bindPlaygroundInstallButton(containerEl, postId, kind);
        }
    }

    /**
     * Fetch Playground URL and mount gate UI, or show AJAX retry on failure.
     *
     * @param {HTMLElement} containerEl
     * @param {string|number} postId
     * @param {string} name
     * @param {{ showProgress?: boolean }} opts
     */
    function pbFetchPlaygroundAndMount(containerEl, postId, name, opts) {
        const kind         = (opts && opts.kind) || 'plugin';
        const config       = getPlaygroundBuilderConfig(kind);
        const showProgress = opts && opts.showProgress;

        if (showProgress) {
            containerEl.className = 'lolacore-bp-playground-progress';
            containerEl.innerHTML = `
                <div class="lolacore-bp-status" style="font-size:12px; color:#666; margin-bottom:4px;">${escapeHtml(lcT('preparingPlayground', 'Preparing Playground…'))}</div>
                <div class="lolacore-bp-bar-container" style="background:#f0f0f1; height:6px; border-radius:3px; overflow:hidden;">
                    <div class="lolacore-bp-bar-fill" style="background:#00a32a; height:100%; width:0%; transition: width 0.4s ease-in-out;"></div>
                </div>`;

            const fillEl = containerEl.querySelector('.lolacore-bp-bar-fill');
            if (fillEl) {
                setTimeout(function() { fillEl.style.width = '30%'; }, 80);
                setTimeout(function() { fillEl.style.width = '60%'; }, 400);
                setTimeout(function() { fillEl.style.width = '80%'; }, 900);
            }
        }

        return pbAjaxPost(config.ajax.playground, postId)
            .then(function(result) {
                if (result.success && result.data && result.data.playground_url) {
                    containerEl.className = 'lolacore-pg-gate-root';
                    containerEl.innerHTML = '';
                    mountPlaygroundGateUI(containerEl, {
                        kind:           kind,
                        postId:         postId,
                        name:           name,
                        url:            result.data.playground_url,
                        installAllowed: Boolean(result.data.install_allowed),
                        gateStatus:     result.data.gate_status || 'untested'
                    });
                    return;
                }

                const errMsg = (result.data && result.data.message) || lcT('playgroundLinkFailed', 'Could not generate Playground link.');
                pbRenderPlaygroundError(containerEl, postId, name, errMsg, kind);
            })
            .catch(function() {
                pbRenderPlaygroundError(containerEl, postId, name, lcT('playgroundLinkFailedNetwork', 'Could not generate Playground link. Check your connection and try again.'), kind);
            });
    }

    /**
     * @param {HTMLElement} containerEl
     * @param {string|number} postId
     * @param {string} name
     * @param {string} message
     */
    function pbRenderPlaygroundError(containerEl, postId, name, message, kind) {
        containerEl.className = 'lolacore-pg-gate-root';
        containerEl.innerHTML = `
            <p class="lolacore-pg-error">${escapeHtml(message)}</p>
            <div class="lolacore-artifact-actions">
                <button type="button" class="lolacore-artifact-btn lolacore-artifact-btn-secondary lolacore-pg-retry-btn">${escapeHtml(lcT('tryAgain', 'Try again'))}</button>
            </div>`;

        const retryBtn = containerEl.querySelector('.lolacore-pg-retry-btn');
        if (retryBtn) {
            retryBtn.addEventListener('click', function() {
                retryBtn.disabled = true;
                retryBtn.textContent = lcT('working', 'Working…');
                pbFetchPlaygroundAndMount(containerEl, postId, name, { showProgress: true, kind: kind || 'plugin' })
                    .finally(function() {
                        retryBtn.disabled = false;
                        retryBtn.textContent = lcT('tryAgain', 'Try again');
                    });
            });
        }
    }

    /**
     * Render a Plugin/Block Builder artifact card (Playground link, create/update success).
     *
     * @param {Object} data Backend payload: { artifact, message, data: { post_id, name, slug, ... } }
     */
    function renderArtifactCard(data) {
        const artifact = data.artifact || '';
        const payload = data.data || {};
        const msgDiv = appendMessage('assistant', data.message || lcT('done', 'Done.'));
        const contentDiv = msgDiv.querySelector('.lolacore-msg-content');

        const isPluginArtifact = artifact === 'plugin_created' || artifact === 'plugin_updated';
        const isBlockArtifact  = artifact === 'block_created' || artifact === 'block_updated';
        const builderKind      = isBlockArtifact ? 'block' : 'plugin';
        const builderConfig    = getPlaygroundBuilderConfig(builderKind);

        const name = payload.name || builderConfig.defaultName;
        const slug = payload.slug || payload.block_slug || payload.block_name || '';
        const version = payload.version ? `v${payload.version}` : '';
        const metaParts = [slug, version].filter(Boolean);
        if ((artifact === 'plugin_created' || artifact === 'block_created') && payload.version === 1) {
            metaParts.push(lcT('readyToTestPlayground', 'Ready to test in Playground'));
        }

        const card = document.createElement('div');
        card.className = 'lolacore-artifact-card';

        let actionsHtml = '';

        if (artifact === 'plugin_playground') {
            console.warn('[Lola] plugin_playground artifact ignored — use the saved plugin card Playground actions.');
        } else if (artifact === 'block_playground') {
            console.warn('[Lola] block_playground artifact ignored — use the saved block card Playground actions.');
        } else if (isPluginArtifact || isBlockArtifact) {
            const postId = payload.post_id || '';
            actionsHtml = postId
                ? '<div class="lolacore-bp-playground-progress" style="margin-top:10px;"></div>'
                : '';
        }

        card.innerHTML = `
            <div class="lolacore-artifact-header">
                <strong>${escapeHtml(name)}</strong>
            </div>
            ${metaParts.length ? `<div class="lolacore-artifact-meta">${metaParts.map(escapeHtml).join(' · ')}</div>` : ''}
            ${actionsHtml}
        `;

        contentDiv.appendChild(card);
        scrollToBottom();

        if ((isPluginArtifact || isBlockArtifact) && payload.post_id) {
            const progressWrap = card.querySelector('.lolacore-bp-playground-progress');
            if (progressWrap) {
                setTimeout(function() {
                    pbFetchPlaygroundAndMount(progressWrap, payload.post_id, name, {
                        showProgress: true,
                        kind: builderKind
                    });
                }, 0);
            }
        }
    }

    function escapeHtml(value) {
        return String(value)
            .replace(/&/g, '&amp;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;');
    }

    function escapeHtmlAttr(value) {
        return escapeHtml(value).replace(/'/g, '&#39;');
    }

    // =========================================================================
    // C. Session Management
    // =========================================================================

    /**
     * Start a fresh chat session: generate UUID, register on backend, show greeting.
     * On first-ever open (no prior sessions + scan data available), runs the
     * onboarding sequence with a visual scan animation and a proactive first message.
     *
     * @param {string} [closeSummary='']  Fresh summary from the just-closed session (if any).
     */
    function startNewSession(closeSummary) {
        console.log('[Lola] startNewSession()');
        activeSessionId = generateUUID();
        sessionStorage.setItem('lolacore_active_session_id', activeSessionId);
        console.log('[Lola] New session UUID:', activeSessionId);

        const registerSession = fetch(ajaxData.ajax_url, {
            method: 'POST',
            headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
            body: new URLSearchParams({
                action:         'lolacore_start_session',
                session_id:     activeSessionId,
                _lolacore_nonce: ajaxData.chat_nonce
            })
        }).catch(err => console.error('[Lola] start_session error:', err));

        // Post-Connectors first real greeting (PRD A.1)
        if (state.trigger_first_real_greeting) {
            runFirstRealGreetingFlow(registerSession);
            return;
        }

        // First-time onboarding: show scan animation + proactive first message
        if (!state.onboarding_complete) {
            runOnboardingSequence(registerSession);
            return;
        }

        // Onboarding done but no provider yet (PRD A.2)
        if (!state.is_configured) {
            showContinuityGreeting();
            return;
        }

        // Returning user: contextual greeting using the freshest summary available
        const summary = closeSummary || state.last_session_summary;
        if (summary) {
            appendMessage('assistant',
                `*${lcT('lastSessionSummary', 'Last session summary:')}* ${summary}\n\n${lcT('greetingContinue', 'Shall we continue or start something new?')}`
            );
        } else {
            appendMessage('assistant', lcT('greetingReady', "Hello. I'm ready. What shall we look into today?"));
        }
    }

    function appendConnectorsCta() {
        const ctaDiv = document.createElement('div');
        ctaDiv.className = 'lolacore-message lolacore-msg-assistant';
        ctaDiv.innerHTML = `
            <div style="width:28px;height:28px;flex-shrink:0;"></div>
            <div class="lolacore-msg-content">
                <a href="options-connectors.php" style="display:inline-block; margin-top:5px; padding: 8px 16px; background:#2271b1; color:#fff; text-decoration:none; border-radius:6px; font-weight:600; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
                    ${escapeHtml(lcT('configureConnectors', 'Configure AI Connectors'))}
                </a>
            </div>
        `;
        messagesBox.appendChild(ctaDiv);
        scrollToBottom();
    }

    function showContinuityGreeting() {
        input.disabled = true;
        btnSend.disabled = true;
        input.placeholder = lcT('continuityPlaceholder', 'Connect an AI provider to chat with me');
        appendMessage('assistant', lcT('continuityGreeting', 'Still waiting on that AI provider so we can get started. The button below takes you there — about 5 minutes.'));
        appendConnectorsCta();
    }

    async function runFirstRealGreetingFlow(registerPromise) {
        if (firstRealGreetingInFlight) {
            return;
        }
        firstRealGreetingInFlight = true;

        state.trigger_first_real_greeting = false;
        state.should_auto_open_first_greeting = false;

        input.disabled = false;
        btnSend.disabled = false;

        await registerPromise;

        showTypingIndicator();

        try {
            const response = await fetch(ajaxData.ajax_url, {
                method:  'POST',
                headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                body: new URLSearchParams({
                    action:          'lolacore_send_message',
                    session_id:      activeSessionId,
                    message:         FIRST_REAL_GREETING_SENTINEL,
                    _lolacore_nonce: ajaxData.chat_nonce
                })
            });

            if (!response.ok) {
                throw new Error(`HTTP ${response.status}`);
            }

            const result = await response.json();
            if (!result.success) {
                throw new Error(result.data?.message || 'Request failed');
            }
            handleBackendResponse(result);
        } catch (error) {
            console.error('[Lola] first real greeting error:', error);
            appendMessage('assistant', lcT('firstRealGreetingFallback', 'Now I can see your site. I went over the initial report — ask me anything, or tell me to walk you through what I found.'));
        } finally {
            hideTypingIndicator();
            firstRealGreetingInFlight = false;
        }

        markFirstRealGreetingDone();
    }

    function applyProviderGateToInput() {
        if (state.onboarding_complete && !state.is_configured) {
            input.disabled = true;
            btnSend.disabled = true;
            input.placeholder = lcT('continuityPlaceholder', 'Connect an AI provider to chat with me');
        } else if (!isBusy) {
            input.disabled = false;
            btnSend.disabled = false;
            input.placeholder = lcT('inputPlaceholder', 'Write to Lola...');
        }
    }

    function markFirstRealGreetingDone() {
        state.trigger_first_real_greeting = false;
        state.should_auto_open_first_greeting = false;

        fetch(ajaxData.ajax_url, {
            method: 'POST',
            headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
            body: new URLSearchParams({
                action:          'lolacore_mark_first_real_greeting',
                _lolacore_nonce: ajaxData.chat_nonce
            })
        }).catch(err => console.error('[Lola] mark_first_real_greeting error:', err));
    }

    async function runOnboardingSequence(registerPromise) {
        console.log('[Lola] Running onboarding sequence');

        // Disable input during onboarding
        input.disabled = true;
        btnSend.disabled = true;

        // Show scan animation
        onboardingBar.classList.remove('lolacore-hidden');
        onboardingText.textContent = lcT('onboardingAnalyzing', 'Analyzing your site…');

        // Wait for session to register
        await registerPromise;

        // Simulate scan phases for visual effect
        await delay(800);
        onboardingText.textContent = lcT('onboardingPlugins', 'Reviewing plugins and security…');
        await delay(700);
        onboardingText.textContent = lcT('onboardingProfile', 'Building your site profile…');
        await delay(500);

        // Hide scan bar
        onboardingBar.classList.add('lolacore-hidden');

        // Render the scan report card
        renderScanReportCard(state.scan_data);

        await delay(1000);

        if (state.is_configured) {
            input.disabled = false;
            btnSend.disabled = false;
            // Send onboarding init to get Lola's proactive first message
            sendMessage('__LOLA_ONBOARDING_INIT__');
        } else {
            // Missing API: show the CTA
            appendMessage('assistant', lcT('onboardingBlock', "I've reviewed your site and the report is ready. To get started, I need you to connect an AI provider in Settings → Connectors. You'll need an account with the provider and an API key — about 5 minutes, and the Connectors screen links you to the right place for each one."));
            appendConnectorsCta();
        }

        // Mark onboarding as completed so it never replays
        completeOnboarding();
    }

    /**
     * Fire-and-forget AJAX call to persist the onboarding_complete flag.
     * Also updates the in-memory state so same-page-load reopens
     * never re-trigger the onboarding sequence.
     */
    function completeOnboarding() {
        // Critical: update in-memory flag immediately
        state.onboarding_complete = true;

        fetch(ajaxData.ajax_url, {
            method: 'POST',
            headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
            body: new URLSearchParams({
                action:          'lolacore_complete_onboarding',
                _lolacore_nonce: ajaxData.chat_nonce
            })
        }).catch(err => console.error('[Lola] completeOnboarding error:', err));
    }

    function renderScanReportCard(scanData) {
        if (!scanData) {
            appendMessage('assistant', lcT('scanCompleteShort', "I've completed the initial scan of your site."));
            return;
        }
        
        // Count active plugins from the flat array structure
        let activePlugins = 0;
        let totalPlugins = 0;
        if (Array.isArray(scanData.plugins)) {
            totalPlugins = scanData.plugins.length;
            activePlugins = scanData.plugins.filter(p => p.is_active).length;
        }

        // Security-based health assessment
        let healthStatus = '✅ ' + lcT('reportHealthy', 'Healthy');
        if (scanData.security) {
            const hasIssues = scanData.security.debug_mode || !scanData.security.ssl_active;
            if (hasIssues) healthStatus = '⚠️ ' + lcT('reportNeedsAttention', 'Needs Attention');
        }
        
        const wpVersion = scanData.wordpress?.wp_version || lcT('unknown', 'Unknown');
        const phpVersion = scanData.php?.php_version || lcT('unknown', 'Unknown');
        const dbSize = scanData.database?.total_size_mb || '?';
        const posts = scanData.content?.posts_count ?? 0;
        const pages = scanData.content?.pages_count ?? 0;
        const themeName = scanData.theme?.active_theme_name || lcT('unknown', 'Unknown');
        
        const cardHtml = `
            <div style="border: 1px solid #dcdcde; border-radius: 8px; overflow: hidden; margin-top: 8px; background: #fff; width: 100%;">
                <div style="background: #f0f6fc; color: #1d2327; padding: 10px 14px; font-weight: 600; border-bottom: 1px solid #dcdcde; display: flex; align-items: center; gap: 8px;">
                    <span>📊</span> <span>${escapeHtml(lcT('reportTitle', 'Initial System Report'))}</span>
                </div>
                <div style="padding: 14px; font-size: 13px; color: #1d2327;">
                    <ul style="margin: 0; padding-left: 20px; line-height: 1.8;">
                        <li><strong>${escapeHtml(lcT('reportStatus', 'Status:'))}</strong> ${healthStatus}</li>
                        <li><strong>${escapeHtml(lcT('reportWordPress', 'WordPress:'))}</strong> ${wpVersion}</li>
                        <li><strong>${escapeHtml(lcT('reportPhp', 'PHP:'))}</strong> ${phpVersion}</li>
                        <li><strong>${escapeHtml(lcT('reportTheme', 'Active Theme:'))}</strong> ${themeName}</li>
                        <li><strong>${escapeHtml(lcT('reportPlugins', 'Plugins:'))}</strong> ${activePlugins} ${escapeHtml(lcT('reportActive', 'active'))} / ${totalPlugins} ${escapeHtml(lcT('reportInstalled', 'installed'))}</li>
                        <li><strong>${escapeHtml(lcT('reportContent', 'Content:'))}</strong> ${posts} ${escapeHtml(lcT('reportPosts', 'posts'))}, ${pages} ${escapeHtml(lcT('reportPages', 'pages'))}</li>
                        <li><strong>${escapeHtml(lcT('reportDatabase', 'Database:'))}</strong> ${dbSize} MB</li>
                    </ul>
                </div>
            </div>
        `;
        
        const msgDiv = appendMessage('assistant', lcT('scanCompleteSummary', "I've completed the initial scan of your site. Here's your summary:"));
        msgDiv.querySelector('.lolacore-msg-content').insertAdjacentHTML('beforeend', cardHtml);
        scrollToBottom();
    }

    function delay(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }

    /**
     * Close the current session and hide the chat panel.
     *
     * The panel closes instantly for responsive UX. The backend close
     * request (which generates a conversation summary via LLM) runs
     * in the background. If the user reopens the widget before the
     * summary arrives, a generic greeting is shown. Once the summary
     * resolves, it's stored in `pendingSummary` for the next open.
     */
    function closeAndResetSession() {
        const closingSessionId = activeSessionId;

        // Reset state and close panel immediately — no waiting
        activeSessionId = null;
        sessionStorage.removeItem('lolacore_active_session_id');
        messagesBox.innerHTML = '';
        panel.classList.add('lolacore-hidden');

        // Fire summary generation in the background
        if (closingSessionId) {
            fetch(ajaxData.ajax_url, {
                method: 'POST',
                headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                body: new URLSearchParams({
                    action:          'lolacore_close_session',
                    session_id:      closingSessionId,
                    _lolacore_nonce: ajaxData.chat_nonce
                })
            })
            .then(r => r.json())
            .then(result => {
                if (result.success && result.data?.summary) {
                    pendingSummary = result.data.summary;
                }
            })
            .catch(err => console.error('[Lola] close_session error:', err));
        }
    }

    /**
     * Load and render the message history for an existing session (page reload).
     *
     * @param {string} sessionId
     */
    function loadExistingSession(sessionId) {
        fetch(ajaxData.ajax_url, {
            method: 'POST',
            headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
            body: new URLSearchParams({
                action:          'lolacore_get_history',
                session_id:      sessionId,
                _lolacore_nonce: ajaxData.chat_nonce
            })
        })
        .then(r => r.json())
        .then(result => {
            if (result.success && result.data?.messages) {
                result.data.messages.forEach(msg => {
                    if ((msg.role === 'user' || msg.role === 'assistant') &&
                        msg.content && msg.content !== '__LOLA_ONBOARDING_INIT__') {
                        appendMessage(msg.role, msg.content);
                    }
                });
            }
        })
        .catch(err => console.error('[Lola] get_history error:', err));
    }

    // Inactivity auto-reset (10 min)
    function checkInactivity() {
        const tenMins = 10 * 60 * 1000;
        if (Date.now() - lastActivity > tenMins && activeSessionId) {
            closeAndResetSession();
        }
    }
    setInterval(checkInactivity, 60000);

    // Restore history on page load if a session already exists
    if (activeSessionId) {
        loadExistingSession(activeSessionId);
    }

    // Auto-open logic on load for first-time users.
    // The backend flag is the single source of truth. If onboarding is pending,
    // clear any stale session from a previous activation cycle so that
    // togglePanel → startNewSession → runOnboardingSequence fires cleanly.
    if (!state.onboarding_complete || state.should_auto_open_first_greeting) {
        activeSessionId = null;
        sessionStorage.removeItem('lolacore_active_session_id');
        messagesBox.innerHTML = '';

        setTimeout(() => {
            togglePanel();
        }, 800);
    }

    // Keep widget state in sync without a full page reload (provider saved on Connectors, etc.).
    window.addEventListener('pageshow', () => {
        scheduleWidgetStateSync(true);
    });

    document.addEventListener('visibilitychange', () => {
        if (document.visibilityState === 'visible') {
            scheduleWidgetStateSync(true);
        }
    });

    // Poll while waiting for a provider (covers Connectors REST save without reload).
    if (state.onboarding_complete && !state.is_configured) {
        const providerWaitPoll = setInterval(() => {
            if (state.is_configured) {
                clearInterval(providerWaitPoll);
                return;
            }
            syncWidgetStateFromServer({ autoOpenOnFirstGreeting: true });
        }, 3000);
    }

    // =========================================================================
    // D. AJAX Chat (replaces SSE)
    // =========================================================================

    /**
     * Classify a non-JSON admin-ajax body so we show an accurate user message.
     *
     * @param {string} raw
     * @param {number} httpStatus
     * @returns {string}
     */
    function classifyChatAjaxFailure(raw, httpStatus) {
        const body  = (raw || '').trim();
        const lower = body.toLowerCase();

        if (body === '0' || body === '-1') {
            return 'empty_ajax';
        }

        if (/wpdberror|error en la base de datos de wordpress|wordpress database error/i.test(body)) {
            return 'database_error';
        }

        if (
            httpStatus === 502 ||
            httpStatus === 503 ||
            httpStatus === 504 ||
            /gateway timeout|bad gateway|service unavailable/i.test(lower)
        ) {
            return 'server_timeout';
        }

        if (
            httpStatus === 403 ||
            httpStatus === 406 ||
            /mod_security|modsecurity|wordfence|sucuri|request blocked|access denied/i.test(lower)
        ) {
            return 'hosting_waf';
        }

        return 'invalid_response';
    }

    /**
     * @param {string} code
     * @returns {string}
     */
    function chatAjaxErrorMessage(code) {
        const messages = {
            database_error: ['databaseError', "I ran into a database problem on your site before my reply could load in the chat. Refresh the page and try again. If it keeps happening, update LolaCore — it is usually a missing plugin table, not your hosting."],
            hosting_waf: ['firewallBlocked', "Your hosting security layer blocked this request before it reached WordPress. Ask your host to allow admin-ajax.php for logged-in administrators, or try again from another network."],
            server_timeout: ['serverTimeoutError', "The server took too long to answer and the connection dropped. Try again in a moment, or ask a simpler question first."],
            empty_ajax: ['ajaxEmptyError', "WordPress returned an empty response — the request may have been interrupted. Refresh the page and try again."],
            invalid_response: ['invalidResponseError', "I got a garbled response from the server — not your hosting blocking the message, but something broke the reply format. Refresh and try again; if it persists, update LolaCore."],
        };

        const entry = messages[code] || messages.invalid_response;
        return lcT(entry[0], entry[1]);
    }

    /**
     * Parse admin-ajax JSON even when HTTP status is 4xx (WordPress uses 403 for nonce failures).
     *
     * WordPress normally answers our admin-ajax actions with a JSON envelope. When the
     * body is not that envelope, classify the failure instead of blaming the host.
     *
     * @param {Response} response Fetch response.
     * @returns {Promise<Object>}
     */
    async function parseChatAjaxResponse(response) {
        const raw = await response.text();
        let result = null;

        try {
            result = JSON.parse(raw);
        } catch (parseError) {
            // PHP/WordPress can print a wpdberror <div> before wp_send_json on sites
            // with WP_DEBUG — salvage the JSON envelope if it is still present.
            const jsonStart = raw.indexOf('{"success"');
            if (jsonStart !== -1) {
                try {
                    result = JSON.parse(raw.slice(jsonStart));
                } catch (salvageError) {
                    result = null;
                }
            }
        }

        if (result && typeof result.success === 'boolean') {
            return result;
        }

        const err = new Error(`Chat AJAX response parse failed (HTTP ${response.status})`);
        err.code = classifyChatAjaxFailure(raw, response.status);
        throw err;
    }

    /**
     * Send a user message to the backend via AJAX (POST → JSON).
     * Handles text responses and tool-confirmation requests.
     *
     * @param {string} text The message text.
     */
    async function sendMessage(text, options = {}) {
        if (!text.trim() || isBusy) return;

        // Show user bubble (unless it's an internal init message)
        if (!text.startsWith('__LOLA_')) {
            appendMessage('user', text);
        }
        input.value       = '';
        input.style.height = 'auto';
        lastActivity      = Date.now();

        showTypingIndicator();

        try {
            const response = await fetch(ajaxData.ajax_url, {
                method:  'POST',
                headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                body: new URLSearchParams({
                    action:          'lolacore_send_message',
                    session_id:      activeSessionId,
                    _lolacore_nonce: ajaxData.chat_nonce,
                    message:         text,
                }),
                signal: options.signal
            });

            const result = await parseChatAjaxResponse(response);
            handleBackendResponse(result);

        } catch (error) {
            // The caller intentionally cancelled this request (e.g. the user hit
            // "Try again" before Lola's recovery message arrived). Stay silent.
            if (error.name === 'AbortError') {
                return;
            }
            // The build card already explains the failure; never show a fake
            // connection error when the recovery LLM round-trip fails.
            if (text.startsWith(BUILDER_FAILED_SENTINEL)) {
                console.warn('[Lola] Builder recovery message skipped:', error);
                return;
            }
            console.error('[Lola] sendMessage error:', error);
            appendMessage('assistant', chatAjaxErrorMessage(error.code), true);
        } finally {
            hideTypingIndicator();
        }
    }

    /**
     * Send the user's decision (approve / cancel) for a pending tool action.
     * Disables the card buttons immediately to prevent double-submission.
     *
     * @param {string}      actionId   The pending action UUID from the backend.
     * @param {string}      decision   'approve' | 'cancel'
     * @param {HTMLElement} cardEl     The confirmation card element (to update its state).
     */
    async function handleToolDecision(actionId, decision, cardEl) {
        // Idempotency guard — see inFlightActionIds declaration for rationale.
        if (inFlightActionIds.has(actionId)) {
            return;
        }
        inFlightActionIds.add(actionId);

        // Disable buttons immediately
        cardEl.querySelectorAll('.lolacore-tc-btn').forEach(b => {
            b.disabled      = true;
            b.style.opacity = '0.5';
        });

        const statusEl       = document.createElement('p');
        statusEl.className   = 'lolacore-tc-status';
        statusEl.textContent = decision === 'approve' ? 'Executing…' : 'Cancelling…';
        cardEl.querySelector('.lolacore-tc-actions').appendChild(statusEl);

        showTypingIndicator();

        try {
            const response = await fetch(ajaxData.ajax_url, {
                method:  'POST',
                headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                body: new URLSearchParams({
                    action:          'lolacore_confirm_tool_action',
                    action_id:       actionId,
                    decision:        decision,
                    session_id:      activeSessionId,
                    _lolacore_nonce: ajaxData.chat_nonce
                })
            });

            const result = await parseChatAjaxResponse(response);

            // Update card status badge
            statusEl.textContent = decision === 'approve' ? '✅ Executed' : '❌ Cancelled';
            statusEl.style.color = decision === 'approve' ? '#00a32a' : '#d63638';

            handleBackendResponse(result);

        } catch (err) {
            statusEl.textContent = lcT('error', 'Error');
            statusEl.style.color = '#d63638';
            if (err.code) {
                appendMessage('assistant', chatAjaxErrorMessage(err.code), true);
            } else {
                appendMessage('assistant', lcT('connectionError', "I couldn't get a response. This is usually a temporary connection issue — try sending your message again. If it keeps happening, check that your AI provider is still connected in Settings → Connectors."), true);
            }
            console.error('[Lola] handleToolDecision error:', err);
        } finally {
            inFlightActionIds.delete(actionId);
            hideTypingIndicator();
        }
    }

    /**
     * Centralised handler for all backend JSON responses.
     * Routes to the correct UI update based on `data.type`.
     *
     * Expected response shape (success):
     *   { success: true, data: { type: 'message'|'tool_confirmation'|'error', content, ... } }
     *
     * @param {Object} result  Parsed wp_send_json_success / wp_send_json_error payload.
     */
    function handleBackendResponse(result) {
        if (!result.success) {
            const msg = result.data?.message || lcT('processingFailed', "Something went wrong and I couldn't process that. Try refreshing the page.");
            appendMessage('assistant', msg, true);
            return;
        }

        const data = result.data;

        switch (data.type) {
            case 'tool_confirmation':
                // Render a confirmation card — user must approve before action executes
                renderToolConfirmation(data);
                break;

            case 'builder_progress':
                renderBuilderProgress(data);
                break;

            case 'artifact':
                renderArtifactCard(data);
                break;

            case 'message':
            default: {
                const assistantText = (data.content || '').trim();
                const fallbackEmpty = "I didn't get a usable reply from the model — the response came back empty. Please send your message again. If this keeps happening with DeepSeek V4 Flash on large plugin requests, switch to DeepSeek V4 Pro in Settings or try a shorter prompt first.";
                const msgDiv = appendMessage('assistant', assistantText || fallbackEmpty);

                // Optionally show memory-saved indicator
                if (data.memory && data.memory.length > 0) {
                    data.memory.forEach(fact => appendMemoryIndicator(msgDiv, fact));
                }
                break;
            }
        }
    }

    // =========================================================================
    // E. Input Bindings
    // =========================================================================

    input.addEventListener('keydown', function(e) {
        if (e.key === 'Enter' && !e.shiftKey) {
            e.preventDefault();
            sendMessage(this.value);
        }
    });

    btnSend.addEventListener('click', function() {
        sendMessage(input.value);
    });

    // Auto-resize textarea up to 120 px
    input.addEventListener('input', function() {
        this.style.height = 'auto';
        this.style.height = (this.scrollHeight < 120 ? this.scrollHeight : 120) + 'px';
    });
});
