/**
 * UI utility functions
 */

// Configuration constants
const UI_CONFIG = {
    NOTIFICATION_DURATION_MS: 6000,
    NOTIFICATION_Z_INDEX: 9999999,
    ALERT_DURATION_MS: 5000
};

// Note: sanitizer functions are available via Vite bundle or we use safe fallbacks

/**
 * Safe HTML setter - uses DOMPurify if available, otherwise sets innerHTML directly
 * for controlled/trusted content only
 */
function safeSetHTML(element, html) {
    if (!element) return;
    // If DOMPurify is available globally (from Vite bundle), use it
    if (typeof DOMPurify !== 'undefined') {
        element.innerHTML = DOMPurify.sanitize(html);
    } else {
        // For controlled content (UI elements we generate), direct assignment is acceptable
        // bearer:disable javascript_lang_dangerous_insert_html
        // eslint-disable-next-line no-unsanitized/property -- audited 2026-03-28: DOMPurify fallback path — callers use safeSetHTML which documents sanitization requirement
        element.innerHTML = html;
    }
}

/**
 * Update a progress bar UI element
 * @param {string|Element} fillElementId - The ID or element to fill
 * @param {string|Element} percentageElementId - The ID or element to show percentage
 * @param {number} percentage - The percentage to set
 */
function updateProgressBar(fillElementId, percentageElementId, percentage) {
    const progressFill = typeof fillElementId === 'string' ? document.getElementById(fillElementId) : fillElementId;
    const progressPercentage = typeof percentageElementId === 'string' ? document.getElementById(percentageElementId) : percentageElementId;

    if (progressFill && progressPercentage) {
        // Convert any value to a percentage between 0-100
        const safePercentage = Math.min(100, Math.max(0, percentage || 0));

        // Update the width of the fill element
        progressFill.style.width = `${safePercentage}%`;

        // Update the percentage text
        progressPercentage.textContent = `${Math.round(safePercentage)}%`;

        // Add classes for visual feedback
        if (safePercentage >= 100) {
            progressFill.classList.add('ldr-complete');
        } else {
            progressFill.classList.remove('ldr-complete');
        }
    }
}

/**
 * GLOBAL fallback for HTML escaping - provides XSS protection even if
 * xss-protection.js fails to load. Loaded via base.html on ALL pages.
 *
 * WARNING: DO NOT redeclare this variable in other top-level scripts.
 * Declaring `const`, `let`, or `var` escapeHtmlFallback at top-level
 * in another <script> tag will cause: SyntaxError: "Identifier
 * 'escapeHtmlFallback' has already been declared" — crashing that
 * script entirely.
 *
 * If you need escapeHtmlFallback in another file, either:
 *   1. Reference this global directly (it's available everywhere)
 *   2. Wrap your code in an IIFE and declare a local copy inside it
 */
// bearer:disable javascript_lang_manual_html_sanitization
const escapeHtmlFallback = (str) => String(str).replace(/[&<>"']/g, (m) => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'})[m]);

/**
 * Show a loading spinner
 * @param {string|Element} container - The container ID or element to add the spinner to
 * @param {string} message - Optional message to show with the spinner
 */
function showSpinner(container, message = 'Loading...') {
    const containerEl = typeof container === 'string' ? document.getElementById(container) : container;

    if (containerEl) {
        containerEl.innerHTML = '';

        // Escape message before including in HTML template
        const escapedMessage = message ? (window.escapeHtml || escapeHtmlFallback)(message) : '';
        const spinnerHTML = `
            <div class="ldr-loading-spinner ldr-centered">
                <div class="ldr-spinner"></div>
                ${escapedMessage ? `<div class="ldr-spinner-message">${escapedMessage}</div>` : ''}
            </div>
        `;

        // Safe: spinner HTML is controlled/static, message is escaped above
        safeSetHTML(containerEl, spinnerHTML, 'ui');
    }
}

/**
 * Hide a loading spinner
 * @param {string|Element} container - The container ID or element with the spinner
 */
function hideSpinner(container) {
    const containerEl = typeof container === 'string' ? document.getElementById(container) : container;

    if (containerEl) {
        const spinner = containerEl.querySelector('.ldr-loading-spinner');
        if (spinner) {
            spinner.remove();
        }
    }
}

/**
 * Show an error message
 * @param {string|Element} container - The container ID or element to add the error to
 * @param {string} message - The error message
 */
function showError(container, message) {
    const containerEl = typeof container === 'string' ? document.getElementById(container) : container;

    if (containerEl) {
        // Escape message before including in HTML template
        const escapedMessage = (window.escapeHtml || escapeHtmlFallback)(message);
        const errorHTML = `
            <div class="ldr-error-message">
                <i class="fas fa-exclamation-circle"></i>
                <span>${escapedMessage}</span>
            </div>
        `;

        // Safe: error HTML is controlled/static, message is escaped above
        safeSetHTML(containerEl, errorHTML, 'ui');
    }
}

/**
 * Get CSS variable value from document root
 * @param {string} varName - CSS variable name (e.g., '--success-color')
 * @param {string} fallback - Fallback value if variable not found
 * @returns {string} The CSS variable value
 */
function getCSSVariable(varName, fallback) {
    const value = getComputedStyle(document.documentElement).getPropertyValue(varName).trim();
    return value || fallback;
}

/**
 * Show a notification message
 * @param {string} message - The message to display
 * @param {string} type - Message type: 'success', 'error', 'info', 'warning'
 * @param {number} duration - How long to show the message in ms
 */
/**
 * Get or lazily create the two persistent notification banners.
 *
 * Live regions need to exist in the DOM before their text content
 * changes, otherwise NVDA/VoiceOver will not reliably announce the
 * message. The previous implementation removed and re-created the
 * banner on every `showMessage` call, which made announcements
 * inconsistent across browsers and screen readers.
 *
 * Two regions are used so that errors/warnings get assertive
 * announcement (interrupts current speech) while success/info get
 * polite (waits for the user to be idle).
 */
function _ensureNotificationBanners() {
    let polite = document.getElementById('notification-banner-polite');
    let assertive = document.getElementById(
        'notification-banner-assertive',
    );

    const buildBanner = (id, role, liveness) => {
        const el = document.createElement('div');
        el.id = id;
        el.className = 'ldr-notification-banner';
        el.setAttribute('role', role);
        el.setAttribute('aria-live', liveness);
        el.setAttribute('aria-atomic', 'true');
        // Stable internal structure: <i class="fas …"></i><span></span>
        // The span's textContent is what gets announced; mutating it
        // (rather than the element itself) is what makes the live
        // region work reliably.
        el.appendChild(document.createElement('i'));
        el.appendChild(document.createElement('span'));
        document.body.appendChild(el);
        return el;
    };

    if (!polite) {
        polite = buildBanner('notification-banner-polite', 'status', 'polite');
    }
    if (!assertive) {
        assertive = buildBanner(
            'notification-banner-assertive',
            'alert',
            'assertive',
        );
    }
    return { polite, assertive };
}

function showMessage(message, type = 'success', duration = UI_CONFIG.NOTIFICATION_DURATION_MS) {
    // Get theme colors from CSS variables with fallbacks
    let accentColor, iconClass;
    switch (type) {
        case 'success':
            accentColor = getCSSVariable('--success-color', '#0acf97');
            iconClass = 'fa-check-circle';
            break;
        case 'error':
            accentColor = getCSSVariable('--error-color', '#fa5c7c');
            iconClass = 'fa-exclamation-circle';
            break;
        case 'warning':
            accentColor = getCSSVariable('--warning-color', '#f9bc0b');
            iconClass = 'fa-exclamation-triangle';
            break;
        case 'info':
        default:
            accentColor = getCSSVariable('--accent-primary', '#6e4ff6');
            iconClass = 'fa-info-circle';
            break;
    }

    const { polite, assertive } = _ensureNotificationBanners();
    const banner =
        type === 'error' || type === 'warning' ? assertive : polite;
    const otherBanner = banner === polite ? assertive : polite;

    // Hide the other banner so it doesn't overlap visually.
    otherBanner.style.transform = 'translateY(-100%)';

    // Apply dynamic accent color for border (static styles handled by CSS class)
    banner.style.borderBottom = `3px solid ${accentColor}`;

    // Mutate the existing live region's children rather than rebuilding
    // the host element. Updating textContent on the inner span is what
    // triggers the screen reader announcement.
    const icon = banner.firstElementChild;
    const textSpan = banner.lastElementChild;
    icon.className = `fas ${iconClass}`;
    icon.style.color = accentColor;
    icon.style.fontSize = '1.1rem';
    textSpan.textContent = message;

    // Slide in
    requestAnimationFrame(() => {
        banner.style.transform = 'translateY(0)';
    });

    // Slide out after duration; clear any previous timer so a rapid
    // second call doesn't slide this one out early.
    clearTimeout(banner._hideTimer);
    banner._hideTimer = setTimeout(() => {
        banner.style.transform = 'translateY(-100%)';
    }, duration);
}

/**
 * Format and render Markdown content
 * @param {string} markdown - The markdown content
 * @returns {string} The rendered HTML
 */
function renderMarkdown(markdown) {
    if (!markdown) {
        return '<div class="alert alert-warning">No content available</div>';
    }

    try {
        // Use marked library if available
        if (typeof marked !== 'undefined') {
            // Configure marked options and custom renderer for links and special elements
            const renderer = new marked.Renderer();
            const _origLink = renderer.link.bind(renderer);
            renderer.link = function(token) {
                return _origLink(token).replace(/^<a /, '<a target="_blank" rel="noopener noreferrer" ');
            };
            marked.setOptions({
                breaks: true,
                gfm: true,
                headerIds: true,
                smartLists: true,
                smartypants: true,
                highlight(code, language) {
                    // Use Prism for syntax highlighting if available
                    if (typeof Prism !== 'undefined' && Prism.languages[language]) {
                        return Prism.highlight(code, Prism.languages[language], language);
                    }
                    return code;
                }
            });

            // Parse markdown and return HTML
            const html = marked.parse(markdown, { renderer });

            // Process any special elements like image references
            const processedHtml = processSpecialMarkdown(html);

            // Sanitize to prevent XSS from markdown content
            const sanitized = typeof DOMPurify !== 'undefined'
                ? DOMPurify.sanitize(processedHtml, {
                    ADD_TAGS: ['semantics', 'annotation'],
                    ADD_ATTR: ['target', 'rel']
                  })
                : processedHtml;

            return `<div class="ldr-markdown-content">${sanitized}</div>`;
        }
        // Fallback if marked is not available - display as plaintext for security
        // Using regex-based partial markdown is fragile and a security risk,
        // so we escape all HTML and display as preformatted text with a warning
        SafeLogger.warn('Marked library not available. Displaying as plaintext for security.');
        const escaped = typeof window.escapeHtml === 'function'
            ? window.escapeHtml(markdown)
            // bearer:disable javascript_lang_manual_html_sanitization
            : markdown.replace(/[&<>"']/g, (m) => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'})[m]);

        return `<div class="markdown-content">
            <div class="alert alert-warning" style="margin-bottom: 1rem;">
                <i class="fas fa-exclamation-triangle"></i> Markdown rendering unavailable. Displaying as plaintext.
            </div>
            <pre style="white-space: pre-wrap; word-wrap: break-word; font-family: inherit;">${escaped}</pre>
        </div>`;
    } catch (error) {
        SafeLogger.error('Error rendering markdown:', error);
        const escapedMessage = typeof window.escapeHtml === 'function'
            ? window.escapeHtml(error.message)
            // bearer:disable javascript_lang_manual_html_sanitization
            : String(error.message).replace(/[&<>"']/g, (m) => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'})[m]);
        return `<div class="alert alert-danger">Error rendering content: ${escapedMessage}</div>`;
    }
}

/**
 * Process special markdown elements
 * @param {string} html - HTML content to process
 * @returns {string} - Processed HTML
 */
function processSpecialMarkdown(html) {
    // Process image references. `ref` is user-influenced content from the
    // markdown body; tighten the allowed shape so HTML-attribute injection
    // (a `"` would break out of the data-image-id attribute and confuse
    // DOMPurify's parser) and src-path traversal (`../`) are impossible.
    const SAFE_IMAGE_REF = /^image-[\w-]+$/;
    return html.replace(/!\[ref:([^\]]+)\]/g, (match, ref) => {
        if (!SAFE_IMAGE_REF.test(ref)) {
            return match;  // Leave unmatched refs as literal markdown.
        }
        return `<div class="ldr-generated-image" data-image-id="${ref}">
            <img src="/static/img/generated/${ref}.png"
                 alt="Generated image ${ref}"
                 class="img-fluid"
                 loading="lazy" />
            <div class="ldr-image-caption">Generated image (${ref})</div>
        </div>`;
    });
}

/**
 * Create a dynamic favicon
 * @param {string} emoji - The emoji to use for the favicon
 */
function createDynamicFavicon(emoji = '⚡') {
    // Create a canvas element
    const canvas = document.createElement('canvas');
    canvas.width = 64;
    canvas.height = 64;

    // Get the 2D drawing context
    const ctx = canvas.getContext('2d');

    // Clear the canvas
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    // Set font
    ctx.font = '48px Arial';
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';

    // Draw the emoji
    ctx.fillText(emoji, 32, 32);

    // Convert to data URL
    const dataUrl = canvas.toDataURL('image/png');

    // Create or update the favicon link
    let link = document.querySelector('link[rel="icon"]');
    if (!link) {
        link = document.createElement('link');
        link.rel = 'icon';
        document.head.appendChild(link);
    }

    // Set the new favicon
    // Data URLs from canvas are safe for favicon use
    if (typeof URLValidator !== 'undefined' && URLValidator.safeAssign) {
        URLValidator.safeAssign(link, 'href', dataUrl);
    } else {
        link.href = dataUrl;
    }
}

/**
 * Update favicon based on status
 * @param {string} status - The research status
 */
function updateFavicon(status) {
    try {
        // Find favicon link or create it if it doesn't exist
        let link = document.querySelector("link[rel='icon']") ||
                document.querySelector("link[rel='shortcut icon']");

        if (!link) {
            SafeLogger.log('Favicon link not found, creating a new one');
            link = document.createElement('link');
            link.rel = 'icon';
            link.type = 'image/x-icon';
            document.head.appendChild(link);
        }

        // Create dynamic favicon using canvas
        const canvas = document.createElement('canvas');
        canvas.width = 32;
        canvas.height = 32;
        const ctx = canvas.getContext('2d');

        // Get theme colors from CSS variables
        const style = getComputedStyle(document.documentElement);
        const successColor = style.getPropertyValue('--success-color').trim() || '#28a745';
        const errorColor = style.getPropertyValue('--error-color').trim() || '#dc3545';
        const mutedColor = style.getPropertyValue('--text-muted').trim() || '#6c757d';
        const accentColor = style.getPropertyValue('--accent-primary').trim() || '#007bff';
        const bgPrimary = style.getPropertyValue('--bg-primary').trim() || '#ffffff';

        // Background color based on status
        let bgColor = accentColor; // Default accent

        if (ResearchStates.isCompleted(status)) {
            bgColor = successColor;
        } else if (ResearchStates.isFailed(status)) {
            bgColor = errorColor;
        } else if (ResearchStates.isCancelled(status)) {
            bgColor = mutedColor;
        }

        // Draw circle background
        ctx.fillStyle = bgColor;
        ctx.beginPath();
        ctx.arc(16, 16, 16, 0, 2 * Math.PI);
        ctx.fill();

        // Draw inner circle
        ctx.fillStyle = bgPrimary;
        ctx.beginPath();
        ctx.arc(16, 16, 10, 0, 2 * Math.PI);
        ctx.fill();

        // Draw letter R
        ctx.fillStyle = bgColor;
        ctx.font = 'bold 16px Arial';
        ctx.textAlign = 'center';
        ctx.textBaseline = 'middle';
        ctx.fillText('R', 16, 16);

        // Set the favicon to the canvas data URL
        link.href = canvas.toDataURL('image/png');

        SafeLogger.log('Updated favicon to:', status);
    } catch (error) {
        SafeLogger.error('Error updating favicon:', error);
    }
}

/**
 * Show an alert message in a container on the page
 * @param {string} message - The message to display
 * @param {string} type - The alert type: success, error, warning, info
 * @param {boolean} skipIfToastShown - Whether to skip showing this alert if a toast was already shown
 */
function showAlert(message, type = 'info', skipIfToastShown = true) {
    // If we're showing a toast and we want to skip the regular alert, just return
    if (skipIfToastShown && window.ui && window.ui.showMessage) {
        return;
    }

    // Find the alert container - look for different possible alert containers
    let alertContainer = document.getElementById('filtered-settings-alert');

    // If not found, try other common alert containers
    if (!alertContainer) {
        alertContainer = document.getElementById('settings-alert');
    }

    if (!alertContainer) {
        alertContainer = document.getElementById('research-alert');
    }

    if (!alertContainer) return;

    // Clear any existing alerts
    alertContainer.innerHTML = '';

    // Escape message before including in HTML template
    const escapedMessage = (window.escapeHtml || escapeHtmlFallback)(message);

    // Create alert element
    const alert = document.createElement('div');
    const alertType = window.LdrAlertHelpers.mapAlertType(type);
    alert.className = `alert alert-${alertType}`;
    safeSetHTML(alert, `<i class="fas ${type === 'success' ? 'fa-check-circle' : 'fa-exclamation-circle'}"></i> ${escapedMessage}`, 'ui');

    // Add a close button
    const closeBtn = document.createElement('button');
    closeBtn.className = 'ldr-alert-close';
    closeBtn.type = 'button';
    closeBtn.setAttribute('aria-label', 'Dismiss alert');
    closeBtn.textContent = '×';
    closeBtn.addEventListener('click', () => {
        alert.remove();
        alertContainer.style.display = 'none';
    });

    alert.appendChild(closeBtn);

    // Add to container
    alertContainer.appendChild(alert);
    alertContainer.style.display = 'block';

    // Auto-hide after configured duration
    setTimeout(() => {
        alert.remove();
        if (alertContainer.children.length === 0) {
            alertContainer.style.display = 'none';
        }
    }, UI_CONFIG.ALERT_DURATION_MS);
}

/**
 * Show an inline error message inside a container element
 * Uses DOM API exclusively (no innerHTML) for XSS safety.
 * @param {string|Element} container - The container ID or element
 * @param {string} message - The error message (used via textContent, inherently safe)
 * @param {Object} options - Options { dismissible: true }
 * @returns {HTMLElement|null} The created error element, or null if container not found
 */
function showInlineError(container, message, options = {}) {
    const containerEl = typeof container === 'string' ? document.getElementById(container) : container;
    if (!containerEl) {
        SafeLogger.warn('[ui.showInlineError] Container not found:', container);
        return null;
    }

    const { dismissible = true } = options || {};

    // Remove any existing inline error in this container
    clearInlineError(containerEl);

    // Build error element entirely via DOM API
    const errorEl = document.createElement('div');
    errorEl.className = 'ldr-inline-error';
    errorEl.setAttribute('role', 'alert');

    const icon = document.createElement('i');
    icon.className = 'fas fa-exclamation-circle';
    errorEl.appendChild(icon);

    const span = document.createElement('span');
    span.textContent = message;
    errorEl.appendChild(span);

    if (dismissible) {
        const closeBtn = document.createElement('button');
        closeBtn.type = 'button';
        closeBtn.className = 'ldr-inline-error-close';
        closeBtn.setAttribute('aria-label', 'Dismiss error');
        closeBtn.textContent = '\u00d7';
        closeBtn.addEventListener('click', () => {
            errorEl.remove();
        });
        errorEl.appendChild(closeBtn);
    }

    containerEl.appendChild(errorEl);
    return errorEl;
}

/**
 * Clear inline errors from a container
 * @param {string|Element} container - The container ID or element
 */
function clearInlineError(container) {
    const containerEl = typeof container === 'string' ? document.getElementById(container) : container;
    if (containerEl) {
        const errors = containerEl.querySelectorAll('.ldr-inline-error');
        errors.forEach(el => el.remove());
    }
}

// Add CSS for alert and notification styles
function addAlertStyles() {
    if (document.getElementById('alert-styles')) return;

    const styleEl = document.createElement('style');
    styleEl.id = 'alert-styles';
    styleEl.textContent = `
        .alert {
            padding: 12px 16px;
            margin-bottom: 1rem;
            border-radius: 8px;
            display: flex;
            align-items: center;
            position: relative;
        }

        .alert i {
            margin-right: 12px;
            font-size: 1.2rem;
        }

        .alert-success {
            background-color: rgba(var(--success-color-rgb), 0.15);
            color: var(--success-color);
            border-left: 4px solid var(--success-color);
        }

        .alert-danger {
            background-color: rgba(var(--error-color-rgb), 0.15);
            color: var(--error-color);
            border-left: 4px solid var(--error-color);
        }

        .alert-info {
            background-color: rgba(var(--accent-tertiary-rgb), 0.15);
            color: var(--accent-tertiary);
            border-left: 4px solid var(--accent-tertiary);
        }

        .alert-warning {
            background-color: rgba(var(--warning-color-rgb), 0.15);
            color: var(--warning-color);
            border-left: 4px solid var(--warning-color);
        }

        .alert-close {
            position: absolute;
            right: 10px;
            top: 8px;
            font-size: 1.2rem;
            font-weight: bold;
            cursor: pointer;
            opacity: 0.7;
        }

        .alert-close:hover {
            opacity: 1;
        }

        /* Notification banner styles */
        .ldr-notification-banner {
            position: fixed;
            top: 0;
            left: 0;
            right: 0;
            padding: 14px 20px;
            background: var(--bg-secondary);
            color: var(--text-primary);
            text-align: center;
            z-index: ${UI_CONFIG.NOTIFICATION_Z_INDEX};
            font-weight: 500;
            font-size: 14px;
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 12px;
            box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4), 0 0 15px rgba(var(--accent-primary-rgb), 0.15);
            transform: translateY(-100%);
            transition: transform 0.3s ease;
            word-wrap: break-word;
            max-width: 100%;
        }
    `;

    document.head.appendChild(styleEl);
}

// Add alert styles when the script loads
addAlertStyles();

// Export the UI functions
window.ui = {
    updateProgressBar,
    showSpinner,
    hideSpinner,
    showError,
    showMessage,
    renderMarkdown,
    createDynamicFavicon,
    updateFavicon,
    showAlert,
    showInlineError,
    clearInlineError
};
