'use client'

import { createContext, useContext, useState, useEffect, useRef, useMemo, useCallback, ReactNode } from 'react'
import { useQueryClient } from '@tanstack/react-query'
import { Note, CheckItem, NOTE_COLORS, NoteColor, LinkMetadata, NoteSize } from '@/lib/types'
import { updateNote, createNote, cleanupOrphanedImages, leaveSharedNote, deleteNote } from '@/app/actions/notes'
import { fetchLinkMetadata } from '@/app/actions/scrape'
import { useNotebooks } from '@/context/notebooks-context'
import { emitNoteChange, NOTE_REQUEST_SAVE_EVENT } from '@/lib/note-change-sync'
import { useAutoTagging } from '@/hooks/use-auto-tagging'
import { useTitleSuggestions } from '@/hooks/use-title-suggestions'
import { toast } from 'sonner'
import { useLanguage } from '@/lib/i18n'
import { useAiConsent } from '@/components/legal/ai-consent-provider'
import { useSession } from 'next-auth/react'
import { getAISettings, updateAISettings } from '@/app/actions/ai-settings'
import { extractImagesFromHTML } from '@/lib/utils'
import { queryKeys } from '@/lib/query-keys'
import type { RichTextEditorHandle } from '@/components/rich-text-editor'
import type { TitleSuggestion } from '@/hooks/use-title-suggestions'
import type { TagSuggestion } from '@/lib/ai/types'
import type { NoteEditorState, NoteEditorActions, NoteEditorContextValue } from './types'

const NoteEditorContext = createContext<NoteEditorContextValue | undefined>(undefined)

interface NoteEditorProviderProps {
  note: Note
  readOnly?: boolean
  fullPage?: boolean
  onNoteSaved?: (savedNote: Note) => void
  children: ReactNode
}

export function NoteEditorProvider({ note, readOnly = false, fullPage = false, onNoteSaved, children }: NoteEditorProviderProps) {
  const { data: session } = useSession()
  const { t } = useLanguage()
  const { requestAiConsent } = useAiConsent()
  const queryClient = useQueryClient()
  const { labels: globalLabels, addLabel, refreshLabels, setNotebookId: setContextNotebookId, notebooks } = useNotebooks()

  const [aiAssistantEnabled, setAiAssistantEnabled] = useState(true)
  const [autoLabelingEnabled, setAutoLabelingEnabled] = useState(true)
  const [autoSaveEnabled, setAutoSaveEnabled] = useState(true)

  useEffect(() => {
    if (session?.user?.id) {
      getAISettings(session.user.id).then(settings => {
        setAiAssistantEnabled(settings.paragraphRefactor !== false)
        setAutoLabelingEnabled(settings.autoLabeling !== false)
        setAutoSaveEnabled(settings.autoSave !== false)
      }).catch(err => console.error("Failed to fetch AI settings", err))
    }
  }, [session?.user?.id])

  const [quotaExceededFeature, setQuotaExceededFeature] = useState<string | null>(null)
  const [title, setTitle] = useState(note.title || '')
  const contentRef = useRef(note.content)
  const [content, setContentState] = useState(note.content)
  const debounceTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)

  const setContentImmediate = useCallback((newVal: string) => {
    contentRef.current = newVal
    if (debounceTimeoutRef.current) {
      clearTimeout(debounceTimeoutRef.current)
    }
    setContentState(newVal)
  }, [])

  // setContent : met à jour contentRef immédiatement (pour les saves),
  // mais ne déclenche un re-render React qu'après 800ms (throttle) pour éviter
  // un render à chaque frappe.
  const setContent = useCallback((newVal: string) => {
    contentRef.current = newVal
    if (debounceTimeoutRef.current) {
      clearTimeout(debounceTimeoutRef.current)
    }
    debounceTimeoutRef.current = setTimeout(() => {
      setContentState(newVal)
    }, 800)
  }, [])
  const [checkItems, setCheckItems] = useState<CheckItem[]>(note.checkItems || [])
  const [labels, setLabels] = useState<string[]>(note.labels || [])
  const [images, setImages] = useState<string[]>(note.images || [])
  const [links, setLinks] = useState<LinkMetadata[]>(note.links || [])
  const [newLabel, setNewLabel] = useState('')
  const [color, setColor] = useState(note.color)
  const [size, setSize] = useState<NoteSize>(note.size || 'small')
  const [isSaving, setIsSaving] = useState(false)
  const [lastSavedAt, setLastSavedAt] = useState<Date | null>(null)
  const [removedImageUrls, setRemovedImageUrls] = useState<string[]>([])
  const [isMarkdown, setIsMarkdown] = useState(note.type === 'markdown')
  const [showMarkdownPreview, setShowMarkdownPreview] = useState(note.type === 'markdown')

  const fileInputRef = useRef<HTMLInputElement>(null)
  const textareaRef = useRef<HTMLTextAreaElement>(null)
  const richTextEditorRef = useRef<RichTextEditorHandle>(null)
  const prevNoteRef = useRef(note)

  useEffect(() => {
    const prev = prevNoteRef.current

    if (note.id !== prev.id) {
      setQuotaExceededFeature(null)
      setTitle(note.title || '')
      setContentImmediate(note.content)
      setCheckItems(note.checkItems || [])
      setLabels(note.labels || [])
      setImages(note.images || [])
      setLinks(note.links || [])
      setColor(note.color)
      setSize(note.size || 'small')
      setIsMarkdown(note.type === 'markdown')
      setShowMarkdownPreview(note.type === 'markdown')
      setCurrentReminder(note.reminder ? new Date(note.reminder as unknown as string) : null)
    } else {
      if (note.title !== prev.title) setTitle(note.title || '')
      // Ne pas réinitialiser le contenu quand seuls images/links changent (post-save)
      if (note.content !== prev.content) setContentImmediate(note.content)
      if (JSON.stringify(note.checkItems || []) !== JSON.stringify(prev.checkItems || [])) {
        setCheckItems(note.checkItems || [])
      }
      if (JSON.stringify(note.labels || []) !== JSON.stringify(prev.labels || [])) {
        setLabels(note.labels || [])
      }
      if (JSON.stringify(note.images || []) !== JSON.stringify(prev.images || [])) {
        setImages(note.images || [])
      }
      if (JSON.stringify(note.links || []) !== JSON.stringify(prev.links || [])) {
        setLinks(note.links || [])
      }
      if (note.color !== prev.color) setColor(note.color)
      if ((note.size || 'small') !== (prev.size || 'small')) setSize(note.size || 'small')
      const noteIsMarkdown = note.type === 'markdown'
      const prevIsMarkdown = prev.type === 'markdown'
      if (noteIsMarkdown !== prevIsMarkdown) {
        setIsMarkdown(noteIsMarkdown)
        setShowMarkdownPreview(noteIsMarkdown)
      }
      const prevReminder = prev.reminder ? new Date(prev.reminder as unknown as string).getTime() : null
      const nextReminder = note.reminder ? new Date(note.reminder as unknown as string).getTime() : null
      if (prevReminder !== nextReminder) {
        setCurrentReminder(note.reminder ? new Date(note.reminder as unknown as string) : null)
      }
    }

    prevNoteRef.current = note
  }, [note])

  useEffect(() => {
    setContextNotebookId(note.notebookId || null)
  }, [note.notebookId, setContextNotebookId])

  const [dismissedTags, setDismissedTags] = useState<string[]>([])
  const dismissedTagsLoadedRef = useRef(false)

  useEffect(() => {
    dismissedTagsLoadedRef.current = false
    try {
      const stored = localStorage.getItem(`dismissed-tags-${note.id}`)
      if (stored) {
        setDismissedTags(JSON.parse(stored))
        dismissedTagsLoadedRef.current = true
      } else {
        setDismissedTags([])
      }
    } catch (_) {
      setDismissedTags([])
    }
  }, [note.id])

  const autoTaggingEnabled = autoLabelingEnabled && dismissedTags.length < 3

  const { suggestions, isAnalyzing: isAnalyzingSuggestions } = useAutoTagging({
    content: content,
    notebookId: note.notebookId,
    enabled: autoTaggingEnabled,
    onQuotaExceeded: () => setQuotaExceededFeature('auto_tag')
  })

  const [showReminderDialog, setShowReminderDialog] = useState(false)
  const [currentReminder, setCurrentReminder] = useState<Date | null>(
    note.reminder ? new Date(note.reminder as unknown as string) : null
  )

  const [showLinkDialog, setShowLinkDialog] = useState(false)
  const [linkUrl, setLinkUrl] = useState('')

  const [titleSuggestions, setTitleSuggestions] = useState<TitleSuggestion[]>([])
  const [isGeneratingTitles, setIsGeneratingTitles] = useState(false)

  const [isReformulating, setIsReformulating] = useState(false)
  const [reformulationModal, setReformulationModal] = useState<{
    originalText: string
    reformulatedText: string
    option: string
  } | null>(null)

  const [isProcessingAI, setIsProcessingAI] = useState(false)
  const [aiOpen, setAiOpen] = useState(false)
  const [infoOpen, setInfoOpen] = useState(false)
  const [isDirty, setIsDirty] = useState(false)

  const [dismissedTitleSuggestions, setDismissedTitleSuggestions] = useState(false)
  const { suggestions: autoTitleSuggestions } = useTitleSuggestions({
    content,
    enabled: fullPage && !title && !dismissedTitleSuggestions,
  })

  useEffect(() => {
    if (autoTitleSuggestions.length > 0) {
      setTitleSuggestions(autoTitleSuggestions)
    }
  }, [autoTitleSuggestions])

  const [previousContentForCopilot, setPreviousContentForCopilot] = useState<string | null>(null)

  const [comparisonNotes, setComparisonNotes] = useState<Array<Partial<Note>>>([])
  const [fusionNotes, setFusionNotes] = useState<Array<Partial<Note>>>([])

  const existingLabelsLower = useMemo(
    () => (note.labels || []).map((l) => l.toLowerCase()),
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [JSON.stringify(note.labels)]
  )
  const filteredSuggestions = useMemo(() => suggestions.filter(s => {
    if (!s || !s.tag) return false
    return !dismissedTags.includes(s.tag) && !existingLabelsLower.includes(s.tag.toLowerCase())
  }), [suggestions, dismissedTags, existingLabelsLower])

  const colorClasses = NOTE_COLORS[color as NoteColor] || NOTE_COLORS.default

  const uploadImageFile = async (file: File) => {
    const formData = new FormData()
    formData.append('file', file)
    const response = await fetch('/api/upload', { method: 'POST', body: formData })
    if (!response.ok) throw new Error('Upload failed')
    const data = await response.json()
    return data.url
  }

  const handleImageUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
    const files = e.target.files
    if (!files) return

    for (const file of Array.from(files)) {
      try {
        const url = await uploadImageFile(file)
        setImages(prev => prev.includes(url) ? prev : [...prev, url])
        setIsDirty(true)
      } catch (error) {
        console.error('Upload error:', error)
        toast.error(t('notes.uploadFailed', { filename: file.name }))
      }
    }
  }

  useEffect(() => {
    const handlePaste = async (e: ClipboardEvent) => {
      if (!isMarkdown && (e.target as HTMLElement)?.closest('.notion-editor')) return;
      const items = e.clipboardData?.items
      if (!items) return
      for (const item of Array.from(items)) {
        if (item.type.startsWith('image/')) {
          e.preventDefault()
          const file = item.getAsFile()
          if (!file) continue
          try {
            const url = await uploadImageFile(file)
            setImages(prev => prev.includes(url) ? prev : [...prev, url])
            setIsDirty(true)
          } catch {
            toast.error(t('notes.uploadFailed', { filename: 'pasted image' }))
          }
        }
      }
    }
    document.addEventListener('paste', handlePaste, { capture: true })
    return () => document.removeEventListener('paste', handlePaste, { capture: true } as any)
  }, [t, isMarkdown])

  useEffect(() => {
    const el = textareaRef.current
    if (!el) return
    el.style.height = 'auto'
    el.style.height = Math.max(el.scrollHeight, 280) + 'px'
  }, [content])

  useEffect(() => {
    if (showMarkdownPreview) return
    const raf = requestAnimationFrame(() => {
      const el = textareaRef.current
      if (!el) return
      el.style.height = 'auto'
      el.style.height = Math.max(el.scrollHeight, 280) + 'px'
      el.focus()
    })
    return () => cancelAnimationFrame(raf)
  }, [showMarkdownPreview])

  const handleRemoveImage = (index: number) => {
    const removedUrl = images[index]
    setImages(images.filter((_, i) => i !== index))
    if (removedUrl) {
      setRemovedImageUrls(prev => [...prev, removedUrl])
    }
    setIsDirty(true)
  }

  const handleAddLink = async () => {
    if (!linkUrl) return

    setShowLinkDialog(false)

    try {
      const metadata = await fetchLinkMetadata(linkUrl)
      if (metadata) {
        setLinks(prev => [...prev, metadata])
        toast.success(t('notes.linkAdded'))
      } else {
        toast.warning(t('notes.linkMetadataFailed'))
        setLinks(prev => [...prev, { url: linkUrl, title: linkUrl }])
      }
    } catch (error) {
      console.error('Failed to add link:', error)
      toast.error(t('notes.linkAddFailed'))
    } finally {
      setLinkUrl('')
    }
  }

  const handleRemoveLink = (index: number) => {
    setLinks(links.filter((_, i) => i !== index))
  }

  /** Images extraites du corps (TipTap / markdown) — pas de couverture auto */
  const contentImages = useMemo(() => {
    if (isMarkdown) {
      const urls = new Set<string>()
      for (const match of content.matchAll(/!\[.*?\]\((.*?)\)/g)) {
        const src = match[1]?.trim()
        if (src) urls.add(src)
      }
      return Array.from(urls)
    }
    return extractImagesFromHTML(content)
  }, [content, isMarkdown])

  /**
   * allImages = galerie explicite + corps (pour IA, sélecteur de couverture).
   * La couverture hero n'utilise PAS allImages[0] automatiquement.
   */
  const allImages = useMemo(() => {
    return Array.from(new Set([...images, ...contentImages]))
  }, [images, contentImages])

  /** Couverture explicite = première entrée de note.images (jamais auto depuis le corps) */
  const coverImage = images[0] ?? null

  const resolveContentForSave = useCallback((): string => {
    if (!isMarkdown) {
      const editor = richTextEditorRef.current?.getEditor()
      if (editor) return editor.getHTML()
    }
    return contentRef.current
  }, [isMarkdown])

  /**
   * note.images = couverture / galerie **explicite** uniquement.
   * Ne plus fusionner les images du corps : sinon, coller une image dans l'éditeur
   * la plaçait sous le titre, et la supprimer du corps la laissait en "standalone" forever.
   */
  const resolveImagesForSave = useCallback((_contentToSave: string): string[] => {
    return images.filter((url) => !removedImageUrls.includes(url))
  }, [images, removedImageUrls])

  const setCoverImage = useCallback((url: string) => {
    setImages((prev) => {
      const rest = prev.filter((u) => u !== url)
      return [url, ...rest]
    })
    setRemovedImageUrls((prev) => prev.filter((u) => u !== url))
    setIsDirty(true)
  }, [])

  const clearCoverImage = useCallback(() => {
    setImages((prev) => {
      if (prev.length === 0) return prev
      setRemovedImageUrls((r) => Array.from(new Set([...r, ...prev])))
      return []
    })
    setIsDirty(true)
  }, [])


  const handleGenerateTitles = async () => {
    // Utiliser le contenu live de l'éditeur (TipTap) plutôt que le state debounced
    const liveContent = resolveContentForSave()
    const fullContentForAI = [
      liveContent,
      ...links.map(l => `${l.title || ''} ${l.description || ''}`)
    ]
    .join(' ')
    .trim()

    const wordCount = fullContentForAI.split(/\s+/).filter(word => word.length > 0).length

    if (wordCount < 10) {
      toast.error(t('ai.titleGenerationMinWords', { count: wordCount }))
      return
    }

    const consented = await requestAiConsent()
    if (!consented) return

    setIsGeneratingTitles(true)
    try {
      const response = await fetch('/api/ai/title-suggestions', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ content: fullContentForAI.substring(0, 8000) }),
      })

      if (!response.ok) {
        if (response.status === 402) {
          setQuotaExceededFeature('auto_title')
          return
        }
        const errorData = await response.json()
        throw new Error(errorData.errorKey ? t(errorData.errorKey) : (errorData.error || t('ai.titleGenerationError')))
      }

      const data = await response.json()
      setTitleSuggestions(data.suggestions || [])
      if (!fullPage && data.suggestions?.[0]?.title) {
        setTitle(data.suggestions[0].title)
        setDismissedTitleSuggestions(true)
        toast.success(t('ai.titlesGenerated', { count: data.suggestions.length }))
      } else if (data.suggestions?.length) {
        toast.success(t('ai.titlesGenerated', { count: data.suggestions.length }))
      }
    } catch (error: any) {
      console.error('Error generating titles:', error)
      toast.error(error.message || t('ai.titleGenerationFailed'))
    } finally {
      setIsGeneratingTitles(false)
    }
  }

  const handleSelectTitle = (title: string) => {
    setTitle(title)
    setTitleSuggestions([])
    toast.success(t('ai.titleApplied'))
  }

  const handleReformulate = async (option: 'clarify' | 'shorten' | 'improve') => {
    const selectedText = window.getSelection()?.toString()

    if (!selectedText && (!content || content.trim().length === 0)) {
      toast.error(t('ai.reformulationNoText'))
      return
    }

    let textToReformulate: string
    if (selectedText && selectedText.trim().split(/\s+/).filter(word => word.length > 0).length >= 10) {
      textToReformulate = selectedText
    } else {
      textToReformulate = content
      if (selectedText) {
        toast.info(t('ai.reformulationSelectionTooShort'))
      }
    }

    const wordCount = textToReformulate.trim().split(/\s+/).filter(word => word.length > 0).length

    if (wordCount < 10) {
      toast.error(t('ai.reformulationMinWords', { count: wordCount }))
      return
    }

    if (wordCount > 500) {
      toast.error(t('ai.reformulationMaxWords'))
      return
    }

    const consented = await requestAiConsent()
    if (!consented) return

    setIsReformulating(true)
    try {
      const response = await fetch('/api/ai/reformulate', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          text: textToReformulate,
          option: option
        }),
      })

      if (!response.ok) {
        if (response.status === 402) {
          setQuotaExceededFeature('reformulate')
          return
        }
        const errorData = await response.json()
        throw new Error(errorData.errorKey ? t(errorData.errorKey) : (errorData.error || t('ai.reformulationError')))
      }

      const data = await response.json()

      setReformulationModal({
        originalText: data.originalText,
        reformulatedText: data.reformulatedText,
        option: data.option
      })
    } catch (error: any) {
      console.error('Error reformulating:', error)
      toast.error(error.message || t('ai.reformulationFailed'))
    } finally {
      setIsReformulating(false)
    }
  }

  const handleClarifyDirect = async () => {
    const wordCount = content.split(/\s+/).filter(w => w.length > 0).length
    if (!content || wordCount < 10) {
      toast.error(t('ai.reformulationMinWords', { count: wordCount }))
      return
    }

    const consented = await requestAiConsent()
    if (!consented) return

    setIsProcessingAI(true)
    try {
      const response = await fetch('/api/ai/reformulate', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ text: content, option: 'clarify' })
      })
      if (response.status === 402) {
        setQuotaExceededFeature('reformulate')
        return
      }
      const data = await response.json()
      if (!response.ok) throw new Error(data.errorKey ? t(data.errorKey) : (data.error || t('notes.clarifyFailed')))
      setContentImmediate(data.reformulatedText || data.text)
      toast.success(t('ai.reformulationApplied'))
    } catch (error) {
      console.error('Clarify error:', error)
      toast.error(t('notes.clarifyFailed'))
    } finally {
      setIsProcessingAI(false)
    }
  }

  const handleShortenDirect = async () => {
    const wordCount = content.split(/\s+/).filter(w => w.length > 0).length
    if (!content || wordCount < 10) {
      toast.error(t('ai.reformulationMinWords', { count: wordCount }))
      return
    }

    const consented = await requestAiConsent()
    if (!consented) return

    setIsProcessingAI(true)
    try {
      const response = await fetch('/api/ai/reformulate', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ text: content, option: 'shorten' })
      })
      if (response.status === 402) {
        setQuotaExceededFeature('reformulate')
        return
      }
      const data = await response.json()
      if (!response.ok) throw new Error(data.errorKey ? t(data.errorKey) : (data.error || t('notes.shortenFailed')))
      setContentImmediate(data.reformulatedText || data.text)
      toast.success(t('ai.reformulationApplied'))
    } catch (error) {
      console.error('Shorten error:', error)
      toast.error(t('notes.shortenFailed'))
    } finally {
      setIsProcessingAI(false)
    }
  }

  const handleImproveDirect = async () => {
    const wordCount = content.split(/\s+/).filter(w => w.length > 0).length
    if (!content || wordCount < 10) {
      toast.error(t('ai.reformulationMinWords', { count: wordCount }))
      return
    }

    const consented = await requestAiConsent()
    if (!consented) return

    setIsProcessingAI(true)
    try {
      const response = await fetch('/api/ai/reformulate', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ text: content, option: 'improve' })
      })
      if (response.status === 402) {
        setQuotaExceededFeature('reformulate')
        return
      }
      const data = await response.json()
      if (!response.ok) throw new Error(data.errorKey ? t(data.errorKey) : (data.error || t('notes.improveFailed')))
      setContentImmediate(data.reformulatedText || data.text)
      toast.success(t('ai.reformulationApplied'))
    } catch (error) {
      console.error('Improve error:', error)
      toast.error(t('notes.improveFailed'))
    } finally {
      setIsProcessingAI(false)
    }
  }

  const handleTransformMarkdown = async () => {
    const wordCount = content.split(/\s+/).filter(w => w.length > 0).length
    if (!content || wordCount < 10) {
      toast.error(t('ai.reformulationMinWords', { count: wordCount }))
      return
    }

    if (wordCount > 500) {
      toast.error(t('ai.reformulationMaxWords'))
      return
    }

    const consented = await requestAiConsent()
    if (!consented) return

    setIsProcessingAI(true)
    try {
      const response = await fetch('/api/ai/transform-markdown', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ text: content })
      })
      const data = await response.json()
      if (!response.ok) throw new Error(data.errorKey ? t(data.errorKey) : (data.error || t('notes.transformFailed')))

      setContentImmediate(data.transformedText)
      setIsMarkdown(true)
      setShowMarkdownPreview(false)

      toast.success(t('ai.transformSuccess'))
    } catch (error) {
      console.error('Transform to markdown error:', error)
      toast.error(t('ai.transformError'))
    } finally {
      setIsProcessingAI(false)
    }
  }

  const handleApplyRefactor = () => {
    if (!reformulationModal) return

    setContentImmediate(reformulationModal.reformulatedText)
    setReformulationModal(null)
    toast.success(t('ai.reformulationApplied'))
  }

  const handleReminderSave = async (date: Date) => {
    if (date < new Date()) {
      toast.error(t('notes.reminderPastError'))
      return
    }
    setCurrentReminder(date)
    try {
      await updateNote(note.id, { reminder: date }, { skipRevalidation: true })
      toast.success(t('notes.reminderSet', { datetime: date.toLocaleString() }))
    } catch {
      toast.error(t('notebook.savingReminder'))
    }
  }

  const handleRemoveReminder = async () => {
    setCurrentReminder(null)
    try {
      await updateNote(note.id, { reminder: null }, { skipRevalidation: true })
      toast.success(t('notes.reminderRemoved'))
    } catch {
      toast.error(t('notebook.removingReminder'))
    }
  }

  // Fonction de sauvegarde unifiée — évite la duplication handleSave/handleSaveInPlace
  const performSave = useCallback(async ({ silent = false, inPlace = false }: { silent?: boolean; inPlace?: boolean } = {}): Promise<boolean> => {
    setIsSaving(true)
    try {
      const contentToSave = resolveContentForSave()
      const imagesToSave = resolveImagesForSave(contentToSave)
      const result = await updateNote(note.id, {
        title: title.trim() || null,
        content: contentToSave,
        checkItems: null,
        labels,
        images: imagesToSave,
        links,
        color,
        reminder: currentReminder,
        isMarkdown,
        type: isMarkdown ? 'markdown' as const : 'richtext' as const,
        size,
      }, { skipRevalidation: true })
      if (contentToSave !== content) setContentImmediate(contentToSave)
      if (imagesToSave.length !== images.length || imagesToSave.some((u, i) => u !== images[i])) setImages(imagesToSave)
      prevNoteRef.current = { ...prevNoteRef.current, ...result }
      const deletedImages = Array.from(new Set([
        ...removedImageUrls,
        ...images.filter(url => !imagesToSave.includes(url))
      ]))
      if (deletedImages.length > 0) {
        cleanupOrphanedImages(deletedImages, note.id).catch(() => {})
        setRemovedImageUrls([])
      }
      await refreshLabels()
      onNoteSaved?.(result)
      queryClient.invalidateQueries({ queryKey: queryKeys.note(note.id) })
      queryClient.invalidateQueries({ queryKey: queryKeys.notes(note.notebookId) })
      emitNoteChange({ type: 'updated', note: result })
      setIsDirty(false)
      setLastSavedAt(new Date())
      if (!silent) toast.success(t('notes.saved') || 'Note sauvegardée !')
      return true
    } catch (error) {
      console.error('[SAVE] updateNote failed:', error)
      toast.error(t('notes.saveFailed') || 'Erreur lors de la sauvegarde.')
      return false
    } finally {
      setIsSaving(false)
    }
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [note.id, note.notebookId, title, labels, images, links, color, currentReminder, isMarkdown, size, removedImageUrls, content, t])

  // Aliases stables pour la compatibilité
  const handleSave = useCallback((opts?: { silent?: boolean }) => performSave({ ...opts, inPlace: false }), [performSave])
  const handleSaveInPlace = useCallback((opts?: { silent?: boolean }) => performSave({ ...opts, inPlace: true }), [performSave])

  const handleCheckItem = (id: string) => {
    setCheckItems(items =>
      items.map(item =>
        item.id === id ? { ...item, checked: !item.checked } : item
      )
    )
  }

  const handleUpdateCheckItem = (id: string, text: string) => {
    setCheckItems(items =>
      items.map(item => (item.id === id ? { ...item, text } : item))
    )
  }

  const handleAddCheckItem = () => {
    setCheckItems([
      ...checkItems,
      { id: Date.now().toString(), text: '', checked: false },
    ])
  }

  const handleRemoveCheckItem = (id: string) => {
    setCheckItems(items => items.filter(item => item.id !== id))
  }

  const handleSelectGhostTag = async (tag: string) => {
    const tagExists = labels.some(l => l.toLowerCase() === tag.toLowerCase())

    if (!tagExists) {
      setLabels(prev => [...prev, tag])
      setIsDirty(true)

      const globalExists = globalLabels.some(l => l.name.toLowerCase() === tag.toLowerCase())
      if (!globalExists) {
        try {
          await addLabel(tag)
        } catch (err) {
          console.error('Error creating auto-label:', err)
        }
      }
      toast.success(t('ai.tagAdded', { tag }))
    }
  }

  const handleDismissGhostTag = (tag: string) => {
    setDismissedTags(prev => {
      const next = [...prev, tag]
      try { localStorage.setItem(`dismissed-tags-${note.id}`, JSON.stringify(next)) } catch (_) {}
      return next
    })
  }

  const handleRemoveLabel = (label: string) => {
    setLabels(labels.filter(l => l !== label))
    setIsDirty(true)
  }

  const handleMakeCopy = async () => {
    try {
      const newNote = await createNote({
        title: `${title || t('notes.untitled')} (${t('notes.copy')})`,
        content: content,
        color: color,
        checkItems: checkItems,
        labels: labels,
        images: images,
        links: links,
        isMarkdown,
        type: isMarkdown ? 'markdown' : 'richtext',
        size: size,
      })
      toast.success(t('notes.copySuccess'))
      queryClient.invalidateQueries({ queryKey: queryKeys.notes(note.notebookId) })
      emitNoteChange({ type: 'created', note: newNote })
    } catch (error) {
      console.error('Failed to copy note:', error)
      toast.error(t('notes.copyFailed'))
    }
  }

  // handleSaveInPlace était un alias de handleSave — supprimé, maintenant unifié dans performSave

  const handleSaveInPlaceRef = useRef(handleSaveInPlace)
  handleSaveInPlaceRef.current = handleSaveInPlace
  const handleSaveRef = useRef(handleSave)
  handleSaveRef.current = handleSave

  const toggleAutoSave = useCallback(async () => {
    const nextVal = !autoSaveEnabled
    setAutoSaveEnabled(nextVal)
    if (session?.user?.id) {
      try {
        await updateAISettings({ autoSave: nextVal })
        toast.success(
          nextVal
            ? t('settings.autoSaveEnabled') || 'Auto-enregistrement activé !'
            : t('settings.autoSaveDisabled') || 'Auto-enregistrement désactivé'
        )
      } catch (err) {
        console.error("Failed to update autoSave setting:", err)
        toast.error(t('general.error'))
      }
    }
  }, [autoSaveEnabled, session?.user?.id, t])

  // Auto-save : 8s après le dernier changement si isDirty et autoSave est activé (silencieux — pas de toast)
  const autoSaveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
  useEffect(() => {
    if (!autoSaveEnabled || !isDirty || isSaving || readOnly) return
    if (autoSaveTimerRef.current) clearTimeout(autoSaveTimerRef.current)
    autoSaveTimerRef.current = setTimeout(() => {
      if (fullPage) {
        void handleSaveInPlaceRef.current({ silent: true })
      } else {
        void handleSaveRef.current({ silent: true })
      }
    }, 8000)
    return () => {
      if (autoSaveTimerRef.current) clearTimeout(autoSaveTimerRef.current)
    }
  }, [isDirty, isSaving, readOnly, fullPage, autoSaveEnabled])

  useEffect(() => {
    const handler = (e: KeyboardEvent) => {
      if ((e.ctrlKey || e.metaKey) && e.key === 's') {
        e.preventDefault()
        if (fullPage) {
          void handleSaveInPlaceRef.current()
        } else {
          void handleSaveRef.current()
        }
      }
    }
    document.addEventListener('keydown', handler)
    return () => document.removeEventListener('keydown', handler)
  }, [fullPage])

  useEffect(() => {
    const onRequestSave = (event: Event) => {
      const detail = (event as CustomEvent<{ noteId?: string }>).detail
      if (detail?.noteId !== note.id || readOnly) return
      if (fullPage) {
        void handleSaveInPlaceRef.current()
      } else {
        void handleSaveRef.current()
      }
    }
    window.addEventListener(NOTE_REQUEST_SAVE_EVENT, onRequestSave)
    return () => window.removeEventListener(NOTE_REQUEST_SAVE_EVENT, onRequestSave)
  }, [note.id, fullPage, readOnly])

  const state: NoteEditorState = useMemo(() => ({
    title,
    content,
    checkItems,
    labels,
    images,
    links,
    newLabel,
    color: color as NoteColor,
    size,
    showMarkdownPreview,
    removedImageUrls,
    isSaving,
    isDirty,
    lastSavedAt,
    isProcessingAI,
    aiOpen,
    infoOpen,
    isGeneratingTitles,
    titleSuggestions,
    dismissedTitleSuggestions,
    isReformulating,
    reformulationModal,
    previousContentForCopilot,
    showReminderDialog,
    currentReminder,
    showLinkDialog,
    linkUrl,
    comparisonNotes,
    fusionNotes,
    dismissedTags,
    filteredSuggestions,
    isAnalyzingSuggestions,
    isMarkdown,
    allImages,
    contentImages,
    coverImage,
    colorClasses,
    quotaExceededFeature,
    autoSaveEnabled,
  }), [
    title, content, checkItems, labels, images, links, newLabel, color, size,
    showMarkdownPreview, removedImageUrls, isSaving, isDirty, lastSavedAt, isProcessingAI, aiOpen, infoOpen,
    isGeneratingTitles, titleSuggestions, dismissedTitleSuggestions, isReformulating,
    reformulationModal, previousContentForCopilot, showReminderDialog, currentReminder,
    showLinkDialog, linkUrl, comparisonNotes, fusionNotes, dismissedTags, filteredSuggestions,
    isAnalyzingSuggestions, isMarkdown, allImages, contentImages, coverImage, colorClasses, quotaExceededFeature, autoSaveEnabled
  ])

  const actions: NoteEditorActions = useMemo(() => ({
    setTitle: (t) => { setTitle(t); setIsDirty(true); setDismissedTitleSuggestions(true) },
    setDismissedTitleSuggestions,
    setContent: (c) => { setContent(c); setIsDirty(true) },
    setContentImmediate: (c) => { setContentImmediate(c); setIsDirty(true) },
    setCheckItems,
    handleCheckItem,
    handleUpdateCheckItem,
    handleAddCheckItem,
    handleRemoveCheckItem,
    setLabels: (l) => { setLabels(l); setIsDirty(true) },
    handleSelectGhostTag,
    handleDismissGhostTag,
    handleRemoveLabel,
    setImages,
    handleImageUpload,
    handleRemoveImage,
    setCoverImage,
    clearCoverImage,
    uploadImageFile,
    setLinks,
    handleAddLink,
    handleRemoveLink,
    setShowMarkdownPreview: (show) => { setShowMarkdownPreview(show); setIsDirty(true) },
    setIsMarkdown: (m) => { setIsMarkdown(m); setIsDirty(true) },
    convertToRichText: (html) => {
      setContentImmediate(html)
      setIsMarkdown(false)
      setShowMarkdownPreview(false)
      setIsDirty(true)
    },
    setColor: (c) => { setColor(c); setIsDirty(true) },
    setSize: (s) => { setSize(s); setIsDirty(true) },
    setShowReminderDialog,
    setCurrentReminder,
    handleReminderSave,
    handleRemoveReminder,
    setShowLinkDialog,
    setLinkUrl,
    handleGenerateTitles,
    handleSelectTitle,
    handleReformulate,
    handleApplyRefactor,
    handleClarifyDirect,
    handleShortenDirect,
    handleImproveDirect,
    handleTransformMarkdown,
    handleSave,
    handleSaveInPlace,
    handleMakeCopy,
    setComparisonNotes,
    setFusionNotes,
    setReformulationModal,
    setIsDirty,
    setAiOpen,
    setInfoOpen,
    setIsProcessingAI,
    setIsGeneratingTitles,
    setIsAnalyzingSuggestions: (_a) => { /* handled by useAutoTagging */ },
    setPreviousContentForCopilot,
    setQuotaExceededFeature,
    toggleAutoSave,
  }), [
    setContent, setContentImmediate, setCoverImage, clearCoverImage,
    handleCheckItem, handleUpdateCheckItem, handleAddCheckItem, handleRemoveCheckItem,
    handleSelectGhostTag, handleDismissGhostTag, handleRemoveLabel, handleImageUpload,
    handleRemoveImage, handleAddLink, handleRemoveLink, handleReminderSave,
    handleRemoveReminder, handleGenerateTitles, handleSelectTitle, handleReformulate,
    handleApplyRefactor, handleClarifyDirect, handleShortenDirect, handleImproveDirect,
    handleTransformMarkdown, handleSave, handleSaveInPlace, handleMakeCopy, setQuotaExceededFeature, toggleAutoSave
  ])

  // Notebooks mappés en dehors du useMemo pour éviter de recréer le tableau à chaque render du contexte
  const mappedNotebooks = useMemo(
    () => notebooks.map(nb => ({ id: nb.id, name: nb.name, parentId: nb.parentId, trashedAt: nb.trashedAt })),
    [notebooks]
  )

  const value: NoteEditorContextValue = useMemo(() => ({
    note,
    readOnly,
    fullPage,
    state,
    actions,
    notebooks: mappedNotebooks,
    globalLabels,
    fileInputRef,
    textareaRef,
    richTextEditorRef,
  }), [note, readOnly, fullPage, state, actions, mappedNotebooks, globalLabels])

  return (
    <NoteEditorContext.Provider value={value}>
      {children}
    </NoteEditorContext.Provider>
  )
}

export function useNoteEditorContext() {
  const context = useContext(NoteEditorContext)
  if (context === undefined) {
    throw new Error('useNoteEditorContext must be used within a NoteEditorProvider')
  }
  return context
}
