import { useEditor, EditorContent, type Editor, Node, mergeAttributes } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import Image from '@tiptap/extension-image'
import Mention from '@tiptap/extension-mention'
import Placeholder from '@tiptap/extension-placeholder'
import { Table, TableRow, TableCell, TableHeader } from '@tiptap/extension-table'
import { useCallback, useEffect, useRef, useState, forwardRef, useImperativeHandle } from 'react'
import {
  Bold, Italic, Strikethrough, Code, List, ListOrdered, Quote,
  Heading1, Heading2, Heading3, Image as ImageIcon, Link as LinkIcon,
  Table as TableIcon, Undo, Redo, Minus, Video, Paperclip, AtSign,
} from 'lucide-react'

export interface PersonItem {
  id: string       // token，文章 @ 时存此值，小程序用此兑换真实密钥
  personId?: string // 管理端编辑/删除用
  name: string
  /** vip_sync：超级个体开通自动同步，共用后台「统一获客计划」 */
  personSource?: string
  /** 绑定的会员用户 id（users.id） */
  userId?: string
  label?: string
  ckbApiKey?: string // 存客宝真实密钥，管理端可见，不对外暴露
  ckbPlanId?: number
  // 存客宝创建计划用（与 Cunkebao 好友申请设置一致）
  remarkType?: string
  remarkFormat?: string
  addFriendInterval?: number
  startTime?: string
  endTime?: string
  deviceGroups?: string
  isPinned?: boolean
}

export interface LinkTagItem {
  id: string
  label: string
  aliases?: string
  url: string
  type: 'url' | 'miniprogram' | 'ckb' | 'wxlink'
  appId?: string
  pagePath?: string
  /** 管理端列表用：库内是否已存目标小程序 AppSecret（接口不下发明文） */
  hasAppSecret?: boolean
}

/** 插入附件 HTML 时转义，防 XSS */
function escapeHtmlRich(text: string): string {
  return text
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
}

function escapeAttrRich(url: string): string {
  return url.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/'/g, '&#39;')
}

export interface RichEditorRef {
  getHTML: () => string
  getMarkdown: () => string
}

interface RichEditorProps {
  content: string
  onChange: (html: string) => void
  onImageUpload?: (file: File) => Promise<string>
  /** 视频、附件等非图片上传（同 /api/upload）；不传则尝试用 onImageUpload */
  onMediaUpload?: (file: File) => Promise<string>
  persons?: PersonItem[]
  linkTags?: LinkTagItem[]
  placeholder?: string
  className?: string
}

function htmlToMarkdown(html: string): string {
  if (!html) return ''
  let md = html
  md = md.replace(/<h1[^>]*>(.*?)<\/h1>/gi, '# $1\n\n')
  md = md.replace(/<h2[^>]*>(.*?)<\/h2>/gi, '## $1\n\n')
  md = md.replace(/<h3[^>]*>(.*?)<\/h3>/gi, '### $1\n\n')
  md = md.replace(/<strong[^>]*>(.*?)<\/strong>/gi, '**$1**')
  md = md.replace(/<b[^>]*>(.*?)<\/b>/gi, '**$1**')
  md = md.replace(/<em[^>]*>(.*?)<\/em>/gi, '*$1*')
  md = md.replace(/<i[^>]*>(.*?)<\/i>/gi, '*$1*')
  md = md.replace(/<s[^>]*>(.*?)<\/s>/gi, '~~$1~~')
  md = md.replace(/<del[^>]*>(.*?)<\/del>/gi, '~~$1~~')
  md = md.replace(/<code[^>]*>(.*?)<\/code>/gi, '`$1`')
  md = md.replace(/<blockquote[^>]*>(.*?)<\/blockquote>/gi, '> $1\n\n')
  md = md.replace(/<img[^>]*src="([^"]*)"[^>]*alt="([^"]*)"[^>]*>/gi, '![$2]($1)')
  md = md.replace(/<img[^>]*src="([^"]*)"[^>]*>/gi, '![]($1)')
  md = md.replace(/<a[^>]*href="([^"]*)"[^>]*>(.*?)<\/a>/gi, '[$2]($1)')
  md = md.replace(/<li[^>]*>(.*?)<\/li>/gi, '- $1\n')
  md = md.replace(/<\/?[uo]l[^>]*>/gi, '\n')
  md = md.replace(/<br\s*\/?>/gi, '\n')
  md = md.replace(/<p[^>]*>(.*?)<\/p>/gi, '$1\n\n')
  md = md.replace(/<hr\s*\/?>/gi, '---\n\n')
  md = md.replace(/<span[^>]*data-type="mention"[^>]*data-id="([^"]*)"[^>]*>@([^<]*)<\/span>/gi, '@$2')
  md = md.replace(/<span[^>]*data-type="linkTag"[^>]*data-url="([^"]*)"[^>]*>#([^<]*)<\/span>/gi, '#[$2]($1)')
  md = md.replace(/<[^>]+>/g, '')
  md = md.replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"').replace(/&#39;/g, "'")
  md = md.replace(/\n{3,}/g, '\n\n')
  return md.trim()
}

function markdownToHtml(md: string): string {
  if (!md) return ''
  if (md.startsWith('<') && md.includes('</')) return md
  let html = md
  html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>')
  html = html.replace(/^## (.+)$/gm, '<h2>$1</h2>')
  html = html.replace(/^# (.+)$/gm, '<h1>$1</h1>')
  html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
  html = html.replace(/\*(.+?)\*/g, '<em>$1</em>')
  html = html.replace(/~~(.+?)~~/g, '<s>$1</s>')
  html = html.replace(/`([^`]+)`/g, '<code>$1</code>')
  html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1" />')
  html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>')
  html = html.replace(/^> (.+)$/gm, '<blockquote><p>$1</p></blockquote>')
  html = html.replace(/^---$/gm, '<hr />')
  html = html.replace(/^- (.+)$/gm, '<li>$1</li>')
  const lines = html.split('\n')
  const result: string[] = []
  for (const line of lines) {
    const trimmed = line.trim()
    if (!trimmed) continue
    if (/^<(?:h[1-6]|blockquote|hr|li|ul|ol|table|img)/.test(trimmed)) {
      result.push(trimmed)
    } else {
      result.push(`<p>${trimmed}</p>`)
    }
  }
  return result.join('')
}

/**
 * LinkTagExtension — 自定义 TipTap 内联节点，保留所有 data-* 属性
 * 解决：insertContent(html) 会经过 TipTap schema 导致自定义属性被丢弃的问题
 */
/** 内嵌视频块（HTML: div.rich-video-wrap > video），便于编辑区限高预览 */
const VideoEmbedExtension = Node.create({
  name: 'videoEmbed',
  group: 'block',
  atom: true,
  draggable: true,

  addAttributes() {
    return {
      src: { default: null },
    }
  },

  parseHTML() {
    return [
      {
        tag: 'div.rich-video-wrap',
        getAttrs: (el: HTMLElement) => {
          const v = el.querySelector('video')
          const src = v?.getAttribute('src')
          return src ? { src } : false
        },
      },
      {
        tag: 'video[src]',
        getAttrs: (el: HTMLElement) => ({ src: el.getAttribute('src') }),
      },
    ]
  },

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  renderHTML({ node }: { node: { attrs: { src?: string | null } } }) {
    const src = node.attrs.src || ''
    return [
      'div',
      { class: 'rich-video-wrap' },
      ['video', { src, controls: true, preload: 'metadata' }],
      ['div', { class: 'rich-video-caption' }, '视频（预览已缩小，保存后 C 端全宽播放）'],
    ]
  },
})

const LinkTagExtension = Node.create({
  name: 'linkTag',
  group: 'inline',
  inline: true,
  selectable: true,
  atom: true,

  addAttributes() {
    return {
      label:    { default: '' },
      url:      { default: '' },
      tagType:  { default: 'url',  parseHTML: (el: HTMLElement) => el.getAttribute('data-tag-type')  || 'url' },
      tagId:    { default: '',     parseHTML: (el: HTMLElement) => el.getAttribute('data-tag-id')     || '' },
      pagePath: { default: '',     parseHTML: (el: HTMLElement) => el.getAttribute('data-page-path')  || '' },
      appId:    { default: '',     parseHTML: (el: HTMLElement) => el.getAttribute('data-app-id')     || '' },
      mpKey:    { default: '',     parseHTML: (el: HTMLElement) => el.getAttribute('data-mp-key')     || '' },
    }
  },

  parseHTML() {
    return [{ tag: 'span[data-type="linkTag"]', getAttrs: (el: HTMLElement) => ({
      label:    el.textContent?.replace(/^#/, '').trim() || '',
      url:      el.getAttribute('data-url')      || '',
      tagType:  el.getAttribute('data-tag-type') || 'url',
      tagId:    el.getAttribute('data-tag-id')   || '',
      pagePath: el.getAttribute('data-page-path')|| '',
      appId:    el.getAttribute('data-app-id')  || '',
      mpKey:    el.getAttribute('data-mp-key')  || '',
    }) }]
  },

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  renderHTML({ node, HTMLAttributes }: { node: any; HTMLAttributes: Record<string, any> }) {
    return ['span', mergeAttributes(HTMLAttributes, {
      'data-type':      'linkTag',
      'data-url':       node.attrs.url,
      'data-tag-type':  node.attrs.tagType,
      'data-tag-id':    node.attrs.tagId,
      'data-page-path': node.attrs.pagePath,
      'data-app-id':    node.attrs.appId || '',
      'data-mp-key':    node.attrs.mpKey || node.attrs.appId || '',
      class: 'link-tag-node',
    }), `#${node.attrs.label}`]
  },
})

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function escapeHtml(s: string): string {
  const div = document.createElement('div')
  div.textContent = s
  return div.innerHTML
}

const MentionSuggestion = (persons: PersonItem[]): any => ({
  items: ({ query }: { query: string }) => {
    const q = query.trim().toLowerCase()
    const list = !q
      ? persons
      : persons.filter(
          (p) =>
            p.name.toLowerCase().includes(q) ||
            p.id.toLowerCase().includes(q) ||
            (p.label && p.label.toLowerCase().includes(q)) ||
            (p.userId && p.userId.toLowerCase().includes(q)),
        )
    return list.slice(0, 16)
  },
  render: () => {
    let popup: HTMLDivElement | null = null
    let selectedIndex = 0
    let items: PersonItem[] = []
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    let command: ((p: { id: string; label: string }) => void) | null = null

    const update = () => {
      if (!popup) return
      popup.innerHTML = items.map((item, i) =>
        `<div class="mention-item ${i === selectedIndex ? 'is-selected' : ''}" data-index="${i}">
          <span class="mention-name">@${escapeHtml(item.name)}</span>
          <span class="mention-id">${escapeHtml(item.label || item.id)}</span>
        </div>`
      ).join('')
      popup.querySelectorAll('.mention-item').forEach(el => {
        el.addEventListener('click', () => {
          const idx = parseInt(el.getAttribute('data-index') || '0')
          if (command && items[idx]) command({ id: items[idx].id, label: items[idx].name })
        })
      })
    }

    return {
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      onStart: (props: any) => {
        popup = document.createElement('div')
        popup.className = 'mention-popup'
        document.body.appendChild(popup)
        items = props.items
        command = props.command
        selectedIndex = 0
        update()
        if (props.clientRect) {
          const rect = props.clientRect()
          if (rect) {
            popup.style.top = `${rect.bottom + 4}px`
            popup.style.left = `${rect.left}px`
          }
        }
      },
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      onUpdate: (props: any) => {
        items = props.items
        command = props.command
        selectedIndex = 0
        update()
        if (props.clientRect && popup) {
          const rect = props.clientRect()
          if (rect) {
            popup.style.top = `${rect.bottom + 4}px`
            popup.style.left = `${rect.left}px`
          }
        }
      },
      onKeyDown: (props: { event: KeyboardEvent }) => {
        if (props.event.key === 'ArrowUp') { selectedIndex = Math.max(0, selectedIndex - 1); update(); return true }
        if (props.event.key === 'ArrowDown') { selectedIndex = Math.min(items.length - 1, selectedIndex + 1); update(); return true }
        if (props.event.key === 'Enter') { if (command && items[selectedIndex]) command({ id: items[selectedIndex].id, label: items[selectedIndex].name }); return true }
        if (props.event.key === 'Escape') { popup?.remove(); popup = null; return true }
        return false
      },
      onExit: () => { popup?.remove(); popup = null },
    }
  },
})

/** 从剪贴板提取图片文件（粘贴截图、复制图片时） */
function getImageFilesFromClipboard(event: ClipboardEvent): File[] {
  const files: File[] = []
  const items = event.clipboardData?.items
  if (!items) return files
  for (let i = 0; i < items.length; i++) {
    if (items[i].type.indexOf('image') !== -1) {
      const file = items[i].getAsFile()
      if (file) files.push(file)
    }
  }
  return files
}

const BASE64_IMG_RE = /src=["'](data:image\/([^;"']+);base64,([A-Za-z0-9+/=]+))["']/gi

/** 将 base64 转为 File */
function base64ToFile(b64: string, mime: string): File {
  const ext = { png: '.png', jpeg: '.jpg', jpg: '.jpg', gif: '.gif', webp: '.webp' }[mime.toLowerCase()] || '.png'
  const bin = atob(b64)
  const arr = new Uint8Array(bin.length)
  for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i)
  return new File([new Blob([arr], { type: `image/${mime}` })], `image${ext}`, { type: `image/${mime}` })
}

/** 将 HTML 中的 base64 图片替换为上传后的 URL（支持多个、去重同图只传一次） */
async function replaceBase64ImagesInHtml(
  html: string,
  onImageUpload: (file: File) => Promise<string>,
): Promise<string> {
  const matches = [...html.matchAll(BASE64_IMG_RE)]
  if (matches.length === 0) return html
  const urlCache = new Map<string, string>() // full dataURL -> 上传后的 url，同图只传一次
  let result = html
  for (const m of matches) {
    const full = m[1]
    const mime = m[2]
    const b64 = m[3]
    let url = urlCache.get(full)
    if (!url) {
      try {
        const file = base64ToFile(b64, mime)
        url = await onImageUpload(file)
        urlCache.set(full, url)
      } catch (e) {
        console.error('base64 图片上传失败', e)
        continue
      }
    }
    // 替换该 base64 的所有出现（同一张图可能被引用多次）
    result = result.split(`src="${full}"`).join(`src="${url}"`).split(`src='${full}'`).join(`src="${url}"`)
  }
  return result
}

const RichEditor = forwardRef<RichEditorRef, RichEditorProps>(({
  content,
  onChange,
  onImageUpload,
  onMediaUpload,
  persons = [],
  linkTags = [],
  placeholder = '开始编辑内容...',
  className,
}, ref) => {
  const fileInputRef = useRef<HTMLInputElement>(null)
  const videoInputRef = useRef<HTMLInputElement>(null)
  const attachInputRef = useRef<HTMLInputElement>(null)
  const editorRef = useRef<Editor | null>(null)
  const [linkUrl, setLinkUrl] = useState('')
  const [showLinkInput, setShowLinkInput] = useState(false)
  const initialContent = useRef(markdownToHtml(content))

  const handlePaste = useCallback(
    (_view: unknown, event: ClipboardEvent) => {
      const ed = editorRef.current
      if (!ed || !onImageUpload) return false

      // 1. 粘贴截图/复制图片：剪贴板直接有 image 文件
      const imageFiles = getImageFilesFromClipboard(event)
      if (imageFiles.length > 0) {
        event.preventDefault()
        ;(async () => {
          for (const file of imageFiles) {
            try {
              const url = await onImageUpload(file)
              if (url) ed.chain().focus().setImage({ src: url }).run()
            } catch (e) {
              console.error('粘贴图片上传失败', e)
            }
          }
        })()
        return true
      }

      // 2. 粘贴 HTML（含 base64 图片）：复制编辑器内容再粘贴时，自动上传 base64 转为 URL
      const html = event.clipboardData?.getData('text/html')
      if (html && /data:image\/[^;"']+;base64,/i.test(html)) {
        event.preventDefault()
        const { from, to } = ed.state.selection
        ;(async () => {
          try {
            const processed = await replaceBase64ImagesInHtml(html, onImageUpload)
            ed.chain().focus().insertContentAt({ from, to }, processed).run()
          } catch (e) {
            console.error('粘贴 HTML 内 base64 转换失败', e)
          }
        })()
        return true
      }

      return false
    },
    [onImageUpload],
  )

  const editor = useEditor({
    extensions: [
      StarterKit.configure({
        link: { openOnClick: false, HTMLAttributes: { class: 'rich-link' } },
      }),
      Image.configure({
        inline: true,
        allowBase64: true,
        HTMLAttributes: { class: 'rich-editor-img-thumb' },
      }),
      VideoEmbedExtension,
      Mention.configure({
        HTMLAttributes: { class: 'mention-tag' },
        suggestion: MentionSuggestion(persons),
      }),
      LinkTagExtension,
      Placeholder.configure({ placeholder }),
      Table.configure({ resizable: true }),
      TableRow, TableCell, TableHeader,
    ],
    content: initialContent.current,
    onUpdate: ({ editor: ed }: { editor: Editor }) => {
      onChange(ed.getHTML())
    },
    editorProps: {
      attributes: { class: 'rich-editor-content' },
      handlePaste,
    },
  })

  useEffect(() => {
    editorRef.current = editor ?? null
  }, [editor])

  useImperativeHandle(ref, () => ({
    getHTML: () => editor?.getHTML() || '',
    getMarkdown: () => htmlToMarkdown(editor?.getHTML() || ''),
  }))

  useEffect(() => {
    if (editor && content !== editor.getHTML()) {
      const html = markdownToHtml(content)
      if (html !== editor.getHTML()) {
        editor.commands.setContent(html)
      }
    }
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [content])

  const uploadMediaFile = useCallback(
    async (file: File) => {
      if (onMediaUpload) return onMediaUpload(file)
      if (onImageUpload) return onImageUpload(file)
      throw new Error('未配置上传')
    },
    [onImageUpload, onMediaUpload],
  )

  const handleImageUpload = useCallback(async (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0]
    if (!file || !editor) return
    if (onImageUpload) {
      const url = await onImageUpload(file)
      if (url) editor.chain().focus().setImage({ src: url }).run()
    } else {
      const reader = new FileReader()
      reader.onload = () => {
        if (typeof reader.result === 'string') editor.chain().focus().setImage({ src: reader.result }).run()
      }
      reader.readAsDataURL(file)
    }
    e.target.value = ''
  }, [editor, onImageUpload])

  const handleVideoUpload = useCallback(
    async (e: React.ChangeEvent<HTMLInputElement>) => {
      const file = e.target.files?.[0]
      if (!file || !editor) return
      try {
        const url = await uploadMediaFile(file)
        if (url) {
          editor.chain().focus().insertContent({ type: 'videoEmbed', attrs: { src: url } }).run()
        }
      } catch (err) {
        console.error(err)
      }
      e.target.value = ''
    },
    [editor, uploadMediaFile],
  )

  const handleAttachmentUpload = useCallback(
    async (e: React.ChangeEvent<HTMLInputElement>) => {
      const file = e.target.files?.[0]
      if (!file || !editor) return
      try {
        const url = await uploadMediaFile(file)
        if (!url) return
        const name = file.name || '附件'
        editor
          .chain()
          .focus()
          .insertContent(
            `<p class="rich-attachment-line"><span class="rich-attachment-badge">附件</span> <a class="rich-attachment-link" href="${escapeAttrRich(url)}" target="_blank" rel="noopener noreferrer">${escapeHtmlRich(name)}</a></p>`,
          )
          .run()
      } catch (err) {
        console.error(err)
      }
      e.target.value = ''
    },
    [editor, uploadMediaFile],
  )

  const insertMentionTrigger = useCallback(() => {
    if (!editor) return
    editor.chain().focus().insertContent('@').run()
  }, [editor])

  const insertLinkTag = useCallback((tag: LinkTagItem) => {
    if (!editor) return
    editor.chain().focus().insertContent([
      {
        type: 'linkTag',
        attrs: {
          label:    tag.label,
          url:      tag.url      || '',
          tagType:  tag.type     || 'url',
          tagId:    tag.id       || '',
          pagePath: tag.pagePath || '',
          appId:    tag.appId    || '',
          mpKey:    tag.type === 'miniprogram' ? (tag.appId || '') : '',
        },
      },
      { type: 'text', text: ' ' },
    ]).run()
  }, [editor])

  const addLink = useCallback(() => {
    if (!editor || !linkUrl) return
    editor.chain().focus().setLink({ href: linkUrl }).run()
    setLinkUrl('')
    setShowLinkInput(false)
  }, [editor, linkUrl])

  if (!editor) return null

  return (
    <div className={`rich-editor-wrapper ${className || ''}`}>
      <div className="rich-editor-toolbar">
        <div className="toolbar-group">
          <button onClick={() => editor.chain().focus().toggleBold().run()} className={editor.isActive('bold') ? 'is-active' : ''} type="button"><Bold className="w-4 h-4" /></button>
          <button onClick={() => editor.chain().focus().toggleItalic().run()} className={editor.isActive('italic') ? 'is-active' : ''} type="button"><Italic className="w-4 h-4" /></button>
          <button onClick={() => editor.chain().focus().toggleStrike().run()} className={editor.isActive('strike') ? 'is-active' : ''} type="button"><Strikethrough className="w-4 h-4" /></button>
          <button onClick={() => editor.chain().focus().toggleCode().run()} className={editor.isActive('code') ? 'is-active' : ''} type="button"><Code className="w-4 h-4" /></button>
        </div>
        <div className="toolbar-divider" />
        <div className="toolbar-group">
          <button onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()} className={editor.isActive('heading', { level: 1 }) ? 'is-active' : ''} type="button"><Heading1 className="w-4 h-4" /></button>
          <button onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()} className={editor.isActive('heading', { level: 2 }) ? 'is-active' : ''} type="button"><Heading2 className="w-4 h-4" /></button>
          <button onClick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()} className={editor.isActive('heading', { level: 3 }) ? 'is-active' : ''} type="button"><Heading3 className="w-4 h-4" /></button>
        </div>
        <div className="toolbar-divider" />
        <div className="toolbar-group">
          <button onClick={() => editor.chain().focus().toggleBulletList().run()} className={editor.isActive('bulletList') ? 'is-active' : ''} type="button"><List className="w-4 h-4" /></button>
          <button onClick={() => editor.chain().focus().toggleOrderedList().run()} className={editor.isActive('orderedList') ? 'is-active' : ''} type="button"><ListOrdered className="w-4 h-4" /></button>
          <button onClick={() => editor.chain().focus().toggleBlockquote().run()} className={editor.isActive('blockquote') ? 'is-active' : ''} type="button"><Quote className="w-4 h-4" /></button>
          <button onClick={() => editor.chain().focus().setHorizontalRule().run()} type="button"><Minus className="w-4 h-4" /></button>
        </div>
        <div className="toolbar-divider" />
        <div className="toolbar-group">
          <input ref={fileInputRef} type="file" accept="image/*" onChange={handleImageUpload} className="hidden" />
          <input ref={videoInputRef} type="file" accept="video/*" onChange={handleVideoUpload} className="hidden" />
          <input ref={attachInputRef} type="file" onChange={handleAttachmentUpload} className="hidden" />
          <button onClick={() => fileInputRef.current?.click()} type="button" title="上传图片"><ImageIcon className="w-4 h-4" /></button>
          <button
            onClick={() => videoInputRef.current?.click()}
            type="button"
            title="上传视频"
            disabled={!onMediaUpload && !onImageUpload}
          >
            <Video className="w-4 h-4" />
          </button>
          <button
            onClick={() => attachInputRef.current?.click()}
            type="button"
            title="上传附件（生成下载链接）"
            disabled={!onMediaUpload && !onImageUpload}
          >
            <Paperclip className="w-4 h-4" />
          </button>
          <button
            onClick={insertMentionTrigger}
            type="button"
            title="插入 @ 并选择人物"
            className={persons.length ? 'mention-trigger-btn' : ''}
            disabled={persons.length === 0}
          >
            <AtSign className="w-4 h-4" />
          </button>
          <button onClick={() => setShowLinkInput(!showLinkInput)} className={editor.isActive('link') ? 'is-active' : ''} type="button" title="链接"><LinkIcon className="w-4 h-4" /></button>
          <button onClick={() => editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run()} type="button" title="表格"><TableIcon className="w-4 h-4" /></button>
        </div>
        <div className="toolbar-divider" />
        <div className="toolbar-group">
          <button onClick={() => editor.chain().focus().undo().run()} disabled={!editor.can().undo()} type="button"><Undo className="w-4 h-4" /></button>
          <button onClick={() => editor.chain().focus().redo().run()} disabled={!editor.can().redo()} type="button"><Redo className="w-4 h-4" /></button>
        </div>
        {linkTags.length > 0 && (
          <>
            <div className="toolbar-divider" />
            <div className="toolbar-group">
              <select
                className="link-tag-select"
                onChange={(e) => {
                  const tag = linkTags.find(t => t.id === e.target.value)
                  if (tag) insertLinkTag(tag)
                  e.target.value = ''
                }}
                defaultValue=""
              >
                <option value="" disabled># 插入链接标签</option>
                {linkTags.map(t => <option key={t.id} value={t.id}>{t.label}</option>)}
              </select>
            </div>
          </>
        )}
      </div>
      {showLinkInput && (
        <div className="link-input-bar">
          <input
            type="url"
            placeholder="输入链接地址..."
            value={linkUrl}
            onChange={e => setLinkUrl(e.target.value)}
            onKeyDown={e => e.key === 'Enter' && addLink()}
            className="link-input"
          />
          <button onClick={addLink} className="link-confirm" type="button">确定</button>
          <button onClick={() => { editor.chain().focus().unsetLink().run(); setShowLinkInput(false) }} className="link-remove" type="button">移除</button>
        </div>
      )}
      <EditorContent editor={editor} />
    </div>
  )
})

RichEditor.displayName = 'RichEditor'
export default RichEditor
