import { useCallback, useEffect, useRef, useState } from 'react';
import mammoth from 'mammoth';
import { AppContext, useApp } from './context/AppContext.js';
import AdminPage from './pages/AdminPage.jsx';
import AuthPage from './pages/AuthPage.jsx';
import HomePage from './pages/HomePage.jsx';
import ImportPage from './pages/ImportPage.jsx';
import ReaderPage from './pages/ReaderPage.jsx';
import SettingsPage from './pages/SettingsPage.jsx';
import SetupPage from './pages/SetupPage.jsx';
import StudyPage from './pages/StudyPage.jsx';
import {
  AlignmentType,
  BorderStyle,
  Document,
  HeadingLevel,
  Packer,
  Paragraph,
  ShadingType,
  Table,
  TableCell,
  TableRow,
  TextRun,
  WidthType,
} from 'docx';
import {
  COMMENTARY_OPTIONS,
  STUDY_TABS,
  CHAPTER_COUNTS,
  bookOptions,
  NT_BOOK_NUMBER,
  storageKey,
  makeId,
  formatRelativeDate,
  formatChunkReference,
  getChunkVerseEntries,
  chunkSpansNextChapter,
  buildExportHtml,
  wordTableHtml,
  extractPartOfSpeech,
  htmlToPlainText,
  renderVerseContent,
  parseBibleChapter,
  indexKey,
  projectKey,
  loadProjectIndex,
  switchStorageUser,
  BookmarkIcon,
  CopyIcon,
  CrossRefChip,
} from './utils/bibleUtils.jsx';

import {
  saveRemoteProject,
  deleteRemoteProject,
  listRemoteProjects,
  loadRemoteProject,
  getCurrentUser,
  registerUser,
  loginUser,
  logoutUser,
  verifyMfaLogin,
  startMfaSetup,
  confirmMfaSetup,
  disableMfa,
  updateProfile,
  changePassword,
  getShareStatus,
  enableSharing,
  disableSharing,
  getSharedProject,
  adminListUsers,
  adminDeleteUser,
  adminResetPassword,
  adminListProjects,
  adminGetProject,
  adminDeleteProject,
} from './syncService.js';

// COMMENTARY_OPTIONS, STUDY_TABS, CHAPTER_COUNTS imported from ./utils/bibleUtils.jsx
export { COMMENTARY_OPTIONS, STUDY_TABS, CHAPTER_COUNTS };

// bookOptions, NT_BOOK_NUMBER imported from ./utils/bibleUtils.jsx
export { bookOptions, NT_BOOK_NUMBER };

// ---------------------------------------------------------------------------
// Stable utility exports (used by tests and other modules)
// ---------------------------------------------------------------------------

// storageKey, makeId imported from ./utils/bibleUtils.jsx
export { storageKey, makeId };

// ---------------------------------------------------------------------------
// Migration helpers (exported for testing)
// ---------------------------------------------------------------------------

export function migrateChunk(chunk) {
  if (chunk.observation !== undefined) {
    return {
      ...chunk,
      spilloverEndVerse: Number.isInteger(chunk.spilloverEndVerse) ? chunk.spilloverEndVerse : null,
      generalNotes: chunk.generalNotes ?? '',
      episodeNumber: chunk.episodeNumber ?? '',
      episodeTitle: chunk.episodeTitle ?? '',
      finalScript: chunk.finalScript ?? '',
      tags: chunk.tags ?? [],
      inkStrokes: chunk.inkStrokes ?? [],
    }; // already new format
  }
  return {
    ...chunk,
    observation: chunk.notes ?? '',
    interpretation: '',
    application: '',
    crossReferences: [],
    tags: [],
    inkStrokes: [],
    spilloverEndVerse: null,
    generalNotes: '',
    episodeNumber: '',
    episodeTitle: '',
    finalScript: '',
    notes: undefined,
  };
}

// formatChunkReference imported from ./utils/bibleUtils.jsx
export { formatChunkReference };


export function migrateProject(raw) {
  if (!raw) return null;
  // Already new format
  if (Array.isArray(raw.chapters)) {
    return {
      ...raw,
      chapters: raw.chapters.map((ch) => ({
        ...ch,
        chunks: ch.chunks.map(migrateChunk),
      })),
    };
  }
  // Old flat format → wrap in chapters array
  const id = raw.id ?? makeId();
  return {
    id,
    title: raw.title ?? '',
    translation: raw.translation ?? 'BSB',
    lastEdited: raw.lastEdited ?? Date.now(),
    selectedChunkId: raw.selectedChunkId ?? null,
    chapters: [
      {
        book: raw.book ?? '',
        bookAbbrev: raw.bookAbbrev ?? '',
        chapter: raw.chapter ?? '1',
        verses: raw.verses ?? [],
        chunks: (raw.chunks ?? []).map(migrateChunk),
      },
    ],
  };
}

// ---------------------------------------------------------------------------
// Storage helpers
// ---------------------------------------------------------------------------

// indexKey, projectKey, loadProjectIndex, switchStorageUser imported from ./utils/bibleUtils.jsx
export { switchStorageUser };

function saveProjectToStorage(project) {
  // Honour an already-stamped lastEdited (e.g. from autosave) so localStorage
  // and the server always receive the identical timestamp.
  const updated = { ...project, lastEdited: project.lastEdited ?? Date.now() };
  window.localStorage.setItem(projectKey(updated.id), JSON.stringify(updated));
  const index = loadProjectIndex();
  const existing = index.findIndex((e) => e.id === updated.id);
  const tags = Array.from(
    new Set(
      (updated.chapters ?? [])
        .flatMap((ch) => ch.chunks ?? [])
        .flatMap((chunk) => chunk.tags ?? []),
    ),
  );
  const summary = {
    id: updated.id,
    title: updated.title,
    lastEdited: updated.lastEdited,
    chapterSummary: buildChapterSummary(updated),
    tags,
  };
  if (existing >= 0) {
    index[existing] = summary;
  } else {
    index.push(summary);
  }
  window.localStorage.setItem(indexKey(), JSON.stringify(index));
  return updated; // caller can use this exact object for the server PUT
}

function deleteProjectFromStorage(id) {
  window.localStorage.removeItem(projectKey(id));
  const index = loadProjectIndex().filter((e) => e.id !== id);
  window.localStorage.setItem(indexKey(), JSON.stringify(index));
}

function loadProjectById(id) {
  try {
    const raw = window.localStorage.getItem(projectKey(id));
    return raw ? migrateProject(JSON.parse(raw)) : null;
  } catch {
    return null;
  }
}

function migrateOldStorageKeys() {
  const keys = Object.keys(window.localStorage);
  keys.forEach((key) => {
    if (!key.startsWith('bible-study-') || key.startsWith('bible-study-index') || key.startsWith('bible-study-project')) return;
    try {
      const raw = JSON.parse(window.localStorage.getItem(key));
      if (!raw || !raw.book) return;
      const migrated = migrateProject(raw);
      if (!migrated) return;
      saveProjectToStorage(migrated);
      window.localStorage.removeItem(key);
    } catch {
      // skip corrupt entries
    }
  });
}

function buildChapterSummary(project) {
  if (!Array.isArray(project.chapters)) return '';
  return project.chapters
    .map((ch) => `${ch.book} ${ch.chapter}`)
    .join(', ');
}

// formatRelativeDate imported from ./utils/bibleUtils.jsx
export { formatRelativeDate };

// ---------------------------------------------------------------------------
// Export / prompt builders (exported for testing)
// ---------------------------------------------------------------------------

// buildExportHtml imported from ./utils/bibleUtils.jsx
export { buildExportHtml };

export function buildMarkdownExport(project) {
  const chapters = Array.isArray(project.chapters) ? project.chapters : [];

  const chapterSections = chapters.map((ch, chapterIndex) => {
    const chunkSections = ch.chunks.map((chunk) => {
      const scripture = formatChunkReference(project, chapterIndex, chunk, '-');
      const versesText = getChunkVerseEntries(project, chapterIndex, chunk)
        .map((verse) => `> **${verse.chapter}:${verse.number}** ${verse.text}`)
        .join('\n>\n');

      const observation = (chunk.observation ?? '').trim() || '_No observation._';
      const interpretation = (chunk.interpretation ?? '').trim() || '_No interpretation._';
      const application = (chunk.application ?? '').trim() || '_No application._';
      const generalNotes = (chunk.generalNotes ?? '').trim();

      const crossRefsLine = (chunk.crossReferences ?? []).length > 0
        ? `**Cross-References:** ${chunk.crossReferences.join(', ')}\n\n`
        : '';

      const greekSection = chunk.greekWords.length === 0
        ? ''
        : `**Greek/Hebrew Words**\n\n${chunk.greekWords.map((word) => {
            const summary = [
              word.strongNumber,
              word.lexeme,
              word.transliteration && `(${word.transliteration})`,
              word.partOfSpeech,
              word.shortDefinition,
            ].filter(Boolean).join(' — ');
            const definition = word.definitionHtml ? `\n  ${htmlToPlainText(word.definitionHtml)}` : '';
            return `- ${summary}${definition}`;
          }).join('\n')}\n\n`;

      return `### ${scripture}\n\n${versesText}\n\n${generalNotes ? `**Background / Notes**\n\n${generalNotes}\n\n` : ''}**Observation**\n\n${observation}\n\n**Interpretation**\n\n${interpretation}\n\n**Application**\n\n${application}\n\n${crossRefsLine}${greekSection}`;
    }).join('---\n\n');

    return `## ${ch.book} ${ch.chapter}\n\n${chunkSections}`;
  }).join('\n');

  return `# ${project.title}\n\n*${project.translation}*\n\n${chapterSections}`;
}

// wordTableHtml, extractPartOfSpeech, htmlToPlainText imported from ./utils/bibleUtils.jsx
export { wordTableHtml, extractPartOfSpeech, htmlToPlainText };

export function createParagraphsFromText(text) {
  return text
    .split(/\n+/)
    .map((line) => line.trim())
    .filter(Boolean)
    .map((line) => new Paragraph({ children: [new TextRun({ text: line })] }));
}

const DOCX_ACCENT = 'C9A84C'; // antique gold
const DOCX_HEADING_FONT = 'Cormorant Garamond';
const DOCX_BODY_FONT = 'Lora';

export function sectionLabel(text) {
  return new Paragraph({
    spacing: { before: 240, after: 80 },
    children: [
      new TextRun({
        text: text.toUpperCase(),
        bold: true,
        color: DOCX_ACCENT,
        font: DOCX_BODY_FONT,
        size: 20,
        characterSpacing: 20,
      }),
    ],
  });
}

// renderVerseContent imported from ./utils/bibleUtils.jsx
export { renderVerseContent };

function buildChunkBodies(project) {
  const chapters = Array.isArray(project.chapters) ? project.chapters : [];
  let chunkIndex = 0;
  return chapters.map((ch, chapterIndex) => {
    return ch.chunks.map((chunk) => {
      chunkIndex += 1;
      const ref = formatChunkReference(project, chapterIndex, chunk, '–');

      const verses = getChunkVerseEntries(project, chapterIndex, chunk)
        .map((v) => `${v.chapter}:${v.number} ${v.text}`)
        .join('\n');

      const observation = (chunk.observation ?? '').trim() || 'No observation.';
      const interpretation = (chunk.interpretation ?? '').trim() || 'No interpretation.';
      const application = (chunk.application ?? '').trim() || 'No application.';

      const crossRefs = (chunk.crossReferences ?? []).length > 0
        ? chunk.crossReferences.join(', ')
        : 'None.';

      const generalNotes = (chunk.generalNotes ?? '').trim();
      const episodeLabelLine = (chunk.episodeNumber ?? '').trim() || (chunk.episodeTitle ?? '').trim()
        ? `Session: ${[chunk.episodeNumber?.trim(), chunk.episodeTitle?.trim()].filter(Boolean).join(' — ')}\n`
        : '';

      const greekWords = chunk.greekWords.length === 0
        ? 'None.'
        : chunk.greekWords.map((word) => {
            const summary = [
              word.strongNumber,
              word.lexeme,
              word.transliteration && `(${word.transliteration})`,
              word.partOfSpeech,
              word.shortDefinition,
            ].filter(Boolean).join(' | ');
            const definition = word.definitionHtml
              ? `\n  ${htmlToPlainText(word.definitionHtml)}`
              : '';
            return `• ${summary}${definition}`;
          }).join('\n');

      return `===

CHUNK ${chunkIndex} — ${ref}
${episodeLabelLine}
Scripture:
${verses}
${generalNotes ? `\nBackground / General Notes:\n${generalNotes}\n` : ''}
Observation:
${observation}

Interpretation:
${interpretation}

Application:
${application}

Cross-References: ${crossRefs}

Greek Words:
${greekWords}
`;
    }).join('\n');
  }).join('\n');
}

export function buildClaudePrompt(project) {
  const chapters = Array.isArray(project.chapters) ? project.chapters : [];
  const chapterLabel = chapters.map((ch) => `${ch.book} ${ch.chapter}`).join(', ');

  const header = `I've prepared a Bible study on ${chapterLabel} (${project.translation}) and need your help turning my notes into a polished study guide.

Below is my work organised by passage chunk, including my OIA notes and Greek word research. Please create a clear, structured study guide that:
- Synthesises my notes into coherent teaching points
- Naturally integrates the Greek word insights
- Includes 2–3 reflection questions per chunk
- Preserves the passage-by-passage structure

---

PROJECT: ${project.title}
TRANSLATION: ${project.translation}
PASSAGE: ${chapterLabel}
`;

  return header + buildChunkBodies(project);
}

export function buildPronunciationGuide(project) {
  const chapters = Array.isArray(project.chapters) ? project.chapters : [];
  const lines = [];
  chapters.forEach((ch) => {
    ch.chunks.forEach((chunk) => {
      (chunk.greekWords ?? []).forEach((word) => {
        const gloss = word.shortDefinition || word.englishGloss || '';
        const lexeme = word.lexeme || '';
        const translit = word.transliteration || '';
        if (!lexeme && !translit) return;
        const label = gloss ? `${gloss} — ` : '';
        lines.push(`${label}${lexeme}${translit ? ` (${translit})` : ''}`);
      });
    });
  });
  if (lines.length === 0) return 'No Greek/Hebrew words collected yet.';
  return lines.join('\n');
}

export function buildPodcastPrompt(project, podcastName) {
  const chapters = Array.isArray(project.chapters) ? project.chapters : [];
  const chapterLabel = chapters.map((ch) => `${ch.book} ${ch.chapter}`).join(', ');
  const episodeLabel = 'EPISODE';
  const showName = (podcastName ?? '').trim();
  const showLine = showName || 'MY BIBLE STUDY PODCAST';
  const signOffLine = showName
    ? `Introduce yourself, then sign off with: "...and this is ${showName}."`
    : 'Introduce yourself and the name of the show as a sign-off.';

  const header = `I'm recording an episode of my Bible study podcast${showName ? ` "${showName}"` : ''} and need a full script written from my study notes below.

Please write the script in this exact structure, using the section markers and tone shown:

${showLine.toUpperCase()}
${episodeLabel}
${chapterLabel} (${project.translation})  ·  [estimate XX–XX minutes based on content]

✝  OPENING PRAYER
[Pause before opening the text — invite God into the study]
A short prayer (4-6 sentences) tying into the themes of this passage.

— ✦ —

🎙️  COLD OPEN
[Co-host delivers the cold open solo, if there is one — hands off to the host at the end]
A few short punchy lines previewing the passage and its hook, ending with a hand-off line introducing the host and the show.

— ✦ —

📖  SEGMENT [N] — [SEGMENT TITLE]
[Brief stage direction in brackets]
One segment per passage chunk (use the chunk reference, observation, interpretation, and application notes below as the raw material). Conversational, spoken-word style — not academic prose. Work through the text the way the host would talk it through out loud, weaving in the OIA notes and cross-references naturally.

— ✦ —

📖  SEGMENT [N] — GREEK WORD STUDY
[Work through the key terms — keep it vivid, help the words come alive]
For each Greek/Hebrew word collected below, a block in this format:

[English Gloss] — [English meaning]
[DO NOT READ: original-language word (transliteration) — part of speech]
2-4 sentences unpacking the word's meaning and why it matters for this passage, conversational tone.

— ✦ —

💬  DISCUSSION QUESTIONS
[From the study guide — ${chapterLabel}]
3 reflection questions drawn from the application notes, numbered.

— ✦ —

✦  CLOSING
[Grounded and direct — send them away with something to carry]
A closing reflection that ties the segments together, a short pull-quote from the passage with its reference, then close out. ${signOffLine}
"Until next time — keep studying verse by verse."

— End of Episode —

${showLine}  ·  ${episodeLabel}  ·  ${chapterLabel}

---

Here are my study notes, organised by passage chunk, including my OIA notes, cross-references, and Greek/Hebrew word research:

PROJECT: ${project.title}
TRANSLATION: ${project.translation}
PASSAGE: ${chapterLabel}
`;

  return header + buildChunkBodies(project);
}

// parseBibleChapter imported from ./utils/bibleUtils.jsx
export { parseBibleChapter };

// Common short-form aliases for typed-in cross-references (e.g. "Rom 3:23")
const BOOK_NAME_ALIASES = {
  'gen': 'GEN', 'genesis': 'GEN', 'exo': 'EXO', 'exod': 'EXO', 'exodus': 'EXO',
  'lev': 'LEV', 'leviticus': 'LEV', 'num': 'NUM', 'numbers': 'NUM',
  'deut': 'DEU', 'deuteronomy': 'DEU', 'josh': 'JOS', 'joshua': 'JOS',
  'judg': 'JDG', 'judges': 'JDG', 'ruth': 'RUT',
  '1 sam': '1SA', '1 samuel': '1SA', '2 sam': '2SA', '2 samuel': '2SA',
  '1 kgs': '1KI', '1 kings': '1KI', '2 kgs': '2KI', '2 kings': '2KI',
  '1 chr': '1CH', '1 chronicles': '1CH', '2 chr': '2CH', '2 chronicles': '2CH',
  'ezra': 'EZR', 'neh': 'NEH', 'nehemiah': 'NEH', 'esth': 'EST', 'esther': 'EST',
  'job': 'JOB', 'ps': 'PSA', 'psa': 'PSA', 'psalm': 'PSA', 'psalms': 'PSA',
  'prov': 'PRO', 'proverbs': 'PRO', 'eccl': 'ECC', 'ecclesiastes': 'ECC',
  'song': 'SNG', 'isa': 'ISA', 'isaiah': 'ISA', 'jer': 'JER', 'jeremiah': 'JER',
  'lam': 'LAM', 'lamentations': 'LAM', 'ezek': 'EZK', 'ezekiel': 'EZK',
  'dan': 'DAN', 'daniel': 'DAN', 'hos': 'HOS', 'hosea': 'HOS', 'joel': 'JOL',
  'amos': 'AMO', 'obad': 'OBA', 'obadiah': 'OBA', 'jonah': 'JON', 'mic': 'MIC',
  'micah': 'MIC', 'nah': 'NAM', 'nahum': 'NAM', 'hab': 'HAB', 'habakkuk': 'HAB',
  'zeph': 'ZEP', 'zephaniah': 'ZEP', 'hag': 'HAG', 'haggai': 'HAG',
  'zech': 'ZEC', 'zechariah': 'ZEC', 'mal': 'MAL', 'malachi': 'MAL',
  'matt': 'MAT', 'mt': 'MAT', 'matthew': 'MAT', 'mark': 'MRK', 'mk': 'MRK',
  'luke': 'LUK', 'lk': 'LUK', 'john': 'JHN', 'jn': 'JHN', 'acts': 'ACT',
  'rom': 'ROM', 'romans': 'ROM', '1 cor': '1CO', '1 corinthians': '1CO',
  '2 cor': '2CO', '2 corinthians': '2CO', 'gal': 'GAL', 'galatians': 'GAL',
  'eph': 'EPH', 'ephesians': 'EPH', 'phil': 'PHP', 'philippians': 'PHP',
  'col': 'COL', 'colossians': 'COL', '1 thess': '1TH', '1 thessalonians': '1TH',
  '2 thess': '2TH', '2 thessalonians': '2TH', '1 tim': '1TI', '1 timothy': '1TI',
  '2 tim': '2TI', '2 timothy': '2TI', 'titus': 'TIT', 'phlm': 'PHM', 'philemon': 'PHM',
  'heb': 'HEB', 'hebrews': 'HEB', 'jas': 'JAS', 'james': 'JAS',
  '1 pet': '1PE', '1 peter': '1PE', '2 pet': '2PE', '2 peter': '2PE',
  '1 jn': '1JN', '1 john': '1JN', '2 jn': '2JN', '2 john': '2JN',
  '3 jn': '3JN', '3 john': '3JN', 'jude': 'JUD', 'rev': 'REV', 'revelation': 'REV',
};

bookOptions.forEach((b) => {
  BOOK_NAME_ALIASES[b.name.toLowerCase()] = b.abbrev;
});

// Parses strings like "Romans 3:23", "1 Timothy 3:2-7" into { bookAbbrev, chapter, verse, endVerse }
function parseCrossRefString(ref) {
  const match = ref.trim().match(/^(.+?)\s+(\d+):(\d+)(?:-(\d+))?$/);
  if (!match) return null;
  const [, bookPart, chapter, verse, endVerse] = match;
  const abbrev = BOOK_NAME_ALIASES[bookPart.trim().toLowerCase()];
  if (!abbrev) return null;
  return {
    bookAbbrev: abbrev,
    chapter: Number(chapter),
    verse: Number(verse),
    endVerse: endVerse ? Number(endVerse) : Number(verse),
  };
}

// Parses a "Passage" cell like "1:1–2", "9:32–10:23", "9:1–19a", or "—" (no specific passage)
function parseEpisodePassage(raw) {
  const str = (raw ?? '').trim().replace(/(\d)[a-z]\b/g, '$1'); // strip "19a"/"19b" verse-letter suffixes
  if (!str || /^[-–—]+$/.test(str)) return null;
  const match = str.match(/^(\d+):(\d+)\s*[-–—]\s*(?:(\d+):)?(\d+)$/)
    ?? str.match(/^(\d+):(\d+)$/);
  if (!match) return 'invalid';
  const [, ch1, v1, ch2, v2raw] = match;
  const endChapter = ch2 ? Number(ch2) : Number(ch1);
  const endVerse = v2raw !== undefined ? Number(v2raw) : Number(v1);
  return {
    startChapter: Number(ch1),
    startVerse: Number(v1),
    endChapter,
    endVerse,
  };
}

// Extracts a "Session # / Title / Passage" list from a docx, including both
// table rows and standalone "Ep. N — Title" / "Session N — Title" paragraphs (e.g. part intros).
async function parseEpisodeListDocx(file) {
  const arrayBuffer = await file.arrayBuffer();
  const { value: html } = await mammoth.convertToHtml({ arrayBuffer });
  const parsedDoc = new DOMParser().parseFromString(html, 'text/html');

  const episodes = new Map(); // episodeNumber -> { episodeNumber, title, passage }

  parsedDoc.querySelectorAll('table tr').forEach((row) => {
    const cells = Array.from(row.querySelectorAll('td,th')).map((c) => c.textContent.trim());
    if (cells.length < 3) return;
    const [epRaw, title, passage] = cells;
    if (!/^\d+$/.test(epRaw)) return; // header row
    episodes.set(epRaw, { episodeNumber: epRaw, title, passage });
  });

  parsedDoc.querySelectorAll('p').forEach((p) => {
    const match = p.textContent.trim().match(/^(?:Ep\.|Episode|Session)\s*(\d+)\s*[-–—]\s*(.+)$/i);
    if (!match) return;
    const [, epRaw, title] = match;
    if (!episodes.has(epRaw)) {
      episodes.set(epRaw, { episodeNumber: epRaw, title: title.trim(), passage: '—' });
    }
  });

  return Array.from(episodes.values()).sort((a, b) => Number(a.episodeNumber) - Number(b.episodeNumber));
}

// BookmarkIcon, CopyIcon, CrossRefChip imported from ./utils/bibleUtils.jsx
export { BookmarkIcon, CopyIcon, CrossRefChip };

// ---------------------------------------------------------------------------
// IndexedDB helpers for whole-Bible search cache
// ---------------------------------------------------------------------------

const BIBLE_IDB_NAME = 'study-app-bible-index';
const BIBLE_IDB_STORE = 'bible-index';
const BIBLE_IDB_TTL = 7 * 24 * 60 * 60 * 1000; // 7 days

function openBibleIndexDB() {
  return new Promise((resolve, reject) => {
    const req = indexedDB.open(BIBLE_IDB_NAME, 1);
    req.onupgradeneeded = (e) => {
      const db = e.target.result;
      if (!db.objectStoreNames.contains(BIBLE_IDB_STORE)) {
        db.createObjectStore(BIBLE_IDB_STORE, { keyPath: 'id' });
      }
    };
    req.onsuccess = (e) => resolve(e.target.result);
    req.onerror = () => reject(req.error);
  });
}

async function getBibleIndexFromIDB(translation) {
  try {
    const db = await openBibleIndexDB();
    return new Promise((resolve, reject) => {
      const tx = db.transaction(BIBLE_IDB_STORE, 'readonly');
      const req = tx.objectStore(BIBLE_IDB_STORE).get(translation);
      req.onsuccess = () => resolve(req.result ?? null);
      req.onerror = () => reject(req.error);
    });
  } catch {
    return null;
  }
}

async function saveBibleIndexToIDB(translation, flat) {
  try {
    const db = await openBibleIndexDB();
    await new Promise((resolve, reject) => {
      const tx = db.transaction(BIBLE_IDB_STORE, 'readwrite');
      const req = tx.objectStore(BIBLE_IDB_STORE).put({ id: translation, flat, cachedAt: Date.now() });
      req.onsuccess = () => resolve();
      req.onerror = () => reject(req.error);
    });
  } catch {
    // Non-fatal; in-memory cache still works for this session
  }
}

// ---------------------------------------------------------------------------
// App component
// ---------------------------------------------------------------------------

const App = () => {
  const [availableTranslations, setAvailableTranslations] = useState(['BSB']);
  const [setup, setSetup] = useState({
    translation: 'BSB',
    book: 'Titus',
    bookAbbrev: 'TIT',
    chapter: '1',
    title: 'Titus 1 Study',
  });
  const [titleEdited, setTitleEdited] = useState(false);
  // authUser: undefined = still checking · null = signed out · object = signed in
  const [authUser, setAuthUser] = useState(undefined);
  const [authServerDown, setAuthServerDown] = useState(false);
  const [authMode, setAuthMode] = useState('login'); // 'login' | 'register'
  const [authForm, setAuthForm] = useState({ email: '', password: '' });
  const [authError, setAuthError] = useState('');
  const [authBusy, setAuthBusy] = useState(false);
  const [authMfaPending, setAuthMfaPending] = useState(false); // password ok, waiting on TOTP/backup code
  const [authMfaCode, setAuthMfaCode] = useState('');
  const [authMfaUseBackup, setAuthMfaUseBackup] = useState(false);
  const [mfaSetup, setMfaSetup] = useState(null); // { secret, qrCodeDataUrl } while setup is in progress
  const [mfaSetupCode, setMfaSetupCode] = useState('');
  const [mfaSetupError, setMfaSetupError] = useState('');
  const [mfaSetupBusy, setMfaSetupBusy] = useState(false);
  const [mfaBackupCodes, setMfaBackupCodes] = useState(null); // shown once, right after enabling
  const [mfaDisablePassword, setMfaDisablePassword] = useState('');
  const [mfaDisableError, setMfaDisableError] = useState('');
  const [podcastNameInput, setPodcastNameInput] = useState('');
  const [podcastNameSaving, setPodcastNameSaving] = useState(false);
  const [podcastNameSaved, setPodcastNameSaved] = useState(false);
  const [changePasswordForm, setChangePasswordForm] = useState({ current: '', next: '', confirm: '' });
  const [changePasswordBusy, setChangePasswordBusy] = useState(false);
  const [changePasswordError, setChangePasswordError] = useState('');
  const [changePasswordSaved, setChangePasswordSaved] = useState(false);
  const [exportMenuOpen, setExportMenuOpen] = useState(false);
  // Read-only share links
  const [shareToken, setShareToken] = useState(null);
  const [sharePanelOpen, setSharePanelOpen] = useState(false);
  const [shareBusy, setShareBusy] = useState(false);
  const [shareCopied, setShareCopied] = useState(false);
  const [sharedViewToken] = useState(() => new URLSearchParams(window.location.search).get('share'));
  const [sharedProject, setSharedProject] = useState(null);
  const [sharedError, setSharedError] = useState('');
  // Admin (only reachable/rendered when authUser.isAdmin)
  const [adminTab, setAdminTab] = useState('users'); // 'users' | 'projects'
  const [adminUsers, setAdminUsers] = useState([]);
  const [adminProjects, setAdminProjects] = useState([]);
  const [adminLoading, setAdminLoading] = useState(false);
  const [adminError, setAdminError] = useState('');
  const [adminViewProject, setAdminViewProject] = useState(null); // full project data being previewed
  const [adminResetResult, setAdminResetResult] = useState(null); // { email, temporaryPassword } shown once
  const [project, setProject] = useState(null);
  // 'home' | 'setup' | 'study' | 'settings'
  const [currentPage, setCurrentPage] = useState('home');
  const [projectIndex, setProjectIndex] = useState([]);
  const [activeChapterIndex, setActiveChapterIndex] = useState(0);
  const [showAddChapterForm, setShowAddChapterForm] = useState(false);
  const [crossRefInput, setCrossRefInput] = useState('');
  const [saveStatus, setSaveStatus] = useState('');
  const [rangeStart, setRangeStart] = useState(null);
  const [rangeEnd, setRangeEnd] = useState(null);
  const [loadingChapter, setLoadingChapter] = useState(false);
  const [errorMessage, setErrorMessage] = useState('');
  const [statusMessage, setStatusMessage] = useState('');
  const [typedChunkStart, setTypedChunkStart] = useState('');
  const [typedChunkEnd, setTypedChunkEnd] = useState('');
  const [typedChunkNextEnd, setTypedChunkNextEnd] = useState('');
  const [clickedSpanNextEnd, setClickedSpanNextEnd] = useState('');
  const [typedChunkBulk, setTypedChunkBulk] = useState('');
  const saveTimerRef = useRef(null);
  const [syncStatus, setSyncStatus] = useState('');   // '' | 'syncing' | 'synced' | 'error'
  const [staleLocalProjects, setStaleLocalProjects] = useState([]); // projects where server is newer
  const [autoRestoredCount, setAutoRestoredCount] = useState(null); // brief "synced from another device" toast
  const [suggestingGreekForChunkId, setSuggestingGreekForChunkId] = useState(null);
  const [suggestingHebrewForChunkId, setSuggestingHebrewForChunkId] = useState(null);
  // suggestModal: null | { chunkId, words: [{ strongKey, lexeme, translit, def }] }
  const [suggestModal, setSuggestModal] = useState(null);
  const [suggestSelection, setSuggestSelection] = useState(new Set());
  const [homeSearch, setHomeSearch] = useState('');
  const [homeSort, setHomeSort] = useState('recent'); // 'recent' | 'title' | 'passage'
  const [homeTagFilter, setHomeTagFilter] = useState('');
  const [homeFullTextResults, setHomeFullTextResults] = useState(null); // null=idle, []=no match, [...]= results
  const fullTextTimerRef = useRef(null);
  const [readingPlan, setReadingPlan] = useState(() => {
    try { return JSON.parse(localStorage.getItem('readingPlan') || 'null'); } catch { return null; }
  });
  const [renamingId, setRenamingId] = useState(null);
  const [renameValue, setRenameValue] = useState('');
  const [importBookAbbrev, setImportBookAbbrev] = useState(bookOptions[0].abbrev);
  const [importTranslation, setImportTranslation] = useState('BSB');
  const [importTitle, setImportTitle] = useState('');
  const [importFile, setImportFile] = useState(null);
  const [importPreview, setImportPreview] = useState(null); // parsed episode specs, before fetching chapters
  const [importBusy, setImportBusy] = useState(false);
  const [importError, setImportError] = useState('');
  const [readerBookAbbrev, setReaderBookAbbrev] = useState(bookOptions[0].abbrev);
  const [readerChapter, setReaderChapter] = useState(1);
  const [readerVerses, setReaderVerses] = useState([]);
  const [readerTotalChapters, setReaderTotalChapters] = useState(1);
  const [readerLoading, setReaderLoading] = useState(false);
  const [readerError, setReaderError] = useState('');
  const [readerInterlinear, setReaderInterlinear] = useState(null); // full book interlinear
  const [readerInterlinearLoading, setReaderInterlinearLoading] = useState(false);
  const [readerSelectedVerse, setReaderSelectedVerse] = useState(null); // verse number with open interlinear panel
  const _readerInterlinearCacheRef = useRef({});
  const [readerFontSize, setReaderFontSize] = useState(1); // em multiplier: 0.875 | 1 | 1.125 | 1.25
  const [readerBookmarks, setReaderBookmarks] = useState(() => {
    try { return JSON.parse(localStorage.getItem('reader-bookmarks') || '{}'); } catch { return {}; }
  });
  const [readerBookmarksPanelOpen, setReaderBookmarksPanelOpen] = useState(false);
  const [readerJumpVerse, setReaderJumpVerse] = useState(null); // verse number to scroll to + flash after a jump
  const [readerCrossRefs, setReaderCrossRefs] = useState(null);
  const [readerCrossRefsLoading, setReaderCrossRefsLoading] = useState(false);
  const [readerShowCrossRefs, setReaderShowCrossRefs] = useState(false);
  const _readerCrossRefCacheRef = useRef({});
  const [readerSearch, setReaderSearch] = useState('');
  const [readerSearchActive, setReaderSearchActive] = useState(false);
  const [readerSearchScope, setReaderSearchScope] = useState('chapter'); // 'chapter' | 'bible'
  const [bibleIndexStatus, setBibleIndexStatus] = useState('idle'); // 'idle' | 'loading' | 'ready' | 'error'
  const _bibleIndexCacheRef = useRef({}); // translation -> flat verse array
  const [audioBook, setAudioBook] = useState(bookOptions[0].abbrev);
  const [audioNarrator, setAudioNarrator] = useState('souer');
  const [audioState, setAudioState] = useState({ status: 'idle', chapter: 0, total: 0 });
  const [readerAudioState, setReaderAudioState] = useState({ status: 'idle', chapter: 0 });
  const [readerInkByPage, setReaderInkByPage] = useState(() => {
    try { return JSON.parse(localStorage.getItem('readerInkByPage') ?? '{}'); }
    catch { return {}; }
  });
  const [readerTextHighlights, setReaderTextHighlights] = useState(() => {
    try {
      const val = JSON.parse(localStorage.getItem('readerTextHighlights') || '[]');
      return Array.isArray(val) ? val : [];
    } catch { return []; }
  });
  // Debounce timer ref so rapid stroke updates don't flood the server
  const inkSaveTimerRef = useRef({});
  const audioRef = useRef(null);
  const audioModeRef = useRef('book'); // 'book' | 'reader' — which player owns audioRef
  const audioBookRef = useRef(audioBook);
  const audioNarratorRef = useRef(audioNarrator);
  const audioStateRef = useRef(audioState);
  audioBookRef.current = audioBook;
  audioNarratorRef.current = audioNarrator;
  audioStateRef.current = audioState;

  const playAudioChapter = async (abbrev, chapterNum, narrator) => {
    const res = await fetch(`https://bible.helloao.org/api/BSB/${abbrev}/${chapterNum}.json`);
    if (!res.ok) throw new Error('Failed to load chapter audio');
    const data = await res.json();
    const total = data.book?.numberOfChapters ?? chapterNum;
    const url = data.thisChapterAudioLinks?.[narrator];
    if (!url) throw new Error('Audio not available for this narrator');
    if (!audioRef.current) {
      audioRef.current = new Audio();
      audioRef.current.addEventListener('ended', () => {
        if (audioModeRef.current === 'reader') {
          setReaderAudioState({ status: 'idle', chapter: 0 });
          return;
        }
        const nextChapter = audioStateRef.current.chapter + 1;
        if (nextChapter > audioStateRef.current.total) {
          setAudioState({ status: 'idle', chapter: 0, total: 0 });
          return;
        }
        playAudioChapter(audioBookRef.current, nextChapter, audioNarratorRef.current).catch(() =>
          setAudioState({ status: 'error', chapter: 0, total: 0 }),
        );
      });
    }
    const audioEl = audioRef.current;
    audioEl.src = url;
    await audioEl.play();
    if (audioModeRef.current === 'reader') {
      setReaderAudioState({ status: 'playing', chapter: chapterNum });
    } else {
      setAudioState({ status: 'playing', chapter: chapterNum, total });
    }
  };

  const handlePlayBookAudio = async () => {
    audioModeRef.current = 'book';
    try {
      await playAudioChapter(audioBook, 1, audioNarrator);
    } catch {
      setAudioState({ status: 'error', chapter: 0, total: 0 });
    }
  };

  const handleStopBookAudio = () => {
    if (audioRef.current) {
      audioRef.current.pause();
      audioRef.current.src = '';
    }
    setAudioState({ status: 'idle', chapter: 0, total: 0 });
    setReaderAudioState({ status: 'idle', chapter: 0 });
  };

  const handleToggleBookAudioPause = () => {
    if (!audioRef.current) return;
    if (audioRef.current.paused) {
      audioRef.current.play();
      setAudioState((prev) => ({ ...prev, status: 'playing' }));
    } else {
      audioRef.current.pause();
      setAudioState((prev) => ({ ...prev, status: 'paused' }));
    }
  };

  const handlePlayReaderAudio = async () => {
    audioModeRef.current = 'reader';
    try {
      await playAudioChapter(readerBookAbbrev, readerChapter, audioNarrator);
    } catch {
      setReaderAudioState({ status: 'error', chapter: 0 });
    }
  };

  const handleToggleReaderAudioPause = () => {
    if (!audioRef.current) return;
    if (audioRef.current.paused) {
      audioRef.current.play();
      setReaderAudioState((prev) => ({ ...prev, status: 'playing' }));
    } else {
      audioRef.current.pause();
      setReaderAudioState((prev) => ({ ...prev, status: 'paused' }));
    }
  };

  // Auto-switch the reader's audio to match the book/chapter currently being read.
  useEffect(() => {
    if (audioModeRef.current !== 'reader') return;
    if (readerAudioState.status !== 'playing' && readerAudioState.status !== 'paused') return;
    if (readerAudioState.chapter === readerChapter) return;
    const wasPaused = readerAudioState.status === 'paused';
    playAudioChapter(readerBookAbbrev, readerChapter, audioNarrator)
      .then(() => {
        if (wasPaused && audioRef.current) {
          audioRef.current.pause();
          setReaderAudioState((prev) => ({ ...prev, status: 'paused' }));
        }
      })
      .catch(() => setReaderAudioState({ status: 'error', chapter: 0 }));
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [readerBookAbbrev, readerChapter]);

  useEffect(() => () => {
    if (audioRef.current) {
      audioRef.current.pause();
      audioRef.current.src = '';
    }
  }, []);
  const [studyLayout, setStudyLayout] = useState(
    () => localStorage.getItem('studyLayout') || 'stacked',
  ); // 'stacked' | 'split' | 'annotate'
  const [drawTool, setDrawTool] = useState('pen');
  const [drawColor, setDrawColor] = useState('#0f172a');
  const [drawSize, setDrawSize] = useState(0.003);
  const [activeStudyTab, setActiveStudyTab] = useState(
    () => localStorage.getItem('activeStudyTab') || 'notes',
  );
  const [mobileStudyTab, setMobileStudyTab] = useState('scripture');

  useEffect(() => {
    localStorage.setItem('studyLayout', studyLayout);
  }, [studyLayout]);

  useEffect(() => {
    localStorage.setItem('activeStudyTab', activeStudyTab);
  }, [activeStudyTab]);

  // Full-text search across all project notes (debounced, synchronous localStorage read)
  useEffect(() => {
    clearTimeout(fullTextTimerRef.current);
    const q = homeSearch.trim().toLowerCase();
    if (q.length < 3) { setHomeFullTextResults(null); return; }
    fullTextTimerRef.current = window.setTimeout(() => {
      const index = loadProjectIndex();
      const results = [];
      for (const entry of index) {
        const proj = loadProjectById(entry.id);
        if (!proj) continue;
        const matches = [];
        for (const [chIdx, ch] of (proj.chapters ?? []).entries()) {
          for (const chunk of (ch.chunks ?? [])) {
            for (const field of ['observation', 'interpretation', 'application', 'generalNotes', 'finalScript']) {
              const text = (chunk[field] ?? '').toLowerCase();
              if (!text.includes(q)) continue;
              const idx = text.indexOf(q);
              const raw = chunk[field] ?? '';
              const start = Math.max(0, idx - 40);
              const snippet = (start > 0 ? '…' : '') + raw.slice(start, idx + q.length + 60) + (idx + q.length + 60 < raw.length ? '…' : '');
              matches.push({ chunkId: chunk.id, ref: formatChunkReference(proj, chIdx, chunk, '–'), field, snippet });
              break; // one match per chunk
            }
          }
        }
        if (matches.length > 0) results.push({ projectId: entry.id, projectTitle: entry.title, matches });
      }
      setHomeFullTextResults(results);
    }, 300);
  }, [homeSearch]);

  const [verseSearch, setVerseSearch] = useState('');
  const [collapsedSections, setCollapsedSections] = useState({});
  const [commentarySource, setCommentarySource] = useState('matthew-henry');
  const [commentaryData, setCommentaryData] = useState(null); // { content: [...] } for current chapter
  const [commentaryLoading, setCommentaryLoading] = useState(false);
  const [commentaryError, setCommentaryError] = useState('');
  const _commentaryCacheRef = useRef({});
  const [interlinearData, setInterlinearData] = useState(null);
  const [interlinearLoading, setInterlinearLoading] = useState(false);
  const [interlinearError, setInterlinearError] = useState('');
  const _interlinearCacheRef = useRef({});

  const loadInterlinearChapter = async (bookAbbrev, chapterNumber) => {
    if (_interlinearCacheRef.current[bookAbbrev]) return _interlinearCacheRef.current[bookAbbrev];
    const res = await fetch(`/interlinear/${bookAbbrev}.json`);
    if (!res.ok) throw new Error('No interlinear data for this book.');
    const data = await res.json();
    _interlinearCacheRef.current[bookAbbrev] = data;
    return data;
  };

  const loadCommentaryChapter = async (commentaryId, bookAbbrev, chapterNumber) => {
    const cacheKey = `${commentaryId}/${bookAbbrev}/${chapterNumber}`;
    if (_commentaryCacheRef.current[cacheKey]) return _commentaryCacheRef.current[cacheKey];
    const res = await fetch(`https://bible.helloao.org/api/c/${commentaryId}/${bookAbbrev}/${chapterNumber}.json`);
    const contentType = res.headers.get('content-type') || '';
    if (!res.ok || !contentType.includes('json')) throw new Error(`No ${commentaryId} commentary for this chapter.`);
    const data = await res.json();
    _commentaryCacheRef.current[cacheKey] = data;
    return data;
  };

  // ---------------------------------------------------------------------------
  // Bible reader (read-only browsing, separate from study projects)
  // ---------------------------------------------------------------------------

  // The HelloAO API has no search endpoint, so whole-Bible search fetches the
  // entire translation once (~7MB) and searches an in-memory flat verse index
  // client-side. Cached per translation for the rest of the session (not persisted
  // across reloads, to avoid eating into localStorage/IndexedDB quota for a cache
  // that's cheap enough to rebuild once per visit).
  const loadBibleIndex = async () => {
    if (_bibleIndexCacheRef.current.BSB) {
      setBibleIndexStatus('ready');
      return;
    }
    setBibleIndexStatus('loading');
    try {
      // Check IndexedDB cache before hitting the network (~7 MB fetch)
      const cached = await getBibleIndexFromIDB('BSB');
      if (cached?.flat && Date.now() - cached.cachedAt < BIBLE_IDB_TTL) {
        _bibleIndexCacheRef.current.BSB = cached.flat;
        setBibleIndexStatus('ready');
        return;
      }

      const res = await fetch('https://bible.helloao.org/api/BSB/complete.json');
      if (!res.ok) throw new Error('Unable to load the full Bible for search.');
      const data = await res.json();
      const flat = [];
      for (const book of data.books ?? []) {
        for (const ch of book.chapters ?? []) {
          const verses = parseBibleChapter({ chapter: ch.chapter });
          for (const v of verses) {
            if (v.text) {
              flat.push({ bookAbbrev: book.id, bookName: book.commonName || book.name, chapter: ch.chapter.number, verse: v.number, text: v.text });
            }
          }
        }
      }
      _bibleIndexCacheRef.current.BSB = flat;
      setBibleIndexStatus('ready');
      // Persist for future visits — non-blocking
      saveBibleIndexToIDB('BSB', flat);
    } catch {
      setBibleIndexStatus('error');
    }
  };

  const loadReaderChapter = async (bookAbbrev, chapterNumber) => {
    setReaderLoading(true);
    setReaderError('');
    try {
      const res = await fetch(`https://bible.helloao.org/api/BSB/${bookAbbrev}/${chapterNumber}.json`);
      if (!res.ok) throw new Error('Unable to load chapter.');
      const data = await res.json();
      const verses = parseBibleChapter(data);
      if (!Array.isArray(verses) || verses.length === 0) {
        throw new Error('Invalid Bible data returned.');
      }
      setReaderVerses(verses);
      setReaderTotalChapters(data.book?.numberOfChapters ?? chapterNumber);
    } catch (err) {
      setReaderError(err.message || 'Failed to load chapter.');
      setReaderVerses([]);
    } finally {
      setReaderLoading(false);
    }
  };

  useEffect(() => {
    if (currentPage !== 'reader') return;
    loadReaderChapter(readerBookAbbrev, readerChapter);
  }, [currentPage, readerBookAbbrev, readerChapter]);

  // After navigating to a verse (from the bookmarks panel or a whole-Bible search
  // result), scroll to it and briefly flash its background so it's easy to spot.
  useEffect(() => {
    if (readerJumpVerse == null || readerLoading) return;
    const el = document.getElementById(`reader-verse-${readerJumpVerse}`);
    if (!el) return;
    el.scrollIntoView({ behavior: 'smooth', block: 'center' });
    const prevTransition = el.style.transition;
    const prevBackground = el.style.backgroundColor;
    el.style.transition = 'background-color 0.4s';
    el.style.backgroundColor = '#fef08a';
    const t = window.setTimeout(() => {
      el.style.backgroundColor = prevBackground;
      window.setTimeout(() => { el.style.transition = prevTransition; }, 400);
    }, 1200);
    setReaderJumpVerse(null);
    return () => window.clearTimeout(t);
  }, [readerVerses, readerLoading, readerJumpVerse]);

  const openBibleReader = () => {
    setCurrentPage('reader');
  };

  // Navigates the reader to a specific verse (bookmarks panel, search results) and
  // scrolls/flashes it once that chapter's verses have loaded.
  const jumpToReaderVerse = (bookAbbrev, chapter, verseNumber) => {
    setReaderBookAbbrev(bookAbbrev);
    setReaderChapter(chapter);
    setReaderSelectedVerse(null);
    setReaderInterlinear(null);
    setReaderCrossRefs(null);
    setReaderSearch('');
    setReaderSearchActive(false);
    setReaderBookmarksPanelOpen(false);
    setReaderJumpVerse(verseNumber);
  };

  const readerGoToPreviousChapter = () => {
    if (readerChapter > 1) { setReaderChapter((c) => c - 1); setReaderSelectedVerse(null); setReaderCrossRefs(null); setReaderSearch(''); setReaderSearchActive(false); }
  };

  const readerGoToNextChapter = () => {
    if (readerChapter < readerTotalChapters) { setReaderChapter((c) => c + 1); setReaderSelectedVerse(null); setReaderCrossRefs(null); setReaderSearch(''); setReaderSearchActive(false); }
  };

  const handleReaderBookChange = (abbrev) => {
    setReaderBookAbbrev(abbrev);
    setReaderChapter(1);
    setReaderSelectedVerse(null);
    setReaderInterlinear(null);
    setReaderCrossRefs(null);
    setReaderSearch('');
    setReaderSearchActive(false);
  };

  const loadReaderInterlinear = async (bookAbbrev) => {
    if (_readerInterlinearCacheRef.current[bookAbbrev]) {
      setReaderInterlinear(_readerInterlinearCacheRef.current[bookAbbrev]);
      return;
    }
    setReaderInterlinearLoading(true);
    try {
      const res = await fetch(`/interlinear/${bookAbbrev}.json`);
      if (!res.ok) throw new Error('no interlinear');
      const data = await res.json();
      _readerInterlinearCacheRef.current[bookAbbrev] = data;
      setReaderInterlinear(data);
    } catch {
      setReaderInterlinear(null);
    } finally {
      setReaderInterlinearLoading(false);
    }
  };

  useEffect(() => {
    if (currentPage !== 'reader') return;
    loadReaderInterlinear(readerBookAbbrev);
    setReaderSelectedVerse(null);
  }, [currentPage, readerBookAbbrev]);

  useEffect(() => {
    if (currentPage !== 'reader' || !readerShowCrossRefs) return;
    loadReaderCrossRefs(readerBookAbbrev, readerChapter);
  }, [currentPage, readerBookAbbrev, readerChapter, readerShowCrossRefs]);

  const speakOriginalWord = (word, strongsNum) => {
    if (!window.speechSynthesis) return;
    if (window.speechSynthesis.speaking) window.speechSynthesis.cancel();
    const lang = strongsNum?.startsWith('H') ? 'he-IL' : 'el-GR';
    const voices = window.speechSynthesis.getVoices();
    const match = voices.find((v) => v.lang.startsWith(lang.split('-')[0]));
    const utt = new SpeechSynthesisUtterance(word + '.');
    utt.lang = lang;
    utt.rate = 0.85;
    if (match) utt.voice = match;
    // Chain a silent tail so the audio session fades rather than clicks shut
    utt.onend = () => {
      const tail = new SpeechSynthesisUtterance('     ');
      tail.volume = 0;
      tail.lang = lang;
      if (match) tail.voice = match;
      window.speechSynthesis.speak(tail);
    };
    window.speechSynthesis.speak(utt);
  };

  // Reader helpers: bookmarks, cross-refs, copy verse
  const BOOKMARK_COLORS = ['#fef08a', '#bbf7d0', '#bfdbfe', '#fecaca', '#e9d5ff'];

  const toggleReaderBookmark = (verseKey) => {
    setReaderBookmarks((prev) => {
      const next = { ...prev };
      if (next[verseKey]) {
        delete next[verseKey];
      } else {
        const usedColors = Object.values(next);
        const color = BOOKMARK_COLORS.find((c) => !usedColors.includes(c)) ?? BOOKMARK_COLORS[0];
        next[verseKey] = color;
      }
      try { localStorage.setItem('reader-bookmarks', JSON.stringify(next)); } catch {}
      return next;
    });
  };

  const updateReaderPageInk = useCallback((bookAbbrev, chapter, strokes) => {
    const key = `${bookAbbrev}_${chapter}`;
    setReaderInkByPage((prev) => {
      const next = { ...prev, [key]: strokes };
      try { localStorage.setItem('readerInkByPage', JSON.stringify(next)); } catch {}
      return next;
    });
    // Debounce server sync — wait 1 s after last stroke before sending
    clearTimeout(inkSaveTimerRef.current[key]);
    inkSaveTimerRef.current[key] = setTimeout(() => {
      fetch(`/api/reader/ink/${encodeURIComponent(bookAbbrev)}/${encodeURIComponent(chapter)}`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ strokes }),
      }).catch(() => {}); // silent — localStorage already saved locally
    }, 1000);
  }, []);

  const cycleBookmarkColor = (verseKey) => {
    setReaderBookmarks((prev) => {
      const cur = prev[verseKey];
      if (!cur) return prev;
      const idx = BOOKMARK_COLORS.indexOf(cur);
      const next = { ...prev, [verseKey]: BOOKMARK_COLORS[(idx + 1) % BOOKMARK_COLORS.length] };
      try { localStorage.setItem('reader-bookmarks', JSON.stringify(next)); } catch {}
      return next;
    });
  };

  const addTextHighlight = (highlight) => {
    const id = (typeof crypto !== 'undefined' && crypto.randomUUID) ? crypto.randomUUID() : `${Date.now()}-${Math.random()}`;
    setReaderTextHighlights(prev => {
      const next = [...prev, { id, ...highlight }];
      try { localStorage.setItem('readerTextHighlights', JSON.stringify(next)); } catch {}
      return next;
    });
  };

  const removeTextHighlight = (id) => {
    setReaderTextHighlights(prev => {
      const next = prev.filter(h => h.id !== id);
      try { localStorage.setItem('readerTextHighlights', JSON.stringify(next)); } catch {}
      return next;
    });
  };

  const loadReaderCrossRefs = async (bookAbbrev, chapterNumber) => {
    const cacheKey = `${bookAbbrev}/${chapterNumber}`;
    if (_readerCrossRefCacheRef.current[cacheKey]) {
      setReaderCrossRefs(_readerCrossRefCacheRef.current[cacheKey]);
      return;
    }
    setReaderCrossRefsLoading(true);
    try {
      const res = await fetch(`https://bible.helloao.org/api/d/open-cross-ref/${bookAbbrev}/${chapterNumber}.json`);
      if (!res.ok) throw new Error('no cross-refs');
      const data = await res.json();
      const byVerse = {};
      for (const item of data.chapter?.content ?? []) {
        if (item.references?.length) byVerse[item.verse] = item.references;
      }
      _readerCrossRefCacheRef.current[cacheKey] = byVerse;
      setReaderCrossRefs(byVerse);
    } catch {
      setReaderCrossRefs(null);
    } finally {
      setReaderCrossRefsLoading(false);
    }
  };

  const copyVerse = (bookName, chapterNum, verseNum, text) => {
    const citation = `${bookName} ${chapterNum}:${verseNum} BSB — ${text}`;
    navigator.clipboard?.writeText(citation);
  };

  // ---------------------------------------------------------------------------
  // Startup: migrate old keys and load index
  // ---------------------------------------------------------------------------
  useEffect(() => {
    migrateOldStorageKeys();
    setProjectIndex(loadProjectIndex());

    getCurrentUser().then((result) => {
      if (result.ok) {
        setAuthUser(result.user);
        if (result.user) {
          setProjectIndex(switchStorageUser(result.user.id));
          // Load reader ink from the server; server wins over any stale localStorage copy
          fetch('/api/reader/ink')
            .then((r) => r.ok ? r.json() : null)
            .then((serverInk) => {
              if (!serverInk) return;
              setReaderInkByPage((local) => {
                const merged = { ...local, ...serverInk };
                try { localStorage.setItem('readerInkByPage', JSON.stringify(merged)); } catch {}
                return merged;
              });
            })
            .catch(() => {});
        }
      } else {
        // Genuine network failure (server unreachable) — fall back to local-only mode.
        setAuthServerDown(true);
        setAuthUser(null);
      }
    });
  }, []);

  // Keep the Settings page's podcast-name field in sync with whichever account is signed in.
  useEffect(() => {
    setPodcastNameInput(authUser?.podcastName ?? '');
  }, [authUser?.id]);

  // A ?share=TOKEN URL loads a read-only view of someone else's project — no
  // login required, so this runs independent of auth state entirely.
  useEffect(() => {
    if (!sharedViewToken) return;
    getSharedProject(sharedViewToken).then((result) => {
      if (result.ok) {
        setSharedProject(result.data);
      } else {
        setSharedError(result.error ?? 'This share link is invalid or has been revoked.');
      }
    });
  }, [sharedViewToken]);

  // Loads current share status whenever the study page's project changes.
  useEffect(() => {
    if (!project?.id || currentPage !== 'study') return;
    setShareToken(null);
    getShareStatus(project.id).then((result) => {
      if (result.ok) setShareToken(result.data.shareToken);
    });
  }, [project?.id, currentPage]);

  const loadAdminData = () => {
    setAdminLoading(true);
    setAdminError('');
    Promise.all([adminListUsers(), adminListProjects()]).then(([usersResult, projectsResult]) => {
      setAdminLoading(false);
      if (!usersResult.ok || !projectsResult.ok) {
        setAdminError(usersResult.error ?? projectsResult.error ?? 'Failed to load admin data.');
        return;
      }
      setAdminUsers(usersResult.data);
      setAdminProjects(projectsResult.data);
    });
  };

  useEffect(() => {
    if (currentPage === 'admin' && authUser?.isAdmin) loadAdminData();
  }, [currentPage, authUser?.isAdmin]);

  // Once we know who's signed in, reconcile the local project index against the server.
  // Runs on every authUser change (including logout -> different login) so a previous
  // account's stale suggestions never linger after switching users. Projects that exist
  // on the server but not on this device (e.g. a brand new browser) are pulled down
  // automatically instead of requiring a manual "Restore" click per project.
  useEffect(() => {
    if (!authUser) {
      setStaleLocalProjects([]);
      return;
    }
    let cancelled = false;
    const localIndex = loadProjectIndex();
    listRemoteProjects().then(async (result) => {
      if (!result.ok || cancelled) return;
      const localMap = new Map(localIndex.map((e) => [e.id, e]));

      const missing = result.data.filter((e) => !localMap.has(e.id));
      if (missing.length > 0) {
        const fetched = await Promise.all(missing.map((e) => loadRemoteProject(e.id)));
        fetched.filter((r) => r.ok).forEach((r) => saveProjectToStorage(r.data));
        if (cancelled) return;
        const restoredCount = fetched.filter((r) => r.ok).length;
        if (restoredCount > 0) {
          setProjectIndex(loadProjectIndex());
          setAutoRestoredCount(restoredCount);
          window.setTimeout(() => setAutoRestoredCount(null), 4000);
        }
      }

      const stale = result.data.filter((e) => {
        const local = localMap.get(e.id);
        return local && (e.lastEdited ?? 0) > (local.lastEdited ?? 0);
      });
      if (!cancelled) setStaleLocalProjects(stale);
    });
    return () => { cancelled = true; };
  }, [authUser]);

  useEffect(() => {
    fetch('https://bible.helloao.org/api/available_translations.json')
      .then((res) => res.json())
      .then((data) => {
        if (Array.isArray(data) && data.length > 0) {
          setAvailableTranslations(data);
        }
      })
      .catch(() => {});
  }, []);

  useEffect(() => {
    if (!titleEdited) {
      setSetup((current) => ({
        ...current,
        title: `${current.book} ${current.chapter} Study`,
      }));
    }
  }, [setup.book, setup.chapter, titleEdited]);

  // Reset cross-ref input when selected chunk changes
  const allChunks = project ? project.chapters.flatMap((ch) => ch.chunks) : [];
  const activeChapter = project?.chapters[activeChapterIndex] ?? null;
  const selectedChunk = allChunks.find((c) => c.id === project?.selectedChunkId) ?? allChunks[0] ?? null;
  const selectedChunkChapterIndex = selectedChunk
    ? project.chapters.findIndex((ch) => ch.chunks.some((c) => c.id === selectedChunk.id))
    : -1;

  useEffect(() => {
    if (collapsedSections.commentary) return;
    if (selectedChunkChapterIndex < 0) return;
    const chapter = project.chapters[selectedChunkChapterIndex];
    setCommentaryLoading(true);
    setCommentaryError('');
    loadCommentaryChapter(commentarySource, chapter.bookAbbrev, chapter.chapter)
      .then((data) => setCommentaryData(data))
      .catch((err) => {
        setCommentaryData(null);
        setCommentaryError(err.message);
      })
      .finally(() => setCommentaryLoading(false));
  }, [commentarySource, selectedChunk?.id, selectedChunkChapterIndex, collapsedSections.commentary]);

  useEffect(() => {
    if (collapsedSections.interlinear) return;
    if (selectedChunkChapterIndex < 0) return;
    const chapter = project.chapters[selectedChunkChapterIndex];
    setInterlinearLoading(true);
    setInterlinearError('');
    loadInterlinearChapter(chapter.bookAbbrev, chapter.chapter)
      .then((data) => setInterlinearData(data?.[String(chapter.chapter)] ?? null))
      .catch((err) => {
        setInterlinearData(null);
        setInterlinearError(err.message);
      })
      .finally(() => setInterlinearLoading(false));
  }, [selectedChunk?.id, selectedChunkChapterIndex, collapsedSections.interlinear]);

  useEffect(() => {
    setCrossRefSuggestions([]);
    setTagInput('');
  }, [selectedChunk?.id]);
  const selectedChunkChapter = selectedChunkChapterIndex >= 0
    ? project.chapters[selectedChunkChapterIndex]
    : null;
  const selectedChunkVerses = selectedChunk
    ? getChunkVerseEntries(project, selectedChunkChapterIndex, selectedChunk)
    : [];
  const selectedChunkGlobalIndex = allChunks.findIndex((c) => c.id === project?.selectedChunkId);

  useEffect(() => {
    setCrossRefInput('');
  }, [project?.selectedChunkId]);

  // Keyboard shortcuts: arrow keys navigate chunks on the study page, Esc closes modals
  useEffect(() => {
    const handleKeyDown = (event) => {
      const tag = event.target?.tagName;
      const isEditable = tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || event.target?.isContentEditable;

      if (event.key === 'Escape') {
        if (suggestModal) {
          setSuggestModal(null);
          setSuggestSelection(new Set());
        }
        return;
      }

      if (currentPage !== 'study' || isEditable || suggestModal) return;

      if (event.key === 'ArrowLeft') {
        event.preventDefault();
        goToPreviousChunk();
      } else if (event.key === 'ArrowRight') {
        event.preventDefault();
        goToNextChunk();
      }
    };
    window.addEventListener('keydown', handleKeyDown);
    return () => window.removeEventListener('keydown', handleKeyDown);
  }, [currentPage, suggestModal, selectedChunkGlobalIndex, allChunks]);

  // Autosave
  useEffect(() => {
    if (!project) return;
    if (saveTimerRef.current) window.clearTimeout(saveTimerRef.current);
    saveTimerRef.current = window.setTimeout(async () => {
      // Stamp lastEdited once so localStorage and server receive the same value
      const toSave = { ...project, lastEdited: Date.now() };

      // 1. Save locally
      saveProjectToStorage(toSave);
      setProjectIndex(loadProjectIndex());
      setSaveStatus('Saved');
      window.setTimeout(() => setSaveStatus(''), 1400);

      // 2. Sync to server with the same timestamped object
      setSyncStatus('syncing');
      const result = await saveRemoteProject(toSave);
      setSyncStatus(result.ok ? 'synced' : 'error');
      // On success clear after 2.5s; on error keep visible until next successful sync
      if (result.ok) window.setTimeout(() => setSyncStatus(''), 2500);
    }, 1000);
    return () => {
      if (saveTimerRef.current) window.clearTimeout(saveTimerRef.current);
    };
  }, [project]);

  // ---------------------------------------------------------------------------
  // Setup helpers
  // ---------------------------------------------------------------------------

  const handleSetupField = (field, value) => {
    if (field === 'book') {
      const selected = bookOptions.find((book) => book.abbrev === value) ?? bookOptions[0];
      setSetup((current) => ({
        ...current,
        book: selected.name,
        bookAbbrev: selected.abbrev,
      }));
    } else {
      setSetup((current) => ({ ...current, [field]: value }));
    }
  };

  const handleLoadChapter = async () => {
    if (!setup.bookAbbrev || !setup.chapter) {
      setErrorMessage('Please choose a book and chapter.');
      return;
    }
    setErrorMessage('');
    setLoadingChapter(true);
    try {
      const response = await fetch(
        `https://bible.helloao.org/api/${setup.translation}/${setup.bookAbbrev}/${setup.chapter}.json`,
      );
      if (!response.ok) throw new Error('Unable to load chapter.');
      const data = await response.json();
      const verses = parseBibleChapter(data);
      if (!data || !Array.isArray(verses) || verses.length === 0) {
        throw new Error('Invalid Bible data returned.');
      }

      if (project) {
        // Adding a chapter to an existing project
        const alreadyExists = project.chapters.some(
          (ch) => ch.bookAbbrev === setup.bookAbbrev && ch.chapter === setup.chapter,
        );
        if (alreadyExists) {
          setErrorMessage('That chapter is already in this project.');
          setLoadingChapter(false);
          return;
        }
        const newChapter = {
          book: setup.book,
          bookAbbrev: setup.bookAbbrev,
          chapter: setup.chapter,
          verses,
          chunks: [],
        };
        updateProject((current) => {
          const updatedChapters = [...current.chapters, newChapter];
          return { ...current, chapters: updatedChapters };
        });
        setActiveChapterIndex(project.chapters.length);
        setShowAddChapterForm(false);
        setErrorMessage('');
      } else {
        // Creating a brand-new project
        const newProject = {
          id: makeId(),
          title: setup.title,
          translation: setup.translation,
          lastEdited: Date.now(),
          selectedChunkId: null,
          chapters: [
            {
              book: setup.book,
              bookAbbrev: setup.bookAbbrev,
              chapter: setup.chapter,
              verses,
              chunks: [],
            },
          ],
        };
        setProject(newProject);
        setActiveChapterIndex(0);
        setCurrentPage('setup');
      }
    } catch (error) {
      setErrorMessage(error.message || 'Failed to load chapter.');
    } finally {
      setLoadingChapter(false);
    }
  };

  const updateProject = (updater) => {
    setProject((current) => (current ? updater(current) : current));
  };

  const beginStudying = () => {
    if (!project || allChunks.length === 0) return;
    updateProject((current) => ({
      ...current,
      selectedChunkId: current.selectedChunkId || allChunks[0]?.id || null,
    }));
    setCurrentPage('study');
  };

  // ---------------------------------------------------------------------------
  // Chunk navigation
  // ---------------------------------------------------------------------------

  const goToPreviousChunk = () => {
    if (selectedChunkGlobalIndex <= 0) return;
    updateProject((current) => ({
      ...current,
      selectedChunkId: allChunks[selectedChunkGlobalIndex - 1].id,
    }));
  };

  const goToNextChunk = () => {
    if (selectedChunkGlobalIndex < 0 || selectedChunkGlobalIndex >= allChunks.length - 1) return;
    updateProject((current) => ({
      ...current,
      selectedChunkId: allChunks[selectedChunkGlobalIndex + 1].id,
    }));
  };

  // ---------------------------------------------------------------------------
  // Chunk CRUD (operates on activeChapter in setup view, on selectedChunkChapter in study)
  // ---------------------------------------------------------------------------

  const addChunkToChapter = (chapterIndex, start, end, spilloverEndVerse = null) => {
    updateProject((current) => {
      const chapters = current.chapters.map((ch, idx) => {
        if (idx !== chapterIndex) return ch;
        const overlap = ch.chunks.find(
          (c) => c.startVerse === start
            && c.endVerse === end
            && (c.spilloverEndVerse ?? null) === (spilloverEndVerse ?? null),
        );
        if (overlap) return ch;
        const newChunk = {
          id: makeId(),
          startVerse: start,
          endVerse: end,
          spilloverEndVerse,
          observation: '',
          interpretation: '',
          application: '',
          crossReferences: [],
          greekWords: [],
          generalNotes: '',
          episodeNumber: '',
          episodeTitle: '',
          finalScript: '',
          tags: [],
        };
        return { ...ch, chunks: [...ch.chunks, newChunk] };
      });
      // Find the newly added chunk id
      const newChunk = chapters[chapterIndex].chunks.at(-1);
      return { ...current, chapters, selectedChunkId: newChunk?.id ?? current.selectedChunkId };
    });
  };

  const addChunk = (start, end, spilloverEndVerse = null) => {
    if (!project) return;
    const chapter = activeChapter;
    if (!chapter) return;
    const overlap = chapter.chunks.find(
      (c) => c.startVerse === start
        && c.endVerse === end
        && (c.spilloverEndVerse ?? null) === (spilloverEndVerse ?? null),
    );
    if (overlap) {
      setStatusMessage('That chunk already exists.');
      window.setTimeout(() => setStatusMessage(''), 1800);
      return;
    }
    addChunkToChapter(activeChapterIndex, start, end, spilloverEndVerse);
  };

  const addChunkRanges = (ranges) => {
    if (!project || !activeChapter || ranges.length === 0) {
      return { addedCount: 0, skippedCount: 0 };
    }

    let addedCount = 0;
    let skippedCount = 0;
    let lastAddedChunkId = null;

    updateProject((current) => {
      const chapters = current.chapters.map((ch, idx) => {
        if (idx !== activeChapterIndex) return ch;
        const existing = new Set(
          ch.chunks.map((c) => `${c.startVerse}-${c.endVerse}-${c.spilloverEndVerse ?? ''}`),
        );
        const newChunks = [];

        ranges.forEach(({ start, end, spilloverEndVerse = null }) => {
          const key = `${start}-${end}-${spilloverEndVerse ?? ''}`;
          if (existing.has(key)) {
            skippedCount += 1;
            return;
          }
          const newChunk = {
            id: makeId(),
            startVerse: start,
            endVerse: end,
            spilloverEndVerse,
            observation: '',
            interpretation: '',
            application: '',
            crossReferences: [],
            greekWords: [],
            tags: [],
          };
          existing.add(key);
          newChunks.push(newChunk);
          addedCount += 1;
          lastAddedChunkId = newChunk.id;
        });

        return newChunks.length > 0
          ? { ...ch, chunks: [...ch.chunks, ...newChunks] }
          : ch;
      });

      return {
        ...current,
        chapters,
        selectedChunkId: lastAddedChunkId ?? current.selectedChunkId,
      };
    });

    return { addedCount, skippedCount };
  };

  const addTypedChunk = () => {
    if (!activeChapter) return;
    const start = Number.parseInt(typedChunkStart, 10);
    const end = Number.parseInt(typedChunkEnd, 10);
    const nextEndRaw = typedChunkNextEnd.trim();
    const nextEnd = nextEndRaw ? Number.parseInt(nextEndRaw, 10) : null;
    const maxVerse = activeChapter.verses.at(-1)?.number ?? 0;
    const nextChapter = project?.chapters?.[activeChapterIndex + 1] ?? null;
    const nextMaxVerse = nextChapter?.verses?.at(-1)?.number ?? 0;

    if (!Number.isInteger(start) || !Number.isInteger(end)) {
      setStatusMessage('Type a valid start and end verse.');
      window.setTimeout(() => setStatusMessage(''), 2200);
      return;
    }
    if (start < 1 || end < 1 || start > maxVerse || end > maxVerse) {
      setStatusMessage(`Verse range must be between 1 and ${maxVerse}.`);
      window.setTimeout(() => setStatusMessage(''), 2200);
      return;
    }
    if (start > end) {
      setStatusMessage('Start verse must be less than or equal to end verse.');
      window.setTimeout(() => setStatusMessage(''), 2200);
      return;
    }

    if (nextEndRaw) {
      if (!nextChapter) {
        setStatusMessage('Add the next chapter first to span chunks across chapters.');
        window.setTimeout(() => setStatusMessage(''), 2400);
        return;
      }
      if (nextChapter.bookAbbrev !== activeChapter.bookAbbrev) {
        setStatusMessage('Chapter spanning only works into the next chapter of the same book.');
        window.setTimeout(() => setStatusMessage(''), 2400);
        return;
      }
      if (!Number.isInteger(nextEnd) || nextEnd < 1 || nextEnd > nextMaxVerse) {
        setStatusMessage(`Next chapter end must be between 1 and ${nextMaxVerse}.`);
        window.setTimeout(() => setStatusMessage(''), 2400);
        return;
      }
      if (end !== maxVerse) {
        setStatusMessage('To span chapters, set End to the last verse of this chapter.');
        window.setTimeout(() => setStatusMessage(''), 2400);
        return;
      }
    }

    const { addedCount } = addChunkRanges([{ start, end, spilloverEndVerse: nextEnd }]);
    if (addedCount === 0) {
      setStatusMessage('That chunk already exists.');
      window.setTimeout(() => setStatusMessage(''), 2200);
      return;
    }

    setTypedChunkStart('');
    setTypedChunkEnd('');
    setTypedChunkNextEnd('');
  };

  const addBulkTypedChunks = () => {
    if (!activeChapter) return;
    const raw = typedChunkBulk.trim();
    if (!raw) {
      setStatusMessage('Type chunk ranges first. Example: 1-6, 7-12');
      window.setTimeout(() => setStatusMessage(''), 2200);
      return;
    }

    const maxVerse = activeChapter.verses.at(-1)?.number ?? 0;
    const nextChapter = project?.chapters?.[activeChapterIndex + 1] ?? null;
    const nextMaxVerse = nextChapter?.verses?.at(-1)?.number ?? 0;
    const parts = raw
      .split(/[\n,;]+/)
      .map((part) => part.trim())
      .filter(Boolean);

    const validRanges = [];
    let invalidCount = 0;

    parts.forEach((part) => {
      const single = /^(\d+)$/.exec(part);
      if (single) {
        const v = Number.parseInt(single[1], 10);
        if (v >= 1 && v <= maxVerse) {
          validRanges.push({ start: v, end: v });
        } else {
          invalidCount += 1;
        }
        return;
      }

      const range = /^(\d+)\s*-\s*(\d+)(?:\s*:\s*(\d+))?$/.exec(part);
      if (!range) {
        invalidCount += 1;
        return;
      }

      const start = Number.parseInt(range[1], 10);
      const end = Number.parseInt(range[2], 10);
      const spilloverEndVerse = range[3] ? Number.parseInt(range[3], 10) : null;
      if (start < 1 || end < 1 || start > end || start > maxVerse || end > maxVerse) {
        invalidCount += 1;
        return;
      }
      if (spilloverEndVerse !== null) {
        if (!nextChapter
          || nextChapter.bookAbbrev !== activeChapter.bookAbbrev
          || spilloverEndVerse < 1
          || spilloverEndVerse > nextMaxVerse
          || end !== maxVerse) {
          invalidCount += 1;
          return;
        }
      }
      validRanges.push({ start, end, spilloverEndVerse });
    });

    if (validRanges.length === 0) {
      setStatusMessage(`No valid ranges found. Use 1-${maxVerse} and format like 1-6, 7-12.`);
      window.setTimeout(() => setStatusMessage(''), 2600);
      return;
    }

    const { addedCount, skippedCount } = addChunkRanges(validRanges);
    const notes = [];
    if (addedCount > 0) notes.push(`Added ${addedCount} chunk${addedCount > 1 ? 's' : ''}`);
    if (skippedCount > 0) notes.push(`skipped ${skippedCount} duplicate${skippedCount > 1 ? 's' : ''}`);
    if (invalidCount > 0) notes.push(`ignored ${invalidCount} invalid`);
    setStatusMessage(notes.join(' • '));
    window.setTimeout(() => setStatusMessage(''), 2800);

    if (addedCount > 0) {
      setTypedChunkBulk('');
    }
  };

  const addClickSpanChunk = () => {
    if (!activeChapter || rangeStart === null) {
      setStatusMessage('Click a start verse first, then set next chapter end.');
      window.setTimeout(() => setStatusMessage(''), 2400);
      return;
    }

    const nextChapter = project?.chapters?.[activeChapterIndex + 1] ?? null;
    if (!nextChapter) {
      setStatusMessage('Add the next chapter first to span into it.');
      window.setTimeout(() => setStatusMessage(''), 2400);
      return;
    }
    if (nextChapter.bookAbbrev !== activeChapter.bookAbbrev) {
      setStatusMessage('Click-based spanning only works into the next chapter of the same book.');
      window.setTimeout(() => setStatusMessage(''), 2600);
      return;
    }

    const nextMaxVerse = nextChapter.verses.at(-1)?.number ?? 0;
    const spilloverEndVerse = Number.parseInt(clickedSpanNextEnd, 10);
    if (!Number.isInteger(spilloverEndVerse) || spilloverEndVerse < 1 || spilloverEndVerse > nextMaxVerse) {
      setStatusMessage(`Next chapter end must be between 1 and ${nextMaxVerse}.`);
      window.setTimeout(() => setStatusMessage(''), 2400);
      return;
    }

    const start = rangeStart;
    const end = activeChapter.verses.at(-1)?.number ?? start;
    const { addedCount } = addChunkRanges([{ start, end, spilloverEndVerse }]);
    if (addedCount === 0) {
      setStatusMessage('That cross-chapter chunk already exists.');
      window.setTimeout(() => setStatusMessage(''), 2400);
      return;
    }

    setClickedSpanNextEnd('');
    setRangeStart(null);
    setRangeEnd(null);
    setStatusMessage('Cross-chapter chunk added.');
    window.setTimeout(() => setStatusMessage(''), 1800);
  };

  const handleVerseClick = (verseNumber, event) => {
    if (event.shiftKey && rangeStart !== null) {
      const start = Math.min(rangeStart, verseNumber);
      const end = Math.max(rangeStart, verseNumber);
      addChunk(start, end);
      setRangeStart(null);
      setRangeEnd(null);
      return;
    }
    setRangeStart(verseNumber);
    setRangeEnd(verseNumber);
  };

  const updateChunk = (chunkId, patch) => {
    updateProject((current) => ({
      ...current,
      chapters: current.chapters.map((ch) => ({
        ...ch,
        chunks: ch.chunks.map((c) => (c.id === chunkId ? { ...c, ...patch } : c)),
      })),
    }));
  };

  const moveChunk = (chunkId, direction) => {
    updateProject((current) => ({
      ...current,
      chapters: current.chapters.map((ch) => {
        const index = ch.chunks.findIndex((c) => c.id === chunkId);
        if (index < 0) return ch;
        const nextIndex = index + direction;
        if (nextIndex < 0 || nextIndex >= ch.chunks.length) return ch;
        const updated = [...ch.chunks];
        const [removed] = updated.splice(index, 1);
        updated.splice(nextIndex, 0, removed);
        return { ...ch, chunks: updated };
      }),
    }));
  };

  const deleteChunk = (chunkId) => {
    updateProject((current) => {
      const newAllChunks = current.chapters
        .flatMap((ch) => ch.chunks)
        .filter((c) => c.id !== chunkId);
      const nextSelected =
        current.selectedChunkId === chunkId
          ? newAllChunks[0]?.id ?? null
          : current.selectedChunkId;
      return {
        ...current,
        selectedChunkId: nextSelected,
        chapters: current.chapters.map((ch) => ({
          ...ch,
          chunks: ch.chunks.filter((c) => c.id !== chunkId),
        })),
      };
    });
  };

  // ---------------------------------------------------------------------------
  // Cross-references
  // ---------------------------------------------------------------------------

  const addCrossRef = (chunkId) => {
    const ref = crossRefInput.trim();
    if (!ref) return;
    updateChunk(chunkId, {
      crossReferences: [...(selectedChunk?.crossReferences ?? []), ref],
    });
    setCrossRefInput('');
  };

  const removeCrossRef = (chunkId, ref) => {
    updateChunk(chunkId, {
      crossReferences: (selectedChunk?.crossReferences ?? []).filter((r) => r !== ref),
    });
  };

  const [tagInput, setTagInput] = useState('');

  const addTag = (chunkId) => {
    const tag = tagInput.trim();
    if (!tag) return;
    const existing = selectedChunk?.tags ?? [];
    if (existing.some((t) => t.toLowerCase() === tag.toLowerCase())) {
      setTagInput('');
      return;
    }
    updateChunk(chunkId, { tags: [...existing, tag] });
    setTagInput('');
  };

  const removeTag = (chunkId, tag) => {
    updateChunk(chunkId, {
      tags: (selectedChunk?.tags ?? []).filter((t) => t !== tag),
    });
  };

  const importFinalScriptDocx = async (chunkId, file) => {
    if (!file) return;
    try {
      const arrayBuffer = await file.arrayBuffer();
      const { value } = await mammoth.extractRawText({ arrayBuffer });
      updateChunk(chunkId, { finalScript: value.trim() });
      setStatusMessage('Imported script from DOCX.');
      window.setTimeout(() => setStatusMessage(''), 1800);
    } catch (err) {
      setStatusMessage('Could not read that DOCX file.');
      window.setTimeout(() => setStatusMessage(''), 1800);
    }
  };

  const [suggestingCrossRefs, setSuggestingCrossRefs] = useState(false);
  const [crossRefSuggestions, setCrossRefSuggestions] = useState([]);
  const _crossRefDatasetCacheRef = useRef({});

  const formatCrossRef = (ref) => {
    const book = bookOptions.find((b) => b.abbrev === ref.book);
    const bookName = book?.name ?? ref.book;
    const verseRange = ref.endVerse && ref.endVerse !== ref.verse
      ? `${ref.verse}-${ref.endVerse}`
      : `${ref.verse}`;
    return `${bookName} ${ref.chapter}:${verseRange}`;
  };

  const suggestCrossRefsForChunk = async (chunkId) => {
    const chunk = allChunks.find((c) => c.id === chunkId);
    const chapterIndex = project?.chapters.findIndex((ch) =>
      ch.chunks.some((c) => c.id === chunkId)
    ) ?? -1;
    const chapter = chapterIndex >= 0 ? project.chapters[chapterIndex] : null;
    if (!chunk || !chapter) return;

    setSuggestingCrossRefs(true);
    setCrossRefSuggestions([]);
    try {
      const cacheKey = `${chapter.bookAbbrev}/${chapter.chapter}`;
      let chapterData = _crossRefDatasetCacheRef.current[cacheKey];
      if (!chapterData) {
        const res = await fetch(`https://bible.helloao.org/api/d/open-cross-ref/${chapter.bookAbbrev}/${chapter.chapter}.json`);
        if (!res.ok) throw new Error('No cross-reference data found for this chapter.');
        chapterData = await res.json();
        _crossRefDatasetCacheRef.current[cacheKey] = chapterData;
      }

      const verseEntries = (chapterData.chapter?.content ?? []).filter(
        (item) => item.verse >= chunk.startVerse
          && item.verse <= chunk.endVerse,
      );

      const existing = new Set(chunk.crossReferences ?? []);
      const seen = new Set();
      const refs = [];
      for (const v of verseEntries) {
        for (const ref of v.references ?? []) {
          const formatted = formatCrossRef(ref);
          if (existing.has(formatted) || seen.has(formatted)) continue;
          seen.add(formatted);
          refs.push({ formatted, score: ref.score ?? 0 });
        }
      }

      refs.sort((a, b) => b.score - a.score);
      if (refs.length === 0) {
        setStatusMessage('No new cross-reference suggestions found for this passage.');
        window.setTimeout(() => setStatusMessage(''), 2500);
        return;
      }
      setCrossRefSuggestions(refs.slice(0, 12).map((r) => r.formatted));
    } catch (err) {
      setStatusMessage(`Cross-reference suggest failed: ${err.message}`);
      window.setTimeout(() => setStatusMessage(''), 3000);
    } finally {
      setSuggestingCrossRefs(false);
    }
  };

  const _verseChapterCacheRef = useRef({});

  const loadVerseText = async (refString) => {
    const parsed = parseCrossRefString(refString);
    if (!parsed) throw new Error('Could not parse reference.');
    const translation = project?.translation ?? 'BSB';
    const cacheKey = `${translation}/${parsed.bookAbbrev}/${parsed.chapter}`;
    let verses = _verseChapterCacheRef.current[cacheKey];
    if (!verses) {
      const res = await fetch(`https://bible.helloao.org/api/${translation}/${parsed.bookAbbrev}/${parsed.chapter}.json`);
      if (!res.ok) throw new Error('Could not load verse text.');
      const data = await res.json();
      verses = parseBibleChapter(data);
      _verseChapterCacheRef.current[cacheKey] = verses;
    }
    const text = verses
      .filter((v) => v.number >= parsed.verse && v.number <= parsed.endVerse)
      .map((v) => v.text)
      .join(' ');
    return text;
  };

  const addSuggestedCrossRef = (chunkId, ref) => {
    updateChunk(chunkId, {
      crossReferences: [...(selectedChunk?.crossReferences ?? []), ref],
    });
    setCrossRefSuggestions((cur) => cur.filter((r) => r !== ref));
  };

  // ---------------------------------------------------------------------------
  // Greek word helpers
  // ---------------------------------------------------------------------------

  const updateChunkWord = (chunkId, wordId, patch) => {
    updateProject((current) => ({
      ...current,
      chapters: current.chapters.map((ch) => ({
        ...ch,
        chunks: ch.chunks.map((chunk) => {
          if (chunk.id !== chunkId) return chunk;
          return {
            ...chunk,
            greekWords: chunk.greekWords.map((word) =>
              word.id === wordId ? { ...word, ...patch } : word,
            ),
          };
        }),
      })),
    }));
  };

  const addGreekWord = (chunkId) => {
    const newWord = {
      id: makeId(),
      query: '',
      strongNumber: '',
      englishGloss: '',
      lexeme: '',
      transliteration: '',
      partOfSpeech: '',
      shortDefinition: '',
      definitionHtml: '',
      loading: false,
    };
    updateProject((current) => ({
      ...current,
      chapters: current.chapters.map((ch) => ({
        ...ch,
        chunks: ch.chunks.map((chunk) =>
          chunk.id === chunkId
            ? { ...chunk, greekWords: [...chunk.greekWords, newWord] }
            : chunk,
        ),
      })),
    }));
  };

  const removeGreekWord = (chunkId, wordId) => {
    updateProject((current) => ({
      ...current,
      chapters: current.chapters.map((ch) => ({
        ...ch,
        chunks: ch.chunks.map((chunk) =>
          chunk.id === chunkId
            ? { ...chunk, greekWords: chunk.greekWords.filter((w) => w.id !== wordId) }
            : chunk,
        ),
      })),
    }));
  };

  // Cache for the NT Strong's concordance (verse → Strong's numbers), loaded once
  const _concordanceRef = useRef(null);
  const _concordanceLoadingRef = useRef(null);

  // Cache for the NT Strong's→English gloss map (from macula-greek dataset), loaded once
  const _glossRef = useRef(null);
  const _glossLoadingRef = useRef(null);
  // Cache for the Hebrew Strong's dictionary (loaded once)
  const _hebrewDictRef = useRef(null);
  const _hebrewDictLoadingRef = useRef(null);

  const loadNtGloss = async () => {
    if (_glossRef.current) return _glossRef.current;
    if (_glossLoadingRef.current) return _glossLoadingRef.current;
    _glossLoadingRef.current = fetch('/nt-strongs-gloss.json')
      .then((res) => res.json())
      .then((data) => {
        _glossRef.current = data;
        _glossLoadingRef.current = null;
        return data;
      });
    return _glossLoadingRef.current;
  };

  const loadNtConcordance = async () => {
    if (_concordanceRef.current) return _concordanceRef.current;
    if (_concordanceLoadingRef.current) return _concordanceLoadingRef.current;
    _concordanceLoadingRef.current = fetch('/nt-strongs-concordance.json')
      .then((res) => res.json())
      .then((data) => {
        _concordanceRef.current = data;
        _concordanceLoadingRef.current = null;
        return data;
      });
    return _concordanceLoadingRef.current;
  };

  const loadHebrewDict = async () => {
    if (_hebrewDictRef.current) return _hebrewDictRef.current;
    if (_hebrewDictLoadingRef.current) return _hebrewDictLoadingRef.current;
    _hebrewDictLoadingRef.current = fetch(
      'https://cdn.jsdelivr.net/gh/openscriptures/strongs@master/hebrew/strongs-hebrew-dictionary.js',
    )
      .then((res) => res.text())
      .then((text) => {
        const start = text.indexOf('{');
        const end = text.lastIndexOf('}') + 1;
        const dict = JSON.parse(text.slice(start, end));
        _hebrewDictRef.current = dict;
        _hebrewDictLoadingRef.current = null;
        return dict;
      });
    return _hebrewDictLoadingRef.current;
  };

  // Common Greek NT function words to pre-uncheck in the picker
  // (articles, prepositions, conjunctions, particles, common pronouns)
  const GREEK_FUNCTION_WORDS = new Set([
    'G3588','G3739','G3748', // ὁ (the), ὅς (who/which), ὅστις
    'G2532','G1161','G1063','G235','G3767','G2443','G5037','G3303', // καί, δέ, γάρ, ἀλλά, οὖν, ἵνα, τέ, μέν
    'G1487','G1437','G3754','G5613','G2531','G5618', // εἰ, ἐάν, ὅτι, ὡς, καθώς, ὥσπερ
    'G1722','G1519','G1537','G575','G4314','G3326','G2596','G1223', // ἐν, εἰς, ἐκ, ἀπό, πρός, μετά, κατά, διά
    'G1909','G5228','G5259','G4862','G3844','G4253','G4012','G1722', // ἐπί, ὑπέρ, ὑπό, σύν, παρά, πρό, περί
    'G3756','G3361','G3780','G3762','G3763', // οὐ, μή, οὐχί, οὐδείς, οὐδέποτε
    'G846','G1473','G4771','G2249','G5210', // αὐτός, ἐγώ, σύ, ἡμεῖς, ὑμεῖς
    'G3778','G1565','G3588', // οὗτος, ἐκεῖνος
    'G5100','G5101','G3956','G3745','G3748', // τις, τίς, πᾶς, ὅσος
    'G3779','G3568','G5119','G1161','G3767', // οὕτως, νῦν, τότε
  ]);

  const suggestGreekWordsForChunk = async (chunkId) => {
    const chunk = allChunks.find((c) => c.id === chunkId);
    const chapterIndex = project?.chapters.findIndex((ch) =>
      ch.chunks.some((c) => c.id === chunkId)
    ) ?? -1;
    const chapter = chapterIndex >= 0 ? project.chapters[chapterIndex] : null;
    if (!chunk || !chapter) return;
    if (!NT_BOOK_NUMBER[chapter.bookAbbrev]) {
      setStatusMessage('Auto-suggest only works for New Testament books.');
      window.setTimeout(() => setStatusMessage(''), 2500);
      return;
    }
    setSuggestingGreekForChunkId(chunkId);
    try {
      const [concordance, dict, gloss] = await Promise.all([loadNtConcordance(), loadGreekDict(), loadNtGloss()]);
      const bookData = concordance[chapter.bookAbbrev] ?? {};
      const chapterData = bookData[chapter.chapter] ?? {};
      const nextChapterData = chunkSpansNextChapter(project, chapterIndex, chunk)
        ? bookData[project.chapters[chapterIndex + 1].chapter] ?? {}
        : {};

      const strongsInRange = new Set();
      for (let v = chunk.startVerse; v <= chunk.endVerse; v++) {
        for (const s of chapterData[String(v)] ?? []) strongsInRange.add(s);
      }
      if (chunkSpansNextChapter(project, chapterIndex, chunk)) {
        for (let v = 1; v <= chunk.spilloverEndVerse; v++) {
          for (const s of nextChapterData[String(v)] ?? []) strongsInRange.add(s);
        }
      }

      if (strongsInRange.size === 0) {
        setStatusMessage("No Strong's data found for this passage.");
        window.setTimeout(() => setStatusMessage(''), 3000);
        return;
      }

      const existingNumbers = new Set(
        chunk.greekWords.map((w) => w.strongNumber.toUpperCase()).filter(Boolean)
      );

      const modalWords = [];
      for (const strongKey of strongsInRange) {
        if (existingNumbers.has(strongKey)) continue;
        const entry = dict[strongKey];
        modalWords.push({
          strongKey,
          lexeme: entry?.lemma ?? '',
          translit: entry?.translit ?? '',
          def: gloss[strongKey] ?? entry?.kjv_def ?? 'No definition found.',
          entry,
        });
      }

      if (modalWords.length === 0) {
        setStatusMessage('All words from this passage are already added.');
        window.setTimeout(() => setStatusMessage(''), 2000);
        return;
      }

      // Pre-select content words; pre-uncheck known function words
      const preSelected = new Set(
        modalWords
          .filter((w) => !GREEK_FUNCTION_WORDS.has(w.strongKey))
          .map((w) => w.strongKey)
      );
      setSuggestModal({
        chunkId,
        language: 'greek',
        helperText: "Content words are pre-checked. Uncheck any you don't need.",
        words: modalWords,
      });
      setSuggestSelection(preSelected);
    } catch (err) {
      setStatusMessage(`Suggest failed: ${err.message}`);
      window.setTimeout(() => setStatusMessage(''), 3000);
    } finally {
      setSuggestingGreekForChunkId(null);
    }
  };

  // Extract a single short English gloss from a verbose kjv_def string.
  // e.g. "[idiom] burn (incense), be mindful..." → "burn"
  //      "another, [idiom] (blood-) thirsty..." → "another"
  const shortHebrewGloss = (kjvDef) => {
    if (!kjvDef) return '';
    let s = kjvDef.trim();
    // strip leading [tag] tokens like [idiom], [phrase]
    s = s.replace(/^(\[[^\]]+\]\s*)+/, '').trim();
    // take first chunk before comma, semicolon, or opening paren
    s = s.split(/[,;(]/)[0].trim();
    // strip trailing punctuation
    s = s.replace(/[.\-]+$/, '').trim();
    return s;
  };

  const ENGLISH_STOP_WORDS = new Set([
    'the', 'and', 'for', 'that', 'with', 'this', 'from', 'were', 'was', 'have', 'has',
    'had', 'are', 'but', 'not', 'you', 'your', 'his', 'her', 'their', 'they', 'them',
    'our', 'out', 'into', 'over', 'under', 'upon', 'then', 'than', 'who', 'what', 'when',
    'where', 'why', 'how', 'also', 'there', 'here', 'all', 'any', 'one', 'two', 'three',
    'he', 'she', 'it', 'we', 'i', 'me', 'my', 'mine', 'ours', 'theirs', 'its',
  ]);

  const suggestHebrewWordsForChunk = async (chunkId) => {
    const chunk = allChunks.find((c) => c.id === chunkId);
    const chapterIndex = project?.chapters.findIndex((ch) =>
      ch.chunks.some((c) => c.id === chunkId)
    ) ?? -1;
    const chapter = chapterIndex >= 0 ? project.chapters[chapterIndex] : null;
    if (!chunk || !chapter) return;
    if (NT_BOOK_NUMBER[chapter.bookAbbrev]) {
      setStatusMessage('Hebrew suggestions are for Old Testament passages.');
      window.setTimeout(() => setStatusMessage(''), 2500);
      return;
    }

    setSuggestingHebrewForChunkId(chunkId);
    try {
      const dict = await loadHebrewDict();
      const versesInChunk = getChunkVerseEntries(project, chapterIndex, chunk)
        .map((v) => v.text)
        .join(' ')
        .toLowerCase();

      const candidateWords = Array.from(new Set(
        versesInChunk
          .replace(/[^a-z\s]/g, ' ')
          .split(/\s+/)
          .map((w) => w.trim())
          .filter((w) => w.length >= 4 && !ENGLISH_STOP_WORDS.has(w)),
      ));

      if (candidateWords.length === 0) {
        setStatusMessage('No Hebrew suggestion candidates found in this passage.');
        window.setTimeout(() => setStatusMessage(''), 2500);
        return;
      }

      const existingNumbers = new Set(
        chunk.greekWords.map((w) => (w.strongNumber || '').toUpperCase()).filter(Boolean)
      );

      const seen = new Set();
      const modalWords = [];
      const entries = Object.entries(dict);

      for (const token of candidateWords) {
        const match = entries.find(([, entry]) => {
          const defs = `${entry.kjv_def || ''} ${entry.strongs_def || ''}`.toLowerCase();
          return defs.split(/[,;()\s]+/).includes(token);
        });
        if (!match) continue;
        const [strongKey, entry] = match;
        if (!/^H\d+$/i.test(strongKey)) continue;
        const normalized = strongKey.toUpperCase();
        if (existingNumbers.has(normalized) || seen.has(normalized)) continue;
        seen.add(normalized);
        modalWords.push({
          strongKey: normalized,
          lexeme: entry?.lemma ?? '',
          translit: entry?.xlit ?? '',
          def: shortHebrewGloss(entry?.kjv_def) || 'No definition found.',
          fullDef: entry?.kjv_def ?? '',
          entry,
        });
        if (modalWords.length >= 20) break;
      }

      if (modalWords.length === 0) {
        setStatusMessage('No Hebrew suggestions found from this passage text.');
        window.setTimeout(() => setStatusMessage(''), 2500);
        return;
      }

      setSuggestModal({
        chunkId,
        language: 'hebrew',
        helperText: 'Heuristic matches from passage text; uncheck anything not useful.',
        words: modalWords,
      });
      setSuggestSelection(new Set(modalWords.map((w) => w.strongKey)));
    } catch (err) {
      setStatusMessage(`Hebrew suggest failed: ${err.message}`);
      window.setTimeout(() => setStatusMessage(''), 3000);
    } finally {
      setSuggestingHebrewForChunkId(null);
    }
  };

  const confirmSuggestWords = () => {
    if (!suggestModal) return;
    const { chunkId, words } = suggestModal;
    const newWords = words
      .filter((w) => suggestSelection.has(w.strongKey))
      .map((w) => ({
        id: makeId(),
        query: w.strongKey,
        strongNumber: w.strongKey,
        englishGloss: w.def,
        lexeme: w.lexeme,
        transliteration: w.translit,
        partOfSpeech: '',
        shortDefinition: w.fullDef || w.def,
        definitionHtml: w.entry ? buildGreekDefinitionHtml(w.strongKey, w.entry) : '',
        loading: false,
      }));
    if (newWords.length > 0) {
      updateProject((current) => ({
        ...current,
        chapters: current.chapters.map((ch) => ({
          ...ch,
          chunks: ch.chunks.map((c) =>
            c.id === chunkId ? { ...c, greekWords: [...c.greekWords, ...newWords] } : c
          ),
        })),
      }));
      const label = suggestModal?.language === 'hebrew' ? 'Hebrew' : 'Greek';
      setStatusMessage(`Added ${newWords.length} ${label} word${newWords.length > 1 ? 's' : ''}.`);
      window.setTimeout(() => setStatusMessage(''), 2000);
    }
    setSuggestModal(null);
    setSuggestSelection(new Set());
  };

  const isGreekStrongNumber = (query) => /^G\d+$/i.test(query.trim());
  const isHebrewStrongNumber = (query) => /^H\d+$/i.test(query.trim());

  const externalLookupLinks = (query) => {
    const raw = query.trim();
    const normalized = raw.toUpperCase();
    const isGreek = /^G\d+$/.test(normalized);
    const isHebrew = /^H\d+$/.test(normalized);
    const num = (isGreek || isHebrew) ? normalized.slice(1) : null;
    if (isGreek && num) {
      return [
        { label: 'BibleHub', url: `https://biblehub.com/greek/${num}.htm` },
        { label: 'Blue Letter Bible', url: `https://www.blueletterbible.org/lexicon/g${num}/esv/0-1/` },
        { label: 'StudyLight', url: `https://www.studylight.org/lexicons/eng/greek/${num}.html` },
      ];
    }
    if (isHebrew && num) {
      return [
        { label: 'BibleHub', url: `https://biblehub.com/hebrew/${num}.htm` },
        { label: 'Blue Letter Bible', url: `https://www.blueletterbible.org/lexicon/h${num}/kjv/wlc/0-1/` },
        { label: 'StudyLight', url: `https://www.studylight.org/lexicons/eng/hebrew/${num}.html` },
      ];
    }
    const enc = encodeURIComponent(raw);
    return [
      { label: 'BibleHub', url: `https://biblehub.com/search.php?q=${enc}` },
      { label: 'Blue Letter Bible', url: `https://www.blueletterbible.org/search/search.cfm?Criteria=${enc}&t=KJV#s=s_lexiconc` },
    ];
  };

  // Module-level cache for the Greek Strong's dictionary (loaded once, ~1.2 MB)
  const _greekDictRef = useRef(null);
  const _greekDictLoadingRef = useRef(null);

  const loadGreekDict = async () => {
    if (_greekDictRef.current) return _greekDictRef.current;
    if (_greekDictLoadingRef.current) return _greekDictLoadingRef.current;
    _greekDictLoadingRef.current = fetch(
      'https://cdn.jsdelivr.net/gh/openscriptures/strongs@master/greek/strongs-greek-dictionary.js',
    )
      .then((res) => res.text())
      .then((text) => {
        const start = text.indexOf('{');
        const end = text.lastIndexOf('}') + 1;
        const dict = JSON.parse(text.slice(start, end));
        _greekDictRef.current = dict;
        _greekDictLoadingRef.current = null;
        return dict;
      });
    return _greekDictLoadingRef.current;
  };

  const buildGreekDefinitionHtml = (key, entry) => {
    let html = '';
    if (entry.strongs_def) html += `<p>${entry.strongs_def.trim()}</p>`;
    if (entry.derivation) html += `<p><em>Derivation:</em> ${entry.derivation}</p>`;
    if (entry.kjv_def) html += `<p><em>KJV uses:</em> ${entry.kjv_def}</p>`;
    return html;
  };

  const fetchBollsDefinition = async (query) => {
    const encoded = encodeURIComponent(query.trim());

    if (isHebrewStrongNumber(query)) {
      const response = await fetch(`https://bolls.life/dictionary-definition/BDBT/${encoded}/`);
      if (!response.ok) return null;
      let defs;
      try { defs = await response.json(); } catch { return null; }
      return Array.isArray(defs) && defs.length > 0 ? defs : null;
    }

    // Greek — look up in the cached OpenScriptures Strong's dictionary.
    const dict = await loadGreekDict();
    const key = isGreekStrongNumber(query) ? query.trim().toUpperCase() : null;

    if (key) {
      const entry = dict[key];
      if (!entry) return null;
      return [{
        topic: key,
        lexeme: entry.lemma || '',
        transliteration: entry.translit || '',
        short_definition: entry.kjv_def || '',
        definition: buildGreekDefinitionHtml(key, entry),
      }];
    }

    // English word search — scan kjv_def and strongs_def for the query term.
    const lowerQ = query.trim().toLowerCase();
    const match = Object.entries(dict).find(([, entry]) =>
      (entry.kjv_def || '').toLowerCase().split(/[,\s]+/).some((w) => w === lowerQ) ||
      (entry.strongs_def || '').toLowerCase().includes(lowerQ),
    );
    if (match) {
      const [matchKey, entry] = match;
      return [{
        topic: matchKey,
        lexeme: entry.lemma || '',
        transliteration: entry.translit || '',
        short_definition: entry.kjv_def || '',
        definition: buildGreekDefinitionHtml(matchKey, entry),
      }];
    }

    return null;
  };

  const lookupWord = async (chunkId, wordId, language = 'greek') => {
    const chunk = allChunks.find((c) => c.id === chunkId);
    const word = chunk?.greekWords.find((w) => w.id === wordId);
    if (!word || !word.query.trim()) return;
    updateChunkWord(chunkId, wordId, { loading: true });
    try {
      // Normalize bare numbers based on selected lookup language.
      const raw = word.query.trim();
      const inferredPrefix = language === 'hebrew' ? 'H' : 'G';
      const normalized = /^\d+$/.test(raw)
        ? `${inferredPrefix}${raw}`
        : /^[gGhH]\d+$/.test(raw)
          ? raw.toUpperCase()
          : raw;

      const [definitions, gloss] = await Promise.all([
        fetchBollsDefinition(normalized),
        loadNtGloss().catch(() => ({})),
      ]);
      if (!definitions || definitions.length === 0) {
        updateChunkWord(chunkId, wordId, {
          strongNumber: '',
          shortDefinition: 'No definition found.',
          definitionHtml: '',
        });
        return;
      }
      const first = definitions[0];
      const strongKey = (first.topic || normalized).toUpperCase();
      const extractedPartOfSpeech = extractPartOfSpeech(first.definition || '');
      updateChunkWord(chunkId, wordId, {
        strongNumber: first.topic || normalized,
        englishGloss: gloss[strongKey] || first.short_definition || '',
        lexeme: first.lexeme || '',
        transliteration: first.transliteration || '',
        partOfSpeech: extractedPartOfSpeech || word.partOfSpeech || '',
        shortDefinition: first.short_definition || '',
        definitionHtml: first.definition || '',
      });
    } catch {
      updateChunkWord(chunkId, wordId, { shortDefinition: 'Lookup failed.' });
    } finally {
      updateChunkWord(chunkId, wordId, { loading: false });
    }
  };

  // ---------------------------------------------------------------------------
  // Export
  // ---------------------------------------------------------------------------

  const exportChapter = () => {
    if (!project) return;
    const html = buildExportHtml(project);
    const blob = new Blob([html], { type: 'text/html;charset=utf-8' });
    const url = URL.createObjectURL(blob);
    const link = document.createElement('a');
    link.href = url;
    link.download = `${project.title.replace(/\s+/g, '-')}-study.html`;
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
    URL.revokeObjectURL(url);
  };

  const printChapterPdf = () => {
    if (!project) return;
    const html = buildExportHtml(project);
    const printWindow = window.open('', '_blank');
    if (!printWindow) {
      alert('Please allow pop-ups for this site to print or save as PDF.');
      return;
    }
    printWindow.document.write(html);
    printWindow.document.close();
    printWindow.onload = () => {
      printWindow.focus();
      printWindow.print();
    };
  };

  const handleEnableSharing = async () => {
    if (!project) return;
    setShareBusy(true);
    const result = await enableSharing(project.id);
    setShareBusy(false);
    if (result.ok) setShareToken(result.data.shareToken);
  };

  const handleDisableSharing = async () => {
    if (!project) return;
    if (!window.confirm('Revoke this share link? Anyone using it will lose access immediately.')) return;
    setShareBusy(true);
    const result = await disableSharing(project.id);
    setShareBusy(false);
    if (result.ok) setShareToken(null);
  };

  const handleCopyShareLink = () => {
    if (!shareToken) return;
    const url = `${window.location.origin}${window.location.pathname}?share=${shareToken}`;
    navigator.clipboard.writeText(url).then(() => {
      setShareCopied(true);
      window.setTimeout(() => setShareCopied(false), 2000);
    });
  };

  const exportChapterMarkdown = () => {
    if (!project) return;
    const markdown = buildMarkdownExport(project);
    const blob = new Blob([markdown], { type: 'text/markdown;charset=utf-8' });
    const url = URL.createObjectURL(blob);
    const link = document.createElement('a');
    link.href = url;
    link.download = `${project.title.replace(/\s+/g, '-')}-study.md`;
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
    URL.revokeObjectURL(url);
  };

  const exportChapterDocx = async () => {
    if (!project) return;
    const children = [
      new Paragraph({ text: project.title, heading: HeadingLevel.TITLE }),
      new Paragraph({
        children: [
          new TextRun({
            text: `${project.translation} — ${buildChapterSummary(project)}`,
            italics: true,
            color: '7A7060',
          }),
        ],
        spacing: { after: 300 },
      }),
    ];

    const noBorder = { style: BorderStyle.NONE, size: 0, color: 'FFFFFF' };

    project.chapters.forEach((ch, chapterIndex) => {
      children.push(new Paragraph({
        text: `${ch.book} ${ch.chapter}`,
        heading: HeadingLevel.HEADING_1,
        border: { bottom: { style: BorderStyle.SINGLE, size: 6, color: DOCX_ACCENT, space: 4 } },
        spacing: { before: 360, after: 160 },
      }));
      ch.chunks.forEach((chunk) => {
        const scriptureHeading = formatChunkReference(project, chapterIndex, chunk, '-');

        const headingRuns = [new TextRun({ text: scriptureHeading, bold: true, size: 28, color: DOCX_ACCENT, font: DOCX_HEADING_FONT })];
        if (chunk.episodeNumber || chunk.episodeTitle) {
          headingRuns.push(new TextRun({
            text: `   (Session ${chunk.episodeNumber || '—'}${chunk.episodeTitle ? `: ${chunk.episodeTitle}` : ''})`,
            italics: true,
            size: 22,
            color: '7A7060',
          }));
        }
        children.push(new Paragraph({ children: headingRuns, spacing: { before: 320, after: 120 } }));

        // Scripture text in a left-bordered table cell for a "callout" look (no fill)
        const scriptureParas = getChunkVerseEntries(project, chapterIndex, chunk).map((verse) => new Paragraph({
          children: [
            new TextRun({ text: `${verse.chapter}:${verse.number} `, bold: true, color: DOCX_ACCENT }),
            new TextRun({ text: verse.text, italics: true }),
          ],
          spacing: { after: 80 },
        }));
        children.push(new Table({
          width: { size: 100, type: WidthType.PERCENTAGE },
          rows: [
            new TableRow({
              children: [
                new TableCell({
                  margins: { top: 120, bottom: 120, left: 180, right: 180 },
                  borders: {
                    ...{ top: noBorder, right: noBorder, bottom: noBorder },
                    left: { style: BorderStyle.SINGLE, size: 24, color: DOCX_ACCENT },
                  },
                  children: scriptureParas,
                }),
              ],
            }),
          ],
        }));
        children.push(new Paragraph({ text: '', spacing: { after: 120 } }));

        if (chunk.generalNotes?.trim()) {
          children.push(sectionLabel('Background / General Notes'));
          children.push(...createParagraphsFromText(chunk.generalNotes));
        }

        children.push(sectionLabel('Observation'));
        children.push(...createParagraphsFromText(chunk.observation || 'No observation.'));
        children.push(sectionLabel('Interpretation'));
        children.push(...createParagraphsFromText(chunk.interpretation || 'No interpretation.'));
        children.push(sectionLabel('Application'));
        children.push(...createParagraphsFromText(chunk.application || 'No application.'));

        if ((chunk.tags ?? []).length > 0) {
          children.push(sectionLabel('Tags'));
          children.push(new Paragraph({
            children: chunk.tags.map((tag, i) => new TextRun({
              text: i === 0 ? `#${tag}` : `   #${tag}`,
              bold: true,
              color: DOCX_ACCENT,
            })),
          }));
        }

        if ((chunk.crossReferences ?? []).length > 0) {
          children.push(sectionLabel('Cross-References'));
          children.push(new Paragraph({ text: chunk.crossReferences.join('  •  ') }));
        }

        if (chunk.greekWords.length > 0) {
          children.push(sectionLabel('Word Studies'));
          const headerCellStyle = (label) => new TableCell({
            width: { size: 20, type: WidthType.PERCENTAGE },
            borders: { bottom: { style: BorderStyle.SINGLE, size: 8, color: DOCX_ACCENT } },
            children: [new Paragraph({ children: [new TextRun({ text: label, bold: true, color: DOCX_ACCENT })] })],
          });
          const tableRows = [
            new TableRow({
              tableHeader: true,
              children: ['Strong', 'Greek', 'Transliteration', 'Part of Speech', 'Short Definition'].map(headerCellStyle),
            }),
            ...chunk.greekWords.map((word) => new TableRow({
              children: [
                new TableCell({ children: [new Paragraph(word.strongNumber || '')] }),
                new TableCell({ children: [new Paragraph(word.lexeme || '')] }),
                new TableCell({ children: [new Paragraph(word.transliteration || '')] }),
                new TableCell({ children: [new Paragraph(word.partOfSpeech || '')] }),
                new TableCell({ children: [new Paragraph(word.shortDefinition || '')] }),
              ],
            })),
          ];
          children.push(new Table({ rows: tableRows, width: { size: 100, type: WidthType.PERCENTAGE } }));
          chunk.greekWords.forEach((word) => {
            if (word.definitionHtml) {
              children.push(new Paragraph({
                spacing: { before: 180, after: 80 },
                children: [new TextRun({ text: `${word.strongNumber} — ${word.lexeme || ''}`, bold: true, color: DOCX_ACCENT })],
              }));
              children.push(...createParagraphsFromText(htmlToPlainText(word.definitionHtml)));
            }
          });
        }

        if (chunk.finalScript?.trim()) {
          children.push(sectionLabel('Final Script'));
          children.push(...createParagraphsFromText(chunk.finalScript));
        }

        children.push(new Paragraph({
          text: '',
          spacing: { after: 200 },
          border: { bottom: { style: BorderStyle.SINGLE, size: 4, color: '2A2518', space: 8 } },
        }));
      });
    });

    const doc = new Document({
      styles: {
        default: {
          document: { run: { font: DOCX_BODY_FONT, color: '0A0A08' } },
        },
        paragraphStyles: [
          {
            id: 'Title',
            name: 'Title',
            basedOn: 'Normal',
            next: 'Normal',
            run: { font: DOCX_HEADING_FONT, size: 56, bold: true, color: '0A0A08' },
          },
          {
            id: 'Heading1',
            name: 'Heading 1',
            basedOn: 'Normal',
            next: 'Normal',
            run: { font: DOCX_HEADING_FONT, size: 36, bold: true, color: '0A0A08' },
          },
          {
            id: 'Heading2',
            name: 'Heading 2',
            basedOn: 'Normal',
            next: 'Normal',
            run: { font: DOCX_HEADING_FONT, size: 30, bold: true, color: '0A0A08' },
          },
        ],
      },
      sections: [{ children }],
    });
    const blob = await Packer.toBlob(doc);
    const url = URL.createObjectURL(blob);
    const link = document.createElement('a');
    link.href = url;
    link.download = `${project.title.replace(/\s+/g, '-')}-study.docx`;
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
    URL.revokeObjectURL(url);
  };

  const copyForClaude = () => {
    if (!project) return;
    const prompt = buildClaudePrompt(project);
    navigator.clipboard.writeText(prompt).then(() => {
      setSaveStatus('Copied! Opening Claude…');
      window.setTimeout(() => setSaveStatus(''), 3000);
      window.open('https://claude.ai/new', '_blank', 'noopener,noreferrer');
    });
  };

  const copyForPodcast = () => {
    if (!project) return;
    const prompt = buildPodcastPrompt(project, authUser?.podcastName);
    navigator.clipboard.writeText(prompt).then(() => {
      setSaveStatus('Copied! Opening Claude…');
      window.setTimeout(() => setSaveStatus(''), 3000);
      window.open('https://claude.ai/new', '_blank', 'noopener,noreferrer');
    });
  };

  const createReadingPlan = (bookAbbrev, bookName, weeks) => {
    const totalChapters = CHAPTER_COUNTS[bookAbbrev] ?? 1;
    const plan = { bookAbbrev, bookName, totalChapters, startDate: Date.now(), targetDate: Date.now() + weeks * 7 * 24 * 60 * 60 * 1000, chaptersRead: [] };
    setReadingPlan(plan);
    try { localStorage.setItem('readingPlan', JSON.stringify(plan)); } catch {}
  };

  const markChapterRead = (chapter) => {
    setReadingPlan((prev) => {
      if (!prev) return prev;
      const chaptersRead = prev.chaptersRead.includes(chapter)
        ? prev.chaptersRead.filter((c) => c !== chapter)
        : [...prev.chaptersRead, chapter].sort((a, b) => a - b);
      const next = { ...prev, chaptersRead };
      try { localStorage.setItem('readingPlan', JSON.stringify(next)); } catch {}
      return next;
    });
  };

  const clearReadingPlan = () => {
    setReadingPlan(null);
    try { localStorage.removeItem('readingPlan'); } catch {}
  };

  const copyPronunciationGuide = () => {
    if (!project) return;
    const guide = buildPronunciationGuide(project);
    navigator.clipboard.writeText(guide).then(() => {
      setSaveStatus('Copied pronunciation guide!');
      window.setTimeout(() => setSaveStatus(''), 2000);
    });
  };

  // ---------------------------------------------------------------------------
  // Home page helpers
  // ---------------------------------------------------------------------------

  const openNewProject = () => {
    setProject(null);
    setActiveChapterIndex(0);
    setShowAddChapterForm(false);
    setRangeStart(null);
    setRangeEnd(null);
    setErrorMessage('');
    setStatusMessage('');
    setTitleEdited(false);
    setCurrentPage('setup');
  };

  const openImportProject = () => {
    setProject(null);
    setImportBookAbbrev(bookOptions[0].abbrev);
    setImportTranslation('BSB');
    setImportTitle('');
    setImportFile(null);
    setImportPreview(null);
    setImportError('');
    setCurrentPage('import');
  };

  const handleImportFileChange = async (file) => {
    setImportFile(file);
    setImportPreview(null);
    setImportError('');
    if (!file) return;
    setImportBusy(true);
    try {
      const episodes = await parseEpisodeListDocx(file);
      if (episodes.length === 0) {
        throw new Error('No "Session # / Title / Passage" table found in that document.');
      }
      const specs = episodes.map((ep) => ({ ...ep, parsed: parseEpisodePassage(ep.passage) }));
      setImportPreview(specs);
    } catch (error) {
      setImportError(error.message || 'Failed to read that document.');
    } finally {
      setImportBusy(false);
    }
  };

  const runEpisodeImport = async () => {
    if (!importPreview || importPreview.length === 0) {
      setImportError('Choose a .docx file first.');
      return;
    }
    setImportBusy(true);
    setImportError('');
    try {
      const selectedBook = bookOptions.find((b) => b.abbrev === importBookAbbrev) ?? bookOptions[0];

      const chapterNumbersNeeded = new Set();
      importPreview.forEach((spec) => {
        if (spec.parsed && spec.parsed !== 'invalid') {
          chapterNumbersNeeded.add(spec.parsed.startChapter);
          chapterNumbersNeeded.add(spec.parsed.endChapter);
        }
      });
      const sortedChapters = Array.from(chapterNumbersNeeded).sort((a, b) => a - b);
      if (sortedChapters.length === 0) {
        throw new Error('No verse passages found to import.');
      }

      const versesByChapter = {};
      for (const chNum of sortedChapters) {
        const response = await fetch(
          `https://bible.helloao.org/api/${importTranslation}/${selectedBook.abbrev}/${chNum}.json`,
        );
        if (!response.ok) throw new Error(`Unable to load ${selectedBook.name} ${chNum}.`);
        const data = await response.json();
        const verses = parseBibleChapter(data);
        if (!verses.length) throw new Error(`No verses returned for ${selectedBook.name} ${chNum}.`);
        versesByChapter[chNum] = verses;
      }

      const chapterIndexByNum = new Map(sortedChapters.map((n, idx) => [n, idx]));
      const chapters = sortedChapters.map((chNum) => ({
        book: selectedBook.name,
        bookAbbrev: selectedBook.abbrev,
        chapter: chNum,
        verses: versesByChapter[chNum],
        chunks: [],
      }));

      // Markers (rows with no passage) attach to the nearest upcoming chapter with a
      // real passage, or fall back to the previous one for trailing markers.
      const anchorChapterFor = importPreview.map((spec, i) => {
        if (spec.parsed && spec.parsed !== 'invalid') return spec.parsed.startChapter;
        for (let j = i + 1; j < importPreview.length; j += 1) {
          const next = importPreview[j].parsed;
          if (next && next !== 'invalid') return next.startChapter;
        }
        for (let j = i - 1; j >= 0; j -= 1) {
          const prev = importPreview[j].parsed;
          if (prev && prev !== 'invalid') return prev.startChapter;
        }
        return sortedChapters[0];
      });

      importPreview.forEach((spec, i) => {
        if (spec.parsed === 'invalid') return; // skip unparseable rows entirely
        const chIdx = chapterIndexByNum.get(anchorChapterFor[i]);
        if (chIdx == null) return;
        const target = chapters[chIdx];

        const baseChunk = {
          id: makeId(),
          observation: '',
          interpretation: '',
          application: '',
          crossReferences: [],
          greekWords: [],
          generalNotes: '',
          episodeNumber: spec.episodeNumber,
          episodeTitle: spec.title,
          finalScript: '',
          tags: [],
        };

        if (!spec.parsed) {
          // No passage (e.g. a part intro/review) -> zero-length marker chunk.
          target.chunks.push({ ...baseChunk, startVerse: 0, endVerse: 0, spilloverEndVerse: null });
          return;
        }

        const { startChapter, startVerse, endChapter, endVerse } = spec.parsed;
        if (endChapter === startChapter) {
          target.chunks.push({ ...baseChunk, startVerse, endVerse, spilloverEndVerse: null });
        } else {
          const lastVerseOfStartChapter = versesByChapter[startChapter]?.at(-1)?.number ?? endVerse;
          target.chunks.push({
            ...baseChunk,
            startVerse,
            endVerse: lastVerseOfStartChapter,
            spilloverEndVerse: endVerse,
          });
        }
      });

      const newProject = {
        id: makeId(),
        title: importTitle.trim() || `${selectedBook.name} Sessions`,
        translation: importTranslation,
        lastEdited: Date.now(),
        selectedChunkId: null,
        chapters,
      };
      setProject(newProject);
      setActiveChapterIndex(0);
      setCurrentPage('setup');
    } catch (error) {
      setImportError(error.message || 'Import failed.');
    } finally {
      setImportBusy(false);
    }
  };

  const resumeProject = (id) => {
    const loaded = loadProjectById(id);
    if (!loaded) return;
    setProject(loaded);
    setActiveChapterIndex(0);
    const loadedChunks = loaded.chapters.flatMap((ch) => ch.chunks);
    const hasSelectedChunk = loaded.selectedChunkId
      && loadedChunks.some((c) => c.id === loaded.selectedChunkId);
    setCurrentPage(hasSelectedChunk ? 'study' : 'setup');
  };

  const renameProjectInStorage = (id, title) => {
    const loaded = loadProjectById(id);
    if (!loaded) return;
    const updated = { ...loaded, title, lastEdited: Date.now() };
    saveProjectToStorage(updated);
    setProjectIndex(loadProjectIndex());
    saveRemoteProject(updated);
    if (project?.id === id) {
      setProject((current) => (current ? { ...current, title } : current));
    }
  };

const deleteProject = (id) => {
    if (!window.confirm('Delete this project? This cannot be undone.')) return;
    deleteProjectFromStorage(id);
    setProjectIndex(loadProjectIndex());
    deleteRemoteProject(id); // fire-and-forget
  };

  const pullLatestFromServer = async (id) => {
    const result = await loadRemoteProject(id);
    if (!result.ok) {
      alert('Could not pull latest version from server.');
      return;
    }
    saveProjectToStorage(result.data);
    setProjectIndex(loadProjectIndex());
    setStaleLocalProjects((prev) => prev.filter((e) => e.id !== id));
  };

  const goHome = () => {
    setProjectIndex(loadProjectIndex());
    setCurrentPage('home');
    setProject(null);
    setRangeStart(null);
    setRangeEnd(null);
    setErrorMessage('');
    setStatusMessage('');
  };

  const authStatus = authUser && (
    <div className="flex items-center gap-2 text-sm text-slate-300">
      <span className="hidden sm:inline">{authUser.email}</span>
      {authUser.isAdmin && (
        <button
          type="button"
          onClick={() => setCurrentPage('admin')}
          className="rounded-xl border border-white/15 bg-white/10 px-3 py-1.5 text-xs text-white transition hover:bg-white/15"
        >
          🛡 Admin
        </button>
      )}
      <button
        type="button"
        onClick={() => setCurrentPage('settings')}
        className="rounded-xl border border-white/15 bg-white/10 px-3 py-1.5 text-xs text-white transition hover:bg-white/15"
      >
        ⚙ Settings
      </button>
      <button
        type="button"
        onClick={async () => {
          await logoutUser();
          setAuthUser(null);
          setProjectIndex(switchStorageUser(null));
          goHome();
        }}
        className="rounded-xl border border-white/15 bg-white/10 px-3 py-1.5 text-xs text-white transition hover:bg-white/15"
      >
        Log out
      </button>
    </div>
  );

  // ---------------------------------------------------------------------------
  // Shared header
  // ---------------------------------------------------------------------------
  const headerButtons = (
    <div className="flex flex-wrap items-center gap-3">
      {currentPage !== 'home' && (
        <button
          type="button"
          onClick={goHome}
          className="rounded-xl border border-white/15 bg-white/10 px-4 py-2 text-sm text-white transition hover:bg-white/15"
        >
          ← Home
        </button>
      )}
      {project && currentPage === 'study' && (
        <button
          type="button"
          onClick={() => setCurrentPage('setup')}
          className="rounded-xl border border-white/15 bg-white/10 px-4 py-2 text-sm text-white transition hover:bg-white/15"
        >
          ← Back to Chunks
        </button>
      )}
      {project && (
        <div className="flex flex-wrap items-center gap-3">
          <button
            type="button"
            onClick={copyForClaude}
            disabled={allChunks.length === 0}
            className="rounded-md bg-violet-500 px-4 py-2 text-sm font-medium text-white shadow-sm transition hover:bg-violet-400 disabled:cursor-not-allowed disabled:bg-slate-500"
            title="Copies your study notes as a Claude prompt, then opens claude.ai so you can paste and go"
          >
            <span className="hidden sm:inline">Study Guide → </span>Claude ↗
          </button>
          <button
            type="button"
            onClick={copyForPodcast}
            disabled={allChunks.length === 0}
            title="Copies a podcast script prompt for Claude, then opens claude.ai — set your show name in Account Settings first"
            className="rounded-md bg-fuchsia-500 px-4 py-2 text-sm font-medium text-white shadow-sm transition hover:bg-fuchsia-400 disabled:cursor-not-allowed disabled:bg-slate-500"
          >
            🎙<span className="hidden sm:inline"> Podcast → Claude ↗</span>
          </button>
          <button
            type="button"
            onClick={copyPronunciationGuide}
            disabled={allChunks.length === 0}
            title="Copy a quick word + transliteration list to keep open while recording"
            className="rounded-md bg-amber-500 px-4 py-2 text-sm font-medium text-white shadow-sm transition hover:bg-amber-400 disabled:cursor-not-allowed disabled:bg-slate-500"
          >
            🗣<span className="hidden sm:inline"> Pronunciation Guide</span>
          </button>
          <div className="relative">
            <button
              type="button"
              onClick={() => setExportMenuOpen((v) => !v)}
              className="flex items-center gap-1.5 rounded-md bg-sky-500 px-4 py-2 text-sm font-medium text-white shadow-sm transition hover:bg-sky-400"
            >
              ⬇ Export
            </button>
            {exportMenuOpen && (
              <div className="absolute left-0 top-full z-20 mt-2 w-56 overflow-hidden rounded-2xl border border-slate-200 bg-white py-1.5 text-left shadow-lg">
                <button type="button" onClick={() => { exportChapter(); setExportMenuOpen(false); }}
                  className="block w-full px-4 py-2 text-left text-sm text-slate-700 hover:bg-slate-50">
                  Export HTML
                </button>
                <button type="button" onClick={() => { exportChapterDocx(); setExportMenuOpen(false); }}
                  className="block w-full px-4 py-2 text-left text-sm text-slate-700 hover:bg-slate-50">
                  Export DOCX
                </button>
                <button type="button" onClick={() => { exportChapterMarkdown(); setExportMenuOpen(false); }}
                  title="Handy for Obsidian, Notion, or any Markdown-based notes app"
                  className="block w-full px-4 py-2 text-left text-sm text-slate-700 hover:bg-slate-50">
                  Export Markdown
                </button>
                <button type="button" onClick={() => { printChapterPdf(); setExportMenuOpen(false); }}
                  title="Opens a print-friendly version in a new tab — choose 'Save as PDF' in the print dialog"
                  className="block w-full px-4 py-2 text-left text-sm text-slate-700 hover:bg-slate-50">
                  🖨 Print / Save PDF
                </button>
              </div>
            )}
          </div>
          <div className="relative">
            <button
              type="button"
              onClick={() => setSharePanelOpen((v) => !v)}
              className={`rounded-md px-4 py-2 text-sm font-medium shadow-sm transition ${shareToken ? 'bg-teal-600 text-white hover:bg-teal-500' : 'border border-slate-300 bg-white text-slate-700 hover:bg-slate-50'}`}
            >
              🔗 {shareToken ? 'Shared' : 'Share'}
            </button>
            {sharePanelOpen && (
              <div className="absolute right-0 top-full z-20 mt-2 w-80 rounded-2xl border border-slate-200 bg-white p-4 text-left shadow-lg">
                <h3 className="text-sm font-semibold text-slate-900">Read-only share link</h3>
                <p className="mt-1 text-xs text-slate-500">
                  Anyone with this link can view (not edit) this study — no account needed. Good for a co-teacher
                  or a listener who wants to follow along.
                </p>
                {shareToken ? (
                  <div className="mt-3 space-y-2">
                    <div className="flex items-center gap-2">
                      <input
                        readOnly
                        value={`${window.location.origin}${window.location.pathname}?share=${shareToken}`}
                        onFocus={(e) => e.target.select()}
                        className="w-full flex-1 truncate rounded-lg border border-slate-300 bg-slate-50 px-2 py-1.5 text-xs text-slate-600"
                      />
                      <button type="button" onClick={handleCopyShareLink}
                        className="shrink-0 rounded-lg bg-sky-500 px-3 py-1.5 text-xs font-semibold text-white hover:bg-sky-400">
                        {shareCopied ? 'Copied!' : 'Copy'}
                      </button>
                    </div>
                    <button type="button" onClick={handleDisableSharing} disabled={shareBusy}
                      className="w-full rounded-lg border border-rose-200 px-3 py-1.5 text-xs font-semibold text-rose-600 hover:bg-rose-50 disabled:cursor-not-allowed disabled:opacity-50">
                      {shareBusy ? 'Revoking…' : 'Revoke link'}
                    </button>
                  </div>
                ) : (
                  <button type="button" onClick={handleEnableSharing} disabled={shareBusy}
                    className="mt-3 w-full rounded-lg bg-teal-600 px-3 py-1.5 text-sm font-semibold text-white hover:bg-teal-500 disabled:cursor-not-allowed disabled:opacity-50">
                    {shareBusy ? 'Enabling…' : 'Enable sharing'}
                  </button>
                )}
              </div>
            )}
          </div>
        </div>
      )}
        <div className="text-right text-sm text-slate-300 space-y-0.5">
          {loadingChapter ? (
            <span>Loading…</span>
          ) : saveStatus ? (
            <span className="text-emerald-300">{saveStatus}</span>
           ) : (
           <span>&nbsp;</span>
          )}
          {syncStatus === 'syncing' && <div className="text-xs text-slate-400">Syncing…</div>}
          {syncStatus === 'synced' && <div className="text-xs text-emerald-400">Synced ✓</div>}
          {syncStatus === 'error' && (
            <div className="flex items-center gap-2">
              <span className="text-xs text-rose-400">⚠ Sync failed</span>
              <button
                type="button"
                onClick={async () => {
                  if (!project) return;
                  setSyncStatus('syncing');
                  const toSave = { ...project, lastEdited: Date.now() };
                  saveProjectToStorage(toSave);
                  const result = await saveRemoteProject(toSave);
                  setSyncStatus(result.ok ? 'synced' : 'error');
                  if (result.ok) window.setTimeout(() => setSyncStatus(''), 2500);
                }}
                className="text-xs text-slate-300 underline hover:text-white"
              >
                Retry
              </button>
            </div>
          )}
        </div>
        {authStatus}
    </div>
  );


  // ---------------------------------------------------------------------------
  // Context value — every state, handler, and computed value pages need.
  // ---------------------------------------------------------------------------
  const contextValue = {
    // Navigation
    currentPage, setCurrentPage,
    goHome,
    authServerDown,
    // Shared view
    sharedViewToken,
    sharedProject,
    sharedError,
    // Auth
    authUser, setAuthUser,
    authStatus,
    authMode, setAuthMode,
    authForm, setAuthForm,
    authError, setAuthError,
    authBusy, setAuthBusy,
    authMfaPending, setAuthMfaPending,
    authMfaCode, setAuthMfaCode,
    authMfaUseBackup, setAuthMfaUseBackup,
    setProjectIndex,
    // Settings — MFA
    mfaSetup, setMfaSetup,
    mfaSetupCode, setMfaSetupCode,
    mfaSetupError, setMfaSetupError,
    mfaSetupBusy, setMfaSetupBusy,
    mfaBackupCodes, setMfaBackupCodes,
    mfaDisablePassword, setMfaDisablePassword,
    mfaDisableError, setMfaDisableError,
    // Settings — password / podcast
    podcastNameInput, setPodcastNameInput,
    podcastNameSaving, setPodcastNameSaving,
    podcastNameSaved, setPodcastNameSaved,
    changePasswordForm, setChangePasswordForm,
    changePasswordBusy, setChangePasswordBusy,
    changePasswordError, setChangePasswordError,
    changePasswordSaved, setChangePasswordSaved,
    // Home
    projectIndex,
    autoRestoredCount,
    staleLocalProjects,
    homeSearch, setHomeSearch,
    homeSort, setHomeSort,
    homeTagFilter, setHomeTagFilter,
    homeFullTextResults,
    readingPlan,
    createReadingPlan,
    markChapterRead,
    clearReadingPlan,
    renamingId, setRenamingId,
    renameValue, setRenameValue,
    openBibleReader,
    openImportProject,
    openNewProject,
    pullLatestFromServer,
    resumeProject,
    renameProjectInStorage,
    deleteProject,
    // Audio
    audioBook, setAudioBook,
    audioNarrator, setAudioNarrator,
    audioState,
    handlePlayBookAudio,
    handleStopBookAudio,
    handleToggleBookAudioPause,
    // Import
    importBookAbbrev, setImportBookAbbrev,
    importTranslation, setImportTranslation,
    importTitle, setImportTitle,
    importFile,
    importPreview,
    importBusy,
    importError,
    handleImportFileChange,
    runEpisodeImport,
    // Reader
    readerBookAbbrev, setReaderBookAbbrev,
    readerChapter, setReaderChapter,
    readerVerses,
    readerTotalChapters,
    readerLoading,
    readerError,
    readerInterlinear,
    readerSelectedVerse, setReaderSelectedVerse,
    readerFontSize, setReaderFontSize,
    readerBookmarks, setReaderBookmarks,
    readerBookmarksPanelOpen, setReaderBookmarksPanelOpen,
    readerCrossRefs, setReaderCrossRefs,
    readerCrossRefsLoading,
    readerShowCrossRefs, setReaderShowCrossRefs,
    readerSearch, setReaderSearch,
    readerSearchActive, setReaderSearchActive,
    readerSearchScope, setReaderSearchScope,
    readerAudioState,
    readerInkByPage,
    updateReaderPageInk,
    readerTextHighlights,
    addTextHighlight,
    removeTextHighlight,
    bibleIndexStatus,
    loadReaderChapter,
    _bibleIndexCacheRef,
    handleReaderBookChange,
    readerGoToPreviousChapter,
    readerGoToNextChapter,
    jumpToReaderVerse,
    toggleReaderBookmark,
    cycleBookmarkColor,
    loadReaderCrossRefs,
    loadBibleIndex,
    handlePlayReaderAudio,
    handleToggleReaderAudioPause,
    copyVerse,
    // Setup
    availableTranslations,
    project, setProject,
    setup,
    titleEdited, setTitleEdited,
    activeChapterIndex, setActiveChapterIndex,
    activeChapter,
    showAddChapterForm, setShowAddChapterForm,
    rangeStart, setRangeStart,
    rangeEnd, setRangeEnd,
    verseSearch, setVerseSearch,
    typedChunkStart, setTypedChunkStart,
    typedChunkEnd, setTypedChunkEnd,
    typedChunkNextEnd, setTypedChunkNextEnd,
    typedChunkBulk, setTypedChunkBulk,
    clickedSpanNextEnd, setClickedSpanNextEnd,
    loadingChapter,
    errorMessage,
    statusMessage,
    allChunks,
    handleSetupField,
    handleLoadChapter,
    beginStudying,
    handleVerseClick,
    addTypedChunk,
    addBulkTypedChunks,
    addClickSpanChunk,
    moveChunk,
    deleteChunk,
    updateProject,
    // Study
    selectedChunk,
    selectedChunkChapterIndex,
    selectedChunkChapter,
    selectedChunkVerses,
    selectedChunkGlobalIndex,
    studyLayout, setStudyLayout,
    drawTool, setDrawTool,
    drawColor, setDrawColor,
    drawSize, setDrawSize,
    activeStudyTab, setActiveStudyTab,
    mobileStudyTab, setMobileStudyTab,
    collapsedSections, setCollapsedSections,
    interlinearData,
    interlinearLoading,
    interlinearError,
    commentarySource, setCommentarySource,
    commentaryData,
    commentaryLoading,
    commentaryError,
    crossRefInput, setCrossRefInput,
    suggestingCrossRefs,
    crossRefSuggestions,
    tagInput, setTagInput,
    suggestModal, setSuggestModal,
    suggestSelection, setSuggestSelection,
    suggestingGreekForChunkId,
    suggestingHebrewForChunkId,
    goToPreviousChunk,
    goToNextChunk,
    updateChunk,
    addCrossRef,
    removeCrossRef,
    suggestCrossRefsForChunk,
    addSuggestedCrossRef,
    addTag,
    removeTag,
    addGreekWord,
    removeGreekWord,
    updateChunkWord,
    lookupWord,
    suggestGreekWordsForChunk,
    suggestHebrewWordsForChunk,
    confirmSuggestWords,
    speakOriginalWord,
    loadVerseText,
    importFinalScriptDocx,
    // Admin
    adminTab, setAdminTab,
    adminUsers,
    adminProjects,
    adminLoading,
    adminError,
    adminViewProject, setAdminViewProject,
    adminResetResult, setAdminResetResult,
    loadAdminData,
    // Functions defined inside App (cannot be module-level exports)
    formatCrossRef,
    buildGreekDefinitionHtml,
    externalLookupLinks,
    // Computed JSX
    authStatus,
    headerButtons,
  };

  return (
    <AppContext.Provider value={contextValue}>
      <AppRouter />
    </AppContext.Provider>
  );
};

function AppRouter() {
  const {
    currentPage,
    authUser,
    authServerDown,
    sharedViewToken,
    sharedProject,
    sharedError,
  } = useApp();

  if (sharedViewToken) {
    if (sharedError) {
      return (
        <div className="flex min-h-screen items-center justify-center bg-slate-900 px-4">
          <div className="w-full max-w-sm rounded-3xl border border-white/10 bg-white p-8 text-center shadow-panel">
            <p className="text-sm uppercase tracking-[0.24em] text-slate-400">Bible Study Project</p>
            <p className="mt-4 text-sm text-rose-600">{sharedError}</p>
          </div>
        </div>
      );
    }
    if (!sharedProject) {
      return (
        <div className="flex min-h-screen items-center justify-center bg-slate-900">
          <p className="text-sm text-slate-400">Loading shared study…</p>
        </div>
      );
    }
    return (
      <div className="min-h-screen bg-slate-100">
        <div className="border-b border-slate-200 bg-slate-900 px-4 py-3 text-center text-xs text-slate-300">
          🔗 Read-only shared view —{' '}
          <a href="/" className="underline hover:text-white">go to Bible Study Project</a>
        </div>
        <iframe
          title="Shared study"
          srcDoc={buildExportHtml(sharedProject)}
          sandbox="allow-popups"
          className="h-[calc(100vh-2.5rem)] w-full border-0"
        />
      </div>
    );
  }

  if (authUser === undefined && !authServerDown) {
    return (
      <div className="flex min-h-screen items-center justify-center bg-slate-900">
        <p className="text-sm text-slate-400">Loading…</p>
      </div>
    );
  }

  if (authUser === null && !authServerDown) return <AuthPage />;

  if (currentPage === 'admin') return <AdminPage />;
  if (currentPage === 'settings') return <SettingsPage />;
  if (currentPage === 'reader') return <ReaderPage />;
  if (currentPage === 'import') return <ImportPage />;
  if (currentPage === 'study') return <StudyPage />;
  if (currentPage === 'setup') return <SetupPage />;
  return <HomePage />;
}

export default App;
