import { useState, useEffect, useCallback, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { IconX, IconSparklesFilled, IconWorldFilled, IconTrashFilled,
         IconArrowBack, IconPlus, IconPencilFilled, IconPointerFilled, IconAlertTriangleFilled,
         IconPlayerPlayFilled, IconListFilled, IconDownloadFilled, IconFileUploadFilled,
         IconChartAreaFilled } from '@tabler/icons-react'
import * as api from '../api'
import type { ConstructorAnalytics } from '../api'
import Select from './ui/Select'
import NumberInput from './ui/NumberInput'
import Tooltip from './ui/Tooltip'

type CellType = 'black' | 'white'
type Symmetry = 'rotational180' | 'mirror' | 'none'
type EditorMode = 'design' | 'fill' | 'clues' | 'test'

const SIZE_BUTTONS: [number, string][] = [[5, '5x5'], [9, '9x9'], [11, '11x11'], [15, '15x15'], [21, '21x21']]

type EmptySlotResult = { cellNumbers: Record<string, number>; twoLetterWarnings: string[] }

const EMPTY_SLOT_RESULT: EmptySlotResult = {
  cellNumbers: {},
  twoLetterWarnings: [],
}
type Dir = 'across' | 'down'

interface EditorClue {
  number: number
  direction: string
  text: string
  answer: string
  startRow: number
  startCol: number
}

interface SavedPuzzle {
  id: string; title: string; width: number; height: number; status: string; updated_at: string
}

// compute word slots from grid cells
function computeSlots(cells: CellType[][], letters: (string | null)[][], width: number, height: number) {
  // first compute numbers (3+ letter runs only)
  const cellNumbers: Record<string, number> = {}
  const twoLetterWarnings: string[] = []
  let num = 1

  const runLen = (r: number, c: number, dr: number, dc: number) => {
    let len = 0
    while (r >= 0 && r < height && c >= 0 && c < width && cells[r]?.[c] === 'white') {
      len++; r += dr; c += dc
    }
    return len
  }

  for (let r = 0; r < height; r++) {
    for (let c = 0; c < width; c++) {
      if (cells[r]?.[c] !== 'white') continue
      const acrossStart = c === 0 || cells[r][c - 1] === 'black'
      const downStart = r === 0 || cells[r - 1]?.[c] === 'black'
      const acrossLen = acrossStart ? runLen(r, c, 0, 1) : 0
      const downLen = downStart ? runLen(r, c, 1, 0) : 0
      if (acrossLen === 2) twoLetterWarnings.push(`row ${r + 1}, col ${c + 1} across`)
      if (downLen === 2) twoLetterWarnings.push(`row ${r + 1}, col ${c + 1} down`)
      if (acrossLen >= 3 || downLen >= 3) cellNumbers[`${r},${c}`] = num++
    }
  }

  // extract words
  const slots: EditorClue[] = []
  // across
  for (let r = 0; r < height; r++) {
    let word = '', startC = 0
    for (let c = 0; c <= width; c++) {
      if (c < width && cells[r][c] === 'white') {
        if (word.length === 0) startC = c
        word += letters[r]?.[c] || '?'
      } else {
        if (word.length >= 3) {
          const n = cellNumbers[`${r},${startC}`]
          if (n) slots.push({ number: n, direction: 'across', answer: word.replace(/\?/g, ''), text: '', startRow: r, startCol: startC })
        }
        word = ''
      }
    }
  }
  // down
  for (let c = 0; c < width; c++) {
    let word = '', startR = 0
    for (let r = 0; r <= height; r++) {
      if (r < height && cells[r][c] === 'white') {
        if (word.length === 0) startR = r
        word += letters[r]?.[c] || '?'
      } else {
        if (word.length >= 3) {
          const n = cellNumbers[`${startR},${c}`]
          if (n) slots.push({ number: n, direction: 'down', answer: word.replace(/\?/g, ''), text: '', startRow: startR, startCol: c })
        }
        word = ''
      }
    }
  }

  return { cellNumbers, slots, twoLetterWarnings }
}

// get all cells belonging to a word
function getWordCells(cells: CellType[][], r: number, c: number, dir: Dir, width: number, height: number): [number, number][] {
  if (!cells[r]) return []
  if (dir === 'across') {
    while (c > 0 && cells[r]?.[c - 1] === 'white') c--
    const result: [number, number][] = []
    while (c < width && cells[r]?.[c] === 'white') { result.push([r, c]); c++ }
    return result
  } else {
    while (r > 0 && cells[r - 1]?.[c] === 'white') r--
    const result: [number, number][] = []
    while (r < height && cells[r]?.[c] === 'white') { result.push([r, c]); r++ }
    return result
  }
}

export default function EditorPanel({ onClose }: { onClose: () => void }) {
  const { t } = useTranslation()
  const [view, setView] = useState<'list' | 'edit'>('list')
  const [puzzleId, setPuzzleId] = useState<string | null>(null)
  const [title, setTitle] = useState('Untitled')
  const [width, setWidth] = useState(15)
  const [height, setHeight] = useState(15)
  const [cells, setCells] = useState<CellType[][]>([])
  const [letters, setLetters] = useState<(string | null)[][]>([])
  const [clues, setClues] = useState<EditorClue[]>([])
  const [symmetry, setSymmetry] = useState<Symmetry>('rotational180')
  const [mode, setMode] = useState<EditorMode>('design')
  const [selectedCell, setSelectedCell] = useState<{ row: number; col: number } | null>(null)
  const [editorDirection, setEditorDirection] = useState<Dir>('across')
  const [activeClueKey, setActiveClueKey] = useState<string | null>(null)
  const [highlightedCells, setHighlightedCells] = useState<Set<string>>(new Set())
  const [filling, setFilling] = useState(false)
  const [saving, setSaving] = useState(false)
  const [savedPuzzles, setSavedPuzzles] = useState<SavedPuzzle[]>([])
  const [editingClueIdx, setEditingClueIdx] = useState<number | null>(null)
  const [suggestions, setSuggestions] = useState<Record<string, string[]>>({})
  const [warnings, setWarnings] = useState<string[]>([])
  const [undoStack, setUndoStack] = useState<{ cells: CellType[][]; letters: (string | null)[][]; clues: EditorClue[] }[]>([])
  const [lastSaved, setLastSaved] = useState('')
  const autosaveTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
  const [showPublishConfirm, setShowPublishConfirm] = useState(false)
  const [skipPublishConfirm, setSkipPublishConfirm] = useState(false)
  const [dontShowAgain, setDontShowAgain] = useState(false)
  const [deleteConfirmId, setDeleteConfirmId] = useState<string | null>(null)
  const [customW, setCustomW] = useState(13)
  const [customH, setCustomH] = useState(13)
  const [isThemed, setIsThemed] = useState(false)
  const [themeName, setThemeName] = useState('')
  const [analyticsId, setAnalyticsId] = useState<string | null>(null)
  const [analyticsData, setAnalyticsData] = useState<ConstructorAnalytics | null>(null)
  const [analyticsLoading, setAnalyticsLoading] = useState(false)

  // load publish confirm preference
  useEffect(() => {
    api.fetchSettings().then(s => { if (s.skipPublishConfirm) setSkipPublishConfirm(true) })
  }, [])
  const [testEntries, setTestEntries] = useState<string[][]>([])
  const [testSelected, setTestSelected] = useState<{ row: number; col: number } | null>(null)

  useEffect(() => { api.editorList().then(setSavedPuzzles) }, [])

  // push current state to undo stack
  const pushUndo = useCallback(() => {
    if (cells.length === 0) return
    setUndoStack(prev => [...prev.slice(-19), { cells: cells.map(r => [...r]), letters: letters.map(r => [...r]), clues: [...clues] }])
  }, [cells, letters, clues])

  const undo = () => {
    if (undoStack.length === 0) return
    const prev = undoStack[undoStack.length - 1]
    setUndoStack(s => s.slice(0, -1))
    setCells(prev.cells)
    setLetters(prev.letters)
    setClues(prev.clues)
  }

  // Ctrl+Z for undo
  useEffect(() => {
    const onKey = (e: KeyboardEvent) => {
      if ((e.ctrlKey || e.metaKey) && e.key === 'z' && view === 'edit') {
        if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return
        e.preventDefault()
        undo()
      }
    }
    window.addEventListener('keydown', onKey)
    return () => window.removeEventListener('keydown', onKey)
  }, [undoStack, view])

  const clearGrid = () => {
    pushUndo()
    setCells(Array.from({ length: height }, () => Array(width).fill('white')))
    setLetters(Array.from({ length: height }, () => Array(width).fill(null)))
    setClues([]); setSuggestions({})
  }

  const clearLetters = () => {
    pushUndo()
    setLetters(Array.from({ length: height }, () => Array(width).fill(null)))
    setClues([]); setSuggestions({})
  }

  const unpublish = async () => {
    if (!puzzleId) return
    try {
      await api.editorSave({ id: puzzleId, title, width, height, grid: cells, clues: clues.length > 0 ? clues : undefined, puzzle: { grid: { cells: letters.map(row => row.map(l => l ? { Letter: l } : null)) } }, status: 'draft' })
      api.editorList().then(setSavedPuzzles)
      setWarnings([t('editor.unpublished')])
    } catch { /* ignored */ }
  }

  const toggleAnalytics = async (id: string) => {
    if (analyticsId === id) { setAnalyticsId(null); return }
    setAnalyticsId(id)
    setAnalyticsLoading(true)
    try {
      const data = await api.fetchPuzzleAnalytics(id)
      setAnalyticsData(data)
    } catch (_e) { setAnalyticsData(null) }
    setAnalyticsLoading(false)
  }

  const confirmDelete = (id: string) => setDeleteConfirmId(id)
  const doDelete = async () => {
    if (!deleteConfirmId) return
    await api.editorDelete(deleteConfirmId)
    setSavedPuzzles(prev => prev.filter(p => p.id !== deleteConfirmId))
    if (puzzleId === deleteConfirmId) { setView('list'); setPuzzleId(null) }
    setDeleteConfirmId(null)
  }

  // autosave: debounce 2s after any change
  useEffect(() => {
    if (view !== 'edit' || cells.length === 0) return
    if (autosaveTimer.current) clearTimeout(autosaveTimer.current)
    autosaveTimer.current = setTimeout(async () => {
      const id = puzzleId || undefined
      try {
        const res = await api.editorSave({ id, title, width, height, grid: cells, clues: clues.length > 0 ? clues : undefined, puzzle: { grid: { cells: letters.map(row => row.map(l => l ? { Letter: l } : null)) } }, status: 'draft' })
        if (!puzzleId) setPuzzleId(res.id)
        setLastSaved(new Date().toLocaleTimeString())
      } catch { /* ignored */ }
    }, 2000)
    return () => { if (autosaveTimer.current) clearTimeout(autosaveTimer.current) }
  }, [cells, letters, clues, title, view])

  // scroll to active clue
  useEffect(() => {
    if (!activeClueKey) return
    const el = document.getElementById(`ec-${activeClueKey}`)
    if (el) el.scrollIntoView({ block: 'nearest', behavior: 'smooth' })
  }, [activeClueKey])

  // keyboard for fill/test
  useEffect(() => {
    const sel = mode === 'test' ? testSelected : mode === 'fill' ? selectedCell : null
    if (!sel) return
    const onKey = (e: KeyboardEvent) => {
      if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return
      if (e.key.length === 1 && /^[a-zA-Z]$/.test(e.key)) {
        e.preventDefault()
        if (mode === 'fill') {
          pushUndo()
          const next = letters.map(row => [...row])
          next[sel.row][sel.col] = e.key.toUpperCase()
          setLetters(next)
          advanceCellInDirection(sel, editorDirection, setSelectedCell)
        } else if (mode === 'test') {
          const next = testEntries.map(row => [...row])
          next[sel.row][sel.col] = e.key.toUpperCase()
          setTestEntries(next)
          advanceCell(sel, setTestSelected)
        }
      } else if (e.key === 'Backspace') {
        e.preventDefault()
        if (mode === 'fill') {
          pushUndo()
          const next = letters.map(row => [...row])
          next[sel.row][sel.col] = null
          setLetters(next)
        } else if (mode === 'test') {
          const next = testEntries.map(row => [...row])
          next[sel.row][sel.col] = ''
          setTestEntries(next)
        }
      }
    }
    window.addEventListener('keydown', onKey)
    return () => window.removeEventListener('keydown', onKey)
  }, [mode, selectedCell, testSelected, letters, testEntries, cells, width, height])

  const advanceCellInDirection = (from: { row: number; col: number }, dir: Dir, setter: (v: { row: number; col: number }) => void) => {
    let { row, col } = from
    if (dir === 'across') {
      col++
      while (col < width && cells[row]?.[col] === 'white') { setter({ row, col }); return }
    } else {
      row++
      while (row < height && cells[row]?.[col] === 'white') { setter({ row, col }); return }
    }
  }

  const advanceCell = (from: { row: number; col: number }, setter: (v: { row: number; col: number }) => void) => {
    let { row, col } = from; col++
    while (row < height) { while (col < width) { if (cells[row]?.[col] === 'white') { setter({ row, col }); return }; col++ }; col = 0; row++ }
  }

  const initGrid = useCallback((w: number, h: number) => {
    setCells(Array.from({ length: h }, () => Array(w).fill('white')))
    setLetters(Array.from({ length: h }, () => Array(w).fill(null)))
    setClues([]); setWarnings([]); setSelectedCell(null); setSuggestions({}); setHighlightedCells(new Set())
  }, [])

  const newPuzzle = (w: number, h: number) => { setWidth(w); setHeight(h); setPuzzleId(null); setTitle('Untitled'); setMode('design'); initGrid(w, h); setView('edit') }

  const loadPuzzle = async (id: string) => {
    const data = await api.editorLoad(id)
    setPuzzleId(data.id); setTitle(data.title); setWidth(data.width); setHeight(data.height)
    const grid = data.grid || []
    const cellBlack = (r: number, c: number) => grid[r]?.[c] === 'black'
    setCells(Array.from({ length: data.height }, (_, r) => Array.from({ length: data.width }, (_, c) => cellBlack(r, c) ? 'black' : 'white')))
    // letters come from the stored puzzle payload when present; otherwise
    // rebuild them from the clue answers so the fill survives a reload
    const rebuilt = Array.from({ length: data.height }, () => Array<string | null>(data.width).fill(null))
    const p = data.puzzle
    if (p?.grid?.cells) {
      for (let r = 0; r < data.height; r++) {
        for (let c = 0; c < data.width; c++) {
          const cell = p.grid.cells[r]?.[c]
          if (cell?.Letter) rebuilt[r][c] = cell.Letter
        }
      }
    } else {
      for (const clue of data.clues || []) {
        const answer = clue.answer || ''
        if (!answer) continue
        const sr = clue.startRow ?? 0
        const sc = clue.startCol ?? 0
        if (clue.direction === 'across') {
          for (let i = 0; i < answer.length && sc + i < data.width; i++) {
            if (cellBlack(sr, sc + i)) break
            rebuilt[sr][sc + i] = answer[i]
          }
        } else {
          for (let i = 0; i < answer.length && sr + i < data.height; i++) {
            if (cellBlack(sr + i, sc)) break
            rebuilt[sr + i][sc] = answer[i]
          }
        }
      }
    }
    setLetters(rebuilt)
    setClues((data.clues || []).map(c => ({ number: c.number, direction: c.direction, text: c.text, answer: c.answer, startRow: c.startRow ?? 0, startCol: c.startCol ?? 0 }))); setMode('design'); setView('edit')
  }

  const hasOffensivePattern = (grid: CellType[][]): boolean => {
    const isBlack = (r: number, c: number) => r >= 0 && c >= 0 && r < height && c < width && grid[r][c] === 'black'
    for (let arm = 2; arm <= 5; arm++) {
      for (let cr = 0; cr < height; cr++) for (let cc = 0; cc < width; cc++) for (const dir of [1, -1]) {
        if (!isBlack(cr, cc)) continue
        if (Array.from({ length: arm }, (_, i) => isBlack(cr - i - 1, cc)).every(Boolean)
          && isBlack(cr - arm, cc + dir) && Array.from({ length: arm }, (_, i) => isBlack(cr, cc + i + 1)).every(Boolean)
          && isBlack(cr + dir, cc + arm) && Array.from({ length: arm }, (_, i) => isBlack(cr + i + 1, cc)).every(Boolean)
          && isBlack(cr + arm, cc - dir) && Array.from({ length: arm }, (_, i) => isBlack(cr, cc - i - 1)).every(Boolean)
          && isBlack(cr - dir, cc - arm)) return true
      }
    }
    for (let arm = 2; arm <= 4; arm++) {
      for (let cr = 0; cr < height - arm * 2; cr++) for (let cc = 0; cc < width - arm - 2; cc++) for (const dir of [1, -1]) {
        const bolt = (sc: number) => Array.from({ length: arm }, (_, i) => isBlack(cr + i, sc)).every(Boolean)
          && Array.from({ length: arm }, (_, i) => isBlack(cr + arm + i, sc + dir * (i + 1))).every(Boolean)
          && Array.from({ length: arm }, (_, i) => isBlack(cr + arm * 2 - 1 + i, sc + dir * arm)).every(Boolean)
        if (bolt(cc) && bolt(cc + arm + 1)) return true
      }
    }
    return false
  }

  const mirrorCoords = (r: number, c: number): [number, number][] => {
    const coords: [number, number][] = [[r, c]]
    if (symmetry === 'rotational180') coords.push([height - 1 - r, width - 1 - c])
    else if (symmetry === 'mirror') coords.push([r, width - 1 - c])
    return coords
  }

  const selectWordInGrid = (r: number, c: number, dir: Dir) => {
    const wordCells = getWordCells(cells, r, c, dir, width, height)
    setHighlightedCells(new Set(wordCells.map(([wr, wc]) => `${wr},${wc}`)))
    setSelectedCell({ row: r, col: c })

    // find matching clue
    const startCell = wordCells[0]
    if (startCell) {
      const clue = clues.find(cl => cl.startRow === startCell[0] && cl.startCol === startCell[1] && cl.direction === dir)
      if (clue) {
        setActiveClueKey(`${clue.number}-${clue.direction}`)
        const idx = clues.indexOf(clue)
        if (idx >= 0) setEditingClueIdx(idx)
      }
    }
  }

  const onCellClick = (r: number, c: number) => {
    if (mode === 'design') {
      const next = cells.map(row => [...row])
      const nextLetters = letters.map(row => [...row])
      const newType = cells[r][c] === 'black' ? 'white' : 'black'
      for (const [mr, mc] of mirrorCoords(r, c)) {
        if (mr >= 0 && mr < height && mc >= 0 && mc < width) { next[mr][mc] = newType; if (newType === 'black') nextLetters[mr][mc] = null }
      }
      if (hasOffensivePattern(next)) return
      pushUndo()
      setCells(next); setLetters(nextLetters); setClues([])
    } else if (mode === 'fill') {
      if (cells[r][c] !== 'white') return
      const sameCell = selectedCell?.row === r && selectedCell?.col === c
      const dir = sameCell ? (editorDirection === 'across' ? 'down' : 'across') : editorDirection
      if (sameCell) setEditorDirection(dir)
      setSelectedCell({ row: r, col: c })
      const wordCells = getWordCells(cells, r, c, dir, width, height)
      setHighlightedCells(new Set(wordCells.map(([wr, wc]) => `${wr},${wc}`)))
    } else if (mode === 'test') {
      if (cells[r][c] === 'white') setTestSelected({ row: r, col: c })
    } else if (mode === 'clues') {
      if (cells[r][c] !== 'white') return
      const sameCell = selectedCell?.row === r && selectedCell?.col === c
      const dir = sameCell ? (editorDirection === 'across' ? 'down' : 'across') : editorDirection
      if (sameCell) setEditorDirection(dir)
      selectWordInGrid(r, c, dir)
    }
  }

  const onClueClick = (clue: EditorClue, idx: number) => {
    const dir: Dir = clue.direction === 'across' ? 'across' : 'down'
    setEditorDirection(dir)
    setActiveClueKey(`${clue.number}-${clue.direction}`)
    setEditingClueIdx(idx)
    // highlight the word in the grid
    const wordCells = getWordCells(cells, clue.startRow, clue.startCol, dir, width, height)
    setHighlightedCells(new Set(wordCells.map(([wr, wc]) => `${wr},${wc}`)))
    setSelectedCell({ row: clue.startRow, col: clue.startCol })
  }

  const generateClues = async () => {
    const { slots } = computeSlots(cells, letters, width, height)
    // merge with existing clues: keep text for unchanged words
    const merged = slots.map(slot => {
      // find existing clue at same position with same answer
      const existing = clues.find(cl =>
        cl.startRow === slot.startRow && cl.startCol === slot.startCol
        && cl.direction === slot.direction && cl.answer === slot.answer
      )
      return { ...slot, text: existing?.text || '' }
    })
    setClues(merged)
    // fetch suggestions for words that don't have them yet
    const answers = [...new Set(merged.map(s => s.answer).filter(a => a.length >= 3 && !a.includes('?')))]
    const needFetch = answers.filter(w => !suggestions[w])
    if (needFetch.length > 0) {
      const newSuggestions = { ...suggestions }
      await Promise.all(needFetch.map(async (word) => { newSuggestions[word] = await api.editorClueSuggestions(word) }))
      setSuggestions(newSuggestions)
    }
    setMode('clues')
  }

  const autofill = async () => {
    setFilling(true); setWarnings([])
    try {
      const locked: { row: number; col: number; letter: string }[] = []
      letters.forEach((row, r) => row.forEach((l, c) => {
        if (l && cells[r][c] === 'white') locked.push({ row: r, col: c, letter: l })
      }))
      const res = await api.editorAutofill(width, height, cells, locked.length > 0 ? locked : undefined)
      if (res.ok) {
        const newLetters = res.letters.map(row => row.map(l => l || null))
        setLetters(newLetters)
        // build clues with position info
        const { slots } = computeSlots(cells, newLetters, width, height)
        // SAFETY: the autofill endpoint shapes its clues list as editor clue entries
        const serverClues = (res.clues || []) as { number: number; direction: string; text?: string }[]
        const merged = slots.map(slot => {
          const sc = serverClues.find(c => c.number === slot.number && c.direction === slot.direction)
          return { ...slot, text: sc?.text || '' }
        })
        setClues(merged)
        // fetch suggestions
        const answerSet = new Set<string>()
        for (const c of merged) { if (c.answer.length >= 3) answerSet.add(c.answer) }
        const newSuggestions = { ...suggestions }
        await Promise.all([...answerSet].filter(w => !newSuggestions[w]).map(async (word) => {
          newSuggestions[word] = await api.editorClueSuggestions(word)
        }))
        setSuggestions(newSuggestions)
        setMode('clues')
      } else { setWarnings([res.error || t('editor.autoFillFailed')]) }
    } catch (e) { setWarnings([e instanceof Error ? e.message : t('editor.autoFillFailed')]) }
    setFilling(false)
  }

  const startTest = () => { setTestEntries(Array.from({ length: height }, () => Array(width).fill(''))); setTestSelected(null); setMode('test') }
  const exitTest = () => { setMode('clues'); setTestSelected(null) }
  const checkTest = () => {
    let correct = true
    for (let r = 0; r < height; r++) for (let c = 0; c < width; c++) {
      if (cells[r][c] === 'black') continue
      if ((testEntries[r]?.[c] || '').toUpperCase() !== (letters[r]?.[c] || '').toUpperCase()) correct = false
    }
    setWarnings(correct ? [t('editor.testPassed')] : [t('editor.testFailed')])
  }

  const updateClueText = (idx: number, text: string) => { const next = [...clues]; next[idx] = { ...next[idx], text }; setClues(next) }
  const pickSuggestion = (idx: number, text: string) => { updateClueText(idx, text); setEditingClueIdx(null) }

  const tryPublish = () => {
    setWarnings([])
    if (!title.trim() || title.trim() === 'Untitled') {
      setWarnings([t('editor.titleRequired')]); return
    }
    if (clues.length === 0 || clues.some(c => !c.text.trim())) {
      setWarnings([t('editor.cluesRequired')]); return
    }
    if (skipPublishConfirm) { doPublish(); return }
    setShowPublishConfirm(true)
  }

  const doPublish = async () => {
    setShowPublishConfirm(false)
    if (dontShowAgain) {
      setSkipPublishConfirm(true)
      api.fetchSettings().then(s => api.saveSettings({ ...s, skipPublishConfirm: true }))
    }
    setSaving(true); setWarnings([])
    const words = [...new Set(clues.map(c => c.answer).filter(a => a.length >= 3))]
    if (words.length > 0) {
      try { const res = await api.editorCheckWords(words); if (res.blocked?.length > 0) { setWarnings(res.blocked.map((w: string) => t('editor.offensiveContent', { word: w }))); setSaving(false); return } } catch { /* ignored */ }
    }
    try {
      const res = await api.editorSave({ id: puzzleId ?? undefined, title, width, height, grid: cells, clues, puzzle: { grid: { cells: letters.map(row => row.map(l => l ? { Letter: l } : null)) } }, status: 'published' })
      setPuzzleId(res.id)
      const pubRes = await api.editorPublish(res.id)
      // save themed word pack if marked as themed
      if (isThemed && themeName.trim()) {
        try {
          const packWords = clues
            .filter(c => c.answer.length >= 3 && c.text.trim())
            .map(c => ({ word: c.answer.toUpperCase(), clues: [{ text: c.text, difficulty: 1 }] }))
          if (packWords.length > 0) {
            await api.createPack({
              name: themeName.trim(),
              description: `Words and clues from "${title}"`,
              tags: [themeName.trim().toLowerCase()],
              words: packWords,
            })
          }
        } catch (_e) { /* pack save is best-effort */ }
      }
      if (pubRes.ok) setWarnings([t('editor.published')])
      api.editorList().then(setSavedPuzzles)
    } catch (e) { setWarnings([e instanceof Error ? e.message : 'Publish failed']) }
    setSaving(false)
  }

  const { cellNumbers, twoLetterWarnings } = cells.length === height && cells[0]?.length === width
    ? computeSlots(cells, letters, width, height)
    : EMPTY_SLOT_RESULT
  const cellSize = width <= 7 ? 44 : width <= 11 ? 36 : width <= 15 ? 28 : 22
  const isTest = mode === 'test'
  const displayLetters = isTest ? testEntries : letters
  const displaySelected = isTest ? testSelected : selectedCell

  if (view === 'list') {
    return (
      <div className="editor-fullscreen" role="main" aria-label={t('editor.title')}>
        <div className="editor-header"><div className="header-left"><button type="button" className="btn btn-sm btn-ghost" onClick={onClose} aria-label={t('common.back')}><IconArrowBack size={16} aria-hidden="true" /></button><button type="button" className="header-brand" onClick={onClose}>{t('app.name')}</button></div></div>
        <div className="editor-list-body">
          <div className="editor-new-section">
            <label className="settings-label">{t('editor.createNew')}</label>
            <div className="settings-row" style={{ flexWrap: 'wrap' }}>
              {SIZE_BUTTONS.map(([s, label]) => (
                <button key={label} className="btn btn-sm" onClick={() => newPuzzle(s, s)}><IconPlus size={11} style={{ verticalAlign: -1, marginRight: 3 }} />{label}</button>
              ))}
            </div>
            <div className="editor-custom-size">
              <label className="settings-label" style={{ marginTop: 8 }}>{t('editor.customSize')}</label>
              <div className="settings-row">
                <NumberInput value={customW} min={3} max={25} onChange={setCustomW} className="editor-size-input" />
                <span style={{ color: 'var(--text-tertiary)', fontSize: '0.75rem', display: 'flex', alignItems: 'center' }}>x</span>
                <NumberInput value={customH} min={3} max={25} onChange={setCustomH} className="editor-size-input" />
                <button className="btn btn-sm" onClick={() => newPuzzle(customW, customH)}><IconPlus size={11} style={{ verticalAlign: -1, marginRight: 3 }} /> {t('common.create')}</button>
              </div>
            </div>
          </div>
          {savedPuzzles.length > 0 && (
            <div className="editor-saved"><label className="settings-label">{t('editor.yourPuzzles')}</label>
              <div className="cc-list">{savedPuzzles.map(p => (
                <div key={p.id}>
                  <div className="cc-clue" style={{ cursor: 'pointer' }}>
                    <div className="cc-clue-text" onClick={() => loadPuzzle(p.id)}>{p.title} ({p.width}x{p.height})</div>
                    <div className="cc-clue-actions">
                      <span className={`cc-status cc-status-${p.status}`}>{p.status}</span>
                      {p.status === 'published' && (
                        <Tooltip text={t('constructorAnalytics.title')}>
                          <button className="btn btn-sm btn-ghost" onClick={(e) => { e.stopPropagation(); toggleAnalytics(p.id) }} style={{ padding: '0.125rem 0.25rem' }}>
                            <IconChartAreaFilled size={11} />
                          </button>
                        </Tooltip>
                      )}
                      {p.status === 'published' ? (
                        <span className="editor-hint-text">{t('editor.unpublishToDelete')}</span>
                      ) : deleteConfirmId === p.id ? (
                        <>
                          <button className="btn btn-sm" style={{ fontSize: '0.5rem', padding: '0.125rem 0.375rem' }} onClick={doDelete}>{t('editor.yesDelete')}</button>
                          <button className="btn btn-sm btn-ghost" style={{ fontSize: '0.5rem', padding: '0.125rem 0.375rem' }} onClick={() => setDeleteConfirmId(null)}>{t('common.cancel')}</button>
                        </>
                      ) : (
                        <button className="cc-delete" onClick={() => confirmDelete(p.id)}><IconTrashFilled size={11} /></button>
                      )}
                    </div>
                  </div>
                  {analyticsId === p.id && (
                    <div style={{ padding: '8px 12px', background: 'var(--surface-alt)', borderRadius: 6, margin: '4px 0 8px', fontSize: '0.75rem' }}>
                      {analyticsLoading ? (
                        <span style={{ color: 'var(--text-tertiary)' }}>{t('common.loading')}</span>
                      ) : !analyticsData || analyticsData.total_solvers === 0 ? (
                        <span style={{ color: 'var(--text-tertiary)' }}>{t('constructorAnalytics.noData')}</span>
                      ) : (
                        <>
                          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 8, marginBottom: 8 }}>
                            <div>
                              <div style={{ color: 'var(--text-tertiary)', fontSize: '0.65rem' }}>{t('constructorAnalytics.totalSolvers')}</div>
                              <div style={{ fontWeight: 600 }}>{analyticsData.total_solvers}</div>
                            </div>
                            <div>
                              <div style={{ color: 'var(--text-tertiary)', fontSize: '0.65rem' }}>{t('constructorAnalytics.avgTime')}</div>
                              <div style={{ fontWeight: 600 }}>{Math.floor(analyticsData.avg_time / 60)}:{String(analyticsData.avg_time % 60).padStart(2, '0')}</div>
                            </div>
                            <div>
                              <div style={{ color: 'var(--text-tertiary)', fontSize: '0.65rem' }}>{t('constructorAnalytics.completionRate')}</div>
                              <div style={{ fontWeight: 600 }}>{Math.round(analyticsData.completion_rate * 100)}%</div>
                            </div>
                          </div>
                          {analyticsData.hint_heatmap && analyticsData.hint_heatmap.length > 0 && (
                            <div>
                              <div style={{ color: 'var(--text-tertiary)', fontSize: '0.65rem', marginBottom: 4 }}>{t('constructorAnalytics.hintHeatmap')}</div>
                              <div style={{
                                display: 'grid',
                                gridTemplateColumns: `repeat(${analyticsData.width || p.width}, 1fr)`,
                                gap: 1, maxWidth: 200
                              }}>
                                {analyticsData.hint_heatmap.flat().map((val: number, i: number) => (
                                  <div key={i} style={{
                                    width: '100%', aspectRatio: '1', borderRadius: 2,
                                    background: val < 0 ? 'var(--cell-black)' : `rgba(239, 68, 68, ${Math.min(val / (analyticsData.max_hints || 1), 1) * 0.8 + 0.1})`,
                                  }} />
                                ))}
                              </div>
                              <div style={{ color: 'var(--text-tertiary)', fontSize: '0.6rem', marginTop: 2 }}>{t('constructorAnalytics.heatmapDesc')}</div>
                            </div>
                          )}
                        </>
                      )}
                    </div>
                  )}
                </div>
              ))}</div>
            </div>
          )}

          <div className="editor-import-export">
            <label className="settings-label" style={{ marginTop: 16 }}>{t('editor.importExport')}</label>
            <div className="settings-row">
              <button className="btn btn-sm" onClick={() => {
                const input = document.createElement('input')
                input.type = 'file'; input.accept = '.ipuz,.json'
                input.onchange = async () => {
                  if (!input.files?.length) return
                  try {
                    const text = await input.files[0].text()
                    const data = JSON.parse(text)
                    const res = await api.editorImport(data)
                    api.editorList().then(setSavedPuzzles)
                    setWarnings([`Imported ${res.imported} puzzle${res.imported !== 1 ? 's' : ''}`])
                  } catch (e) { setWarnings([e instanceof Error ? e.message : 'Import failed']) }
                }
                input.click()
              }}>
                <IconFileUploadFilled size={11} style={{ verticalAlign: -1, marginRight: 3 }} /> {t('common.import')}
              </button>
              {savedPuzzles.length > 0 && (
                <button className="btn btn-sm" onClick={() => api.editorExportAll()}>
                  <IconDownloadFilled size={11} style={{ verticalAlign: -1, marginRight: 3 }} /> {t('editor.exportAll')}
                </button>
              )}
            </div>
          </div>
        </div>
      </div>
    )
  }

  return (
    <div className="editor-fullscreen" role="main" aria-label={t('editor.title')}>
      <div className="editor-header">
        <div className="editor-header-left">
          <button type="button" className="btn btn-sm btn-ghost" onClick={() => setView('list')} aria-label={t('common.back')}><IconArrowBack size={16} aria-hidden="true" /></button>
          <button type="button" className="header-brand" onClick={onClose}>{t('app.name')}</button>
          <input className="editor-title-input" value={title} onChange={e => setTitle(e.target.value)} placeholder={t('editor.puzzleTitle')} />
        </div>
        <div className="editor-header-actions">
          <div className="editor-mode-toggle">
            <button className={`btn btn-sm ${mode === 'design' ? 'btn-primary' : ''}`} onClick={() => { setMode('design'); setHighlightedCells(new Set()) }}><IconPointerFilled size={12} style={{ verticalAlign: -1, marginRight: 3 }} /> {t('editor.design')}</button>
            <button className={`btn btn-sm ${mode === 'fill' ? 'btn-primary' : ''}`} onClick={() => { setMode('fill'); setHighlightedCells(new Set()) }}><IconPencilFilled size={12} style={{ verticalAlign: -1, marginRight: 3 }} /> {t('editor.fill')}</button>
            <button className={`btn btn-sm ${mode === 'clues' ? 'btn-primary' : ''}`} onClick={generateClues}><IconListFilled size={12} style={{ verticalAlign: -1, marginRight: 3 }} /> {t('editor.clues')}</button>
            {clues.length > 0 && <button className={`btn btn-sm ${mode === 'test' ? 'btn-primary' : ''}`} onClick={isTest ? exitTest : startTest}><IconPlayerPlayFilled size={12} style={{ verticalAlign: -1, marginRight: 3 }} />{isTest ? t('editor.exitTest') : t('common.test')}</button>}
          </div>
          <button className="btn btn-sm btn-primary" onClick={autofill} disabled={filling}><IconSparklesFilled size={13} style={{ verticalAlign: -2, marginRight: 3 }} />{filling ? t('editor.filling') : t('editor.autoFill')}</button>
          <Tooltip text={t('editor.undoCtrlZ')}><button className="btn btn-sm" onClick={undo} disabled={undoStack.length === 0}><IconArrowBack size={13} style={{ verticalAlign: -2, marginRight: 3 }} /> {t('common.undo')}</button></Tooltip>
          <Select value="" onChange={v => {
            if (v === 'clear-all') clearGrid()
            else if (v === 'clear-letters') clearLetters()
            else if (v === 'unpublish') unpublish()
            else if (v === 'export' && puzzleId) api.editorExport(puzzleId)
          }} className="btn btn-sm" options={[
            { value: '', label: t('common.more') },
            { value: 'clear-letters', label: t('editor.clearLetters') },
            { value: 'clear-all', label: t('editor.clearEverything') },
            ...(puzzleId ? [{ value: 'export', label: t('editor.exportIpuz') }] : []),
            ...(savedPuzzles.find(p => p.id === puzzleId)?.status === 'published' ? [{ value: 'unpublish', label: t('editor.unpublish') }] : []),
          ]} />
          {clues.length > 0 && <button className="btn btn-sm" onClick={tryPublish} disabled={saving}><IconWorldFilled size={13} style={{ verticalAlign: -2, marginRight: 3 }} /> {t('common.publish')}</button>}
          {lastSaved && <span className="editor-autosave-indicator">saved {lastSaved}</span>}
          <button className="btn btn-sm btn-ghost" onClick={onClose}><IconX size={16} /></button>
        </div>
      </div>

      {(warnings.length > 0 || twoLetterWarnings.length > 0) && (
        <div className="editor-warnings" style={{ justifyContent: 'center' }}>
          <IconAlertTriangleFilled size={14} />
          {warnings.map((w, i) => <span key={`w${i}`}>{w}</span>)}
          {twoLetterWarnings.length > 0 && <span>{twoLetterWarnings.length} two-letter slot{twoLetterWarnings.length > 1 ? 's' : ''} (min 3 letters)</span>}
        </div>
      )}

      <div className="editor-workspace">
        <div className="editor-grid-area">
          <svg viewBox={`0 0 ${width * cellSize} ${height * cellSize}`} style={{ maxWidth: width * cellSize, width: '100%' }}>
            {cells.map((row, r) => row.map((cell, c) => {
              const isSel = displaySelected?.row === r && displaySelected?.col === c
              const isHighlighted = highlightedCells.has(`${r},${c}`)
              const val = displayLetters[r]?.[c]
              return (
                <g key={`${r}-${c}`} onClick={() => onCellClick(r, c)} style={{ cursor: 'pointer' }}>
                  <rect x={c * cellSize} y={r * cellSize} width={cellSize} height={cellSize}
                    fill={cell === 'black' ? 'var(--text)' : isSel ? 'var(--cell-selected, #bfdbfe)' : isHighlighted ? 'var(--accent-soft, #e0e7ff)' : 'var(--surface)'}
                    stroke="var(--border)" strokeWidth={0.5} />
                  {cellNumbers[`${r},${c}`] && <text x={c * cellSize + 2} y={r * cellSize + cellSize * 0.3} fontSize={cellSize * 0.25} fill="var(--text-tertiary)" fontFamily="var(--font-mono)">{cellNumbers[`${r},${c}`]}</text>}
                  {val && <text x={c * cellSize + cellSize / 2} y={r * cellSize + cellSize * 0.75} fontSize={cellSize * 0.5} fill="var(--text)" fontFamily="var(--font-display)" fontWeight="700" textAnchor="middle">{val}</text>}
                </g>
              )
            }))}
          </svg>
          <div className="editor-grid-hint">
            {mode === 'design' ? t('editor.designHint')
              : mode === 'fill' ? t('editor.fillHint', { dir: editorDirection })
              : mode === 'test' ? t('editor.testHint')
              : t('editor.clueHint')}
          </div>
          {mode === 'design' && (
            <Select value={symmetry} onChange={v => { const s = String(v); if (s === 'rotational180' || s === 'mirror' || s === 'none') setSymmetry(s) }} className="btn btn-sm" style={{ marginTop: 8 }} options={[
              { value: 'rotational180', label: t('editor.rotationalSymmetry') },
              { value: 'mirror', label: t('editor.mirrorSymmetry') },
              { value: 'none', label: t('editor.noSymmetry') },
            ]} />
          )}
          {isTest && <button className="btn btn-sm" onClick={checkTest} style={{ marginTop: 8 }}>{t('editor.checkAnswers')}</button>}
        </div>

        <div className="editor-clue-area">
          {(mode === 'clues' || mode === 'test' || clues.length > 0) && clues.length > 0 ? (
            <>
              <label className="settings-label">{isTest ? t('editor.clues') : t('editor.cluesClickToEdit')}</label>
              <div className="editor-clue-list">
                {clues.map((clue, idx) => (
                  <div key={`${clue.number}-${clue.direction}`} id={`ec-${clue.number}-${clue.direction}`}
                    className={`editor-clue-item ${activeClueKey === `${clue.number}-${clue.direction}` ? 'editor-clue-active' : ''}`}
                    onClick={() => !isTest && onClueClick(clue, idx)}>
                    <span className="editor-clue-num">{clue.number}{clue.direction === 'across' ? 'a' : 'd'}</span>
                    {!isTest && <span className="editor-clue-answer">{clue.answer}</span>}
                    {!isTest && editingClueIdx === idx ? (
                      <div className="editor-clue-edit-area">
                        <input className="editor-clue-input" value={clue.text} onChange={e => updateClueText(idx, e.target.value)}
                          onBlur={() => setTimeout(() => setEditingClueIdx(null), 200)}
                          onKeyDown={e => e.key === 'Enter' && setEditingClueIdx(null)} placeholder={t('editor.writeClue')} autoFocus />
                        {suggestions[clue.answer]?.length > 0 && (
                          <div className="editor-suggestions">
                            {suggestions[clue.answer].map((s, si) => (
                              <button key={si} className="editor-suggestion" onClick={() => pickSuggestion(idx, s)}>{s}</button>
                            ))}
                          </div>
                        )}
                      </div>
                    ) : (
                      <span className={`editor-clue-text ${!clue.text ? 'editor-clue-empty-text' : ''}`}>
                        {clue.text || (isTest ? '' : t('editor.clickToAddClue'))}
                      </span>
                    )}
                  </div>
                ))}
              </div>
            </>
          ) : (
            <div className="editor-clue-empty">
              <p dangerouslySetInnerHTML={{ __html: t('editor.emptyHint1') }} />
              <p dangerouslySetInnerHTML={{ __html: t('editor.emptyHint2') }} />
            </div>
          )}
        </div>
      </div>

      {showPublishConfirm && (
        <div className="settings-overlay" onClick={() => setShowPublishConfirm(false)}>
          <div className="editor-publish-dialog" onClick={e => e.stopPropagation()}>
            <h3>{t('editor.publishTitle')}</h3>
            <p>{t('editor.publishDesc')}</p>
            <p>{t('editor.publishNote')}</p>
            <label className="editor-publish-check">
              <input type="checkbox" checked={isThemed} onChange={e => setIsThemed(e.target.checked)} />
              {t('editor.isThemed')}
            </label>
            {isThemed && (
              <input type="text" placeholder={t('editor.themePlaceholder')}
                value={themeName} onChange={e => setThemeName(e.target.value)}
                style={{ width: '100%', marginTop: 6, marginBottom: 6, padding: '0.375rem 0.5rem',
                  fontFamily: 'var(--font-body)', fontSize: '0.75rem',
                  border: '1px solid var(--border)', background: 'var(--surface)', color: 'var(--text)' }} />
            )}
            <label className="editor-publish-check">
              <input type="checkbox" checked={dontShowAgain} onChange={e => setDontShowAgain(e.target.checked)} />
              {t('editor.dontShowAgain')}
            </label>
            <div className="editor-publish-actions">
              <button className="btn btn-sm" onClick={() => setShowPublishConfirm(false)}>{t('common.cancel')}</button>
              <button className="btn btn-sm btn-primary" onClick={doPublish}>{t('common.publish')}</button>
            </div>
          </div>
        </div>
      )}
    </div>
  )
}
