import { useState, useEffect, useRef, useCallback, lazy, Suspense } from 'react'
import { useParams, useNavigate, useSearchParams, Link } from 'react-router-dom'
import { useAuth } from '../context/AuthContext'
import api from '../api/client'

const TeachChat = lazy(() => import('../components/TeachChat'))

const OPTION_LETTERS = ['A', 'B', 'C', 'D', 'E', 'F']
const QUESTION_HIGHLIGHT_WORDS = 8
const OPTION_HIGHLIGHT_WORDS = 7
const TTS_PRELOAD_AHEAD = 5

function mergeTextRanges(ranges) {
  const sorted = ranges
    .filter(r => Number.isFinite(r.start) && Number.isFinite(r.end) && r.end > r.start)
    .sort((a, b) => a.start - b.start || a.end - b.end)
  const merged = []
  sorted.forEach(range => {
    const last = merged[merged.length - 1]
    if (!last || range.start > last.end) {
      merged.push({ start: range.start, end: range.end })
    } else {
      last.end = Math.max(last.end, range.end)
    }
  })
  return merged
}

function removeTextRange(ranges, removeRange) {
  const next = []
  ranges.forEach(range => {
    if (removeRange.end <= range.start || removeRange.start >= range.end) {
      next.push(range)
      return
    }
    if (removeRange.start > range.start) next.push({ start: range.start, end: removeRange.start })
    if (removeRange.end < range.end) next.push({ start: removeRange.end, end: range.end })
  })
  return mergeTextRanges(next)
}

function getSpeechChunkRange(text, maxWords, activeChunk) {
  if (activeChunk === null || activeChunk === undefined) return null
  const words = []
  const re = /\S+/g
  let match
  while ((match = re.exec(text || ''))) words.push({ start: match.index, end: match.index + match[0].length })
  const startWord = activeChunk * maxWords
  if (!words[startWord]) return null
  const endWord = Math.min(startWord + maxWords - 1, words.length - 1)
  return { start: words[startWord].start, end: words[endWord].end }
}

function getManualHighlightSelection(selection = window.getSelection?.()) {
  if (!selection || selection.rangeCount === 0 || !selection.toString().trim()) return null

  const offsetFromNode = (node, offset) => {
    const element = node.nodeType === Node.TEXT_NODE ? node.parentElement : node
    const span = element?.closest?.('[data-manual-highlight-id]')
    if (!span) return null
    let charOffset = offset
    if (node.nodeType !== Node.TEXT_NODE) {
      charOffset = offset <= 0 ? 0 : Number(span.dataset.end || span.dataset.start || 0) - Number(span.dataset.start || 0)
    }
    return {
      id: span.dataset.manualHighlightId,
      offset: Number(span.dataset.start || 0) + charOffset,
    }
  }

  const range = selection.getRangeAt(0)
  const start = offsetFromNode(range.startContainer, range.startOffset)
  const end = offsetFromNode(range.endContainer, range.endOffset)
  if (!start || !end || start.id !== end.id) return null
  const ordered = start.offset <= end.offset ? { start: start.offset, end: end.offset } : { start: end.offset, end: start.offset }
  if (ordered.end <= ordered.start) return null
  return { id: start.id, ...ordered }
}

function ManualHighlightText({ text, textId, highlights = [], speechRange = null, onRemoveHighlight = null }) {
  const ranges = mergeTextRanges(highlights)
  const boundaries = new Set([0, (text || '').length])
  ranges.forEach(range => { boundaries.add(range.start); boundaries.add(range.end) })
  if (speechRange) { boundaries.add(speechRange.start); boundaries.add(speechRange.end) }
  const points = [...boundaries]
    .filter(point => point >= 0 && point <= (text || '').length)
    .sort((a, b) => a - b)

  return points.slice(0, -1).map((start, i) => {
    const end = points[i + 1]
    if (end <= start) return null
    const manuallyHighlighted = ranges.some(range => start >= range.start && end <= range.end)
    const speechHighlighted = speechRange && start >= speechRange.start && end <= speechRange.end
    const className = [
      'manual-highlight-segment',
      manuallyHighlighted ? 'manual-highlight-active' : '',
      speechHighlighted ? 'speech-highlight-active' : '',
    ].filter(Boolean).join(' ')
    return (
      <span
        key={`${start}-${end}`}
        className={className}
        data-manual-highlight-id={textId}
        data-start={start}
        data-end={end}
        onContextMenu={manuallyHighlighted ? (event) => {
          event.preventDefault()
          onRemoveHighlight?.(textId, start)
        } : undefined}
        title={manuallyHighlighted ? 'Right-click to remove this highlight' : undefined}
      >
        {text.slice(start, end)}
      </span>
    )
  })
}

function splitSpeechChunks(text, maxWords) {
  const words = (text || '').trim().split(/\s+/).filter(Boolean)
  if (!words.length) return []
  const chunks = []
  for (let i = 0; i < words.length; i += maxWords) {
    chunks.push(words.slice(i, i + maxWords).join(' '))
  }
  return chunks
}

function questionStem(question) {
  return (question?.question_text || '').replace('[IMAGE]', '').trim()
}

function getQuestionSpeechSegments(question, index) {
  if (!question) return []
  const segments = []
  splitSpeechChunks(questionStem(question), QUESTION_HIGHLIGHT_WORDS).forEach((chunk, chunkIndex) => {
    segments.push({
      type: 'question',
      chunkIndex,
      text: chunkIndex === 0 ? `Question ${index + 1}. ${chunk}` : chunk,
    })
  })
  if (question.options?.length) segments.push({ type: 'meta', text: 'Options.' })
  ;(question.options || []).forEach((option, i) => {
    splitSpeechChunks(option, OPTION_HIGHLIGHT_WORDS).forEach((chunk, chunkIndex) => {
      segments.push({
        type: 'option',
        index: i,
        chunkIndex,
        text: chunkIndex === 0 ? `${OPTION_LETTERS[i] || i + 1}. ${chunk}` : chunk,
      })
    })
  })
  return segments
}

function buildQuestionSpeechText(question, index) {
  const segments = getQuestionSpeechSegments(question, index)
  return segments.map(s => s.text).join(' ')
}

function getActiveSpeechSegment(audio, segments) {
  if (!segments.length) return null
  if (!audio || !Number.isFinite(audio.duration) || audio.duration <= 0) return 0
  const weights = segments.map(segment => Math.max(12, segment.text.length))
  const total = weights.reduce((sum, weight) => sum + weight, 0)
  const target = (audio.currentTime / audio.duration) * total
  let cursor = 0
  for (let i = 0; i < weights.length; i += 1) {
    cursor += weights[i]
    if (target <= cursor) return i
  }
  return segments.length - 1
}

function TTSButton({ text, voice, segments = [], onActiveChange, onSegmentChange, getAudio, autoPlay, onEnded }) {
  const [state, setState] = useState('idle') // idle | loading | playing
  const audioRef = useRef(null)

  useEffect(() => {
    return () => {
      audioRef.current?.pause()
      onActiveChange?.(false)
      onSegmentChange?.(null)
    }
  }, [])

  useEffect(() => {
    if (autoPlay && state === 'idle') speak()
  }, [autoPlay])

  const setStateAndNotify = (s) => {
    setState(s)
    onActiveChange?.(s !== 'idle')
    if (s === 'idle') onSegmentChange?.(null)
  }

  const speak = async () => {
    if (state === 'playing') {
      audioRef.current?.pause()
      setStateAndNotify('idle')
      return
    }
    if (state === 'loading') return
    try {
      setStateAndNotify('loading')
      let url
      let revokeOnDone = false
      if (getAudio) {
        url = (await getAudio(text, voice))?.url
      } else {
        const res = await api.post('/tts/speak', { text, voice: voice || null }, { responseType: 'blob' })
        url = URL.createObjectURL(res.data)
        revokeOnDone = true
      }
      if (!url) throw new Error('No audio returned')
      const audio = new Audio(url)
      audioRef.current = audio
      const updateSegment = () => {
        const activeIndex = getActiveSpeechSegment(audio, segments)
        onSegmentChange?.(activeIndex === null ? null : segments[activeIndex] || null)
      }
      audio.onloadedmetadata = updateSegment
      audio.ontimeupdate = updateSegment
      audio.onended = () => { setStateAndNotify('idle'); onEnded?.(); if (revokeOnDone) URL.revokeObjectURL(url) }
      audio.onerror = () => { setStateAndNotify('idle'); if (revokeOnDone) URL.revokeObjectURL(url) }
      await audio.play()
      setStateAndNotify('playing')
      updateSegment()
    } catch { setStateAndNotify('idle') }
  }

  const label = state === 'loading' ? '⏳ Loading...' : state === 'playing' ? '⏹ Stop' : '🔊 Listen'
  const bg = state === 'playing' ? '#ef4444' : state === 'loading' ? '#e2e8f0' : '#e0e7ff'
  const color = state === 'playing' ? 'white' : state === 'loading' ? '#64748b' : '#3730a3'

  return (
    <button onClick={speak} title={label} disabled={state === 'loading'} style={{
      background: bg, color, border: 'none', borderRadius: '6px',
      padding: '4px 10px', cursor: state === 'loading' ? 'not-allowed' : 'pointer',
      fontSize: '0.8rem', marginLeft: 8, whiteSpace: 'nowrap',
    }}>
      {label}
    </button>
  )
}

function TimerDisplay({ seconds, total }) {
  const pct = total > 0 ? (seconds / total) * 100 : 100
  const mins = Math.floor(seconds / 60), secs = seconds % 60
  const color = pct > 50 ? '#22c55e' : pct > 20 ? '#f59e0b' : '#ef4444'
  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
      <div style={{ fontWeight: 700, fontSize: '1.1rem', color }}>
        {String(mins).padStart(2,'0')}:{String(secs).padStart(2,'0')}
      </div>
      <div style={{ width: 100, background: '#e2e8f0', borderRadius: 999, height: 6 }}>
        <div style={{ width: `${pct}%`, height: '100%', background: color, borderRadius: 999, transition: 'width 1s' }} />
      </div>
    </div>
  )
}

function CourseQuizStart({ quiz, onStart }) {
  const mode = quiz.mode === 'timed' ? 'exam' : 'study'
  return (
    <div style={{ maxWidth: 480, margin: '40px auto' }}>
      <div className="card" style={{ textAlign: 'center' }}>
        <h2 style={{ marginBottom: 8 }}>{quiz.title}</h2>
        <p style={{ color: 'var(--text-muted)', fontSize: '0.9rem', marginBottom: 8 }}>
          {quiz.questions_per_attempt || quiz.questions_count} questions
          {quiz.mode === 'timed' && quiz.time_limit_minutes ? ` · ${quiz.time_limit_minutes} min time limit` : ''}
        </p>
        <p style={{ fontSize: '0.88rem', marginBottom: 20 }}>
          {mode === 'exam'
            ? 'This is a timed exam — answers are hidden until you submit.'
            : 'Study mode — answers and explanations shown as you go.'}
        </p>
        <button className="btn btn-primary" onClick={() => onStart(mode, '', quiz.time_limit_minutes || null)}>
          Begin Quiz
        </button>
      </div>
    </div>
  )
}

function ModeSelectScreen({ quiz, voices, onStart }) {
  const [selectedVoice, setSelectedVoice] = useState(voices.find(v => v.is_default)?.id || voices[0]?.id || '')
  const [customTimer, setCustomTimer] = useState(quiz.time_limit_minutes || '')
  const [startError, setStartError] = useState('')
  const [startingMode, setStartingMode] = useState('')

  const handleStart = async (mode) => {
    const timerMinutes = mode === 'exam' && customTimer ? parseInt(customTimer) : null
    setStartError('')
    setStartingMode(mode)
    try {
      await onStart(mode, selectedVoice, timerMinutes)
    } catch {
      setStartError('Could not start the quiz. Try again.')
    } finally {
      setStartingMode('')
    }
  }

  return (
    <div style={{ maxWidth: 520, margin: '40px auto' }}>
      <div className="card" style={{ textAlign: 'center' }}>
        <div style={{ fontSize: '2.5rem', marginBottom: 12 }}>📝</div>
        <h2 style={{ marginBottom: 6 }}>{quiz.title}</h2>
        <p style={{ color: '#64748b', fontSize: '0.9rem', marginBottom: 24 }}>
          {quiz.questions_count} questions
          {quiz.time_limit_minutes ? ` · ${quiz.time_limit_minutes} min limit` : ''}
        </p>
        <p style={{ fontWeight: 600, marginBottom: 16, color: '#374151' }}>Choose how to take this quiz:</p>
        <div style={{ display: 'flex', gap: 12, justifyContent: 'center', marginBottom: 20 }}>
          {[
            { mode: 'study', icon: '📖', label: 'Study Mode', desc: 'Answers & explanations shown as you go', color: '#22c55e', bg: '#f0fdf4' },
            { mode: 'exam', icon: '🎯', label: 'Exam Mode', desc: 'Answers hidden until submitted', color: '#3b82f6', bg: '#eff6ff' },
          ].map(({ mode, icon, label, desc, color, bg }) => (
            <div key={mode} onClick={() => !startingMode && handleStart(mode)}
              style={{ flex: 1, border: `2px solid ${color}`, borderRadius: 12, padding: '18px 12px', cursor: startingMode ? 'wait' : 'pointer', background: bg, transition: 'transform 0.1s', opacity: startingMode && startingMode !== mode ? 0.55 : 1 }}
              onMouseEnter={e => e.currentTarget.style.transform = 'scale(1.03)'}
              onMouseLeave={e => e.currentTarget.style.transform = 'none'}
            >
              <div style={{ fontSize: '1.8rem', marginBottom: 6 }}>{icon}</div>
              <div style={{ fontWeight: 700, color, marginBottom: 4 }}>{label}</div>
              <div style={{ fontSize: '0.8rem', color }}>{startingMode === mode ? 'Starting...' : desc}</div>
            </div>
          ))}
        </div>
        {startError && (
          <div style={{ background: '#fef2f2', color: '#991b1b', border: '1px solid #fecaca', borderRadius: 8, padding: '8px 10px', fontSize: '0.82rem', marginBottom: 14 }}>
            {startError}
          </div>
        )}

        <div style={{ borderTop: '1px solid var(--border)', paddingTop: 14, marginBottom: 14 }}>
          <label style={{ fontSize: '0.85rem', color: 'var(--text-muted)', display: 'block', marginBottom: 6 }}>⏱ Timer for Exam Mode <span style={{ fontWeight: 400 }}>(minutes, optional)</span></label>
          <input type="number" min={1} value={customTimer}
            onChange={e => setCustomTimer(e.target.value)}
            placeholder="No time limit"
            onClick={e => e.stopPropagation()}
            style={{ padding: '6px 10px', borderRadius: 8, border: '1px solid var(--border)', fontSize: '0.9rem', width: '100%', background: 'var(--input-bg)', color: 'var(--text)' }} />
          <p style={{ fontSize: '0.75rem', color: 'var(--text-subtle)', marginTop: 4 }}>Auto-submits when timer expires. Timer pauses if you leave and resumes when you come back.</p>
        </div>

        {voices.length > 0 && (
          <div style={{ borderTop: '1px solid var(--border)', paddingTop: 14 }}>
            <label style={{ fontSize: '0.85rem', color: 'var(--text-muted)', display: 'block', marginBottom: 6 }}>🔊 Voice for read-aloud</label>
            <select value={selectedVoice} onChange={e => setSelectedVoice(e.target.value)}
              style={{ padding: '6px 10px', borderRadius: 8, border: '1px solid #d1d5db', fontSize: '0.9rem', width: '100%' }}>
              {voices.map(v => <option key={v.id} value={v.id}>{v.name}{v.is_default ? ' (default)' : ''}</option>)}
            </select>
          </div>
        )}
      </div>
    </div>
  )
}

function getQuizSessionId() {
  const key = 'pedshub_quiz_session_id'
  try {
    const existing = localStorage.getItem(key)
    if (existing) return existing
    const created = window.crypto?.randomUUID?.() || `${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`
    localStorage.setItem(key, created)
    return created
  } catch {
    return `${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`
  }
}

// Stable per-device session ID lets the same app/webview resume after restart.
const SESSION_ID = getQuizSessionId()

export default function QuizPage() {
  const { id } = useParams()
  const navigate = useNavigate()
  const [searchParams] = useSearchParams()
  const returnTo = searchParams.get('return_to')
  const { user } = useAuth()
  const isModerator = user?.role === 'admin' || user?.role === 'moderator'
  const [quiz, setQuiz] = useState(null)
  const [voices, setVoices] = useState([])
  const [selectedVoice, setSelectedVoice] = useState('')
  const [ttsActive, setTtsActive] = useState(false)
  const [readThrough, setReadThrough] = useState(false)
  const [quizMode, setQuizMode] = useState(null)
  const [answers, setAnswers] = useState({})
  const [currentIdx, setCurrentIdx] = useState(0)
  const [loading, setLoading] = useState(true)
  const [starting, setStarting] = useState(false)
  const [submitting, setSubmitting] = useState(false)
  const [attemptId, setAttemptId] = useState(null)
  const [timeLeft, setTimeLeft] = useState(null)
  const [totalTime, setTotalTime] = useState(null)
  const [toast, setToast] = useState('')
  const [navOpen, setNavOpen] = useState(false)
  const [expandedImagePath, setExpandedImagePath] = useState('')
  const [imageZoom, setImageZoom] = useState(1)
  const [startedAt, setStartedAt] = useState(null)
  const [favorites, setFavorites] = useState([])
  const [activeReadSegment, setActiveReadSegment] = useState(null)
  const [manualHighlights, setManualHighlights] = useState({})
  const timerRef = useRef(null)
  const toastRef = useRef(null)
  const hasStarted = useRef(false)
  const ttsCacheRef = useRef(new Map())
  const autoAdvanceRef = useRef(null)
  const savedHighlightSelectionRef = useRef(null)
  const autoHighlightTimerRef = useRef(null)

  const showToast = (msg) => {
    setToast(msg)
    clearTimeout(toastRef.current)
    toastRef.current = setTimeout(() => setToast(''), 3000)
  }

  useEffect(() => {
    try {
      const saved = localStorage.getItem(`quiz-highlights:${id}`)
      setManualHighlights(saved ? JSON.parse(saved) : {})
    } catch {
      setManualHighlights({})
    }
  }, [id])

  useEffect(() => {
    try {
      localStorage.setItem(`quiz-highlights:${id}`, JSON.stringify(manualHighlights))
    } catch { }
  }, [id, manualHighlights])

  const [leaveTarget, setLeaveTarget] = useState(null)
  const questions = quiz?.questions || []
  const current = questions[currentIdx]
  const isStudy = quizMode === 'study'

  const applyManualHighlightSelection = useCallback((selected = getManualHighlightSelection() || savedHighlightSelectionRef.current) => {
    if (!selected || !current) return
    const [questionKey, fieldKey] = selected.id.split('::')
    if (questionKey !== String(current.id)) return

    const existing = manualHighlights[current.id]?.[fieldKey] || []
    const removeExisting = existing.some(range => selected.start >= range.start && selected.end <= range.end)

    setManualHighlights(prev => {
      const questionRanges = prev[current.id] || {}
      const currentRanges = questionRanges[fieldKey] || []
      const nextRanges = removeExisting
        ? removeTextRange(currentRanges, selected)
        : mergeTextRanges([...currentRanges, { start: selected.start, end: selected.end }])
      const nextQuestion = { ...questionRanges, [fieldKey]: nextRanges }
      if (!nextRanges.length) delete nextQuestion[fieldKey]
      const next = { ...prev, [current.id]: nextQuestion }
      if (!Object.keys(nextQuestion).length) delete next[current.id]
      return next
    })

    window.getSelection?.().removeAllRanges()
    savedHighlightSelectionRef.current = null
    showToast(removeExisting ? 'Highlight removed' : 'Highlighted')
  }, [current?.id, manualHighlights])

  const captureHighlightSelection = useCallback(() => {
    const selected = getManualHighlightSelection()
    if (!selected || !current) return
    const [questionKey] = selected.id.split('::')
    if (questionKey !== String(current.id)) return
    savedHighlightSelectionRef.current = selected
    clearTimeout(autoHighlightTimerRef.current)
    autoHighlightTimerRef.current = setTimeout(() => applyManualHighlightSelection(selected), 450)
  }, [current?.id, applyManualHighlightSelection])

  useEffect(() => {
    document.addEventListener('selectionchange', captureHighlightSelection)
    document.addEventListener('mouseup', captureHighlightSelection)
    document.addEventListener('touchend', captureHighlightSelection)
    return () => {
      clearTimeout(autoHighlightTimerRef.current)
      document.removeEventListener('selectionchange', captureHighlightSelection)
      document.removeEventListener('mouseup', captureHighlightSelection)
      document.removeEventListener('touchend', captureHighlightSelection)
    }
  }, [captureHighlightSelection])

  const fetchTtsAudio = useCallback(async (text, voice) => {
    const cleanText = (text || '').trim()
    if (!cleanText) return null
    const key = `${voice || 'default'}::${cleanText}`
    const cached = ttsCacheRef.current.get(key)
    if (cached?.url) return cached
    if (cached?.promise) return cached.promise

    const promise = api.post('/tts/speak', { text: cleanText, voice: voice || null }, { responseType: 'blob' })
      .then(res => {
        const entry = { url: URL.createObjectURL(res.data) }
        ttsCacheRef.current.set(key, entry)
        return entry
      })
      .catch(err => {
        if (ttsCacheRef.current.get(key)?.promise === promise) ttsCacheRef.current.delete(key)
        throw err
      })
    ttsCacheRef.current.set(key, { promise })
    return promise
  }, [])

  useEffect(() => {
    return () => {
      clearTimeout(autoAdvanceRef.current)
      ttsCacheRef.current.forEach(entry => { if (entry.url) URL.revokeObjectURL(entry.url) })
      ttsCacheRef.current.clear()
    }
  }, [])

  useEffect(() => {
    setActiveReadSegment(null)
    setTtsActive(false)
    savedHighlightSelectionRef.current = null
    clearTimeout(autoHighlightTimerRef.current)
    if (!readThrough) setActiveReadSegment(null)
    clearTimeout(autoAdvanceRef.current)
  }, [currentIdx, readThrough])

  useEffect(() => {
    if (!quizMode || !questions.length || !voices.length) return
    for (let index = currentIdx; index <= Math.min(currentIdx + TTS_PRELOAD_AHEAD, questions.length - 1); index += 1) {
      const q = questions[index]
      if (!q) return
      fetchTtsAudio(buildQuestionSpeechText(q, index), selectedVoice).catch(() => {})
    }
  }, [quizMode, questions, currentIdx, selectedVoice, voices.length, fetchTtsAudio])

  const resumeQuiz = useCallback(async (saved, availableVoices = []) => {
    const savedMode = saved.mode || saved.quizMode
    const mode = savedMode === 'exam' ? 'exam' : 'study'
    const savedIdx = saved.current_idx ?? saved.currentIdx ?? 0
    const savedAnswers = saved.answers || {}

    const aid = saved.attempt_id || saved.attemptId || ''
    // For feedback modes, load quiz with correct answers BEFORE showing.
    if (mode === 'study') {
      try {
        const quizRes = await api.get(`/quizzes/${id}?study=true${aid ? `&attempt_id=${aid}` : ''}`)
        setQuiz(quizRes.data)
      } catch (err) {
        console.error('Failed to load quiz:', err)
        navigate('/')
        return
      }
    } else {
      // For exam mode, ensure quiz data is loaded
      try {
        const quizRes = await api.get(`/quizzes/${id}${aid ? `?attempt_id=${aid}` : ''}`)
        setQuiz(quizRes.data)
      } catch (err) {
        console.error('Failed to load quiz:', err)
        navigate('/')
        return
      }
    }

    hasStarted.current = true
    setQuizMode(mode)
    setAnswers(savedAnswers)
    setCurrentIdx(savedIdx)
    setAttemptId(saved.attempt_id || saved.attemptId)
    if (saved.voice && availableVoices.some(v => v.id === saved.voice)) setSelectedVoice(saved.voice)
    if (saved.started_at) setStartedAt(saved.started_at)
    // Restore timer — calculate remaining from started_at + total_time
    if (saved.total_time && saved.started_at) {
      const elapsed = Math.floor((new Date() - new Date(saved.started_at)) / 1000)
      const remaining = Math.max(0, saved.total_time - elapsed)
      setTimeLeft(remaining)
      setTotalTime(saved.total_time)
    }
  }, [id])

  // Warn before tab/window close when mid-quiz
  useEffect(() => {
    if (!attemptId) return
    const msg = timeLeft !== null
      ? 'Your quiz is timed. If you close this tab, the timer will continue and auto-submit when time expires. Progress is saved.'
      : 'You have an in-progress quiz. Your progress is saved.'
    const handler = (e) => { e.preventDefault(); e.returnValue = msg }
    window.addEventListener('beforeunload', handler)
    return () => window.removeEventListener('beforeunload', handler)
  }, [attemptId, timeLeft])

  useEffect(() => {
    const load = async () => {
      try {
        const [quizRes, voicesRes, favoritesRes] = await Promise.all([
          api.get(`/quizzes/${id}`),
          api.get('/tts/voices').catch(() => ({ data: [] })),
          api.get('/favorites').catch(() => ({ data: [] })),
        ])
        setQuiz(quizRes.data)
        setVoices(voicesRes.data)
        setFavorites(favoritesRes.data)
        const def = voicesRes.data.find(v => v.is_default)
        if (def) setSelectedVoice(def.id)

        // Check for saved progress and auto-resume
        try {
          const progressRes = await api.get('/attempts/progress', {
            params: { quiz_id: id },
            headers: { 'x-quiz-session': SESSION_ID },
          })
          if (progressRes.data) {
            await resumeQuiz(progressRes.data, voicesRes.data)
          }
        } catch (err) {
          if (err.response?.status === 409) {
            showToast(err.response.data.detail || 'Quiz active on another device')
          }
        }
      } catch { navigate('/') }
      finally { setLoading(false) }
    }
    load()
    return () => clearInterval(timerRef.current)
  }, [id])

  const startQuiz = async (mode, voice, timerMinutes = null) => {
    if (hasStarted.current) return
    hasStarted.current = true
    setSelectedVoice(voice)
    setStarting(true)
    // DON'T set quizMode yet — wait for data to load first
    // (prevents mobile race condition where quiz renders without correct_answer)

    try {
      // Start attempt first (may select random question subset)
      const attemptRes = await api.post(`/attempts/start?quiz_id=${id}`)
      setAttemptId(attemptRes.data.id)
      const aid = attemptRes.data.id

      // Fetch quiz with attempt_id for question pool filtering
      let quizData = quiz
      const studyParam = mode === 'study' ? '&study=true' : ''
      const quizRes = await api.get(`/quizzes/${id}?attempt_id=${aid}${studyParam}`)
      quizData = quizRes.data
      setQuiz(quizData)
      const mins = timerMinutes || quizData.time_limit_minutes
      const now = new Date().toISOString()
      setStartedAt(now)
      if (mode === 'exam' && mins) {
        const secs = mins * 60
        setTimeLeft(secs); setTotalTime(secs)
      }
      // NOW set mode — quiz data is fully loaded, safe to render
      setQuizMode(mode)
      api.post('/attempts/progress', {
        quiz_id: parseInt(id),
        attempt_id: aid,
        answers: {},
        current_idx: 0,
        mode,
        voice: voice || null,
        time_left: mode === 'exam' && mins ? mins * 60 : null,
        started_at: now,
        total_time: mode === 'exam' && mins ? mins * 60 : null,
      }, { headers: { 'x-quiz-session': SESSION_ID } }).catch(() => {})
    } catch { navigate('/') }
    finally { setStarting(false) }
  }

const timerStarted = timeLeft !== null
  useEffect(() => {
    if (!timerStarted) return
    timerRef.current = setInterval(() => {
      setTimeLeft(t => { if (t <= 1) { clearInterval(timerRef.current); return 0 } return t - 1 })
    }, 1000)
    return () => clearInterval(timerRef.current)
  }, [timerStarted])

  // Auto-submit when timer reaches zero
  useEffect(() => {
    if (timeLeft === 0) handleSubmit(true)
  }, [timeLeft])

  const saveProgressNow = useCallback((overrides = {}) => {
    if (!attemptId || !quizMode) return Promise.resolve()
    return api.post('/attempts/progress', {
      quiz_id: parseInt(id),
      attempt_id: attemptId,
      answers,
      current_idx: currentIdx,
      mode: quizMode,
      voice: selectedVoice || null,
      time_left: timeLeft,
      started_at: startedAt,
      total_time: totalTime,
      ...overrides,
    }, { headers: { 'x-quiz-session': SESSION_ID } }).catch(() => {})
  }, [id, answers, currentIdx, attemptId, quizMode, selectedVoice, timeLeft, startedAt, totalTime])

  // Save progress to Redis (survives logout/browser change)
  const saveProgressRef = useRef(null)
  useEffect(() => {
    if (!attemptId || !quizMode) return
    clearTimeout(saveProgressRef.current)
    saveProgressRef.current = setTimeout(() => { saveProgressNow() }, 500)
    return () => clearTimeout(saveProgressRef.current)
  }, [saveProgressNow, attemptId, quizMode])

  useEffect(() => {
    if (!attemptId || !quizMode) return
    const flush = () => { saveProgressNow() }
    const flushWhenHidden = () => { if (document.visibilityState === 'hidden') flush() }
    window.addEventListener('pagehide', flush)
    document.addEventListener('visibilitychange', flushWhenHidden)
    return () => {
      window.removeEventListener('pagehide', flush)
      document.removeEventListener('visibilitychange', flushWhenHidden)
    }
  }, [attemptId, quizMode, saveProgressNow])

  const setAnswer = (questionId, value) => setAnswers(prev => ({ ...prev, [questionId]: value }))

  const clearCurrentHighlights = () => {
    if (!current || !manualHighlights[current.id]) return
    setManualHighlights(prev => {
      const next = { ...prev }
      delete next[current.id]
      return next
    })
    showToast('Question highlights cleared')
  }

  const removeJoinedHighlight = (textId, offset) => {
    if (!current) return
    const [questionKey, fieldKey] = textId.split('::')
    if (questionKey !== String(current.id)) return
    const existing = manualHighlights[current.id]?.[fieldKey] || []
    const joined = existing.find(range => offset >= range.start && offset < range.end)
    if (!joined) return
    setManualHighlights(prev => {
      const questionRanges = prev[current.id] || {}
      const nextRanges = (questionRanges[fieldKey] || []).filter(range => range.start !== joined.start || range.end !== joined.end)
      const nextQuestion = { ...questionRanges, [fieldKey]: nextRanges }
      if (!nextRanges.length) delete nextQuestion[fieldKey]
      const next = { ...prev, [current.id]: nextQuestion }
      if (!Object.keys(nextQuestion).length) delete next[current.id]
      return next
    })
    showToast('Highlight removed')
  }

  const highlightsFor = (fieldKey) => manualHighlights[current?.id]?.[fieldKey] || []

  const hasActiveTextSelection = () => Boolean(window.getSelection?.().toString().trim())

  const safeNavigate = (targetIdx, { keepReadThrough = false } = {}) => {
    if (!keepReadThrough) setReadThrough(false)
    setCurrentIdx(targetIdx)
  }

  const handleSubmit = useCallback(async (autoSubmit = false) => {
    if (!attemptId || submitting) return
    if (!autoSubmit && Object.keys(answers).length === 0) {
      showToast('No answers selected — submitting with 0 answered.')
      await new Promise(r => setTimeout(r, 1200))
    }
    clearInterval(timerRef.current)
    api.delete(`/attempts/progress/${attemptId}`).catch(() => {})
    setSubmitting(true)
    try {
      const submission = {
        answers: Object.entries(answers).map(([qid, answer]) => ({
          question_id: parseInt(qid), user_answer: answer,
        })),
      }
      const res = await api.post(`/attempts/${attemptId}/submit`, submission)
      if (returnTo) {
        navigate(`/results/${attemptId}?return_to=${encodeURIComponent(returnTo)}`, { state: { result: res.data } })
      } else {
        navigate(`/results/${attemptId}`, { state: { result: res.data } })
      }
    } catch (err) {
      if (!autoSubmit) showToast(err.response?.data?.detail || 'Submission failed')
    } finally { setSubmitting(false) }
  }, [attemptId, answers, submitting, navigate, showToast])

  if (loading) return <div className="loading"><div className="spinner"></div> Loading quiz...</div>
  if (!quiz) return null

  if (!quizMode) return (
    <div>
      {isModerator && (
        <div style={{ textAlign: 'right', marginBottom: 8, display: 'flex', gap: 8, justifyContent: 'flex-end', flexWrap: 'wrap' }}>
          <Link to={`/quizzes/${id}/edit`} className="btn btn-secondary btn-sm">✏️ Edit Questions</Link>
        </div>
      )}
      {starting ? (
        <div style={{ textAlign: 'center', padding: '60px 0' }}>
          <div className="spinner" style={{ margin: '0 auto 16px' }} />
          <div style={{ color: 'var(--text-muted)', fontSize: '0.95rem' }}>Loading quiz…</div>
        </div>
      ) : returnTo ? (
        <CourseQuizStart quiz={quiz} onStart={startQuiz} />
      ) : (
        <ModeSelectScreen quiz={quiz} voices={voices} onStart={startQuiz} />
      )}
    </div>
  )

  const answeredCount = Object.keys(answers).length
  const totalCount = questions.length
  const isLast = currentIdx === totalCount - 1
  const quizNavigation = (position = 'bottom') => (
    <div className={`quiz-nav-controls quiz-nav-controls-${position}`}>
      <button className="btn btn-secondary"
        onClick={() => safeNavigate(Math.max(0, currentIdx - 1))}
        disabled={currentIdx === 0}>← Prev</button>

      <button className="quiz-nav-toggle btn btn-secondary btn-sm"
        onClick={() => setNavOpen(v => !v)}>
        {currentIdx + 1} / {totalCount} {navOpen ? '▼' : '▲'}
      </button>

      {isLast ? (
        <button className="btn btn-primary" onClick={() => handleSubmit(false)} disabled={submitting}>
          {submitting ? 'Submitting...' : 'Submit Quiz'}
        </button>
      ) : (
        <button className="btn btn-primary" onClick={() => safeNavigate(Math.min(totalCount - 1, currentIdx + 1))}>Next →</button>
      )}
    </div>
  )
  const activeReadForCurrent = current && activeReadSegment?.questionId === current.id
  const questionSpeechRange = current
    ? getSpeechChunkRange(
      questionStem(current),
      QUESTION_HIGHLIGHT_WORDS,
      activeReadForCurrent && activeReadSegment.type === 'question' ? activeReadSegment.chunkIndex : null,
    )
    : null

  const toggleFavorite = async (questionId) => {
    const isFavorited = favorites.includes(questionId)
    try {
      if (isFavorited) {
        await api.delete(`/favorites/${questionId}`)
        setFavorites(prev => prev.filter(id => id !== questionId))
        showToast('Removed from favorites')
      } else {
        await api.post('/favorites', { question_id: questionId })
        setFavorites(prev => [...prev, questionId])
        showToast('Added to favorites')
      }
    } catch (err) {
      showToast(err.response?.data?.detail || 'Failed to update favorite')
    }
  }

  const QuestionDot = ({ q, i }) => {
    const isActive = i === currentIdx
    const isDone = !!answers[q.id]
    return (
      <button key={q.id} onClick={() => { safeNavigate(i); setNavOpen(false) }} style={{
        width: 34, height: 34, borderRadius: '50%', border: isActive ? '2px solid var(--primary)' : 'none',
        cursor: 'pointer', fontSize: '0.78rem', fontWeight: 600, flexShrink: 0,
        background: isActive ? 'var(--primary)' : isDone ? 'var(--correct-bg)' : 'var(--border)',
        color: isActive ? 'white' : isDone ? 'var(--correct-fg)' : 'var(--text-muted)',
        transition: 'background 0.1s',
      }}>{i + 1}</button>
    )
  }

  return (
    <div className="quiz-bottom">
      {/* In-app leave confirmation */}
      {leaveTarget && (
        <div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.6)', zIndex: 1000, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 16 }}>
          <div style={{ background: 'var(--card-bg)', borderRadius: 14, padding: 28, maxWidth: 420, width: '100%', textAlign: 'center', boxShadow: '0 20px 60px rgba(0,0,0,0.3)' }}>
            <div style={{ fontSize: '2rem', marginBottom: 12 }}>⏸</div>
            <h2 style={{ marginBottom: 8 }}>Suspend quiz?</h2>
            <p style={{ color: 'var(--text-muted)', fontSize: '0.875rem', marginBottom: 20 }}>
              Your progress is saved — you can resume from where you left off.
              {timeLeft !== null && (
                <><br/><br/>
                <strong style={{ color: '#16a34a' }}>Timer will pause</strong> while you are away and resume when you return. <br/>
                <span style={{ fontSize: '0.8rem' }}>
                  Note: closing the tab without suspending leaves the timer running, and it may auto-submit if time runs out.
                </span>
                </>
              )}
            </p>
            <div style={{ display: 'flex', gap: 10, justifyContent: 'center' }}>
              <button className="btn btn-secondary" onClick={() => setLeaveTarget(null)}>Stay</button>
              <button className="btn btn-primary" onClick={async () => {
                // Save with suspended=true so the timer pauses on the server side
                if (attemptId && quizMode) {
                  try {
                    await api.post('/attempts/progress', {
                      quiz_id: parseInt(id),
                      attempt_id: attemptId,
                      answers,
                      current_idx: currentIdx,
                      mode: quizMode,
                      voice: selectedVoice || null,
                      time_left: timeLeft,
                      started_at: startedAt,
                      total_time: totalTime,
                      suspended: true,
                    }, { headers: { 'x-quiz-session': SESSION_ID } })
                  } catch { }
                }
                const target = leaveTarget
                setLeaveTarget(null)
                navigate(target)
              }}>Suspend &amp; Leave</button>
            </div>
          </div>
        </div>
      )}
      {toast && (
        <div style={{
          position: 'fixed', bottom: 24, left: '50%', transform: 'translateX(-50%)',
          background: 'rgba(15,23,42,0.92)', color: '#f1f5f9',
          padding: '10px 20px', borderRadius: 24, fontSize: '0.85rem',
          zIndex: 500, whiteSpace: 'nowrap', pointerEvents: 'none',
          animation: 'fadeInUp 0.2s ease',
        }}>
          {toast}
        </div>
      )}

      {/* Header */}
      <div className="card quiz-header-card" style={{ marginBottom: 14 }}>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 8 }}>
          <div>
            <h2 className="quiz-header-title">{quiz.title}</h2>
            <div style={{ fontSize: '0.82rem', color: 'var(--text-muted)', display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
              <span style={{
                background: isStudy ? '#d1fae5' : '#e0e7ff',
                color: isStudy ? '#065f46' : '#3730a3',
                padding: '1px 8px', borderRadius: 12, fontWeight: 600,
              }}>
                {isStudy ? '📖 Study' : '🎯 Exam'}
              </span>
              <span>Q {currentIdx + 1} / {totalCount}</span>
              <span style={{ color: 'var(--text-subtle)' }}>{answeredCount} answered</span>
            </div>
          </div>
          <div style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
            {timeLeft !== null && <TimerDisplay seconds={timeLeft} total={totalTime} />}
            <button className="btn btn-secondary btn-sm" onClick={() => setLeaveTarget(returnTo || '/')} title="Save progress and exit">
              ⏸ Suspend
            </button>
            {isModerator && <Link to={`/quizzes/${id}/edit`} className="btn btn-secondary btn-sm">✏️ Edit</Link>}
          </div>
        </div>
        {voices.length > 1 && (
          <div style={{ marginTop: 8, paddingTop: 8, borderTop: '1px solid var(--border)', display: 'flex', alignItems: 'center', gap: 8 }}>
            <label style={{ fontSize: '0.75rem', color: 'var(--text-muted)', whiteSpace: 'nowrap' }}>🔊</label>
            <select value={selectedVoice} onChange={e => setSelectedVoice(e.target.value)}
              disabled={ttsActive}
              style={{ padding: '3px 8px', borderRadius: 6, border: '1px solid var(--border)', fontSize: '0.78rem', opacity: ttsActive ? 0.5 : 1, background: 'var(--input-bg)', color: 'var(--text)' }}>
              {voices.map(v => <option key={v.id} value={v.id}>{v.name}</option>)}
            </select>
          </div>
        )}
      </div>

      <div className="progress-bar" style={{ marginBottom: 16 }}>
        <div className="fill" style={{ width: `${((currentIdx + 1) / totalCount) * 100}%` }} />
      </div>

      {quizNavigation('top')}

      {/* Two-column layout: content + desktop sidebar */}
      <div className="quiz-layout">
        {/* Main content */}
        <div style={{ flex: 1, minWidth: 0 }}>
          {current && (
            <div className="question-card" style={{
              boxShadow: activeReadForCurrent ? '0 0 0 3px rgba(59, 130, 246, 0.22)' : undefined,
              borderColor: activeReadForCurrent ? '#60a5fa' : undefined,
            }}>
              <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 8, gap: 12 }}>
                <h3 style={{ marginBottom: 0, flex: 1 }}>
                  Q{currentIdx + 1}.{' '}
                  <ManualHighlightText
                    text={questionStem(current)}
                    textId={`${current.id}::question`}
                    highlights={highlightsFor('question')}
                    speechRange={questionSpeechRange}
                    onRemoveHighlight={removeJoinedHighlight}
                  />
                </h3>
                <button
                  onClick={() => toggleFavorite(current.id)}
                  title={favorites.includes(current.id) ? 'Remove from favorites' : 'Add to favorites'}
                  style={{
                    background: 'none',
                    border: 'none',
                    cursor: 'pointer',
                    fontSize: '1.4rem',
                    padding: 4,
                    lineHeight: 1,
                    transition: 'transform 0.1s',
                  }}
                  onMouseEnter={e => e.currentTarget.style.transform = 'scale(1.15)'}
                  onMouseLeave={e => e.currentTarget.style.transform = 'none'}
                >
                  {favorites.includes(current.id) ? '⭐' : '☆'}
                </button>
              </div>
              <div style={{ marginBottom: 10, display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center' }}>
                {voices.length > 0 && (
                  <TTSButton
                    key={`${current.id}_${selectedVoice || 'default'}`}
                    text={buildQuestionSpeechText(current, currentIdx)}
                    voice={selectedVoice}
                    segments={getQuestionSpeechSegments(current, currentIdx)}
                    getAudio={fetchTtsAudio}
                    autoPlay={readThrough}
                    onEnded={() => {
                      if (readThrough) {
                        if (currentIdx < totalCount - 1) {
                          safeNavigate(currentIdx + 1, { keepReadThrough: true })
                        } else {
                          setReadThrough(false)
                        }
                        return
                      }
                    }}
                    onActiveChange={setTtsActive}
                    onSegmentChange={segment => setActiveReadSegment(segment === null ? null : { questionId: current.id, ...segment })}
                  />
                )}
                {voices.length > 0 && (
                  <button
                    className={`btn btn-sm ${readThrough ? 'btn-primary' : 'btn-secondary'}`}
                    onClick={() => setReadThrough(v => !v)}
                    title="Read each question aloud and advance automatically"
                  >
                    {readThrough ? 'Stop listen-through' : 'Listen through'}
                  </button>
                )}
                <div className="manual-highlight-toolbar" aria-label="Question highlight tools">
                  <button className="btn btn-secondary btn-sm" onMouseDown={e => e.preventDefault()} onClick={clearCurrentHighlights} disabled={!manualHighlights[current.id]} title="Clear all highlights on this question">
                    Clear
                  </button>
                </div>
              </div>
              <span className="badge" style={{ background: '#e0e7ff', color: '#3730a3', margin: '6px 0 12px', display: 'inline-block' }}>
                {current.question_type === 'mcq' ? 'Multiple Choice' : current.question_type === 'true_false' ? 'True / False' : 'Fill in the Blank'}
              </span>
              {current.image_path && (
                <button className="question-image-preview" onClick={() => { setImageZoom(1); setExpandedImagePath(current.image_path) }} title="Expand image" type="button">
                  <img src={`/uploads/${current.image_path}`} alt="Question illustration"
                    onError={e => e.currentTarget.closest('button').style.display = 'none'} />
                </button>
              )}
              {expandedImagePath && (
                <div className="image-lightbox" role="dialog" aria-modal="true" aria-label="Expanded question image" onClick={() => setExpandedImagePath('')}>
                  <div className="image-lightbox-controls" onClick={e => e.stopPropagation()}>
                    <button type="button" onClick={() => setImageZoom(z => Math.max(1, z - 0.25))} disabled={imageZoom <= 1}>-</button>
                    <span>{Math.round(imageZoom * 100)}%</span>
                    <button type="button" onClick={() => setImageZoom(z => Math.min(4, z + 0.25))} disabled={imageZoom >= 4}>+</button>
                    <button type="button" onClick={() => setImageZoom(1)} disabled={imageZoom === 1}>Reset</button>
                  </div>
                  <button className="image-lightbox-close" onClick={() => setExpandedImagePath('')} type="button" aria-label="Close expanded image">×</button>
                  <div className="image-lightbox-viewport" onClick={e => e.stopPropagation()}>
                    <img
                      src={`/uploads/${expandedImagePath}`}
                      alt="Expanded question illustration"
                      style={{
                        maxWidth: imageZoom === 1 ? 'min(100%, 1100px)' : 'none',
                        maxHeight: imageZoom === 1 ? '92vh' : 'none',
                        width: imageZoom > 1 ? `${imageZoom * 100}%` : undefined,
                      }}
                    />
                  </div>
                </div>
              )}
              {(current.question_type === 'mcq' || current.question_type === 'true_false') && current.options ? (
                <div className="options" style={{ marginTop: 8 }}>
                  {current.options.map((opt, i) => {
                    const isSelected = answers[current.id] === opt
                    const hasAnswered = isStudy && !!answers[current.id]
                    const isCorrectOpt = opt.trim().toLowerCase() === (current.correct_answer || '').trim().toLowerCase()
                    const showCorrect = hasAnswered && isCorrectOpt
                    const showWrong = hasAnswered && isSelected && !isCorrectOpt
                    const letter = String.fromCharCode(65 + i)
                    const activeOptionChunk = activeReadForCurrent && activeReadSegment.type === 'option' && activeReadSegment.index === i
                      ? activeReadSegment.chunkIndex
                      : null
                    const optionSpeechRange = getSpeechChunkRange(opt, OPTION_HIGHLIGHT_WORDS, activeOptionChunk)
                    const optionFieldKey = `option-${i}`
                    return (
                      <div key={i}
                        className={`option ${isSelected && !hasAnswered ? 'selected' : ''} ${showCorrect ? 'correct' : ''} ${showWrong ? 'incorrect' : ''}`}
                        onClick={() => !hasAnswered && !hasActiveTextSelection() && setAnswer(current.id, opt)}
                        style={{
                          cursor: hasAnswered ? 'default' : 'pointer',
                          borderColor: activeOptionChunk !== null ? '#60a5fa' : undefined,
                          boxShadow: activeOptionChunk !== null ? '0 0 0 3px rgba(59, 130, 246, 0.2)' : undefined,
                          transition: 'background 0.15s ease, box-shadow 0.15s ease',
                        }}>
                        <span className="option-letter">{letter}</span>
                        <span style={{ flex: 1 }}>
                          <ManualHighlightText
                            text={opt}
                            textId={`${current.id}::${optionFieldKey}`}
                            highlights={highlightsFor(optionFieldKey)}
                            speechRange={optionSpeechRange}
                            onRemoveHighlight={removeJoinedHighlight}
                          />
                        </span>
                        {showCorrect && <span style={{ marginLeft: 'auto', fontSize: '0.8rem', fontWeight: 700, color: 'var(--correct-fg)' }}>✓ Correct</span>}
                        {showWrong && <span style={{ marginLeft: 'auto', fontSize: '0.8rem', fontWeight: 700, color: 'var(--wrong-fg)' }}>✗ Wrong</span>}
                      </div>
                    )
                  })}
                </div>
              ) : (
                <input type="text" placeholder="Type your answer..."
                  value={answers[current.id] || ''}
                  onChange={e => setAnswer(current.id, e.target.value)}
                  style={{ marginTop: 10, width: '100%', padding: '10px 14px', border: '1px solid var(--border)', borderRadius: 8, background: 'var(--input-bg)', color: 'var(--text)' }} />
              )}
              {isStudy && answers[current.id] && (
                <>
                  {current.explanation && (
                    <div className="explanation" style={{ marginTop: 16, whiteSpace: 'pre-line' }}>
                      <strong>Explanation:</strong> {current.explanation}
                    </div>
                  )}
                  {current.question_type === 'fill_blank' && (
                    <div className="explanation" style={{ marginTop: 12, borderLeftColor: '#22c55e' }}>
                      <strong>Correct Answer:</strong> {current.correct_answer}
                    </div>
                  )}
                </>
              )}
            </div>
          )}

          {quizNavigation('bottom')}

          {/* Mobile: collapsible number grid */}
          {navOpen && (
            <div className="quiz-nav-mobile-grid">
              {questions.map((q, i) => <QuestionDot key={q.id} q={q} i={i} />)}
            </div>
          )}

          {answeredCount > 0 && !isLast && (
            <div style={{ textAlign: 'center', marginTop: 14 }}>
              <button className="btn btn-secondary btn-sm" onClick={() => handleSubmit(false)} disabled={submitting}>
                Submit now ({answeredCount}/{totalCount} answered)
              </button>
            </div>
          )}
        </div>

        {/* Desktop sidebar */}
        <div className="quiz-sidebar">
          <div style={{ fontWeight: 700, fontSize: '0.78rem', textTransform: 'uppercase', letterSpacing: '0.05em', color: 'var(--text-muted)', marginBottom: 12 }}>
            Questions
          </div>
          <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
            {questions.map((q, i) => <QuestionDot key={q.id} q={q} i={i} />)}
          </div>
          <div style={{ marginTop: 16, paddingTop: 12, borderTop: '1px solid var(--border)', fontSize: '0.78rem', color: 'var(--text-muted)' }}>
            <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
              <span><span style={{ display: 'inline-block', width: 10, height: 10, borderRadius: '50%', background: 'var(--correct-bg)', border: '1px solid var(--correct-bd)', marginRight: 4 }} />{answeredCount} answered</span>
              <span><span style={{ display: 'inline-block', width: 10, height: 10, borderRadius: '50%', background: 'var(--border)', marginRight: 4 }} />{totalCount - answeredCount} remaining</span>
            </div>
            {answeredCount > 0 && !isLast && (
              <button className="btn btn-primary btn-sm" style={{ marginTop: 10, width: '100%' }}
                onClick={() => handleSubmit(false)} disabled={submitting}>
                {submitting ? 'Submitting...' : 'Submit Quiz'}
              </button>
            )}
          </div>
        </div>
      </div>

      {/* AI tutor — only in study mode, lazy-loaded */}
      {isStudy && current && (
        <Suspense fallback={null}>
          <TeachChat question={current} />
        </Suspense>
      )}
    </div>
  )
}
