'use client'

import { useEffect, useRef, useState, useCallback, forwardRef, useImperativeHandle } from 'react'
import { createPortal } from 'react-dom'
import { useLanguage } from '@/lib/i18n'
import { sanitizeRichHtml } from '@/lib/sanitize-content'
import { useEditor, EditorContent, useEditorState } from '@tiptap/react'
import { BubbleMenu } from '@tiptap/react/menus'
import StarterKit from '@tiptap/starter-kit'
import Underline from '@tiptap/extension-underline'
import Placeholder from '@tiptap/extension-placeholder'
import TiptapLink from '@tiptap/extension-link'
import Highlight from '@tiptap/extension-highlight'
import Image from '@tiptap/extension-image'
import TextAlign from '@tiptap/extension-text-align'
import TaskList from '@tiptap/extension-task-list'
import TaskItem from '@tiptap/extension-task-item'
import { Table } from '@tiptap/extension-table'
import { TableRow } from '@tiptap/extension-table-row'
import { TableCell } from '@tiptap/extension-table-cell'
import { TableHeader } from '@tiptap/extension-table-header'
import Superscript from '@tiptap/extension-superscript'
import Subscript from '@tiptap/extension-subscript'
import Typography from '@tiptap/extension-typography'
import { ChartExtension } from './tiptap-chart-extension'
import { ChartSuggestionsDialog } from './chart-suggestions-dialog'
import { UniqueIdExtension } from './tiptap-unique-id-extension'
import { LiveBlockExtension } from './tiptap-live-block-extension'
import { StructuredViewBlockExtension, insertStructuredViewBlockAtSelection } from './tiptap-structured-view-block-extension'
import { ToggleExtension, insertToggleBlock } from './tiptap-toggle-extension'
import { CalloutExtension, insertCalloutBlock } from './tiptap-callout-extension'
import { OutlineExtension, insertOutlineBlock } from './tiptap-outline-extension'
import { FindReplaceBar, FindReplaceExtension } from './editor-find-replace-bar'
import { LinkPreviewExtension, insertLinkPreview, isUrl } from './tiptap-link-preview-extension'
import { MathEquationExtension, InlineMathExtension, insertMathEquation } from './tiptap-math-extension'
import { ColumnsExtension, ColumnNode, insertColumnsBlock } from './tiptap-columns-extension'
import { RtlPreserveExtension } from './tiptap-rtl-preserve-extension'
import { ClipArticleExtension } from './tiptap-clip-article-extension'
import { BlockPicker, type BlockSuggestion } from './block-picker'
import { EditorBlockDragHandle } from './editor-block-drag-handle'
import { BlockActionMenu } from './block-action-menu'
import { SmartPasteMenu } from './smart-paste-menu'
import { SmartPasteExtendedMenu } from './smart-paste-extended-menu'
import { MobileEditorToolbar } from './mobile-editor-toolbar'
import { MobileActionSheet } from './mobile-action-sheet'
import { globalDragHandleExtensions } from '@/lib/editor/global-drag-handle-extension'
import { resolveBlockAtDragHandle } from '@/lib/editor/block-at-drag-handle'
import { parseBlockReferenceFromText, recallLastBlockReference, type ParsedBlockReference } from '@/lib/editor/parse-block-reference'
import { getEmptyParagraphAtSelection } from '@/lib/editor/empty-paragraph-at-selection'
import { SmartPasteExtension } from '@/lib/editor/smart-paste-extension'
import { BlockSelectionExtension } from '@/lib/editor/block-selection-extension'
import { MarkdownPasteExtension } from '@/lib/editor/markdown-paste-extension'
import { TurnIntoShortcutExtension } from '@/lib/editor/turn-into-shortcut-extension'
import { UndoRedoFeedbackExtension } from '@/lib/editor/undo-redo-feedback-extension'
import type { Node as PMNode } from '@tiptap/pm/model'
import { detectTextDirection } from '@/lib/clip/rtl-content'
import { stripHtmlToPlainText } from '@/lib/text/plain-text'
import { NoteLinkPicker, type NoteLinkOption } from './note-link-picker'
import { applyClipRtlDirection } from '@/lib/editor/apply-clip-rtl-direction'
import { NOTE_REQUEST_SAVE_EVENT } from '@/lib/note-change-sync'
import { openNotePeek } from '@/lib/note-peek-sync'
import { useAiConsent } from '@/components/legal/ai-consent-provider'
import type { Editor } from '@tiptap/core'
import type { EditorState } from '@tiptap/pm/state'
import {
  Bold, Italic, Underline as UnderlineIcon, Strikethrough, Code,
  Heading1, Heading2, Heading3, List, ListOrdered, CheckSquare,
  Quote, CodeXml, Minus, ImageIcon, Type, Highlighter, Link as LinkIcon, Link2,
  Sparkles, Wand2, Scissors, Lightbulb, X, Check, ExternalLink,
  FileText, Pilcrow, MessageSquare, AlignLeft, AlignCenter, AlignRight,
  Superscript as SuperscriptIcon, Subscript as SubscriptIcon, Expand, Plus,
  SpellCheck, Languages, BookOpen, Presentation, BarChart3, Database,
  ChevronsRightLeft, MessageSquareWarning, ListTree, FunctionSquare, Columns3, Loader2, Trash2
} from 'lucide-react'
import { cn } from '@/lib/utils'
import { toast } from 'sonner'
import { NodeSelection } from '@tiptap/pm/state'

export interface RichTextEditorHandle {
  getEditor: () => Editor | null
  triggerChartSuggestions: () => void
  insertCitation: (
    payload: { noteId: string; noteTitle: string; excerpt: string },
    options?: { atEnd?: boolean }
  ) => boolean
  insertLiveBlock: (block: BlockSuggestion, options?: { atEnd?: boolean }) => boolean
}

export interface RichTextEditorProps {
  content?: string
  onChange?: (content: string) => void
  /** Immediate parent state update (image paste, structural inserts) — skips typing debounce. */
  onChangeImmediate?: (content: string) => void
  className?: string
  placeholder?: string
  onImageUpload?: (file: File) => Promise<string>
  noteId?: string
  notebookId?: string | null
  noteTitle?: string
  /** URL source du clip (BBC Persian, etc.) — pour RTL explicite des listes */
  sourceUrl?: string | null
}

interface RichTextEditorRef {
  triggerChartSuggestions: () => void
}

type SlashItem = {
  title: string
  description: string
  icon: any
  category?: string
  shortcut?: string
  isImage?: boolean
  isAi?: boolean
  isFavorite?: boolean
  aiOption?: 'clarify' | 'shorten' | 'improve' | 'write'
  command: (editor: Editor, range?: any) => void
}

type SlashCategoryId = 'text' | 'media' | 'data' | 'embed' | 'ai'

type SlashMenuItem = SlashItem & { categoryId: SlashCategoryId; slashKeywords?: string[] }

const ORDERED_SLASH_CATEGORIES: SlashCategoryId[] = ['text', 'media', 'data', 'embed', 'ai']

function slashCategoryLabel(id: SlashCategoryId, t: (key: string) => string): string {
  switch (id) {
    case 'text': return t('richTextEditor.slashCatText') || 'Texte'
    case 'media': return t('richTextEditor.slashCatMedia') || 'Médias'
    case 'data': return t('richTextEditor.slashCatData') || 'Données'
    case 'embed': return t('richTextEditor.slashCatEmbed') || 'Intégré'
    case 'ai': return t('richTextEditor.slashCatAi') || 'IA Note'
  }
}

/** Sent to /api/ai/reformulate as target language (unchanged for prompt compatibility). */
const TRANSLATE_TARGET_API_VALUES = ['Francais', 'English', 'Espanol', 'Deutsch', 'Persan', 'Portugais', 'Italiano', 'Chinois', 'Japonais'] as const

const AI_REFORMULATE_FALLBACK = '__RICH_TEXT_AI_FALLBACK__'

const CustomImage = Image.extend({
  // Treat image as a single selectable unit (Backspace/Delete must remove it)
  atom: true,
  selectable: true,
  draggable: true,

  addAttributes() {
    return {
      ...this.parent?.(),
      width: {
        default: '100%',
        parseHTML: element => element.style.width || element.getAttribute('width') || '100%',
        renderHTML: attributes => {
          if (!attributes.width) return {}
          return { style: `width: ${attributes.width}; max-width: 100%; height: auto;` }
        }
      }
    }
  },

  addKeyboardShortcuts() {
    const deleteSelectedImage = () => {
      const { selection } = this.editor.state
      if (selection instanceof NodeSelection && selection.node.type.name === 'image') {
        return this.editor.chain().focus().deleteSelection().run()
      }
      // Cursor just after an image block → Backspace removes it
      const { $from, empty } = selection
      if (!empty) return false
      const before = $from.nodeBefore
      if (before?.type.name === 'image') {
        const from = $from.pos - before.nodeSize
        return this.editor.chain().focus().deleteRange({ from, to: $from.pos }).run()
      }
      // Cursor at start of paragraph right after image (common after paste under title)
      if ($from.parentOffset === 0 && $from.depth >= 1) {
        const index = $from.index($from.depth - 1)
        if (index > 0) {
          const prev = $from.node($from.depth - 1).child(index - 1)
          if (prev.type.name === 'image') {
            const pos = $from.before($from.depth) - prev.nodeSize
            return this.editor.chain().focus().deleteRange({ from: pos, to: pos + prev.nodeSize }).run()
          }
        }
      }
      return false
    }

    return {
      Backspace: deleteSelectedImage,
      Delete: () => {
        const { selection } = this.editor.state
        if (selection instanceof NodeSelection && selection.node.type.name === 'image') {
          return this.editor.chain().focus().deleteSelection().run()
        }
        // Cursor just before an image → Delete removes it
        const { $from, empty } = selection
        if (!empty) return false
        const after = $from.nodeAfter
        if (after?.type.name === 'image') {
          return this.editor.chain().focus().deleteRange({ from: $from.pos, to: $from.pos + after.nodeSize }).run()
        }
        return false
      },
    }
  },
})

/** Insert / toggle a bullet list without the toggle-off → re-insert glitch. */
function insertBulletList(editor: Editor) {
  // Already a bullet list → toggle off (do not re-insert)
  if (editor.isActive('bulletList')) {
    editor.chain().focus().toggleBulletList().run()
    return
  }
  // Convert from other list types if needed
  if (editor.isActive('orderedList')) editor.chain().focus().toggleOrderedList().run()
  if (editor.isActive('taskList')) editor.chain().focus().toggleTaskList().run()
  const ok = editor.chain().focus().toggleBulletList().run()
  if (!ok) {
    editor.chain().focus().insertContent('<ul><li><p></p></li></ul>').run()
  }
}

function insertOrderedList(editor: Editor) {
  if (editor.isActive('orderedList')) {
    editor.chain().focus().toggleOrderedList().run()
    return
  }
  if (editor.isActive('bulletList')) editor.chain().focus().toggleBulletList().run()
  if (editor.isActive('taskList')) editor.chain().focus().toggleTaskList().run()
  const ok = editor.chain().focus().toggleOrderedList().run()
  if (!ok) {
    editor.chain().focus().insertContent('<ol><li><p></p></li></ol>').run()
  }
}

const slashCommands: SlashItem[] = [
  // Basic blocks — lists BEFORE table so defaults / shortcuts never collide with Tableau
  { title: 'Text', description: 'Plain paragraph', icon: Pilcrow, category: 'Basic blocks', shortcut: '¶', command: (e) => e.chain().focus().setParagraph().run() },
  { title: 'Heading 1', description: 'Big section heading', icon: Heading1, category: 'Basic blocks', shortcut: '#', command: (e) => e.chain().focus().toggleHeading({ level: 1 }).run() },
  { title: 'Heading 2', description: 'Medium section heading', icon: Heading2, category: 'Basic blocks', shortcut: '##', command: (e) => e.chain().focus().toggleHeading({ level: 2 }).run() },
  { title: 'Heading 3', description: 'Small section heading', icon: Heading3, category: 'Basic blocks', shortcut: '###', command: (e) => e.chain().focus().toggleHeading({ level: 3 }).run() },
  { title: 'Bullet List', description: 'Unordered list', icon: List, category: 'Basic blocks', shortcut: '-', command: (e) => insertBulletList(e) },
  { title: 'Numbered List', description: 'Ordered numbered list', icon: ListOrdered, category: 'Basic blocks', shortcut: '1.', command: (e) => insertOrderedList(e) },
  { title: 'To-do List', description: 'Checkboxes for tasks', icon: CheckSquare, category: 'Basic blocks', shortcut: '[]', command: (e) => e.chain().focus().toggleTaskList().run() },
  { title: 'Quote', description: 'Capture a quote', icon: Quote, category: 'Basic blocks', shortcut: '>', command: (e) => e.chain().focus().toggleBlockquote().run() },
  { title: 'Code Block', description: 'Code snippet', icon: CodeXml, category: 'Basic blocks', shortcut: '```', command: (e) => e.chain().focus().toggleCodeBlock().run() },
  { title: 'Divider', description: 'Horizontal separator', icon: Minus, category: 'Basic blocks', shortcut: '---', command: (e) => e.chain().focus().setHorizontalRule().run() },
  { title: 'Table', description: 'Insert a simple table', icon: () => <span className="text-xs font-bold border rounded px-1">TBL</span>, category: 'Basic blocks', command: (e) => e.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run() },
  // Media
  { title: 'Image', description: 'Embed image from URL', icon: ImageIcon, category: 'Media', isImage: true, command: () => { } },
  // Formatting
  { title: 'Align Left', description: 'Align text left', icon: AlignLeft, category: 'Formatting', command: (e) => e.chain().focus().setTextAlign('left').run() },
  { title: 'Align Center', description: 'Center text', icon: AlignCenter, category: 'Formatting', command: (e) => e.chain().focus().setTextAlign('center').run() },
  { title: 'Align Right', description: 'Align text right', icon: AlignRight, category: 'Formatting', command: (e) => e.chain().focus().setTextAlign('right').run() },
  // IA Note
  { title: 'Clarifier', description: 'Rendre le texte plus clair', icon: Lightbulb, category: 'IA Note', isAi: true, aiOption: 'clarify', command: () => { } },
  { title: 'Raccourcir', description: 'Condenser le texte', icon: Scissors, category: 'IA Note', isAi: true, aiOption: 'shorten', command: () => { } },
  { title: 'Améliorer', description: 'Améliorer le style', icon: Wand2, category: 'IA Note', isAi: true, aiOption: 'improve', command: () => { } },
  {
    title: 'Développer', description: 'Élaborer et enrichir le texte', icon: Expand, category: 'IA Note', isAi: true, aiOption: 'clarify', command: () => { }
  },
  // Formatting extensions
  { title: 'Bold', description: 'Make text bold', icon: Bold, category: 'Formatting', command: (e) => e.chain().focus().toggleBold().run() },
  { title: 'Italic', description: 'Make text italic', icon: Italic, category: 'Formatting', command: (e) => e.chain().focus().toggleItalic().run() },
  { title: 'Underline', description: 'Underline text', icon: UnderlineIcon, category: 'Formatting', command: (e) => e.chain().focus().toggleUnderline().run() },
  { title: 'Strike', description: 'Strikethrough text', icon: Strikethrough, category: 'Formatting', command: (e) => e.chain().focus().toggleStrike().run() },
  { title: 'Highlight', description: 'Highlight text', icon: Highlighter, category: 'Formatting', command: (e) => e.chain().focus().toggleHighlight().run() },
  { title: 'Superscript', description: 'Text above the baseline', icon: SuperscriptIcon, category: 'Formatting', command: (e) => e.chain().focus().toggleSuperscript().run() },
  { title: 'Subscript', description: 'Text below the baseline', icon: SubscriptIcon, category: 'Formatting', command: (e) => e.chain().focus().toggleSubscript().run() },
  // AI Tools
  {
    title: 'Diagramme', description: 'Générer un diagramme Excalidraw', icon: BookOpen, category: 'IA Note', command: (e) => {
      const event = new CustomEvent('memento-open-ai', { detail: { tab: 'actions', scroll: 'diagram' } })
      window.dispatchEvent(event)
    }
  },
  {
    title: 'Présentation', description: 'Générer des slides HTML/PPTX', icon: Presentation, category: 'IA Note', command: (e) => {
      const event = new CustomEvent('memento-open-ai', { detail: { tab: 'actions', scroll: 'slides' } })
      window.dispatchEvent(event)
    }
  },
  {
    title: 'Suggest Charts', description: 'AI suggère des graphiques basés sur votre contenu', icon: BarChart3, category: 'IA Note', isAi: true, command: (e) => {
      // Handler will be called by SlashCommandMenu
    }
  },
  {
    title: 'Living Block', description: 'Insérer un bloc vivant depuis une autre note', icon: Link2, category: 'Basic blocks', shortcut: '/bloc',
    command: (_e) => {
      window.dispatchEvent(new CustomEvent('memento-open-block-picker'))
    }
  },
  {
    title: 'Database', description: 'Inline database', icon: Database, category: 'Basic blocks', shortcut: '/database',
    command: (e) => { insertStructuredViewBlockAtSelection(e) },
  },
  {
    title: 'Toggle', description: 'Collapsible section', icon: ChevronsRightLeft, category: 'Basic blocks', shortcut: '>',
    command: (e) => { insertToggleBlock(e) },
  },
  {
    title: 'Callout', description: 'Highlighted info box', icon: MessageSquareWarning, category: 'Basic blocks', shortcut: '!',
    command: (e) => { insertCalloutBlock(e, 'info') },
  },
  {
    title: 'Outline', description: 'Table of contents from headings', icon: ListTree, category: 'Basic blocks', shortcut: '/toc',
    command: (e) => { insertOutlineBlock(e) },
  },
  {
    title: 'Link Preview', description: 'Embed a URL as a rich card', icon: Link2, category: 'Basic blocks', shortcut: '/link',
    command: (e) => {
      window.dispatchEvent(new CustomEvent('memento-open-link-preview'))
    },
  },
  {
    title: 'Math', description: 'LaTeX equation block', icon: FunctionSquare, category: 'Basic blocks', shortcut: '$$',
    command: (e) => { insertMathEquation(e) },
  },
  {
    title: 'Columns', description: 'Side-by-side layout', icon: Columns3, category: 'Basic blocks', shortcut: '/cols',
    command: (e) => { insertColumnsBlock(e, 2) },
  },
  {
    title: 'Écrire avec l\'IA', description: 'Générer du contenu au curseur', icon: Sparkles, category: 'IA Note', isAi: true, aiOption: 'write', command: () => { }
  },
]

async function aiReformulate(text: string, option: string, t: any, language?: string): Promise<string> {
  const res = await fetch('/api/ai/reformulate', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ text, option, format: 'html', language }),
  })
  const data = await res.json()
  if (!res.ok) {
    if (data?.errorKey === 'ai.wordCountMin') {
      throw new Error(t('ai.wordCountMin') || `Minimum ${data?.params?.min || 10} mots requis (${data?.params?.current || 0} actuels)`)
    }
    if (data?.errorKey === 'ai.wordCountMax') {
      throw new Error(t('ai.wordCountMax') || `Maximum ${data?.params?.max || 500} mots (${data?.params?.current || 0} actuels)`)
    }
    if (data?.errorKey === 'ai.featureLocked') {
      throw new Error(t('ai.featureLocked') || 'Cette fonctionnalité nécessite le plan PRO.')
    }
    if (data?.errorKey === 'ai.quotaExceeded') {
      throw new Error(t('ai.quotaExceeded') || 'Limite mensuelle atteinte.')
    }
    if (data?.quotaExceeded) {
      throw new Error(t('ai.quotaExceeded') || 'Limite mensuelle atteinte.')
    }
    const serverMsg = typeof data?.error === 'string' && !data.error.includes(':') ? data.error.trim() : AI_REFORMULATE_FALLBACK
    throw new Error(serverMsg)
  }
  return data.reformulatedText || data.text || text
}

function useImageInsert() {
  const [open, setOpen] = useState(false)
  const editorRef = useRef<Editor | null>(null)

  const requestInsert = useCallback((editor: Editor) => {
    editorRef.current = editor
    setOpen(true)
  }, [])

  const confirm = useCallback((url: string) => {
    if (url.trim() && editorRef.current) {
      editorRef.current.chain().focus().setImage({ src: url.trim() }).run()
    }
    setOpen(false)
    editorRef.current = null
  }, [])

  const cancel = useCallback(() => {
    setOpen(false)
    editorRef.current = null
  }, [])

  return { open, requestInsert, confirm, cancel }
}

export const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorProps>(
  function RichTextEditor({ content, onChange, onChangeImmediate, className, placeholder, onImageUpload, noteId, notebookId, noteTitle, sourceUrl }, ref) {
    const { t } = useLanguage()
    const { requestAiConsent } = useAiConsent()
    const imageInsert = useImageInsert()
    const [blockPickerOpen, setBlockPickerOpen] = useState(false)
    const [blockMenuState, setBlockMenuState] = useState<{
      anchor: DOMRect
      pos: number
      node: PMNode | null
    } | null>(null)
    const dragBlockRef = useRef<{ node: PMNode | null; pos: number }>({ node: null, pos: -1 })
    const smartPastePendingRef = useRef<{
      reference: ParsedBlockReference
      blockPos: number
      blockNode: PMNode
      blockStatus?: { exists: boolean; content: string; sourceNoteTitle: string }
    } | null>(null)
    const [smartPasteMenu, setSmartPasteMenu] = useState<{
      anchor: { top: number; left: number }
      reference: ParsedBlockReference
      sourceNoteTitle?: string
    } | null>(null)
    const [smartPasteExtended, setSmartPasteExtended] = useState<{
      type: 'url' | 'code'
      text: string
      anchor: { top: number; left: number }
      isImage?: boolean
      isVideo?: boolean
    } | null>(null)
    const [isMobile, setIsMobile] = useState(false)
    const [actionSheetOpen, setActionSheetOpen] = useState(false)
    const [showFindReplace, setShowFindReplace] = useState(false)
    const [linkPreviewUrl, setLinkPreviewUrl] = useState<string | null>(null)

    useEffect(() => {
      const handler = () => setLinkPreviewUrl('')
      window.addEventListener('memento-open-link-preview', handler)
      return () => window.removeEventListener('memento-open-link-preview', handler)
    }, [])

    useEffect(() => {
      const handleFindShortcut = (e: KeyboardEvent) => {
        if ((e.metaKey || e.ctrlKey) && e.key === 'f' && !e.shiftKey) {
          const target = e.target as HTMLElement
          if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') return
          e.preventDefault()
          setShowFindReplace(v => !v)
        }
      }
      document.addEventListener('keydown', handleFindShortcut)
      return () => document.removeEventListener('keydown', handleFindShortcut)
    }, [])
    const [noteLinkPickerOpen, setNoteLinkPickerOpen] = useState(false)
    const [noteLinkQuery, setNoteLinkQuery] = useState('')
    const noteLinkRangeRef = useRef<{ from: number; to: number } | null>(null)
    const noteLinkPickerOpenRef = useRef(false)
    noteLinkPickerOpenRef.current = noteLinkPickerOpen
    const lastEmittedContent = useRef<string>(content || '')
    const editorInstanceRef = useRef<Editor | null>(null)
    const onChangeRef = useRef(onChange)
    onChangeRef.current = onChange
    const onChangeImmediateRef = useRef(onChangeImmediate)
    onChangeImmediateRef.current = onChangeImmediate
    const htmlDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null)

    const emitContentChange = useCallback((html: string, opts?: { immediate?: boolean }) => {
      lastEmittedContent.current = html
      // Cancel pending debounced HTML emit so it cannot overwrite with stale doc
      if (htmlDebounceRef.current) {
        clearTimeout(htmlDebounceRef.current)
        htmlDebounceRef.current = null
      }
      if (opts?.immediate && onChangeImmediateRef.current) {
        onChangeImmediateRef.current(html)
      } else {
        onChangeRef.current?.(html)
      }
    }, [])

    // Debounced version for onUpdate — avoids calling getHTML() on every keystroke
    const emitContentChangeDebounced = useCallback((editor: any) => {
      if (htmlDebounceRef.current) clearTimeout(htmlDebounceRef.current)
      htmlDebounceRef.current = setTimeout(() => {
        emitContentChange(editor.getHTML())
      }, 400)
    }, [emitContentChange])

    useEffect(() => {
      if (typeof window === 'undefined') return
      const checkMobile = () => setIsMobile(window.innerWidth < 768)
      checkMobile()
      window.addEventListener('resize', checkMobile)
      return () => window.removeEventListener('resize', checkMobile)
    }, [])

    // Listen to the slash-command event to open the BlockPicker
    useEffect(() => {
      const openHandler = () => setBlockPickerOpen(true)
      window.addEventListener('memento-open-block-picker', openHandler)
      return () => window.removeEventListener('memento-open-block-picker', openHandler)
    }, [])

    const handleSelectBlockRef = useRef<(block: BlockSuggestion) => void>(() => {})
    const insertCitationRef = useRef<(payload: { noteId: string; noteTitle: string; excerpt: string }, options?: { atEnd?: boolean }) => boolean>(() => false)

    useEffect(() => {
      const insertHandler = (event: Event) => {
        const block = (event as CustomEvent<{ block: BlockSuggestion }>).detail?.block
        if (block) handleSelectBlockRef.current(block)
      }
      const citationHandler = (event: Event) => {
        const detail = (event as CustomEvent<{ noteId: string; noteTitle: string; excerpt: string; atEnd?: boolean }>).detail
        if (!detail) return
        insertCitationRef.current(detail, { atEnd: detail.atEnd !== false })
      }
      window.addEventListener('memento-insert-live-block', insertHandler)
      window.addEventListener('memento-insert-citation', citationHandler)
      return () => {
        window.removeEventListener('memento-insert-live-block', insertHandler)
        window.removeEventListener('memento-insert-citation', citationHandler)
      }
    }, [])

    const handleSelectNoteLink = useCallback((selected: NoteLinkOption) => {
      const ed = editorInstanceRef.current
      if (!ed) {
        setNoteLinkPickerOpen(false)
        return
      }

      let range = noteLinkRangeRef.current
      if (!range) {
        const { from } = ed.state.selection
        const start = Math.max(0, from - 80)
        const textBefore = ed.state.doc.textBetween(start, from, '\n', '')
        const match = textBefore.match(/\[\[([^\]]*)$/)
        if (match) {
          range = { from: from - match[0].length, to: from }
        }
      }
      if (!range) {
        setNoteLinkPickerOpen(false)
        return
      }

      const title = (selected.title || t('documentInfo.network.untitled')).trim()
      const href = `/home?openNote=${encodeURIComponent(selected.id)}`

      ed.chain()
        .focus()
        .deleteRange({ from: range.from, to: range.to })
        .insertContent({
          type: 'text',
          text: title,
          marks: [{
            type: 'link',
            attrs: {
              href,
              target: '_blank',
              rel: 'noopener noreferrer',
            },
          }],
        })
        .insertContent(' ')
        .run()

      const html = ed.getHTML()
      emitContentChange(html)
      setNoteLinkPickerOpen(false)
      noteLinkRangeRef.current = null

      if (noteId) {
        window.dispatchEvent(new CustomEvent(NOTE_REQUEST_SAVE_EVENT, {
          detail: { noteId, reason: 'note-link' },
        }))
      }
    }, [emitContentChange, noteId, t])

    const editor = useEditor({
      extensions: [
        StarterKit.configure({ heading: { levels: [1, 2, 3] }, link: false, underline: false }),
        Underline,
        TiptapLink.configure({ openOnClick: false, autolink: true }),
        Highlight.configure({ multicolor: false }),
        CustomImage.configure({ inline: false, allowBase64: true }),
        TextAlign.configure({ types: ['heading', 'paragraph', 'image'] }),
        TaskList,
        TaskItem.configure({ nested: true }),
        Table.configure({ resizable: true }),
        TableRow,
        TableHeader,
        TableCell,
        Superscript,
        Subscript,
        Typography,
        ChartExtension,
        UniqueIdExtension,
        ...globalDragHandleExtensions,
        SmartPasteExtension,
        BlockSelectionExtension,
        MarkdownPasteExtension,
        TurnIntoShortcutExtension,
        UndoRedoFeedbackExtension,
        LiveBlockExtension,
        StructuredViewBlockExtension,
        ToggleExtension,
        CalloutExtension,
        OutlineExtension,
        FindReplaceExtension,
        LinkPreviewExtension,
        MathEquationExtension,
        InlineMathExtension,
        ColumnsExtension,
        ColumnNode,
        ClipArticleExtension,
        RtlPreserveExtension,
        Placeholder.configure({
          placeholder: ({ node }) => {
            if (node.type.name === 'heading') {
              const level = node.attrs.level
              if (level === 1) return t('richTextEditor.placeholderH1') || 'Titre principal...'
              if (level === 2) return t('richTextEditor.placeholderH2') || 'Titre de section...'
              if (level === 3) return t('richTextEditor.placeholderH3') || 'Sous-titre...'
            }
            if (node.type.name === 'taskItem') {
              return t('richTextEditor.placeholderTodo') || 'Ajouter une tâche...'
            }
            if (node.type.name === 'codeBlock') {
              return t('richTextEditor.placeholderCode') || 'Écrire du code...'
            }
            if (node.type.name === 'blockquote') {
              return t('richTextEditor.placeholderQuote') || 'Saisir une citation...'
            }
            return placeholder || t('richTextEditor.placeholderText') || t('richTextEditor.placeholder') || "Tapez '/' pour insérer un bloc..."
          }
        }),
      ],
      content: content || '',
      immediatelyRender: false,
      shouldRerenderOnTransaction: false,
      editorProps: {
        attributes: { class: 'notion-editor tiptap' },
        handleDOMEvents: {
          keydown: (view, event) => {
            if (event.defaultPrevented) return false
            if (event.key !== 'Enter' && event.key !== ' ') return false
            const { from, empty } = view.state.selection
            if (!empty) return false
            const textBefore = view.state.doc.textBetween(Math.max(0, from - 32), from, '\n')
            if (!/\/(database|db|vue|tableau|structured)$/i.test(textBefore)) return false
            event.preventDefault()
            const slashIdx = textBefore.lastIndexOf('/')
            const deleteFrom = from - (textBefore.length - slashIdx)
            const ed = editorInstanceRef.current
            if (!ed) return false
            ed.chain().focus().deleteRange({ from: deleteFrom, to: from }).run()
            if (!insertStructuredViewBlockAtSelection(ed, notebookId)) {
              toast.error(t('structuredViewBlock.loadError') || 'Impossible de charger les données structurées.')
            }
            return true
          },
          click: (_view, event) => {
            const link = (event.target as HTMLElement).closest('a[href]')
            if (!link) return false
            const href = link.getAttribute('href') || ''
            const noteIdMatch = href.match(/[?&]openNote=([^&#]+)/)
            if (noteIdMatch) {
              event.preventDefault()
              event.stopPropagation()
              const targetId = decodeURIComponent(noteIdMatch[1])
              const blockMatch = href.match(/#block-([^&#]+)/)
              openNotePeek({
                noteId: targetId,
                blockId: blockMatch ? decodeURIComponent(blockMatch[1]) : undefined,
              })
              return true
            }
            if (href.startsWith('http://') || href.startsWith('https://')) {
              event.preventDefault()
              event.stopPropagation()
              window.open(href, '_blank', 'noopener,noreferrer')
              return true
            }
            return false
          },
        },
        handlePaste: (_view, event) => {
          if (!onImageUpload) return false
          const items = Array.from(event.clipboardData?.items || [])
          const hasImage = items.some(item => item.type.startsWith('image/'))
          if (!hasImage) return false
          event.preventDefault()
          event.stopPropagation()
          const imageFiles = items
            .filter(item => item.type.startsWith('image/'))
            .map(item => item.getAsFile())
            .filter((file): file is File => file !== null)

          void (async () => {
            for (const file of imageFiles) {
              try {
                toast.info(t('notes.uploading'))
                const url = await onImageUpload(file)
                const ed = editorInstanceRef.current
                if (!ed || ed.isDestroyed) continue
                // Insert at cursor; force immediate parent sync so save/ref don't drop the img
                const inserted = ed.chain().focus().setImage({ src: url }).run()
                if (inserted) {
                  const html = ed.getHTML()
                  emitContentChange(html, { immediate: true })
                  // Prefetch so the img is warm in the browser cache
                  const preload = new window.Image()
                  preload.src = url
                  toast.success(t('notes.uploadSuccess') || 'Image ajoutée')
                } else {
                  toast.error(t('notes.uploadFailed') || 'Insertion image échouée')
                }
              } catch (err) {
                console.error('[editor] image paste failed:', err)
                toast.error(t('notes.uploadFailed'))
              }
            }
          })()
          return true
        },
        // ProseMirror signature: (view, pos, event)
        handleClick: (view, pos, event) => {
          // Click on img → NodeSelection so Backspace/Delete and bubble toolbar work
          const target = event.target as HTMLElement | null
          if (!target || target.tagName !== 'IMG') return false
          if (!view.editable) return false
          try {
            let imagePos: number | null = null
            const nodeAt = view.state.doc.nodeAt(pos)
            if (nodeAt?.type.name === 'image') {
              imagePos = pos
            } else {
              const $pos = view.state.doc.resolve(pos)
              if ($pos.nodeAfter?.type.name === 'image') {
                imagePos = $pos.pos
              } else if ($pos.nodeBefore?.type.name === 'image') {
                imagePos = $pos.pos - $pos.nodeBefore.nodeSize
              } else {
                // Walk up: pos may sit inside / around the leaf
                for (let d = $pos.depth; d > 0; d--) {
                  if ($pos.node(d).type.name === 'image') {
                    imagePos = $pos.before(d)
                    break
                  }
                }
              }
            }
            if (imagePos == null) {
              // Last resort: posAtDOM on the img element
              const domPos = view.posAtDOM(target, 0)
              const $dom = view.state.doc.resolve(domPos)
              if ($dom.nodeAfter?.type.name === 'image') imagePos = $dom.pos
              else if (view.state.doc.nodeAt(domPos)?.type.name === 'image') imagePos = domPos
            }
            if (imagePos == null) return false
            const node = view.state.doc.nodeAt(imagePos)
            if (!node || node.type.name !== 'image') return false
            const tr = view.state.tr.setSelection(NodeSelection.create(view.state.doc, imagePos))
            view.dispatch(tr)
            view.focus()
            return true
          } catch {
            return false
          }
        },
      },
      onUpdate: ({ editor: e, transaction }) => {
        // Image removed → immediate parent sync so a stale content prop cannot resurrect it
        if (transaction.docChanged && (lastEmittedContent.current || '').includes('<img')) {
          const html = e.getHTML()
          if (!html.includes('<img')) {
            emitContentChange(html, { immediate: true })
            return
          }
        }
        // Debounced getHTML() — avoids traversing the whole doc on every keystroke
        emitContentChangeDebounced(e)
        if (!e.isEditable) return
        const { from, empty } = e.state.selection
        if (!empty) return
        const start = Math.max(0, from - 80)
        const textBefore = e.state.doc.textBetween(start, from, '\n', '')
        const match = textBefore.match(/\[\[([^\]]*)$/)
        if (match) {
          noteLinkRangeRef.current = { from: from - match[0].length, to: from }
          setNoteLinkQuery(match[1])
          setNoteLinkPickerOpen(true)
        } else if (!noteLinkPickerOpenRef.current) {
          setNoteLinkPickerOpen(false)
          noteLinkRangeRef.current = null
        }
      },
      onCreate: ({ editor: e }) => {
        requestAnimationFrame(() => {
          applyClipRtlDirection(e, { sourceUrl })
        })
      },
    })

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

    useEffect(() => {
      if (!editor) return
      editor.storage.liveBlock.hostNoteId = noteId ?? null
    }, [editor, noteId])

    useEffect(() => {
      if (!editor) return
      if ((editor.storage as any).structuredViewBlock) {
        (editor.storage as any).structuredViewBlock.notebookId = notebookId ?? null
      }
    }, [editor, notebookId])

    useEffect(() => {
      if (!editor) return

      editor.storage.smartPaste.onPaste = (view, event) => {
        const clipboardText = event.clipboardData?.getData('text/plain') ?? ''
        let blockRef = parseBlockReferenceFromText(clipboardText)
        if (!blockRef) {
          const recalled = recallLastBlockReference()
          if (recalled && (!clipboardText.trim() || clipboardText.trim() === recalled.raw)) {
            blockRef = recalled
          }
        }
        if (blockRef) {
          const emptyParagraph = getEmptyParagraphAtSelection(view.state)
          if (!emptyParagraph) return false

          event.preventDefault()
          event.stopPropagation()

          const coords = view.coordsAtPos(view.state.selection.from)
          smartPastePendingRef.current = {
            reference: blockRef,
            blockPos: emptyParagraph.pos,
            blockNode: emptyParagraph.node,
          }

          queueMicrotask(() => {
            setSmartPasteMenu({
              anchor: { top: coords.bottom, left: coords.left },
              reference: blockRef,
            })
          })

          void fetch(
            `/api/blocks/${encodeURIComponent(blockRef.blockId)}/status?sourceNoteId=${encodeURIComponent(blockRef.sourceNoteId)}`,
          )
            .then((res) => (res.ok ? res.json() : null))
            .then((data: { content?: string; sourceNoteTitle?: string; exists?: boolean } | null) => {
              if (smartPastePendingRef.current?.reference.raw !== blockRef.raw) return
              const recalled = recallLastBlockReference()
              const sessionFallback =
                recalled?.raw === blockRef.raw
                  ? {
                      content: recalled.blockContent?.trim() ?? '',
                      sourceNoteTitle: recalled.sourceNoteTitle?.trim() ?? '',
                    }
                  : { content: '', sourceNoteTitle: '' }
              const exists = Boolean(data?.exists) || sessionFallback.content.length > 0
              const content = data?.exists ? (data.content ?? '') : (sessionFallback.content || data?.content || '')
              const sourceNoteTitle = data?.sourceNoteTitle || sessionFallback.sourceNoteTitle || ''
              smartPastePendingRef.current!.blockStatus = {
                exists,
                content,
                sourceNoteTitle,
              }
              setSmartPasteMenu((prev) =>
                prev?.reference.raw === blockRef.raw
                  ? { ...prev, sourceNoteTitle: sourceNoteTitle || prev.sourceNoteTitle }
                  : prev,
              )
            })
            .catch(() => {})

          return true
        }

        // Détection d'URL HTTP(S) cliquable ou média
        const isUrl = /^https?:\/\/[^\s]+$/i.test(clipboardText.trim())
        if (isUrl) {
          const url = clipboardText.trim()
          event.preventDefault()
          event.stopPropagation()
          const coords = view.coordsAtPos(view.state.selection.from)
          const isImage = /\.(jpeg|jpg|gif|png|svg|webp)($|\?)/i.test(url)
          const isVideo = /(youtube\.com|youtu\.be|vimeo\.com|streamable\.com)/i.test(url) || /\.(mp4|webm|ogg)($|\?)/i.test(url)
          
          setSmartPasteExtended({
            type: 'url',
            text: url,
            anchor: { top: coords.bottom, left: coords.left },
            isImage,
            isVideo,
          })
          return true
        }

        // Détection de code source technique
        const hasCodeSpecialChars = /[{}[\];]/.test(clipboardText)
        const hasCodeKeywords = /(const\s+\w+\s*=|let\s+\w+\s*=|function\s+\w*\(|import\s+.*from|class\s+\w+|def\s+\w+\(|public\s+class\s+\w+|#include\s+<|import\s+react|var\s+\w+\s*=)/.test(clipboardText)
        const isMultiline = clipboardText.split('\n').length > 1
        if ((hasCodeSpecialChars && hasCodeKeywords) || (isMultiline && hasCodeSpecialChars && clipboardText.includes('('))) {
          event.preventDefault()
          event.stopPropagation()
          const coords = view.coordsAtPos(view.state.selection.from)
          
          setSmartPasteExtended({
            type: 'code',
            text: clipboardText,
            anchor: { top: coords.bottom, left: coords.left },
          })
          return true
        }

        return false
      }

      return () => {
        editor.storage.smartPaste.onPaste = null
      }
    }, [editor])

    // Chart suggestions dialog state
    const [chartSuggestionsOpen, setChartSuggestionsOpen] = useState(false)
    const [currentNoteContent, setCurrentNoteContent] = useState(content || '')

    useEffect(() => {
      if (editor && content !== undefined && content !== lastEmittedContent.current) {
        // While the user is editing, never clobber local deletes (e.g. removed image)
        // with a stale parent `content` that has not caught up yet.
        if (editor.isFocused) {
          return
        }
        const html = content || ''
        lastEmittedContent.current = html
        queueMicrotask(() => {
          if (!editor.isDestroyed) {
            editor.commands.setContent(html)
            requestAnimationFrame(() => {
              applyClipRtlDirection(editor, { sourceUrl })
            })
          }
        })
      }
      if (content !== undefined && !editor?.isFocused) {
        setCurrentNoteContent(content || '')
      }
    }, [content, editor, sourceUrl])

    // Chart suggestion handlers
    const handleOpenChartSuggestions = useCallback(async () => {
      if (!editor || !editor.isEditable) return
      const consented = await requestAiConsent()
      if (!consented) return

      setChartSuggestionsOpen(true)
    }, [editor, requestAiConsent])

    const insertCitationInEditor = useCallback((
      payload: { noteId: string; noteTitle: string; excerpt: string },
      options?: { atEnd?: boolean }
    ) => {
      if (!editor || !editor.isEditable) return false
      const plainExcerpt = stripHtmlToPlainText(payload.excerpt)
      if (!plainExcerpt) return false
      const isRtl = detectTextDirection(`${payload.noteTitle}\n${plainExcerpt}`) === 'rtl'
      const rtlAttrs = isRtl ? { dir: 'rtl' as const, lang: 'fa' as const } : {}
      const chain = editor.chain()
      if (options?.atEnd !== false) {
        chain.focus('end')
      } else {
        chain.focus()
      }
      chain.insertContent([
        { type: 'paragraph', content: [] },
        {
          type: 'blockquote',
          attrs: rtlAttrs.dir ? { dir: rtlAttrs.dir } : {},
          content: [{
            type: 'paragraph',
            attrs: rtlAttrs,
            content: [{ type: 'text', text: plainExcerpt }],
          }],
        },
        {
          type: 'paragraph',
          attrs: rtlAttrs,
          content: [
            { type: 'text', text: '— ' },
            {
              type: 'text',
              text: payload.noteTitle,
              marks: [{ type: 'link', attrs: { href: `/home?openNote=${payload.noteId}`, target: '_blank', rel: 'noopener noreferrer' } }],
            },
          ],
        },
      ]).scrollIntoView().run()
      emitContentChange(editor.getHTML())
      return true
    }, [editor, emitContentChange])

    const insertLiveBlockInEditor = useCallback((block: BlockSuggestion, options?: { atEnd?: boolean }) => {
      if (!editor || !editor.isEditable) return false
      const chain = editor.chain()
      if (options?.atEnd !== false) {
        chain.focus('end')
      } else {
        chain.focus()
      }
      chain.insertContent({
        type: 'liveBlock',
        attrs: {
          sourceNoteId: block.noteId,
          blockId: block.blockId,
          snapshotContent: block.content,
          sourceNoteTitle: block.noteTitle,
        },
      }).scrollIntoView().run()
      emitContentChange(editor.getHTML())
      return true
    }, [editor, emitContentChange])

    insertCitationRef.current = insertCitationInEditor

    useImperativeHandle(ref, () => ({
      getEditor: () => editor,
      triggerChartSuggestions: () => {
        if (editor) {
          handleOpenChartSuggestions()
        }
      },
      insertCitation: insertCitationInEditor,
      insertLiveBlock: insertLiveBlockInEditor,
    }), [editor, handleOpenChartSuggestions, insertCitationInEditor, insertLiveBlockInEditor])

    const handleSelectChart = useCallback((chartContent: string) => {
      if (!editor || !editor.isEditable) return

      try {
        // [debug removed]

        // Get current selection
        const { from, to, empty } = editor.state.selection

        // Insert chart AFTER the selected text (not replace it)
        // First insert a paragraph for spacing, then the chart, then another paragraph
        editor.chain()
          .focus()
          .insertContentAt(to, [
            {
              type: 'paragraph',
              content: []
            },
            {
              type: 'chartBlock',
              attrs: {
                code: chartContent,
                language: 'chart'
              }
            },
            {
              type: 'paragraph',
              content: []
            }
          ])
          .run()

        // [debug removed]
      } catch (error) {
        console.error('[handleSelectChart] Failed:', error)
        toast.error('Failed to insert chart: ' + (error as Error).message)
      }
    }, [editor])

    const handleSelectBlock = useCallback(async (block: BlockSuggestion) => {
      setBlockPickerOpen(false)
      if (!editor) return

      if (noteId) {
        try {
          await fetch('/api/blocks/embed', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ sourceNoteId: block.noteId, blockId: block.blockId, targetNoteId: noteId }),
          })
        } catch {
          // Non-fatal
        }
      }

      editor.chain().focus().insertContent({
        type: 'liveBlock',
        attrs: {
          sourceNoteId: block.noteId,
          blockId: block.blockId,
          snapshotContent: block.content,
          sourceNoteTitle: block.noteTitle,
        },
      }).run()
      emitContentChange(editor.getHTML())
    }, [editor, noteId, emitContentChange])

    handleSelectBlockRef.current = handleSelectBlock

    const openBlockActionMenu = useCallback((anchorRect: DOMRect) => {
      if (!editor) return
      const block = resolveBlockAtDragHandle(editor)
      if (!block) return
      dragBlockRef.current = block
      setBlockMenuState({ anchor: anchorRect, pos: block.pos, node: block.node })
    }, [editor])

    const closeBlockActionMenu = useCallback(() => {
      setBlockMenuState(null)
    }, [])

    const closeSmartPasteMenu = useCallback(() => {
      smartPastePendingRef.current = null
      setSmartPasteMenu(null)
    }, [])

    const handleBlockReferenceCopied = useCallback((html: string) => {
      emitContentChange(html)
      if (noteId) {
        window.dispatchEvent(new CustomEvent(NOTE_REQUEST_SAVE_EVENT, {
          detail: { noteId, reason: 'block-reference-copy' },
        }))
      }
    }, [emitContentChange, noteId])

    const fetchBlockStatus = useCallback(async (reference: ParsedBlockReference) => {
      const cached = smartPastePendingRef.current?.blockStatus
      if (cached && smartPastePendingRef.current?.reference.raw === reference.raw) {
        return cached
      }

      const recalled = recallLastBlockReference()
      const sessionFallback =
        recalled?.raw === reference.raw
          ? {
              content: recalled.blockContent?.trim() ?? '',
              sourceNoteTitle: recalled.sourceNoteTitle?.trim() ?? '',
            }
          : { content: '', sourceNoteTitle: '' }

      const res = await fetch(
        `/api/blocks/${encodeURIComponent(reference.blockId)}/status?sourceNoteId=${encodeURIComponent(reference.sourceNoteId)}`,
      )
      if (!res.ok) {
        return {
          exists: sessionFallback.content.length > 0,
          content: sessionFallback.content,
          sourceNoteTitle: sessionFallback.sourceNoteTitle,
        }
      }
      const data = await res.json()
      if (data.exists) {
        return {
          exists: true,
          content: data.content ?? '',
          sourceNoteTitle: data.sourceNoteTitle ?? '',
        }
      }
      return {
        exists: sessionFallback.content.length > 0,
        content: sessionFallback.content || data.content || '',
        sourceNoteTitle: sessionFallback.sourceNoteTitle || data.sourceNoteTitle || '',
      }
    }, [])

    const handleSmartPasteLive = useCallback(async () => {
      const pending = smartPastePendingRef.current
      const ed = editorInstanceRef.current
      if (!pending || !ed) {
        closeSmartPasteMenu()
        return
      }

      const status = await fetchBlockStatus(pending.reference)

      if (noteId) {
        try {
          await fetch('/api/blocks/embed', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
              sourceNoteId: pending.reference.sourceNoteId,
              blockId: pending.reference.blockId,
              targetNoteId: noteId,
            }),
          })
        } catch {
          // Non-fatal
        }
      }

      const liveBlockType = ed.schema.nodes.liveBlock
      if (liveBlockType) {
        ed.chain()
          .focus()
          .command(({ tr, dispatch }) => {
            const node = liveBlockType.create({
              sourceNoteId: pending.reference.sourceNoteId,
              blockId: pending.reference.blockId,
              snapshotContent: status.content,
              sourceNoteTitle: status.sourceNoteTitle,
            })
            tr.replaceWith(pending.blockPos, pending.blockPos + pending.blockNode.nodeSize, node)
            if (dispatch) dispatch(tr)
            return true
          })
          .run()
        emitContentChange(ed.getHTML())
        if (noteId) {
          window.dispatchEvent(new CustomEvent(NOTE_REQUEST_SAVE_EVENT, {
            detail: { noteId, reason: 'smart-paste-live-block' },
          }))
        }
      }

      closeSmartPasteMenu()
    }, [closeSmartPasteMenu, emitContentChange, fetchBlockStatus, noteId])

    const handleSmartPastePlain = useCallback(async () => {
      const pending = smartPastePendingRef.current
      const ed = editorInstanceRef.current
      if (!pending || !ed) {
        closeSmartPasteMenu()
        return
      }

      const status = await fetchBlockStatus(pending.reference)
      const linkHref = pending.reference.raw.match(/^https?:\/\//)
        ? pending.reference.raw
        : `${window.location.origin}/home?openNote=${encodeURIComponent(pending.reference.sourceNoteId)}#block-${pending.reference.blockId}`
      const linkText = status.sourceNoteTitle?.trim() || pending.reference.raw

      ed.chain()
        .focus()
        .insertContent({
          type: 'text',
          text: linkText,
          marks: [{
            type: 'link',
            attrs: {
              href: linkHref,
              target: '_blank',
              rel: 'noopener noreferrer',
            },
          }],
        })
        .run()

      emitContentChange(ed.getHTML())
      closeSmartPasteMenu()
    }, [closeSmartPasteMenu, emitContentChange, fetchBlockStatus])

    const handlePasteUrlLink = useCallback((url: string) => {
      if (!editor) return
      const { from, to, empty } = editor.state.selection
      if (empty) {
        editor.chain().focus().insertContent(`<a href="${url}">${url}</a>`).run()
      } else {
        editor.chain().focus().setLink({ href: url }).run()
      }
      setSmartPasteExtended(null)
      emitContentChange(editor.getHTML())
    }, [editor, emitContentChange])

    const handlePasteUrlImage = useCallback((url: string) => {
      if (!editor) return
      editor.chain().focus().setImage({ src: url }).run()
      setSmartPasteExtended(null)
      emitContentChange(editor.getHTML(), { immediate: true })
    }, [editor, emitContentChange])

    const handlePasteUrlVideo = useCallback((url: string) => {
      if (!editor) return
      const { from, to, empty } = editor.state.selection
      if (empty) {
        editor.chain().focus().insertContent(`🎥 <a href="${url}">${t('richTextEditor.slashVideo') || 'Vidéo'} (${url})</a>`).run()
      } else {
        editor.chain().focus().setLink({ href: url }).run()
      }
      setSmartPasteExtended(null)
      emitContentChange(editor.getHTML())
    }, [editor, emitContentChange, t])

    const handlePasteCodeBlock = useCallback((code: string) => {
      if (!editor) return
      editor.chain().focus().insertContent({
        type: 'codeBlock',
        content: [{ type: 'text', text: code }]
      }).run()
      setSmartPasteExtended(null)
      emitContentChange(editor.getHTML())
    }, [editor, emitContentChange])

    const handlePastePlain = useCallback((text: string) => {
      if (!editor) return
      editor.chain().focus().insertContent(text).run()
      setSmartPasteExtended(null)
      emitContentChange(editor.getHTML())
    }, [editor, emitContentChange])

    return (
      <div className={cn('notion-editor-wrapper', className)}>
        {editor && (
          <BubbleMenu
            editor={editor}
            className="notion-bubble-menu"
            {...({
              tippyOptions: {
                appendTo: () => document.body,
                zIndex: 99999,
                fallbackPlacements: ['bottom', 'top']
              }
            } as any)}
            shouldShow={({ editor: e, state }: { editor: Editor; state: EditorState }) => {
              const { from, to } = state.selection
              const isImage = e.isActive('image')
              return (from !== to && !e.isActive('codeBlock')) || isImage
            }}
          >
            <BubbleToolbar editor={editor} onSuggestCharts={handleOpenChartSuggestions} />
          </BubbleMenu>
        )}

        {editor && <SlashCommandMenu editor={editor} onInsertImage={imageInsert.requestInsert} onSuggestCharts={handleOpenChartSuggestions} />}

        <EditorBlockDragHandle editor={editor} onOpenMenu={openBlockActionMenu} />

        <EditorContent editor={editor} />

        {editor && showFindReplace && (
          <FindReplaceBar editor={editor} onClose={() => setShowFindReplace(false)} />
        )}

        {editor && linkPreviewUrl !== null && (
          <div className="fixed inset-0 z-[9998] flex items-center justify-center bg-black/30" onClick={() => setLinkPreviewUrl(null)}>
            <div className="bg-popover rounded-xl border border-border shadow-lg w-96 max-w-[90vw] p-4" onClick={(e) => e.stopPropagation()} dir="auto">
              <div className="text-sm font-medium mb-3">{t('richTextEditor.linkPreviewModalTitle')}</div>
              <input
                type="url"
                autoFocus
                value={linkPreviewUrl}
                onChange={(e) => setLinkPreviewUrl(e.target.value)}
                onKeyDown={(e) => {
                  e.stopPropagation()
                  if (e.key === 'Enter') {
                    e.preventDefault()
                    if (linkPreviewUrl.trim()) {
                      insertLinkPreview(editor, linkPreviewUrl.trim())
                      setLinkPreviewUrl(null)
                    }
                  }
                  if (e.key === 'Escape') setLinkPreviewUrl(null)
                }}
                placeholder="https://..."
                className="w-full rounded-md border border-border bg-background px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-primary/30"
              />
              <div className="flex justify-end gap-2 mt-3">
                <button onClick={() => setLinkPreviewUrl(null)} className="px-3 py-1.5 text-sm rounded-md hover:bg-muted transition-colors">
                  {t('richTextEditor.imageModalCancel')}
                </button>
                <button
                  onClick={() => {
                    if (linkPreviewUrl.trim()) {
                      insertLinkPreview(editor, linkPreviewUrl.trim())
                      setLinkPreviewUrl(null)
                    }
                  }}
                  disabled={!linkPreviewUrl.trim()}
                  className="px-3 py-1.5 text-sm rounded-md bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-40 transition-colors"
                >
                  {t('richTextEditor.linkPreviewModalInsert')}
                </button>
              </div>
            </div>
          </div>
        )}

        {editor && blockMenuState && (
          <BlockActionMenu
            editor={editor}
            anchorRect={blockMenuState.anchor}
            blockPos={blockMenuState.pos}
            blockNode={blockMenuState.node}
            noteId={noteId}
            sourceNoteTitle={noteTitle}
            onBlockReferenceCopied={handleBlockReferenceCopied}
            onClose={closeBlockActionMenu}
          />
        )}

        {smartPasteMenu && (
          <SmartPasteMenu
            anchor={smartPasteMenu.anchor}
            reference={smartPasteMenu.reference}
            sourceNoteTitle={smartPasteMenu.sourceNoteTitle}
            onLive={() => { void handleSmartPasteLive() }}
            onPlain={() => { void handleSmartPastePlain() }}
            onClose={closeSmartPasteMenu}
          />
        )}

        {smartPasteExtended && (
          <SmartPasteExtendedMenu
            type={smartPasteExtended.type}
            text={smartPasteExtended.text}
            anchor={smartPasteExtended.anchor}
            isImage={smartPasteExtended.isImage}
            isVideo={smartPasteExtended.isVideo}
            onLink={() => handlePasteUrlLink(smartPasteExtended.text)}
            onLinkPreview={() => { insertLinkPreview(editor, smartPasteExtended.text); setSmartPasteExtended(null) }}
            onImage={() => handlePasteUrlImage(smartPasteExtended.text)}
            onVideo={() => handlePasteUrlVideo(smartPasteExtended.text)}
            onCodeBlock={() => handlePasteCodeBlock(smartPasteExtended.text)}
            onPlain={() => handlePastePlain(smartPasteExtended.text)}
            onClose={() => setSmartPasteExtended(null)}
          />
        )}

        {editor && isMobile && (
          <MobileEditorToolbar
            editor={editor}
            onOpenActionSheet={() => setActionSheetOpen(true)}
            onInsertImage={imageInsert.requestInsert as any}
          />
        )}

        {editor && actionSheetOpen && (
          <MobileActionSheet
            editor={editor}
            isOpen={actionSheetOpen}
            onClose={() => setActionSheetOpen(false)}
          />
        )}

        {imageInsert.open && (
          <ImageModal onConfirm={imageInsert.confirm} onCancel={imageInsert.cancel} />
        )}

        {chartSuggestionsOpen && editor && (
          <ChartSuggestionsDialog
            isOpen={chartSuggestionsOpen}
            content={currentNoteContent}
            selection={editor.state.selection.empty ? null : editor.state.doc.textBetween(editor.state.selection.from, editor.state.selection.to, ' ')}
            onClose={() => setChartSuggestionsOpen(false)}
            onSelectChart={handleSelectChart}
          />
        )}

        <BlockPicker
          isOpen={blockPickerOpen}
          onClose={() => setBlockPickerOpen(false)}
          currentNoteId={noteId}
          onSelectBlock={handleSelectBlock}
        />

        <NoteLinkPicker
          isOpen={noteLinkPickerOpen}
          query={noteLinkQuery}
          currentNoteId={noteId}
          onClose={() => {
            setNoteLinkPickerOpen(false)
            noteLinkRangeRef.current = null
          }}
          onSelect={handleSelectNoteLink}
        />
      </div>
    )
  }
)

function ImageModal({ onConfirm, onCancel }: { onConfirm: (url: string) => void; onCancel: () => void }) {
  const { t } = useLanguage()
  const [url, setUrl] = useState('')
  const [preview, setPreview] = useState<string | null>(null)
  const [error, setError] = useState('')
  const inputRef = useRef<HTMLInputElement>(null)

  useEffect(() => { inputRef.current?.focus() }, [])

  const handleConfirm = () => {
    if (!url.trim()) return
    if (!/^https?:\/\/.+/i.test(url.trim())) { setError(t('richTextEditor.imageModalInvalidUrl')); return }
    onConfirm(url.trim())
  }

  return (
    <div className="notion-overlay" onClick={onCancel}>
      <div className="notion-image-modal" onClick={(e) => e.stopPropagation()}>
        <div className="text-sm font-medium mb-3">{t('richTextEditor.imageModalTitle')}</div>
        <input
          ref={inputRef}
          type="url"
          value={url}
          onChange={(e) => { setUrl(e.target.value); setError(''); setPreview(null) }}
          onKeyDown={(e) => { if (e.key === 'Enter') handleConfirm(); if (e.key === 'Escape') onCancel() }}
          placeholder={t('richTextEditor.imageUrlPlaceholder')}
          className="notion-modal-input"
        />
        {url.trim() && !preview && (
          <button onClick={() => setPreview(url.trim())} className="text-xs text-muted-foreground hover:text-foreground transition-colors underline mt-2">
            {t('richTextEditor.imageModalPreview')}
          </button>
        )}
        {preview && (
          <div className="mt-2 rounded-lg overflow-hidden border border-border max-h-40">
            <img src={preview} alt="" className="max-h-40 object-contain w-full" onError={() => { setPreview(null); setError(t('richTextEditor.imageModalLoadFailed')) }} />
          </div>
        )}
        {error && <div className="mt-1.5 text-xs text-destructive">{error}</div>}
        <div className="flex justify-end gap-2 mt-4">
          <button onClick={onCancel} className="notion-modal-btn">{t('richTextEditor.imageModalCancel')}</button>
          <button onClick={handleConfirm} className="notion-modal-btn notion-modal-btn-primary">{t('richTextEditor.imageModalInsert')}</button>
        </div>
      </div>
    </div>
  )
}

function BubbleToolbar({ editor, onSuggestCharts }: { editor: Editor | null; onSuggestCharts?: () => void }) {
  const { t, language } = useLanguage()
  const { requestAiConsent } = useAiConsent()

  const toastAiError = (err: unknown) => {
    const msg = err instanceof Error ? err.message : ''
    toast.error(msg && msg !== AI_REFORMULATE_FALLBACK ? msg : t('richTextEditor.aiReformulateFailed'))
  }

  // Wrap the current text selection in a container block (toggle or callout)
  const wrapSelectionInBlock = useCallback((blockType: string) => {
    if (!editor) return
    const { from, to } = editor.state.selection
    if (from === to) return

    // Get the selected slice (all blocks in the range)
    const slice = editor.state.doc.slice(from, to)
    const content = slice.content

    // Create the container node with the selected content inside
    const containerNode = editor.state.schema.nodes[blockType].create(
      blockType === 'calloutBlock' ? { type: 'info' } : { opened: true },
      content,
    )

    // Replace the selection with the container
    const tr = editor.state.tr
    tr.replaceRangeWith(from, to, containerNode)
    editor.view.dispatch(tr)
  }, [editor])

  const [aiOpen, setAiOpen] = useState(false)
  const [aiLoading, setAiLoading] = useState(false)
  const [linkOpen, setLinkOpen] = useState(false)
  const [linkUrl, setLinkUrl] = useState('')
  const linkInputRef = useRef<HTMLInputElement>(null)
  const [translateOpen, setTranslateOpen] = useState(false)
  const [customLang, setCustomLang] = useState('')
  const [aiModal, setAiModal] = useState<{ type: 'explain' | 'preview'; origText: string; html: string; from: number; to: number } | null>(null)

  const editorState = useEditorState({
    editor,
    selector: (ctx) => ({
      isBold: ctx.editor?.isActive('bold') ?? false,
      isItalic: ctx.editor?.isActive('italic') ?? false,
      isUnderline: ctx.editor?.isActive('underline') ?? false,
      isStrike: ctx.editor?.isActive('strike') ?? false,
      isCode: ctx.editor?.isActive('code') ?? false,
      isHighlight: ctx.editor?.isActive('highlight') ?? false,
      isSuperscript: ctx.editor?.isActive('superscript') ?? false,
      isSubscript: ctx.editor?.isActive('subscript') ?? false,
      isLink: ctx.editor?.isActive('link') ?? false,
      isImage: ctx.editor?.isActive('image') ?? false,
      selectionFrom: ctx.editor?.state?.selection?.from ?? 0,
      selectionTo: ctx.editor?.state?.selection?.to ?? 0,
    })
  })

  useEffect(() => {
    if (linkOpen && linkInputRef.current) linkInputRef.current.focus()
  }, [linkOpen])

  if (!editor || !editorState) return null

  const marks = [
    { icon: Bold, active: editorState.isBold, action: () => editor.chain().focus().toggleBold().run(), title: t('richTextEditor.bold') },
    { icon: Italic, active: editorState.isItalic, action: () => editor.chain().focus().toggleItalic().run(), title: t('richTextEditor.italic') },
    { icon: UnderlineIcon, active: editorState.isUnderline, action: () => editor.chain().focus().toggleUnderline().run(), title: t('richTextEditor.underline') },
    { icon: Strikethrough, active: editorState.isStrike, action: () => editor.chain().focus().toggleStrike().run(), title: t('richTextEditor.strike') },
    { icon: Code, active: editorState.isCode, action: () => editor.chain().focus().toggleCode().run(), title: t('richTextEditor.code') },
    { icon: Highlighter, active: editorState.isHighlight, action: () => editor.chain().focus().toggleHighlight().run(), title: t('richTextEditor.highlight') },
    { icon: SuperscriptIcon, active: editorState.isSuperscript, action: () => editor.chain().focus().toggleSuperscript().run(), title: t('richTextEditor.superscript') },
    { icon: SubscriptIcon, active: editorState.isSubscript, action: () => editor.chain().focus().toggleSubscript().run(), title: t('richTextEditor.subscript') },
  ]

  const handleAI = async (option: 'clarify' | 'shorten' | 'improve' | 'fix_grammar' | 'translate' | 'explain', targetLang?: string) => {
    const { from, to } = editor.state.selection
    const text = editor.state.doc.textBetween(from, to, ' ')
    if (!text || text.split(/\s+/).length < 2) return
    const consented = await requestAiConsent()
    if (!consented) return
    setAiLoading(true)
    setAiOpen(false)
    setTranslateOpen(false)
    try {
      const lang = option === 'translate' ? (targetLang || language) : language
      const result = await aiReformulate(text, option, t, lang)
      window.dispatchEvent(new Event('ai-usage-changed'))
      if (option === 'explain') {
        setAiModal({ type: 'explain', origText: text, html: result, from, to })
      } else {
        setAiModal({ type: 'preview', origText: text, html: result, from, to })
      }
    } catch (err) {
      console.error('AI error:', err)
      toastAiError(err)
    } finally {
      setAiLoading(false)
    }
  }

  const applyAiResult = () => {
    if (!aiModal || !editor) { setAiModal(null); return }
    const clean = aiModal.html.replace(/^<p>([\s\S]*)<\/p>$/, '$1').trim()
    editor.chain().focus().insertContentAt({ from: aiModal.from, to: aiModal.to }, clean).run()
    setAiModal(null)
  }

  const openLinkEditor = () => {
    const existing = editor.getAttributes('link').href || ''
    setLinkUrl(existing)
    setLinkOpen(true)
    setAiOpen(false)
  }

  const applyLink = () => {
    if (linkUrl.trim()) {
      editor.chain().focus().extendMarkRange('link').setLink({ href: linkUrl.trim() }).run()
    } else {
      editor.chain().focus().extendMarkRange('link').unsetLink().run()
    }
    setLinkOpen(false)
  }

  if (linkOpen) {
    return (
      <div className="flex items-center gap-1.5 px-2 py-1.5">
        <LinkIcon className="w-3.5 h-3.5 text-muted-foreground flex-shrink-0" />
        <input
          ref={linkInputRef}
          type="url"
          value={linkUrl}
          onChange={(e) => setLinkUrl(e.target.value)}
          onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); applyLink() }; if (e.key === 'Escape') { setLinkOpen(false); editor.chain().focus().run() } }}
          placeholder={t('richTextEditor.linkPlaceholder')}
          className="notion-inline-input"
        />
        <button onClick={applyLink} className="notion-bubble-btn notion-bubble-btn-active"><Check className="w-3.5 h-3.5" /></button>
        {editorState.isLink && (
          <button onClick={() => { editor.chain().focus().extendMarkRange('link').unsetLink().run(); setLinkOpen(false) }} className="notion-bubble-btn">
            <ExternalLink className="w-3.5 h-3.5" />
          </button>
        )}
        <button onClick={() => { setLinkOpen(false); editor.chain().focus().run() }} className="notion-bubble-btn"><X className="w-3.5 h-3.5" /></button>
      </div>
    )
  }

  return (
    <div className="flex items-center gap-0.5 px-1 py-0.5 relative">
      {marks.map((m, i) => (
        <button key={i} onClick={m.action} title={m.title} className={cn('notion-bubble-btn rounded-md', m.active && 'notion-bubble-btn-active')}>
          <m.icon className="w-3.5 h-3.5" />
        </button>
      ))}
      <div className="w-px h-4 bg-gray-300 dark:bg-gray-600 mx-0.5" />
      <button onClick={openLinkEditor} className={cn('notion-bubble-btn rounded-md', editorState.isLink && 'notion-bubble-btn-active')}><LinkIcon className="w-3.5 h-3.5" /></button>
      <button onClick={() => setAiOpen(!aiOpen)} className={cn('notion-bubble-btn rounded-md', aiLoading && 'animate-pulse')}><Sparkles className="w-3.5 h-3.5" /></button>
      <div className="w-px h-4 bg-gray-300 dark:bg-gray-600 mx-0.5" />
      <button onClick={() => wrapSelectionInBlock('toggleBlock')} title="Section repliable" className="notion-bubble-btn rounded-md">
        <ChevronsRightLeft className="w-3.5 h-3.5" />
      </button>
      <button onClick={() => wrapSelectionInBlock('calloutBlock')} title="Encadré" className="notion-bubble-btn rounded-md">
        <MessageSquareWarning className="w-3.5 h-3.5" />
      </button>
      {editorState.isImage && (
        <>
          <div className="w-px h-4 bg-gray-300 dark:bg-gray-600 mx-0.5" />
          <button
            type="button"
            onClick={() => editor.chain().focus().updateAttributes('image', { width: '25%' }).run()}
            className="notion-bubble-btn rounded-md text-xs font-medium px-1"
            title="25%"
          >
            25%
          </button>
          <button
            type="button"
            onClick={() => editor.chain().focus().updateAttributes('image', { width: '50%' }).run()}
            className="notion-bubble-btn rounded-md text-xs font-medium px-1"
            title="50%"
          >
            50%
          </button>
          <button
            type="button"
            onClick={() => editor.chain().focus().updateAttributes('image', { width: '100%' }).run()}
            className="notion-bubble-btn rounded-md text-xs font-medium px-1"
            title="100%"
          >
            100%
          </button>
          <button
            type="button"
            onClick={() => {
              const { selection } = editor.state
              if (selection instanceof NodeSelection && selection.node.type.name === 'image') {
                editor.chain().focus().deleteSelection().run()
              } else if (editor.isActive('image')) {
                // Fallback: delete the nearest selected image node
                editor.chain().focus().deleteSelection().run()
              }
            }}
            className="notion-bubble-btn rounded-md text-red-600 dark:text-red-400"
            title={t('richTextEditor.deleteImage') || t('common.delete') || 'Supprimer l\'image'}
            aria-label={t('richTextEditor.deleteImage') || 'Supprimer l\'image'}
          >
            <Trash2 className="w-3.5 h-3.5" />
          </button>
        </>
      )}
      {aiOpen && (
        <div className="notion-ai-submenu">
          <button className="notion-ai-subitem" onClick={() => handleAI('clarify')}><Lightbulb className="w-3.5 h-3.5 text-amber-500" /><span>{t('richTextEditor.slashClarify')}</span></button>
          <button className="notion-ai-subitem" onClick={() => handleAI('shorten')}><Scissors className="w-3.5 h-3.5 text-primary" /><span>{t('richTextEditor.slashShorten')}</span></button>
          <button className="notion-ai-subitem" onClick={() => handleAI('improve')}><Wand2 className="w-3.5 h-3.5 text-purple-500" /><span>{t('richTextEditor.slashImprove')}</span></button>
          <button className="notion-ai-subitem" onClick={() => handleAI('fix_grammar')}><SpellCheck className="w-3.5 h-3.5 text-green-500" /><span>{t('ai.action.fixGrammar')}</span></button>
          <button className="notion-ai-subitem" onClick={() => setTranslateOpen(v => !v)}><Languages className="w-3.5 h-3.5 text-indigo-500" /><span>{t('ai.action.translate')}</span></button>
          {translateOpen && (
            <div className="notion-ai-lang-picker">
              {TRANSLATE_TARGET_API_VALUES.map((apiValue) => (
                <button key={apiValue} className="notion-ai-lang-item" onClick={() => handleAI('translate', apiValue)}>
                  {t(`richTextEditor.translateTargets.${apiValue}`)}
                </button>
              ))}
              <div className="flex items-center gap-1 px-1 pt-1 w-full border-t border-border/40 mt-1">
                <input
                  className="notion-inline-input text-xs flex-1 min-w-0"
                  placeholder={t('ai.action.customLang') || 'Autre...'}
                  value={customLang}
                  onChange={e => setCustomLang(e.target.value)}
                  onKeyDown={e => { if (e.key === 'Enter' && customLang.trim()) handleAI('translate', customLang.trim()) }}
                />
              </div>
            </div>
          )}
          <button className="notion-ai-subitem" onClick={() => handleAI('explain')}><BookOpen className="w-3.5 h-3.5 text-orange-500" /><span>{t('ai.action.explain')}</span></button>
          {onSuggestCharts && (
            <button className="notion-ai-subitem" onClick={() => { setAiOpen(false); onSuggestCharts() }}><BarChart3 className="w-3.5 h-3.5 text-blue-500" /><span>{t('ai.action.generateChart') || 'Générer un graphique'}</span></button>
          )}
        </div>
      )}
      {aiModal && (
        <div className="notion-ai-result-overlay" onClick={() => setAiModal(null)}>
          <div className="notion-ai-result-modal" onClick={e => e.stopPropagation()}>
            <div className="notion-ai-result-header">
              <span className="text-sm font-semibold">
                {aiModal.type === 'explain' ? t('ai.action.explain') : (t('ai.result.preview') || 'Apercu IA')}
              </span>
              <button onClick={() => setAiModal(null)} className="notion-bubble-btn ml-auto"><X className="w-3.5 h-3.5" /></button>
            </div>
            {aiModal.type === 'preview' && (
              <div className="notion-ai-result-section">
                <span className="notion-ai-result-label">{t('ai.result.original') || 'Original'}</span>
                <p className="text-sm text-muted-foreground line-through opacity-60 mt-1">{aiModal.origText}</p>
              </div>
            )}
            <div className="notion-ai-result-section">
              {aiModal.type === 'preview' && <span className="notion-ai-result-label">{t('ai.result.suggestion') || 'Suggestion'}</span>}
              <div className="prose prose-sm max-w-none text-sm mt-1" dangerouslySetInnerHTML={{ __html: sanitizeRichHtml(aiModal.html) }} />
            </div>
            <div className="flex justify-end gap-2 mt-3 pt-3 border-t border-border/40">
              <button onClick={() => setAiModal(null)} className="notion-modal-btn">{t('common.cancel') || 'Annuler'}</button>
              {aiModal.type === 'preview' && (
                <button onClick={applyAiResult} className="notion-modal-btn notion-modal-btn-primary">{t('ai.result.apply') || 'Appliquer'}</button>
              )}
            </div>
          </div>
        </div>
      )}
    </div>
  )
}

function SlashPreview({ itemTitle, t }: { itemTitle: string; t: (k: string) => string }) {
  switch (itemTitle) {
    case 'Table':
    case 'Tableau':
      return (
        <div className="slash-preview-box">
          <div className="slash-preview-title">{t('richTextEditor.slashTable') || 'Tableau'}</div>
          <div className="slash-preview-grid">
            <div className="slash-preview-grid-header"></div>
            <div className="slash-preview-grid-header"></div>
            <div className="slash-preview-grid-header"></div>
            <div className="slash-preview-grid-cell"></div>
            <div className="slash-preview-grid-cell"></div>
            <div className="slash-preview-grid-cell"></div>
            <div className="slash-preview-grid-cell"></div>
            <div className="slash-preview-grid-cell"></div>
            <div className="slash-preview-grid-cell"></div>
          </div>
          <p className="slash-preview-tip">{t('richTextEditor.previewTableTip') || 'Organisez vos données en lignes et colonnes.'}</p>
        </div>
      )
    case 'Database':
    case 'Base de données':
      return (
        <div className="slash-preview-box">
          <div className="slash-preview-title">{t('richTextEditor.slashDatabase') || 'Base de Données'}</div>
          <div className="slash-preview-db">
            <div className="slash-preview-db-row header">
              <span>{t('structuredViews.propertyName') || 'Nom'}</span>
              <span>{t('structuredViews.propertyStatus') || 'Statut'}</span>
            </div>
            <div className="slash-preview-db-row">
              <span>{t('wizard.taskA') || 'Tâche A'}</span>
              <span className="badge badge-todo">{t('structuredViews.filterTodo') || 'À faire'}</span>
            </div>
            <div className="slash-preview-db-row">
              <span>{t('wizard.taskB') || 'Tâche B'}</span>
              <span className="badge badge-done">{t('structuredViews.filterDone') || 'Fait'}</span>
            </div>
          </div>
          <p className="slash-preview-tip">{t('richTextEditor.previewDatabaseTip') || 'Ajoutez des colonnes et des vues Kanban structurées.'}</p>
        </div>
      )
    case 'Suggest Charts':
    case 'Suggest Chart':
    case 'Graphiques IA':
      return (
        <div className="slash-preview-box">
          <div className="slash-preview-title">{t('richTextEditor.slashCharts') || 'Graphique IA'}</div>
          <div className="slash-preview-chart">
            <div className="chart-bar" style={{ height: '40%' }}></div>
            <div className="chart-bar" style={{ height: '75%' }}></div>
            <div className="chart-bar" style={{ height: '50%' }}></div>
            <div className="chart-bar" style={{ height: '90%' }}></div>
          </div>
          <p className="slash-preview-tip">{t('richTextEditor.previewChartsTip') || 'Générez un graphique interactif à partir de votre texte.'}</p>
        </div>
      )
    case 'Living Block':
    case 'Bloc vivant':
      return (
        <div className="slash-preview-box">
          <div className="slash-preview-title">{t('richTextEditor.slashLivingBlock') || 'Bloc Vivant'}</div>
          <div className="slash-preview-live">
            <div className="live-note">Note A</div>
            <div className="live-sync-line"></div>
            <div className="live-note">Note B</div>
          </div>
          <p className="slash-preview-tip">{t('richTextEditor.previewLivingBlockTip') || 'Synchronisez du contenu entre plusieurs notes.'}</p>
        </div>
      )
    case 'Diagramme':
    case 'Diagram':
      return (
        <div className="slash-preview-box">
          <div className="slash-preview-title">{t('richTextEditor.slashDiagram') || 'Diagramme'}</div>
          <div className="slash-preview-excalidraw">
            <div className="excalidraw-circle"></div>
            <div className="excalidraw-arrow"></div>
            <div className="excalidraw-rect"></div>
          </div>
          <p className="slash-preview-tip">{t('richTextEditor.previewDiagramTip') || 'Esquissez des concepts ou générez des diagrammes via IA.'}</p>
        </div>
      )
    case 'Présentation':
    case 'Presentation':
      return (
        <div className="slash-preview-box">
          <div className="slash-preview-title">{t('richTextEditor.slashSlides') || 'Présentation'}</div>
          <div className="slash-preview-slides">
            <div className="slide-item"></div>
            <div className="slide-item active"></div>
            <div className="slide-item"></div>
          </div>
          <p className="slash-preview-tip">{t('richTextEditor.previewSlidesTip') || 'Créez des présentations interactives exportables.'}</p>
        </div>
      )
    case 'Code Block':
    case 'Code':
    case 'Bloc de code':
      return (
        <div className="slash-preview-box">
          <div className="slash-preview-title">{t('richTextEditor.slashCode') || 'Bloc de Code'}</div>
          <div className="slash-preview-code">
            <div className="flex gap-1 mb-1">
              <span className="code-dot red"></span>
              <span className="code-dot yellow"></span>
              <span className="code-dot green"></span>
            </div>
            <div className="code-line green"></div>
            <div className="code-line blue"></div>
          </div>
          <p className="slash-preview-tip">{t('richTextEditor.previewCodeTip') || 'Ajoutez du code avec coloration syntaxique.'}</p>
        </div>
      )
    default:
      return null
  }
}

function SlashCommandMenu({ editor, onInsertImage, onSuggestCharts }: { editor: Editor; onInsertImage: (editor: Editor) => void; onSuggestCharts: () => void }) {
  const { t } = useLanguage()
  const { requestAiConsent } = useAiConsent()
  const [isOpen, setIsOpen] = useState(false)
  const [query, setQuery] = useState('')
  const [selectedIndex, setSelectedIndex] = useState(0)
  const [activeCategory, setActiveCategory] = useState<SlashCategoryId | null>(null)
  const [coords, setCoords] = useState({ top: 0, left: 0 })
  const [previewCoords, setPreviewCoords] = useState({ top: 0, left: 0, side: 'right' as 'right' | 'left' })
  const [aiLoading, setAiLoading] = useState(false)
  const [aiWriterMode, setAiWriterMode] = useState(false)
  const aiWriterModeRef = useRef(false)
  const [aiWriterPrompt, setAiWriterPrompt] = useState('')
  const [aiWriterLoading, setAiWriterLoading] = useState(false)
  const menuRef = useRef<HTMLDivElement>(null)
  const selectedItemRef = useRef<HTMLButtonElement>(null)
  const menuInteracting = useRef(false)
  const [frequentCommands, setFrequentCommands] = useState<SlashMenuItem[]>([])

  // Resolve by English command title — NEVER by array index (reorder-safe)
  const sc = (enTitle: string) => {
    const cmd = slashCommands.find(c => c.title === enTitle)
    if (!cmd) throw new Error(`Slash command missing: ${enTitle}`)
    return cmd
  }

  const localCommands: SlashMenuItem[] = [
    { ...sc('Text'), title: t('richTextEditor.slashText'), description: t('richTextEditor.slashTextDesc'), categoryId: 'text' },
    { ...sc('Heading 1'), title: t('richTextEditor.slashH1'), description: t('richTextEditor.slashH1Desc'), categoryId: 'text' },
    { ...sc('Heading 2'), title: t('richTextEditor.slashH2'), description: t('richTextEditor.slashH2Desc'), categoryId: 'text' },
    { ...sc('Heading 3'), title: t('richTextEditor.slashH3'), description: t('richTextEditor.slashH3Desc'), categoryId: 'text' },
    { ...sc('Bullet List'), title: t('richTextEditor.slashBullet'), description: t('richTextEditor.slashBulletDesc'), categoryId: 'text', slashKeywords: ['bullet', 'liste', 'puce', 'ul', 'puces'] },
    { ...sc('Numbered List'), title: t('richTextEditor.slashNumbered'), description: t('richTextEditor.slashNumberedDesc'), categoryId: 'text', slashKeywords: ['numbered', 'numérotée', 'numerotee', 'ol', '1.'] },
    { ...sc('To-do List'), title: t('richTextEditor.slashTodo'), description: t('richTextEditor.slashTodoDesc'), categoryId: 'text', slashKeywords: ['todo', 'tache', 'tâche', 'checkbox'] },
    { ...sc('Quote'), title: t('richTextEditor.slashQuote'), description: t('richTextEditor.slashQuoteDesc'), categoryId: 'text' },
    { ...sc('Code Block'), title: t('richTextEditor.slashCode'), description: t('richTextEditor.slashCodeDesc'), categoryId: 'text' },
    { ...sc('Divider'), title: t('richTextEditor.slashDivider'), description: t('richTextEditor.slashDividerDesc'), categoryId: 'text' },
    { ...sc('Table'), title: t('richTextEditor.slashTable'), description: t('richTextEditor.slashTableDesc'), categoryId: 'data', slashKeywords: ['table', 'tableau', 'grid', 'grille'] },
    { ...sc('Image'), title: t('richTextEditor.slashImage'), description: t('richTextEditor.slashImageDesc'), categoryId: 'media' },
    { ...sc('Align Left'), title: t('richTextEditor.slashAlignLeft'), description: t('richTextEditor.slashAlignLeftDesc'), categoryId: 'text' },
    { ...sc('Align Center'), title: t('richTextEditor.slashAlignCenter'), description: t('richTextEditor.slashAlignCenterDesc'), categoryId: 'text' },
    { ...sc('Align Right'), title: t('richTextEditor.slashAlignRight'), description: t('richTextEditor.slashAlignRightDesc'), categoryId: 'text' },
    { ...sc('Clarifier'), title: t('richTextEditor.slashClarify'), description: t('richTextEditor.slashClarifyDesc'), categoryId: 'ai' },
    { ...sc('Raccourcir'), title: t('richTextEditor.slashShorten'), description: t('richTextEditor.slashShortenDesc'), categoryId: 'ai' },
    { ...sc('Améliorer'), title: t('richTextEditor.slashImprove'), description: t('richTextEditor.slashImproveDesc'), categoryId: 'ai' },
    { ...sc('Développer'), title: t('richTextEditor.slashExpand'), description: t('richTextEditor.slashExpandDesc'), categoryId: 'ai' },
    { ...sc('Bold'), title: t('richTextEditor.bold'), description: t('richTextEditor.bold'), categoryId: 'text' },
    { ...sc('Italic'), title: t('richTextEditor.italic'), description: t('richTextEditor.italic'), categoryId: 'text' },
    { ...sc('Underline'), title: t('richTextEditor.underline'), description: t('richTextEditor.underline'), categoryId: 'text' },
    { ...sc('Strike'), title: t('richTextEditor.strike'), description: t('richTextEditor.strike'), categoryId: 'text' },
    { ...sc('Highlight'), title: t('richTextEditor.highlight'), description: t('richTextEditor.highlight'), categoryId: 'text' },
    { ...sc('Superscript'), title: t('richTextEditor.slashSuperscript'), description: t('richTextEditor.slashSuperscriptDesc'), categoryId: 'text' },
    { ...sc('Subscript'), title: t('richTextEditor.slashSubscript'), description: t('richTextEditor.slashSubscriptDesc'), categoryId: 'text' },
    { ...sc('Diagramme'), title: t('richTextEditor.slashDiagram'), description: t('richTextEditor.slashDiagramDesc'), categoryId: 'ai' },
    { ...sc('Présentation'), title: t('richTextEditor.slashSlides'), description: t('richTextEditor.slashSlidesDesc'), categoryId: 'ai' },
    { ...sc('Suggest Charts'), title: t('richTextEditor.slashCharts') || 'Graphiques IA', description: t('richTextEditor.slashChartsDesc') || 'IA suggère des graphiques', categoryId: 'ai' },
    { ...sc('Living Block'), title: t('richTextEditor.slashLivingBlock') || 'Bloc vivant', description: t('richTextEditor.slashLivingBlockDesc') || 'Insérer depuis une autre note', categoryId: 'embed' },
    { ...sc('Database'), title: t('richTextEditor.slashDatabase'), description: t('richTextEditor.slashDatabaseDesc'), categoryId: 'data', slashKeywords: ['database', 'db', 'base', 'données', 'donnees', 'vue', 'structured', 'structuree', 'structurée'] },
    { ...sc('Toggle'), title: t('richTextEditor.slashToggle'), description: t('richTextEditor.slashToggleDesc'), categoryId: 'text', slashKeywords: ['toggle', 'accordion', 'replier', 'deroulant', 'déroulant', 'section'] },
    { ...sc('Callout'), title: t('richTextEditor.slashCallout'), description: t('richTextEditor.slashCalloutDesc'), categoryId: 'text', slashKeywords: ['callout', 'encadre', 'encadré', 'info', 'alerte', 'astuce', 'tip', 'warning'] },
    { ...sc('Outline'), title: t('richTextEditor.slashOutline'), description: t('richTextEditor.slashOutlineDesc'), categoryId: 'text', slashKeywords: ['outline', 'sommaire', 'toc', 'matieres', 'matières', 'plan'] },
    { ...sc('Link Preview'), title: t('richTextEditor.slashLinkPreview'), description: t('richTextEditor.slashLinkPreviewDesc'), categoryId: 'embed', slashKeywords: ['link', 'lien', 'url', 'preview', 'apercu', 'aperçu', 'embed', 'card', 'carte'] },
    { ...sc('Math'), title: t('richTextEditor.slashMath'), description: t('richTextEditor.slashMathDesc'), categoryId: 'text', slashKeywords: ['math', 'maths', 'equation', 'équation', 'formula', 'formule', 'latex', 'katex'] },
    { ...sc('Columns'), title: t('richTextEditor.slashColumns'), description: t('richTextEditor.slashColumnsDesc'), categoryId: 'text', slashKeywords: ['columns', 'colonnes', 'cols', 'layout', 'mise', 'page', 'cote', 'côte'] },
    { ...sc("Écrire avec l'IA"), title: t('richTextEditor.slashAiWriter') || 'Écrire avec l\'IA', description: t('richTextEditor.slashAiWriterDesc') || 'Générer du contenu au curseur', categoryId: 'ai', slashKeywords: ['ecrire', 'écrire', 'write', 'ia', 'ai', 'generer', 'générer', 'rediger', 'rédiger'] },
    {
      title: t('richTextEditor.slashNoteLink'),
      description: t('richTextEditor.slashNoteLinkDesc'),
      icon: Link2,
      categoryId: 'embed' as SlashCategoryId,
      command: (e) => {
        e.chain().focus().insertContent('[[').run()
      },
    },
  ]

  const closeMenu = useCallback(() => {
    setIsOpen(false); setQuery(''); setSelectedIndex(0); setActiveCategory(null); setAiWriterMode(false); aiWriterModeRef.current = false
  }, [])

  const handleAiWriterSubmit = useCallback(async () => {
    if (!aiWriterPrompt.trim()) return
    setAiWriterLoading(true)
    try {
      const res = await fetch('/api/ai/reformulate', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          text: '',
          option: 'write',
          format: 'html',
          writePrompt: aiWriterPrompt.trim(),
        }),
      })
      const data = await res.json()
      if (!res.ok) { toast.error(data.error || 'Erreur'); return }
      let html = data.reformulatedText || data.text || ''
      // Clean up excessive whitespace
      html = html
        .replace(/<p>\s*<\/p>/g, '')
        .replace(/(<p[^>]*>)\s+/g, '$1')
        .replace(/\s+(<\/p>)/g, '$1')
        .replace(/\n{3,}/g, '\n\n')
        .trim()

      closeMenu()

      // Stream the content paragraph by paragraph
      const paragraphs = html.match(/<(?:p|h[1-3]|ul|ol|div|blockquote|pre)[^>]*>[\s\S]*?<\/(?:p|h[1-3]|ul|ol|div|blockquote|pre)>/gi) || [html]
      for (let i = 0; i < paragraphs.length; i++) {
        editor.chain().focus().insertContent(paragraphs[i]).run()
        if (i < paragraphs.length - 1) {
          await new Promise(r => setTimeout(r, 120))
        }
      }
    } catch (e: any) { toast.error(e.message || 'Erreur') }
    finally { setAiWriterLoading(false) }
  }, [aiWriterPrompt, editor, closeMenu])

  const deleteSlashText = useCallback(() => {
    const { from, to } = editor.state.selection
    const textBefore = editor.state.doc.textBetween(Math.max(0, from - 50), from, '\n')
    const slashIdx = textBefore.lastIndexOf('/')
    if (slashIdx >= 0) {
      const deleteFrom = from - (textBefore.length - slashIdx)
      editor.chain().focus().deleteRange({ from: deleteFrom, to }).run()
    }
  }, [editor])

  const handleSelect = useCallback(async (item: SlashMenuItem) => {
    // Persister l'historique d'usage pour la section Favoris
    try {
      const usageStr = localStorage.getItem('memento-slash-command-usage') || '{}'
      const usage = JSON.parse(usageStr) as Record<string, number>
      usage[item.title] = (usage[item.title] || 0) + 1
      localStorage.setItem('memento-slash-command-usage', JSON.stringify(usage))
    } catch (e) {
      console.warn('Slash usage count error:', e)
    }

    const toastAi = (err: unknown) => {
      const msg = err instanceof Error ? err.message : ''
      toast.error(msg && msg !== AI_REFORMULATE_FALLBACK ? msg : t('richTextEditor.aiReformulateFailed'))
    }
    if (item.isImage) {
      deleteSlashText(); closeMenu(); onInsertImage(editor)
    } else if (item.isAi && item.aiOption === 'write') {
      aiWriterModeRef.current = true
      setAiWriterMode(true)
      setAiWriterPrompt('')
      deleteSlashText()
      const { from } = editor.state.selection
      const c = editor.view.coordsAtPos(from)
      setCoords({ top: c.bottom + 8, left: c.left })
    } else if (item.isAi && item.aiOption) {
      deleteSlashText(); closeMenu(); setAiLoading(true)
      try {
        const consented = await requestAiConsent()
        if (!consented) return
        const allText = editor.state.doc.textContent
        if (!allText || allText.split(/\s+/).length < 5) return
        const result = await aiReformulate(allText, item.aiOption, t)
        editor.chain().focus().setContent(result).run()
      } catch (err) {
        console.error('AI slash error:', err)
        toastAi(err)
      }
      finally { setAiLoading(false) }
    } else if (
      item.title === 'Suggest Charts'
      || item.title === (t('richTextEditor.slashCharts') || 'Graphiques IA')
    ) {
      deleteSlashText(); closeMenu(); onSuggestCharts()
    } else if (item.title === t('richTextEditor.slashDatabase')) {
      deleteSlashText(); closeMenu()
      const currentNotebookId = (editor.storage as any).structuredViewBlock?.notebookId as string | null
      if (!insertStructuredViewBlockAtSelection(editor, currentNotebookId)) {
        toast.error(t('structuredViewBlock.loadError') || 'Impossible de charger les données structurées.')
      }
    } else {
      deleteSlashText(); item.command(editor); closeMenu()
    }
  }, [editor, closeMenu, deleteSlashText, onInsertImage, onSuggestCharts, t, requestAiConsent])

  // Charger les favoris fréquents lors de l'ouverture
  useEffect(() => {
    if (!isOpen) return
    try {
      const usageStr = localStorage.getItem('memento-slash-command-usage') || '{}'
      const usage = JSON.parse(usageStr) as Record<string, number>
      const sorted = localCommands
        .filter(c => (usage[c.title] || 0) > 0)
        .sort((a, b) => (usage[b.title] || 0) - (usage[a.title] || 0))
        .slice(0, 4)
      setFrequentCommands(sorted)
    } catch (e) {
      setFrequentCommands([])
    }
  }, [isOpen])

  const presentCategoryIds = new Set(localCommands.map(c => c.categoryId))
  const allCategories = ORDERED_SLASH_CATEGORIES.filter(id => presentCategoryIds.has(id))

  const q = query.toLowerCase()
  const textFiltered = localCommands.filter(c =>
    c.title.toLowerCase().includes(q)
    || c.description.toLowerCase().includes(q)
    || (c.shortcut?.toLowerCase().includes(q) ?? false)
    || (c.slashKeywords?.some((kw) => kw.includes(q) || q.includes(kw)) ?? false)
  )

  const baseFiltered = activeCategory ? textFiltered.filter(c => c.categoryId === activeCategory) : textFiltered

  // Injecter les favoris tout en haut si pas de recherche ni filtre de catégorie actif
  const filtered = !q && !activeCategory && frequentCommands.length > 0
    ? [...frequentCommands.map(c => ({ ...c, isFavorite: true })), ...baseFiltered]
    : baseFiltered

  const availableCategoriesInSearch = textFiltered.reduce((acc, item) => {
    const id = item.categoryId
    if (!acc[id]) acc[id] = []
    acc[id].push(item)
    return acc
  }, {} as Record<SlashCategoryId, SlashMenuItem[]>)

  const categories = baseFiltered.reduce((acc, item) => {
    const id = item.categoryId
    if (!acc[id]) acc[id] = []
    acc[id].push(item)
    return acc
  }, {} as Record<SlashCategoryId, SlashMenuItem[]>)

  // Auto-scroll selected item into view
  useEffect(() => {
    selectedItemRef.current?.scrollIntoView({ block: 'nearest', behavior: 'smooth' })
  }, [selectedIndex])

  // Ajustement dynamique de la position de la preview
  useEffect(() => {
    if (!isOpen) return
    const menuWidth = 320
    const previewWidth = 280
    const padding = 12
    const wouldOverflowRight = coords.left + menuWidth + previewWidth + padding > window.innerWidth

    if (wouldOverflowRight) {
      setPreviewCoords({
        top: coords.top,
        left: coords.left - previewWidth - padding,
        side: 'left',
      })
    } else {
      setPreviewCoords({
        top: coords.top,
        left: coords.left + menuWidth + padding,
        side: 'right',
      })
    }
  }, [coords, isOpen])

  useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
      if (!isOpen) return
      if (aiWriterModeRef.current) return  // Let the AI writer input handle keystrokes
      if (e.key === 'ArrowDown') { e.preventDefault(); setSelectedIndex(i => (i + 1) % filtered.length); return }
      if (e.key === 'ArrowUp') { e.preventDefault(); setSelectedIndex(i => (i - 1 + filtered.length) % filtered.length); return }
      if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') {
        e.preventDefault()
        const availableTabs = [null as SlashCategoryId | null, ...allCategories.filter(id => (availableCategoriesInSearch[id]?.length ?? 0) > 0)]
        const currentIndex = availableTabs.indexOf(activeCategory)
        const nextIndex = e.key === 'ArrowRight'
          ? (currentIndex + 1) % availableTabs.length
          : (currentIndex - 1 + availableTabs.length) % availableTabs.length
        setActiveCategory(availableTabs[nextIndex])
        setSelectedIndex(0)
        return
      }
      if (e.key === 'Enter') {
        e.preventDefault()
        const item = filtered[selectedIndex]
        if (item) {
          handleSelect(item)
        } else if (/^(database|db|vue|tableau|structured)$/i.test(query)) {
          deleteSlashText()
          closeMenu()
          const currentNotebookId = (editor.storage as any).structuredViewBlock?.notebookId as string | null
          if (!insertStructuredViewBlockAtSelection(editor, currentNotebookId)) {
            toast.error(t('structuredViewBlock.loadError') || 'Impossible de charger les données structurées.')
          }
        }
        return
      }
      if (e.key === 'Escape') { e.preventDefault(); closeMenu(); return }
      if (e.key === 'Tab') {
        e.preventDefault()
        const availableTabs = [null as SlashCategoryId | null, ...allCategories.filter(id => (availableCategoriesInSearch[id]?.length ?? 0) > 0)]
        const nextIndex = (availableTabs.indexOf(activeCategory) + 1) % availableTabs.length
        setActiveCategory(availableTabs[nextIndex])
        setSelectedIndex(0)
        return
      }
    }
    document.addEventListener('keydown', handleKeyDown, true)
    return () => document.removeEventListener('keydown', handleKeyDown, true)
  }, [isOpen, selectedIndex, filtered, handleSelect, closeMenu, activeCategory, allCategories, availableCategoriesInSearch, query, deleteSlashText, editor, t])

  useEffect(() => {
    if (!isOpen) return
    const { from } = editor.state.selection
    const c = editor.view.coordsAtPos(from)

    // Check if menu would overflow bottom
    const menuHeight = menuRef.current?.offsetHeight || 300
    const wouldOverflow = c.bottom + menuHeight + 20 > window.innerHeight

    if (wouldOverflow) {
      setCoords({ top: c.top - menuHeight - 8, left: c.left })
    } else {
      setCoords({ top: c.bottom + 8, left: c.left })
    }
  }, [isOpen, editor, query, filtered.length, coords.left])

  useEffect(() => {
    const handleClick = (e: MouseEvent) => {
      if (isOpen && !aiWriterModeRef.current && menuRef.current && !menuRef.current.contains(e.target as Node)) {
        closeMenu()
      }
    }
    document.addEventListener('click', handleClick)
    return () => document.removeEventListener('click', handleClick)
  }, [isOpen, closeMenu])

  useEffect(() => {
    const handler = () => {
      // Ignore events fired while user clicks inside the menu
      if (menuInteracting.current) return
      if (aiWriterModeRef.current) return  // Synchronous check via ref
      const { from, empty } = editor.state.selection
      if (!empty) { if (isOpen) closeMenu(); return }
      const text = editor.state.doc.textBetween(Math.max(0, from - 50), from, '\n')
      const m = text.match(/\/([^\s/]*)$/)
      if (m) {
        setQuery(m[1])
        setSelectedIndex(0)
        if (!isOpen) { setIsOpen(true); setActiveCategory(null) }
      }
      else if (isOpen) closeMenu()
    }
    editor.on('update', handler)
    editor.on('selectionUpdate', handler)
    return () => { editor.off('update', handler); editor.off('selectionUpdate', handler) }
  }, [editor, isOpen, closeMenu, aiWriterMode])

  if (!aiWriterMode && (!isOpen || filtered.length === 0)) return null

  let flatIndex = -1
  const sectionIds = ORDERED_SLASH_CATEGORIES.filter(id => (categories[id]?.length ?? 0) > 0)
  const totalVisible = sectionIds.length

  const visibleSectionIds = [...sectionIds]
  // Si pas de query, ajouter la section Favoris au début
  const hasFavorites = !q && !activeCategory && frequentCommands.length > 0
  if (hasFavorites) {
    visibleSectionIds.unshift('frequent' as any)
  }

  const selectedItem = filtered[selectedIndex]
  const showPreview = selectedItem && [
    'Table', 'Tableau', 'Database', 'Suggest Charts', 'Suggest Chart', 'Living Block', 'Bloc vivant', 'Diagramme', 'Diagram', 'Présentation', 'Presentation', 'Code Block', 'Code', 'Bloc de code'
  ].includes(selectedItem.title)

  return createPortal(
    <>
      {aiWriterMode ? (
        <div
          ref={menuRef}
          className="notion-slash-menu"
          style={{ top: coords.top, left: coords.left, minWidth: '360px' }}
          onClick={(e) => e.stopPropagation()}
          dir="auto"
        >
          <div className="flex items-center gap-2 px-3 py-2.5 border-b border-border/30">
            <Sparkles size={16} className="text-brand-accent flex-shrink-0" />
            <input
              type="text"
              value={aiWriterPrompt}
              onChange={(e) => setAiWriterPrompt(e.target.value)}
              onKeyDown={(e) => {
                e.stopPropagation()
                if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); void handleAiWriterSubmit() }
                if (e.key === 'Escape') closeMenu()
              }}
              autoFocus
              disabled={aiWriterLoading}
              placeholder={t('richTextEditor.aiWriterPlaceholder') || 'Décris ce que tu veux écrire...'}
              className="flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground"
            />
            {aiWriterLoading && <Loader2 size={14} className="animate-spin text-brand-accent flex-shrink-0" />}
          </div>
          {!aiWriterLoading && aiWriterPrompt.trim() && (
            <button
              onClick={() => void handleAiWriterSubmit()}
              className="w-full px-3 py-2 text-xs font-medium text-brand-accent hover:bg-brand-accent/10 transition-colors text-left"
            >
              {t('general.send') || 'Générer'} ↵
            </button>
          )}
        </div>
      ) : (
      <div
        ref={menuRef}
        className="notion-slash-menu"
        style={{ top: coords.top, left: coords.left }}
        onClick={(e) => e.stopPropagation()}
      >
        {/* Category tabs */}
        {!query && totalVisible > 1 && (
          <div className="notion-slash-tabs" onMouseDown={(e) => e.preventDefault()}>
            <button
              className={cn('notion-slash-tab', !activeCategory && 'notion-slash-tab-active')}
              onMouseDown={(e) => e.preventDefault()}
              onClick={() => { setActiveCategory(null); setSelectedIndex(0) }}
            >
              {t('richTextEditor.slashTabAll')}
            </button>
            {allCategories.filter(id => (availableCategoriesInSearch[id]?.length ?? 0) > 0).map(catId => (
              <button
                key={catId}
                className={cn('notion-slash-tab', activeCategory === catId && 'notion-slash-tab-active', catId === 'ai' && 'notion-slash-tab-ai')}
                onMouseDown={(e) => e.preventDefault()}
                onClick={() => { setActiveCategory(activeCategory === catId ? null : catId); setSelectedIndex(0) }}
              >
                {catId === 'ai' && <Sparkles className="w-2.5 h-2.5" />}
                {slashCategoryLabel(catId, t)}
                <span className="notion-slash-tab-count">{availableCategoriesInSearch[catId]?.length ?? 0}</span>
              </button>
            ))}
          </div>
        )}

        {/* Header hint */}
        <div className="notion-slash-header">
          <span>{t('richTextEditor.slashHint')}</span>
        </div>

        {aiLoading && (
          <div className="notion-slash-loading">
            <Sparkles className="w-3.5 h-3.5 animate-spin" />
            <span>{t('richTextEditor.slashLoading')}</span>
          </div>
        )}
        {!aiLoading && visibleSectionIds.map((catId) => {
          const items = catId === ('frequent' as any)
            ? frequentCommands.map(c => ({ ...c, isFavorite: true }))
            : categories[catId]!

          return (
            <div key={catId} className="notion-slash-section">
              <div className={cn(
                'notion-slash-label',
                catId === 'ai' && 'notion-slash-label-ai',
                catId === ('frequent' as any) && 'text-amber-500 font-semibold'
              )}>
                {catId === 'ai' && <Sparkles className="w-3 h-3 inline mr-1" />}
                {catId === ('frequent' as any) ? (t('richTextEditor.frequentCommands') || '★ Fréquents') : slashCategoryLabel(catId, t)}
              </div>
              {items.map((item) => {
                flatIndex++
                const idx = flatIndex
                const isSelected = idx === selectedIndex
                return (
                  <button
                    key={`${catId}-${item.title}-${item.shortcut ?? ''}-${item.isFavorite ? 'fav' : 'normal'}`}
                    ref={isSelected ? selectedItemRef : null}
                    className={cn(
                      'notion-slash-item',
                      isSelected && 'notion-slash-item-selected',
                      item.isFavorite && 'notion-slash-item-favorite'
                    )}
                    onMouseDown={(e) => e.preventDefault()}
                    onClick={() => handleSelect(item)}
                    onMouseEnter={() => setSelectedIndex(idx)}
                  >
                    <div className={cn('notion-slash-icon', item.isAi && 'notion-slash-icon-ai')}>
                      <item.icon className="w-4 h-4" />
                    </div>
                    <div className="notion-slash-content">
                      <span className="notion-slash-title">{item.title}</span>
                      <span className="notion-slash-desc">{item.description}</span>
                    </div>
                    {item.shortcut && (
                      <span className="notion-slash-shortcut">{item.shortcut}</span>
                    )}
                  </button>
                )
              })}
            </div>
          )
        })}
      </div>
      )}

      {showPreview && (
        <div
          className="notion-slash-preview"
          style={{ top: previewCoords.top, left: previewCoords.left }}
        >
          <SlashPreview itemTitle={selectedItem.title} t={t} />
        </div>
      )}
    </>,
    document.body
  )
}






