import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import {
  Dialog,
  DialogContent,
  DialogFooter,
  DialogHeader,
  DialogTitle
} from '@/components/ui/dialog'
import { Label } from '@/components/ui/label'
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue
} from '@/components/ui/select'
import logger from '@/lib/logger'
import { isLanguageToolConfigured } from '@/lib/languagetool-client'
import { languageToolLintExtension, requestAdvancedLabGrammarLint } from '@/lib/languagetool-cm-linter'
import { pickLanguageToolCodeForTranslateTarget } from '@/lib/languagetool-language-order'
import {
  buildResolvedTranslateMenuLanguageOptions,
  translateLanguageOptionMatchesQuery
} from '@/lib/language-display-meta'
import { LanguageSelectOptionLines } from '@/lib/language-select-option-lines'
import { buildLabLanguageToolPreferenceList } from '@/lib/trinity-languages'
import { parseLabSlice, type AdvancedEventLabSlice } from '@/lib/advanced-event-lab-slice'
import { translateAdvancedLabMarkup } from '@/lib/advanced-lab-markup-protect'
import {
  fetchTranslateLanguages,
  isTranslateConfigured,
  translateApiLanguageCode,
  type TranslateLanguageOption
} from '@/lib/translate-client'
import { setReadAloudTranslationForEvent } from '@/lib/read-aloud-translation-override'
import { defaultKeymap, history, historyKeymap } from '@codemirror/commands'
import { markdown } from '@codemirror/lang-markdown'
import { StreamLanguage } from '@codemirror/language'
import { asciidoc } from 'codemirror-asciidoc'
import { EditorSelection, EditorState, type Extension } from '@codemirror/state'
import { oneDark } from '@codemirror/theme-one-dark'
import {
  EditorView,
  keymap,
  lineNumbers,
  placeholder as cmPlaceholder
} from '@codemirror/view'
import { Undo2 } from 'lucide-react'
import type { MutableRefObject, ReactNode } from 'react'
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import { AdvancedEventLabMarkupToolbar } from './AdvancedEventLabMarkupToolbar'
import { AdvancedEventLabPreviewPane } from './AdvancedEventLabPreviewPane'
import customEmojiService from '@/services/custom-emoji.service'
import postEditorCache from '@/services/post-editor-cache.service'
import type { TEmoji } from '@/types'

const PREVIEW_DEBOUNCE_MS = 200

const LAB_UNDO_STORAGE_V = 1 as const
const LAB_UNDO_INTERVAL_MS = 30_000
const LAB_UNDO_MAX_CHECKPOINTS = 10

function labUndoSessionStorageKey(storageId: string): string {
  return `jumble:advLabUndo:${storageId}`
}

function cloneLabSlice(s: AdvancedEventLabSlice): AdvancedEventLabSlice {
  return { kind: s.kind, content: s.content, tags: s.tags.map((row) => [...row]) }
}

function labSlicesEqual(a: AdvancedEventLabSlice, b: AdvancedEventLabSlice): boolean {
  if (a.kind !== b.kind || a.content !== b.content) return false
  return JSON.stringify(a.tags) === JSON.stringify(b.tags)
}

function parseCheckpointEntry(raw: unknown): AdvancedEventLabSlice | null {
  let json: string
  try {
    json = JSON.stringify(raw)
  } catch {
    return null
  }
  const parsed = parseLabSlice(json)
  return parsed.ok ? parsed.value : null
}

function loadLabCheckpointsFromSession(
  storageId: string,
  base: AdvancedEventLabSlice
): AdvancedEventLabSlice[] | null {
  if (!storageId || typeof sessionStorage === 'undefined') return null
  try {
    const key = labUndoSessionStorageKey(storageId)
    const raw = sessionStorage.getItem(key)
    if (!raw) return null
    const o = JSON.parse(raw) as { v?: number; checkpoints?: unknown }
    if (!o || o.v !== LAB_UNDO_STORAGE_V || !Array.isArray(o.checkpoints)) return null
    const out: AdvancedEventLabSlice[] = []
    for (const row of o.checkpoints) {
      const slice = parseCheckpointEntry(row)
      if (!slice) return null
      out.push(slice)
    }
    if (out.length === 0) return null
    const last = out[out.length - 1]!
    if (!labSlicesEqual(last, base)) return null
    return out.slice(0, LAB_UNDO_MAX_CHECKPOINTS).map(cloneLabSlice)
  } catch {
    return null
  }
}

function persistLabCheckpointsToSession(storageId: string, checkpoints: AdvancedEventLabSlice[]): void {
  if (!storageId || typeof sessionStorage === 'undefined') return
  try {
    sessionStorage.setItem(
      labUndoSessionStorageKey(storageId),
      JSON.stringify({ v: LAB_UNDO_STORAGE_V, checkpoints })
    )
  } catch {
    // quota / private mode
  }
}

function clearLabCheckpointsSession(storageId: string): void {
  if (!storageId || typeof sessionStorage === 'undefined') return
  try {
    sessionStorage.removeItem(labUndoSessionStorageKey(storageId))
  } catch {
    /* ignore */
  }
}

/** Subset of {@link TPostTextareaHandle} so media upload + toolbar can target the lab surface. */
export type AdvancedLabBodyHandle = {
  getText: () => string
  insertText: (text: string) => void
  appendText: (text: string, addNewline?: boolean) => void
  insertEmoji: (emoji: string | TEmoji) => void
}

function cmInsertAtSelection(view: EditorView, text: string) {
  const sel = view.state.selection.main
  view.dispatch({
    changes: { from: sel.from, to: sel.to, insert: text },
    selection: EditorSelection.cursor(sel.from + text.length)
  })
  view.focus()
}

function cmAppendAtEnd(view: EditorView, text: string, addNewline = false) {
  const doc = view.state.doc
  const at = doc.length
  const prefix = addNewline && doc.length > 0 ? '\n' : ''
  const insert = prefix + text
  view.dispatch({
    changes: { from: at, to: at, insert },
    selection: EditorSelection.cursor(at + insert.length)
  })
  view.focus()
}

export type AdvancedEventLabDialogProps = {
  open: boolean
  onOpenChange: (open: boolean) => void
  /** Snapshot when opening; parent should memoize. */
  initial: AdvancedEventLabSlice | null
  /** When false, Apply keeps `initial.kind`. */
  kindEditable?: boolean
  markupMode: 'markdown' | 'asciidoc'
  /** `i18n.language` for LanguageTool default ordering. */
  i18nLanguage?: string
  /** When set, user can store translation for read-aloud for this event id. */
  contextEventId?: string | null
  onApply: (payload: AdvancedEventLabSlice) => void
  /** Filled while the markup editor is mounted (for uploads / shared toolbar). */
  bodyApiRef?: MutableRefObject<AdvancedLabBodyHandle | null>
  /** Same icon row as the main composer; should use {@link bodyApiRef} for inserts. */
  formatToolbar?: ReactNode
  /**
   * When set, lab markup/tags are debounced to the post-editor draft store (same persistence as TipTap)
   * so a **reload** can restore in-progress lab work. Closing without Apply (dismiss actions, including the cancel button, Escape, overlay)
   * clears this draft so the next open is seeded from TipTap again.
   */
  draftPersistenceKey?: string | null
  /** Lab preview: resolve custom `:shortcode:` from this author's NIP-30 inventory when tags do not define them. */
  previewAuthorPubkey?: string | null
  /** Lab preview: `emoji` tags on the fake event (e.g. copied from the event being edited). */
  previewEmojiTags?: string[][]
}

function useDarkModeFlag(): boolean {
  const [dark, setDark] = useState(() =>
    typeof document !== 'undefined'
      ? document.documentElement.classList.contains('dark')
      : false
  )
  useEffect(() => {
    const el = document.documentElement
    const obs = new MutationObserver(() => {
      setDark(el.classList.contains('dark'))
    })
    obs.observe(el, { attributes: true, attributeFilter: ['class'] })
    return () => obs.disconnect()
  }, [])
  return dark
}

export default function AdvancedEventLabDialog({
  open,
  onOpenChange,
  initial,
  kindEditable = true,
  markupMode,
  i18nLanguage,
  contextEventId,
  onApply,
  bodyApiRef,
  formatToolbar,
  draftPersistenceKey = null,
  previewAuthorPubkey = null,
  previewEmojiTags
}: AdvancedEventLabDialogProps) {
  const { t, i18n } = useTranslation()
  /** `useTranslation().t` can change identity every render; never list it as a layout-effect dep (editor remount loop). */
  const labTRef = useRef(t)
  labTRef.current = t
  const dark = useDarkModeFlag()
  const markupHost = useRef<HTMLDivElement>(null)
  const markupView = useRef<EditorView | null>(null)
  const sliceRef = useRef<AdvancedEventLabSlice | null>(null)
  const draftPersistenceKeyRef = useRef<string | null>(null)
  const labUndoAnonIdRef = useRef<string | null>(null)
  const labCheckpointsRef = useRef<AdvancedEventLabSlice[]>([])
  const [undoUiTick, setUndoUiTick] = useState(0)
  const labPersistTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
  const previewDebounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
  const schedulePreviewUpdateRef = useRef<(text: string) => void>(() => {})
  /** When true, closing is from Apply (draft already cleared); skip discard cleanup. */
  const skipClearLabDraftOnCloseRef = useRef(false)
  /** Debounce writes to the draft map; pagehide/beforeunload flush immediately to disk. */
  const LAB_DRAFT_DEBOUNCE_MS = 500

  const [previewDoc, setPreviewDoc] = useState('')

  /** Stable while payload matches; avoids remounting the editor when the parent passes a new `initial` object reference. */
  const labEditorMountFingerprint =
    initial != null ? `${initial.kind}\0${initial.content}\0${JSON.stringify(initial.tags)}` : ''

  const mergedLabPreviewEmojiTags = useMemo(() => {
    if (!open || !initial) return []
    const fromInitial = initial.tags.filter(([n]) => n === 'emoji').map((r) => [...r])
    const fromProp = previewEmojiTags ?? []
    const m = new Map<string, string[]>()
    for (const row of fromInitial) {
      const sc = row[1]?.trim()
      if (sc) m.set(sc.toLowerCase(), row)
    }
    for (const row of fromProp) {
      const sc = row[1]?.trim()
      if (sc) m.set(sc.toLowerCase(), row)
    }
    return [...m.values()]
  }, [open, initial, previewEmojiTags])

  const schedulePreviewUpdate = useCallback((text: string) => {
    if (previewDebounceTimerRef.current) {
      clearTimeout(previewDebounceTimerRef.current)
    }
    previewDebounceTimerRef.current = setTimeout(() => {
      previewDebounceTimerRef.current = null
      setPreviewDoc(text)
    }, PREVIEW_DEBOUNCE_MS)
  }, [])

  useEffect(() => {
    schedulePreviewUpdateRef.current = schedulePreviewUpdate
  }, [schedulePreviewUpdate])

  draftPersistenceKeyRef.current = draftPersistenceKey ?? null

  useEffect(() => {
    if (!open) labUndoAnonIdRef.current = null
  }, [open])

  const undoSessionId = useMemo(() => {
    if (!open) return ''
    if (draftPersistenceKey) return draftPersistenceKey
    if (!labUndoAnonIdRef.current) labUndoAnonIdRef.current = crypto.randomUUID()
    return labUndoAnonIdRef.current
  }, [open, draftPersistenceKey])

  const bumpUndoUi = useCallback(() => {
    setUndoUiTick((n) => n + 1)
  }, [])

  /** Writes the live CodeMirror doc into the draft cache. `urgent` flushes localStorage synchronously (tab hide / unload only). */
  const flushLabDraftNow = useCallback((key: string, urgent = false) => {
    const v = markupView.current
    const s = sliceRef.current
    if (!v || !s) return
    if (labPersistTimerRef.current) {
      clearTimeout(labPersistTimerRef.current)
      labPersistTimerRef.current = null
    }
    postEditorCache.setAdvancedLabDraft(key, {
      kind: s.kind,
      content: v.state.doc.toString(),
      tags: s.tags.map((row) => [...row])
    })
    if (urgent) postEditorCache.flushPersist()
  }, [])

  useEffect(() => {
    if (!open || !draftPersistenceKey) return
    const key = draftPersistenceKey
    const onPageLeave = () => {
      flushLabDraftNow(key, true)
    }
    window.addEventListener('pagehide', onPageLeave)
    window.addEventListener('beforeunload', onPageLeave)
    const onVisibility = () => {
      if (document.visibilityState === 'hidden') onPageLeave()
    }
    document.addEventListener('visibilitychange', onVisibility)
    return () => {
      window.removeEventListener('pagehide', onPageLeave)
      window.removeEventListener('beforeunload', onPageLeave)
      document.removeEventListener('visibilitychange', onVisibility)
    }
  }, [open, draftPersistenceKey, flushLabDraftNow])

  useEffect(() => {
    if (!open) {
      if (previewDebounceTimerRef.current) {
        clearTimeout(previewDebounceTimerRef.current)
        previewDebounceTimerRef.current = null
      }
      setPreviewDoc('')
    }
  }, [open])

  const handleDialogOpenChange = useCallback(
    (next: boolean) => {
      if (!next) {
        if (!skipClearLabDraftOnCloseRef.current) {
          if (labPersistTimerRef.current) {
            clearTimeout(labPersistTimerRef.current)
            labPersistTimerRef.current = null
          }
          const key = draftPersistenceKeyRef.current
          if (key) {
            postEditorCache.clearAdvancedLabDraft(key)
          }
        }
        skipClearLabDraftOnCloseRef.current = false
      }
      onOpenChange(next)
    },
    [onOpenChange]
  )

  const scheduleLabDraftPersist = useCallback(() => {
    const key = draftPersistenceKeyRef.current
    if (!key) return
    if (labPersistTimerRef.current) {
      clearTimeout(labPersistTimerRef.current)
    }
    labPersistTimerRef.current = setTimeout(() => {
      labPersistTimerRef.current = null
      const v = markupView.current
      const s = sliceRef.current
      if (!s) return
      const content = v?.state.doc.toString() ?? s.content
      postEditorCache.setAdvancedLabDraft(key, {
        kind: s.kind,
        content,
        tags: s.tags.map((row) => [...row])
      })
    }, LAB_DRAFT_DEBOUNCE_MS)
  }, [])

  const restoreSliceInEditor = useCallback(
    (slice: AdvancedEventLabSlice) => {
      const v = markupView.current
      if (!v) return
      v.dispatch({
        changes: { from: 0, to: v.state.doc.length, insert: slice.content },
        selection: EditorSelection.cursor(0)
      })
      sliceRef.current = cloneLabSlice(slice)
      setPreviewDoc(slice.content)
      scheduleLabDraftPersist()
      if (isLanguageToolConfigured()) requestAdvancedLabGrammarLint(v)
      bumpUndoUi()
    },
    [bumpUndoUi, scheduleLabDraftPersist]
  )

  const pushLabCheckpoint = useCallback(() => {
    const v = markupView.current
    const s = sliceRef.current
    if (!v || !s || !undoSessionId) return
    const snap = cloneLabSlice({
      kind: s.kind,
      content: v.state.doc.toString(),
      tags: s.tags.map((row) => [...row])
    })
    const cp = labCheckpointsRef.current
    const last = cp[cp.length - 1]
    if (last && labSlicesEqual(last, snap)) return
    cp.push(snap)
    while (cp.length > LAB_UNDO_MAX_CHECKPOINTS) cp.shift()
    persistLabCheckpointsToSession(undoSessionId, cp)
    bumpUndoUi()
  }, [undoSessionId, bumpUndoUi])

  const handleUndoCheckpoint = useCallback(() => {
    const v = markupView.current
    const s = sliceRef.current
    if (!v || !s || !undoSessionId) return
    const cp = labCheckpointsRef.current
    if (cp.length === 0) {
      toast.message(t('Advanced lab undo checkpoint none'))
      return
    }
    const live = cloneLabSlice({
      kind: s.kind,
      content: v.state.doc.toString(),
      tags: s.tags.map((row) => [...row])
    })
    const last = cp[cp.length - 1]!
    if (!labSlicesEqual(live, last)) {
      restoreSliceInEditor(last)
      persistLabCheckpointsToSession(undoSessionId, cp)
      toast.success(t('Advanced lab undo checkpoint restored'))
      return
    }
    if (cp.length < 2) {
      toast.message(t('Advanced lab undo checkpoint none'))
      return
    }
    cp.pop()
    const target = cp[cp.length - 1]!
    restoreSliceInEditor(target)
    persistLabCheckpointsToSession(undoSessionId, cp)
    toast.success(t('Advanced lab undo checkpoint restored'))
  }, [undoSessionId, restoreSliceInEditor, t])

  const canUndoCheckpoint = useMemo(() => {
    if (!open) return false
    const v = markupView.current
    const s = sliceRef.current
    const cp = labCheckpointsRef.current
    if (!v || !s || cp.length === 0) return false
    const live: AdvancedEventLabSlice = {
      kind: s.kind,
      content: v.state.doc.toString(),
      tags: s.tags.map((row) => [...row])
    }
    const last = cp[cp.length - 1]!
    if (!labSlicesEqual(live, last)) return true
    return cp.length >= 2
  }, [undoUiTick, open, previewDoc])

  useEffect(() => {
    if (!open || !initial) return
    const pushId = window.setInterval(() => {
      pushLabCheckpoint()
    }, LAB_UNDO_INTERVAL_MS)
    const uiId = window.setInterval(() => {
      bumpUndoUi()
    }, 2500)
    return () => {
      clearInterval(pushId)
      clearInterval(uiId)
    }
  }, [open, labEditorMountFingerprint, pushLabCheckpoint, bumpUndoUi])

  const [translateLangs, setTranslateLangs] = useState<TranslateLanguageOption[]>([])
  const ltList = useMemo(
    () => buildLabLanguageToolPreferenceList(i18nLanguage ?? i18n.language, translateLangs),
    [i18nLanguage, i18n.language, translateLangs]
  )
  const [ltLang, setLtLang] = useState(() => ltList[0] ?? 'en-US')
  const ltLangRef = useRef(ltLang)
  ltLangRef.current = ltLang
  const [translateLoad, setTranslateLoad] = useState<'idle' | 'loading' | 'ready' | 'empty' | 'error'>('idle')
  const [translateSource, setTranslateSource] = useState('auto')
  const [translateTarget, setTranslateTarget] = useState('en')
  const [ltLangFilter, setLtLangFilter] = useState('')
  const [translateSrcFilter, setTranslateSrcFilter] = useState('')
  const [translateTgtFilter, setTranslateTgtFilter] = useState('')

  const ltListFiltered = useMemo(() => {
    const f = ltList.filter((code) => translateLanguageOptionMatchesQuery(code, ltLangFilter))
    return f.length > 0 ? f : ltList
  }, [ltList, ltLangFilter])
  const translateLangsFilteredSrc = useMemo(() => {
    const f = translateLangs.filter((l) => translateLanguageOptionMatchesQuery(l.code, translateSrcFilter))
    return f.length > 0 ? f : translateLangs
  }, [translateLangs, translateSrcFilter])
  const translateLangsFilteredTgt = useMemo(() => {
    const f = translateLangs.filter((l) => translateLanguageOptionMatchesQuery(l.code, translateTgtFilter))
    return f.length > 0 ? f : translateLangs
  }, [translateLangs, translateTgtFilter])
  const showTranslateSourceAuto = useMemo(() => {
    const q = translateSrcFilter.trim().toLowerCase()
    if (!q) return true
    if (translateSource === 'auto') return true
    const autoLabel = t('Advanced lab translation source auto').toLowerCase()
    return q.includes('auto') || q.includes('detect') || autoLabel.includes(q)
  }, [translateSrcFilter, translateSource, t])

  useEffect(() => {
    if (!open) {
      setLtLangFilter('')
      setTranslateSrcFilter('')
      setTranslateTgtFilter('')
    }
  }, [open])

  useEffect(() => {
    if (!open) return
    setLtLang((prev) => (ltList.includes(prev) ? prev : ltList[0] ?? 'en-US'))
  }, [open, ltList])

  useEffect(() => {
    const v = markupView.current
    if (!v || !open || !isLanguageToolConfigured()) return
    requestAdvancedLabGrammarLint(v)
  }, [ltLang, open])

  useEffect(() => {
    if (!open || !isTranslateConfigured()) {
      setTranslateLangs([])
      setTranslateLoad('idle')
      return
    }
    let cancelled = false
    setTranslateLoad('loading')
    void fetchTranslateLanguages()
      .then((list) => {
        if (cancelled) return
        const resolved = buildResolvedTranslateMenuLanguageOptions(list)
        if (!resolved.length) {
          setTranslateLangs([])
          setTranslateLoad('empty')
          return
        }
        setTranslateLangs([...resolved])
        setTranslateSource('auto')
        const codes = resolved.map((l: TranslateLanguageOption) => l.code)
        const tgt = codes.includes('en') ? 'en' : codes[0]!
        setTranslateTarget(tgt)
        setTranslateLoad('ready')
      })
      .catch(() => {
        if (cancelled) return
        const resolved = buildResolvedTranslateMenuLanguageOptions([])
        setTranslateLangs([...resolved])
        setTranslateSource('auto')
        const codes = resolved.map((l: TranslateLanguageOption) => l.code)
        setTranslateTarget(codes.includes('en') ? 'en' : codes[0]!)
        setTranslateLoad('ready')
      })
    return () => {
      cancelled = true
    }
  }, [open])

  const destroyEditors = useCallback(() => {
    if (bodyApiRef) bodyApiRef.current = null
    markupView.current?.destroy()
    markupView.current = null
  }, [bodyApiRef])

  useLayoutEffect(() => {
    if (!open || !initial) {
      destroyEditors()
      return
    }

    let cancelled = false
    let rafId = 0
    let attempts = 0
    const MAX_RAF_ATTEMPTS = 120

    const mountEditors = () => {
      if (cancelled) return
      const mkEl = markupHost.current
      if (!mkEl) {
        attempts += 1
        if (attempts < MAX_RAF_ATTEMPTS) {
          rafId = requestAnimationFrame(mountEditors)
        }
        return
      }

      destroyEditors()

      const baseSlice: AdvancedEventLabSlice = {
        kind: initial.kind,
        content: initial.content,
        tags: initial.tags.map((row) => [...row])
      }
      sliceRef.current = baseSlice
      setPreviewDoc(baseSlice.content)

      const markupLang: Extension =
        markupMode === 'asciidoc' ? StreamLanguage.define(asciidoc) : markdown()

      const mkExtensions: Extension[] = [
        history(),
        keymap.of([...defaultKeymap, ...historyKeymap]),
        lineNumbers(),
        cmPlaceholder(
          labTRef.current(
            markupMode === 'asciidoc'
              ? 'Advanced lab markup placeholder asciidoc'
              : 'Advanced lab markup placeholder markdown'
          )
        ),
        markupLang,
        EditorView.theme({
          '&': { height: '100%', maxHeight: '100%', minHeight: 0 },
          '.cm-scroller': { overflow: 'auto', minHeight: 0 },
          // Large dvh mins fight stacked flex/grid rows and overflow onto the preview; host + row cap height instead.
          '.cm-content': {
            minHeight: '11rem',
            fontFamily: 'var(--font-mono, ui-monospace, monospace)'
          }
        }),
        EditorView.updateListener.of((update) => {
          if (!update.docChanged) return
          const content = update.state.doc.toString()
          const s = sliceRef.current
          if (!s) return
          s.content = content
          schedulePreviewUpdateRef.current(content)
          scheduleLabDraftPersist()
        })
      ]
      if (isLanguageToolConfigured()) {
        mkExtensions.push(
          languageToolLintExtension(() => ltLangRef.current, 650, () => markupMode)
        )
      }
      if (dark) mkExtensions.push(oneDark)

      const mkState = EditorState.create({
        doc: baseSlice.content,
        extensions: mkExtensions
      })

      markupView.current = new EditorView({ state: mkState, parent: mkEl })

      const loaded =
        undoSessionId && undoSessionId.length > 0
          ? loadLabCheckpointsFromSession(undoSessionId, baseSlice)
          : null
      labCheckpointsRef.current =
        loaded && loaded.length > 0 ? loaded : [cloneLabSlice(baseSlice)]
      if (undoSessionId) {
        persistLabCheckpointsToSession(undoSessionId, labCheckpointsRef.current)
      }
      bumpUndoUi()

      if (bodyApiRef) {
        bodyApiRef.current = {
          getText: () => markupView.current?.state.doc.toString() ?? '',
          insertText: (text: string) => {
            const v = markupView.current
            if (!v) return
            cmInsertAtSelection(v, text)
            const s = sliceRef.current
            if (s) s.content = v.state.doc.toString()
          },
          appendText: (raw: string, addNewline = false) => {
            const v = markupView.current
            if (!v) return
            cmAppendAtEnd(v, raw, addNewline)
            const s = sliceRef.current
            if (s) s.content = v.state.doc.toString()
          },
          insertEmoji: (emoji: string | TEmoji) => {
            const v = markupView.current
            if (!v) return
            let piece: string
            if (typeof emoji === 'string') {
              piece = emoji
            } else {
              const sc = emoji.shortcode?.trim()
              if (sc) {
                piece = `:${sc}: `
              } else {
                const id = customEmojiService.getEmojiId(emoji)
                const ce = customEmojiService.getEmojiById(id)
                piece = ce ? `:${ce.shortcode}: ` : `:${id}: `
              }
            }
            cmInsertAtSelection(v, piece)
            const s = sliceRef.current
            if (s) s.content = v.state.doc.toString()
          }
        }
      }
    }

    mountEditors()

    return () => {
      cancelled = true
      cancelAnimationFrame(rafId)
      if (previewDebounceTimerRef.current) {
        clearTimeout(previewDebounceTimerRef.current)
        previewDebounceTimerRef.current = null
      }
      if (labPersistTimerRef.current) {
        clearTimeout(labPersistTimerRef.current)
        labPersistTimerRef.current = null
      }
      const key = draftPersistenceKeyRef.current
      if (key) {
        flushLabDraftNow(key)
      }
      destroyEditors()
    }
  }, [
    open,
    labEditorMountFingerprint,
    markupMode,
    dark,
    destroyEditors,
    bodyApiRef,
    scheduleLabDraftPersist,
    flushLabDraftNow,
    undoSessionId,
    bumpUndoUi
  ])

  const handleApply = () => {
    const s = sliceRef.current
    if (!s) {
      toast.error(t('Advanced lab applyError'))
      return
    }
    const content = markupView.current?.state.doc.toString() ?? s.content
    const kind = kindEditable ? s.kind : (initial?.kind ?? s.kind)
    const payload: AdvancedEventLabSlice = {
      kind,
      content,
      tags: s.tags.map((row) => [...row])
    }
    skipClearLabDraftOnCloseRef.current = true
    onApply(payload)
    if (draftPersistenceKeyRef.current) {
      postEditorCache.clearAdvancedLabDraft(draftPersistenceKeyRef.current)
    }
    if (undoSessionId) {
      clearLabCheckpointsSession(undoSessionId)
      labCheckpointsRef.current = []
    }
    handleDialogOpenChange(false)
  }

  const handleTranslate = async () => {
    if (!isTranslateConfigured()) {
      toast.message(t('Advanced lab translate not configured'))
      return
    }
    const text = markupView.current?.state.doc.toString() ?? sliceRef.current?.content ?? ''
    if (!text.trim()) return
    if (translateLoad !== 'ready' || translateLangs.length === 0) return
    if (
      translateSource !== 'auto' &&
      translateApiLanguageCode(translateSource) === translateApiLanguageCode(translateTarget)
    ) {
      toast.message(t('Advanced lab translation same source target'))
      return
    }
    logger.info('[AdvancedLab] translate button', {
      source: translateSource,
      target: translateTarget,
      inputChars: text.length
    })
    try {
      const out = await translateAdvancedLabMarkup(text, translateTarget, translateSource, markupMode)
      if (!markupView.current) return
      markupView.current.dispatch({
        changes: { from: 0, to: markupView.current.state.doc.length, insert: out }
      })
      const s = sliceRef.current
      if (s) s.content = out
      if (isLanguageToolConfigured()) {
        const nextLt = pickLanguageToolCodeForTranslateTarget(translateTarget, ltList)
        if (nextLt !== ltLang) {
          logger.info('[AdvancedLab] grammar language synced after translate', {
            from: ltLang,
            to: nextLt,
            translateTarget
          })
          setLtLang(nextLt)
        }
      }
      toast.success(t('Advanced lab translate done'))
    } catch (e) {
      toast.error(e instanceof Error ? e.message : String(e))
    }
  }

  const handleReadAloudBuffer = () => {
    if (!contextEventId) return
    const text = markupView.current?.state.doc.toString().trim() ?? ''
    if (!text) return
    setReadAloudTranslationForEvent(contextEventId, text)
    toast.success(t('Advanced lab read aloud buffer set'))
  }

  return (
    <Dialog open={open} onOpenChange={handleDialogOpenChange}>
      <DialogContent
        overlayClassName="z-[205]"
        className={cnDialogShell()}
        aria-describedby={undefined}
      >
        <DialogHeader className="shrink-0 px-4 pt-4 pb-2 pr-12 border-b">
          <DialogTitle>{t('Advanced event lab')}</DialogTitle>
        </DialogHeader>

        <div className="flex flex-col gap-2 px-4 py-2 border-b shrink-0 flex-wrap">
          <div className="flex flex-wrap items-end gap-3">
            {isLanguageToolConfigured() ? (
              <div className="space-y-1 min-w-[10rem]">
                <Label htmlFor="lt-lang">{t('Advanced lab grammar language')}</Label>
                <Select
                  value={ltLang}
                  onValueChange={(code) => {
                    logger.info('[AdvancedLab] grammar language changed', { from: ltLang, to: code })
                    setLtLang(code)
                  }}
                >
                  <SelectTrigger id="lt-lang" className="min-w-[220px] max-w-md w-auto">
                    <SelectValue />
                  </SelectTrigger>
                  <SelectContent
                    className="max-h-64 min-w-[var(--radix-select-trigger-width)] p-0"
                    onCloseAutoFocus={(e) => e.preventDefault()}
                  >
                    <div
                      className="sticky top-0 z-10 border-b border-border bg-popover p-2"
                      onPointerDown={(e) => e.stopPropagation()}
                    >
                      <Input
                        type="search"
                        value={ltLangFilter}
                        onChange={(e) => setLtLangFilter(e.target.value)}
                        placeholder={t('Language list filter placeholder')}
                        className="h-8"
                        aria-label={t('Language list filter placeholder')}
                      />
                    </div>
                    <div className="py-1">
                      {ltListFiltered.map((code) => (
                        <SelectItem
                          key={code}
                          value={code}
                          className="items-start py-2.5 whitespace-normal"
                        >
                          <LanguageSelectOptionLines tag={code} className="w-full" />
                        </SelectItem>
                      ))}
                    </div>
                  </SelectContent>
                </Select>
              </div>
            ) : null}
            {isTranslateConfigured() ? (
              <div className="flex flex-col gap-2 min-w-0">
                {translateLoad === 'idle' || translateLoad === 'loading' ? (
                  <p className="text-xs text-muted-foreground">{t('Advanced lab translation languages loading')}</p>
                ) : null}
                {translateLoad === 'ready' ? (
                  <div className="flex flex-wrap items-end gap-3">
                    <div className="space-y-1 min-w-[10rem]">
                      <Label htmlFor="tr-src">{t('Advanced lab translation source')}</Label>
                      <Select
                        value={translateSource}
                        onValueChange={(v) => {
                          logger.info('[AdvancedLab] translation source language changed', {
                            from: translateSource,
                            to: v
                          })
                          setTranslateSource(v)
                          if (v !== 'auto' && v === translateTarget) {
                            const alt = translateLangs.find((l) => l.code !== v)?.code
                            if (alt) setTranslateTarget(alt)
                          }
                        }}
                      >
                        <SelectTrigger id="tr-src" className="min-w-[220px] max-w-md w-auto">
                          <SelectValue />
                        </SelectTrigger>
                        <SelectContent
                          className="max-h-64 min-w-[var(--radix-select-trigger-width)] p-0"
                          onCloseAutoFocus={(e) => e.preventDefault()}
                        >
                          <div
                            className="sticky top-0 z-10 border-b border-border bg-popover p-2"
                            onPointerDown={(e) => e.stopPropagation()}
                          >
                            <Input
                              type="search"
                              value={translateSrcFilter}
                              onChange={(e) => setTranslateSrcFilter(e.target.value)}
                              placeholder={t('Language list filter placeholder')}
                              className="h-8"
                              aria-label={t('Language list filter placeholder')}
                            />
                          </div>
                          <div className="py-1">
                            {showTranslateSourceAuto ? (
                              <SelectItem value="auto">{t('Advanced lab translation source auto')}</SelectItem>
                            ) : null}
                            {translateLangsFilteredSrc.map((l) => (
                              <SelectItem
                                key={l.code}
                                value={l.code}
                                className="items-start py-2.5 whitespace-normal"
                              >
                                <LanguageSelectOptionLines tag={l.code} className="w-full" />
                              </SelectItem>
                            ))}
                          </div>
                        </SelectContent>
                      </Select>
                    </div>
                    <div className="space-y-1 min-w-[10rem]">
                      <Label htmlFor="tr-tgt">{t('Advanced lab translation target')}</Label>
                      <Select
                        value={translateTarget}
                        onValueChange={(v) => {
                          logger.info('[AdvancedLab] translation target language changed', {
                            from: translateTarget,
                            to: v
                          })
                          setTranslateTarget(v)
                          if (translateSource !== 'auto' && v === translateSource) {
                            setTranslateSource('auto')
                          }
                        }}
                      >
                        <SelectTrigger id="tr-tgt" className="min-w-[220px] max-w-md w-auto">
                          <SelectValue />
                        </SelectTrigger>
                        <SelectContent
                          className="max-h-64 min-w-[var(--radix-select-trigger-width)] p-0"
                          onCloseAutoFocus={(e) => e.preventDefault()}
                        >
                          <div
                            className="sticky top-0 z-10 border-b border-border bg-popover p-2"
                            onPointerDown={(e) => e.stopPropagation()}
                          >
                            <Input
                              type="search"
                              value={translateTgtFilter}
                              onChange={(e) => setTranslateTgtFilter(e.target.value)}
                              placeholder={t('Language list filter placeholder')}
                              className="h-8"
                              aria-label={t('Language list filter placeholder')}
                            />
                          </div>
                          <div className="py-1">
                            {translateLangsFilteredTgt.map((l) => (
                              <SelectItem
                                key={l.code}
                                value={l.code}
                                className="items-start py-2.5 whitespace-normal"
                              >
                                <LanguageSelectOptionLines tag={l.code} className="w-full" />
                              </SelectItem>
                            ))}
                          </div>
                        </SelectContent>
                      </Select>
                    </div>
                    <Button type="button" variant="secondary" size="sm" onClick={() => void handleTranslate()}>
                      {t('Advanced lab translate')}
                    </Button>
                  </div>
                ) : null}
                {translateLoad === 'empty' ? (
                  <p className="text-xs text-destructive">{t('Advanced lab translation languages empty')}</p>
                ) : null}
                {translateLoad === 'error' ? (
                  <p className="text-xs text-destructive">{t('Advanced lab translation languages error')}</p>
                ) : null}
              </div>
            ) : null}
            {contextEventId && isTranslateConfigured() ? (
              <Button type="button" variant="outline" size="sm" onClick={handleReadAloudBuffer}>
                {t('Advanced lab use translation read aloud')}
              </Button>
            ) : null}
            <Button
              type="button"
              variant="outline"
              size="sm"
              disabled={!canUndoCheckpoint}
              title={t('Advanced lab undo checkpoint hint')}
              onClick={handleUndoCheckpoint}
            >
              <Undo2 className="h-4 w-4 mr-1 inline" />
              {t('Advanced lab undo checkpoint')}
            </Button>
          </div>
        </div>

        <div className="flex min-h-0 flex-1 flex-col overflow-y-auto overscroll-y-contain">
          <AdvancedEventLabMarkupToolbar markupMode={markupMode} viewRef={markupView} sliceRef={sliceRef} />

          <div className="flex min-h-0 flex-1 flex-col gap-0 px-4 py-2 max-lg:grid max-lg:grid-rows-[minmax(0,1fr)_minmax(0,1fr)] lg:flex lg:flex-row lg:py-2">
            <div className="flex min-h-0 min-w-0 flex-col gap-2 overflow-hidden max-lg:min-h-0 lg:flex-1 lg:pr-3">
              <h3 className="shrink-0 text-left text-sm font-semibold leading-none text-foreground">
                {t(
                  markupMode === 'asciidoc'
                    ? 'Advanced lab markup label asciidoc'
                    : 'Advanced lab markup label markdown'
                )}
              </h3>
              <div
                ref={markupHost}
                className="flex min-h-0 flex-1 flex-col overflow-hidden rounded-md border bg-muted/20 lg:min-h-[min(42dvh,24rem)]"
              />
            </div>
            <div className="flex min-h-0 min-w-0 flex-col gap-2 overflow-hidden -mx-4 border-t-2 border-border bg-muted/40 px-4 pb-3 pt-4 max-lg:min-h-0 max-lg:rounded-b-lg lg:mx-0 lg:mt-0 lg:flex-[0_1_42%] lg:max-w-[min(50%,40rem)] lg:rounded-none lg:border-t-0 lg:border-l lg:border-border lg:bg-transparent lg:px-0 lg:pb-0 lg:pt-0 lg:pl-3">
              <h3 className="shrink-0 text-left text-sm font-semibold leading-none text-foreground">
                {t('Advanced lab preview')}
              </h3>
              <div className="flex min-h-0 flex-1 overflow-y-auto rounded-md border border-border bg-background py-2 pl-0 pr-0 text-left lg:bg-muted/10 lg:px-2">
                <AdvancedEventLabPreviewPane
                  markupMode={markupMode}
                  source={previewDoc}
                  previewAuthorPubkey={previewAuthorPubkey}
                  previewEmojiTags={mergedLabPreviewEmojiTags}
                />
              </div>
            </div>
          </div>
        </div>

        {formatToolbar ? (
          <div className="shrink-0 border-t bg-muted/20 px-2 py-2">{formatToolbar}</div>
        ) : null}

        <DialogFooter className="shrink-0 px-4 py-3 border-t gap-2">
          <Button type="button" variant="outline" onClick={() => handleDialogOpenChange(false)}>
            {t('Advanced lab cancel undo')}
          </Button>
          <Button type="button" onClick={handleApply}>
            {t('Apply')}
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  )
}

/** Responsive shell: ~5× prior max width cap and ~3× vertical use of viewport (still clamped). */
function cnDialogShell(): string {
  return [
    'z-[250] max-w-none flex min-h-0 flex-col gap-0 overflow-hidden p-0',
    'w-[min(98vw,calc(72rem*5))]',
    'h-[min(94vh,calc(28rem*3))]',
    'max-h-[min(96vh,90dvh)]'
  ].join(' ')
}
