'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 } 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)

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

  const [title, setTitle] = useState(note.title || '')
  const [content, setContent] = useState(note.content)
  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 [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) {
      setTitle(note.title || '')
      setContent(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) setContent(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
  })

  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 = (note.labels || []).map((l) => l.toLowerCase())
  const filteredSuggestions = suggestions.filter(s => {
    if (!s || !s.tag) return false
    return !dismissedTags.includes(s.tag) && !existingLabelsLower.includes(s.tag.toLowerCase())
  })

  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))
  }

  const allImages = useMemo(() => {
    const extracted = !isMarkdown ? extractImagesFromHTML(content) : []
    return Array.from(new Set([...images, ...extracted]))
  }, [images, content, isMarkdown])

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

  const resolveImagesForSave = useCallback((contentToSave: string): string[] => {
    const extracted = !isMarkdown ? extractImagesFromHTML(contentToSave) : []
    return Array.from(new Set([...images, ...extracted]))
  }, [images, isMarkdown])

  const handleGenerateTitles = async () => {
    const fullContentForAI = [
      content,
      ...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 }),
      })

      if (!response.ok) {
        const errorData = await response.json()
        throw new Error(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) {
        const errorData = await response.json()
        throw new Error(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' })
      })
      const data = await response.json()
      if (!response.ok) throw new Error(data.error || t('notes.clarifyFailed'))
      setContent(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' })
      })
      const data = await response.json()
      if (!response.ok) throw new Error(data.error || t('notes.shortenFailed'))
      setContent(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' })
      })
      const data = await response.json()
      if (!response.ok) throw new Error(data.error || t('notes.improveFailed'))
      setContent(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.error || t('notes.transformFailed'))

      setContent(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

    setContent(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'))
    }
  }

  const handleSave = async () => {
    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) setContent(contentToSave)
      if (JSON.stringify(imagesToSave) !== JSON.stringify(images)) setImages(imagesToSave)
      prevNoteRef.current = { ...prevNoteRef.current, ...result }
      if (removedImageUrls.length > 0) {
        cleanupOrphanedImages(removedImageUrls, note.id).catch(() => {})
      }
      await refreshLabels()
      onNoteSaved?.(result)
      queryClient.invalidateQueries({ queryKey: queryKeys.note(note.id) })
      queryClient.invalidateQueries({ queryKey: queryKeys.notes(note.notebookId) })
      emitNoteChange({ type: 'updated', note: result })
      toast.success(t('notes.saved') || 'Note sauvegardée !')
    } catch (error) {
      console.error('[SAVE] updateNote failed:', error)
      toast.error(t('notes.saveFailed') || 'Erreur lors de la sauvegarde.')
    } finally {
      setIsSaving(false)
    }
  }

  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'))
    }
  }

  const handleSaveInPlace = async () => {
    setIsSaving(true)
    try {
      const contentToSave = resolveContentForSave()
      const imagesToSave = resolveImagesForSave(contentToSave)
      const updatePayload = {
        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,
      }
      const result = await updateNote(note.id, updatePayload, { skipRevalidation: true })
      if (contentToSave !== content) setContent(contentToSave)
      if (JSON.stringify(imagesToSave) !== JSON.stringify(images)) setImages(imagesToSave)
      prevNoteRef.current = { ...prevNoteRef.current, ...result }
      if (removedImageUrls.length > 0) {
        cleanupOrphanedImages(removedImageUrls, note.id).catch(() => {})
      }
      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)
      toast.success(t('notes.saved') || 'Saved')
    } catch (error) {
      console.error('[SAVE] updateNote failed:', error)
      toast.error(t('notes.saveFailed') || 'Save failed')
    } finally {
      setIsSaving(false)
    }
  }

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

  useEffect(() => {
    if (!fullPage) return
    const handler = (e: KeyboardEvent) => {
      if ((e.ctrlKey || e.metaKey) && e.key === 's') {
        e.preventDefault()
        void handleSaveInPlaceRef.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,
    isProcessingAI,
    aiOpen,
    infoOpen,
    isGeneratingTitles,
    titleSuggestions,
    dismissedTitleSuggestions,
    isReformulating,
    reformulationModal,
    previousContentForCopilot,
    showReminderDialog,
    currentReminder,
    showLinkDialog,
    linkUrl,
    comparisonNotes,
    fusionNotes,
    dismissedTags,
    filteredSuggestions,
    isAnalyzingSuggestions,
    isMarkdown,
    allImages,
    colorClasses,
  }), [
    title, content, checkItems, labels, images, links, newLabel, color, size,
    showMarkdownPreview, removedImageUrls, isSaving, isDirty, isProcessingAI, aiOpen, infoOpen,
    isGeneratingTitles, titleSuggestions, dismissedTitleSuggestions, isReformulating,
    reformulationModal, previousContentForCopilot, showReminderDialog, currentReminder,
    showLinkDialog, linkUrl, comparisonNotes, fusionNotes, dismissedTags, filteredSuggestions,
    isAnalyzingSuggestions, isMarkdown, allImages, colorClasses
  ])

  const actions: NoteEditorActions = {
    setTitle: (t) => { setTitle(t); setIsDirty(true); setDismissedTitleSuggestions(true) },
    setDismissedTitleSuggestions,
    setContent: (c) => { setContent(c); setIsDirty(true) },
    setCheckItems,
    handleCheckItem,
    handleUpdateCheckItem,
    handleAddCheckItem,
    handleRemoveCheckItem,
    setLabels: (l) => { setLabels(l); setIsDirty(true) },
    handleSelectGhostTag,
    handleDismissGhostTag,
    handleRemoveLabel,
    setImages,
    handleImageUpload,
    handleRemoveImage,
    uploadImageFile,
    setLinks,
    handleAddLink,
    handleRemoveLink,
    setShowMarkdownPreview: (show) => { setShowMarkdownPreview(show); setIsDirty(true) },
    setIsMarkdown: (m) => { setIsMarkdown(m); 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,
  }

  const value: NoteEditorContextValue = useMemo(() => ({
    note,
    readOnly,
    fullPage,
    state,
    actions,
    notebooks: notebooks.map(nb => ({ id: nb.id, name: nb.name, parentId: nb.parentId, trashedAt: nb.trashedAt })),
    globalLabels,
    fileInputRef,
    textareaRef,
    richTextEditorRef,
  }), [note, readOnly, fullPage, state, actions, notebooks, 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
}
