/**
 * Frontend JavaScript for Antimanual AI Search Block
 * Integrates with existing Antimanual AI Search API
 *
 * @package Antimanual
 * @since 1.2.0
 */

(function() {
    'use strict';

    /**
     * Escape a string for safe insertion into HTML attributes and text content.
     *
     * @param {string} str The raw string.
     * @return {string} Escaped string safe for HTML contexts.
     */
    function escapeHtml(str) {
        if (!str) return '';
        var div = document.createElement('div');
        div.appendChild(document.createTextNode(str));
        return div.innerHTML;
    }

    /**
     * Sanitize HTML by only allowing safe tags and attributes.
     * Strips all script, style, event handlers, and dangerous elements.
     *
     * @param {string} html The raw HTML string from the API.
     * @return {string} Sanitized HTML string.
     */
    function sanitizeHtml(html) {
        if (!html) return '';

        var allowedTags = [
            'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'br', 'hr',
            'strong', 'em', 'b', 'i', 'u', 'mark',
            'a', 'img',
            'ul', 'ol', 'li',
            'pre', 'code', 'blockquote',
            'table', 'thead', 'tbody', 'tr', 'th', 'td',
            'div', 'span', 'sup', 'sub'
        ];

        var allowedAttrs = {
            'a': ['href', 'title', 'target', 'rel'],
            'img': ['src', 'alt', 'width', 'height'],
            'td': ['colspan', 'rowspan'],
            'th': ['colspan', 'rowspan'],
            'code': ['class'],
            'pre': ['class'],
            'div': ['class'],
            'span': ['class']
        };

        var tempDiv = document.createElement('div');
        tempDiv.innerHTML = html;

        /**
         * Recursively clean DOM nodes, removing disallowed tags and attributes.
         *
         * @param {HTMLElement} element The parent element to clean.
         */
        function cleanNode(element) {
            var nodesToRemove = [];

            for (var i = 0; i < element.childNodes.length; i++) {
                var node = element.childNodes[i];

                if (node.nodeType === Node.ELEMENT_NODE) {
                    var tagName = node.tagName.toLowerCase();

                    if (allowedTags.indexOf(tagName) === -1) {
                        // Move child nodes up, then mark for removal.
                        while (node.firstChild) {
                            element.insertBefore(node.firstChild, node);
                        }
                        nodesToRemove.push(node);
                    } else {
                        // Remove disallowed attributes.
                        var attrs = Array.prototype.slice.call(node.attributes);
                        var tagAllowed = allowedAttrs[tagName] || [];

                        attrs.forEach(function(attr) {
                            if (tagAllowed.indexOf(attr.name) === -1) {
                                node.removeAttribute(attr.name);
                            }
                        });

                        // Sanitize href to prevent javascript: URLs.
                        if (tagName === 'a' && node.hasAttribute('href')) {
                            var href = node.getAttribute('href');
                            if (href && !/^https?:\/\//i.test(href) && href.charAt(0) !== '/' && href.charAt(0) !== '#') {
                                node.setAttribute('href', '#');
                            }
                            // Force safe target/rel for external links.
                            node.setAttribute('target', '_blank');
                            node.setAttribute('rel', 'noopener noreferrer');
                        }

                        // Sanitize img src to prevent javascript: URLs.
                        if (tagName === 'img' && node.hasAttribute('src')) {
                            var src = node.getAttribute('src');
                            if (src && !/^https?:\/\//i.test(src) && src.charAt(0) !== '/') {
                                node.removeAttribute('src');
                            }
                        }

                        cleanNode(node);
                    }
                }
            }

            nodesToRemove.forEach(function(n) {
                if (n.parentNode) {
                    n.parentNode.removeChild(n);
                }
            });
        }

        cleanNode(tempDiv);
        return tempDiv.innerHTML;
    }

    /**
     * Get a translatable string from localized data.
     *
     * @param {string} key    The string key.
     * @param {string} backup Fallback value if localization is missing.
     * @return {string} The translated string.
     */
    function __( key, backup ) {
        var strings = (window.antimanual_vars && window.antimanual_vars.strings) ? window.antimanual_vars.strings : {};
        return strings[ key ] || backup || key;
    }

    /**
     * Sync the search button state with the current field value.
     *
     * @param {HTMLInputElement}   searchField  The search field.
     * @param {HTMLButtonElement}  searchButton The search button.
     */
    function syncSearchButtonState(searchField, searchButton) {
        var hasValue = searchField.value.trim().length > 0;

        searchButton.disabled = !hasValue;
        searchButton.classList.toggle('disabled', !hasValue);
    }

    /**
     * Start a search for the current block state.
     *
     * @param {HTMLElement}      block            The search block root.
     * @param {HTMLInputElement} searchField      The search field.
     * @param {HTMLButtonElement} searchButton    The search button.
     * @param {HTMLElement}      resultsContainer The results container.
     */
    function triggerSearch(block, searchField, searchButton, resultsContainer) {
        syncSearchButtonState(searchField, searchButton);

        if (searchButton.disabled) {
            searchField.focus();
            return;
        }

        block.dataset.resultsClosed = 'false';
        hideSuggestedQuestions(block);
        hidePopularKeywords(block);
        performSearch(searchField, resultsContainer, block);
    }

    /**
     * Set a search query into the field, then trigger a search.
     *
     * @param {HTMLElement}      block            The search block root.
     * @param {HTMLInputElement} searchField      The search field.
     * @param {HTMLButtonElement} searchButton    The search button.
     * @param {HTMLElement}      resultsContainer The results container.
     * @param {string}           value            The value to search.
     */
    function setSearchQueryAndSearch(block, searchField, searchButton, resultsContainer, value) {
        if (!value) {
            return;
        }

        searchField.value = value;
        triggerSearch(block, searchField, searchButton, resultsContainer);
    }

    /**
     * Build a stable local storage key for a vote.
     *
     * @param {number|null} queryLogId Logged query row ID.
     * @param {string}      query      Search query text.
     * @return {string} Vote storage key.
     */
    function getVoteStorageKey(queryLogId, query) {
        if (queryLogId) {
            return 'antimanual-voted-' + String(queryLogId);
        }

        return 'antimanual-voted-query-' + query;
    }

    document.addEventListener('DOMContentLoaded', function() {
        var searchBlocks = document.querySelectorAll('.antimanual-search-block');

        searchBlocks.forEach(function(block) {
            initializeSearchBlock(block);
        });
    });

    /**
     * Initialize a single search block instance.
     *
     * @param {HTMLElement} block The search block root element.
     */
    function initializeSearchBlock(block) {
        var searchField = block.querySelector('.search-field');
        var searchButton = block.querySelector('.search-button');
        var resultsContainer = block.querySelector('.ai-search-results');

        if (!searchField || !searchButton || !resultsContainer) {
            return;
        }

        // Track if user manually closed results.
        block.dataset.resultsClosed = 'false';

        // Store abort controller for cancelling pending requests.
        block.abortController = null;

        syncSearchButtonState(searchField, searchButton);

        // Handle search field input changes.
        searchField.addEventListener('input', function() {
            syncSearchButtonState(this, searchButton);
        });

        // Handle Enter key press in search field.
        searchField.addEventListener('keydown', function(e) {
            if (e.key === 'Enter' && this.value.trim().length > 0) {
                e.preventDefault();
                triggerSearch(block, searchField, searchButton, resultsContainer);
            }
        });

        // Handle search button click.
        searchButton.addEventListener('click', function() {
            triggerSearch(block, searchField, searchButton, resultsContainer);
        });

        // Handle Escape key to close results.
        block.addEventListener('keydown', function(e) {
            if (e.key === 'Escape' && resultsContainer.innerHTML.trim() !== '') {
                e.preventDefault();
                closeSearchResults(block);
                searchField.focus();
            }
        });

        // Handle popular keyword tag clicks.
        var keywordTags = block.querySelectorAll('.popular-keyword-tag');
        keywordTags.forEach(function(tag) {
            tag.addEventListener('click', function() {
                setSearchQueryAndSearch(block, searchField, searchButton, resultsContainer, this.dataset.keyword);
            });
        });

        // Handle suggested question tag clicks.
        var questionTags = block.querySelectorAll('.suggested-question-tag');
        questionTags.forEach(function(tag) {
            tag.addEventListener('click', function() {
                setSearchQueryAndSearch(block, searchField, searchButton, resultsContainer, this.dataset.question);
            });
        });
    }

    /**
     * Hide the popular keywords row when a search is active.
     *
     * @param {HTMLElement} block The search block element.
     */
    function hidePopularKeywords(block) {
        var keywords = block.querySelector('.popular-keywords');
        if (keywords) {
            keywords.style.display = 'none';
        }
    }

    /**
     * Show the popular keywords row (e.g. when results are closed).
     *
     * @param {HTMLElement} block The search block element.
     */
    function showPopularKeywords(block) {
        var keywords = block.querySelector('.popular-keywords');
        if (keywords) {
            keywords.style.display = '';
        }
    }

    /**
     * Hide the suggested questions section when a search is active.
     *
     * @param {HTMLElement} block The search block element.
     */
    function hideSuggestedQuestions(block) {
        var questions = block.querySelector('.suggested-questions');
        if (questions) {
            questions.style.display = 'none';
        }
    }

    /**
     * Show the suggested questions section (e.g. when results are closed).
     *
     * @param {HTMLElement} block The search block element.
     */
    function showSuggestedQuestions(block) {
        var questions = block.querySelector('.suggested-questions');
        if (questions) {
            questions.style.display = '';
        }
    }

    /**
     * Close search results and cancel any pending request.
     *
     * @param {HTMLElement} block The search block element.
     */
    function closeSearchResults(block) {
        var resultsContainer = block.querySelector('.ai-search-results');
        var searchField = block.querySelector('.search-field');
        var searchButton = block.querySelector('.search-button');

        if (!resultsContainer) return;

        // Mark as manually closed.
        block.dataset.resultsClosed = 'true';

        // Abort any pending request.
        if (block.abortController) {
            block.abortController.abort();
            block.abortController = null;
        }

        // Clear the results and ARIA.
        resultsContainer.innerHTML = '';
        resultsContainer.removeAttribute('aria-busy');

        // Clear the search field and reset button state.
        if (searchField && searchButton) {
            searchField.value = '';
            syncSearchButtonState(searchField, searchButton);
        }

        // Re-show popular keywords and suggested questions.
        showPopularKeywords(block);
        showSuggestedQuestions(block);
    }

    /**
     * Filter HTML content to extract clean text for clipboard copying.
     *
     * @param {string} htmlContent The HTML content to filter.
     * @return {string} Clean text representation.
     */
    function filterHtmlContent(htmlContent) {
        if (!htmlContent) return '';

        var tempDiv = document.createElement('div');
        tempDiv.innerHTML = htmlContent;

        var allowedTags = ['h2', 'h3', 'h4', 'h5', 'h6', 'p', 'pre', 'strong', 'em', 'a', 'img', 'ul', 'ol', 'li'];

        /**
         * Recursively extract clean text from DOM nodes.
         *
         * @param {HTMLElement} element The parent element.
         * @return {string} Extracted text.
         */
        function extractCleanText(element) {
            var result = '';

            for (var i = 0; i < element.childNodes.length; i++) {
                var node = element.childNodes[i];

                if (node.nodeType === Node.TEXT_NODE) {
                    var text = node.textContent.trim();
                    if (text) {
                        result += text + ' ';
                    }
                } else if (node.nodeType === Node.ELEMENT_NODE) {
                    var tagName = node.tagName.toLowerCase();

                    if (allowedTags.indexOf(tagName) !== -1) {
                        switch (tagName) {
                            case 'h2':
                            case 'h3':
                            case 'h4':
                            case 'h5':
                            case 'h6':
                                result += '\n\n' + node.textContent.trim() + '\n';
                                break;
                            case 'p':
                                result += '\n' + node.textContent.trim() + '\n';
                                break;
                            case 'pre':
                                result += '\n```\n' + node.textContent.trim() + '\n```\n';
                                break;
                            case 'strong':
                                result += '**' + node.textContent.trim() + '** ';
                                break;
                            case 'em':
                                result += '*' + node.textContent.trim() + '* ';
                                break;
                            case 'a':
                                var href = node.getAttribute('href');
                                var linkText = node.textContent.trim();
                                result += href ? '[' + linkText + '](' + href + ') ' : linkText + ' ';
                                break;
                            case 'img':
                                var alt = node.getAttribute('alt');
                                result += alt ? '[Image: ' + alt + '] ' : '[Image] ';
                                break;
                            case 'ul':
                            case 'ol':
                                result += '\n' + extractListContent(node, tagName === 'ol') + '\n';
                                break;
                            case 'li':
                                result += extractCleanText(node);
                                break;
                            default:
                                result += extractCleanText(node);
                        }
                    } else {
                        result += extractCleanText(node);
                    }
                }
            }

            return result;
        }

        /**
         * Extract list content with proper formatting.
         *
         * @param {HTMLElement} listElement The list element (ul/ol).
         * @param {boolean}     isOrdered   Whether this is an ordered list.
         * @return {string} Formatted list text.
         */
        function extractListContent(listElement, isOrdered) {
            var listResult = '';
            var items = listElement.querySelectorAll(':scope > li');

            items.forEach(function(item, index) {
                var prefix = isOrdered ? (index + 1) + '. ' : '• ';
                var itemText = item.textContent.trim();
                if (itemText) {
                    listResult += prefix + itemText + '\n';
                }
            });

            return listResult;
        }

        var cleanText = extractCleanText(tempDiv);

        cleanText = cleanText
            .replace(/\n\s*\n\s*\n/g, '\n\n')
            .replace(/[ \t]+/g, ' ')
            .trim();

        return cleanText;
    }

    /**
     * Perform the AI search API call.
     *
     * @param {HTMLInputElement} searchField      The search input element.
     * @param {HTMLElement}      resultsContainer The results container element.
     * @param {HTMLElement}      block            The parent search block.
     */
    /**
     * Perform a standard blocking search request.
     *
     * @param {HTMLInputElement} searchField      The search input element.
     * @param {HTMLElement}      resultsContainer The results container element.
     * @param {HTMLElement}      block            The parent search block.
     * @param {string}           searchQuery      The search query string.
     * @param {string}           apiUrl           The REST API base URL.
     * @param {Object}           headers          The HTTP request headers.
     * @param {string}           answerDetail     The requested response length detail.
     */
    function performSearchBlocking(searchField, resultsContainer, block, searchQuery, apiUrl, headers, answerDetail) {
        // Abort previous request if still in-flight.
        if (block.abortController) {
            block.abortController.abort();
        }
        block.abortController = new AbortController();

        fetch(apiUrl + '/search', {
            method: 'POST',
            credentials: 'same-origin',
            headers: headers,
            body: 'message=' + encodeURIComponent(searchQuery) + '&answer_detail=' + encodeURIComponent(answerDetail),
            signal: block.abortController.signal
        })
        .then(function(response) {
            return response.json();
        })
        .then(function(data) {
            if (block.dataset.resultsClosed === 'true') {
                return;
            }

            if (data && data.success && data.data) {
                displaySearchResults(resultsContainer, data.data, searchQuery, block);
            } else {
                showError(resultsContainer, data.message || __('error_generic', 'Something went wrong'), block);
            }
        })
        .catch(function(error) {
            if (error.name === 'AbortError') {
                return;
            }
            console.error('Search Error:', error);
            showError(resultsContainer, __('error_network', 'Network error occurred'), block);
        })
        .finally(function() {
            block.abortController = null;
        });
    }

    /**
     * Perform a streaming search request via Server-Sent Events.
     *
     * @param {HTMLInputElement} searchField      The search input element.
     * @param {HTMLElement}      resultsContainer The results container element.
     * @param {HTMLElement}      block            The parent search block.
     * @param {string}           searchQuery      The search query string.
     * @param {string}           apiUrl           The REST API base URL.
     * @param {Object}           headers          The HTTP request headers.
     * @param {string}           answerDetail     The requested response length detail.
     */
    function performStreamingSearch(searchField, resultsContainer, block, searchQuery, apiUrl, headers, answerDetail) {
        // Abort previous request if still in-flight.
        if (block.abortController) {
            block.abortController.abort();
        }
        block.abortController = new AbortController();

        var aiAnswerLabel = __('ai_answer', 'AI Answer');
        var closeLabel = __('close', 'Close');

        // Render the streaming container shell immediately.
        resultsContainer.setAttribute('aria-busy', 'true');
        resultsContainer.innerHTML =
            '<div class="ai-answer" role="region" aria-label="' + escapeHtml(aiAnswerLabel) + '">' +
                '<button class="ai-answer-close-btn" type="button" aria-label="' + escapeHtml(closeLabel) + '">' +
                    '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">' +
                        '<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>' +
                    '</svg>' +
                '</button>' +
                '<div class="ai-answer-header">' +
                    '<svg class="ai-icon" xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 32 32" aria-hidden="true">' +
                        '<g>' +
                            '<path d="m13.294 7.436.803 2.23a8.835 8.835 0 0 0 5.316 5.316l2.23.803a.229.229 0 0 1 0 .43l-2.23.803a8.835 8.835 0 0 0-5.316 5.316l-.803 2.23a.229.229 0 0 1-.43 0l-.803-2.23a8.835 8.835 0 0 0-5.316-5.316l-2.23-.803a.229.229 0 0 1 0-.43l2.23-.803a8.835 8.835 0 0 0 5.316-5.316l.803-2.23a.228.228 0 0 1 .43 0z" fill="currentColor"/>' +
                            '<path d="M23.332 2.077l.407 1.129a4.477 4.477 0 0 0 2.692 2.692l1.129.407a.116.116 0 0 1 0 .218l-1.129.407a4.477 4.477 0 0 0-2.692 2.692l-.407 1.129a.116.116 0 0 1-.218 0l-.407-1.129a4.477 4.477 0 0 0-2.692-2.692l-1.129-.407a.116.116 0 0 1 0-.218l1.129-.407a4.477 4.477 0 0 0 2.692-2.692l.407-1.129a.116.116 0 0 1 .218 0z" fill="currentColor"/>' +
                            '<path d="M23.332 21.25l.407 1.129a4.477 4.477 0 0 0 2.692 2.692l1.129.407a.116.116 0 0 1 0 .218l-1.129.407a4.477 4.477 0 0 0-2.692 2.692l-.407 1.129a.116.116 0 0 1-.218 0l-.407-1.129a4.477 4.477 0 0 0-2.692-2.692l-1.129-.407a.116.116 0 0 1 0-.218l1.129-.407a4.477 4.477 0 0 0 2.692-2.692l.407-1.129c.037-.102.182-.102.218 0z" fill="currentColor"/>' +
                        '</g>' +
                    '</svg>' +
                    '<h4>' + escapeHtml(aiAnswerLabel) + '</h4>' +
                '</div>' +
                '<div class="answer-content">' +
                    '<span class="atml-stream-cursor">▍</span>' +
                '</div>' +
            '</div>';

        var closeBtn = resultsContainer.querySelector('.ai-answer-close-btn');
        if (closeBtn) {
            closeBtn.addEventListener('click', function() {
                closeSearchResults(block);
            });
        }

        var contentContainer = resultsContainer.querySelector('.answer-content');
        var accumulated = '';
        var finalMeta = null;
        var streamError = null;

        var sseHeaders = Object.assign({}, headers, {
            'Accept': 'text/event-stream'
        });

        var bodyParams = new URLSearchParams();
        bodyParams.append('message', searchQuery);
        bodyParams.append('answer_detail', answerDetail);

        var fallbackToBlocking = function(reason) {
            console.warn('Search stream fallback to blocking due to:', reason);
            showLoadingAnimation(resultsContainer, block);
            performSearchBlocking(searchField, resultsContainer, block, searchQuery, apiUrl, headers, answerDetail);
        };

        fetch(apiUrl + '/search/stream', {
            method: 'POST',
            credentials: 'same-origin',
            headers: sseHeaders,
            body: bodyParams.toString(),
            signal: block.abortController.signal
        })
        .then(function(response) {
            if (!response.ok || !response.body || response.status === 404 || response.status === 409) {
                fallbackToBlocking('http_status_' + response.status);
                return;
            }

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

            function readStream() {
                reader.read().then(function(result) {
                    if (result.done) {
                        finalizeStream();
                        return;
                    }

                    buffer += decoder.decode(result.value, { stream: true });

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

                        if (parsed.event === 'chunk') {
                            accumulated += parsed.data?.delta || '';
                            if (contentContainer) {
                                contentContainer.innerHTML = sanitizeHtml(accumulated) + '<span class="atml-stream-cursor">▍</span>';
                            }
                        } else if (parsed.event === 'meta') {
                            finalMeta = parsed.data;
                        } else if (parsed.event === 'error') {
                            streamError = parsed.data?.message || 'Stream error';
                        }
                    }

                    readStream();
                }).catch(function(err) {
                    if (err.name === 'AbortError') {
                        return;
                    }
                    console.error('Stream read error:', err);
                    fallbackToBlocking('read_error');
                });
            }

            function finalizeStream() {
                if (streamError) {
                    fallbackToBlocking(streamError);
                    return;
                }

                if (!accumulated && !finalMeta) {
                    fallbackToBlocking('empty_response');
                    return;
                }

                var finalData = {
                    answer: accumulated,
                    references: finalMeta?.references || [],
                    query_log_id: finalMeta?.query_log_id || null,
                    is_irrelevant: finalMeta?.is_irrelevant || false
                };

                displaySearchResults(resultsContainer, finalData, searchQuery, block);
            }

            readStream();
        })
        .catch(function(error) {
            if (error.name === 'AbortError') {
                return;
            }
            console.error('Streaming Search Error:', error);
            fallbackToBlocking('network_error');
        });
    }

    /**
     * Parse an SSE frame into event name and JSON data.
     *
     * @param {string} frame The raw SSE frame.
     * @return {Object|null} Parsed event object or null.
     */
    function parseSearchSseFrame(frame) {
        var event = 'message';
        var dataLines = [];
        var lines = frame.split('\n');
        for (var i = 0; i < lines.length; i++) {
            var line = lines[i].replace(/\r$/, '');
            if (line === '' || line.indexOf(':') === 0) continue;
            var colon = line.indexOf(':');
            if (colon === -1) continue;
            var field = line.slice(0, colon);
            var value = line.slice(colon + 1).replace(/^ /, '');
            if (field === 'event') {
                event = value;
            } else if (field === 'data') {
                dataLines.push(value);
            }
        }
        if (dataLines.length === 0) return null;
        try {
            return { event: event, data: JSON.parse(dataLines.join('\n')) };
        } catch (e) {
            return null;
        }
    }

    /**
     * Perform the search query for the search block.
     * Automatically routes to streaming or blocking search based on capability.
     *
     * @param {HTMLInputElement} searchField      The search input element.
     * @param {HTMLElement}      resultsContainer The results container element.
     * @param {HTMLElement}      block            The parent search block.
     */
    function performSearch(searchField, resultsContainer, block) {
        var searchQuery = searchField.value.trim();

        if (!searchQuery) {
            searchField.focus();
            return;
        }

        // Show loading animation.
        showLoadingAnimation(resultsContainer, block);

        var apiUrl = window.antimanual_vars?.rest_url || window.antimanual_chatbot_vars?.rest_url;

        if (!apiUrl) {
            showError(resultsContainer, __('error_api_not_configured', 'API endpoint not configured'), block);
            return;
        }

        var headers = {
            'Content-Type': 'application/x-www-form-urlencoded',
        };

        // Add dedicated public nonce for Search REST endpoint.
        var publicNonce =
            window.antimanual_vars?.public_nonce ||
            window.antimanual_chatbot_vars?.public_nonce ||
            '';
        if (publicNonce) {
            headers['X-Antimanual-Public-Nonce'] = publicNonce;
        }

        // Add standard WordPress REST nonce for authenticated sessions.
        var restNonce =
            window.antimanual_vars?.nonce ||
            window.antimanual_chatbot_vars?.nonce ||
            '';
        if (restNonce) {
            headers['X-WP-Nonce'] = restNonce;
        }

        // Get answer detail level from block data attribute.
        var answerDetail = block.dataset.answerDetail || 'balanced';
        if (['brief', 'balanced', 'detailed'].indexOf(answerDetail) === -1) {
            answerDetail = 'balanced';
        }

        var canStream = (window.antimanual_vars?.streaming_enabled === true || window.antimanual_vars?.streaming_enabled === 1 || window.antimanual_vars?.streaming_enabled === '1')
            && typeof window.fetch === 'function'
            && typeof ReadableStream !== 'undefined';

        if (canStream) {
            performStreamingSearch(searchField, resultsContainer, block, searchQuery, apiUrl, headers, answerDetail);
        } else {
            performSearchBlocking(searchField, resultsContainer, block, searchQuery, apiUrl, headers, answerDetail);
        }
    }

    /**
     * Show loading animation in the results container.
     *
     * @param {HTMLElement} container The results container.
     * @param {HTMLElement} block     The parent search block.
     */
    function showLoadingAnimation(container, block) {
        container.setAttribute('aria-busy', 'true');
        container.innerHTML =
            '<div class="ai-answer">' +
                '<button class="ai-answer-close-btn" type="button" aria-label="' + escapeHtml(__('close', 'Close')) + '">' +
                    '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">' +
                        '<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>' +
                    '</svg>' +
                '</button>' +
                '<div class="ai-answer-loading" role="status" aria-label="' + escapeHtml(__('loading', 'Loading')) + '">' +
                    '<div class="antimanual-loading-dots">' +
                        '<span class="antimanual-dot"></span>' +
                        '<span class="antimanual-dot"></span>' +
                        '<span class="antimanual-dot"></span>' +
                    '</div>' +
                    '<p>' + escapeHtml(__('generating_answer', 'Generating AI answer...')) + '</p>' +
                '</div>' +
            '</div>';

        var closeBtn = container.querySelector('.ai-answer-close-btn');
        if (closeBtn) {
            closeBtn.addEventListener('click', function() {
                closeSearchResults(block);
            });
        }
    }

    /**
     * Display search results in the container.
     *
     * @param {HTMLElement} container The results container.
     * @param {Object}      data      The API response data.
     * @param {string}      query     The original search query.
     * @param {HTMLElement}  block     The parent search block.
     */
    function displaySearchResults(container, data, query, block) {
        container.removeAttribute('aria-busy');

        var cleanTextForCopy = filterHtmlContent(data.answer || data.reply || '');
        var answerHtml = data.answer || data.reply || '';
        var queryLogId = data.query_log_id || null;

        // Sanitize the answer HTML before injection.
        var safeAnswerHtml = sanitizeHtml(answerHtml);

        var uniqueLinks = [];
        if (data.references && Array.isArray(data.references)) {
            data.references.forEach(function(ref) {
                if (ref.link && uniqueLinks.indexOf(ref.link) === -1) {
                    uniqueLinks.push(ref.link);
                }
            });
        }

        // Build references HTML.
        var referencesItems = '';
        uniqueLinks.forEach(function(link) {
            var ref = data.references.find(function(r) { return r.link === link; });
            var safeLink = escapeHtml(link || '#');
            var safeTitle = escapeHtml((ref && ref.title) || __('source', 'Source'));

            // Validate URL protocol.
            if (!/^https?:\/\//i.test(link) && link.charAt(0) !== '/') {
                safeLink = '#';
            }

            referencesItems +=
                '<a href="' + safeLink + '" target="_blank" rel="noopener noreferrer">' +
                    '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 36 36" fill="currentColor" aria-hidden="true">' +
                        '<path d="M17.6,24.32l-2.46,2.44a4,4,0,0,1-5.62,0,3.92,3.92,0,0,1,0-5.55l4.69-4.65a4,4,0,0,1,5.62,0,3.86,3.86,0,0,1,1,1.71A2,2,0,0,0,21.1,18l1.29-1.28a5.89,5.89,0,0,0-1.15-1.62,6,6,0,0,0-8.44,0L8.1,19.79a5.91,5.91,0,0,0,0,8.39,6,6,0,0,0,8.44,0l3.65-3.62c-.17,0-.33,0-.5,0A8,8,0,0,1,17.6,24.32Z"></path>' +
                        '<path d="M28.61,7.82a6,6,0,0,0-8.44,0l-3.65,3.62c.17,0,.33,0,.49,0h0a8,8,0,0,1,2.1.28l2.46-2.44a4,4,0,0,1,5.62,0,3.92,3.92,0,0,1,0,5.55l-4.69,4.65a4,4,0,0,1-5.62,0,3.86,3.86,0,0,1-1-1.71,2,2,0,0,0-.28.23l-1.29,1.28a5.89,5.89,0,0,0,1.15,1.62,6,6,0,0,0,8.44,0l4.69-4.65a5.92,5.92,0,0,0,0-8.39Z"></path>' +
                    '</svg>' +
                    '<span class="text">' + safeTitle + '</span>' +
                '</a>';
        });

        var escapedQuery = escapeHtml(query);
        var sourcesLabel = __('sources', 'Sources');
        var aiAnswerLabel = __('ai_answer', 'AI Answer');
        var helpfulLabel = __('helpful', 'Helpful');
        var notHelpfulLabel = __('not_helpful', 'Not helpful');
        var copyLabel = __('copy', 'Copy');
        var closeLabel = __('close', 'Close');
        var noAnswerText = escapeHtml(__('no_answer', 'No answer available'));

        var sourcesInnerHtml = '';
        if (uniqueLinks.length > 0) {
            sourcesInnerHtml =
                '<svg fill="currentColor" width="16px" class="antimanual-icon ea-arrow-down" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" aria-hidden="true">' +
                    '<path d="M233.4 406.6c12.5 12.5 32.8 12.5 45.3 0l192-192c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L256 338.7 86.6 169.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l192 192z"></path>' +
                '</svg>' +
                '<span>' + escapeHtml(sourcesLabel) + '</span>' +
                '<span class="sources-count">' + uniqueLinks.length + '</span>';
        }

        var sourcesToggleHtml = uniqueLinks.length > 0
            ? '<button class="sources" type="button" aria-expanded="false" aria-controls="refs-' + escapeHtml(block.id) + '">' +
                sourcesInnerHtml +
            '</button>'
            : '';

        var referencesHtml = uniqueLinks.length > 0
            ? '<div class="references" id="refs-' + escapeHtml(block.id) + '" style="display: none;">' +
                referencesItems +
            '</div>'
            : '';

        container.innerHTML =
            '<div class="ai-answer" role="region" aria-label="' + escapeHtml(aiAnswerLabel) + '">' +
                '<button class="ai-answer-close-btn" type="button" aria-label="' + escapeHtml(closeLabel) + '">' +
                    '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">' +
                        '<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>' +
                    '</svg>' +
                '</button>' +
                '<div class="ai-answer-header">' +
                    '<svg class="ai-icon" xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 32 32" aria-hidden="true">' +
                        '<g>' +
                            '<path d="m13.294 7.436.803 2.23a8.835 8.835 0 0 0 5.316 5.316l2.23.803a.229.229 0 0 1 0 .43l-2.23.803a8.835 8.835 0 0 0-5.316 5.316l-.803 2.23a.229.229 0 0 1-.43 0l-.803-2.23a8.835 8.835 0 0 0-5.316-5.316l-2.23-.803a.229.229 0 0 1 0-.43l2.23-.803a8.835 8.835 0 0 0 5.316-5.316l.803-2.23a.228.228 0 0 1 .43 0z" fill="currentColor"/>' +
                            '<path d="M23.332 2.077l.407 1.129a4.477 4.477 0 0 0 2.692 2.692l1.129.407a.116.116 0 0 1 0 .218l-1.129.407a4.477 4.477 0 0 0-2.692 2.692l-.407 1.129a.116.116 0 0 1-.218 0l-.407-1.129a4.477 4.477 0 0 0-2.692-2.692l-1.129-.407a.116.116 0 0 1 0-.218l1.129-.407a4.477 4.477 0 0 0 2.692-2.692l.407-1.129a.116.116 0 0 1 .218 0z" fill="currentColor"/>' +
                            '<path d="M23.332 21.25l.407 1.129a4.477 4.477 0 0 0 2.692 2.692l1.129.407a.116.116 0 0 1 0 .218l-1.129.407a4.477 4.477 0 0 0-2.692 2.692l-.407 1.129a.116.116 0 0 1-.218 0l-.407-1.129a4.477 4.477 0 0 0-2.692-2.692l-1.129-.407a.116.116 0 0 1 0-.218l1.129-.407a4.477 4.477 0 0 0 2.692-2.692l.407-1.129c.037-.102.182-.102.218 0z" fill="currentColor"/>' +
                        '</g>' +
                    '</svg>' +
                    '<h4>' + escapeHtml(aiAnswerLabel) + '</h4>' +
                '</div>' +
                '<div class="answer-content">' +
                    (safeAnswerHtml || noAnswerText) +
                '</div>' +
                '<div class="sources-container">' +
                    '<div class="sources-head-wrapper">' +
                        sourcesToggleHtml +
                        '<div class="answer-actions">' +
                            '<button class="action-button helpful-btn" type="button" aria-label="' + escapeHtml(helpfulLabel) + '" aria-pressed="false" data-vote-type="yes" data-query="' + escapedQuery + '">' +
                                '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">' +
                                    '<path d="M1.75 23h2.5C5.215 23 6 22.215 6 21.25V9.75C6 8.785 5.215 8 4.25 8h-2.5C.785 8 0 8.785 0 9.75v11.5C0 22.215.785 23 1.75 23zM12.781.75c-1 0-1.5.5-1.5 3 0 2.376-2.301 4.288-3.781 5.273v12.388c1.601.741 4.806 1.839 9.781 1.839h1.6c1.95 0 3.61-1.4 3.94-3.32l1.12-6.5a3.998 3.998 0 0 0-3.94-4.68h-4.72s.75-1.5.75-4c0-3-2.25-4-3.25-4z"/>' +
                                '</svg>' +
                            '</button>' +
                            '<button class="action-button not-helpful-btn" type="button" aria-label="' + escapeHtml(notHelpfulLabel) + '" aria-pressed="false" data-vote-type="no" data-query="' + escapedQuery + '">' +
                                '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">' +
                                    '<path d="M22.25 1h-2.5C18.785 1 18 1.785 18 2.75v11.5c0 .965.785 1.75 1.75 1.75h2.5c.965 0 1.75-.785 1.75-1.75V2.75C24 1.785 23.215 1 22.25 1zM5.119.75c-1.95 0-3.61 1.4-3.94 3.32l-1.12 6.5a3.998 3.998 0 0 0 3.94 4.68h4.72s-.75 1.5-.75 4c0 3 2.25 4 3.25 4s1.5-.5 1.5-3c0-2.376 2.301-4.288 3.781-5.273V2.589C14.899 1.848 11.694.75 6.719.75z"/>' +
                                '</svg>' +
                            '</button>' +
                            '<button class="action-button copy-btn" type="button" aria-label="' + escapeHtml(copyLabel) + '">' +
                                '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true"><g><path d="M18.548 2H9.452A3.456 3.456 0 0 0 6 5.452V6h-.548A3.456 3.456 0 0 0 2 9.452v9.096A3.456 3.456 0 0 0 5.452 22h9.096c1.748 0 3.182-1.312 3.406-3h.594A3.456 3.456 0 0 0 22 15.548V5.452A3.456 3.456 0 0 0 18.548 2zM20 15.548c0 .8-.651 1.452-1.452 1.452H18V9.452A3.456 3.456 0 0 0 14.548 6H8v-.548C8 4.652 8.651 4 9.452 4h9.096c.8 0 1.452.651 1.452 1.452z"></path></g></svg>' +
                            '</button>' +
                        '</div>' +
                    '</div>' +
                    referencesHtml +
                '</div>' +
            '</div>';

        // Bind close button.
        var closeBtn = container.querySelector('.ai-answer-close-btn');
        if (closeBtn) {
            closeBtn.addEventListener('click', function() {
                closeSearchResults(block);
            });
        }

        // Bind sources toggle with ARIA.
        var sourcesButton = container.querySelector('.sources');
        if (sourcesButton) {
            sourcesButton.addEventListener('click', function() {
                var referencesEl = container.querySelector('.references');
                if (referencesEl) {
                    var isVisible = referencesEl.style.display !== 'none';
                    referencesEl.style.display = isVisible ? 'none' : 'flex';
                    this.classList.toggle('expanded', !isVisible);
                    this.setAttribute('aria-expanded', String(!isVisible));
                }
            });
        }

        // Bind copy button — store text in closure, not in attribute.
        var copyButton = container.querySelector('.copy-btn');
        if (copyButton) {
            copyButton.addEventListener('click', function() {
                copyToClipboard(cleanTextForCopy);
            });
        }

        // Bind voting buttons — scoped to this block instance.
        var voteButtons = container.querySelectorAll('.action-button[data-vote-type]');
        voteButtons.forEach(function(button) {
            var voteKey = getVoteStorageKey(queryLogId, query);
            var votedType = localStorage.getItem(voteKey);

            if (!queryLogId) {
                button.disabled = true;
                button.classList.add('is-voted-disabled');
                button.setAttribute('aria-label', __('feedback_unavailable', 'Feedback is unavailable for this answer.'));
            } else if (votedType) {
                button.disabled = true;
                if (button.getAttribute('data-vote-type') === votedType) {
                    button.classList.add('voted');
                    button.setAttribute('aria-pressed', 'true');
                } else {
                    button.classList.remove('voted');
                }
                button.setAttribute('aria-label', __('already_voted', 'You have already voted'));
            } else {
                button.disabled = false;
                button.classList.remove('voted');
            }

            button.addEventListener('click', function() {
                handleFeedback(
                    this.getAttribute('data-vote-type'),
                    this.getAttribute('data-query'),
                    this,
                    queryLogId,
                    block
                );
            });
        });
    }

    /**
     * Show error message in the results container.
     *
     * @param {HTMLElement} container The container to show the error in.
     * @param {string}      message   The error message.
     * @param {HTMLElement}  block     The parent search block.
     */
    function showError(container, message, block) {
        container.removeAttribute('aria-busy');

        var safeMessage = escapeHtml(message);
        var closeLabel = __('close', 'Close');
        var errorLabel = __('error', 'Error');
        var errorPrefixText = escapeHtml(__('error_prefix', "Sorry, I couldn't process your request: "));
        var errorSuffixText = escapeHtml(__('error_suffix', 'Please try again or contact support if the issue persists.'));

        container.innerHTML =
            '<div class="ai-answer" role="alert">' +
                '<button class="ai-answer-close-btn" type="button" aria-label="' + escapeHtml(closeLabel) + '">' +
                    '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">' +
                        '<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>' +
                    '</svg>' +
                '</button>' +
                '<div class="ai-answer-header">' +
                    '<svg class="ai-icon" fill="#ef4444" width="28" height="28" viewBox="0 0 24 24" aria-hidden="true">' +
                        '<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>' +
                    '</svg>' +
                    '<h4>' + escapeHtml(errorLabel) + '</h4>' +
                '</div>' +
                '<div class="answer-content">' +
                    '<p>' + errorPrefixText + safeMessage + '</p>' +
                    '<p>' + errorSuffixText + '</p>' +
                '</div>' +
            '</div>';

        var closeBtn = container.querySelector('.ai-answer-close-btn');
        if (closeBtn && block) {
            closeBtn.addEventListener('click', function() {
                closeSearchResults(block);
            });
        }
    }

    /**
     * Handle feedback voting system.
     *
     * @param {string}           voteType      'yes' or 'no'.
     * @param {string}           query         The search query that was voted on.
     * @param {HTMLButtonElement} clickedButton The button that was clicked.
     * @param {number|null}      queryLogId    The database record ID for this query.
     * @param {HTMLElement}      block         The parent search block (for scoping).
     */
    function handleFeedback(voteType, query, clickedButton, queryLogId, block) {
        var voteKey = getVoteStorageKey(queryLogId, query);

        if (!queryLogId) {
            showTemporaryMessage(__('feedback_unavailable', 'Feedback is unavailable for this answer.'), 'info');
            return;
        }

        if (localStorage.getItem(voteKey)) {
            showTemporaryMessage(__('already_voted_message', 'You have already voted for this answer.'), 'info');
            disableVotingButtons(voteType, block);
            return;
        }

        var formData = new FormData();
        formData.append('action', 'antimanual_vote');
        formData.append('vote_type', voteType);
        formData.append('query_log_id', queryLogId);
        formData.append('query', query);

        if (window.antimanual_vars?.nonce) {
            formData.append('nonce', window.antimanual_vars.nonce);
        }

        if (window.antimanual_vars?.public_nonce) {
            formData.append('public_nonce', window.antimanual_vars.public_nonce);
        }

        var ajaxUrl = window.antimanual_vars?.ajax_url ||
                      window.antimanual_chatbot_vars?.ajax_url ||
                      window.ajaxurl ||
                      '';

        if (!ajaxUrl) {
            showTemporaryMessage(__('error_ajax_not_configured', 'AJAX endpoint not configured'), 'error');
            if (clickedButton) {
                clickedButton.disabled = false;
                clickedButton.classList.remove('is-loading');
            }
            return;
        }

        // Show loading state.
        if (clickedButton) {
            clickedButton.disabled = true;
            clickedButton.classList.add('is-loading');
        }

        fetch(ajaxUrl, {
            method: 'POST',
            credentials: 'same-origin',
            body: formData
        })
        .then(function(response) {
            return response.json().then(function(data) {
                return {
                    ok: response.ok,
                    status: response.status,
                    payload: data
                };
            });
        })
        .then(function(result) {
            var data = result.payload || {};

            if (data.success) {
                localStorage.setItem(voteKey, voteType);
                showTemporaryMessage(__('feedback_thanks', 'Thank you for your feedback!'), 'success');
                disableVotingButtons(voteType, block);
                if (clickedButton) {
                    clickedButton.classList.add('voted');
                    clickedButton.setAttribute('aria-pressed', 'true');
                }
            } else {
                if (result.status === 409) {
                    localStorage.setItem(voteKey, 'recorded');
                    disableVotingButtons(null, block);
                }

                showTemporaryMessage(data.data?.message || __('error_vote_failed', 'Failed to record vote'), result.status === 409 ? 'info' : 'error');
            }
        })
        .catch(function(error) {
            console.error('Voting error:', error);
            showTemporaryMessage(__('error_network', 'Network error occurred'), 'error');
        })
        .finally(function() {
            if (clickedButton) {
                clickedButton.disabled = false;
                clickedButton.classList.remove('is-loading');
            }
        });
    }

    /**
     * Disable voting buttons scoped to a specific block to prevent duplicate votes.
     *
     * @param {string}      votedType The vote type that was selected ('yes'/'no').
     * @param {HTMLElement}  block     The parent search block.
     */
    function disableVotingButtons(votedType, block) {
        var scope = block || document;
        var voteButtons = scope.querySelectorAll('.action-button[data-vote-type]');

        voteButtons.forEach(function(button) {
            button.disabled = true;
            button.classList.add('is-voted-disabled');
            button.setAttribute('aria-label', __('already_voted', 'You have already voted'));
            if (votedType && button.getAttribute('data-vote-type') === votedType) {
                button.classList.add('voted');
                button.setAttribute('aria-pressed', 'true');
            } else {
                button.classList.remove('voted');
            }
        });
    }

    /**
     * Show temporary toast message to user.
     *
     * @param {string} message Message to show.
     * @param {string} type    Message type: 'success', 'error', 'info'.
     */
    function showTemporaryMessage(message, type) {
        type = type || 'info';

        var existingMessage = document.querySelector('.antimanual-temp-message');
        if (existingMessage) {
            existingMessage.remove();
        }

        var messageEl = document.createElement('div');
        messageEl.className = 'antimanual-temp-message antimanual-temp-message--' + type;
        messageEl.setAttribute('role', 'status');
        messageEl.setAttribute('aria-live', 'polite');
        messageEl.textContent = message;
        document.body.appendChild(messageEl);

        // Trigger animation.
        setTimeout(function() {
            messageEl.classList.add('is-visible');
        }, 10);

        // Auto remove after 3 seconds.
        setTimeout(function() {
            messageEl.classList.remove('is-visible');
            setTimeout(function() {
                if (messageEl.parentNode) {
                    messageEl.parentNode.removeChild(messageEl);
                }
            }, 300);
        }, 3000);
    }

    /**
     * Copy text to clipboard with fallback.
     *
     * @param {string} text The text to copy.
     */
    function copyToClipboard(text) {
        if (navigator.clipboard && navigator.clipboard.writeText) {
            navigator.clipboard.writeText(text).then(function() {
                showTemporaryMessage(__('copied', 'Copied to clipboard!'), 'success');
            }).catch(function() {
                fallbackCopyToClipboard(text);
            });
        } else {
            fallbackCopyToClipboard(text);
        }
    }

    /**
     * Fallback clipboard copy using textarea.
     *
     * @param {string} text The text to copy.
     */
    function fallbackCopyToClipboard(text) {
        var textArea = document.createElement('textarea');
        textArea.value = text;
        textArea.style.position = 'fixed';
        textArea.style.left = '-999999px';
        textArea.style.top = '-999999px';
        document.body.appendChild(textArea);
        textArea.focus();
        textArea.select();

        try {
            document.execCommand('copy');
            showTemporaryMessage(__('copied', 'Copied to clipboard!'), 'success');
        } catch (err) {
            showTemporaryMessage(__('copy_failed', 'Failed to copy text'), 'error');
        }

        document.body.removeChild(textArea);
    }

})();
