/**
 * Editor abstraction — wraps Quill / tiptap / TinyMCE behind a common
 * interface. The compose window loads this module and calls
 * createEditor() based on the user's setting.
 *
 * rmf-tiny is dynamic-imported (not static) so a missing/unreachable
 * adapter fails ONLY the TinyMCE branch — Quill and tiptap stay
 * working. A static top-of-file import would crash the whole module
 * if the browser couldn't resolve "@bobfrankston/rmf-tiny" via the
 * import map, and compose.ts (which imports createEditor from here)
 * would silently fail to attach handlers.
 */

export interface MailxEditor {
    setHtml(html: string): void;
    getHtml(): string;
    getText(): string;
    focus(): void;
    setCursor(pos: number): void;
    /** The editor's root editable element (for checking content) */
    root: HTMLElement;
    /** The scrollable container for positioning ghost text */
    getScrollContainer(): HTMLElement;
    /** Register a handler for content changes */
    onContentChange(handler: () => void): void;
    /** Register a keydown handler on the editor */
    onKeyDown(handler: (e: KeyboardEvent) => void): void;
    /** Insert plain text at the current cursor position */
    insertTextAtCursor(text: string): void;
}

// ── Quill ──

declare const Quill: any;

/** URL-ish test: accepts http(s)://, mailto:, tel:, and bare domains with a dot. */
function looksLikeUrl(s: string): boolean {
    const t = s.trim();
    if (!t) return false;
    if (/^(https?|mailto|tel):/i.test(t)) return true;
    // bare domain (e.g. "example.com/path") — require a dot and no internal whitespace
    return /^[\w-]+(\.[\w-]+)+(\/\S*)?$/.test(t);
}
function normalizeUrl(s: string): string {
    const t = s.trim();
    if (!t) return t;
    if (/^(https?|mailto|tel):/i.test(t)) return t;
    if (/^[\w.+-]+@[\w-]+(\.[\w-]+)+$/.test(t)) return `mailto:${t}`;
    return `https://${t}`;
}

/** Floating modal that edits both link text and URL. Returns null on Cancel,
 *  { text, url } on OK, or { text: "", url: "" } on "Remove link". */
function openLinkDialog(initialText: string, initialUrl: string): Promise<{ text: string; url: string; remove?: boolean } | null> {
    return new Promise(resolve => {
        const backdrop = document.createElement("div");
        backdrop.className = "mailx-modal-backdrop";
        const panel = document.createElement("div");
        panel.className = "mailx-modal";
        panel.innerHTML = `
            <div class="mailx-modal-title">Edit link</div>
            <label class="mailx-modal-label">Text
                <input type="text" class="mailx-modal-input" id="mailx-link-text">
            </label>
            <label class="mailx-modal-label">URL
                <input type="text" class="mailx-modal-input" id="mailx-link-url" spellcheck="false" autocomplete="off">
            </label>
            <div class="mailx-modal-buttons">
                <button type="button" class="mailx-modal-btn" data-action="remove">Remove link</button>
                <span class="mailx-modal-spacer"></span>
                <button type="button" class="mailx-modal-btn" data-action="cancel">Cancel</button>
                <button type="button" class="mailx-modal-btn mailx-modal-btn-primary" data-action="ok">OK</button>
            </div>`;
        backdrop.appendChild(panel);
        document.body.appendChild(backdrop);

        const textInput = panel.querySelector<HTMLInputElement>("#mailx-link-text")!;
        const urlInput = panel.querySelector<HTMLInputElement>("#mailx-link-url")!;
        textInput.value = initialText;
        urlInput.value = initialUrl;

        const close = (result: { text: string; url: string; remove?: boolean } | null) => {
            backdrop.remove();
            document.removeEventListener("keydown", onKey, true);
            resolve(result);
        };
        const commit = () => close({ text: textInput.value, url: normalizeUrl(urlInput.value) });
        const onKey = (e: KeyboardEvent) => {
            if (e.key === "Escape") { e.stopPropagation(); e.preventDefault(); close(null); }
            else if (e.key === "Enter") { e.stopPropagation(); e.preventDefault(); commit(); }
        };
        document.addEventListener("keydown", onKey, true);

        panel.querySelectorAll<HTMLButtonElement>(".mailx-modal-btn").forEach(btn => {
            btn.addEventListener("click", () => {
                const action = btn.dataset.action;
                if (action === "cancel") close(null);
                else if (action === "remove") close({ text: textInput.value, url: "", remove: true });
                else commit();
            });
        });
        backdrop.addEventListener("mousedown", (e) => { if (e.target === backdrop) close(null); });
        // Focus URL if we have text, else text first
        (initialText ? urlInput : textInput).focus();
        (initialText ? urlInput : textInput).select();
    });
}

function createQuillEditor(container: HTMLElement): MailxEditor {
    /** Open the link dialog for the current selection or cursor. If range has
     *  a selection, prefill text from the selected text; if cursor is inside
     *  a link, prefill both from the link's range. */
    const openLinkForRange = async (quill: any, range: any) => {
        if (!range) return;
        // Expand a bare cursor inside a link to the full link range
        let linkRange = range;
        const format = quill.getFormat(range);
        if (range.length === 0 && format.link) {
            // Walk left and right to find the link extent
            const [leaf, offset] = quill.getLeaf(range.index);
            if (leaf) {
                // Build a range that spans the whole link
                const text = quill.getText();
                let start = range.index, end = range.index;
                while (start > 0 && quill.getFormat(start - 1, 1).link === format.link) start--;
                while (end < text.length - 1 && quill.getFormat(end, 1).link === format.link) end++;
                linkRange = { index: start, length: end - start };
            }
        }
        const currentText = linkRange.length ? quill.getText(linkRange.index, linkRange.length).replace(/\n$/, "") : "";
        const currentUrl = format.link || "";
        const result = await openLinkDialog(currentText, currentUrl);
        if (!result) return;
        if (result.remove) {
            if (linkRange.length) quill.formatText(linkRange.index, linkRange.length, "link", false);
            return;
        }
        if (!result.url) return;
        const newText = result.text || result.url;
        if (linkRange.length) {
            // Replace the existing text+link with new text+link
            quill.deleteText(linkRange.index, linkRange.length);
            quill.insertText(linkRange.index, newText, { link: result.url });
            quill.setSelection(linkRange.index + newText.length, 0);
        } else {
            quill.insertText(linkRange.index, newText, { link: result.url });
            quill.setSelection(linkRange.index + newText.length, 0);
        }
    };

    // Extra keybindings for formatting that Quill doesn't wire up by default.
    // Ctrl+K (insert/edit link) is the one most users expect; we also add shortcuts
    // for strikethrough, lists, indent, color, and clear-formatting.
    const extraBindings: any = {
        insertLink: {
            key: "K", shortKey: true,
            handler: function (this: any, range: any) {
                openLinkForRange(this.quill, range);
            },
        },
        removeLink: {
            key: "K", shortKey: true, shiftKey: true,
            handler: function (this: any) { this.quill.format("link", false); },
        },
        strike: {
            key: "X", shortKey: true, shiftKey: true,
            handler: function (this: any, range: any) {
                if (!range) return true;
                const cur = this.quill.getFormat(range).strike;
                this.quill.format("strike", !cur);
            },
        },
        orderedList: {
            key: "7", shortKey: true, shiftKey: true,
            handler: function (this: any) { this.quill.format("list", "ordered"); },
        },
        bulletList: {
            key: "8", shortKey: true, shiftKey: true,
            handler: function (this: any) { this.quill.format("list", "bullet"); },
        },
        indent: {
            key: "]", shortKey: true,
            handler: function (this: any, range: any, context: any) {
                this.quill.format("indent", (context.format.indent || 0) + 1);
            },
        },
        outdent: {
            key: "[", shortKey: true,
            handler: function (this: any, range: any, context: any) {
                this.quill.format("indent", Math.max(0, (context.format.indent || 0) - 1));
            },
        },
        color: {
            key: "C", shortKey: true, shiftKey: true,
            handler: function (this: any, range: any) {
                if (!range) return true;
                const current = this.quill.getFormat(range).color || "";
                const color = prompt("Text color (name or #hex, blank to clear):", current);
                if (color === null) return;
                this.quill.format("color", color || false);
            },
        },
        clearFormat: {
            key: "\\", shortKey: true,
            handler: function (this: any, range: any) {
                if (!range) return true;
                this.quill.removeFormat(range.index, range.length || 0);
            },
        },
    };

    const q = new Quill(container, {
        theme: "snow",
        placeholder: "Write your message...",
        modules: {
            toolbar: [
                [{ font: [] }, { size: ["small", false, "large", "huge"] }],
                [{ header: [1, 2, 3, false] }],
                ["bold", "italic", "underline", "strike"],
                [{ color: [] }, { background: [] }],
                [{ list: "ordered" }, { list: "bullet" }],
                [{ align: [] }],
                ["blockquote", "link", "image"],
                ["clean"]
            ],
            keyboard: { bindings: extraBindings },
        }
    });

    // Make toolbar buttons non-tabbable so Tab goes straight to editor body
    document.querySelectorAll(".ql-toolbar button, .ql-toolbar select, .ql-toolbar .ql-picker-label").forEach(
        el => (el as HTMLElement).setAttribute("tabindex", "-1")
    );

    // Native spell-check: WebView2 / Chromium underlines misspellings and the
    // right-click menu offers "Add to dictionary". Quill clears spellcheck on
    // its root by default AND may re-clear it asynchronously as part of its
    // setup (user-reported "spell-check broken again" with the one-shot set).
    // Set once now, again after the frame flushes so post-init clears can't
    // win, and install a MutationObserver to re-assert if anything later
    // strips the attribute. Cheap — fires at most on each clear.
    const applySpellcheck = () => {
        if (q.root.getAttribute("spellcheck") !== "true") q.root.setAttribute("spellcheck", "true");
        if (q.root.getAttribute("autocorrect") !== "on") q.root.setAttribute("autocorrect", "on");
        if (q.root.getAttribute("autocapitalize") !== "on") q.root.setAttribute("autocapitalize", "on");
    };
    applySpellcheck();
    requestAnimationFrame(applySpellcheck);
    setTimeout(applySpellcheck, 100);
    const spellObs = new MutationObserver(() => applySpellcheck());
    spellObs.observe(q.root, { attributes: true, attributeFilter: ["spellcheck", "autocorrect", "autocapitalize"] });

    // Toolbar link button: open our modal instead of Quill's built-in URL prompt.
    const toolbar = q.getModule("toolbar");
    toolbar?.addHandler("link", function (this: any) {
        openLinkForRange(q, q.getSelection() || { index: q.getLength() - 1, length: 0 });
    });

    // Paste handling:
    // - text/html clipboard with exactly one anchor (the common "copy a link
    //   with anchor text from a webpage" case): take it over from Quill —
    //   Quill's clipboard module was producing duplicates ("click here" as
    //   text PLUS a separate "https://example.com" as a link tail). Insert
    //   the anchor's text content as a single linked run.
    // - text/html with richer content: defer to Quill (preserves formatting).
    // - text/plain that's a URL: insert as a link, optionally wrapping any
    //   currently-selected text.
    // - Anything else: default Quill behavior (verbatim plain or HTML).
    // C36: AI proofread on right-click → "Proofread selection" item.
    // Uses the existing aiTransform IPC. Gated by autocomplete.proofreadEnabled.
    q.root.addEventListener("contextmenu", async (e: MouseEvent) => {
        try {
            const sel = q.getSelection();
            if (!sel || sel.length === 0) return;  // no selection — let native menu handle
            const settingsRaw = localStorage.getItem("mailx-ai-proofread-enabled");
            if (settingsRaw !== "true") return;  // feature off
            const text = q.getText(sel.index, sel.length);
            if (!text.trim()) return;
            e.preventDefault();
            // Inline action menu next to the cursor
            const menu = document.createElement("div");
            menu.style.cssText = `position:fixed;z-index:2500;background:var(--color-bg);color:var(--color-text);border:1px solid var(--color-border);border-radius:6px;box-shadow:0 4px 16px rgba(0,0,0,0.2);padding:4px 0;font-size:13px;min-width:160px;`;
            menu.style.left = `${e.clientX}px`;
            menu.style.top = `${e.clientY}px`;
            const item = document.createElement("div");
            item.textContent = "Proofread selection";
            item.style.cssText = `padding:6px 12px;cursor:pointer;`;
            item.addEventListener("mouseenter", () => item.style.background = "var(--color-bg-hover)");
            item.addEventListener("mouseleave", () => item.style.background = "");
            item.addEventListener("click", async () => {
                menu.remove();
                try {
                    const { aiTransform } = await import("../lib/api-client.js");
                    const r = await aiTransform({ action: "proofread", text });
                    if (r?.text && r.text !== text) {
                        q.deleteText(sel.index, sel.length);
                        q.insertText(sel.index, r.text);
                        q.setSelection(sel.index, r.text.length);
                    } else if (r?.reason) {
                        alert(`Proofread: ${r.reason}`);
                    }
                } catch (err: any) {
                    alert(`Proofread failed: ${err?.message || err}`);
                }
            });
            menu.appendChild(item);
            document.body.appendChild(menu);
            const dismiss = () => { menu.remove(); document.removeEventListener("mousedown", dismiss); };
            setTimeout(() => document.addEventListener("mousedown", dismiss), 0);
        } catch { /* fall through to native menu */ }
    });

    // IMPORTANT: register on the capture phase AND use stopImmediatePropagation
    // when we handle the paste ourselves. Quill 2.x's clipboard module attaches
    // its own listener on the same root element; preventDefault only stops the
    // browser's default contenteditable insertion, NOT Quill's parallel listener.
    // Without stopImmediatePropagation the two handlers fire independently and
    // both insert — user sees the URL twice. Capture phase guarantees we run
    // before Quill so stopImmediatePropagation actually blocks it.
    q.root.addEventListener("paste", (e: ClipboardEvent) => {
        const cb = e.clipboardData;
        if (!cb) return;

        // Helper: call when we've handled the paste ourselves. Stops Quill's
        // own listener from also processing the same event.
        const consume = () => {
            e.preventDefault();
            e.stopImmediatePropagation();
        };

        // Q3: image-on-clipboard → inline as data: URL.
        for (const item of Array.from(cb.items)) {
            if (item.kind === "file" && item.type.startsWith("image/")) {
                const file = item.getAsFile();
                if (!file) continue;
                consume();
                const reader = new FileReader();
                reader.onload = () => {
                    const dataUrl = String(reader.result || "");
                    const range = q.getSelection(true) || { index: q.getLength(), length: 0 };
                    q.insertEmbed(range.index, "image", dataUrl);
                    q.setSelection(range.index + 1, 0);
                };
                reader.readAsDataURL(file);
                return;
            }
        }

        const html = cb.getData("text/html");
        const plain = cb.getData("text/plain");

        if (html) {
            // Detect "single anchor" clipboard — copy from a browser usually
            // produces something like:
            //   <meta charset='utf-8'><a href="https://example.com">click here</a>
            // or wrapped in <html><body>. Parse and check.
            try {
                const tmp = document.createElement("div");
                tmp.innerHTML = html;
                // Strip script/style, then unwrap <html>/<body> noise.
                const root = tmp.querySelector("body") || tmp;
                // Walk for the only meaningful element
                const meaningful = Array.from(root.childNodes).filter(n => {
                    if (n.nodeType === Node.TEXT_NODE) return (n.textContent || "").trim().length > 0;
                    if (n.nodeType === Node.ELEMENT_NODE) {
                        const tag = (n as Element).tagName.toLowerCase();
                        return tag !== "meta" && tag !== "style" && tag !== "script";
                    }
                    return false;
                });
                if (meaningful.length === 1 && (meaningful[0] as HTMLElement).tagName?.toLowerCase() === "a") {
                    const a = meaningful[0] as HTMLAnchorElement;
                    const href = a.getAttribute("href") || "";
                    const text = (a.textContent || "").trim();
                    if (href && text) {
                        consume();
                        const range = q.getSelection(true);
                        if (!range) return;
                        if (range.length > 0) {
                            // Selected text exists — replace with the linked anchor text
                            q.deleteText(range.index, range.length);
                        }
                        q.insertText(range.index, text, { link: href });
                        q.setSelection(range.index + text.length, 0);
                        return;
                    }
                }
                // Single text-node wrapping the URL — common when copying from
                // browser address bar (Chrome ships text/html as
                // `<meta><span>URL</span>` alongside text/plain). Fall through
                // to the plain-URL path below instead of letting Quill insert
                // the bare URL text AND our handler insert it linked — which
                // is exactly the double-paste the user reported.
                const textOnly = (root.textContent || "").trim();
                if (textOnly && looksLikeUrl(textOnly) && !root.querySelector("a")) {
                    consume();
                    const range = q.getSelection(true);
                    if (!range) return;
                    const url = normalizeUrl(textOnly);
                    if (range.length > 0) {
                        q.formatText(range.index, range.length, "link", url);
                        q.setSelection(range.index + range.length, 0);
                    } else {
                        q.insertText(range.index, textOnly, { link: url });
                        q.setSelection(range.index + textOnly.length, 0);
                    }
                    return;
                }
            } catch { /* fall through to Quill default */ }
            return; // Quill handles richer HTML clipboard (no consume → Quill runs)
        }

        if (plain && looksLikeUrl(plain)) {
            consume();
            const range = q.getSelection(true);
            if (!range) return;
            const url = normalizeUrl(plain);
            if (range.length > 0) {
                // Preserve existing selection text, just format it as a link
                q.formatText(range.index, range.length, "link", url);
                q.setSelection(range.index + range.length, 0);
            } else {
                q.insertText(range.index, plain.trim(), { link: url });
                q.setSelection(range.index + plain.trim().length, 0);
            }
        }
    }, true);  // capture=true — run before Quill's own paste listener

    // Hover preview: show the target URL in a floating tooltip when the
    // pointer is over a link. Built on top of native mouseover/mouseout
    // rather than Quill's ql-tooltip (which is keyboard-triggered).
    let hoverTip: HTMLElement | null = null;
    q.root.addEventListener("mouseover", (e: MouseEvent) => {
        const a = (e.target as HTMLElement).closest("a[href]") as HTMLAnchorElement | null;
        if (!a) return;
        if (hoverTip) hoverTip.remove();
        hoverTip = document.createElement("div");
        hoverTip.className = "mailx-link-hover";
        hoverTip.textContent = a.getAttribute("href") || "";
        document.body.appendChild(hoverTip);
        const rect = a.getBoundingClientRect();
        hoverTip.style.left = `${Math.max(8, rect.left)}px`;
        hoverTip.style.top = `${rect.bottom + 4}px`;
    });
    q.root.addEventListener("mouseout", (e: MouseEvent) => {
        const to = e.relatedTarget as HTMLElement | null;
        if (to && to.closest("a[href]")) return;
        if (hoverTip) { hoverTip.remove(); hoverTip = null; }
    });

    return {
        setHtml(html: string): void {
            q.clipboard.dangerouslyPasteHTML(html);
        },
        getHtml(): string {
            return q.root.innerHTML;
        },
        getText(): string {
            return q.getText();
        },
        focus(): void {
            q.focus();
        },
        setCursor(pos: number): void {
            q.setSelection(pos, 0);
        },
        root: q.root,
        getScrollContainer(): HTMLElement {
            return q.root;
        },
        onContentChange(handler: () => void): void {
            q.on("text-change", handler);
        },
        onKeyDown(handler: (e: KeyboardEvent) => void): void {
            q.root.addEventListener("keydown", handler);
        },
        insertTextAtCursor(text: string): void {
            const sel = q.getSelection();
            if (sel) q.insertText(sel.index, text);
        }
    };
}

// ── tiptap ──

async function createTiptapEditor(container: HTMLElement): Promise<MailxEditor> {
    // tiptap loaded via CDN — use global UMD bundles
    const { Editor } = (window as any).tiptapCore;
    const { StarterKit } = (window as any).tiptapStarterKit;
    const { Link } = (window as any).tiptapExtensionLink;
    const { Image } = (window as any).tiptapExtensionImage;
    const { Underline } = (window as any).tiptapExtensionUnderline;
    const { Placeholder } = (window as any).tiptapExtensionPlaceholder;

    // Build toolbar
    const toolbar = document.createElement("div");
    toolbar.className = "tt-toolbar";
    toolbar.innerHTML = `
        <select class="tt-heading" tabindex="-1">
            <option value="p">Normal</option>
            <option value="1">Heading 1</option>
            <option value="2">Heading 2</option>
            <option value="3">Heading 3</option>
        </select>
        <button class="tt-btn" data-cmd="bold" title="Bold" tabindex="-1"><b>B</b></button>
        <button class="tt-btn" data-cmd="italic" title="Italic" tabindex="-1"><i>I</i></button>
        <button class="tt-btn" data-cmd="underline" title="Underline" tabindex="-1"><u>U</u></button>
        <button class="tt-btn" data-cmd="strike" title="Strikethrough" tabindex="-1"><s>S</s></button>
        <button class="tt-btn" data-cmd="bulletList" title="Bullet list" tabindex="-1">&#8226;</button>
        <button class="tt-btn" data-cmd="orderedList" title="Ordered list" tabindex="-1">1.</button>
        <button class="tt-btn" data-cmd="blockquote" title="Blockquote" tabindex="-1">&ldquo;</button>
        <button class="tt-btn" data-cmd="link" title="Link" tabindex="-1">&#128279;</button>
        <button class="tt-btn" data-cmd="clearFormat" title="Clear formatting" tabindex="-1">&#8999;</button>
    `;

    // Content area
    const content = document.createElement("div");
    content.className = "tt-content";

    container.appendChild(toolbar);
    container.appendChild(content);

    const ed = new Editor({
        element: content,
        extensions: [
            StarterKit,
            Link.configure({ openOnClick: false }),
            Image,
            Underline,
            Placeholder.configure({ placeholder: "Write your message..." }),
        ],
        content: "",
    });

    // Wire toolbar buttons
    toolbar.querySelectorAll(".tt-btn").forEach((btn: Element) => {
        btn.addEventListener("mousedown", (e) => {
            e.preventDefault();
            const cmd = (btn as HTMLElement).dataset.cmd;
            switch (cmd) {
                case "bold": ed.chain().focus().toggleBold().run(); break;
                case "italic": ed.chain().focus().toggleItalic().run(); break;
                case "underline": ed.chain().focus().toggleUnderline().run(); break;
                case "strike": ed.chain().focus().toggleStrike().run(); break;
                case "bulletList": ed.chain().focus().toggleBulletList().run(); break;
                case "orderedList": ed.chain().focus().toggleOrderedList().run(); break;
                case "blockquote": ed.chain().focus().toggleBlockquote().run(); break;
                case "link": {
                    const url = prompt("URL:");
                    if (url) ed.chain().focus().setLink({ href: url }).run();
                    break;
                }
                case "clearFormat": ed.chain().focus().clearNodes().unsetAllMarks().run(); break;
            }
        });
    });

    // Wire heading select
    const headingSelect = toolbar.querySelector(".tt-heading") as HTMLSelectElement;
    headingSelect?.addEventListener("change", () => {
        const val = headingSelect.value;
        if (val === "p") ed.chain().focus().setParagraph().run();
        else ed.chain().focus().toggleHeading({ level: parseInt(val) as 1 | 2 | 3 }).run();
    });

    // Keyboard shortcuts — Quill wires these via Modules; tiptap needs them
    // hooked manually. Match the Quill bindings (Ctrl+Shift+7/8 for lists,
    // Ctrl+K for link, Ctrl+Shift+K to unlink, Ctrl+Shift+X strike, Ctrl+\
    // clear). Listen on the editor's content element so we don't intercept
    // typing in the To/Cc/Bcc fields.
    content.addEventListener("keydown", (e: KeyboardEvent) => {
        const mod = e.ctrlKey || e.metaKey;
        if (!mod) return;
        const k = e.key.toLowerCase();
        if (e.shiftKey && k === "7") { e.preventDefault(); ed.chain().focus().toggleOrderedList().run(); return; }
        if (e.shiftKey && k === "8") { e.preventDefault(); ed.chain().focus().toggleBulletList().run(); return; }
        if (e.shiftKey && k === "x") { e.preventDefault(); ed.chain().focus().toggleStrike().run(); return; }
        if (e.shiftKey && k === "k") { e.preventDefault(); ed.chain().focus().unsetLink().run(); return; }
        if (!e.shiftKey && k === "k") {
            e.preventDefault();
            const url = prompt("URL:");
            if (url) ed.chain().focus().setLink({ href: url }).run();
            return;
        }
        if (k === "\\") { e.preventDefault(); ed.chain().focus().clearNodes().unsetAllMarks().run(); return; }
    });

    const editorEl = content.querySelector(".tiptap") as HTMLElement || content;

    return {
        setHtml(html: string): void {
            ed.commands.setContent(html);
        },
        getHtml(): string {
            return ed.getHTML();
        },
        getText(): string {
            return ed.getText();
        },
        focus(): void {
            ed.commands.focus("start");
        },
        setCursor(pos: number): void {
            ed.commands.focus("start");
        },
        root: editorEl,
        getScrollContainer(): HTMLElement {
            return editorEl;
        },
        onContentChange(handler: () => void): void {
            ed.on("update", handler);
        },
        onKeyDown(handler: (e: KeyboardEvent) => void): void {
            editorEl.addEventListener("keydown", handler);
        },
        insertTextAtCursor(text: string): void {
            ed.commands.insertContent(text);
        }
    };
}

// ── Factory ──

export type EditorType = "quill" | "tiptap" | "tinymce";

export async function createEditor(container: HTMLElement, type: EditorType): Promise<MailxEditor> {
    if (type === "tiptap") {
        return createTiptapEditor(container);
    }
    if (type === "tinymce") {
        return createTinyMceEditor(container);
    }
    return createQuillEditor(container);
}

/** Default jsDelivr URL — works without an API key. Users can override
 *  in Settings if they want Tiny Cloud or a self-hosted bundle. */
// Local bundled TinyMCE — copied from node_modules/tinymce/ into
// client/lib/tinymce/ by build-tinymce.js. Loaded with no internet
// dependency at runtime. Relative to compose.html (`client/compose/`)
// so it works under both msger custom protocol and Android file:// .
// Users wanting Tiny Cloud premium override this via the
// `mailx-tinymce-cdn` localStorage entry surfaced in Settings.
const DEFAULT_TINYMCE_CDN = "../lib/tinymce/tinymce.min.js";

/** TinyMCE branch — dynamic-imports the rmf-tiny adapter so a missing
 *  module only affects this branch. Throws on failure so the outer
 *  compose.ts fallback can take over (and properly load Quill's
 *  assets first). Don't fall back to Quill inline here — Quill's
 *  global isn't loaded when type === "tinymce" was requested, so
 *  `new Quill()` throws "Quill is not defined" and masks the real
 *  rmf-tiny error in the log. */
async function createTinyMceEditor(container: HTMLElement): Promise<MailxEditor> {
    let cdnUrl = DEFAULT_TINYMCE_CDN;
    let apiKey: string | undefined;
    try {
        cdnUrl = localStorage.getItem("mailx-tinymce-cdn") || DEFAULT_TINYMCE_CDN;
        apiKey = localStorage.getItem("mailx-tinymce-apikey") || undefined;
    } catch { /* private mode — use defaults */ }
    // Build step copies node_modules/@bobfrankston/rmf-tiny/src/adapter.js
    // → client/lib/rmf-tiny.js. Bare-name `@bobfrankston/rmf-tiny` resolution
    // would target `node_modules/`, which msger's custom protocol can't reach
    // (outside contentDir=client/). The relative path stays inside the
    // served root.
    const m = (await import("../lib/rmf-tiny.js" as any)) as {
        createTinyMceEditor(container: HTMLElement, opts: { cdnUrl?: string; apiKey?: string }): Promise<MailxEditor>;
    };
    const ed = await m.createTinyMceEditor(container, { cdnUrl, apiKey });
    return ed as unknown as MailxEditor;
}
