// Guide reader + autosave editor, the two flashcard modes (Cram + FSRS
// long-term), the quiz screen, the New-guide dialog, and the guide AI
// panel. Extracted from app.jsx in Wave 6.
//
// Exports (via Object.assign(window, { ... }) at bottom):
//   GuideScreen, FlashcardScreen, CramScreen, LongTermScreen, QuizScreen,
//   NewGuideDialog, AutoTextarea, GuideAIPanel
//
// File-private (not exported):
//   _jspdfPromise, _loadJsPdf, downloadGuidePdf,
//   CRAM_DURATIONS, FSRS_RATING_LABELS
//
// External deps consumed (must be window.* by the time these render):
//   from data modules: useSubjects, useGuides, findGuide, findGuideSync,
//     updateGuide, deleteGuide, useReviewsForGuide, loadReviewsForGuide,
//     upsertReview, buildDueQueue, FSRS (global scheduler obj),
//     generateFlashcardsForGuide, stageGeneration, newGuideId,
//     addDocumentsBulk, fileKindFromExt, parseFile
//   from helpers.jsx: Page, ModalShell, ModalHead, navigate, relativeTime,
//     AiGlyph, formatBytes, formatChars
//   from Wave 3 promotion: SubjectDialog, UploadRow, ModeBtn
//   from app.jsx (still top-level): AIChatPanel (used by GuideAIPanel)

(function () {

// Lazy-load jsPDF (only when the user actually clicks Download PDF). Tries
// multiple CDNs since some host paths return 404 for newer versions.
let _jspdfPromise = null;
function _loadJsPdf() {
  if (window.jspdf?.jsPDF) return Promise.resolve(window.jspdf.jsPDF);
  if (_jspdfPromise) return _jspdfPromise;
  const urls = [
    'https://cdn.jsdelivr.net/npm/jspdf@2.5.1/dist/jspdf.umd.min.js',
    'https://unpkg.com/jspdf@2.5.1/dist/jspdf.umd.min.js',
    'https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js',
  ];
  const tryOne = (url) => new Promise((resolve, reject) => {
    const s = document.createElement('script');
    s.src = url;
    s.onload = () => {
      if (window.jspdf?.jsPDF) resolve(window.jspdf.jsPDF);
      else reject(new Error(`${url} loaded but jspdf.jsPDF missing`));
    };
    s.onerror = () => reject(new Error(`Failed to fetch ${url}`));
    document.head.appendChild(s);
  });
  _jspdfPromise = (async () => {
    let lastErr;
    for (const u of urls) {
      try { return await tryOne(u); }
      catch (e) { lastErr = e; console.warn('[jsPDF]', e.message); }
    }
    throw lastErr || new Error('All CDNs failed');
  })();
  return _jspdfPromise;
}

// Build and trigger a PDF download for a study guide. Programmatic text
// layout (no canvas rasterization) so the resulting PDF is small and
// text-selectable.
async function downloadGuidePdf({ title, subjectName, subjectHint, bigIdea, sections = [], flashcards = [], quiz = [] }) {
  let JsPDF;
  try { JsPDF = await _loadJsPdf(); }
  catch (e) { alert('Could not load PDF library: ' + e.message); return; }

  const doc = new JsPDF({ unit: 'pt', format: 'letter' });
  const PAGE_W = 612;
  const PAGE_H = 792;
  const M = 54;                          // 0.75in margin
  const CONTENT_W = PAGE_W - 2 * M;
  let y = M;

  const ensureSpace = (h) => {
    if (y + h > PAGE_H - M) { doc.addPage(); y = M; }
  };
  const setFont = (size, weight) => {
    doc.setFontSize(size);
    doc.setFont('helvetica', weight === 'bold' ? 'bold' : (weight === 'italic' ? 'italic' : 'normal'));
  };
  // Lay out a block of text at the current cursor, wrapping to CONTENT_W.
  // Advances y. Adds page breaks as needed. trailing = extra space after.
  const writeBlock = (text, { size = 11, weight = 'normal', indent = 0, trailing = 4, color = [0, 0, 0] } = {}) => {
    if (!text && text !== 0) return;
    setFont(size, weight);
    doc.setTextColor(color[0], color[1], color[2]);
    const lines = doc.splitTextToSize(String(text), CONTENT_W - indent);
    const lineHeight = size * 1.35;
    for (const line of lines) {
      ensureSpace(lineHeight);
      doc.text(line, M + indent, y + size * 0.85);
      y += lineHeight;
    }
    y += trailing;
  };

  // — Header —
  if (subjectName || subjectHint) {
    writeBlock(`${subjectName || ''}${subjectHint ? ' · ' + subjectHint : ''}`.toUpperCase(), {
      size: 9, color: [110, 110, 110], trailing: 6,
    });
  }
  writeBlock(title || 'Untitled guide', { size: 22, weight: 'bold', trailing: 14 });

  // — Big idea —
  if (bigIdea) {
    writeBlock('SUMMARY', { size: 9, weight: 'bold', color: [110, 110, 110], trailing: 2 });
    writeBlock(bigIdea, { size: 11, weight: 'italic', trailing: 16 });
  }

  // — Sections —
  for (let si = 0; si < sections.length; si++) {
    const s = sections[si];
    const num = String(s.number || si + 1).padStart(2, '0');
    ensureSpace(40);
    writeBlock(`${num}. ${s.title || '(untitled section)'}`, {
      size: 15, weight: 'bold', trailing: 8,
    });
    if (s.body) {
      const paras = String(s.body).split(/\n\s*\n/).map((p) => p.trim()).filter(Boolean);
      for (const p of paras) writeBlock(p, { size: 11, trailing: 8 });
    }
    if ((s.terms || []).length > 0) {
      writeBlock('Key terms', { size: 12, weight: 'bold', trailing: 4 });
      for (const t of s.terms) {
        writeBlock(t.term || '', { size: 11, weight: 'bold', trailing: 1 });
        writeBlock(t.def || '', { size: 11, indent: 14, trailing: 6 });
      }
    }
    y += 6;
  }

  // — Flashcards —
  if (flashcards.length > 0) {
    ensureSpace(40);
    writeBlock(`Flashcards · ${flashcards.length}`, { size: 15, weight: 'bold', trailing: 8 });
    flashcards.forEach((card) => {
      writeBlock(`Q. ${card.front || ''}`, { size: 11, weight: 'bold', trailing: 1 });
      writeBlock(`A. ${card.back || ''}`,  { size: 11, trailing: 8 });
    });
    y += 4;
  }

  // — Quiz —
  if (quiz.length > 0) {
    ensureSpace(40);
    writeBlock(`Quiz · ${quiz.length}`, { size: 15, weight: 'bold', trailing: 8 });
    quiz.forEach((q, i) => {
      writeBlock(`${i + 1}. ${q.question || ''}`, { size: 11, weight: 'bold', trailing: 2 });
      (q.options || []).forEach((opt, j) => {
        const correct = j === q.correctIndex;
        const marker = String.fromCharCode(65 + j);
        writeBlock(`${marker}. ${opt || ''}${correct ? '   ✓' : ''}`, {
          size: 11, weight: correct ? 'bold' : 'normal', indent: 14, trailing: 1,
        });
      });
      if (q.hint) {
        writeBlock(`Hint: ${q.hint}`, { size: 10, weight: 'italic', indent: 14, color: [110, 110, 110], trailing: 8 });
      } else {
        y += 6;
      }
    });
  }

  const safe = (title || 'study-guide').replace(/[^\w\s\-]/g, '').trim().replace(/\s+/g, '-') || 'study-guide';
  doc.save(`${safe}.pdf`);
}

// ─── Guide reader ──────────────────────────────────────────────────────────

function GuideScreen({ subjectId, guideId }) {
  const subjects = useSubjects();
  const subj = subjects.find((s) => s.id === subjectId) || { id: subjectId, name: '(unknown subject)', emoji: '📄' };
  const [guide, setGuide] = React.useState(() => findGuideSync(guideId));
  const [tried, setTried] = React.useState(!!findGuideSync(guideId));
  const [aiPanelOpen, setAiPanelOpen] = React.useState(false);

  React.useEffect(() => {
    if (tried) return;
    let cancelled = false;
    findGuide(guideId)
      .then((g) => {
        if (cancelled) return;
        setGuide(g);
        setTried(true);
      })
      .catch((e) => {
        if (cancelled) return;
        console.error('findGuide failed', e);
        setTried(true);
      });
    return () => { cancelled = true; };
  }, [guideId, tried]);

  // ── Editable working copy + autosave (debounced 1s) ──────────────────────
  const [working, setWorking] = React.useState(null);
  React.useEffect(() => {
    if (guide && !working) {
      setWorking({
        title: guide.title || '',
        content: guide.content
          ? JSON.parse(JSON.stringify(guide.content))
          : { sections: [], flashcards: [], quiz: [] },
      });
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [guide]);

  const [saveState, setSaveState] = React.useState('idle'); // idle|dirty|saving|saved|error
  const [savedAt, setSavedAt] = React.useState(null);
  const [saveError, setSaveError] = React.useState(null);
  const pendingRef = React.useRef(null);
  const timerRef = React.useRef(null);

  const flush = React.useCallback(async () => {
    if (!pendingRef.current) return;
    const patch = pendingRef.current;
    pendingRef.current = null;
    setSaveState('saving');
    try {
      const dbPatch = {};
      if (patch.title != null) dbPatch.title = patch.title;
      if (patch.content != null) {
        dbPatch.content = {
          subjectId: guide?.subjectId,
          sources: guide?.sources,
          guide: patch.content,
        };
      }
      const updated = await updateGuide(guideId, dbPatch);
      // Only swap guide state when the update returned a row. updateGuide
      // returns null on transient cache miss + fetch fail; setGuide(null)
      // would blank the editor while `working` still has unsaved edits, and
      // the next flush would persist subjectId: undefined and corrupt content.
      if (updated) setGuide(updated);
      setSaveState('saved');
      setSavedAt(new Date());
      setSaveError(null);
    } catch (e) {
      console.error('guide save failed', e);
      setSaveError(e.message || String(e));
      setSaveState('error');
    }
  }, [guide, guideId]);

  const queueSave = React.useCallback((patch) => {
    pendingRef.current = { ...(pendingRef.current || {}), ...patch };
    setSaveState('dirty');
    if (timerRef.current) clearTimeout(timerRef.current);
    timerRef.current = setTimeout(flush, 1000);
  }, [flush]);

  const updateContent = React.useCallback((mutator) => {
    setWorking((w) => {
      if (!w) return w;
      const nextContent = JSON.parse(JSON.stringify(w.content || {}));
      mutator(nextContent);
      queueSave({ content: nextContent });
      return { ...w, content: nextContent };
    });
  }, [queueSave]);

  const updateTitle = React.useCallback((v) => {
    setWorking((w) => w ? { ...w, title: v } : w);
    queueSave({ title: (v || '').trim() || 'Untitled guide' });
  }, [queueSave]);

  const flushRef = React.useRef(flush);
  React.useEffect(() => { flushRef.current = flush; }, [flush]);
  React.useEffect(() => {
    const onBeforeUnload = () => {
      if (timerRef.current) { clearTimeout(timerRef.current); flushRef.current(); }
    };
    window.addEventListener('beforeunload', onBeforeUnload);
    return () => {
      window.removeEventListener('beforeunload', onBeforeUnload);
      if (timerRef.current) { clearTimeout(timerRef.current); flushRef.current(); }
    };
  }, []);

  // Flashcards are now generated on demand from the guide content (the initial
  // study-guide generation produces an empty flashcards array).
  const [genCardsState, setGenCardsState] = React.useState('idle'); // idle|generating|error
  const generateFlashcards = React.useCallback(async () => {
    if (!working) return;
    const existing = (working.content?.flashcards || []).length;
    if (existing > 0 && !confirm(`This will replace the ${existing} existing card${existing === 1 ? '' : 's'} with a fresh deck. Continue?`)) {
      return;
    }
    setGenCardsState('generating');
    try {
      const subjName = subj?.name;
      const cards = await generateFlashcardsForGuide(
        { title: working.title, content: working.content },
        { subjectName: subjName }
      );
      updateContent((cc) => { cc.flashcards = cards; });
      setGenCardsState('idle');
    } catch (e) {
      console.error('flashcard generation failed', e);
      alert('Flashcard generation failed: ' + (e.message || String(e)));
      setGenCardsState('error');
    }
  }, [working, subj?.name, updateContent]);

  const guidePath = `/subject/${subjectId}/guide/${guideId}`;

  if (!tried) {
    return (
      <Page crumbs={[{ label: 'Dashboard', to: '/' }, 'Loading…']}>
        <div style={{ flex: 1, display: 'grid', placeItems: 'center', color: 'var(--ink-mute)', fontSize: 13 }}>
          <span className="pulse">Loading guide…</span>
        </div>
      </Page>
    );
  }

  if (!guide || !working) {
    return (
      <Page crumbs={[{ label: 'Dashboard', to: '/' }, 'Not found']}>
        <div style={{ padding: 32 }}>
          <div style={{ fontSize: 14 }}>Guide not found.</div>
          <button className="btn sm" style={{ marginTop: 12 }} onClick={() => navigate('/library')}>Browse all guides</button>
        </div>
      </Page>
    );
  }

  const c = working.content || {};
  const sections = c.sections || [];
  const flashcards = c.flashcards || [];
  const quiz = c.quiz || [];
  const firstQ = quiz[0];

  const statusLabel =
    saveState === 'saving' ? 'Saving…' :
    saveState === 'dirty'  ? 'Unsaved' :
    saveState === 'error'  ? 'Save failed' :
    savedAt ? `Saved ${relativeTime(savedAt.toISOString())}` :
    `Saved ${relativeTime(guide.generatedAt)}`;

  return (
    <Page
      crumbs={[
        { label: 'Dashboard', to: '/' },
        { label: subj.name, to: `/subject/${subj.id}` },
        working.title || 'Untitled guide',
      ]}
      actions={
        <>
          <span className={"save-status " + (saveState === 'saving' ? 'saving' : saveState === 'error' ? 'error' : '')}
            title={saveError || ''}>{statusLabel}</span>
          <button className={"btn sm" + (aiPanelOpen ? ' active' : '')} onClick={() => setAiPanelOpen((o) => !o)}><AiGlyph />AI</button>
          <button className="btn sm" onClick={() => downloadGuidePdf({
            title: working.title,
            subjectName: subj.name,
            subjectHint: c.subjectHint,
            bigIdea: c.bigIdea,
            sections,
            flashcards,
            quiz,
          })} title="Download as PDF">↓ PDF</button>
          {quiz.length > 0 && <button className="btn sm" onClick={() => navigate(`${guidePath}/quiz`)}>Quiz</button>}
        </>
      }
    >
      <div className="reader-grid">
        <aside className="toc">
          <div className="toc-label">Contents</div>
          {sections.map((s, i) => (
            <div key={i} className="toc-item">
              <span className="num">{String(s.number || i + 1).padStart(2, '0')}</span>
              <span style={{ flex: 1 }}>{s.title || '(untitled section)'}</span>
            </div>
          ))}
          {(flashcards.length > 0 || quiz.length > 0) && (
            <div style={{ borderTop: '1px solid var(--hairline)', margin: '12px 8px' }} />
          )}
          {flashcards.length > 0 && (
            <div className="toc-item" onClick={() => navigate(`${guidePath}/flashcards`)}>
              <span className="num">★</span><span>Flashcards · {flashcards.length}</span>
            </div>
          )}
          {quiz.length > 0 && (
            <div className="toc-item" onClick={() => navigate(`${guidePath}/quiz`)}>
              <span className="num">?</span><span>Quiz · {quiz.length}</span>
            </div>
          )}
        </aside>

        <main className="reader fade-in">
          <div className="reader-inner editable-guide">
            <div className="eyebrow">{subj.name}{c.subjectHint ? ` · ${c.subjectHint}` : ''}</div>
            <h1>
              <AutoTextarea
                className="bare-textarea"
                value={working.title}
                placeholder="Untitled guide"
                onChange={(v) => updateTitle(v)}
              />
            </h1>
            <div className="eyebrow-row">
              <span className="mono">¶ {guide.sources || 0} source{guide.sources === 1 ? '' : 's'}</span>
              <span style={{ color: 'var(--ink-whisper)' }}>·</span>
              <span>generated {relativeTime(guide.generatedAt)}</span>
            </div>

            <div className="big-idea">
              <div className="label">Summary</div>
              <div className="body">
                <AutoTextarea
                  className="bare-textarea"
                  value={c.bigIdea || ''}
                  placeholder="A one-sentence summary of the big idea…"
                  onChange={(v) => updateContent((cc) => { cc.bigIdea = v; })}
                />
              </div>
            </div>

            {sections.map((s, si) => (
              <section key={si}>
                <h2>
                  <span className="num">{String(s.number || si + 1).padStart(2, '0')}</span>
                  <input
                    className="bare-input"
                    style={{ flex: 1 }}
                    value={s.title || ''}
                    placeholder="Section title"
                    onChange={(e) => updateContent((cc) => { cc.sections[si].title = e.target.value; })}
                  />
                </h2>
                <AutoTextarea
                  className="bare-textarea reader-body"
                  value={s.body || ''}
                  placeholder="Body — separate paragraphs with a blank line."
                  onChange={(v) => updateContent((cc) => { cc.sections[si].body = v; })}
                />
                {(s.terms || []).length > 0 && <h3>Key terms</h3>}
                {(s.terms || []).map((t, ti) => (
                  <div key={ti} className="term editable-term">
                    <input
                      className="bare-input t-term"
                      value={t.term || ''}
                      placeholder="Term"
                      onChange={(e) => updateContent((cc) => { cc.sections[si].terms[ti].term = e.target.value; })}
                    />
                    <AutoTextarea
                      className="bare-textarea t-def"
                      value={t.def || ''}
                      placeholder="Definition"
                      onChange={(v) => updateContent((cc) => { cc.sections[si].terms[ti].def = v; })}
                    />
                    <button
                      type="button"
                      className="row-x"
                      title="Remove term"
                      onClick={() => updateContent((cc) => { cc.sections[si].terms.splice(ti, 1); })}
                    >×</button>
                  </div>
                ))}
                <div style={{ marginTop: 8 }}>
                  <button
                    type="button"
                    className="btn ghost sm"
                    onClick={() => updateContent((cc) => {
                      cc.sections[si].terms = cc.sections[si].terms || [];
                      cc.sections[si].terms.push({ term: '', def: '' });
                    })}
                  >＋ Add term</button>
                </div>
              </section>
            ))}

            <section>
              <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 12 }}>
                <h2>
                  <span className="num">★</span>
                  Flashcards <span className="mono" style={{ fontSize: 12, color: 'var(--ink-mute)', fontWeight: 400, marginLeft: 6 }}>· {flashcards.length}</span>
                </h2>
                <button
                  type="button"
                  className="btn sm"
                  disabled={genCardsState === 'generating'}
                  onClick={generateFlashcards}
                  title={flashcards.length > 0 ? 'Regenerate from this guide (replaces existing cards)' : 'Generate flashcards from this guide'}
                  style={{ opacity: genCardsState === 'generating' ? 0.6 : 1 }}
                >
                  {genCardsState === 'generating'
                    ? 'Generating…'
                    : (flashcards.length > 0 ? '✦ Regenerate' : '✦ Generate')}
                </button>
              </div>

              {flashcards.length === 0 ? (
                <div style={{ padding: '24px', marginTop: 12, textAlign: 'center', border: '1px dashed var(--hairline-bold)', borderRadius: 'var(--r-md)', background: 'var(--page-tint)' }}>
                  <div style={{ fontSize: 13, color: 'var(--ink-mute)', marginBottom: 10 }}>
                    No flashcards yet.{' '}
                    <span style={{ color: 'var(--ink-soft)' }}>
                      Click <strong>Generate</strong> to build a deck from this guide's terms and concepts,
                      or add cards manually.
                    </span>
                  </div>
                  <button
                    type="button"
                    className="btn ghost sm"
                    onClick={() => updateContent((cc) => {
                      cc.flashcards = cc.flashcards || [];
                      cc.flashcards.push({ front: '', back: '' });
                    })}
                  >＋ Add card manually</button>
                </div>
              ) : (
                <>
                  <div style={{ display: 'flex', flexDirection: 'column', gap: 6, marginTop: 12 }}>
                    {flashcards.map((card, i) => (
                      <div key={i} className="editable-card">
                        <span className="mono card-num">{i + 1}</span>
                        <input
                          className="bare-input card-front"
                          value={card.front || ''}
                          placeholder="Front (term/question)"
                          onChange={(e) => updateContent((cc) => { cc.flashcards[i].front = e.target.value; })}
                        />
                        <AutoTextarea
                          className="bare-textarea card-back"
                          value={card.back || ''}
                          placeholder="Back (answer)"
                          onChange={(v) => updateContent((cc) => { cc.flashcards[i].back = v; })}
                        />
                        <button
                          type="button"
                          className="row-x"
                          title="Remove card"
                          onClick={() => updateContent((cc) => { cc.flashcards.splice(i, 1); })}
                        >×</button>
                      </div>
                    ))}
                  </div>
                  <div style={{ marginTop: 10 }}>
                    <button
                      type="button"
                      className="btn ghost sm"
                      onClick={() => updateContent((cc) => {
                        cc.flashcards = cc.flashcards || [];
                        cc.flashcards.push({ front: '', back: '' });
                      })}
                    >＋ Add card</button>
                  </div>
                </>
              )}
            </section>

            {firstQ && (
              <div className="quick-check" style={{ marginTop: 32 }}>
                <div className="qc-label">? Quick check</div>
                <div className="qc-q">{firstQ.question}</div>
                {(firstQ.options || []).map((o, i) => (
                  <div key={i} className="qc-opt">
                    <span className="letter">{String.fromCharCode(65 + i)}</span>
                    <span>{o}</span>
                  </div>
                ))}
                <div style={{ textAlign: 'right', marginTop: 10 }}>
                  <button className="btn ghost sm" onClick={() => navigate(`${guidePath}/quiz`)}>Open full quiz →</button>
                </div>
              </div>
            )}
          </div>
        </main>

        <aside className="meta-rail">
          <div className="eyebrow" style={{ marginBottom: 10 }}>About</div>
          <div className="row-pair"><span>Sources</span><span className="num">{guide.sources || 0}</span></div>
          <div className="row-pair"><span>Sections</span><span className="num">{sections.length}</span></div>
          <div className="row-pair"><span>Flashcards</span><span className="num">{flashcards.length}</span></div>
          <div className="row-pair"><span>Quiz items</span><span className="num">{quiz.length}</span></div>
          <div className="row-pair"><span>Generated</span><span className="num" style={{ fontSize: 11 }}>{relativeTime(guide.generatedAt)}</span></div>

          <div style={{ marginTop: 24 }}>
            <div className="eyebrow" style={{ marginBottom: 8 }}>Actions</div>
            {flashcards.length > 0 && <button className="btn sm" style={{ width: '100%', justifyContent: 'center', marginBottom: 6 }} onClick={() => navigate(`${guidePath}/flashcards`)}>★ Study cards</button>}
          </div>

          <button className="btn danger sm" style={{ width: '100%', justifyContent: 'center', marginTop: 18 }}
            onClick={async () => {
              if (!confirm('Delete this generated guide? This cannot be undone.')) return;
              try { await deleteGuide(guide.id); navigate(`/subject/${subj.id}`); }
              catch (e) { alert('Delete failed: ' + e.message); }
            }}>
            Delete guide
          </button>
        </aside>
      </div>
      {aiPanelOpen && <GuideAIPanel guide={guide} subject={subj} onClose={() => setAiPanelOpen(false)} />}
    </Page>
  );
}

// Auto-growing textarea — height tracks content. Used throughout the editable
// guide so paragraphs and definitions don't show a scrollbar.
function AutoTextarea({ value, onChange, className, placeholder, ...rest }) {
  const ref = React.useRef(null);
  const resize = () => {
    const el = ref.current;
    if (!el) return;
    el.style.height = 'auto';
    el.style.height = el.scrollHeight + 'px';
  };
  React.useEffect(() => { resize(); }, [value]);
  return (
    <textarea
      ref={ref}
      className={className}
      value={value || ''}
      placeholder={placeholder}
      rows={1}
      onChange={(e) => { onChange(e.target.value); }}
      onInput={resize}
      {...rest}
    />
  );
}

// ─── Flashcards ────────────────────────────────────────────────────────────

// ─── Flashcard mode picker ────────────────────────────────────────────────
//
// Two modes:
//   Cram      — stateless. Pass/Fail, 2-consecutive-pass to clear a card,
//               failed cards re-queue, session ends when all clear or time up.
//               No DB writes.
//   Long-term — FSRS-4.5 spaced repetition. Per-card stability/difficulty/
//               next_review persisted in flashcard_reviews (RLS-scoped).
function FlashcardScreen({ subjectId, guideId }) {
  const subjects = useSubjects();
  const subj = subjects.find((s) => s.id === subjectId) || { id: subjectId, name: '(unknown subject)' };
  const [guide, setGuide] = React.useState(() => findGuideSync(guideId));
  React.useEffect(() => {
    if (guide) return;
    let cancelled = false;
    findGuide(guideId)
      .then((g) => { if (!cancelled) setGuide(g); })
      .catch((e) => { if (!cancelled) console.error('findGuide failed', e); });
    return () => { cancelled = true; };
  }, [guideId]);

  const guidePath = `/subject/${subjectId}/guide/${guideId}`;
  const cards = guide ? (guide.content?.flashcards || []) : [];

  return (
    <Page
      crumbs={[
        { label: 'Dashboard', to: '/' },
        { label: subj.name, to: `/subject/${subjectId}` },
        { label: guide?.title || 'Guide', to: guidePath },
        'Flashcards',
      ]}
      actions={<button className="btn" onClick={() => navigate(guidePath)}>Exit</button>}
    >
      <div className="cd-page" style={{ overflow: 'auto', maxWidth: 880, margin: '0 auto' }}>
        <div className="cd-page-head">
          <div className="cd-collage" aria-hidden="true">
            <span className="s1"></span><span className="s2"></span><span className="s3"></span>
          </div>
          <div className="cd-page-eyebrow">§ Study mode</div>
          <h1 className="cd-page-title">Pick a mode.</h1>
          <p className="cd-page-sub">
            <b>{cards.length}</b> {cards.length === 1 ? 'card' : 'cards'} in this deck.
            <span className="dot">·</span>
            Cram for a deadline, or learn long-term with spaced repetition.
          </p>
        </div>

        {cards.length === 0 ? (
          <div className="cd-empty" style={{ flexDirection: 'column', gap: 12 }}>
            <div style={{ fontFamily: 'var(--serif)', fontSize: 22, color: 'var(--cd-ink)', fontWeight: 500 }}>No flashcards yet</div>
            <div style={{ fontSize: 13, color: 'var(--cd-muted)' }}>
              Open the guide and click <strong>✦ Generate</strong> next to Flashcards to build a deck.
            </div>
            <button className="btn dark" onClick={() => navigate(guidePath)}>← Back to guide</button>
          </div>
        ) : (
            <div className="mode-grid">
              <button
                className="mode-tile"
                onClick={() => navigate(`${guidePath}/flashcards/cram`)}
              >
                <div className="mode-glyph">⚡</div>
                <div className="mode-name">Cram</div>
                <div className="mode-tag">Timed · stateless</div>
                <div className="mode-desc">
                  Drill the whole deck before a deadline. Mark each card <strong>Pass</strong> or
                  <strong> Fail</strong>; failed cards re-queue, and a card clears after 2 passes in a row.
                </div>
                <div className="mode-meta">
                  <span>{cards.length} cards</span>
                  <span>·</span>
                  <span>no progress saved</span>
                </div>
              </button>

              <button
                className="mode-tile"
                onClick={() => navigate(`${guidePath}/flashcards/longterm`)}
              >
                <div className="mode-glyph">∞</div>
                <div className="mode-name">Long-term</div>
                <div className="mode-tag">Spaced repetition · FSRS</div>
                <div className="mode-desc">
                  Learn for retention. Rate each card <strong>Again / Hard / Good / Easy</strong>;
                  the FSRS scheduler picks when you'll see it next. New cards capped at 10/day.
                </div>
                <div className="mode-meta">
                  <span>persistent state</span>
                  <span>·</span>
                  <span>per-card schedule</span>
                </div>
              </button>
            </div>
          )}
      </div>
    </Page>
  );
}

// ─── Cram mode ────────────────────────────────────────────────────────────
const CRAM_DURATIONS = [5, 10, 15, 20, 30]; // minutes

function CramScreen({ subjectId, guideId }) {
  const subjects = useSubjects();
  const subj = subjects.find((s) => s.id === subjectId) || { id: subjectId, name: '(unknown subject)' };
  const [guide, setGuide] = React.useState(() => findGuideSync(guideId));
  React.useEffect(() => {
    if (guide) return;
    let cancelled = false;
    findGuide(guideId)
      .then((g) => { if (!cancelled) setGuide(g); })
      .catch((e) => { if (!cancelled) console.error('findGuide failed', e); });
    return () => { cancelled = true; };
  }, [guideId]);

  const guidePath = `/subject/${subjectId}/guide/${guideId}`;
  const flashcardsPath = `${guidePath}/flashcards`;
  const cards = guide ? (guide.content?.flashcards || []) : [];

  // Phase: 'pick' (choose duration) → 'session' → 'done'
  const [phase, setPhase] = React.useState('pick');
  const [durationMin, setDurationMin] = React.useState(10);
  const [endsAt, setEndsAt] = React.useState(null);
  const [now, setNow] = React.useState(Date.now());
  const [queue, setQueue] = React.useState([]); // [{ idx, card, passStreak, attempts }]
  const [doneCount, setDoneCount] = React.useState(0);
  const [flipped, setFlipped] = React.useState(false);
  const [endedReason, setEndedReason] = React.useState(null); // 'cleared' | 'timeup' | 'exit'

  // Snapshot of original deck size at session start (for progress %).
  const totalCardsRef = React.useRef(0);

  const startSession = (mins) => {
    const dur = mins || durationMin;
    setDurationMin(dur);
    const initial = cards.map((card, i) => ({ idx: i, card, passStreak: 0, attempts: 0 }));
    // Shuffle so order isn't deterministic between sessions.
    for (let i = initial.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));
      [initial[i], initial[j]] = [initial[j], initial[i]];
    }
    setQueue(initial);
    setDoneCount(0);
    setFlipped(false);
    totalCardsRef.current = initial.length;
    setEndsAt(Date.now() + dur * 60 * 1000);
    setEndedReason(null);
    setPhase('session');
  };

  // Tick + auto-end at time-up
  React.useEffect(() => {
    if (phase !== 'session') return;
    const t = setInterval(() => setNow(Date.now()), 1000);
    return () => clearInterval(t);
  }, [phase]);
  React.useEffect(() => {
    if (phase === 'session' && endsAt && now >= endsAt) {
      setEndedReason('timeup');
      setPhase('done');
    }
  }, [phase, endsAt, now]);

  const current = phase === 'session' ? queue[0] : null;

  const handlePass = () => {
    setQueue((q) => {
      if (q.length === 0) return q;
      const [head, ...rest] = q;
      const nextStreak = head.passStreak + 1;
      const nextAttempts = head.attempts + 1;
      if (nextStreak >= 2) {
        // Done — drop card.
        setDoneCount((d) => d + 1);
        if (rest.length === 0) {
          setEndedReason('cleared');
          setPhase('done');
        }
        return rest;
      }
      // Re-queue at end with bumped streak.
      return [...rest, { ...head, passStreak: nextStreak, attempts: nextAttempts }];
    });
    setFlipped(false);
  };

  const handleFail = () => {
    setQueue((q) => {
      if (q.length === 0) return q;
      const [head, ...rest] = q;
      // Reset streak; re-queue at end.
      return [...rest, { ...head, passStreak: 0, attempts: head.attempts + 1 }];
    });
    setFlipped(false);
  };

  // Keyboard: Space flips, F/J fail/pass (or 1/2)
  React.useEffect(() => {
    if (phase !== 'session') return;
    const onKey = (e) => {
      if (e.target?.tagName === 'INPUT' || e.target?.tagName === 'TEXTAREA') return;
      if (e.key === ' ') { e.preventDefault(); setFlipped((f) => !f); }
      else if (e.key === '1' || e.key === 'f' || e.key === 'F') { e.preventDefault(); handleFail(); }
      else if (e.key === '2' || e.key === 'j' || e.key === 'J') { e.preventDefault(); handlePass(); }
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [phase]);

  const remainingMs = endsAt ? Math.max(0, endsAt - now) : 0;
  const mm = Math.floor(remainingMs / 60000);
  const ss = Math.floor((remainingMs % 60000) / 1000);
  const timerLabel = `${String(mm).padStart(2, '0')}:${String(ss).padStart(2, '0')}`;
  const remainingCards = queue.length;
  const progressPct = totalCardsRef.current > 0
    ? Math.round((doneCount / totalCardsRef.current) * 100)
    : 0;

  const crumbs = [
    { label: 'Dashboard', to: '/' },
    { label: subj.name, to: `/subject/${subjectId}` },
    { label: guide?.title || 'Guide', to: guidePath },
    { label: 'Flashcards', to: flashcardsPath },
    'Cram',
  ];

  // ── Pick phase ──────────────────────────────────────────────────────────
  if (phase === 'pick') {
    return (
      <Page crumbs={crumbs}
        actions={<button className="btn" onClick={() => navigate(flashcardsPath)}>Back</button>}>
        <div className="cd-page" style={{ overflow: 'auto', maxWidth: 560, margin: '0 auto' }}>
          <div className="cd-page-head">
            <div className="cd-collage" aria-hidden="true">
              <span className="s1"></span><span className="s2"></span><span className="s3"></span>
            </div>
            <div className="cd-page-eyebrow">⚡ Cram</div>
            <h1 className="cd-page-title">How long?</h1>
            <p className="cd-page-sub">
              Pick a session length. Cards re-queue on Fail; clear after 2 passes in a row.
            </p>
          </div>

            <div className="cram-durations">
              {CRAM_DURATIONS.map((m) => (
                <button key={m}
                  className={"cram-dur" + (m === durationMin ? ' on' : '')}
                  onClick={() => setDurationMin(m)}>
                  <div className="num">{m}</div>
                  <div className="lbl">min</div>
                </button>
              ))}
            </div>

            <button
              className="btn accent"
              style={{ width: '100%', marginTop: 18, height: 44, justifyContent: 'center', fontSize: 14 }}
              onClick={() => startSession(durationMin)}
              disabled={cards.length === 0}
            >
              Start cram session →
            </button>

            <div style={{ marginTop: 14, fontFamily: 'var(--mono)', fontSize: 11, color: 'var(--ink-mute)', textTransform: 'uppercase', letterSpacing: '0.06em', textAlign: 'center' }}>
              {cards.length} cards · no progress saved
            </div>
        </div>
      </Page>
    );
  }

  // ── Done phase ──────────────────────────────────────────────────────────
  if (phase === 'done') {
    const cleared = endedReason === 'cleared';
    return (
      <Page crumbs={crumbs}
        actions={<button className="btn ghost sm" onClick={() => navigate(flashcardsPath)}>Back to modes</button>}>
        <div className="scroll">
          <div className="page-pad fade-in" style={{ maxWidth: 560, margin: '0 auto', textAlign: 'center', paddingTop: 80 }}>
            <div className="eyebrow">⚡ Cram · {endedReason === 'timeup' ? 'time up' : 'cleared'}</div>
            <h1 className="page-title" style={{ fontSize: 48, marginTop: 6 }}>
              {cleared ? <>You cleared the deck.</> : <>Time's up.</>}
            </h1>
            <div className="page-subtitle" style={{ justifyContent: 'center' }}>
              <span><strong style={{ color: 'var(--ink-strong)' }}>{doneCount}</strong> of <strong style={{ color: 'var(--ink-strong)' }}>{totalCardsRef.current}</strong> cards cleared in {durationMin} min.</span>
            </div>
            <div style={{ display: 'flex', justifyContent: 'center', gap: 8, marginTop: 16 }}>
              <button className="btn sm" onClick={() => startSession(durationMin)}>Restart</button>
              <button className="btn accent sm" onClick={() => navigate(flashcardsPath)}>Back to modes</button>
            </div>
          </div>
        </div>
      </Page>
    );
  }

  // ── Session phase ───────────────────────────────────────────────────────
  return (
    <Page crumbs={crumbs}
      actions={
        <>
          <span className="mono" style={{ fontSize: 13, color: remainingMs < 60000 ? 'var(--brick)' : 'var(--ink-strong)', marginRight: 6 }}>{timerLabel}</span>
          <span className="mono" style={{ fontSize: 12, color: 'var(--ink-mute)' }}>{remainingCards} left · {doneCount} done</span>
          <button className="btn ghost sm" onClick={() => { setEndedReason('exit'); setPhase('done'); }}>End</button>
        </>
      }
    >
      <div className="cram-stage">
        <div className="cram-progress">
          <div className="cram-progress-bar" style={{ width: progressPct + '%' }} />
        </div>

        {!current ? (
          <div style={{ padding: 32 }}>Loading…</div>
        ) : (
          <>
            <div className="flash-deck" onClick={() => setFlipped((f) => !f)} style={{ cursor: 'pointer' }}>
              <div className="flash-shadow s1" />
              <div className="flash-shadow s2" />
              <div className="flash-card">
                <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
                  <span className="chip">{subj.name}{guide ? ` · ${guide.title}` : ''}</span>
                  <span className="flash-front-label">
                    {flipped ? 'Back' : 'Front'} · streak {current.passStreak}/2
                  </span>
                </div>
                <div style={{ textAlign: 'center' }}>
                  <div className="flash-front-label" style={{ marginBottom: 14 }}>{flipped ? 'Answer' : 'Question'}</div>
                  <div className="flash-body">{flipped ? current.card.back : current.card.front}</div>
                </div>
                <div style={{ textAlign: 'center', color: 'var(--ink-whisper)', fontSize: 11.5 }}>
                  <span className="kbd">Space</span> flip · <span className="kbd">F</span> fail · <span className="kbd">J</span> pass
                </div>
              </div>
            </div>

            <div className="cram-rate">
              <button className="cram-rate-btn fail" onClick={handleFail}>
                <span className="lbl">Fail</span>
                <span className="hint">re-queue</span>
                <span className="k kbd">F</span>
              </button>
              <button className="cram-rate-btn pass" onClick={handlePass}>
                <span className="lbl">Pass</span>
                <span className="hint">{current.passStreak === 1 ? 'clears card' : 'streak +1'}</span>
                <span className="k kbd">J</span>
              </button>
            </div>
          </>
        )}
      </div>
    </Page>
  );
}

// ─── Long-term (FSRS) mode ────────────────────────────────────────────────
const FSRS_RATING_LABELS = [
  { id: 1, name: 'Again', cls: 'again' },
  { id: 2, name: 'Hard',  cls: 'hard'  },
  { id: 3, name: 'Good',  cls: 'good'  },
  { id: 4, name: 'Easy',  cls: 'easy'  },
];

function LongTermScreen({ subjectId, guideId }) {
  const subjects = useSubjects();
  const subj = subjects.find((s) => s.id === subjectId) || { id: subjectId, name: '(unknown subject)' };
  const [guide, setGuide] = React.useState(() => findGuideSync(guideId));
  React.useEffect(() => {
    if (guide) return;
    let cancelled = false;
    findGuide(guideId)
      .then((g) => { if (!cancelled) setGuide(g); })
      .catch((e) => { if (!cancelled) console.error('findGuide failed', e); });
    return () => { cancelled = true; };
  }, [guideId]);

  const reviewsMap = useReviewsForGuide(guideId);
  const [loadError, setLoadError] = React.useState(null);
  React.useEffect(() => {
    loadReviewsForGuide(guideId).catch((e) => {
      console.error('reviews load failed', e);
      setLoadError(e?.message || String(e));
    });
  }, [guideId]);

  const guidePath = `/subject/${subjectId}/guide/${guideId}`;
  const flashcardsPath = `${guidePath}/flashcards`;
  const cards = guide ? (guide.content?.flashcards || []) : [];

  // Build the day's queue from cards + cached review rows. Re-built when
  // either side changes. Items come out in priority order.
  const queue = React.useMemo(
    () => buildDueQueue(cards, reviewsMap, { newCap: 10 }),
    [cards, reviewsMap]
  );

  const [pos, setPos] = React.useState(0);
  const [flipped, setFlipped] = React.useState(false);
  const [stats, setStats] = React.useState({ done: 0, again: 0 });
  // Synchronous re-entry guard — flips inside the event tick, ref so rapid
  // 1-2-3 key presses on the same card resolve to a single grade. Released
  // once we've advanced past the card; the save itself runs in background.
  const gradingRef = React.useRef(null);

  // Reset on guide change.
  React.useEffect(() => { setPos(0); setFlipped(false); setStats({ done: 0, again: 0 }); }, [guideId]);

  const current = queue[pos];

  const previews = React.useMemo(() => {
    if (!current) return null;
    return window.FSRS.previewIntervals(current.review || null);
  }, [current]);

  const grade = (rating) => {
    if (!current) return;
    if (gradingRef.current === current.index) return;
    gradingRef.current = current.index;
    const next = window.FSRS.schedule(current.review || null, rating);
    const cardIndex = current.index;
    const wasAgain = rating === 1;
    // Optimistic: advance UI immediately, persist in background.
    setStats((s) => ({ done: s.done + 1, again: s.again + (wasAgain ? 1 : 0) }));
    setFlipped(false);
    setPos((p) => p + 1);
    upsertReview(guideId, cardIndex, next).catch((e) => {
      console.error('grade failed', e);
      // Revert the stats bump for the failed save; the position stays
      // advanced (rewinding mid-session would be more confusing than the
      // card simply reappearing next FSRS cycle, which it will).
      setStats((s) => ({
        done: Math.max(0, s.done - 1),
        again: Math.max(0, s.again - (wasAgain ? 1 : 0)),
      }));
      alert('Save failed for card ' + (cardIndex + 1) + ': ' + (e.message || String(e)) + '\n\nIt will reappear next session.');
    });
  };

  React.useEffect(() => {
    const onKey = (e) => {
      if (e.target?.tagName === 'INPUT' || e.target?.tagName === 'TEXTAREA') return;
      if (e.key === ' ') { e.preventDefault(); setFlipped((f) => !f); }
      else if (flipped && '1234'.includes(e.key)) { e.preventDefault(); grade(parseInt(e.key, 10)); }
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [flipped, current]);

  const crumbs = [
    { label: 'Dashboard', to: '/' },
    { label: subj.name, to: `/subject/${subjectId}` },
    { label: guide?.title || 'Guide', to: guidePath },
    { label: 'Flashcards', to: flashcardsPath },
    'Long-term',
  ];

  // ── Migration-needed error: catches the case when the Supabase table
  //    doesn't exist yet (typical first-run after deploying this feature).
  if (loadError && /relation .*flashcard_reviews|does not exist|not found/i.test(loadError)) {
    return (
      <Page crumbs={crumbs}
        actions={<button className="btn ghost sm" onClick={() => navigate(flashcardsPath)}>Back</button>}>
        <div className="page-pad" style={{ maxWidth: 560, margin: '0 auto', textAlign: 'center', paddingTop: 60 }}>
          <div className="eyebrow">∞ Long-term</div>
          <h1 className="page-title">DB migration required.</h1>
          <div style={{ fontSize: 13.5, color: 'var(--ink-soft)', lineHeight: 1.6, marginTop: 12 }}>
            The <code>flashcard_reviews</code> table doesn't exist yet. Open the Supabase dashboard's
            SQL editor and paste the contents of <strong>migrations/2026-05-01_flashcard_reviews.sql</strong>,
            then reload this page.
          </div>
        </div>
      </Page>
    );
  }

  if (cards.length === 0) {
    return (
      <Page crumbs={crumbs}
        actions={<button className="btn ghost sm" onClick={() => navigate(flashcardsPath)}>Back</button>}>
        <div className="page-pad" style={{ maxWidth: 560, margin: '0 auto', textAlign: 'center', paddingTop: 80 }}>
          <h1 className="page-title">No flashcards yet.</h1>
          <button className="btn accent sm" style={{ marginTop: 14 }} onClick={() => navigate(guidePath)}>← Back to guide</button>
        </div>
      </Page>
    );
  }

  // Done with today's queue.
  if (pos >= queue.length) {
    // Compute the "next due" date from cached reviews for a heads-up.
    let soonest = null;
    for (const r of reviewsMap.values()) {
      if (!r.next_review) continue;
      const t = new Date(r.next_review).getTime();
      if (t > Date.now() && (soonest == null || t < soonest)) soonest = t;
    }
    const nextLabel = soonest
      ? `Next card due ${relativeTime(new Date(soonest).toISOString()).replace('ago', 'from now')}`
      : 'Add more cards or come back tomorrow.';

    return (
      <Page crumbs={crumbs}
        actions={<button className="btn ghost sm" onClick={() => navigate(flashcardsPath)}>Back to modes</button>}>
        <div className="page-pad fade-in" style={{ maxWidth: 560, margin: '0 auto', textAlign: 'center', paddingTop: 80 }}>
          <div className="eyebrow">∞ Long-term · done for today</div>
          <h1 className="page-title" style={{ fontSize: 48, marginTop: 6 }}>All caught up.</h1>
          <div className="page-subtitle" style={{ justifyContent: 'center' }}>
            <span><strong style={{ color: 'var(--ink-strong)' }}>{stats.done}</strong> reviews · <strong style={{ color: 'var(--ink-strong)' }}>{stats.again}</strong> Again.</span>
          </div>
          <div style={{ fontSize: 13, color: 'var(--ink-mute)', marginTop: 14 }}>{nextLabel}</div>
        </div>
      </Page>
    );
  }

  const card = current.card;
  const isNew = !current.review || current.review.state === 'new';

  return (
    <Page crumbs={crumbs}
      actions={
        <>
          <span className="mono" style={{ fontSize: 12, color: 'var(--ink-mute)' }}>
            {pos + 1} / {queue.length} · {stats.done} done
          </span>
          <button className="btn ghost sm" onClick={() => navigate(flashcardsPath)}>End</button>
        </>
      }
    >
      <div className="flash-stage">
        <div className="flash-deck" onClick={() => setFlipped((f) => !f)} style={{ cursor: 'pointer' }}>
          <div className="flash-shadow s1" />
          <div className="flash-shadow s2" />
          <div className="flash-card">
            <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
              <span className="chip">{subj.name}{guide ? ` · ${guide.title}` : ''}</span>
              <span className="flash-front-label">
                {isNew ? 'NEW' : (current.review?.state || 'review').toUpperCase()} · {flipped ? 'Back' : 'Front'}
              </span>
            </div>
            <div style={{ textAlign: 'center' }}>
              <div className="flash-front-label" style={{ marginBottom: 14 }}>{flipped ? 'Answer' : 'Question'}</div>
              <div className="flash-body">{flipped ? card.back : card.front}</div>
            </div>
            <div style={{ textAlign: 'center', color: 'var(--ink-whisper)', fontSize: 11.5 }}>
              {flipped
                ? <><span className="kbd">1</span>–<span className="kbd">4</span> rate</>
                : <><span className="kbd">Space</span> flip to rate</>}
            </div>
          </div>
        </div>

        {flipped && previews && (
          <div className="flash-rate">
            {FSRS_RATING_LABELS.map((r) => (
              <button
                key={r.id}
                className={"flash-rate-btn " + r.cls}
                onClick={() => grade(r.id)}
              >
                <span className="lbl">{r.name}</span>
                <span className="ivl">{previews[r.id]}</span>
                <span className="k kbd">{r.id}</span>
              </button>
            ))}
          </div>
        )}
      </div>
    </Page>
  );
}

// ─── Quiz ──────────────────────────────────────────────────────────────────

function QuizScreen({ subjectId, guideId }) {
  const subjects = useSubjects();
  const subj = subjects.find((s) => s.id === subjectId) || { id: subjectId, name: '(unknown subject)' };
  const [guide, setGuide] = React.useState(() => findGuideSync(guideId));
  React.useEffect(() => {
    if (guide) return;
    let cancelled = false;
    findGuide(guideId)
      .then((g) => { if (!cancelled) setGuide(g); })
      .catch((e) => { if (!cancelled) console.error('findGuide failed', e); });
    return () => { cancelled = true; };
  }, [guideId]);

  const guidePath = `/subject/${subjectId}/guide/${guideId}`;
  const questions = guide ? (guide.content?.quiz || []) : [];
  const [idx, setIdx] = React.useState(0);
  const [picks, setPicks] = React.useState([]);
  const [revealed, setRevealed] = React.useState(false);
  const [showHint, setShowHint] = React.useState(false);

  const q = questions[idx];
  const pick = picks[idx];
  const isCorrect = q && pick === q.correctIndex;
  const score = picks.reduce((n, p, i) => n + (p === questions[i]?.correctIndex ? 1 : 0), 0);
  const answered = picks.filter((p) => p != null).length;

  const choose = (i) => {
    if (revealed) return;
    setPicks((arr) => { const next = arr.slice(); next[idx] = i; return next; });
  };

  return (
    <Page
      crumbs={[
        { label: 'Dashboard', to: '/' },
        { label: subj.name, to: `/subject/${subjectId}` },
        { label: guide?.title || 'Guide', to: guidePath },
        'Quiz',
      ]}
      actions={
        <>
          <span className="mono" style={{ fontSize: 12, color: 'var(--ink-mute)', marginRight: 8 }}>
            {questions.length === 0 ? '0 questions' : `${answered} / ${questions.length} · score ${score}`}
          </span>
          <button className="btn ghost sm" onClick={() => navigate(guidePath)}>Exit</button>
        </>
      }
    >
      <div className="scroll">
        <div className="page-pad" style={{ paddingTop: 36 }}>
          <div style={{ maxWidth: 620, margin: '0 auto' }}>
            {questions.length === 0 ? (
              <div style={{ padding: '36px', textAlign: 'center', border: '1px dashed var(--hairline-bold)', borderRadius: 'var(--r-md)' }}>
                <div style={{ fontFamily: 'var(--serif)', fontSize: 22, color: 'var(--ink-strong)', fontWeight: 500 }}>No quiz</div>
                <div style={{ fontSize: 13, color: 'var(--ink-mute)', margin: '8px 0 16px' }}>This guide doesn't have a quiz. Generate one from your own files.</div>
                <button className="btn accent sm" onClick={() => navigate(`/subject/${subjectId}/upload`)}>↑ Upload files</button>
              </div>
            ) : (
              <>
                <div className="quiz-progress">
                  {questions.map((_, i) => {
                    const p = picks[i];
                    const right = p != null && p === questions[i].correctIndex;
                    const wrong = p != null && p !== questions[i].correctIndex;
                    const cls = right ? 'right' : wrong ? 'wrong' : i === idx ? 'cur' : '';
                    return <div key={i} className={"seg " + cls} />;
                  })}
                </div>

                <div className="eyebrow">Question {String(idx + 1).padStart(2, '0')} of {String(questions.length).padStart(2, '0')} · multiple choice</div>
                <h1 className="quiz-q">{q.question}</h1>

                <div>
                  {q.options.map((o, i) => {
                    const sel = pick === i;
                    const isAnswer = revealed && i === q.correctIndex;
                    const isWrong  = revealed && sel && i !== q.correctIndex;
                    let cls = '';
                    if (isAnswer) cls = 'right';
                    else if (isWrong) cls = 'wrong';
                    else if (sel) cls = 'sel';
                    return (
                      <div key={i} className={"quiz-opt " + cls} onClick={() => choose(i)}>
                        <div className="letter">{isAnswer ? '✓' : isWrong ? '✗' : String.fromCharCode(65 + i)}</div>
                        <span>{o}</span>
                      </div>
                    );
                  })}
                </div>

                {q.hint && !revealed && (
                  <div style={{ marginTop: 16, padding: '12px 14px', border: '1px dashed var(--hairline-bold)', borderRadius: 'var(--r-md)' }}>
                    <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
                      <span className="eyebrow">¿ Hint</span>
                      {!showHint ? <button className="btn ghost sm" onClick={() => setShowHint(true)}>Show</button> : null}
                    </div>
                    {showHint && <div style={{ fontSize: 13, color: 'var(--ink-soft)', marginTop: 6, fontStyle: 'italic' }}>{q.hint}</div>}
                  </div>
                )}

                {revealed && (
                  <div style={{
                    marginTop: 16, padding: '14px 16px',
                    background: isCorrect ? 'var(--moss-tint)' : 'var(--brick-tint)',
                    border: '1px solid ' + (isCorrect ? 'var(--moss)' : 'var(--brick)'),
                    borderRadius: 'var(--r-md)',
                  }}>
                    <div style={{ fontWeight: 600, color: isCorrect ? 'var(--moss)' : 'var(--brick)' }}>
                      {isCorrect ? '✓ Correct' : '✗ Not quite'}
                    </div>
                    <div style={{ fontSize: 13, color: 'var(--ink-soft)', marginTop: 4 }}>
                      Correct answer: <strong style={{ color: 'var(--ink-strong)' }}>{q.options[q.correctIndex]}</strong>
                    </div>
                  </div>
                )}

                <div style={{ marginTop: 24, display: 'flex', justifyContent: 'space-between' }}>
                  <button className="btn ghost sm"
                    onClick={() => { setRevealed(false); setShowHint(false); setIdx((i) => Math.max(i - 1, 0)); }}
                    disabled={idx === 0} style={{ opacity: idx === 0 ? 0.4 : 1 }}>← Previous</button>
                  {!revealed
                    ? <button className="btn accent" disabled={pick == null} onClick={() => setRevealed(true)} style={{ opacity: pick == null ? 0.4 : 1 }}>Check answer</button>
                    : idx < questions.length - 1
                      ? <button className="btn accent" onClick={() => { setRevealed(false); setShowHint(false); setIdx((i) => i + 1); }}>Next →</button>
                      : <button className="btn accent" onClick={() => navigate(guidePath)}>Finish · {score} / {questions.length}</button>
                  }
                </div>
              </>
            )}
          </div>
        </div>
      </div>
    </Page>
  );
}

// ─── New-guide dialog: subject picker + file importer + settings ───────────

function NewGuideDialog({ defaultSubjectId, onClose }) {
  const subjects = useSubjects();
  const [subjectId, setSubjectId] = React.useState(defaultSubjectId || (subjects[0]?.id || ''));
  const [showAddSubject, setShowAddSubject] = React.useState(false);
  const [entries, setEntries] = React.useState([]);
  const [dragOver, setDragOver] = React.useState(false);
  const [notes, setNotes] = React.useState('');
  const [mode, setMode] = React.useState('full');
  const inputRef = React.useRef(null);
  const idCounter = React.useRef(0);

  // Auto-select on first load if a subject became available
  React.useEffect(() => {
    if (!subjectId && subjects.length > 0) setSubjectId(subjects[0].id);
  }, [subjects.length]);

  const addFiles = async (filesList) => {
    const files = Array.from(filesList);
    const fresh = files.map((file) => ({
      id: ++idCounter.current,
      file,
      name: file.name,
      kind: fileKindFromExt(file.name.split('.').pop()),
      size: formatBytes(file.size),
      status: 'parsing',
      parsed: null,
      error: null,
    }));
    setEntries((prev) => [...prev, ...fresh]);

    for (const e of fresh) {
      try {
        const parsed = await parseFile(e.file);
        setEntries((prev) => prev.map((x) => x.id === e.id ? { ...x, status: 'done', parsed } : x));
      } catch (err) {
        setEntries((prev) => prev.map((x) => x.id === e.id ? { ...x, status: 'error', error: err.message } : x));
      }
    }
  };

  const onDrop = (ev) => {
    ev.preventDefault();
    setDragOver(false);
    if (ev.dataTransfer?.files?.length) addFiles(ev.dataTransfer.files);
  };

  const remove = (id) => setEntries((prev) => prev.filter((e) => e.id !== id));

  const okFiles = entries.filter((e) => e.status === 'done');
  const parsing = entries.some((e) => e.status === 'parsing');
  const totalChars = okFiles.reduce((n, e) => n + (e.parsed?.text?.length || 0), 0);
  const canGenerate = subjectId && okFiles.length > 0 && !parsing;

  const startGenerate = async () => {
    const parsed = okFiles.map((e) => e.parsed);
    stageGeneration(subjectId, { files: parsed, notes, mode });

    // Persist parsed files to the subject's document library so the Files
    // section stays comprehensive. Awaited so the row is in the cache before
    // we navigate away.
    const docRows = okFiles.map((e) => ({
      name: e.name,
      ext: (e.name.split('.').pop() || '').toLowerCase(),
      kind: e.parsed?.kind === 'image' ? 'image' : 'text',
      text_content: e.parsed?.kind === 'text' ? e.parsed.text : null,
      bytes: e.file?.size ?? null,
    }));
    try { await addDocumentsBulk(docRows, { subjectId }); }
    catch (err) { console.warn('document save failed', err); }

    const newId = newGuideId();
    onClose();
    navigate(`/subject/${subjectId}/guide/${newId}/generate`);
  };

  return (
    <ModalShell width={620} onClose={onClose}>
      <ModalHead title="New guide" sub="Pick a subject, drop your sources, and generate." onClose={onClose} />

      <label className="field" style={{ marginBottom: 0 }}>
        <span>Subject</span>
        <div style={{ display: 'flex', gap: 8 }}>
          <select className="input" value={subjectId} onChange={(e) => setSubjectId(e.target.value)} style={{ flex: 1 }}>
            {subjects.length === 0 && <option value="">— no subjects yet —</option>}
            {subjects.map((s) => <option key={s.id} value={s.id}>{s.emoji} {s.name}</option>)}
          </select>
          <button type="button" className="btn sm" onClick={() => setShowAddSubject(true)}>＋ New</button>
        </div>
      </label>

      <div className="field" style={{ marginBottom: 0 }}>
        <span>Sources</span>
        <div
          onDragOver={(e) => { e.preventDefault(); setDragOver(true); }}
          onDragLeave={() => setDragOver(false)}
          onDrop={onDrop}
          style={{
            position: 'relative',
            border: '1.5px dashed ' + (dragOver ? 'var(--accent)' : 'var(--hairline-bold)'),
            borderRadius: 'var(--r-md)',
            padding: '20px 16px',
            textAlign: 'center',
            background: dragOver ? 'var(--accent-faint)' : 'var(--page-tint)',
            cursor: 'pointer',
            transition: 'all 0.12s',
          }}
        >
          {/* Native click target. Absolutely positioned over the whole dropzone
              so the OS picker opens on a real user click — no JS .click() race
              and no double-fire from a parent <label>. opacity 0 keeps it
              invisible; cursor inherits so the dropzone still shows pointer. */}
          <input ref={inputRef} type="file" multiple
            accept=".pdf,.docx,.pptx,.txt,.md,.png,.jpg,.jpeg,.webp,.gif"
            style={{
              position: 'absolute', inset: 0, width: '100%', height: '100%',
              opacity: 0, cursor: 'pointer',
            }}
            onChange={(e) => { if (e.target.files?.length) addFiles(e.target.files); e.target.value = ''; }}
          />
          <div style={{ fontFamily: 'var(--serif)', fontSize: 17, color: 'var(--ink-strong)', fontWeight: 500, pointerEvents: 'none' }}>
            {dragOver ? 'Release to add' : 'Drop files here, or click to browse.'}
          </div>
          <div style={{ fontSize: 11, color: 'var(--ink-mute)', marginTop: 4, fontFamily: 'var(--mono)', textTransform: 'uppercase', letterSpacing: '0.08em', pointerEvents: 'none' }}>
            PDF · DOCX · PPTX · TXT · MD · PNG · JPG
          </div>
        </div>
      </div>

      {entries.length > 0 && (
        <div style={{ maxHeight: 180, overflowY: 'auto', border: '1px solid var(--hairline)', borderRadius: 'var(--r-sm)', background: 'var(--page-tint)' }}>
          {entries.map((e) => <UploadRow key={e.id} entry={e} onRemove={() => remove(e.id)} />)}
        </div>
      )}

      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14 }}>
        <label className="field" style={{ marginBottom: 0 }}>
          <span>Output</span>
          <div style={{ display: 'flex', gap: 6 }}>
            <ModeBtn active={mode === 'full'}        onClick={() => setMode('full')}        title="Study guide" sub="sections + cards + quiz" />
            <ModeBtn active={mode === 'flashcards'} onClick={() => setMode('flashcards')}   title="Flashcards"   sub="every term" />
          </div>
        </label>

        <label className="field" style={{ marginBottom: 0 }}>
          <span>Instructions for the AI (optional)</span>
          <textarea className="input" rows={3} value={notes} onChange={(e) => setNotes(e.target.value)}
            placeholder="e.g. focus on chapters 3–5; explain like I'm new to the field" style={{ minHeight: 60 }} />
        </label>
      </div>

      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', fontSize: 11.5, color: 'var(--ink-mute)', fontFamily: 'var(--mono)', textTransform: 'uppercase', letterSpacing: '0.06em' }}>
        <span>{okFiles.length} of {entries.length || 0} ready · {formatChars(totalChars)}</span>
        <span>gpt-4o-mini · vision</span>
      </div>

      <div style={{ display: 'flex', gap: 8, marginTop: 4 }}>
        <button type="button" className="btn ghost sm" onClick={onClose}>Cancel</button>
        <div style={{ flex: 1 }} />
        <button type="button" className="btn accent" disabled={!canGenerate} onClick={startGenerate}
          style={{ height: 36, padding: '0 18px', opacity: canGenerate ? 1 : 0.5, cursor: canGenerate ? 'pointer' : 'not-allowed' }}>
          ✦ {mode === 'flashcards' ? 'Generate flashcards' : 'Generate guide'}
        </button>
      </div>

      {showAddSubject && (
        <SubjectDialog onClose={(opts) => {
          setShowAddSubject(false);
          if (opts?.created) setSubjectId(opts.created.id);
        }} />
      )}
    </ModalShell>
  );
}

// ── Guide-context wrapper for AIChatPanel ─────────────────────────────────
// AIChatPanel itself stays in app.jsx until Wave 8 — referenced here as a
// bare identifier, resolves through the global script scope.
function GuideAIPanel({ guide, subject, onClose }) {
  const context = React.useMemo(() => {
    const c = guide?.content || {};
    const sectionsBlob = (c.sections || []).map((s) => {
      const terms = (s.terms || []).map((t) => `  - ${t.term}: ${t.def}`).join('\n');
      return `## ${s.number}. ${s.title}\n${s.body || ''}${terms ? '\n\nKey terms:\n' + terms : ''}`;
    }).join('\n\n').slice(0, 12000);
    const sysPrompt =
      `You are a study assistant for the guide titled "${guide?.title || 'Untitled guide'}"` +
      (subject?.name ? ` (subject: ${subject.name})` : '') + `.\n\n` +
      `Use this material to answer questions. If asked something outside this material, ` +
      `answer helpfully but note that it is not in the guide.\n\n` +
      (c.bigIdea ? `BIG IDEA:\n${c.bigIdea}\n\n` : '') +
      (sectionsBlob ? `SECTIONS & KEY TERMS:\n${sectionsBlob}` : '');
    return {
      kind: 'guide',
      id: guide?.id || null,
      title: guide?.title || 'Untitled guide',
      // subjects table has no `color` column — derive a deterministic color
      // from the subject id via liveblocks.js's colorForUserId() so the chip
      // still renders with a consistent hue per subject.
      color: subject?.id && typeof window.colorForUserId === 'function'
        ? window.colorForUserId(subject.id)
        : null,
      systemPrompt: sysPrompt,
      placeholder: 'Ask anything about this guide…  (Enter sends, Shift+Enter newline)',
      presets: [
        'Quiz me — 5 mixed questions',
        'Summarize the big idea in 1 paragraph',
        'List the 5 most important terms',
        'What might be on a test from this?',
      ],
      emptyHint: `Ask anything about ${guide?.title || 'this guide'}. The AI is grounded in the guide's content (sections + key terms).`,
    };
  }, [guide?.id, guide?.title, guide?.content, subject?.id, subject?.name]);
  return <AIChatPanel context={context} onClose={onClose} />;
}

// ─── Exports ───────────────────────────────────────────────────────────────

Object.assign(window, {
  GuideScreen,
  AutoTextarea,
  FlashcardScreen,
  CramScreen,
  LongTermScreen,
  QuizScreen,
  NewGuideDialog,
  GuideAIPanel,
});

})();
