/* ============================================================
   IJMA AI — Concepts pages (live Qurʾan + hadith references)
   ============================================================ */
const C_DATA = window.IJMA_CONCEPTS;
/* which family of the medieval picture each concept belongs to (shown as a tag) */
const CONCEPT_CAT = {
  god: 'The divine', light: 'The divine', word: 'The divine', creation: 'The divine', prophets: 'The divine',
  angels: 'The unseen', demons: 'The unseen',
  soul: 'The human soul', intellect: 'The human soul', will: 'The human soul', mind: 'The human soul', body: 'The human soul',
  heavens: 'The cosmos', fate: 'The cosmos', time: 'The cosmos',
  death: 'Last things', resurrection: 'Last things', paradise: 'Last things', hell: 'Last things',
  knowledge: 'The moral life', mercy: 'The moral life', justice: 'The moral life',
};
const AQ = 'https://api.alquran.cloud/v1';
const HADITH_CDN = 'https://cdn.jsdelivr.net/gh/fawazahmed0/hadith-api@1/editions';

/* module-level caches so re-visits don't refetch */
const _arabicCache = {};   // global ayah number -> arabic text
const _editionCache = {};  // collection -> hadiths array

async function aqSearch(edition, term) {
  const r = await fetch(`${AQ}/search/${encodeURIComponent(term)}/all/${edition}`);
  if (!r.ok) return [];
  const j = await r.json();
  if (j.status !== 'OK' || !j.data || !j.data.matches) return [];
  return j.data.matches;
}

async function aqArabic(globalNum) {
  if (_arabicCache[globalNum]) return _arabicCache[globalNum];
  const r = await fetch(`${AQ}/ayah/${globalNum}/${C_DATA.arabicEdition}`);
  const j = await r.json();
  const t = (j.status === 'OK' && j.data) ? j.data.text : '';
  _arabicCache[globalNum] = t;
  return t;
}

async function aqSeed(key, edition) {
  const r = await fetch(`${AQ}/ayah/${key}/editions/${edition},${C_DATA.arabicEdition}`);
  const j = await r.json();
  if (j.status !== 'OK' || !j.data) return null;
  const en = j.data.find(d => d.edition.identifier === edition) || j.data[0];
  const ar = j.data.find(d => d.edition.identifier === C_DATA.arabicEdition);
  if (ar) _arabicCache[en.number] = ar.text;
  return { num: en.number, s: en.surah.number, a: en.numberInSurah, surahName: en.surah.englishName, en: en.text, ar: ar ? ar.text : null };
}

async function loadConceptQuran(c) {
  const map = new Map();
  const lists = await Promise.all(c.quranTerms.map(t => aqSearch(C_DATA.quranEdition, t).catch(() => [])));
  const termCounts = c.quranTerms
    .map((t, i) => ({ term: t, count: lists[i].length }))
    .filter(x => x.count > 0)
    .sort((a, b) => b.count - a.count);
  for (const matches of lists) {
    for (const m of matches) {
      if (!map.has(m.number)) {
        map.set(m.number, { num: m.number, s: m.surah.number, a: m.numberInSurah, surahName: m.surah.englishName, en: m.text, ar: _arabicCache[m.number] || null });
      }
    }
  }
  if (c.quranSeed && c.quranSeed.length) {
    const seeds = await Promise.all(c.quranSeed.map(k => aqSeed(k, C_DATA.quranEdition).catch(() => null)));
    for (const v of seeds) { if (v && !map.has(v.num)) map.set(v.num, v); }
  }
  const verses = Array.from(map.values()).sort((x, y) => x.s - y.s || x.a - y.a);
  return { verses, termCounts };
}

async function loadEdition(coll) {
  if (_editionCache[coll]) return _editionCache[coll];
  const r = await fetch(`${HADITH_CDN}/eng-${coll}.min.json`);
  if (!r.ok) throw new Error('Could not load ' + coll);
  const j = await r.json();
  const arr = j.hadiths || [];
  _editionCache[coll] = arr;
  return arr;
}

/* strip the isnād opener ("Narrated So-and-so:", "X reported:", "It is narrated
   on the authority of X that…") so the card opens straight into the matn */
const stripNarrator = t => t.replace(
  /^\s*(?:narrated[^:]{0,100}|[^:]{0,100}\breported|it (?:is|was) narrated[^:]{0,120}):\s*/i, ''
).trim();

async function loadConceptHadith(c, colls, onProgress) {
  const pats = c.hadithPatterns.map(p => new RegExp(p, 'i'));
  const allHits = [];
  const totals = {};
  for (const coll of colls) {
    if (onProgress) onProgress(coll);
    const arr = await loadEdition(coll);
    totals[coll] = arr.length;
    for (const h of arr) {
      if (h.text && pats.some(re => re.test(h.text))) {
        allHits.push({ coll, num: h.hadithnumber, ref: h.reference, text: stripNarrator(h.text), grades: h.grades || [] });
      }
    }
  }
  /* pass 1 — collapse exact duplicates: normalized text fingerprint (first 120 chars) */
  const _norm = t => t.toLowerCase().replace(/[^a-z0-9 ]/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 120);
  const groups = new Map();
  for (const h of allHits) {
    const key = _norm(h.text);
    if (!groups.has(key)) {
      groups.set(key, { text: h.text, sources: [] });
    }
    groups.get(key).sources.push({ coll: h.coll, num: h.num, ref: h.ref, grades: h.grades });
  }
  /* pass 2 — cluster near-duplicates (same report, minor wording diffs) by word-set similarity.
     The "narrated So-and-so:" prefix is stripped so the same matn via different narrators still clusters. */
  const _wordSet = t => new Set(
    t.replace(/^narrated[^:]{0,80}:/i, '').toLowerCase()
      .replace(/[^a-z0-9 ]/g, ' ').split(/\s+/).filter(w => w.length > 2)
  );
  const _sim = (a, b) => {
    if (!a.size || !b.size) return 0;
    let inter = 0;
    for (const w of a) if (b.has(w)) inter++;
    return inter / (a.size + b.size - inter);   // Jaccard
  };
  const SIM_THRESHOLD = 0.55;
  const items = Array.from(groups.values()).map(g => ({ ...g, _ws: _wordSet(g.text) }));
  const clusters = [];
  for (const it of items) {
    let home = null;
    for (const cl of clusters) {
      if (cl.variants.some(v => _sim(v._ws, it._ws) >= SIM_THRESHOLD)) { home = cl; break; }
    }
    if (home) home.variants.push(it);
    else clusters.push({ variants: [it] });
  }
  const merged = clusters.map(cl => ({
    variants: cl.variants.map(v => ({ text: v.text, sources: v.sources })),
  }));
  /* per-term breakdown: how many distinct (merged) reports mention each term */
  const labels = c.hadithLabels || c.hadithPatterns.map(hadithPatLabel);
  const counts = labels.map(() => 0);
  for (const cl of merged) {
    const text = cl.variants.map(v => v.text).join(' • ');
    pats.forEach((re, i) => { if (re.test(text)) counts[i]++; });
  }
  const termCounts = labels
    .map((term, i) => ({ term, count: counts[i], re: pats[i] }))
    .filter(x => x.count > 0)
    .sort((a, b) => b.count - a.count);
  return { merged, totals, termCounts };
}

/* derive a readable label from a hadith regex pattern when none is given:
   take the first alternative, drop regex syntax */
const hadithPatLabel = p => p.split('|')[0]
  .replace(/\\b/g, '').replace(/\[[^\]]*\]\??/g, '').replace(/[\\^$.*+?(){}]/g, '').trim() || p;

/* ---------- other scriptures: Bible / Torah (KJV) + Bhagavad Gītā ---------- */
const KJV_URL = 'https://cdn.jsdelivr.net/gh/thiagobodruk/bible@master/json/en_kjv.json';
const GITA_VERSE_URL = 'https://cdn.jsdelivr.net/gh/gita/gita@main/data/verse.json';
const GITA_TRANS_URL = 'https://cdn.jsdelivr.net/gh/gita/gita@main/data/translation.json';

const KJV_BOOKS = ['Genesis', 'Exodus', 'Leviticus', 'Numbers', 'Deuteronomy', 'Joshua', 'Judges', 'Ruth',
  '1 Samuel', '2 Samuel', '1 Kings', '2 Kings', '1 Chronicles', '2 Chronicles', 'Ezra', 'Nehemiah', 'Esther',
  'Job', 'Psalms', 'Proverbs', 'Ecclesiastes', 'Song of Solomon', 'Isaiah', 'Jeremiah', 'Lamentations',
  'Ezekiel', 'Daniel', 'Hosea', 'Joel', 'Amos', 'Obadiah', 'Jonah', 'Micah', 'Nahum', 'Habakkuk', 'Zephaniah',
  'Haggai', 'Zechariah', 'Malachi', 'Matthew', 'Mark', 'Luke', 'John', 'Acts', 'Romans', '1 Corinthians',
  '2 Corinthians', 'Galatians', 'Ephesians', 'Philippians', 'Colossians', '1 Thessalonians', '2 Thessalonians',
  '1 Timothy', '2 Timothy', 'Titus', 'Philemon', 'Hebrews', 'James', '1 Peter', '2 Peter', '1 John', '2 John',
  '3 John', 'Jude', 'Revelation'];
const TORAH_BOOK_COUNT = 5;   // Genesis–Deuteronomy

/* corpus downloads are cached as promises so concurrent tabs share one fetch */
const _corpusCache = {};
function fetchCorpus(url) {
  if (!_corpusCache[url]) {
    _corpusCache[url] = fetch(url).then(r => {
      if (!r.ok) { delete _corpusCache[url]; throw new Error('fetch failed: ' + url); }
      return r.json();
    }).catch(e => { delete _corpusCache[url]; throw e; });
  }
  return _corpusCache[url];
}

/* match the term and simple plurals (demon→demons) but not unrelated longer
   words (demon↛demonstrations) by anchoring a trailing word boundary */
const _termPats = terms => terms.map(t => ({ term: t, re: new RegExp('\\b' + _reEsc(t) + '(?:s|es)?\\b', 'i') }));
/* a concept's base terms plus any corpus-specific equivalents, e.g. Māra (the
   Buddhist demon) only makes sense as a "demons" term inside the Pāli canon */
const conceptTerms = (c, kind) => c.quranTerms.concat((c.crossTerms && c.crossTerms[kind]) || []);

async function loadConceptBible(c, torahOnly) {
  const books = await fetchCorpus(KJV_URL);
  const pats = _termPats(conceptTerms(c, torahOnly ? 'torah' : 'bible'));
  const counts = {};
  const rows = [];
  books.forEach((b, bi) => {
    if (torahOnly && bi >= TORAH_BOOK_COUNT) return;
    b.chapters.forEach((ch, ci) => ch.forEach((vText, vi) => {
      const clean = vText.replace(/\{[^}]*\}/g, '').replace(/\s+/g, ' ').trim();   // drop KJV translator notes
      const matched = pats.filter(p => p.re.test(clean));
      if (matched.length) {
        matched.forEach(p => { counts[p.term] = (counts[p.term] || 0) + 1; });
        const book = KJV_BOOKS[bi] || b.abbrev;
        rows.push({
          refLabel: `${book} ${ci + 1}:${vi + 1}`,
          url: `https://www.biblegateway.com/passage/?search=${encodeURIComponent(`${book} ${ci + 1}:${vi + 1}`)}&version=KJV`,
          en: clean,
        });
      }
    }));
  });
  const termCounts = Object.entries(counts).map(([term, count]) => ({ term, count })).sort((a, b) => b.count - a.count);
  return { rows, termCounts };
}

async function loadConceptGita(c) {
  const [verses, trans] = await Promise.all([fetchCorpus(GITA_VERSE_URL), fetchCorpus(GITA_TRANS_URL)]);
  const vById = new Map(verses.map(v => [v.id, v]));
  /* group every English rendering per verse — translators differ in vocabulary,
     so a verse matches if ANY of its English translations matches a term */
  const engByVerse = new Map();
  for (const t of trans) {
    if (t.lang !== 'english') continue;
    if (!engByVerse.has(t.verse_id)) engByVerse.set(t.verse_id, []);
    engByVerse.get(t.verse_id).push(t);
  }
  const pats = _termPats(conceptTerms(c, 'gita'));
  const counts = {};
  const rows = [];
  for (const [vid, list] of engByVerse) {
    let best = null, bestTerms = null;
    for (const t of list) {
      const en = (t.description || '').replace(/\s+/g, ' ').trim();
      const matched = pats.filter(p => p.re.test(en));
      if (matched.length && (!bestTerms || matched.length > bestTerms.length)) {
        best = { t, en };
        bestTerms = matched;
      }
    }
    if (best) {
      bestTerms.forEach(p => { counts[p.term] = (counts[p.term] || 0) + 1; });
      const v = vById.get(vid);
      const ch = v ? v.chapter_number : '?', vs = v ? v.verse_number : '?';
      rows.push({
        id: vid, refLabel: `BG ${ch}.${vs}`,
        url: v ? `https://bhagavadgita.io/chapter/${ch}/verse/${vs}` : 'https://bhagavadgita.io',
        en: best.en, sa: v ? v.text : '', author: best.t.authorName,
      });
    }
  }
  rows.sort((a, b) => a.id - b.id);
  const termCounts = Object.entries(counts).map(([term, count]) => ({ term, count })).sort((a, b) => b.count - a.count);
  return { rows, termCounts };
}

/* shared matcher over a bundled {name, verses:[{ref, sa, en, url}]} corpus */
function searchBundle(books, c, kind, fallbackUrl) {
  const pats = _termPats(conceptTerms(c, kind));
  const counts = {};
  const rows = [];
  for (const b of books) {
    for (const v of b.verses) {
      const en = (v.en || '').replace(/\s+/g, ' ').trim();
      if (!en) continue;
      const matched = pats.filter(p => p.re.test(en));
      if (matched.length) {
        matched.forEach(p => { counts[p.term] = (counts[p.term] || 0) + 1; });
        rows.push({ refLabel: v.ref || b.name, url: v.url || fallbackUrl, en, sa: (v.sa || '').trim() });
      }
    }
  }
  const termCounts = Object.entries(counts).map(([term, count]) => ({ term, count })).sort((a, b) => b.count - a.count);
  return { rows, termCounts };
}

/* bundled corpora: [global, script src, fallback url]. Loaded on demand so the
   ~13 MB of scripture only downloads when its tab (or the Overview) is opened. */
const BUNDLES = {
  upanishads: ['IJMA_UPANISHADS', 'upanishads-data.js', 'https://www.wisdomlib.org'],
  pali: ['IJMA_PALI', 'pali-canon-data.js', 'https://suttacentral.net'],
  suttas: ['IJMA_SUTTAS', 'suttas-data.js', 'https://suttacentral.net'],
  daodejing: ['IJMA_DAODEJING', 'daodejing-data.js', 'https://ctext.org/dao-de-jing'],
  avesta: ['IJMA_AVESTA', 'avesta-data.js', 'https://avesta.org'],
  fourbooks: ['IJMA_FOURBOOKS', 'fourbooks-data.js', 'https://ctext.org/analects'],
  vedas: ['IJMA_VEDAS', 'vedas-data.js', 'https://en.wikisource.org/wiki/The_Hymns_of_the_Rigveda'],
  mishnah: ['IJMA_MISHNAH', 'mishnah-data.js', 'https://www.sefaria.org/Mishnah'],
  plato: ['IJMA_PLATO', 'plato-data.js', 'https://classics.mit.edu/Browse/browse-Plato.html'],
  aristotle: ['IJMA_ARISTOTLE', 'aristotle-data.js', 'https://classics.mit.edu/Browse/browse-Aristotle.html'],
  plotinus: ['IJMA_PLOTINUS', 'plotinus-data.js', 'https://en.wikisource.org/wiki/Plotinus_(MacKenna)'],
};
const _bundleP = {};
function ensureBundle(src) {
  if (!_bundleP[src]) {
    _bundleP[src] = new Promise((res, rej) => {
      const s = document.createElement('script'); s.src = src; s.async = true;
      s.onload = () => res(); s.onerror = () => { delete _bundleP[src]; rej(new Error('load ' + src)); };
      document.head.appendChild(s);
    });
  }
  return _bundleP[src];
}
async function loadBundle(kind, c) {
  const [global, src, fallback] = BUNDLES[kind];
  if (!window[global]) await ensureBundle(src);
  return searchBundle(window[global] || [], c, kind, fallback);
}

/* ---------- search-term highlighting ---------- */
const _reEsc = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
/* quranTerms are plain words; extend to the whole word so "angel" lights up "angels" */
const quranHl = c => new RegExp('(?:' + c.quranTerms.map(t => _reEsc(t)).join('|') + ')\\w*', 'gi');
/* hadithPatterns are already regexes — same ones used to filter the reports */
const hadithHl = c => new RegExp('(?:' + c.hadithPatterns.map(p => '(?:' + p + ')').join('|') + ')\\w*', 'gi');
/* scripture tabs match on quranTerms + the corpus-specific crossTerms, so the
   highlighter must cover both (e.g. "Eden" on Bible, "Māra" on Pāli) */
const scriptureHl = (c, kind) => {
  const terms = conceptTerms(c, kind).filter(Boolean);
  if (!terms.length) return null;
  const cjk = terms.filter(_isCJK), latin = terms.filter(t => !_isCJK(t));
  const parts = [];
  if (latin.length) parts.push('(?:' + latin.map(_reEsc).join('|') + ')\\w*');
  if (cjk.length) parts.push('(?:' + cjk.map(_reEsc).join('|') + ')');
  return new RegExp(parts.join('|'), 'gi');
};

function Hl({ text, re }) {
  if (!re || !text) return text || null;
  const out = [];
  let last = 0, m;
  re.lastIndex = 0;
  while ((m = re.exec(text)) !== null) {
    if (m.index > last) out.push(text.slice(last, m.index));
    out.push(<mark key={m.index} className="hl-term">{m[0]}</mark>);
    last = m.index + m[0].length;
    if (m[0].length === 0) re.lastIndex++;
  }
  out.push(text.slice(last));
  return out;
}

/* ---------- verse row (lazy Arabic on expand) ---------- */
function VerseRow({ v, hl }) {
  const [open, setOpen] = useState(false);
  const [ar, setAr] = useState(v.ar);
  useEffect(() => {
    if (open && !ar) { aqArabic(v.num).then(setAr).catch(() => setAr('')); }
  }, [open]);
  return (
    <div className={'verse-row' + (open ? ' open' : '')}>
      <div className="verse-head" role="button" tabIndex={0} aria-expanded={open}
        onClick={() => setOpen(o => !o)}
        onKeyDown={e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setOpen(o => !o); } }}>
        <a className="verse-ref verse-ref-link" href={`https://quran.com/${v.s}/${v.a}`}
          target="_blank" rel="noopener noreferrer" onClick={e => e.stopPropagation()}>Q {v.s}:{v.a}</a>
        <span className="verse-surah">{v.surahName}</span>
        <span className="verse-en"><Hl text={v.en} re={hl} /></span>
        <span className="verse-chev"><Icon name="chevron" size={12} /></span>
      </div>
      {open && (
        <div className="verse-ar-wrap">
          {ar == null ? <span className="cncpt-muted">Loading Arabic…</span>
            : ar ? <div className="verse-ar" lang="ar" dir="rtl">{ar}</div>
              : <span className="cncpt-muted">Arabic unavailable.</span>}
        </div>
      )}
    </div>
  );
}

/* clickable frequency pills — picking one filters the list to that term/form */
const _isCJK = s => /[㐀-鿿]/.test(s);
function termTest(term) {
  if (_isCJK(term)) return t => (t || '').includes(term);
  const re = new RegExp('\\b' + _reEsc(term) + '(?:s|es)?\\b', 'i');
  return t => re.test(t || '');
}
function FreqPills({ counts, active, onPick, title }) {
  if (!counts || !counts.length) return null;
  return (
    <div className="quran-freq">
      {counts.map(f => (
        <button key={f.term} type="button"
          className={'quran-freq-pill' + (active === f.term ? ' active' : '')}
          onClick={() => onPick(active === f.term ? null : f.term)}
          title={active === f.term ? 'Clear filter' : (title ? title(f) : `Show only "${f.term}"`)}>
          <span className="quran-freq-term">{f.term}</span>
          <span className="quran-freq-n">{f.count}</span>
        </button>
      ))}
    </div>
  );
}

function ConceptQuran({ c }) {
  const [st, setSt] = useState('loading');
  const [verses, setVerses] = useState([]);
  const [termCounts, setTermCounts] = useState([]);
  const [term, setTerm] = useState(null);
  const hl = useMemo(() => quranHl(c), [c.id]);
  const load = () => {
    setSt('loading'); setTerm(null);
    loadConceptQuran(c).then(({ verses: vs, termCounts: tc }) => { setVerses(vs); setTermCounts(tc); setSt('ok'); }).catch(() => setSt('err'));
  };
  useEffect(() => { load(); }, [c.id]);
  const shown = useMemo(() => { if (!term) return verses; const test = termTest(term); return verses.filter(v => test(v.en)); }, [verses, term]);
  return (
    <section style={{ marginTop: '2.4rem' }}>
      <div className="section-label" style={{ marginBottom: '.8rem' }}>
        <span className="eyebrow">From the Qurʾan · live via alquran.cloud</span>
        <h2>Qurʾanic references {st === 'ok' && <span className="cncpt-count">{term ? `${shown.length} / ${verses.length}` : verses.length}</span>}</h2>
        {st === 'ok' && <FreqPills counts={termCounts} active={term} onPick={setTerm} title={f => `${f.count} verse${f.count === 1 ? '' : 's'} mention "${f.term}"`} />}
      </div>
      {st === 'loading' && <div className="cncpt-loading"><span className="spinner" /> Searching the Qurʾan…</div>}
      {st === 'err' && <div className="cncpt-err">Couldn't reach the Qurʾan API. <button className="btn" onClick={load}>Retry</button></div>}
      {st === 'ok' && (
        verses.length === 0 ? <div className="cncpt-muted">No verses returned.</div> :
          <div className="verse-list">{shown.map(v => <VerseRow key={v.num} v={v} hl={hl} />)}</div>
      )}
    </section>
  );
}

/* ---------- hadith ---------- */
const COLL_LABEL = { bukhari: 'Ṣaḥīḥ al-Bukhārī', muslim: 'Ṣaḥīḥ Muslim' };

function HadithMatch({ h, hl }) {
  /* flatten every reference across all near-duplicate variants; stepping through
     them swaps both the displayed text and the reference together */
  const refs = useMemo(
    () => h.variants.flatMap((v, vi) => v.sources.map(s => ({ ...s, vi }))),
    [h]
  );
  const [rawIdx, setIdx] = useState(0);
  const idx = Math.min(rawIdx, refs.length - 1);
  const src = refs[idx];
  const text = h.variants[src.vi].text;
  const grade = src.grades && src.grades.length ? src.grades.map(g => g.grade).filter(Boolean)[0] : '';
  const multi = refs.length > 1;
  return (
    <div className="hadith-card">
      <div className="hadith-text"><Hl text={text} re={hl} /></div>
      <div className="hadith-sources">
        {multi && (
          <button className="hadith-src-nav" onClick={() => setIdx(i => (i - 1 + refs.length) % refs.length)} aria-label="Previous narration">&#8592;</button>
        )}
        <span className="hadith-coll">{COLL_LABEL[src.coll] || src.coll}</span>
        <a className="hadith-ref hadith-ref-link" href={`https://sunnah.com/${src.coll}:${src.num}`}
          target="_blank" rel="noopener noreferrer">
          no.{' '}{src.num}{src.ref && src.ref.book ? ` · bk. ${src.ref.book}:${src.ref.hadith}` : ''}
        </a>
        {grade ? <span className="hadith-grade">{grade}</span> : <span className="hadith-grade sahih">ṣaḥīḥ</span>}
        {multi && (
          <>
            <span className="hadith-src-count">narration {idx + 1} / {refs.length}</span>
            <button className="hadith-src-nav" onClick={() => setIdx(i => (i + 1) % refs.length)} aria-label="Next narration">&#8594;</button>
          </>
        )}
      </div>
    </div>
  );
}

const HADITH_PER_PAGE = 50;

function ConceptHadith({ c }) {
  const [st, setSt] = useState('loading');
  const [data, setData] = useState(null);
  const [progress, setProgress] = useState('');
  const [shown, setShown] = useState(HADITH_PER_PAGE);
  const [term, setTerm] = useState(null);
  const hl = useMemo(() => hadithHl(c), [c.id]);
  const load = () => {
    setSt('loading');
    setShown(HADITH_PER_PAGE); setTerm(null);
    loadConceptHadith(c, C_DATA.hadithCollections, coll => setProgress(coll))
      .then(d => { setData(d); setSt('ok'); }).catch(() => setSt('err'));
  };
  /* auto-load — collections are cached at module level, so this only
     downloads once per session no matter how many concepts are visited */
  useEffect(() => { load(); }, [c.id]);
  const allHits = data ? data.merged : [];
  const termCounts = data ? data.termCounts || [] : [];
  const hits = useMemo(() => {
    if (!term) return allHits;
    const re = (termCounts.find(t => t.term === term) || {}).re;
    if (!re) return allHits;
    return allHits.filter(h => h.variants.some(v => re.test(v.text)));
  }, [allHits, term, termCounts]);
  return (
    <section style={{ marginTop: '2.8rem' }}>
      <div className="section-label" style={{ marginBottom: '.8rem' }}>
        <span className="eyebrow">From the Sunna · live via the hadith CDN</span>
        <h2>Hadith references {st === 'ok' && <span className="cncpt-count">{term ? `${hits.length} / ${allHits.length}` : allHits.length}</span>}</h2>
        {st === 'ok' && <FreqPills counts={termCounts} active={term} onPick={t => { setTerm(t); setShown(HADITH_PER_PAGE); }} title={f => `${f.count} report${f.count === 1 ? '' : 's'} mention "${f.term}"`} />}
      </div>
      {st === 'loading' && <div className="cncpt-loading"><span className="spinner" /> Loading {progress ? (COLL_LABEL[progress] || progress) : 'collections'} and filtering…</div>}
      {st === 'err' && <div className="cncpt-err">Couldn't reach the hadith API. <button className="btn" onClick={load}>Retry</button></div>}
      {st === 'ok' && (
        hits.length === 0 ? <div className="cncpt-muted">No matches found.</div> : (
          <>
            <div className="hadith-list">
              {hits.slice(0, shown).map((h, i) => <HadithMatch key={i} h={h} hl={hl} />)}
            </div>
            {shown < hits.length && (
              <button className="btn" style={{ marginTop: '.8rem' }} onClick={() => setShown(n => n + HADITH_PER_PAGE)}>
                Show more ({hits.length - shown} remaining)
              </button>
            )}
          </>
        )
      )}
    </section>
  );
}

/* ---------- Bible / Torah / Gītā / Upaniṣad tabs ---------- */
/* one row for all of them: ref links out to the source site; rows with original-
   language text (Sanskrit) expand to show it */
function ScriptureVerseRow({ v, hl }) {
  const [open, setOpen] = useState(false);
  const expandable = !!v.sa;
  return (
    <div className={'verse-row' + (open ? ' open' : '')}>
      <div className="verse-head" role={expandable ? 'button' : undefined} tabIndex={expandable ? 0 : undefined}
        aria-expanded={expandable ? open : undefined}
        style={expandable ? null : { cursor: 'default' }}
        onClick={expandable ? () => setOpen(o => !o) : undefined}
        onKeyDown={expandable ? e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setOpen(o => !o); } } : undefined}>
        <a className="verse-ref verse-ref-link" style={{ minWidth: '9.5em' }} href={v.url}
          target="_blank" rel="noopener noreferrer" onClick={e => e.stopPropagation()}>{v.refLabel}</a>
        <span className="verse-en"><Hl text={v.en} re={hl} /></span>
        {expandable && <span className="verse-chev"><Icon name="chevron" size={12} /></span>}
      </div>
      {open && (
        <div className="verse-ar-wrap">
          <div className="verse-orig">{v.sa}</div>
          {v.author && <div className="cncpt-muted" style={{ marginTop: '.4rem' }}>tr. {v.author}</div>}
        </div>
      )}
    </div>
  );
}

const SCRIPTURE_META = {
  bible: { eyebrow: 'From the Bible · King James Version', title: 'Biblical references', loading: 'Searching the Bible…', err: 'Couldn\'t load the Bible corpus.' },
  torah: { eyebrow: 'From the Torah · Pentateuch (KJV)', title: 'Torah references', loading: 'Searching the Torah…', err: 'Couldn\'t load the Torah corpus.' },
  gita: { eyebrow: 'From the Bhagavad Gītā · gita corpus', title: 'Gītā references', loading: 'Searching the Gītā…', err: 'Couldn\'t load the Gītā corpus.' },
  upanishads: { eyebrow: 'From the Upaniṣads · 7 principal, via wisdomlib', title: 'Upaniṣad references', loading: 'Searching the Upaniṣads…', err: 'Couldn\'t load the Upaniṣad corpus.' },
  mishnah: { eyebrow: 'From the Mishnah · via Sefaria (Davidson)', title: 'Mishnaic references', loading: 'Searching the Mishnah…', err: 'Couldn\'t load the Mishnah corpus.' },
  pali: { eyebrow: 'From the Dhammapada · tr. Sujato', title: 'Dhammapada references', loading: 'Searching the Dhammapada…', err: 'Couldn\'t load the Dhammapada corpus.' },
  suttas: { eyebrow: 'From the Sutta Piṭaka · Khuddaka (Snp, Ud, Iti), tr. Sujato', title: 'Sutta references', loading: 'Searching the suttas…', err: 'Couldn\'t load the Sutta corpus.' },
  daodejing: { eyebrow: 'From the Daodejing · tr. James Legge, Chinese via ctext', title: 'Daodejing references', loading: 'Searching the Daodejing…', err: 'Couldn\'t load the Daodejing corpus.' },
  avesta: { eyebrow: 'From the Avesta · Yasna & Gāthās, tr. Mills/Darmesteter', title: 'Avesta references', loading: 'Searching the Avesta…', err: 'Couldn\'t load the Avesta corpus.' },
  fourbooks: { eyebrow: 'From the Four Books · Analects, Mencius, Great Learning, Doctrine of the Mean (Legge)', title: 'Confucian references', loading: 'Searching the Four Books…', err: 'Couldn\'t load the Four Books corpus.' },
  vedas: { eyebrow: 'From the Vedas · Rigveda, tr. Griffith', title: 'Vedic references', loading: 'Searching the Rigveda…', err: 'Couldn\'t load the Vedic corpus.' },
  plato: { eyebrow: 'From Plato · the dialogues, tr. Jowett', title: 'Platonic references', loading: 'Searching Plato…', err: 'Couldn\'t load the Plato corpus.' },
  plotinus: { eyebrow: 'From Plotinus · the Enneads, tr. MacKenna', title: 'Plotinian references', loading: 'Searching the Enneads…', err: 'Couldn\'t load the Plotinus corpus.' },
  aristotle: { eyebrow: 'From Aristotle · the works, MIT Classics', title: 'Aristotelian references', loading: 'Searching Aristotle…', err: 'Couldn\'t load the Aristotle corpus.' },
};
const SCRIPTURE_PER_PAGE = 50;

function ConceptScripture({ c, kind }) {
  const meta = SCRIPTURE_META[kind];
  const [st, setSt] = useState('loading');
  const [rows, setRows] = useState([]);
  const [termCounts, setTermCounts] = useState([]);
  const [shown, setShown] = useState(SCRIPTURE_PER_PAGE);
  const [term, setTerm] = useState(null);
  const hl = useMemo(() => scriptureHl(c, kind), [c.id, kind]);
  const load = () => {
    setSt('loading');
    setShown(SCRIPTURE_PER_PAGE); setTerm(null);
    const p = kind === 'gita' ? loadConceptGita(c)
      : (kind === 'bible' || kind === 'torah') ? loadConceptBible(c, kind === 'torah')
        : loadBundle(kind, c);
    p.then(({ rows: rs, termCounts: tc }) => { setRows(rs); setTermCounts(tc); setSt('ok'); }).catch(() => setSt('err'));
  };
  useEffect(() => { load(); }, [c.id, kind]);
  const shown_rows = useMemo(() => { if (!term) return rows; const test = termTest(term); return rows.filter(v => test(v.en)); }, [rows, term]);
  return (
    <section style={{ marginTop: '2.4rem' }}>
      <div className="section-label" style={{ marginBottom: '.8rem' }}>
        <span className="eyebrow">{meta.eyebrow}</span>
        <h2>{meta.title} {st === 'ok' && <span className="cncpt-count">{term ? `${shown_rows.length} / ${rows.length}` : rows.length}</span>}</h2>
        {st === 'ok' && <FreqPills counts={termCounts} active={term} onPick={t => { setTerm(t); setShown(SCRIPTURE_PER_PAGE); }} title={f => `${f.count} verse${f.count === 1 ? '' : 's'} mention "${f.term}"`} />}
      </div>
      {st === 'loading' && <div className="cncpt-loading"><span className="spinner" /> {meta.loading}</div>}
      {st === 'err' && <div className="cncpt-err">{meta.err} <button className="btn" onClick={load}>Retry</button></div>}
      {st === 'ok' && (
        rows.length === 0 ? <div className="cncpt-muted">No verses matched this concept's terms.</div> : (
          <>
            <div className="verse-list">
              {shown_rows.slice(0, shown).map((v, i) => <ScriptureVerseRow key={v.refLabel + '-' + i} v={v} hl={hl} />)}
            </div>
            {shown < shown_rows.length && (
              <button className="btn" style={{ marginTop: '.8rem' }} onClick={() => setShown(n => n + SCRIPTURE_PER_PAGE)}>
                Show more ({shown_rows.length - shown} remaining)
              </button>
            )}
          </>
        )
      )}
    </section>
  );
}

/* ---------- per-concept comparison matrix ---------- */
/* each corpus + how to count this concept's references in it */
const _bundleCount = kind => c => loadBundle(kind, c).then(r => r.rows.length);
const COMPARE_CORPORA = [
  ['quran', 'Qurʾan', c => loadConceptQuran(c).then(r => r.verses.length)],
  ['hadith', 'Hadith', c => loadConceptHadith(c, C_DATA.hadithCollections).then(r => r.merged.length)],
  ['bible', 'Bible', c => loadConceptBible(c, false).then(r => r.rows.length)],
  ['torah', 'Torah', c => loadConceptBible(c, true).then(r => r.rows.length)],
  ['mishnah', 'Mishnah', _bundleCount('mishnah')],
  ['gita', 'Gītā', c => loadConceptGita(c).then(r => r.rows.length)],
  ['upanishads', 'Upaniṣads', _bundleCount('upanishads')],
  ['vedas', 'Vedas', _bundleCount('vedas')],
  ['pali', 'Dhammapada', _bundleCount('pali')],
  ['suttas', 'Suttas', _bundleCount('suttas')],
  ['daodejing', 'Daodejing', _bundleCount('daodejing')],
  ['fourbooks', 'Four Books', _bundleCount('fourbooks')],
  ['avesta', 'Avesta', _bundleCount('avesta')],
  ['plato', 'Plato', _bundleCount('plato')],
  ['aristotle', 'Aristotle', _bundleCount('aristotle')],
  ['plotinus', 'Plotinus', _bundleCount('plotinus')],
];

function ConceptCompare({ c, onTab }) {
  const [counts, setCounts] = useState({});
  useEffect(() => {
    let alive = true;
    setCounts({});
    COMPARE_CORPORA.forEach(([key, , fn]) => {
      fn(c).then(n => { if (alive) setCounts(p => ({ ...p, [key]: n })); })
        .catch(() => { if (alive) setCounts(p => ({ ...p, [key]: -1 })); });
    });
    return () => { alive = false; };
  }, [c.id]);
  const max = Math.max(1, ...Object.values(counts).filter(n => n > 0));
  return (
    <section style={{ marginTop: '2.4rem' }}>
      <div className="section-label" style={{ marginBottom: '1rem' }}>
        <span className="eyebrow">Across the traditions</span>
        <h2>Where this concept appears</h2>
      </div>
      <div className="cmp-matrix" style={{ '--accent': c.accent }}>
        {COMPARE_CORPORA.map(([key, label]) => {
          const n = counts[key];
          const val = typeof n === 'number' && n > 0 ? n : 0;
          return (
            <button key={key} className="cmp-row" onClick={() => onTab && onTab(key)} title={`Open the ${label} tab`}>
              <span className="cmp-label">{label}</span>
              <span className="cmp-bar-wrap"><span className="cmp-bar" style={{ width: (val / max * 100) + '%' }} /></span>
              <span className="cmp-n">{n === undefined ? '…' : (n < 0 ? '—' : n)}</span>
            </button>
          );
        })}
      </div>
      <p style={{ fontFamily: 'var(--mono)', fontSize: '11px', color: 'var(--ink-4)', marginTop: '1rem', maxWidth: '70ch', lineHeight: 1.6 }}>
        Matching verses or reports per corpus — a rough measure of where this concept lives across the traditions. Click any bar to read that corpus. Counts use each concept's search terms, so they are indicative, not exhaustive.
      </p>
    </section>
  );
}

/* ---------- pages ---------- */
/* tabs grouped by tradition, each group sharing a coloured underline */
const CONCEPT_GROUPS = [
  { color: null, tabs: [['overview', 'Overview']] },
  { color: '#2f6a4f', tabs: [['quran', 'Qurʾan'], ['hadith', 'Hadith']] },           // Islam
  { color: '#3b6ea5', tabs: [['bible', 'Bible'], ['torah', 'Torah'], ['mishnah', 'Mishnah']] }, // Judaeo-Christian
  { color: '#c2772f', tabs: [['gita', 'Gītā'], ['upanishads', 'Upaniṣads'], ['vedas', 'Vedas']] }, // Vedic
  { color: '#a8842a', tabs: [['pali', 'Dhammapada'], ['suttas', 'Suttas']] },         // Buddhist
  { color: '#9a3d3d', tabs: [['daodejing', 'Daodejing'], ['fourbooks', 'Four Books']] }, // Chinese
  { color: '#6a4b8a', tabs: [['avesta', 'Avesta']] },                                 // Zoroastrian
  { color: '#2f7a7a', tabs: [['plato', 'Plato'], ['aristotle', 'Aristotle'], ['plotinus', 'Plotinus']] }, // Hellenic
];
const CONCEPT_TABS = CONCEPT_GROUPS.flatMap(g => g.tabs);

function ConceptPage({ id, onNav }) {
  const c = C_DATA.concepts.find(x => x.id === id);
  const [tab, setTab] = useState('quran');
  /* Qurʾan + hadith load eagerly; the heavier corpora (Bible 4.5 MB, Gītā ~4 MB)
     mount on first visit to their tab, then stay mounted */
  const [visited, setVisited] = useState({ quran: true, hadith: true });
  const goTab = t => { setTab(t); setVisited(v => v[t] ? v : { ...v, [t]: true }); };
  useEffect(() => { window.scrollTo(0, 0); setTab('quran'); setVisited({ quran: true, hadith: true }); }, [id]);
  if (!c) return (
    <main className="page-pad" style={{ maxWidth: 'var(--maxw)', margin: '0 auto', padding: '2.4rem clamp(1.2rem,4vw,3rem)' }}>
      <p>Unknown concept. <button className="btn" onClick={() => onNav('#/concepts')}>All concepts</button></p>
    </main>
  );
  return (
    <main className="fade-in" key={id} style={{ maxWidth: 'var(--maxw)', margin: '0 auto', padding: '2.4rem clamp(1.2rem,4vw,3rem) 5rem' }}>
      <button className="btn btn-ghost" style={{ marginBottom: '1.4rem', paddingLeft: 0 }} onClick={() => onNav('#/concepts')}>
        <span style={{ display: 'inline-flex', transform: 'rotate(180deg)', marginRight: '.4em', verticalAlign: '-2px' }}><Icon name="arrow" size={14} /></span>
        Concepts
      </button>
      <div className="concept-hero">
        {c.art && (
          <div className="concept-hero-art" style={{ '--accent': c.accent }}>
            <div className="concept-hero-img" style={{ backgroundImage: `url("${c.art}")` }} />
            {c.artCredit && <span className="concept-hero-credit">{c.artCredit}</span>}
          </div>
        )}
        <div className="concept-hero-text">
          {CONCEPT_CAT[c.id] && <span className="concept-cat-tag" style={{ marginBottom: '.6rem' }}>{CONCEPT_CAT[c.id]}</span>}
          <div style={{ display: 'flex', alignItems: 'baseline', gap: '.8em', flexWrap: 'wrap', marginBottom: '.5rem' }}>
            <h1 style={{ fontSize: 'clamp(28px,3.4vw,42px)', fontWeight: 700, letterSpacing: '-0.01em', margin: 0 }}>{c.title}</h1>
            <span style={{ fontFamily: 'var(--mono)', fontSize: '15px', color: 'var(--ink-3)' }}><Ar>{c.ar}</Ar> · {c.translit}</span>
          </div>
          <p style={{ fontSize: '18px', color: 'var(--ink-2)', maxWidth: '54ch', margin: '.6rem 0 0', lineHeight: 1.55 }}>{c.blurb}</p>
        </div>
      </div>
      <div className="cncpt-tabs" role="tablist">
        {CONCEPT_GROUPS.map((g, gi) => (
          <div key={gi} className="cncpt-tabgroup" style={g.color ? { '--grp': g.color } : null}>
            {g.tabs.map(([k, label]) => (
              <button key={k} role="tab" aria-selected={tab === k} className={'tab' + (tab === k ? ' active' : '')} onClick={() => goTab(k)}>{label}</button>
            ))}
          </div>
        ))}
      </div>
      {/* visited tabs stay mounted so loaded references survive tab switches */}
      {visited.overview && <div style={{ display: tab === 'overview' ? 'block' : 'none' }}><ConceptCompare c={c} onTab={goTab} /></div>}
      <div style={{ display: tab === 'quran' ? 'block' : 'none' }}><ConceptQuran c={c} /></div>
      <div style={{ display: tab === 'hadith' ? 'block' : 'none' }}><ConceptHadith c={c} /></div>
      {CONCEPT_TABS.filter(([k]) => !['overview', 'quran', 'hadith'].includes(k)).map(([k]) => (
        visited[k] ? <div key={k} style={{ display: tab === k ? 'block' : 'none' }}><ConceptScripture c={c} kind={k} /></div> : null
      ))}
      <p style={{ fontFamily: 'var(--mono)', fontSize: '11px', color: 'var(--ink-4)', marginTop: '2.5rem', maxWidth: '74ch', lineHeight: 1.6 }}>
        {C_DATA.intro.note}
      </p>
    </main>
  );
}

function ConceptsPage({ onNav }) {
  const I = C_DATA.intro;
  useEffect(() => { window.scrollTo(0, 0); }, []);
  return (
    <main className="fade-in" style={{ maxWidth: 'var(--maxw)', margin: '0 auto', padding: '2.4rem clamp(1.2rem,4vw,3rem) 5rem' }}>
      <div className="eyebrow" style={{ marginBottom: '.6rem' }}>{I.eyebrow}</div>
      <h1 style={{ fontSize: 'clamp(28px,3.6vw,42px)', fontWeight: 700, letterSpacing: '-0.01em', margin: '0 0 .8rem' }}>{I.title}</h1>
      <p style={{ fontSize: '18px', color: 'var(--ink-2)', maxWidth: '62ch', margin: '0 0 2rem', lineHeight: 1.55 }}>{I.lede}</p>
      <div className="concept-grid">
        {C_DATA.concepts.map(c => (
          <button key={c.id} className="concept-card" style={{ '--accent': c.accent }} onClick={() => onNav('#/concepts/' + c.id)}>
            {c.art && <div className="concept-card-art" style={{ backgroundImage: `url("${c.art}")` }} />}
            <div className="concept-card-ar" lang="ar"><Ar>{c.ar}</Ar></div>
            <div className="concept-card-title">{c.title}</div>
            <div className="concept-card-translit">{c.translit}</div>
            <p className="concept-card-blurb">{c.blurb}</p>
            {CONCEPT_CAT[c.id] && <span className="concept-cat-tag">{CONCEPT_CAT[c.id]}</span>}
          </button>
        ))}
      </div>
    </main>
  );
}

Object.assign(window, { ConceptsPage, ConceptPage });
