// JournaLock rich text editor — formatting, bullets, right-click spell suggestions.
(function () {
    'use strict';

    var spellWords = null;
    var spellLoading = null;
    var editors = new Map();
    var spellMenu = null;
    var spellMenuState = null;
    var USER_DICT_KEY = 'jl-spell-user-words';

    var EXTRA_WORDS = (
        'journal journallock blockchain timestamp hash encryption vault attorney inventor patent trademark copyright ' +
        'disclosure prosecution claim claims embodiment prior art uspto ptab litigation counsel affidavit notary ' +
        'witness testimony deposition exhibit paragraph therefore however nevertheless agreement contract license ' +
        'because through though although spelling grammar punctuation misspelled corrected suggestion'
    ).split(/\s+/);

    function getEl(elementId) {
        return elementId ? document.getElementById(elementId) : null;
    }

    // Blazor once bound Value="currentContent" (the identifier, not the field).
    // Treat that leftover as empty so the canvas is a blank writing surface.
    function sanitizeEditorHtml(html) {
        var raw = (html || '').trim();
        if (!raw) return '';
        var plain = raw.replace(/<[^>]+>/g, '').replace(/&nbsp;/gi, ' ').trim();
        if (plain === 'currentContent' || plain === 'currentTitle') return '';
        return html || '';
    }

    function dispatchInput(el) {
        if (!el) return;
        el.dispatchEvent(new Event('input', { bubbles: true }));
    }

    function loadUserDictionary() {
        try {
            var raw = localStorage.getItem(USER_DICT_KEY);
            if (!raw) return new Set();
            return new Set(JSON.parse(raw).map(function (w) { return String(w).toLowerCase(); }));
        } catch (e) {
            return new Set();
        }
    }

    function saveUserWord(word) {
        var lower = (word || '').toLowerCase().trim();
        if (!lower) return;
        var set = loadUserDictionary();
        set.add(lower);
        try {
            localStorage.setItem(USER_DICT_KEY, JSON.stringify(Array.from(set)));
        } catch (e) { /* ignore */ }
        if (spellWords) spellWords.add(lower);
        if (spellBuckets && lower.length >= 2) {
            var key = lower.slice(0, 2);
            if (!spellBuckets[key]) spellBuckets[key] = [];
            if (spellBuckets[key].indexOf(lower) < 0) spellBuckets[key].push(lower);
        }
    }

    function loadSpellDictionary() {
        if (spellWords) return Promise.resolve(spellWords);
        if (spellLoading) return spellLoading;

        // Free with the app: general English + legal + medical packs (no purchase / no key).
        var packs = [
            'data/spell-words-en.txt',
            'data/spell-legal-en.txt',
            'data/spell-medical-en.txt'
        ];

        spellLoading = Promise.all(packs.map(function (url) {
            return fetch(url).then(function (r) { return r.ok ? r.text() : ''; }).catch(function () { return ''; });
        })).then(function (texts) {
            var set = new Set();
            texts.forEach(function (text) {
                if (!text) return;
                text.split(/\r?\n/).forEach(function (w) {
                    w = w.trim().toLowerCase();
                    if (w.length > 0) set.add(w);
                });
            });
            EXTRA_WORDS.forEach(function (w) { set.add(w.toLowerCase()); });
            loadUserDictionary().forEach(function (w) { set.add(w); });
            ['ok', 'api', 'aws', 'ios', 'pdf', 'url', 'usa', 'uspto', 'nda', 'ai', 'groq', 'hipaa', 'phi', 'emr', 'ehr'].forEach(function (w) {
                set.add(w);
            });
            spellWords = set;
            spellBuckets = null; // rebuild buckets after load
            return set;
        }).catch(function () {
            spellWords = new Set(['the', 'and', 'to', 'of', 'a', 'in', 'for', 'is', 'on', 'that', 'journal', 'patent']);
            EXTRA_WORDS.forEach(function (w) { spellWords.add(w.toLowerCase()); });
            spellBuckets = null;
            return spellWords;
        });

        return spellLoading;
    }

    function levenshtein(a, b) {
        if (a === b) return 0;
        if (!a.length) return b.length;
        if (!b.length) return a.length;
        var row = [];
        for (var i = 0; i <= b.length; i++) row[i] = i;
        for (var i = 1; i <= a.length; i++) {
            var prev = i;
            for (var j = 1; j <= b.length; j++) {
                var val = a[i - 1] === b[j - 1] ? row[j - 1] : Math.min(row[j - 1], row[j], prev) + 1;
                row[j - 1] = prev;
                prev = val;
            }
            row[b.length] = prev;
        }
        return row[b.length];
    }

    function preserveCase(original, suggestion) {
        if (!original || !suggestion) return suggestion;
        if (original === original.toUpperCase()) return suggestion.toUpperCase();
        if (original[0] === original[0].toUpperCase()) {
            return suggestion.charAt(0).toUpperCase() + suggestion.slice(1);
        }
        return suggestion;
    }

    function isMisspelled(word, dict) {
        if (!word || word.length < 2) return false;
        // Skip pure numbers, codes, ALL-CAPS acronyms, snake_case tokens
        if (/^\d+$/.test(word)) return false;
        if (/^[A-Z]{2,}$/.test(word)) return false;
        if (word.includes('_') || word.includes('-')) return false;
        // Proper names / sentence-start titles: Capitalized mixed case — don't false-flag "Jim", "Belmont"
        if (/^[A-Z][a-z]+$/.test(word) && word.length >= 2) {
            var lowerCap = word.toLowerCase();
            if (dict.has(lowerCap)) return false;
            return false; // assume proper noun / title case — user can still right-click + Add
        }
        var lower = word.toLowerCase();
        if (dict.has(lower)) return false;
        if (lower.endsWith("'s") && dict.has(lower.slice(0, -2))) return false;
        if (lower.endsWith("'t") && dict.has(lower.slice(0, -2))) return false;
        if (lower.endsWith("'re") && dict.has(lower.slice(0, -3))) return false;
        if (lower.endsWith("'ve") && dict.has(lower.slice(0, -3))) return false;
        if (lower.endsWith("'ll") && dict.has(lower.slice(0, -3))) return false;
        if (lower.endsWith("'d") && dict.has(lower.slice(0, -2))) return false;
        // Common English morphology without bloating the dictionary
        if (lower.endsWith('s') && lower.length > 3 && dict.has(lower.slice(0, -1))) return false;
        if (lower.endsWith('es') && lower.length > 4 && dict.has(lower.slice(0, -2))) return false;
        if (lower.endsWith('ies') && lower.length > 5 && dict.has(lower.slice(0, -3) + 'y')) return false;
        if (lower.endsWith('ed') && lower.length > 4) {
            if (dict.has(lower.slice(0, -2))) return false;
            if (dict.has(lower.slice(0, -1))) return false; // baked -> bake
            if (lower.length > 5 && lower[lower.length - 3] === lower[lower.length - 4] && dict.has(lower.slice(0, -3))) return false; // stopped
        }
        if (lower.endsWith('ing') && lower.length > 5) {
            if (dict.has(lower.slice(0, -3))) return false;
            if (dict.has(lower.slice(0, -3) + 'e')) return false; // making -> make
            if (lower.length > 6 && lower[lower.length - 4] === lower[lower.length - 5] && dict.has(lower.slice(0, -4))) return false;
        }
        if (lower.endsWith('ly') && lower.length > 4 && dict.has(lower.slice(0, -2))) return false;
        if (lower.endsWith('ness') && lower.length > 6 && dict.has(lower.slice(0, -4))) return false;
        if (lower.endsWith('ment') && lower.length > 6 && dict.has(lower.slice(0, -4))) return false;
        if (lower.endsWith('tion') && lower.length > 6) {
            if (dict.has(lower.slice(0, -4) + 'te')) return false;
            if (dict.has(lower.slice(0, -4) + 'e')) return false;
        }
        return true;
    }

    /** Prefix buckets so suggestions stay fast with a ~25k word dictionary. */
    var spellBuckets = null;
    function getSpellBuckets(dict) {
        if (spellBuckets) return spellBuckets;
        spellBuckets = {};
        dict.forEach(function (entry) {
            if (!entry || entry.length < 2) return;
            var key = entry.slice(0, 2);
            if (!spellBuckets[key]) spellBuckets[key] = [];
            spellBuckets[key].push(entry);
        });
        return spellBuckets;
    }

    function suggestWords(word, dict, limit) {
        limit = limit || 6;
        var lower = (word || '').toLowerCase();
        if (!lower || lower.length < 2) return [];

        var maxDist = lower.length <= 4 ? 1 : lower.length <= 8 ? 2 : 2;
        var candidates = [];
        var buckets = getSpellBuckets(dict);
        var keys = {};
        // Nearby first-two-letter buckets (typos often keep first letter)
        var prefix = lower.slice(0, 2);
        keys[prefix] = true;
        if (lower.length >= 1) keys[lower[0] + (lower[1] || 'a')] = true;
        // Also scan same-length-ish words starting with first letter only via adjacent buckets
        Object.keys(buckets).forEach(function (k) {
            if (k[0] === lower[0]) keys[k] = true;
        });

        Object.keys(keys).forEach(function (k) {
            var list = buckets[k];
            if (!list) return;
            for (var i = 0; i < list.length; i++) {
                var entry = list[i];
                if (Math.abs(entry.length - lower.length) > maxDist + 1) continue;
                var dist = levenshtein(lower, entry);
                if (dist > 0 && dist <= maxDist) {
                    candidates.push({ word: entry, dist: dist });
                }
            }
        });

        candidates.sort(function (a, b) {
            return a.dist - b.dist || a.word.localeCompare(b.word);
        });

        var seen = new Set();
        var out = [];
        for (var i = 0; i < candidates.length && out.length < limit; i++) {
            var item = candidates[i];
            if (seen.has(item.word)) continue;
            seen.add(item.word);
            out.push(preserveCase(word, item.word));
        }
        return out;
    }

    function clearSpellMarks(root) {
        if (!root) return;
        root.querySelectorAll('.jl-misspell').forEach(function (span) {
            var parent = span.parentNode;
            if (!parent) return;
            parent.replaceChild(document.createTextNode(span.textContent || ''), span);
            parent.normalize();
        });
    }

    function wrapMisspellingsInTextNode(textNode, dict) {
        var text = textNode.textContent || '';
        if (!text.trim()) return;

        var wordRe = /[A-Za-z']+/g;
        var match;
        var last = 0;
        var frag = document.createDocumentFragment();
        var changed = false;

        while ((match = wordRe.exec(text)) !== null) {
            var word = match[0];
            var start = match.index;
            if (start > last) {
                frag.appendChild(document.createTextNode(text.substring(last, start)));
            }
            if (isMisspelled(word, dict)) {
                var span = document.createElement('span');
                span.className = 'jl-misspell';
                span.setAttribute('spellcheck', 'false');
                span.textContent = word;
                frag.appendChild(span);
                changed = true;
            } else {
                frag.appendChild(document.createTextNode(word));
            }
            last = start + word.length;
        }

        if (last < text.length) {
            frag.appendChild(document.createTextNode(text.substring(last)));
        }

        if (changed && textNode.parentNode) {
            textNode.parentNode.replaceChild(frag, textNode);
        }
    }

    function applySpellCheck(elementId) {
        var el = getEl(elementId);
        if (!el || el.dataset.jlSpellOff === 'true' || el.isContentEditable === false) return Promise.resolve();

        var saved = saveSelection(el);

        return loadSpellDictionary().then(function (dict) {
            clearSpellMarks(el);
            var walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, {
                acceptNode: function (node) {
                    if (!node.parentElement) return NodeFilter.FILTER_REJECT;
                    if (node.parentElement.closest('.jl-misspell')) return NodeFilter.FILTER_REJECT;
                    if (node.parentElement.tagName === 'SCRIPT' || node.parentElement.tagName === 'STYLE') {
                        return NodeFilter.FILTER_REJECT;
                    }
                    return NodeFilter.FILTER_ACCEPT;
                }
            });

            var nodes = [];
            while (walker.nextNode()) nodes.push(walker.currentNode);
            nodes.forEach(function (n) { wrapMisspellingsInTextNode(n, dict); });
            if (saved) restoreSelection(el, saved);
        });
    }

    function saveSelection(el) {
        var sel = window.getSelection();
        if (!sel || sel.rangeCount === 0 || !el) return null;
        var range = sel.getRangeAt(0);
        if (!el.contains(range.commonAncestorContainer)) return null;
        return range.cloneRange();
    }

    function restoreSelection(el, saved) {
        if (!saved || !el) return;
        var sel = window.getSelection();
        if (!sel) return;
        sel.removeAllRanges();
        sel.addRange(saved);
    }

    function notifyDotNet(elementId) {
        var state = editors.get(elementId);
        if (!state || !state.dotNetRef) return;
        var el = getEl(elementId);
        if (!el) return;
        state.dotNetRef.invokeMethodAsync('OnEditorInput', el.innerHTML || '')
            .catch(function () { });
    }

    function notifyDotNetBlur(elementId) {
        var state = editors.get(elementId);
        if (!state || !state.dotNetRef) return;
        var el = getEl(elementId);
        if (!el) return;
        state.dotNetRef.invokeMethodAsync('OnEditorBlur', el.innerHTML || '')
            .catch(function () { });
    }

    function handleInput(elementId) {
        var el = getEl(elementId);
        if (!el) return;
        updatePlaceholder(el);
        notifyDotNet(elementId);
    }

    function updatePlaceholder(el) {
        if (!el) return;
        var empty = !(el.innerText || '').trim();
        el.classList.toggle('jl-text-editor__empty', empty);
    }

    var selectionCache = new Map();

    function cacheSelection(elementId) {
        var el = getEl(elementId);
        if (!el) return;
        var sel = window.getSelection();
        if (!sel || sel.rangeCount === 0) return;
        var range = sel.getRangeAt(0);
        if (!el.contains(range.commonAncestorContainer)) return;
        selectionCache.set(elementId, range.cloneRange());
    }

    function restoreCachedSelection(elementId) {
        var el = getEl(elementId);
        var range = selectionCache.get(elementId);
        if (!el || !range) return false;
        try {
            el.focus();
            var sel = window.getSelection();
            if (!sel) return false;
            sel.removeAllRanges();
            sel.addRange(range);
            return true;
        } catch (e) {
            return false;
        }
    }

    function execOn(elementId, command, value) {
        var el = getEl(elementId);
        if (!el) return false;
        restoreCachedSelection(elementId);
        el.focus();
        try {
            var ok = document.execCommand(command, false, value ?? null);
            dispatchInput(el);
            handleInput(elementId);
            cacheSelection(elementId);
            return ok;
        } catch (e) {
            console.warn('jlTextEditor:', command, e);
            return false;
        }
    }

    function normalizeToTextPoint(range) {
        if (!range) return null;
        var node = range.startContainer;
        var offset = range.startOffset;
        if (node.nodeType === Node.TEXT_NODE) {
            return { node: node, offset: Math.min(offset, (node.textContent || '').length) };
        }
        if (node.nodeType === Node.ELEMENT_NODE) {
            var kids = node.childNodes;
            if (offset < kids.length && kids[offset] && kids[offset].nodeType === Node.TEXT_NODE) {
                return { node: kids[offset], offset: 0 };
            }
            if (offset > 0 && kids[offset - 1] && kids[offset - 1].nodeType === Node.TEXT_NODE) {
                var prev = kids[offset - 1];
                return { node: prev, offset: (prev.textContent || '').length };
            }
            var walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT);
            var first = walker.nextNode();
            if (first) return { node: first, offset: 0 };
        }
        return null;
    }

    function expandRangeToWord(range) {
        if (!range) return null;
        var point = normalizeToTextPoint(range);
        if (!point) return null;
        var node = point.node;
        var text = node.textContent || '';
        var start = point.offset;
        var end = range.collapsed ? start : range.endOffset;
        if (!range.collapsed && range.endContainer === node) {
            end = range.endOffset;
        } else if (range.collapsed) {
            end = start;
        }
        while (start > 0 && /[A-Za-z']/.test(text.charAt(start - 1))) start--;
        while (end < text.length && /[A-Za-z']/.test(text.charAt(end))) end++;
        if (start === end) return null;
        var wordRange = document.createRange();
        wordRange.setStart(node, start);
        wordRange.setEnd(node, end);
        return { range: wordRange, word: text.substring(start, end) };
    }

    function getWordFromSelection(el) {
        var sel = window.getSelection();
        if (!sel || sel.rangeCount === 0) return null;
        var range = sel.getRangeAt(0);
        if (!el.contains(range.commonAncestorContainer)) return null;
        if (!sel.isCollapsed && (sel.toString() || '').trim()) {
            return expandRangeToWord(range.cloneRange());
        }
        return expandRangeToWord(range.cloneRange());
    }

    function getWordFromPoint(el, clientX, clientY) {
        var range = null;
        if (document.caretRangeFromPoint) {
            range = document.caretRangeFromPoint(clientX, clientY);
        } else if (document.caretPositionFromPoint) {
            var pos = document.caretPositionFromPoint(clientX, clientY);
            if (pos) {
                range = document.createRange();
                range.setStart(pos.offsetNode, pos.offset);
                range.collapse(true);
            }
        }
        if (!range || !el.contains(range.startContainer)) {
            return getWordFromSelection(el);
        }
        return expandRangeToWord(range);
    }

    function getTextOffsetInElement(el, targetNode, targetOffset) {
        if (!el || !targetNode) return -1;
        var offset = 0;
        var walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, {
            acceptNode: function (node) {
                if (node.parentElement && node.parentElement.closest('.jl-misspell')) {
                    return NodeFilter.FILTER_ACCEPT;
                }
                return NodeFilter.FILTER_ACCEPT;
            }
        });
        var node;
        while ((node = walker.nextNode())) {
            if (node === targetNode) {
                return offset + Math.min(targetOffset, (node.textContent || '').length);
            }
            offset += (node.textContent || '').length;
        }
        return -1;
    }

    function getRangeAtTextOffset(el, charOffset, length) {
        if (!el || charOffset < 0 || length < 1) return null;
        var pos = 0;
        var walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
        var node;
        while ((node = walker.nextNode())) {
            var text = node.textContent || '';
            var len = text.length;
            if (pos + len > charOffset) {
                var start = charOffset - pos;
                var end = Math.min(start + length, len);
                if (start >= end) return null;
                var wordRange = document.createRange();
                wordRange.setStart(node, start);
                wordRange.setEnd(node, end);
                return wordRange;
            }
            pos += len;
        }
        return null;
    }

    function findWordRangeInElement(el, word) {
        if (!el || !word) return null;
        var wordRe = new RegExp('\\b' + word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b');
        var walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
        var node;
        while ((node = walker.nextNode())) {
            var text = node.textContent || '';
            var match = wordRe.exec(text);
            if (match) {
                var wordRange = document.createRange();
                wordRange.setStart(node, match.index);
                wordRange.setEnd(node, match.index + match[0].length);
                return wordRange;
            }
        }
        return null;
    }

    function applySpellSuggestion(elementId, replacement) {
        var el = getEl(elementId);
        var menu = spellMenuState;
        if (!el || !menu || menu.elementId !== elementId || !replacement) return;

        clearSpellMarks(el);

        var wordRange = null;
        if (menu.textOffset >= 0) {
            wordRange = getRangeAtTextOffset(el, menu.textOffset, menu.wordLength);
            if (wordRange && wordRange.toString() !== menu.word) {
                wordRange = null;
            }
        }

        if (!wordRange && menu.range) {
            try {
                if (menu.range.startContainer && menu.range.startContainer.isConnected &&
                    el.contains(menu.range.startContainer)) {
                    wordRange = menu.range.cloneRange();
                }
            } catch (ignore) { wordRange = null; }
        }

        if (!wordRange) {
            wordRange = findWordRangeInElement(el, menu.word);
        }

        if (!wordRange) return;

        el.focus();
        var textNode = document.createTextNode(replacement);
        wordRange.deleteContents();
        wordRange.insertNode(textNode);
        el.normalize();

        var sel = window.getSelection();
        if (sel) {
            sel.removeAllRanges();
            var after = document.createRange();
            after.setStartAfter(textNode);
            after.collapse(true);
            sel.addRange(after);
        }

        el.dataset.jlSpellMenuOpen = 'false';
        hideSpellMenu();
        dispatchInput(el);
        handleInput(elementId);
    }

    function ensureSpellMenu() {
        if (spellMenu) return spellMenu;
        spellMenu = document.createElement('div');
        spellMenu.id = 'jl-spell-menu';
        spellMenu.className = 'jl-spell-menu';
        spellMenu.setAttribute('role', 'menu');
        spellMenu.style.display = 'none';
        document.body.appendChild(spellMenu);

        document.addEventListener('mousedown', function (e) {
            if (!spellMenu || spellMenu.style.display === 'none') return;
            if (spellMenu.contains(e.target)) return;
            hideSpellMenu();
        }, true);
        document.addEventListener('scroll', function () { hideSpellMenu(); }, true);
        window.addEventListener('resize', function () { hideSpellMenu(); });
        return spellMenu;
    }

    function hideSpellMenu() {
        if (spellMenuState) {
            var openEl = getEl(spellMenuState.elementId);
            if (openEl) openEl.dataset.jlSpellMenuOpen = 'false';
        }
        if (spellMenu) spellMenu.style.display = 'none';
        spellMenuState = null;
    }

    function renderSpellMenu(items, title) {
        var menu = ensureSpellMenu();
        menu.innerHTML = '';
        var heading = document.createElement('div');
        heading.className = 'jl-spell-menu__title';
        heading.textContent = title;
        menu.appendChild(heading);

        items.forEach(function (item) {
            if (item.type === 'sep') {
                var sep = document.createElement('div');
                sep.className = 'jl-spell-menu__sep';
                menu.appendChild(sep);
                return;
            }
            var btn = document.createElement('button');
            btn.type = 'button';
            btn.className = 'jl-spell-menu__item' + (item.muted ? ' jl-spell-menu__item--muted' : '');
            btn.textContent = item.label;
            btn.disabled = !!item.disabled;
            if (item.action) {
                btn.addEventListener('mousedown', function (e) {
                    e.preventDefault();
                    e.stopPropagation();
                });
                btn.addEventListener('click', function (e) {
                    e.preventDefault();
                    e.stopPropagation();
                    item.action();
                });
            }
            menu.appendChild(btn);
        });
    }

    function showSpellMenuAt(x, y) {
        var menu = ensureSpellMenu();
        menu.style.display = 'block';
        menu.style.left = '0px';
        menu.style.top = '0px';
        var rect = menu.getBoundingClientRect();
        var left = Math.min(x, window.innerWidth - rect.width - 8);
        var top = Math.min(y, window.innerHeight - rect.height - 8);
        menu.style.left = Math.max(8, left) + 'px';
        menu.style.top = Math.max(8, top) + 'px';
    }

    function openSpellMenuForWord(elementId, hit, clientX, clientY) {
        var el = getEl(elementId);
        var state = editors.get(elementId);
        if (!el || !state || !hit || !hit.word) return;

        if (!hit.word || hit.word.length < 2) {
            renderSpellMenu([{ label: 'Select a word to see suggestions', muted: true }], 'Spelling');
            showSpellMenuAt(clientX, clientY);
            return;
        }

        el.dataset.jlSpellMenuOpen = 'true';
        var textOffset = getTextOffsetInElement(el, hit.range.startContainer, hit.range.startOffset);

        spellMenuState = {
            elementId: elementId,
            word: hit.word,
            wordLength: hit.word.length,
            textOffset: textOffset,
            range: hit.range.cloneRange(),
            x: clientX,
            y: clientY
        };

        loadSpellDictionary().then(function (dict) {
            if (!spellMenuState || spellMenuState.word !== hit.word) return;

            var local = suggestWords(hit.word, dict, 6);
            var items = [];

            if (local.length > 0) {
                local.forEach(function (s) {
                    items.push({
                        label: s,
                        action: (function (suggestion) {
                            return function () {
                                applySpellSuggestion(elementId, suggestion);
                            };
                        })(s)
                    });
                });
            } else if (!isMisspelled(hit.word, dict)) {
                items.push({ label: '"' + hit.word + '" looks correct', muted: true });
            } else {
                items.push({ label: 'No free suggestions — try AI check', muted: true });
            }

            items.push({ type: 'sep' });
            items.push({
                label: 'Add "' + hit.word + '" to dictionary',
                action: function () {
                    saveUserWord(hit.word);
                    applySpellCheck(elementId);
                }
            });

            renderSpellMenu(items, hit.word);
            showSpellMenuAt(clientX, clientY);
        });
    }

    function openSpellContextMenu(elementId, e) {
        var el = getEl(elementId);
        var state = editors.get(elementId);
        if (!el || !state || el.dataset.jlDisabled === 'true') return;

        if (e && e.preventDefault) e.preventDefault();
        if (e && e.stopPropagation) e.stopPropagation();

        var hit = getWordFromSelection(el);
        if (!hit && e) hit = getWordFromPoint(el, e.clientX, e.clientY);
        var x = e ? e.clientX : (window.innerWidth / 2);
        var y = e ? e.clientY : (window.innerHeight / 2);
        openSpellMenuForWord(elementId, hit, x, y);
    }

    window.jlTextEditor = {
        init: function (elementId, dotNetRef, placeholder, options) {
            var el = getEl(elementId);
            if (!el) return;

            options = options || {};

            if (!editors.has(elementId)) {
                editors.set(elementId, { dotNetRef: dotNetRef, spellTimer: null, hasGroqAi: !!options.hasGroqAi });
            } else {
                var existing = editors.get(elementId);
                existing.dotNetRef = dotNetRef;
                existing.hasGroqAi = !!options.hasGroqAi;
            }

            if (el.dataset.jlRichInit === '1') return;
            el.dataset.jlRichInit = '1';
            el.contentEditable = el.dataset.jlDisabled === 'true' ? 'false' : 'true';
            el.spellcheck = true;
            el.setAttribute('role', 'textbox');
            el.setAttribute('aria-multiline', 'true');
            if (placeholder) el.dataset.placeholder = placeholder;

            el.addEventListener('input', function () { handleInput(elementId); });
            el.addEventListener('keyup', function () { cacheSelection(elementId); });
            el.addEventListener('mouseup', function () { cacheSelection(elementId); });
            el.addEventListener('pointerdown', function () {
                if (el.dataset.jlDisabled === 'true') return;
                blurAllEditorsExcept(elementId);
                window.requestAnimationFrame(function () {
                    try {
                        if (document.activeElement !== el && !el.contains(document.activeElement)) {
                            el.focus({ preventScroll: true });
                        }
                    } catch (ignore) {
                        try { el.focus(); } catch (e2) { /* ignore */ }
                    }
                });
            });
            el.addEventListener('blur', function () {
                if (el.dataset.jlSpellMenuOpen === 'true') {
                    if (spellMenu && spellMenu.style.display !== 'none') return;
                    el.dataset.jlSpellMenuOpen = 'false';
                }
                // Defer so focus can land on the next editor before Blazor re-renders (MAUI WebView).
                setTimeout(function () {
                    notifyDotNetBlur(elementId);
                    applySpellCheck(elementId);
                }, 0);
            });
            el.addEventListener('contextmenu', function (e) { openSpellContextMenu(elementId, e); });
            el.addEventListener('mouseup', function (e) {
                if (e.button === 2) openSpellContextMenu(elementId, e);
            });
            el.addEventListener('pointerdown', function (e) {
                if (e.button === 2) openSpellContextMenu(elementId, e);
            });
            el.addEventListener('paste', function (e) {
                e.preventDefault();
                var text = (e.clipboardData || window.clipboardData).getData('text/plain');
                document.execCommand('insertText', false, text);
                handleInput(elementId);
            });
            el.addEventListener('keydown', function (e) {
                if (e.key === 'Tab') {
                    e.preventDefault();
                    e.stopPropagation();
                    var inset = e.shiftKey ? '\t' : '    ';
                    document.execCommand('insertText', false, inset);
                    handleInput(elementId);
                }
            });

            updatePlaceholder(el);
        },

        setGroqAvailable: function (elementId, hasGroqAi) {
            var state = editors.get(elementId);
            if (state) state.hasGroqAi = !!hasGroqAi;
        },

        cacheSelection: function (elementId) {
            cacheSelection(elementId);
        },

        bindTitleToBody: function (titleInputId, bodyElementId) {
            var titleEl = document.getElementById(titleInputId);
            var bodyEl = getEl(bodyElementId);
            if (!titleEl || !bodyEl) return;
            if (titleEl.dataset.jlTitleBound === '1') {
                if (bodyEl.focus) bodyEl.focus();
                return;
            }
            titleEl.dataset.jlTitleBound = '1';
            titleEl.addEventListener('keydown', function (e) {
                if (e.key === 'Tab' || e.key === 'Enter') {
                    e.preventDefault();
                    e.stopPropagation();
                    if (bodyEl.focus) bodyEl.focus();
                }
            });
            if (bodyEl.focus) bodyEl.focus();
        },

        formatInput: function (inputId, cmd) {
            var el = document.getElementById(inputId);
            if (!el || el.tagName !== 'INPUT' && el.tagName !== 'TEXTAREA') return;
            var start = el.selectionStart || 0;
            var end = el.selectionEnd || 0;
            var val = el.value || '';
            var sel = val.substring(start, end);
            if (!sel) return;
            var open = '', close = '';
            if (cmd === 'bold') { open = '**'; close = '**'; }
            else if (cmd === 'italic') { open = '_'; close = '_'; }
            else if (cmd === 'underline') { open = '__'; close = '__'; }
            else return;
            el.value = val.substring(0, start) + open + sel + close + val.substring(end);
            el.dispatchEvent(new Event('input', { bubbles: true }));
            el.focus();
        },

        isFocused: function (elementId) {
            var el = getEl(elementId);
            if (!el) return false;
            return document.activeElement === el || el.contains(document.activeElement);
        },

        showSpellMenuForSelection: function (elementId) {
            var el = getEl(elementId);
            if (!el) return;
            el.focus();
            var hit = getWordFromSelection(el);
            if (!hit) {
                renderSpellMenu([{ label: 'Click a word, then press abc or right-click', muted: true }], 'Spelling');
                var rect = el.getBoundingClientRect();
                showSpellMenuAt(rect.left + 24, rect.top + 24);
                return;
            }
            var sel = window.getSelection();
            var rect2 = sel && sel.rangeCount ? sel.getRangeAt(0).getBoundingClientRect() : el.getBoundingClientRect();
            openSpellMenuForWord(elementId, hit, rect2.left, rect2.bottom + 4);
        },

        dispose: function (elementId) {
            if (editors.has(elementId)) {
                var s = editors.get(elementId);
                if (s.spellTimer) clearTimeout(s.spellTimer);
                editors.delete(elementId);
            }
            // Allow a remounted immersion editor (same id) to re-init after Exit → Immersion again.
            var el = getEl(elementId);
            if (el) {
                try { delete el.dataset.jlRichInit; } catch (_) {
                    try { el.removeAttribute('data-jl-rich-init'); } catch (e2) { /* ignore */ }
                }
            }
        },

        setDisabled: function (elementId, disabled) {
            var el = getEl(elementId);
            if (!el) return;
            el.dataset.jlDisabled = disabled ? 'true' : 'false';
            el.contentEditable = disabled ? 'false' : 'true';
        },

        getHtml: function (elementId) {
            var el = getEl(elementId);
            return el ? (el.innerHTML || '') : '';
        },

        getPlainText: function (elementId) {
            var el = getEl(elementId);
            return el ? (el.innerText || '').replace(/\r\n/g, '\n') : '';
        },

        setHtml: function (elementId, html) {
            var el = getEl(elementId);
            if (!el) return;
            el.innerHTML = sanitizeEditorHtml(html);
            updatePlaceholder(el);
        },

        setHtmlIfNeeded: function (elementId, html) {
            var el = getEl(elementId);
            if (!el) return;
            var current = el.innerHTML || '';
            var next = sanitizeEditorHtml(html);
            if (current === next) return;
            var saved = saveSelection(el);
            el.innerHTML = next;
            updatePlaceholder(el);
            if (saved) restoreSelection(el, saved);
        },

        appendPlainText: function (elementId, text) {
            var el = getEl(elementId);
            if (!el || !text) return;
            el.focus();
            document.execCommand('insertText', false, text);
            handleInput(elementId);
        },

        focus: function (elementId) {
            var el = getEl(elementId);
            if (el) el.focus();
        },

        blur: function (elementId) {
            var el = getEl(elementId);
            if (!el) return;
            el.dataset.jlSpellMenuOpen = 'false';
            hideSpellMenu();
            el.blur();
            notifyDotNetBlur(elementId);
        },

        exec: function (elementId, command) {
            return execOn(elementId, command);
        },

        format: function (elementId, command) {
            return execOn(elementId, command);
        },

        bold: function (elementId) { return execOn(elementId, 'bold'); },
        italic: function (elementId) { return execOn(elementId, 'italic'); },
        strikeThrough: function (elementId) { return execOn(elementId, 'strikeThrough'); },
        insertUnorderedList: function (elementId) { return execOn(elementId, 'insertUnorderedList'); },
        undo: function (elementId) { return execOn(elementId, 'undo'); },
        redo: function (elementId) { return execOn(elementId, 'redo'); },
        cut: function (elementId) { return execOn(elementId, 'cut'); },
        copy: function (elementId) { return execOn(elementId, 'copy'); },
        paste: function (elementId) { return execOn(elementId, 'paste'); },
        selectAll: function (elementId) { return execOn(elementId, 'selectAll'); },

        toggleBullets: function (elementId) {
            return execOn(elementId, 'insertUnorderedList');
        },

        refreshSpellCheck: function (elementId) {
            return applySpellCheck(elementId);
        },

        setPlainText: function (elementId, text) {
            var el = getEl(elementId);
            if (!el) return;
            clearSpellMarks(el);
            var lines = (text || '').split(/\r?\n/);
            if (lines.length === 0 || (lines.length === 1 && !lines[0])) {
                el.innerHTML = '';
            } else {
                el.innerHTML = lines.map(function (line) {
                    var escaped = line
                        .replace(/&/g, '&amp;')
                        .replace(/</g, '&lt;')
                        .replace(/>/g, '&gt;');
                    return escaped.length ? '<div>' + escaped + '</div>' : '<div><br></div>';
                }).join('');
            }
            updatePlaceholder(el);
            dispatchInput(el);
            handleInput(elementId);
        }
    };

    function blurAllEditors() {
        blurAllEditorsExcept(null);
    }

    function blurAllEditorsExcept(exceptElementId) {
        editors.forEach(function (state, id) {
            if (exceptElementId && id === exceptElementId) return;
            var el = getEl(id);
            if (!el) return;
            if (document.activeElement === el || el.contains(document.activeElement)) {
                el.dataset.jlSpellMenuOpen = 'false';
                hideSpellMenu();
                el.blur();
                notifyDotNetBlur(id);
            }
        });
    }

    function isJlEditorArea(target) {
        return target && target.classList && target.classList.contains('jl-text-editor__area');
    }

    function isNativeFormField(target) {
        if (!target || !target.tagName) return false;
        var tag = target.tagName;
        if (tag === 'INPUT' || tag === 'SELECT' || tag === 'TEXTAREA') return true;
        return false;
    }

    if (!window.__jlEditorFocusBridge) {
        window.__jlEditorFocusBridge = true;
        document.addEventListener('focusin', function (e) {
            var target = e.target;
            if (!target) return;
            if (isJlEditorArea(target)) {
                blurAllEditorsExcept(target.id);
                return;
            }
            if (isNativeFormField(target)) {
                blurAllEditorsExcept(null);
            }
        }, true);
        document.addEventListener('pointerdown', function (e) {
            var target = e.target;
            if (!target) return;
            if (isNativeFormField(target)) {
                blurAllEditors();
                return;
            }
            if (isJlEditorArea(target)) {
                blurAllEditorsExcept(target.id);
            } else if (target.closest && target.closest('.jl-text-editor__area')) {
                var area = target.closest('.jl-text-editor__area');
                if (area && area.id) blurAllEditorsExcept(area.id);
            } else if (target.closest && target.closest('.jl-text-editor__toolbar')) {
                var host = target.closest('.jl-text-editor');
                var area = host ? host.querySelector('.jl-text-editor__area') : null;
                if (area && area.id) {
                    cacheSelection(area.id);
                    blurAllEditorsExcept(area.id);
                }
            }
        }, true);
    }

    window.jlTextEditor = window.jlTextEditor || {};
    window.jlTextEditor.blurAll = blurAllEditors;

    window.jlNativeField = {
        prepare: function (inputId) {
            blurAllEditors();
            if (!inputId) return;
            var input = getEl(inputId);
            if (!input) return;
            window.requestAnimationFrame(function () {
                try {
                    input.focus({ preventScroll: true });
                } catch (e) {
                    try { input.focus(); } catch (ignore) { }
                }
            });
        }
    };

    loadSpellDictionary();
})();