// Neurify — Reader (Editorial). Renders authored or generated chapter content.
const { useState: useStateR, useEffect: useEffectR, useRef: useRefR } = React;

function getSection(id) { return (window.CORTEX || []).find(s => s.id === id); }
function getChapter(id, n) { const s = getSection(id); return s && s.chapters.find(c => c.n === n); }
function chapKey(id, n) { return `${id}-${n}`; }

// mastery store
function readMastery() { try { return JSON.parse(localStorage.getItem('cx_mastery') || '{}'); } catch { return {}; } }
function writeMastery(m) { localStorage.setItem('cx_mastery', JSON.stringify(m)); window.dispatchEvent(new Event('cx-mastery')); }
function readProgress() { try { return JSON.parse(localStorage.getItem('cx_progress') || '{}'); } catch { return {}; } }
function writeProgress(p) { localStorage.setItem('cx_progress', JSON.stringify(p)); window.dispatchEvent(new Event('cx-progress')); }

// ─────────── highlights store ───────────
const HL_COLORS = ['yellow', 'green', 'blue', 'pink'];
function readHighlights() { try { return JSON.parse(localStorage.getItem('cx_highlights') || '{}'); } catch { return {}; } }
function writeHighlights(h) { localStorage.setItem('cx_highlights', JSON.stringify(h)); window.dispatchEvent(new Event('cx-highlights')); }
function chapterHighlights(key) { return readHighlights()[key] || []; }
function saveHighlight(key, hl) {
  const all = readHighlights(); const list = all[key] || [];
  const i = list.findIndex(h => h.id === hl.id);
  if (i >= 0) list[i] = hl; else list.push(hl);
  all[key] = list; writeHighlights(all);
}
function deleteHighlight(key, id) {
  const all = readHighlights(); if (!all[key]) return;
  all[key] = all[key].filter(h => h.id !== id);
  if (!all[key].length) delete all[key];
  writeHighlights(all);
}

// offset helpers: measure char offset of (node,offset) within root's concatenated text
function offsetWithin(root, node, off) {
  let total = 0; const walk = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null);
  let t;
  while ((t = walk.nextNode())) {
    if (t === node) return total + off;
    total += t.nodeValue.length;
  }
  return total;
}
// wrap [start,end) of root's text in <mark> spans (splitting text nodes, multiple if crossing elements)
function wrapByOffset(root, start, end, color, id) {
  if (end <= start) return [];
  const walk = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
    acceptNode: (n) => n.parentElement && n.parentElement.closest('.hl') ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT
  });
  const targets = []; let pos = 0; let t;
  while ((t = walk.nextNode())) {
    const len = t.nodeValue.length; const a = pos, b = pos + len;
    const s = Math.max(start, a), e = Math.min(end, b);
    if (s < e) targets.push({ node: t, from: s - a, to: e - a });
    pos = b; if (pos >= end) break;
  }
  const marks = [];
  for (const { node, from, to } of targets) {
    let n = node;
    if (from > 0) n = n.splitText(from);
    if (to - from < n.nodeValue.length) n.splitText(to - from);
    const mark = document.createElement('mark');
    mark.className = 'hl hl-' + color; mark.dataset.hlId = id;
    n.parentNode.insertBefore(mark, n); mark.appendChild(n);
    marks.push(mark);
  }
  return marks;
}
function unwrapHighlight(root, id) {
  root.querySelectorAll(`mark.hl[data-hl-id="${id}"]`).forEach(m => {
    const p = m.parentNode;
    while (m.firstChild) p.insertBefore(m.firstChild, m);
    p.removeChild(m); p.normalize();
  });
}

const CALLOUT_ICON = { cave: 'warning', redflag: 'flag', pitfall: 'warning', tipp: 'lightbulb', praxistipp: 'lightbulb', faustregel: 'lightbulb', algorithm: 'list_view', merke: 'info', hinweis: 'info' };

function Block({ b }) {
  if (b.h2) return <div className="rd-h2" data-h2={b.h2}>{b.h2}</div>;
  if (b.h3) return <div className="rd-h3" data-anchor={b.h3} dangerouslySetInnerHTML={{ __html: b.h3 }} />;
  if (b.p) return <p dangerouslySetInnerHTML={{ __html: b.p }} />;
  if (b.ul) return <ul className="rd-ul">{b.ul.map((li, i) => <li key={i} dangerouslySetInnerHTML={{ __html: li }} />)}</ul>;
  if (b.ol) return <ol className="rd-ol">{b.ol.map((li, i) => <li key={i} dangerouslySetInnerHTML={{ __html: li }} />)}</ol>;
  if (b.callout) {
    const c = b.callout;
    const icon = CALLOUT_ICON[c.type] || 'info';
    const numbered = c.type === 'algorithm';
    return (
      <div className={`callout ${c.type}`}>
        <div className="cl"><span className="ci"><Ic n={icon} size={17} /></span>{c.label}</div>
        <div className="ct">
          {c.text && <p className="co-p" dangerouslySetInnerHTML={{ __html: c.text }} />}
          {c.items && <ol className={`co-list${numbered ? ' co-num' : ''}`}>{c.items.map((it, i) => <li key={i} dangerouslySetInnerHTML={{ __html: it }} />)}</ol>}
        </div>
      </div>
    );
  }
  if (b.quellen) {
    return (
      <ol className="rd-quellen">
        {b.quellen.map((q, i) => <li key={i} dangerouslySetInnerHTML={{ __html: q }} />)}
      </ol>
    );
  }
  if (b.table) {
    return (
      <div className="rd-table-wrap">
        <table className="rd-table">
          <thead><tr>{b.table.head.map((h, i) => <th key={i}>{h}</th>)}</tr></thead>
          <tbody>{b.table.rows.map((r, i) => <tr key={i}>{r.map((cell, j) => <td key={j} dangerouslySetInnerHTML={{ __html: cell }} />)}</tr>)}</tbody>
        </table>
      </div>
    );
  }
  return null;
}

function Reader({ sectionId, chapterN, highlight, onBack, onOpenChapter, onSearch, onAccent }) {
  const section = getSection(sectionId);
  const chapter = getChapter(sectionId, chapterN);
  const screenRef = useRefR(null);
  const scRef = useRefR(null);
  const bodyRef = useRefR(null);
  const progressRef = useRefR(null);
  const [marked, setMarked] = useStateR(false);
  const [sel, setSel] = useStateR(null); // {x,y, start,end, blockIndex, existingId, color}
  const key = chapKey(sectionId, chapterN);
  const content = (window.CORTEX_CONTENT || {})[key];
  const [mastery, setMasteryState] = useStateR(() => readMastery()[key] || null);

  useEffectR(() => {
    const el = scRef.current; if (!el) return;
    let scrollRaf = 0;
    let animationRaf = 0;
    let completed = false;
    let current = 0;
    let target = 0;
    let lastFrame = 0;
    const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

    const paint = (time) => {
      if (reduceMotion) {
        current = target;
        if (progressRef.current) progressRef.current.style.transform = `scaleX(${current})`;
        animationRaf = 0;
        return;
      }
      const elapsed = lastFrame ? Math.min(time - lastFrame, 64) : 16;
      lastFrame = time;
      // Exponential smoothing stays consistent across 60/90/120 Hz displays.
      current += (target - current) * (1 - Math.exp(-elapsed / 48));
      if (Math.abs(target - current) < 0.0001) current = target;
      if (progressRef.current) progressRef.current.style.transform = `scaleX(${current})`;
      animationRaf = current === target ? 0 : requestAnimationFrame(paint);
    };

    const update = () => {
      scrollRaf = 0;
      const max = el.scrollHeight - el.clientHeight;
      target = max > 0 ? Math.min(1, Math.max(0, el.scrollTop / max)) : 0;
      if (!animationRaf) { lastFrame = 0; animationRaf = requestAnimationFrame(paint); }

      if (!completed && target > 0.85) {
        completed = true;
        const p = readProgress();
        if ((p[key] || 0) < 1) { p[key] = 1; writeProgress(p); }
      }
    };
    // Keep the hot scroll path outside React so long articles do not re-render
    // on every frame. transform is compositor-only and avoids layout work.
    const onScroll = () => { if (!scrollRaf) scrollRaf = requestAnimationFrame(update); };
    el.addEventListener('scroll', onScroll, { passive: true });
    update();
    return () => {
      el.removeEventListener('scroll', onScroll);
      if (scrollRaf) cancelAnimationFrame(scrollRaf);
      if (animationRaf) cancelAnimationFrame(animationRaf);
    };
  }, [key]);

  useEffectR(() => {
    const screen = screenRef.current;
    const scroller = scRef.current;
    if (!screen || !scroller) return;
    let gesture = null;
    let lastTopTap = 0;

    const onTouchStart = (event) => {
      if (event.touches.length !== 1) { gesture = null; return; }
      const touch = event.touches[0];
      const rect = screen.getBoundingClientRect();
      gesture = {
        x: touch.clientX, y: touch.clientY, time: Date.now(),
        edge: touch.clientX - rect.left <= 24,
        top: touch.clientY - rect.top <= 72,
        moved: false
      };
    };
    const onTouchMove = (event) => {
      if (!gesture || event.touches.length !== 1) return;
      const touch = event.touches[0];
      const dx = touch.clientX - gesture.x;
      const dy = touch.clientY - gesture.y;
      if (Math.abs(dx) > 8 || Math.abs(dy) > 8) gesture.moved = true;
      // Once an edge gesture is clearly horizontal, keep vertical reading
      // scroll from competing with it.
      if (gesture.edge && dx > 10 && Math.abs(dx) > Math.abs(dy)) event.preventDefault();
    };
    const onTouchEnd = (event) => {
      if (!gesture || event.changedTouches.length !== 1) { gesture = null; return; }
      const touch = event.changedTouches[0];
      const dx = touch.clientX - gesture.x;
      const dy = touch.clientY - gesture.y;
      const elapsed = Date.now() - gesture.time;

      if (gesture.edge && dx >= 72 && Math.abs(dy) <= 60 && elapsed <= 700) {
        gesture = null;
        onBack();
        return;
      }

      if (gesture.top && !gesture.moved && elapsed <= 300) {
        const now = Date.now();
        if (now - lastTopTap <= 340) {
          lastTopTap = 0;
          scroller.scrollTo({ top: 0, behavior: 'smooth' });
        } else {
          lastTopTap = now;
        }
      }
      gesture = null;
    };

    screen.addEventListener('touchstart', onTouchStart, { passive: true });
    screen.addEventListener('touchmove', onTouchMove, { passive: false });
    screen.addEventListener('touchend', onTouchEnd, { passive: true });
    return () => {
      screen.removeEventListener('touchstart', onTouchStart);
      screen.removeEventListener('touchmove', onTouchMove);
      screen.removeEventListener('touchend', onTouchEnd);
    };
  }, [key, onBack]);

  // mark progress read + lastread on open
  useEffectR(() => {
    const p = readProgress(); p[key] = Math.max(p[key] || 0, 0.15); writeProgress(p);
    localStorage.setItem('cx_lastread', key);
  }, [key]);
  // highlight a pearl's source passage for 5 s when opened from Daily Pearl
  useEffectR(() => {
    if (!highlight) return;
    const el = scRef.current; if (!el) return;
    const t = setTimeout(() => {
      const needle = highlight.toLowerCase();
      const node = [...el.querySelectorAll('.rd-body p, .rd-body .callout, .rd-body li')].find(n => (n.textContent || '').toLowerCase().includes(needle));
      const target = node && (node.closest('.callout') || node);
      if (target) {
        el.scrollTo({ top: target.offsetTop - 80, behavior: 'smooth' });
        target.classList.add('rd-hl');
        setTimeout(() => target.classList.remove('rd-hl'), 5000);
      }
    }, 360);
    return () => clearTimeout(t);
  }, [key, highlight]);

  // restore saved highlights after body renders (and re-apply when store changes externally)
  useEffectR(() => {
    const body = bodyRef.current; if (!body) return;
    const blocks = [...body.children];
    body.querySelectorAll('mark.hl').forEach(m => { const p = m.parentNode; while (m.firstChild) p.insertBefore(m.firstChild, m); p.removeChild(m); });
    blocks.forEach(b => b.normalize && b.normalize());
    chapterHighlights(key).forEach(h => {
      const blk = blocks[h.blockIndex]; if (blk) wrapByOffset(blk, h.start, h.end, h.color, h.id);
    });
  }, [key, content]);

  // selection → toolbar
  const closeToolbar = () => setSel(null);
  useEffectR(() => {
    const body = bodyRef.current; if (!body) return;
    const onUp = (e) => {
      // tap on an existing highlight → edit toolbar
      const mark = e.target.closest && e.target.closest('mark.hl');
      const s = window.getSelection();
      if ((!s || s.isCollapsed || !s.toString().trim()) ) {
        if (mark) {
          const r = mark.getBoundingClientRect(); const host = scRef.current.closest('.screen').getBoundingClientRect();
          setSel({ x: r.left + r.width / 2 - host.left, y: r.top - host.top, existingId: mark.dataset.hlId, color: mark.className.match(/hl-(\w+)/)?.[1] });
        } else setSel(null);
        return;
      }
      const range = s.getRangeAt(0);
      const block = [...body.children].find(b => b.contains(range.startContainer));
      if (!block) { setSel(null); return; }
      const start = offsetWithin(block, range.startContainer, range.startOffset);
      let end = block.contains(range.endContainer) ? offsetWithin(block, range.endContainer, range.endOffset) : block.textContent.length;
      if (end <= start) { setSel(null); return; }
      const r = range.getBoundingClientRect(); const host = scRef.current.closest('.screen').getBoundingClientRect();
      setSel({ x: r.left + r.width / 2 - host.left, y: r.top - host.top, blockIndex: [...body.children].indexOf(block), start, end });
    };
    body.addEventListener('mouseup', onUp);
    body.addEventListener('touchend', onUp);
    return () => { body.removeEventListener('mouseup', onUp); body.removeEventListener('touchend', onUp); };
  }, [key, content]);

  const pickColor = (color) => {
    const body = bodyRef.current; if (!body || !sel) return;
    if (sel.existingId) {
      // recolor existing
      const h = chapterHighlights(key).find(x => x.id === sel.existingId);
      if (h) { unwrapHighlight(body, sel.existingId); const blk = body.children[h.blockIndex]; if (blk) wrapByOffset(blk, h.start, h.end, color, h.id); saveHighlight(key, { ...h, color }); }
    } else {
      const id = 'h' + Date.now().toString(36) + Math.random().toString(36).slice(2, 5);
      const blk = body.children[sel.blockIndex];
      const text = blk ? blk.textContent.slice(sel.start, sel.end) : '';
      if (blk) wrapByOffset(blk, sel.start, sel.end, color, id);
      saveHighlight(key, { id, color, blockIndex: sel.blockIndex, start: sel.start, end: sel.end, text });
    }
    if (window.getSelection) window.getSelection().removeAllRanges();
    setSel(null);
  };
  const removeHl = () => {
    const body = bodyRef.current;
    if (sel && sel.existingId) { unwrapHighlight(body, sel.existingId); deleteHighlight(key, sel.existingId); }
    setSel(null);
  };

  if (!section || !chapter) return null;
  const cColor = `c-${section.color}`;
  const subs = chapter.subs || [];
  const minutes = (content && content.minutes) || Math.max(4, Math.round(subs.length * 0.9));
  const idx = section.chapters.findIndex(c => c.n === chapterN);
  const prev = section.chapters[idx - 1];
  const next = section.chapters[idx + 1];

  const stand = content ? content.stand
    : `Strukturierte Übersicht aus dem Bereich ${section.title} mit ${subs.length} Kernpunkten — von der Einordnung bis zum klinischen Vorgehen.`;
  // hide the standfirst when it just repeats the opening paragraph
  const plainTxt = (s) => (s || '').replace(/<[^>]+>/g, '').replace(/[…\.]+\s*$/, '').replace(/\s+/g, ' ').trim().toLowerCase();
  const firstP = content && content.body.find(b => b.p);
  const standDupe = !!(content && firstP && plainTxt(stand).length > 20 && plainTxt(firstP.p).startsWith(plainTxt(stand)));

  const setMast = (v) => {
    const m = readMastery(); m[key] = v; writeMastery(m); setMasteryState(v);
    window.dispatchEvent(new CustomEvent('cx-toast', { detail: { msg: v === 'beherrscht' ? 'Als beherrscht markiert' : v === 'teilweise' ? 'Teilweise gemerkt' : 'Zum Wiederholen markiert', delta: v === 'beherrscht' ? '+1 %' : '' } }));
  };

  const jumpTo = (label) => {
    const el = scRef.current; if (!el) return;
    const target = [...el.querySelectorAll('[data-h2]')].find(h => h.dataset.h2 === label) || el.querySelector(`[data-anchor="${label}"]`);
    if (target) el.scrollTo({ top: target.offsetTop - 70, behavior: 'smooth' });
  };
  const scrollTop = () => { const el = scRef.current; if (el) el.scrollTo({ top: 0, behavior: 'smooth' }); };
  const doubleClickTop = (event) => {
    const screen = screenRef.current;
    if (!screen || event.clientY - screen.getBoundingClientRect().top > 72) return;
    if (event.target.closest('button, a, input, textarea, select')) return;
    scrollTop();
  };
  const jumps = content ? content.body.filter(b => b.h2).map(b => b.h2) : [];

  return (
    <div className={`screen ${cColor} anim-slide`} ref={screenRef} onDoubleClick={doubleClickTop}>
      <div className="rd-bar"><i ref={progressRef} /></div>
      <div className="rd-top">
        <button className="rd-pill" onClick={onBack} aria-label="Zurück"><Ic n="arrow_back" size={21} /></button>
        <div className="grp">
          <button className={`rd-pill ${marked ? 'on' : ''}`} onClick={() => setMarked(m => !m)} aria-label="Merken"><Ic n={marked ? 'bookmark' : 'bookmark_border'} size={20} /></button>
          <button className="rd-pill" aria-label="Teilen"><Ic n="share" size={19} /></button>
        </div>
      </div>
      <div className="scroll" ref={scRef}>
        <div className="reader">
          <div className="rd-hero">
            <span className="crumb">{section.title} · Kapitel {chapter.n}</span>
            <h1>{chapter.title}</h1>
            {!standDupe && <p className="rd-stand">{stand}</p>}
            <div className="rd-byline">
              <span>{minutes} Min Lesezeit</span><span className="dot" />
              <span>{subs.length} Lernkarten</span><span className="dot" />
              <span>{content ? 'Redaktionell' : 'Struktur'}</span>
            </div>
          </div>

          {jumps.length > 1 && (
            <div className="rd-jump">
              {jumps.map((j, i) => <button key={i} onClick={() => jumpTo(j)}>{j}</button>)}
            </div>
          )}

          <div className="rd-body" ref={bodyRef}>
            {content && content.body.map((b, i) => <Block key={i} b={b} />)}
            {!content && (
              <>
                <div className="rd-h2" data-h2="Übersicht">Übersicht</div>
                <p>Dieses Kapitel bündelt {subs.length} klinische Kernpunkte aus dem Bereich <b>{section.title}</b>. Die redaktionelle Volltext-Fassung mit Cave-, Merke- und Tipp-Boxen folgt — die Struktur unten zeigt alle Lernkarten des Kapitels.</p>
              </>
            )}

            {/* cross-links */}
            {content && content.related && content.related.length > 0 && (
              <>
                <div className="rd-h2">Verwandte Kapitel</div>
                {content.related.map(([rid, rn], i) => {
                  const rs = getSection(rid); const rc = getChapter(rid, rn);
                  if (!rc) return null;
                  return (
                    <button className={`xlink c-${rs.color}`} key={i} onClick={() => onOpenChapter(rid, rn)}>
                      <div className="xt"><div className="xk">{rs.title}</div><div className="xh">{rc.title}</div></div>
                      <span className="xchev"><Ic n="north_east" size={18} /></span>
                    </button>
                  );
                })}
              </>
            )}

            {/* mastery */}
            <div className="mastery">
              <div className="mh">Selbsteinschätzung</div>
              <div className="mhint">Wie sicher bist du in diesem Kapitel?</div>
              <div className="ms-grid">
                {[['beherrscht', 'Beherrscht'], ['teilweise', 'Teilweise'], ['wiederholen', 'Wiederholen']].map(([v, l]) => (
                  <button key={v} className={`ms-box ${v} ${mastery === v ? 'on' : ''}`} onClick={() => setMast(v)}>
                    <span className="msd" /><span className="msl">{l}</span>
                  </button>
                ))}
              </div>
            </div>
          </div>

          {(prev || next) && (
            <div className="rd-foot">
              {prev ? <button onClick={() => onOpenChapter(sectionId, prev.n)}><div className="fk">Zurück · Kap. {prev.n}</div><div className="fh">{prev.title}</div></button> : <button style={{ visibility: 'hidden' }} />}
              {next ? <button className="next" onClick={() => onOpenChapter(sectionId, next.n)}><div className="fk">Weiter · Kap. {next.n}</div><div className="fh">{next.title}</div></button> : <button style={{ visibility: 'hidden' }} />}
            </div>
          )}
          <div style={{ height: 96 }} />
        </div>
      </div>

      {/* selection highlight toolbar */}
      {sel && (
        <div className="hl-bar" style={{ left: Math.max(70, Math.min(sel.x, 290)), top: Math.max(64, sel.y - 54) }}>
          {HL_COLORS.map(c => (
            <button key={c} className={`hl-sw hl-${c}${sel.color === c ? ' on' : ''}`} onClick={() => pickColor(c)} aria-label={c} />
          ))}
          <span className="hl-div" />
          <button className="hl-rm" onClick={removeHl} aria-label="Entfernen"><Ic n={sel.existingId ? 'delete' : 'close'} size={17} /></button>
        </div>
      )}

      {/* floating chapter nav bar */}
      <div className="rd-navbar">
        <button className="rdn-prev" disabled={!prev} onClick={() => prev && onOpenChapter(sectionId, prev.n)} aria-label="Vorheriges Kapitel"><Ic n="chevron_left" size={22} /></button>
        <span className="rdn-mid"><b>Kap. {chapter.n}</b> / {section.chapters.length} Kapitel</span>
        {next
          ? <button className="rdn-next" onClick={() => onOpenChapter(sectionId, next.n)}>Kapitel {next.n} <Ic n="arrow_forward" size={18} /></button>
          : <button className="rdn-next done" onClick={onBack}>Fertig <Ic n="check" size={18} /></button>}
      </div>

      <div className="rd-fabs">
        {onSearch && <button className="rd-fab" onClick={onSearch} aria-label="Suchen"><Ic n="search" size={21} /></button>}
        <button className="rd-fab" onClick={scrollTop} aria-label="Nach oben"><Ic n="arrow_upward" size={21} /></button>
      </div>
    </div>
  );
}

window.Reader = Reader;
window.cxReadMastery = readMastery; window.cxWriteMastery = writeMastery;
window.cxReadProgress = readProgress; window.cxWriteProgress = writeProgress;
window.cxGetSection = getSection; window.cxGetChapter = getChapter; window.cxChapKey = chapKey;
window.cxReadHighlights = readHighlights; window.cxHlColors = HL_COLORS;
