import { useRef, useEffect, useCallback, useState } from 'react';
import DOMPurify from 'dompurify';
import { useInputBarStore } from '../store/inputBar';
import { assetsApi, tosThumb, rewriteTosUrl } from '../lib/api';
import type { UploadedFile, AssetSearchResult } from '../types';
import { parseAssetMentionsFromDOM } from '../lib/assetMentions';
import { showToast } from './Toast';
import styles from './PromptInput.module.css';

const placeholders: Record<string, string> = {
  universal: '上传参考图或视频，输入文字，通过 @ 引用素材',
  keyframe: '输入描述，定义首帧到尾帧的运动过程',
};

export function PromptInput() {
  const prompt = useInputBarStore((s) => s.prompt);
  const setPrompt = useInputBarStore((s) => s.setPrompt);
  const editorHtml = useInputBarStore((s) => s.editorHtml);
  const setEditorHtml = useInputBarStore((s) => s.setEditorHtml);
  const mode = useInputBarStore((s) => s.mode);
  const references = useInputBarStore((s) => s.references);
  const insertAtTrigger = useInputBarStore((s) => s.insertAtTrigger);

  const editorRef = useRef<HTMLDivElement>(null);
  const [showMentionPopup, setShowMentionPopup] = useState(false);
  const [mentionPos, setMentionPos] = useState({ top: 0, left: 0 });
  const typedAtRef = useRef(false); // tracks if popup was triggered by typing @
  const [highlightedIdx, setHighlightedIdx] = useState(0);
  const [hoverRef, setHoverRef] = useState<UploadedFile | null>(null);
  const [hoverPos, setHoverPos] = useState({ top: 0, left: 0 });
  const [mentionMode, setMentionMode] = useState<'references' | 'assets'>('references');
  const [assetSearchResults, setAssetSearchResults] = useState<AssetSearchResult[]>([]);
  const searchTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);

  // Auto-focus
  useEffect(() => {
    editorRef.current?.focus();
  }, []);

  // Sync editor when editorHtml changes (e.g. after submit or reEdit)
  useEffect(() => {
    const el = editorRef.current;
    if (!el) return;
    if (el.innerHTML !== editorHtml) {
      el.innerHTML = DOMPurify.sanitize(editorHtml, { ALLOWED_TAGS: ['span', 'br', 'img'], ALLOWED_ATTR: ['class', 'contenteditable', 'data-ref-id', 'data-ref-type', 'data-asset-group-id', 'data-group-name', 'data-asset-id', 'data-asset-type', 'data-asset-name', 'data-duration', 'data-thumb-url', 'draggable', 'src', 'alt', 'width', 'height', 'style'] });
      // If the HTML is plain text but we have references or asset mentions, rebuild mention spans
      // This handles the case where editorHtml comes from backend (plain text only)
      const currentAssetMentions = useInputBarStore.getState().assetMentions || [];
      if (editorHtml && !editorHtml.includes('data-ref-id') && (references.length > 0 || currentAssetMentions.length > 0)) {
        rebuildMentionSpans(el);
      }
    }
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [editorHtml]);

  // Handle @ button from toolbar
  useEffect(() => {
    if (insertAtTrigger === 0) return;
    if (references.length === 0) return;
    typedAtRef.current = false;
    openMentionPopup();
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [insertAtTrigger]);

  // Helper: create a mention span with optional thumbnail
  const createMentionSpan = useCallback((opts: {
    refId: string; refType: string; label: string; thumbUrl?: string;
    assetGroupId?: string; groupName?: string;
    assetId?: string; assetType?: string; assetName?: string; duration?: string;
  }) => {
    const span = document.createElement('span');
    span.className = styles.mention;
    span.contentEditable = 'false';
    span.dataset.refId = opts.refId;
    span.dataset.refType = opts.refType;
    span.draggable = true;
    if (opts.thumbUrl) span.dataset.thumbUrl = opts.thumbUrl;
    // New asset attributes (individual asset reference)
    if (opts.assetId) span.dataset.assetId = opts.assetId;
    if (opts.assetType) span.dataset.assetType = opts.assetType;
    if (opts.assetName) span.dataset.assetName = opts.assetName;
    if (opts.duration) span.dataset.duration = opts.duration;
    // Legacy group attributes (backward compat for old records)
    if (opts.assetGroupId) span.dataset.assetGroupId = opts.assetGroupId;
    if (opts.groupName) span.dataset.groupName = opts.groupName;

    // Render icon/thumbnail based on type
    const isAudio = opts.refType === 'audio' || opts.assetType === 'Audio';
    if (isAudio) {
      const icon = document.createElement('span');
      icon.className = styles.mentionAudioIcon;
      icon.setAttribute('aria-hidden', 'true');
      span.appendChild(icon);
    } else if (opts.thumbUrl) {
      const img = document.createElement('img');
      img.src = tosThumb(opts.thumbUrl, 32);
      img.className = styles.mentionImg;
      img.setAttribute('width', '16');
      img.setAttribute('height', '16');
      img.style.cssText = 'width:16px;height:16px;border-radius:3px;object-fit:cover;vertical-align:middle;margin-right:3px;display:inline-block;pointer-events:none';
      img.onerror = () => { img.style.display = 'none'; };
      span.appendChild(img);
    }
    // @ 前缀隐藏（textContent 保留用于模式匹配，视觉上不显示）
    const atHidden = document.createElement('span');
    atHidden.style.cssText = 'font-size:0;width:0;overflow:hidden;display:inline';
    atHidden.textContent = '@';
    span.appendChild(atHidden);
    span.appendChild(document.createTextNode(opts.label));
    return span;
  }, []);

  // Rebuild mention spans from plain text @label patterns
  const rebuildMentionSpans = useCallback((el: HTMLElement) => {
    // Collect all targets to match: references + asset mentions
    const currentAssetMentions = useInputBarStore.getState().assetMentions || [];
    type MatchTarget = {
      label: string; refId: string; refType: string; thumbUrl: string;
      assetGroupId?: string; groupName?: string;
      assetId?: string; assetType?: string; assetName?: string; duration?: string;
    };
    const targets: MatchTarget[] = [
      ...references.map((ref) => ({
        label: ref.label, refId: ref.id, refType: ref.type, thumbUrl: ref.previewUrl,
      })),
      ...currentAssetMentions.map((am: Record<string, unknown>) => {
        // New format (individual asset)
        if (am.assetId) {
          return {
            label: am.label as string, refId: am.assetId as string, refType: 'asset',
            thumbUrl: (am.thumbUrl as string) || '',
            assetId: am.assetId as string, assetType: am.assetType as string,
            assetName: am.label as string, duration: String(am.duration || 0),
          };
        }
        // Legacy format (group reference)
        return {
          label: am.label as string, refId: (am.groupId as string) || '', refType: 'asset',
          thumbUrl: (am.thumbUrl as string) || '',
          assetGroupId: am.groupId as string, groupName: am.label as string,
        };
      }),
    ];

    if (targets.length === 0) return;

    // Sort targets by label length descending — longer labels match first
    // Prevents "苏晓雨" from stealing the match before "苏晓雨音频"
    targets.sort((a, b) => b.label.length - a.label.length);

    const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
    const replacements: { node: Text; matches: { start: number; end: number; target: MatchTarget }[] }[] = [];

    let textNode: Text | null;
    while ((textNode = walker.nextNode() as Text | null)) {
      const text = textNode.textContent || '';
      const matches: { start: number; end: number; target: MatchTarget }[] = [];
      for (const target of targets) {
        const pattern = `@${target.label}`;
        let idx = text.indexOf(pattern);
        while (idx !== -1) {
          matches.push({ start: idx, end: idx + pattern.length, target });
          idx = text.indexOf(pattern, idx + pattern.length);
        }
      }
      if (matches.length > 0) {
        // Sort by position, remove overlapping matches
        matches.sort((a, b) => a.start - b.start);
        const filtered: typeof matches = [];
        let lastEnd = 0;
        for (const m of matches) {
          if (m.start >= lastEnd) {
            filtered.push(m);
            lastEnd = m.end;
          }
        }
        replacements.push({ node: textNode, matches: filtered });
      }
    }

    for (const { node, matches } of replacements) {
      const text = node.textContent || '';
      const frag = document.createDocumentFragment();
      let lastIdx = 0;
      for (const m of matches) {
        if (m.start > lastIdx) {
          frag.appendChild(document.createTextNode(text.slice(lastIdx, m.start)));
        }
        const span = createMentionSpan({
          refId: m.target.refId,
          refType: m.target.refType,
          label: m.target.label,
          thumbUrl: m.target.thumbUrl,
          assetGroupId: m.target.assetGroupId,
          groupName: m.target.groupName,
          assetId: m.target.assetId,
          assetType: m.target.assetType,
          assetName: m.target.assetName,
          duration: m.target.duration,
        });
        frag.appendChild(span);
        lastIdx = m.end;
      }
      if (lastIdx < text.length) {
        frag.appendChild(document.createTextNode(text.slice(lastIdx)));
      }
      node.parentNode?.replaceChild(frag, node);
    }

    if (replacements.length > 0) {
      setEditorHtml(el.innerHTML);
    }
  }, [references, setEditorHtml, createMentionSpan]);

  const openMentionPopup = useCallback(() => {
    const el = editorRef.current;
    if (!el) return;
    el.focus();

    const sel = window.getSelection();
    const editorRect = el.getBoundingClientRect();

    // Position popup above cursor line
    let top = 0; // fallback: top of editor
    let left = 0;

    if (sel && sel.rangeCount > 0) {
      const range = sel.getRangeAt(0);
      const rect = range.getBoundingClientRect();

      if (rect.height > 0) {
        top = rect.top - editorRect.top - 8;
        left = Math.max(0, rect.left - editorRect.left);
      } else {
        // Collapsed range returns zero rect — use temp marker
        const marker = document.createElement('span');
        marker.textContent = '\u200B';
        range.insertNode(marker);
        const mRect = marker.getBoundingClientRect();
        top = mRect.top - editorRect.top - 8;
        left = Math.max(0, mRect.left - editorRect.left);
        marker.remove();
        el.normalize();
      }
    }

    setMentionPos({ top, left });
    setHighlightedIdx(0);
    setShowMentionPopup(true);
  }, []);

  const extractText = useCallback(() => {
    const el = editorRef.current;
    if (!el) return;
    setPrompt(el.textContent || '');
    setEditorHtml(el.innerHTML);
    // Sync assetMentions from DOM — prevents stale refs after deleting @mention spans
    const mentions: Record<string, unknown>[] = [];
    el.querySelectorAll('[data-ref-type="asset"]').forEach((span) => {
      const s = span as HTMLElement;
      if (s.dataset.assetId) {
        mentions.push({
          assetId: s.dataset.assetId,
          label: s.dataset.assetName || s.textContent?.replace('@', '') || '',
          thumbUrl: s.dataset.thumbUrl || '',
          assetType: s.dataset.assetType || 'Image',
          duration: parseFloat(s.dataset.duration || '0'),
        });
      } else if (s.dataset.assetGroupId) {
        mentions.push({
          groupId: s.dataset.assetGroupId,
          label: s.dataset.groupName || s.textContent?.replace('@', '') || '',
          thumbUrl: s.dataset.thumbUrl || '',
        });
      }
    });
    useInputBarStore.setState({ assetMentions: mentions });
  }, [setPrompt, setEditorHtml]);

  // Remove orphaned mention spans when a reference is deleted
  // Skip asset-type spans — they are not tied to uploaded references
  useEffect(() => {
    const el = editorRef.current;
    if (!el) return;
    const refIds = new Set(references.map((r) => r.id));
    const spans = el.querySelectorAll<HTMLElement>('[data-ref-id]');
    let changed = false;
    spans.forEach((span) => {
      if (span.dataset.refType === 'asset') return; // skip asset mentions
      if (!refIds.has(span.dataset.refId!)) {
        span.remove();
        changed = true;
      }
    });
    if (changed) {
      el.normalize();
      extractText();
    }
  }, [references, extractText]);

  // Sync editorHtml immediately on ANY DOM change (backspace delete, etc.)
  // Without this, deleting a mention span doesn't update editorHtml until next input event
  useEffect(() => {
    const el = editorRef.current;
    if (!el) return;
    const observer = new MutationObserver(() => extractText());
    observer.observe(el, { childList: true, subtree: true, characterData: true });
    return () => observer.disconnect();
  }, [extractText]);

  const handleInput = useCallback(() => {
    extractText();

    // Detect if user just typed @
    const sel = window.getSelection();
    if (!sel || sel.rangeCount === 0) return;
    const range = sel.getRangeAt(0);
    const node = range.startContainer;
    if (node.nodeType !== Node.TEXT_NODE) return;

    const text = node.textContent || '';
    const offset = range.startOffset;

    // Find the last @ before cursor
    const textBeforeCursor = text.substring(0, offset);
    const lastAtIdx = textBeforeCursor.lastIndexOf('@');

    if (lastAtIdx < 0) {
      // No @ before cursor, close popup
      setShowMentionPopup(false);
      return;
    }

    if (lastAtIdx >= 0) {
      const textAfterAt = textBeforeCursor.substring(lastAtIdx + 1);

      if (textAfterAt.length === 0 && references.length > 0) {
        // Just typed @, show reference popup
        typedAtRef.current = true;
        setMentionMode('references');
        openMentionPopup();
      } else if (textAfterAt.length > 0 && !textAfterAt.includes(' ')) {
        // Text after @, search assets (Chinese + English)
        if (searchTimerRef.current) clearTimeout(searchTimerRef.current);
        searchTimerRef.current = setTimeout(() => {
          assetsApi.search(textAfterAt).then((res) => {
            if (res.data.results.length > 0) {
              setAssetSearchResults(res.data.results);
              setMentionMode('assets');
              typedAtRef.current = true;
              setHighlightedIdx(0);
              openMentionPopup();
            } else {
              setShowMentionPopup(false);
            }
          }).catch(() => { showToast('素材搜索失败，请重试'); });
        }, 300);
      } else if (textAfterAt.includes(' ')) {
        // Space after @ text, close popup
        setShowMentionPopup(false);
      }
    }
  }, [extractText, references.length, openMentionPopup]);

  const insertMention = useCallback((ref: UploadedFile) => {
    setShowMentionPopup(false);
    const el = editorRef.current;
    if (!el) return;

    el.focus();
    const sel = window.getSelection();
    if (!sel || sel.rangeCount === 0) return;

    const range = sel.getRangeAt(0);

    // If triggered by typing @, remove the @ character first
    if (typedAtRef.current) {
      typedAtRef.current = false;
      const node = range.startContainer;
      if (node.nodeType === Node.TEXT_NODE) {
        const text = node.textContent || '';
        const offset = range.startOffset;
        const atIdx = text.lastIndexOf('@', offset - 1);
        if (atIdx >= 0) {
          node.textContent = text.substring(0, atIdx) + text.substring(offset);
          range.setStart(node, atIdx);
          range.collapse(true);
        }
      }
    }

    range.deleteContents();

    // Create mention span with thumbnail
    const mention = createMentionSpan({
      refId: ref.id,
      refType: ref.type,
      label: ref.label,
      thumbUrl: ref.previewUrl,
    });

    // Insert mention + trailing space
    range.insertNode(mention);

    const space = document.createTextNode('\u00A0');
    mention.after(space);

    // Move cursor after space
    const newRange = document.createRange();
    newRange.setStartAfter(space);
    newRange.collapse(true);
    sel.removeAllRanges();
    sel.addRange(newRange);

    extractText();
  }, [extractText]);

  const insertAssetMention = useCallback((asset: AssetSearchResult) => {
    // Instant check: count limit
    const stats = editorRef.current ? parseAssetMentionsFromDOM(editorRef.current) : { counts: { image: 0, video: 0, audio: 0 }, durations: { video: 0, audio: 0 } };
    const refs = useInputBarStore.getState().references;
    const refCounts = { image: 0, video: 0, audio: 0 };
    refs.forEach((r) => refCounts[r.type]++);
    const typeKey = asset.asset_type === 'Video' ? 'video' : asset.asset_type === 'Audio' ? 'audio' : 'image';
    const maxMap = { image: 9, video: 3, audio: 3 };
    if (refCounts[typeKey] + stats.counts[typeKey] >= maxMap[typeKey]) {
      const typeLabel = asset.asset_type === 'Video' ? '视频' : asset.asset_type === 'Audio' ? '音频' : '图片';
      showToast(`${typeLabel}已达上限`);
      return;
    }
    // Instant check: duration limit (video/audio)
    if (asset.asset_type === 'Video' || asset.asset_type === 'Audio') {
      if (!asset.duration) {
        // Duration unknown (still processing or ffprobe failed) — warn but allow
        showToast('该素材时长未确定，提交时将由服务端校验');
      } else {
        const existingDur = refs.filter((r) => r.type === typeKey && r.duration).reduce((s, r) => s + (r.duration || 0), 0);
        const assetDur = typeKey === 'video' ? stats.durations.video : stats.durations.audio;
        if (existingDur + assetDur + asset.duration > 15.4) {
          const typeLabel = asset.asset_type === 'Video' ? '视频' : '音频';
          showToast(`${typeLabel}总时长超过15秒限制`);
          return;
        }
      }
    }

    setShowMentionPopup(false);
    setMentionMode('references');
    setAssetSearchResults([]);
    const el = editorRef.current;
    if (!el) return;

    el.focus();
    const sel = window.getSelection();
    if (!sel || sel.rangeCount === 0) return;

    const range = sel.getRangeAt(0);

    // Remove the @query text that was typed
    if (typedAtRef.current) {
      typedAtRef.current = false;
      const node = range.startContainer;
      if (node.nodeType === Node.TEXT_NODE) {
        const text = node.textContent || '';
        const offset = range.startOffset;
        const atIdx = text.lastIndexOf('@', offset - 1);
        if (atIdx >= 0) {
          node.textContent = text.substring(0, atIdx) + text.substring(offset);
          range.setStart(node, atIdx);
          range.collapse(true);
        }
      }
    }

    range.deleteContents();

    // Create mention span for individual asset
    const mention = createMentionSpan({
      refId: String(asset.id),
      refType: 'asset',
      label: asset.name,
      thumbUrl: asset.thumbnail_url || asset.url,
      assetId: String(asset.id),
      assetType: asset.asset_type,
      assetName: asset.name,
      duration: asset.duration != null ? String(asset.duration) : '',
    });

    range.insertNode(mention);

    const space = document.createTextNode('\u00A0');
    mention.after(space);

    const newRange = document.createRange();
    newRange.setStartAfter(space);
    newRange.collapse(true);
    sel.removeAllRanges();
    sel.addRange(newRange);

    extractText();
  }, [extractText, editorHtml, references]);

  const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
    if (showMentionPopup) {
      const items = mentionMode === 'assets' ? assetSearchResults : references;
      if (items.length === 0) return;
      if (e.key === 'Escape') {
        e.preventDefault();
        setShowMentionPopup(false);
        setMentionMode('references');
      } else if (e.key === 'ArrowDown') {
        e.preventDefault();
        setHighlightedIdx((prev) => (prev + 1) % items.length);
      } else if (e.key === 'ArrowUp') {
        e.preventDefault();
        setHighlightedIdx((prev) => (prev - 1 + items.length) % items.length);
      } else if (e.key === 'Enter') {
        e.preventDefault();
        if (mentionMode === 'assets') {
          insertAssetMention(assetSearchResults[highlightedIdx]);
        } else {
          insertMention(references[highlightedIdx]);
        }
      }
    }
  }, [showMentionPopup, mentionMode, references, assetSearchResults, highlightedIdx, insertMention, insertAssetMention]);

  const handlePaste = useCallback((e: React.ClipboardEvent) => {
    e.preventDefault();

    // Handle pasted image files (Ctrl+V screenshot / copied image)
    const items = e.clipboardData.items;
    const imageFiles: File[] = [];
    for (let i = 0; i < items.length; i++) {
      if (items[i].type.startsWith('image/')) {
        const file = items[i].getAsFile();
        if (file) imageFiles.push(file);
      }
    }
    if (imageFiles.length > 0) {
      useInputBarStore.getState().addReferences(imageFiles);
      return;
    }

    // Check if clipboard HTML contains mention spans (from our editor)
    const html = e.clipboardData.getData('text/html');
    if (html && html.includes('data-ref-id')) {
      const sanitized = DOMPurify.sanitize(html, {
        ALLOWED_TAGS: ['span', 'br', 'img'],
        ALLOWED_ATTR: [
          'class', 'contenteditable', 'data-ref-id', 'data-ref-type',
          'data-asset-group-id', 'data-group-name',
          'data-asset-id', 'data-asset-type', 'data-asset-name', 'data-duration',
          'data-thumb-url', 'draggable', 'src', 'alt', 'width', 'height', 'style',
        ],
      });
      document.execCommand('insertHTML', false, sanitized);
      extractText();
      return;
    }

    // Plain text paste — strip @label patterns to prevent duplicate mention tags
    let text = e.clipboardData.getData('text/plain');
    for (const ref of references) {
      const pattern = `@${ref.label}`;
      while (text.includes(pattern)) {
        text = text.replace(pattern, ref.label);
      }
    }
    document.execCommand('insertText', false, text);
    extractText();
  }, [extractText, references]);

  // Mention hover — delegated event (supports both reference and asset mentions)
  const handleMouseOver = useCallback((e: React.MouseEvent) => {
    const target = (e.target as HTMLElement).closest('[data-ref-id]') as HTMLElement | null;
    if (!target) return;

    const refId = target.dataset.refId;
    const refType = target.dataset.refType;

    // 音频标签不显示 hover 预览
    if (refType === 'audio') return;

    // 参考图：从 references 中查找
    let found = references.find((r) => r.id === refId);

    // 素材库标签：用 data-thumb-url 构造预览数据
    if (!found && refType === 'asset') {
      const assetType = target.dataset.assetType || 'Image';
      if (assetType === 'Audio') return; // 音频素材不弹预览
      const thumbUrl = target.dataset.thumbUrl;
      if (thumbUrl) {
        found = {
          id: refId || '',
          type: assetType === 'Video' ? 'video' : 'image',
          previewUrl: thumbUrl,
          label: target.dataset.assetName || target.textContent || '',
        };
      }
    }

    if (!found) return;

    const rect = target.getBoundingClientRect();
    const wrapperRect = editorRef.current!.parentElement!.getBoundingClientRect();
    setHoverRef(found);
    setHoverPos({
      top: rect.top - wrapperRect.top - 8,
      left: rect.left - wrapperRect.left + rect.width / 2,
    });
  }, [references]);

  const handleMouseOut = useCallback((e: React.MouseEvent) => {
    const target = (e.target as HTMLElement).closest('[data-ref-id]');
    if (target) setHoverRef(null);
  }, []);

  // Close mention popup on click outside
  useEffect(() => {
    if (!showMentionPopup) return;
    const handler = (e: MouseEvent) => {
      const popup = document.querySelector(`.${styles.mentionPopup}`);
      if (popup && !popup.contains(e.target as Node)) {
        setShowMentionPopup(false);
      }
    };
    document.addEventListener('mousedown', handler);
    return () => document.removeEventListener('mousedown', handler);
  }, [showMentionPopup]);

  const isEmpty = !prompt.trim() && !editorHtml;

  return (
    <div className={styles.wrapper}>
      {isEmpty && (
        <div className={styles.placeholder}>{placeholders[mode]}</div>
      )}
      <div
        ref={editorRef}
        className={styles.editor}
        contentEditable
        suppressContentEditableWarning
        onInput={handleInput}
        onKeyDown={handleKeyDown}
        onPaste={handlePaste}
        onMouseOver={handleMouseOver}
        onMouseOut={handleMouseOut}
        onDragStart={(e) => {
          const target = (e.target as HTMLElement).closest('[data-ref-id]') as HTMLElement | null;
          if (target) {
            e.dataTransfer.setData('text/html', target.outerHTML);
            e.dataTransfer.effectAllowed = 'move';
            target.classList.add(styles.dragging);
            setHoverRef(null);
          }
        }}
        onDragOver={(e) => {
          e.preventDefault();
          // 拖拽 mention 标签时让光标跟随鼠标位置
          if (!e.dataTransfer.types.includes('Files')) {
            const range = document.caretRangeFromPoint(e.clientX, e.clientY);
            if (range) {
              const sel = window.getSelection();
              sel?.removeAllRanges();
              sel?.addRange(range);
            }
          }
        }}
        onDrop={(e) => {
          e.preventDefault();
          const html = e.dataTransfer.getData('text/html');
          if (html && html.includes('data-ref-id')) {
            // 1. 先用鼠标坐标算出目标位置，插入临时 marker（此时 DOM 还没变）
            const dropRange = document.caretRangeFromPoint(e.clientX, e.clientY);
            if (!dropRange) return;
            const marker = document.createTextNode('\u200B');
            dropRange.insertNode(marker);

            // 2. 再删除原始标签（DOM 重排不影响 marker 位置）
            const dragging = editorRef.current?.querySelector(`.${styles.dragging}`);
            if (dragging) dragging.remove();

            // 3. 在 marker 位置插入标签
            const temp = document.createElement('div');
            temp.innerHTML = html;
            const node = temp.firstChild;
            if (node) {
              marker.parentNode?.insertBefore(node, marker);
            }
            marker.remove();

            editorRef.current?.normalize();
            extractText();
          }
        }}
      />

      {/* Mention popup */}
      {showMentionPopup && (
        <div
          className={styles.mentionPopup}
          style={{ top: mentionPos.top, left: mentionPos.left }}
        >
          {mentionMode === 'references' && references.length > 0 && (
            <>
              <div className={styles.mentionHeader}>可能@的内容</div>
              {references.map((ref, idx) => (
                <button
                  key={`${ref.id}-${idx}`}
                  className={`${styles.mentionItem} ${idx === highlightedIdx ? styles.mentionItemActive : ''}`}
                  onMouseDown={(e) => {
                    e.preventDefault();
                    insertMention(ref);
                  }}
                >
                  <div className={styles.mentionThumb}>
                    {ref.type === 'video' ? (
                      <video src={rewriteTosUrl(ref.previewUrl)} muted className={styles.thumbMedia} />
                    ) : ref.type === 'audio' ? (
                      <span style={{ fontSize: 16 }}>{'\u266B'}</span>
                    ) : (
                      <img src={tosThumb(ref.previewUrl, 72)} alt="" className={styles.thumbMedia} />
                    )}
                  </div>
                  <span className={styles.mentionLabel}>{ref.label}</span>
                  <span className={styles.mentionType}>
                    {ref.type === 'video' ? '视频' : ref.type === 'audio' ? '音频' : '图片'}
                  </span>
                </button>
              ))}
            </>
          )}
          {mentionMode === 'assets' && assetSearchResults.length > 0 && (
            <>
              <div className={styles.mentionHeader}>人物素材库匹配</div>
              {assetSearchResults.map((asset, idx) => (
                <button
                  key={asset.id}
                  className={`${styles.mentionItem} ${idx === highlightedIdx ? styles.mentionItemActive : ''}`}
                  onMouseDown={(e) => {
                    e.preventDefault();
                    insertAssetMention(asset);
                  }}
                >
                  <div className={styles.mentionThumb}>
                    {asset.asset_type === 'Audio' ? (
                      <span style={{ fontSize: 16 }}>♫</span>
                    ) : (asset.thumbnail_url || asset.url) ? (
                      <img src={tosThumb(asset.thumbnail_url || asset.url, 72)} alt="" className={styles.thumbMedia} />
                    ) : (
                      <span style={{ fontSize: 9, color: 'var(--color-text-disabled)' }}>无图</span>
                    )}
                  </div>
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <span className={styles.mentionLabel}>{asset.name}</span>
                    <span style={{ fontSize: 10, color: 'var(--color-border-modal-hover)', marginLeft: 4 }}>{asset.group_name}</span>
                  </div>
                  <span className={styles.mentionType}>
                    {asset.asset_type === 'Video' ? '视频' : asset.asset_type === 'Audio' ? '音频' : '图片'}
                  </span>
                </button>
              ))}
            </>
          )}
        </div>
      )}

      {/* Hover preview tooltip */}
      {hoverRef && (
        <div
          className={styles.previewTooltip}
          style={{ top: hoverPos.top, left: hoverPos.left }}
        >
          {hoverRef.type === 'video' ? (
            <video
              src={rewriteTosUrl(hoverRef.previewUrl)}
              autoPlay
              loop
              muted
              playsInline
              className={styles.previewMedia}
            />
          ) : hoverRef.type === 'audio' ? (
            <div style={{ width: 120, height: 80, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 32 }}>{'\u266B'}</div>
          ) : (
            <img
              src={tosThumb(hoverRef.previewUrl, 200)}
              alt={hoverRef.label}
              className={styles.previewMedia}
            />
          )}
          <div className={styles.previewLabel}>{hoverRef.label}</div>
        </div>
      )}
    </div>
  );
}
