/**
 * Semantic Search — Shared Module
 *
 * Reusable functions for semantic search across any page:
 * - renderSnippet: markdown-to-HTML for search snippets
 * - buildTieredResults: three-tier merge of text + semantic results
 * - createSemanticResultCard: standalone card element for a semantic match
 * - isSafeExternalUrl: URL scheme validation
 *
 * Exposed via window.SemanticSearch
 */
(function() {

// bearer:disable javascript_lang_manual_html_sanitization
const esc = window.escapeHtml || (s => String(s || '').replace(/[&<>"']/g, m => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":"&#39;"})[m]));

/**
 * Render a markdown snippet as sanitized inline HTML.
 * Falls back to escapeHtml when marked/DOMPurify are not loaded.
 * @param {string} md - markdown snippet
 * @param {string} [query] - optional search query for keyword highlighting
 */
function renderSnippet(md, query) {
    if (!md) return '';
    let html;
    if (window.marked && window.DOMPurify) {
        html = window.marked.parseInline(String(md));
        html = window.DOMPurify.sanitize(html, {
            ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'span', 'a', 'code', 'br', 'p', 'ul', 'ol', 'li', 'mark'],
            ALLOWED_ATTR: ['href', 'title', 'class'],
            ALLOW_DATA_ATTR: false,
        });
    } else {
        html = esc(md);
        // Skip highlightTerms in fallback path: injecting <mark> into
        // esc'd text would produce garbled entities like &lt;<mark>b</mark>&gt;
        return html;
    }
    if (query) {
        html = highlightTerms(html, query);
    }
    return html;
}

/**
 * Highlight query terms in an HTML string by wrapping matches in <mark> tags.
 * Only operates on text between HTML tags to avoid breaking markup.
 * @param {string} html - HTML string
 * @param {string} query - search query (split into words, each highlighted)
 * @returns {string} HTML with <mark> wrapped terms
 */
function highlightTerms(html, query) {
    if (!html || !query) return html;
    // Extract meaningful words (3+ chars to avoid noise)
    const words = query.split(/\s+/).filter(function(w) { return w.length >= 3; });
    if (words.length === 0) return html;
    // Escape regex special characters in each word
    const escaped = words.map(function(w) {
        return w.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    });
    const pattern = new RegExp('(' + escaped.join('|') + ')', 'gi');
    // Split on HTML tags to only highlight text content, not inside tags
    return html.replace(/(<[^>]*>)|([^<]+)/g, function(match, tag, text) {
        if (tag) return tag; // preserve HTML tags as-is
        return text.replace(pattern, '<mark class="ldr-search-highlight">$1</mark>');
    });
}

/**
 * Three-tier merge of text-filtered and semantic search results.
 *
 * Tier 1: items matching both text + semantic (sorted by similarity DESC)
 * Tier 2: text-only matches (original order, typically recency)
 * Tier 3: semantic-only matches (sorted by similarity DESC)
 *
 * @param {Array} textResults - items from text filter
 * @param {Array} semanticResults - results from semantic API (must have .similarity)
 * @param {Object} [options] - optional ID key configuration
 * @param {string} [options.textIdKey='id'] - property name for ID on text result items
 * @param {string} [options.semanticIdKey='research_id'] - property name for ID on semantic results
 * @returns {{ tier1: Array, tier2: Array, tier3: Array }}
 */
function buildTieredResults(textResults, semanticResults, options) {
    const textIdKey = (options && options.textIdKey) || 'id';
    const semanticIdKey = (options && options.semanticIdKey) || 'research_id';

    // Group semantic results by ID, keep best per unique ID
    const semanticMap = new Map();
    for (const sr of semanticResults) {
        if (!sr[semanticIdKey]) continue;
        const rid = String(sr[semanticIdKey]);
        const existing = semanticMap.get(rid);
        if (!existing || sr.similarity > existing.similarity) {
            semanticMap.set(rid, sr);
        }
    }

    // Classify text results
    const tier1 = [];
    const tier2 = [];
    const matchedSemanticIds = new Set();

    for (const textItem of textResults) {
        const rid = String(textItem[textIdKey]);
        const sr = semanticMap.get(rid);
        if (sr) {
            tier1.push({
                historyItem: textItem,
                semanticMatch: { similarity: sr.similarity, snippet: sr.snippet || '' }
            });
            matchedSemanticIds.add(rid);
        } else {
            tier2.push({ historyItem: textItem });
        }
    }

    // Collect semantic-only results
    const tier3 = [];
    for (const [rid, sr] of semanticMap) {
        if (!matchedSemanticIds.has(rid)) {
            tier3.push({ semanticResult: sr });
        }
    }

    // Sort within tiers
    tier1.sort((a, b) => b.semanticMatch.similarity - a.semanticMatch.similarity);
    tier3.sort((a, b) => b.semanticResult.similarity - a.semanticResult.similarity);

    return { tier1, tier2, tier3 };
}

/**
 * Create a self-contained semantic result card element.
 *
 * @param {Object} result - semantic search result with: similarity, snippet, and
 *                          domain-specific fields
 * @param {Object} [config] - optional card configuration for different domains.
 *   Defaults produce the original research-history card.
 *   Each function receives the result object:
 *   - getId(r):       data-id attribute value (default: r.research_id)
 *   - getTitle(r):    display title (default: r.research_title || r.title)
 *   - getUrl(r):      link/click target URL (default: URLBuilder.resultsPage)
 *   - getBadges(r):   array of {icon, label} (default: report/source badge)
 *   - getDate(r):     date string or null (default: r.research_created_at)
 *   - getSubtitle(r): optional subtitle line or null
 * @returns {HTMLElement}
 */
function createSemanticResultCard(result, config, query) {
    const cfg = config || {};

    const cardId = (cfg.getId || function(r) { return r.research_id || ''; })(result);
    const displayTitle = (cfg.getTitle || function(r) { return r.research_title || r.title || 'Untitled'; })(result);
    const cardUrl = (cfg.getUrl || function(r) {
        return (typeof URLBuilder !== 'undefined' && r.research_id)
            ? URLBuilder.resultsPage(r.research_id) : '#';
    })(result);

    const badges = cfg.getBadges
        ? cfg.getBadges(result)
        : [{ icon: result.type === 'report' ? 'file-alt' : 'link',
             label: result.type === 'report' ? 'Report' : 'Source' }];

    const rawDate = cfg.getDate
        ? cfg.getDate(result)
        : (result.research_created_at || null);

    const subtitle = cfg.getSubtitle ? cfg.getSubtitle(result) : null;

    const similarity = result.similarity != null ? String(result.similarity) : '';

    let dateStr = '';
    if (rawDate) {
        try {
            dateStr = new Date(rawDate).toLocaleDateString();
        } catch {
            dateStr = '';
        }
    }

    const card = document.createElement('div');
    card.className = 'ldr-semantic-result';
    card.dataset.id = cardId;

    // Build badges HTML
    let badgesHtml = '';
    for (const b of badges) {
        if (b && b.label) {
            badgesHtml += `<span class="ldr-badge ldr-badge-info ldr-semantic-type-badge"><i class="fas fa-${esc(b.icon || 'file')}"></i> ${esc(b.label)}</span> `;
        }
    }

    // bearer:disable javascript_lang_dangerous_insert_html
    // eslint-disable-next-line no-unsanitized/property -- audited 2026-03-28: all interpolations use escapeHtml/esc, numeric coercion, or hardcoded strings
    card.innerHTML = `
        <div class="ldr-semantic-result-header">
            <div>
                ${badgesHtml}
                ${similarity ? `<span class="ldr-ai-match-badge"><i class="fas fa-brain"></i> ${esc(similarity)}% match</span>` : ''}
            </div>
            ${dateStr ? `<small class="ldr-semantic-result-date">${esc(dateStr)}</small>` : ''}
        </div>
        <h4 class="ldr-semantic-result-title">
            <a href="${esc(cardUrl)}">${esc(displayTitle)}</a>
        </h4>
        ${subtitle ? `<p class="ldr-semantic-result-subtitle">${esc(subtitle)}</p>` : ''}
        ${result.snippet ? `<div class="ldr-history-item-snippet">${renderSnippet(result.snippet, query)}</div>` : ''}
        ${result.url && isSafeExternalUrl(result.url) ? `
            <p class="ldr-semantic-result-source">
                <a href="${esc(result.url)}" target="_blank" rel="noopener noreferrer">
                    <i class="fas fa-external-link-alt"></i> View Source
                </a>
            </p>
        ` : ''}
    `;

    // Click handler — navigate to card URL
    card.addEventListener('click', (e) => {
        if (!e.target.closest('a') && cardId && cardUrl !== '#' && typeof URLValidator !== 'undefined') {
            URLValidator.safeAssign(window.location, 'href', cardUrl);
        }
    });

    return card;
}

/**
 * Validate external URL for safe rendering.
 */
function isSafeExternalUrl(url) {
    if (!url || typeof url !== 'string') return false;

    if (typeof URLValidator !== 'undefined' && URLValidator.isSafeUrl) {
        return URLValidator.isSafeUrl(url, { allowMailto: false });
    }

    const trimmedUrl = url.trim().toLowerCase();
    const unsafeSchemes = ['javascript:', 'data:', 'vbscript:', 'about:', 'blob:', 'file:'];
    for (const scheme of unsafeSchemes) {
        if (trimmedUrl.startsWith(scheme)) return false;
    }
    return trimmedUrl.startsWith('http://') || trimmedUrl.startsWith('https://');
}

// Expose shared API
window.SemanticSearch = {
    renderSnippet,
    highlightTerms,
    buildTieredResults,
    createSemanticResultCard,
    isSafeExternalUrl,
};

})();
