"use client";
import { jsx as _jsx } from "react/jsx-runtime";
import { useEditContextRef } from "../client/editContext";
import { useFieldModification } from "../client/fieldModificationStore";
import { useSlotContext } from "../views/editorSlotContext";
import { extractDOMSelectionContext, sanitizeSlateText, } from "../utils/selectionContext";
import { applyExpandedSelectionToContentEditable } from "../utils/expandSelectionAtCaret";
import { useDebouncedCallback } from "use-debounce";
import { useEffect, useRef, useState, useCallback } from "react";
import { Editor, Range } from "slate";
import { ReactSlate } from "./richtext";
// Minimal fallback profile for when profile is null or undefined
const FALLBACK_PROFILE = {
    toolbar: {
        groups: [
            {
                id: "basic-formatting",
                label: "Basic Formatting",
                display: "buttons",
                showIconsOnly: true,
                options: [],
            },
        ],
    },
};
export function RichTextEditorComponent({ field, readOnly, profile, updateFieldValue, showControls, }) {
    const editContextRef = useEditContextRef();
    const slotContext = useSlotContext();
    const [focused, setFocused] = useState(false);
    const [value, setValue] = useState(field.value);
    const editorRef = useRef(null);
    const slateRef = useRef(null);
    const suppressExternalUpdatesRef = useRef(false);
    const suppressTimerRef = useRef(null);
    const lastSentRichTextCaretRef = useRef("clear");
    const profileLoaded = !!profile;
    const previousProfileLoadedRef = useRef(profileLoaded);
    if (!editContextRef.current)
        return null;
    const fieldItem = field.descriptor.item;
    if (!fieldItem)
        return null;
    const handleEditorMouseDown = useCallback((event) => {
        const editableElement = editorRef.current?.querySelector(".slate-editable");
        const target = event.target;
        if (!(target instanceof Node) || !editableElement?.contains(target)) {
            return;
        }
        if (editContextRef.current?.selectedRange) {
            editContextRef.current?.setSelectedRange(undefined);
        }
    }, [editContextRef]);
    const captureSelectionRange = useCallback(() => {
        if (!editorRef.current) {
            return undefined;
        }
        const selection = window.getSelection();
        if (!selection || selection.rangeCount === 0) {
            return undefined;
        }
        const range = selection.getRangeAt(0);
        // Find the actual editable content area (not the wrapper that includes toolbar)
        const editableElement = editorRef.current.querySelector(".slate-editable");
        const contentContainer = editableElement || editorRef.current;
        // Check if the selection is within the editable content area
        if (!contentContainer.contains(range.commonAncestorContainer)) {
            return undefined;
        }
        // Get the raw selected text and sanitize it from Slate's invisible characters
        const rawSelectedText = range.toString();
        const selectedText = sanitizeSlateText(rawSelectedText);
        // Only track if there's actual selected text (not just cursor position)
        if (!selectedText) {
            const isStillFocused = editorRef.current?.contains(document.activeElement);
            // Clear selection range if there's no selected text
            // BUT: only if the focus is still within the editor.
            // If we lost focus to the AI dialog, we want to KEEP the selection range.
            if (isStillFocused &&
                editContextRef.current?.selectedRange?.fieldId === field.id) {
                editContextRef.current?.setSelectedRange(undefined);
            }
            return undefined;
        }
        // Calculate global offsets within the editable content area only
        const getGlobalTextOffset = (container, targetNode, localOffset) => {
            let offset = 0;
            const treeWalker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, null);
            let currentNode = treeWalker.nextNode();
            while (currentNode) {
                if (currentNode === targetNode) {
                    return offset + localOffset;
                }
                offset += (currentNode.textContent || "").length;
                currentNode = treeWalker.nextNode();
            }
            return offset;
        };
        const globalStartOffset = getGlobalTextOffset(contentContainer, range.startContainer, range.startOffset);
        const globalEndOffset = getGlobalTextOffset(contentContainer, range.endContainer, range.endOffset);
        // Extract plain text context from the editable content area only (excludes toolbar)
        const { contextBefore, contextAfter } = extractDOMSelectionContext(contentContainer, globalStartOffset, globalEndOffset);
        // Capture the Slate selection from the live DOM range. Slate's internal
        // selection can lag behind native selection events under load, so using it
        // first can turn a replacement into an insertion at a stale caret.
        let slateSelection = slateRef.current?.getSelectionFromDOMRange(range) ?? null;
        if (slateSelection && Range.isCollapsed(slateSelection)) {
            slateSelection = null;
        }
        if (!slateSelection) {
            const currentSlateSelection = slateRef.current?.getSelection() ?? null;
            slateSelection =
                currentSlateSelection && !Range.isCollapsed(currentSlateSelection)
                    ? currentSlateSelection
                    : null;
        }
        return {
            itemId: fieldItem.id,
            fieldId: field.id,
            language: fieldItem.language,
            version: fieldItem.version,
            startOffset: globalStartOffset,
            endOffset: globalEndOffset,
            text: selectedText,
            contextBefore,
            contextAfter,
            // Create a callback that uses the Slate API to replace text
            replaceText: slateSelection
                ? (newText) => {
                    slateRef.current?.replaceTextAtSelection(slateSelection, newText);
                }
                : undefined,
        };
    }, [
        editContextRef,
        field.id,
        fieldItem.id,
        fieldItem.language,
        fieldItem.version,
    ]);
    const publishSelectionRange = useCallback((selectionRange) => {
        if (!selectionRange)
            return;
        // Update selection if it has changed
        const currentRange = editContextRef.current?.selectedRange;
        if (selectionRange.text !== currentRange?.text ||
            selectionRange.startOffset !== currentRange?.startOffset ||
            selectionRange.endOffset !== currentRange?.endOffset ||
            (selectionRange.text &&
                selectionRange.fieldId !== currentRange?.fieldId) ||
            selectionRange.itemId !== currentRange?.itemId) {
            editContextRef.current?.setSelectedRange(selectionRange);
        }
    }, [editContextRef]);
    const clearRichTextCaretPresence = useCallback(() => {
        const editor = editContextRef.current;
        if (!editor || lastSentRichTextCaretRef.current === "clear")
            return;
        editor.sendSocketMessage({
            type: "caret-position",
            payload: { offset: null, source: "form-field" },
        });
        lastSentRichTextCaretRef.current = "clear";
    }, [editContextRef]);
    const publishRichTextCaretPresence = useCallback(() => {
        const editorContext = editContextRef.current;
        const slateEditor = slateRef.current?.getEditor();
        const slateSelection = slateRef.current?.getSelection();
        if (!editorContext ||
            !slateEditor ||
            !slateSelection ||
            !Range.isRange(slateSelection) ||
            !Range.isCollapsed(slateSelection) ||
            readOnly) {
            clearRichTextCaretPresence();
            return;
        }
        let offset = 0;
        try {
            offset = Editor.string(slateEditor, {
                anchor: Editor.start(slateEditor, []),
                focus: slateSelection.anchor,
            }).length;
        }
        catch {
            clearRichTextCaretPresence();
            return;
        }
        const nextKey = [
            field.id,
            fieldItem.id,
            fieldItem.language,
            fieldItem.version,
            offset,
        ].join(":");
        if (lastSentRichTextCaretRef.current === nextKey)
            return;
        editorContext.sendSocketMessage({
            type: "caret-position",
            payload: {
                fieldId: field.id,
                item: fieldItem,
                offset,
                value,
                source: "form-field",
            },
        });
        lastSentRichTextCaretRef.current = nextKey;
    }, [
        clearRichTextCaretPresence,
        editContextRef,
        field.id,
        fieldItem,
        readOnly,
        value,
    ]);
    // Handle selection change for AI text editing - track selection to editContext
    const handleSelectionChange = useDebouncedCallback(() => {
        publishSelectionRange(captureSelectionRange());
        publishRichTextCaretPresence();
    }, 50);
    const flushSelectionChange = useCallback(() => {
        handleSelectionChange();
        handleSelectionChange.flush();
    }, [handleSelectionChange]);
    // Set up selection change listener. The handler itself validates that the
    // native range is inside this editor; relying on React focus state here can
    // miss the selection event that happens during focus under load.
    useEffect(() => {
        document.addEventListener("selectionchange", handleSelectionChange);
        return () => {
            document.removeEventListener("selectionchange", handleSelectionChange);
        };
    }, [handleSelectionChange]);
    useEffect(() => clearRichTextCaretPresence, [clearRichTextCaretPresence]);
    useEffect(() => {
        if (readOnly) {
            clearRichTextCaretPresence();
        }
    }, [clearRichTextCaretPresence, readOnly]);
    // Field-specific subscription - only rerenders when THIS field changes
    const { modifiedField } = useFieldModification(field.id, fieldItem.id, fieldItem.language, fieldItem.version, slotContext?.fieldModificationStore);
    useEffect(() => {
        // Only update if the editor is not currently being edited by the user
        const isEditorActive = focused && editorRef.current?.contains(document.activeElement);
        // Avoid accepting external value changes briefly after local edits to prevent flicker
        if (suppressExternalUpdatesRef.current)
            return;
        const newValue = field.isHistoric
            ? field.value
            : (modifiedField?.value ?? field.value);
        if (!isEditorActive && newValue !== value) {
            setValue(newValue);
        }
    }, [field.value, field.isHistoric, modifiedField?.value, focused, value]);
    const setFieldValue = useCallback((value, capturedDescriptor) => {
        if (updateFieldValue) {
            // Use the updateFieldValue prop when provided (WYSIWYG mode).
            updateFieldValue(value);
            return;
        }
        // Use direct field editing when updateFieldValue is not provided (Editor mode).
        // Persistence remains debounced inside operations.editField.
        void editContextRef.current?.operations.editField({
            field: capturedDescriptor,
            value,
            refresh: "none",
            originatingSlotId: slotContext?.slotId,
        });
    }, [editContextRef, slotContext?.slotId, updateFieldValue]);
    const handleChange = (newValue) => {
        // Treat common HTML representations of "empty" as equivalent so we don't
        // immediately save when the editor normalizes an empty value on focus.
        const isEmptyHtml = (html) => {
            const normalized = (html ?? "").trim();
            return (normalized === "" ||
                normalized === "<p><br></p>" ||
                normalized === "<br>" ||
                normalized === "<p></p>");
        };
        const originalValue = typeof field.value === "string" ? field.value : "";
        // If both the stored value and the new value are semantically empty,
        // skip saving. This prevents an immediate empty save when the field
        // is first focused and Slate normalizes the content.
        if (isEmptyHtml(newValue) && isEmptyHtml(originalValue)) {
            return;
        }
        if (newValue !== value && !readOnly) {
            setValue(newValue);
            // Temporarily suppress external updates to avoid flicker from backend refreshes
            suppressExternalUpdatesRef.current = true;
            if (suppressTimerRef.current)
                clearTimeout(suppressTimerRef.current);
            const debounceMs = editContextRef.current?.configuration.debounceFieldEditsInterval ?? 300;
            const suppressMs = Math.max(1000, debounceMs + 500);
            suppressTimerRef.current = setTimeout(() => {
                suppressExternalUpdatesRef.current = false;
            }, suppressMs);
            // Capture the field descriptor at edit time to prevent saving to wrong language
            setFieldValue(newValue, field.descriptor);
        }
    };
    // Use the profile directly or fall back to the minimal profile
    const editorProfile = profile ?? FALLBACK_PROFILE;
    useEffect(() => {
        const wasProfileLoaded = previousProfileLoadedRef.current;
        previousProfileLoadedRef.current = profileLoaded;
        if (wasProfileLoaded || !profileLoaded || !focused || readOnly) {
            return;
        }
        window.setTimeout(() => {
            const editableElement = editorRef.current?.querySelector(".slate-editable");
            editableElement?.focus();
        }, 0);
    }, [focused, profileLoaded, readOnly]);
    // Handle keyboard shortcut (Ctrl/Cmd + .) to trigger AI text edit
    const handleKeyDown = useCallback((e) => {
        // Check for Ctrl+. or Cmd+.
        if ((e.ctrlKey || e.metaKey) && e.key === ".") {
            // Allow triggering even without selection - just need to be focused in this editor
            if (focused &&
                editContextRef.current?.focusedField?.fieldId === field.id) {
                e.preventDefault();
                e.stopPropagation();
                const editableElement = editorRef.current?.querySelector(".slate-editable");
                if (editableElement) {
                    applyExpandedSelectionToContentEditable(editableElement);
                }
                const selectionRange = captureSelectionRange();
                publishSelectionRange(selectionRange);
                // Dispatch custom event to trigger AI dialog
                document.dispatchEvent(new CustomEvent("inline-ai-open", {
                    detail: { selectionRange },
                }));
                return;
            }
        }
    }, [captureSelectionRange, focused, field.id, publishSelectionRange]);
    // const editorProfile = getDebugProfile(); // Debug profile with all options
    //console.log(editorProfile);
    return (_jsx("div", { ref: editorRef, onKeyDown: handleKeyDown, onMouseDown: handleEditorMouseDown, children: _jsx(ReactSlate, { ref: slateRef, value: value, onChange: handleChange, onFocus: () => {
                setFocused(true);
                window.setTimeout(publishRichTextCaretPresence, 0);
            }, onBlur: () => {
                flushSelectionChange();
                clearRichTextCaretPresence();
                setFocused(false);
            }, onMouseUp: () => {
                flushSelectionChange();
                publishRichTextCaretPresence();
            }, onKeyUp: () => {
                flushSelectionChange();
                publishRichTextCaretPresence();
            }, readOnly: readOnly, profile: editorProfile, showControls: showControls }, profile ? "profile-loaded" : "profile-fallback") }));
}
//# sourceMappingURL=RichTextEditorComponent.js.map