"use client";

import {
  forwardRef,
  useCallback,
  useEffect,
  useImperativeHandle,
  useMemo,
  useRef,
  useState,
  type CompositionEvent,
  type KeyboardEvent,
  type MouseEvent as ReactMouseEvent,
  type ReactNode,
  type SyntheticEvent,
} from "react";
import { createPortal } from "react-dom";
import {
  CheckSquareOutlined,
  CopyOutlined,
  FolderAddOutlined,
  ScissorOutlined,
} from "@ant-design/icons";

export type ComposerPromptToken =
  | {
      type: "text";
      text: string;
    }
  | {
      type: "reference";
      id: string;
      mediaType: "image" | "video" | "audio";
      label: string;
      sourceNodeId: string;
      url: string;
      thumbnailUrl?: string;
      duration?: number;
    };

export interface ComposerMentionReferenceItem {
  key: string;
  label: string;
  value: string;
  image?: string;
  sourceNodeId?: string;
  referenceType?: "image" | "video" | "audio";
  url?: string;
  thumbnailUrl?: string;
  duration?: number;
}

interface ComposerMentionInputProps {
  value: string;
  readOnly?: boolean;
  placeholder?: string;
  minHeightPx: number;
  maxHeightPx: number;
  className?: string;
  onChange: (value: string, tokens: ComposerPromptToken[]) => void;
  onFocus?: () => void;
  onClick?: (element: HTMLDivElement) => void;
  onKeyUp?: (event: KeyboardEvent<HTMLDivElement>) => void;
  onKeyDown?: (event: KeyboardEvent<HTMLDivElement>) => void;
  onCompositionStart?: (event: CompositionEvent<HTMLDivElement>) => void;
  onCompositionEnd?: (event: CompositionEvent<HTMLDivElement>) => void;
  referenceItems?: ComposerMentionReferenceItem[];
  referenceMenuLabel?: string;
  onReferenceSelect?: (item: ComposerMentionReferenceItem) => void;
}

type TextContextMenuAction = "copy" | "cut" | "paste" | "selectAll";

type TextContextMenuState = {
  position: { x: number; y: number };
  canCopy: boolean;
  canCut: boolean;
  canPaste: boolean;
};

type TextContextMenuItem = {
  key: TextContextMenuAction;
  label: string;
  icon: ReactNode;
  disabled?: boolean;
};

function textToTokens(text: string): ComposerPromptToken[] {
  return text.length > 0 ? [{ type: "text", text }] : [];
}

function createReferenceToken(item: ComposerMentionReferenceItem): ComposerPromptToken | null {
  if (!item.sourceNodeId || !item.referenceType || !item.url) return null;
  return {
    type: "reference",
    id: item.key,
    mediaType: item.referenceType,
    label: item.value.trim(),
    sourceNodeId: item.sourceNodeId,
    url: item.url,
    ...(item.thumbnailUrl ? { thumbnailUrl: item.thumbnailUrl } : {}),
    ...(typeof item.duration === "number" && item.duration > 0 ? { duration: item.duration } : {}),
  };
}

function getPromptTextFromTokens(tokens: ComposerPromptToken[]): string {
  return tokens.map((token) => token.type === "reference" ? token.label : token.text).join("");
}

function getReferenceItemLabel(item: ComposerMentionReferenceItem): string {
  return item.value.trim();
}

function appendTextToken(tokens: ComposerPromptToken[], text: string): void {
  if (!text) return;
  const previous = tokens[tokens.length - 1];
  if (previous?.type === "text") {
    previous.text += text;
    return;
  }
  tokens.push({ type: "text", text });
}

function parseReferenceTag(element: HTMLElement): ComposerPromptToken | null {
  const mediaType = element.dataset.referenceMediaType;
  const label = element.dataset.referenceLabel;
  const sourceNodeId = element.dataset.referenceSourceNodeId;
  const url = element.dataset.referenceUrl;
  if (
    mediaType !== "image" &&
    mediaType !== "video" &&
    mediaType !== "audio"
  ) {
    return null;
  }
  if (!label || !sourceNodeId || !url) return null;
  const duration = Number(element.dataset.referenceDuration);
  return {
    type: "reference",
    id: element.dataset.referenceId || sourceNodeId,
    mediaType,
    label,
    sourceNodeId,
    url,
    ...(element.dataset.referenceThumbnailUrl ? { thumbnailUrl: element.dataset.referenceThumbnailUrl } : {}),
    ...(Number.isFinite(duration) && duration > 0 ? { duration } : {}),
  };
}

function parseTokensFromDom(element: HTMLDivElement): ComposerPromptToken[] {
  const tokens: ComposerPromptToken[] = [];

  element.childNodes.forEach((child) => {
    if (child.nodeType === Node.TEXT_NODE) {
      appendTextToken(tokens, child.textContent ?? "");
      return;
    }
    if (!(child instanceof HTMLElement)) {
      appendTextToken(tokens, child.textContent ?? "");
      return;
    }
    if (child.dataset.composerReferenceTag === "true") {
      const referenceToken = parseReferenceTag(child);
      if (referenceToken) {
        tokens.push(referenceToken);
        return;
      }
    }
    appendTextToken(tokens, child.textContent ?? "");
  });

  return tokens;
}

function createReferenceTagElement(item: ComposerMentionReferenceItem): HTMLElement | null {
  const token = createReferenceToken(item);
  if (!token || token.type !== "reference") return null;

  const tag = document.createElement("span");
  tag.contentEditable = "false";
  tag.setAttribute("contenteditable", "false");
  tag.dataset.composerReferenceTag = "true";
  tag.dataset.referenceId = token.id;
  tag.dataset.referenceMediaType = token.mediaType;
  tag.dataset.referenceLabel = token.label;
  tag.dataset.referenceSourceNodeId = token.sourceNodeId;
  tag.dataset.referenceUrl = token.url;
  if (token.thumbnailUrl) tag.dataset.referenceThumbnailUrl = token.thumbnailUrl;
  if (token.duration) tag.dataset.referenceDuration = String(token.duration);
  tag.className = "mx-0.5 inline-flex h-5 max-w-[12rem] select-none items-center gap-1 rounded-md border border-[var(--border-subtle)] bg-[var(--surface-1)] px-1.5 align-middle text-xs leading-none text-[var(--text-primary)]";

  if (token.thumbnailUrl) {
    const thumbnail = document.createElement("img");
    thumbnail.src = token.thumbnailUrl;
    thumbnail.alt = "";
    thumbnail.draggable = false;
    thumbnail.className = "h-4 w-4 shrink-0 rounded-[3px] object-cover";
    tag.appendChild(thumbnail);
  }

  const label = document.createElement("span");
  label.contentEditable = "false";
  label.setAttribute("contenteditable", "false");
  label.className = "min-w-0 truncate";
  label.textContent = token.label;
  tag.appendChild(label);
  return tag;
}

function buildPromptDomNodes(
  value: string,
  referenceItems: ComposerMentionReferenceItem[]
): Node[] {
  const nodes: Node[] = [];
  const sortedReferenceItems = referenceItems
    .filter((item) => getReferenceItemLabel(item).length > 0)
    .sort((a, b) => getReferenceItemLabel(b).length - getReferenceItemLabel(a).length);
  let cursor = 0;

  while (cursor < value.length) {
    let matchedItem: ComposerMentionReferenceItem | null = null;
    for (const item of sortedReferenceItems) {
      const label = getReferenceItemLabel(item);
      if (value.startsWith(label, cursor)) {
        matchedItem = item;
        break;
      }
    }

    if (matchedItem) {
      const tag = createReferenceTagElement(matchedItem);
      if (tag) {
        nodes.push(tag);
        cursor += getReferenceItemLabel(matchedItem).length;
        continue;
      }
    }

    const nextMatchIndex = sortedReferenceItems.reduce((nextIndex, item) => {
      const index = value.indexOf(getReferenceItemLabel(item), cursor + 1);
      if (index === -1) return nextIndex;
      return nextIndex === -1 ? index : Math.min(nextIndex, index);
    }, -1);
    const end = nextMatchIndex === -1 ? value.length : nextMatchIndex;
    nodes.push(document.createTextNode(value.slice(cursor, end)));
    cursor = end;
  }

  return nodes.length > 0 ? nodes : [document.createTextNode("")];
}

function renderPromptValue(
  element: HTMLDivElement,
  value: string,
  referenceItems: ComposerMentionReferenceItem[]
): void {
  element.replaceChildren(...buildPromptDomNodes(value, referenceItems));
}

function isReferenceTagNode(node: Node | null): node is HTMLElement {
  return node instanceof HTMLElement && node.dataset.composerReferenceTag === "true";
}

function getChildNodeIndex(element: HTMLDivElement, node: Node): number {
  return Array.prototype.indexOf.call(element.childNodes, node);
}

function setCaretAtRemovedNodePosition(element: HTMLDivElement, removedNodeIndex: number): void {
  const selection = window.getSelection();
  if (!selection) return;

  const nextNode = element.childNodes[removedNodeIndex] ?? null;
  const previousNode = element.childNodes[removedNodeIndex - 1] ?? null;
  const nextRange = document.createRange();

  if (nextNode?.nodeType === Node.TEXT_NODE) {
    nextRange.setStart(nextNode, 0);
  } else if (previousNode?.nodeType === Node.TEXT_NODE) {
    nextRange.setStart(previousNode, previousNode.textContent?.length ?? 0);
  } else {
    nextRange.setStart(element, Math.min(removedNodeIndex, element.childNodes.length));
  }

  nextRange.collapse(true);
  selection.removeAllRanges();
  selection.addRange(nextRange);
}

function removeAdjacentReferenceTag(element: HTMLDivElement, direction: "backward" | "forward"): boolean {
  const selection = window.getSelection();
  if (!selection || selection.rangeCount === 0 || !selection.isCollapsed) return false;
  const range = selection.getRangeAt(0);
  if (!element.contains(range.startContainer)) return false;

  let candidate: Node | null = null;
  if (isReferenceTagNode(range.startContainer)) {
    candidate = range.startContainer;
  } else if (range.startContainer.nodeType === Node.TEXT_NODE) {
    const textNode = range.startContainer;
    if (direction === "backward" && range.startOffset === 0) {
      candidate = textNode.previousSibling;
    }
    if (direction === "forward" && range.startOffset === (textNode.textContent?.length ?? 0)) {
      candidate = textNode.nextSibling;
    }
  } else if (range.startContainer === element) {
    candidate = direction === "backward"
      ? element.childNodes[range.startOffset - 1] ?? null
      : element.childNodes[range.startOffset] ?? null;
  }

  if (!isReferenceTagNode(candidate)) return false;
  const candidateIndex = getChildNodeIndex(element, candidate);
  if (candidateIndex < 0) return false;
  candidate.remove();
  setCaretAtRemovedNodePosition(element, candidateIndex);
  return true;
}

function getContentEditableCaretOffset(element: HTMLDivElement): number {
  const selection = window.getSelection();
  if (!selection || selection.rangeCount === 0) return element.textContent?.length ?? 0;
  const range = selection.getRangeAt(0);
  if (!element.contains(range.endContainer)) return element.textContent?.length ?? 0;

  const beforeRange = range.cloneRange();
  beforeRange.selectNodeContents(element);
  beforeRange.setEnd(range.endContainer, range.endOffset);
  return beforeRange.toString().length;
}

function setContentEditableCaretOffset(element: HTMLDivElement, offset: number): void {
  const selection = window.getSelection();
  if (!selection) return;
  const safeOffset = Math.max(0, Math.min(offset, element.textContent?.length ?? 0));
  const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
  let remaining = safeOffset;
  let current = walker.nextNode();
  const range = document.createRange();

  while (current) {
    const textLength = current.textContent?.length ?? 0;
    if (remaining <= textLength) {
      range.setStart(current, remaining);
      range.collapse(true);
      selection.removeAllRanges();
      selection.addRange(range);
      return;
    }
    remaining -= textLength;
    current = walker.nextNode();
  }

  range.setStart(element, element.childNodes.length);
  range.collapse(true);
  selection.removeAllRanges();
  selection.addRange(range);
}

function selectionBelongsToElement(selection: Selection | null, element: HTMLDivElement): boolean {
  if (!selection || selection.rangeCount === 0) return false;
  const range = selection.getRangeAt(0);
  return element.contains(range.startContainer) && element.contains(range.endContainer);
}

function getSelectedTextInElement(element: HTMLDivElement): string {
  const selection = window.getSelection();
  if (!selection || !selectionBelongsToElement(selection, element) || selection.isCollapsed) return "";
  return selection.toString();
}

function selectElementContents(element: HTMLDivElement): void {
  const selection = window.getSelection();
  if (!selection) return;
  const range = document.createRange();
  range.selectNodeContents(element);
  selection.removeAllRanges();
  selection.addRange(range);
}

function insertPlainTextAtSelection(element: HTMLDivElement, text: string): void {
  const selection = window.getSelection();
  if (!selection || !selectionBelongsToElement(selection, element)) {
    element.focus();
    setContentEditableCaretOffset(element, element.textContent?.length ?? 0);
  }

  const activeSelection = window.getSelection();
  if (!activeSelection || activeSelection.rangeCount === 0) return;
  const range = activeSelection.getRangeAt(0);
  range.deleteContents();
  const textNode = document.createTextNode(text);
  range.insertNode(textNode);
  range.setStartAfter(textNode);
  range.collapse(true);
  activeSelection.removeAllRanges();
  activeSelection.addRange(range);
}

function isReferenceNavigationKey(key: string): boolean {
  return key === "ArrowDown" || key === "ArrowUp" || key === "Enter" || key === "Escape";
}

export const ComposerMentionInput = forwardRef<HTMLDivElement, ComposerMentionInputProps>(
  function ComposerMentionInput({
    value,
    readOnly = false,
    placeholder,
    minHeightPx,
    maxHeightPx,
    className,
    onChange,
    onFocus,
    onClick,
    onKeyUp,
    onKeyDown,
    onCompositionStart,
    onCompositionEnd,
    referenceItems = [],
    referenceMenuLabel,
    onReferenceSelect,
  }, forwardedRef) {
    const inputRef = useRef<HTMLDivElement>(null);
    const isComposingRef = useRef(false);
    const mentionTriggerIndexRef = useRef<number | null>(null);
    const [menuPosition, setMenuPosition] = useState<{ left: number; top: number } | null>(null);
    const [textContextMenu, setTextContextMenu] = useState<TextContextMenuState | null>(null);
    const [activeReferenceIndex, setActiveReferenceIndex] = useState(0);
    const isReferenceMenuOpen = Boolean(menuPosition) && referenceItems.length > 0;

    useImperativeHandle(forwardedRef, () => inputRef.current as HTMLDivElement);

    const stopCanvasInteraction = useCallback((event: SyntheticEvent<HTMLDivElement>) => {
      event.stopPropagation();
    }, []);

    const closeReferenceMenu = useCallback(() => {
      mentionTriggerIndexRef.current = null;
      setMenuPosition(null);
      setActiveReferenceIndex(0);
    }, []);

    const closeTextContextMenu = useCallback(() => {
      setTextContextMenu(null);
    }, []);

    const emitTokens = useCallback((tokens: ComposerPromptToken[]) => {
      onChange(getPromptTextFromTokens(tokens), tokens);
    }, [onChange]);

    const emitDomValue = useCallback((element: HTMLDivElement) => {
      const tokens = parseTokensFromDom(element);
      emitTokens(tokens.length > 0 ? tokens : textToTokens(element.textContent ?? ""));
    }, [emitTokens]);

    const emitValue = useCallback((nextValue: string) => {
      onChange(nextValue, textToTokens(nextValue));
    }, [onChange]);

    const handleTextContextMenu = useCallback((event: ReactMouseEvent<HTMLDivElement>) => {
      event.preventDefault();
      event.stopPropagation();
      closeReferenceMenu();

      const element = event.currentTarget;
      const selectedText = getSelectedTextInElement(element);
      setTextContextMenu({
        position: { x: event.clientX, y: event.clientY },
        canCopy: selectedText.length > 0,
        canCut: !readOnly && selectedText.length > 0,
        canPaste: !readOnly,
      });
    }, [closeReferenceMenu, readOnly]);

    const handleTextContextMenuAction = useCallback((action: TextContextMenuAction) => {
      const element = inputRef.current;
      if (!element) return;

      const run = async () => {
        if (action === "selectAll") {
          element.focus();
          selectElementContents(element);
          return;
        }

        const selectedText = getSelectedTextInElement(element);
        if (action === "copy") {
          if (!selectedText) return;
          await navigator.clipboard?.writeText(selectedText);
          return;
        }

        if (action === "cut") {
          if (readOnly || !selectedText) return;
          await navigator.clipboard?.writeText(selectedText);
          window.getSelection()?.deleteFromDocument();
          emitDomValue(element);
          return;
        }

        if (action === "paste") {
          if (readOnly) return;
          const text = await navigator.clipboard?.readText();
          if (!text) return;
          insertPlainTextAtSelection(element, text);
          emitDomValue(element);
        }
      };

      closeTextContextMenu();
      void run().catch(() => {
        if (action === "copy") document.execCommand("copy");
        if (action === "cut" && !readOnly) {
          document.execCommand("cut");
          emitDomValue(element);
        }
        if (action === "paste" && !readOnly) {
          void navigator.clipboard?.readText().then((text) => {
            insertPlainTextAtSelection(element, text);
            emitDomValue(element);
          });
        }
      });
    }, [closeTextContextMenu, emitDomValue, readOnly]);

    const syncReferenceMenuFromCursor = useCallback((element: HTMLDivElement) => {
      if (readOnly || referenceItems.length === 0) {
        closeReferenceMenu();
        return;
      }

      const cursor = getContentEditableCaretOffset(element);
      const inputValue = element.textContent ?? "";
      const previousChar = cursor > 0 ? inputValue[cursor - 1] : "";
      if (previousChar !== "@") {
        closeReferenceMenu();
        return;
      }

      const selection = window.getSelection();
      const selectionRange = selection?.rangeCount ? selection.getRangeAt(0) : null;
      const selectionRect = selectionRange && "getBoundingClientRect" in selectionRange
        ? selectionRange.getBoundingClientRect()
        : null;
      const inputRect = element.getBoundingClientRect();
      mentionTriggerIndexRef.current = cursor - 1;
      setMenuPosition({
        left: Math.max(8, Math.min((selectionRect?.left || inputRect.left) + 16, window.innerWidth - 296)),
        top: Math.max(8, (selectionRect?.top || inputRect.top) - 8),
      });
      setActiveReferenceIndex(0);
    }, [closeReferenceMenu, readOnly, referenceItems.length]);

    const insertReference = useCallback((item: ComposerMentionReferenceItem) => {
      const element = inputRef.current;
      if (!element) return;

      const tag = createReferenceTagElement(item);
      if (!tag) {
        const currentValue = getPromptTextFromTokens(parseTokensFromDom(element));
        const fallbackIndex = currentValue.endsWith("@") ? currentValue.length - 1 : currentValue.length;
        const triggerIndex = mentionTriggerIndexRef.current ?? fallbackIndex;
        const before = currentValue.slice(0, triggerIndex);
        const after = currentValue[triggerIndex] === "@" ? currentValue.slice(triggerIndex + 1) : currentValue.slice(triggerIndex);
        const nextValue = `${before}${item.value}${after}`;
        renderPromptValue(element, nextValue, referenceItems);
        emitValue(nextValue);
        closeReferenceMenu();
        return;
      }

      const currentValue = getPromptTextFromTokens(parseTokensFromDom(element));
      const fallbackIndex = currentValue.endsWith("@") ? currentValue.length - 1 : currentValue.length;
      const triggerIndex = mentionTriggerIndexRef.current ?? fallbackIndex;
      const before = currentValue.slice(0, triggerIndex);
      const after = currentValue[triggerIndex] === "@" ? currentValue.slice(triggerIndex + 1) : currentValue.slice(triggerIndex);
      const nextValue = `${before}${item.value}${after}`;
      renderPromptValue(element, nextValue, referenceItems);
      emitDomValue(element);
      onReferenceSelect?.(item);
      closeReferenceMenu();

      window.setTimeout(() => {
        if (!element.isConnected) return;
        element.focus();
        setContentEditableCaretOffset(element, before.length + item.value.length);
      }, 0);
    }, [closeReferenceMenu, emitDomValue, emitValue, onReferenceSelect, referenceItems]);

    const referenceMenu = useMemo(() => {
      if (!isReferenceMenuOpen || !menuPosition) return null;

      return createPortal(
        <div
          className="nodrag nopan nowheel fixed z-[10080] w-48 overflow-hidden rounded-lg border border-[var(--border-subtle)] bg-[#1f1f21] p-1.5 shadow-[0_12px_32px_rgba(0,0,0,0.36)]"
          style={{ left: menuPosition.left, top: menuPosition.top }}
          onPointerDown={(event) => event.stopPropagation()}
          onMouseDown={(event) => event.stopPropagation()}
          onWheel={(event) => event.stopPropagation()}
        >
          {referenceMenuLabel ? (
            <div className="sr-only">
              {referenceMenuLabel}
            </div>
          ) : null}
          <div className="space-y-1">
            {referenceItems.map((item, itemIndex) => {
              const isActive = itemIndex === activeReferenceIndex;
              return (
                <button
                  key={item.key}
                  type="button"
                  onMouseEnter={() => setActiveReferenceIndex(itemIndex)}
                  onClick={() => insertReference(item)}
                  className={`flex h-11 w-full items-center gap-2 rounded-md px-2 text-left text-xs font-medium transition-colors ${
                    isActive
                      ? "bg-[#3a3a3d] text-white"
                      : "bg-[#303033] text-[var(--text-secondary)] hover:bg-[#3a3a3d] hover:text-white"
                  }`}
                >
                  {item.image ? <img src={item.image} alt="" className="h-8 w-8 shrink-0 rounded object-cover" /> : null}
                  <span className="min-w-0 flex-1 truncate">{item.label}</span>
                </button>
              );
            })}
          </div>
        </div>,
        document.body
      );
    }, [
      activeReferenceIndex,
      insertReference,
      isReferenceMenuOpen,
      menuPosition,
      referenceItems,
      referenceMenuLabel,
    ]);

    const textContextMenuElement = useMemo(() => {
      if (!textContextMenu) return null;

      const items: TextContextMenuItem[] = [
        { key: "copy", label: "复制", icon: <CopyOutlined />, disabled: !textContextMenu.canCopy },
        { key: "cut", label: "剪切", icon: <ScissorOutlined />, disabled: !textContextMenu.canCut },
        { key: "paste", label: "粘贴", icon: <FolderAddOutlined />, disabled: !textContextMenu.canPaste },
        { key: "selectAll", label: "全选", icon: <CheckSquareOutlined /> },
      ];
      const viewportPadding = 8;
      const menuWidth = 116;
      const menuHeight = items.length * 34 + 8;
      const left = Math.min(textContextMenu.position.x, window.innerWidth - menuWidth - viewportPadding);
      const top = Math.min(textContextMenu.position.y, window.innerHeight - menuHeight - viewportPadding);

      return createPortal(
        <div
          className="fixed z-[10090] min-w-[116px] rounded-lg border border-[var(--border-subtle)] bg-[var(--surface-1)] p-1 text-[13px] text-[var(--text-primary)] shadow-[var(--shadow-panel)]"
          style={{ left, top, width: menuWidth }}
          role="menu"
          onPointerDown={(event) => event.stopPropagation()}
          onMouseDown={(event) => event.stopPropagation()}
          onContextMenu={(event) => event.preventDefault()}
        >
          {items.map((item) => (
            <button
              key={item.key}
              type="button"
              role="menuitem"
              aria-label={item.label}
              disabled={item.disabled}
              onClick={() => handleTextContextMenuAction(item.key)}
              className={[
                "flex h-[34px] w-full items-center gap-2 rounded-md px-2 text-left transition-colors hover:bg-[var(--surface-hover)]",
                item.disabled ? "cursor-not-allowed opacity-40 hover:bg-transparent" : "",
              ].join(" ")}
            >
              <span className="flex w-4 shrink-0 items-center justify-center text-[14px]">{item.icon}</span>
              <span className="min-w-0 flex-1 truncate">{item.label}</span>
            </button>
          ))}
        </div>,
        document.body
      );
    }, [handleTextContextMenuAction, textContextMenu]);

    useEffect(() => {
      if (!textContextMenu) return;
      const close = () => closeTextContextMenu();
      const handleKeyDown = (event: globalThis.KeyboardEvent) => {
        if (event.key === "Escape") closeTextContextMenu();
      };
      window.addEventListener("pointerdown", close);
      window.addEventListener("wheel", close, { passive: true });
      window.addEventListener("keydown", handleKeyDown);
      return () => {
        window.removeEventListener("pointerdown", close);
        window.removeEventListener("wheel", close);
        window.removeEventListener("keydown", handleKeyDown);
      };
    }, [closeTextContextMenu, textContextMenu]);

    useEffect(() => {
      const element = inputRef.current;
      if (!element) return;
      const tokens = parseTokensFromDom(element);
      const currentValue = getPromptTextFromTokens(tokens);
      const hasReferenceTags = tokens.some((token) => token.type === "reference");
      const shouldHaveReferenceTags = referenceItems.some((item) => value.includes(getReferenceItemLabel(item)));
      if (currentValue === value && hasReferenceTags === shouldHaveReferenceTags) return;
      if (document.activeElement === element && currentValue === value) return;
      const shouldRestoreCaret = document.activeElement === element;
      const caretOffset = shouldRestoreCaret ? getContentEditableCaretOffset(element) : null;
      renderPromptValue(element, value, referenceItems);
      if (caretOffset !== null) {
        setContentEditableCaretOffset(element, caretOffset);
        window.requestAnimationFrame(() => {
          if (!element.isConnected || document.activeElement !== element) return;
          setContentEditableCaretOffset(element, caretOffset);
        });
      }
    }, [referenceItems, value]);

    useEffect(() => {
      if (readOnly || referenceItems.length === 0) closeReferenceMenu();
    }, [closeReferenceMenu, readOnly, referenceItems.length]);

    return (
      <div className="relative">
        {referenceMenu}
        {textContextMenuElement}
        {value.length === 0 && !readOnly && placeholder ? (
          <div className="pointer-events-none absolute left-0 top-0 pr-12 text-sm leading-6 text-[var(--text-muted)]">
            {placeholder}
          </div>
        ) : null}
        <div
          ref={inputRef}
          role="textbox"
          aria-label={placeholder}
          aria-multiline="true"
          contentEditable={!readOnly}
          suppressContentEditableWarning
          spellCheck={false}
          tabIndex={readOnly ? -1 : 0}
          data-placeholder={placeholder}
          onInput={(event) => {
            if (isComposingRef.current) return;
            emitDomValue(event.currentTarget);
            syncReferenceMenuFromCursor(event.currentTarget);
          }}
          onFocus={onFocus}
          onClick={(event) => {
            event.stopPropagation();
            onClick?.(event.currentTarget);
            syncReferenceMenuFromCursor(event.currentTarget);
          }}
          onKeyUp={(event) => {
            onKeyUp?.(event);
            if (isReferenceMenuOpen && isReferenceNavigationKey(event.key)) return;
            syncReferenceMenuFromCursor(event.currentTarget);
          }}
          onKeyDown={(event) => {
            if (isReferenceMenuOpen) {
              if (event.key === "ArrowDown") {
                event.preventDefault();
                setActiveReferenceIndex((current) => (current + 1) % referenceItems.length);
                return;
              }
              if (event.key === "ArrowUp") {
                event.preventDefault();
                setActiveReferenceIndex((current) => (current - 1 + referenceItems.length) % referenceItems.length);
                return;
              }
              if (event.key === "Enter") {
                event.preventDefault();
                insertReference(referenceItems[activeReferenceIndex] ?? referenceItems[0]);
                return;
              }
              if (event.key === "Escape") {
                event.preventDefault();
                closeReferenceMenu();
                return;
              }
              if (event.key === " " || event.key === "Tab") {
                closeReferenceMenu();
              }
            }
            if (event.key === "Backspace" || event.key === "Delete") {
              const removed = removeAdjacentReferenceTag(
                event.currentTarget,
                event.key === "Backspace" ? "backward" : "forward"
              );
              if (removed) {
                event.preventDefault();
                emitDomValue(event.currentTarget);
                return;
              }
            }
            if (event.key === "Enter") closeReferenceMenu();
            onKeyDown?.(event);
          }}
          onPaste={(event) => {
            event.preventDefault();
            const text = event.clipboardData.getData("text/plain");
            document.execCommand("insertText", false, text);
          }}
          onContextMenu={handleTextContextMenu}
          onCompositionStart={(event) => {
            isComposingRef.current = true;
            closeReferenceMenu();
            onCompositionStart?.(event);
          }}
          onCompositionEnd={(event) => {
            isComposingRef.current = false;
            emitDomValue(event.currentTarget);
            onCompositionEnd?.(event);
          }}
          onPointerDownCapture={stopCanvasInteraction}
          onMouseDownCapture={stopCanvasInteraction}
          onWheelCapture={stopCanvasInteraction}
          style={{
            minHeight: minHeightPx,
            maxHeight: maxHeightPx,
            whiteSpace: "pre-wrap",
          }}
          className={className}
        />
      </div>
    );
  }
);
