/* global React, ReactDOM, Icons, Icon, cx */
/* ============================================================
   Content blocks — the layout standard for course pages.

   A course page is NOT a wall of text: it is an ordered list of blocks,
   each one a visual component of the design system. Authors write data
   (see course-ddhh-n1.jsx) and <ContentBlocks> renders it.

   Pages contain no written exercises and no tests: assessment lives only
   in the test at the end of each unit. Pages stay engaging through
   interactive components (tabs, timeline, myth cards, case analysis)
   and quiet motion (blocks reveal as they enter the screen).

   Inline markup inside any `text`: **bold**, *italic*, and source refs
   such as [N1], [N3, art. 25] or [A4–A6], rendered as quiet ref chips.

   Rhythm guideline for authors (≈ one screen at a time):
     · open with `lead` (one or two sentences, what this page is about)
     · never more than 2 consecutive `p` blocks — break them with a visual block
     · at least one interactive block per page (tabs, timeline, myth, case)
     · close the last page of a unit with `summary`

   Block catalogue (t = type):
     lead      { text }                         intro paragraph, larger
     p         { text }                         body paragraph
     h         { text, kicker? }                section subheading
     list      { items: [text] }                bullet list
     concepts  { items: [{ term, text }] }      definition cards
     cards     { items: [{ icon, title, text, tag? }], cols? }
     tabs      { items: [{ icon, tag, title, text }] }   interactive: one panel at a time
     figures   { items: [{ value, label }] }    big numbers (count up when numeric)
     timeline  { items: [{ year, text }], title? }       interactive: navigable rail
     steps     { title?, items: [{ title, text? }] }
     compare   { left: {label, title, text|items}, right: {...}, tone? "versus" | "goodbad" }
     table     { head: [..], rows: [[..]], title? }
     chips     { title?, items: [{ code, label }] }
     quote     { text, source }
     callout   { tone: "info" | "key" | "warn", title?, text }
     case      { title?, text, questions?: [text] }  interactive: analysis keys unfold
     myth      { items: [{ myth, fact }] }       interactive: card flips
     summary   { items: [text], sources? }
   ============================================================ */

const REF_RE = /(\*\*[^*]+\*\*|\*[^*\s][^*]*\*|\[(?:[NIA]\d+|[TIAE])[^\]]*\])/g;

/* ============================================================
   Hypertext layer
   · Source refs  [N3, art. 25]  → chip that opens the source card (title, type, official text ↗)
   · Glossary     first mention of a course term on each page → definition card
   · Cross-refs   «página 2.3», «páginas 3.1 y 3.2», «unidad 5» → links inside the course
   Links need the reader context (CourseCtx); elsewhere Rich renders plain text.
   ============================================================ */
const CourseCtx = React.createContext(null); // { course, itemId, unitN, ids: Set }
const GlossCtx = React.createContext(null);  // glossary entries this block may link
const PlainCtx = React.createContext(false); // inside interactive controls: no nested links or buttons

const SOURCE_TYPES = {
  N: { code: "T", label: "Norma · texto de tratado u otra norma" },
  I: { code: "I", label: "Interpretación de un órgano competente" },
  A: { code: "A", label: "Aportación académica" },
};
const sourceOf = code => {
  const r = (window.COURSE_REFS || {})[code];
  return typeof r === "string" ? { t: r } : r || null;
};
// "A4–A6" → [A4, A5, A6] · "N20, art. 35; N22" → [N20, N22] · "N1, arts. 1–2" → [N1]
function expandCodes(inner) {
  const out = [];
  const re = /([NIA])(\d+)(?:\s*[–-]\s*([NIA])?(\d+))?/g;
  let m;
  while ((m = re.exec(inner))) {
    const from = +m[2], to = m[4] ? +m[4] : from;
    for (let n = from; n <= Math.max(from, to); n++) out.push(m[1] + n);
  }
  return [...new Set(out)];
}
function collectCodes(blocks) {
  const all = [];
  JSON.stringify(blocks).replace(/\[((?:[NIA]\d+)[^\]]*)\]/g, (_, inner) => { all.push(...expandCodes(inner)); return ""; });
  const order = { N: 0, I: 1, A: 2 };
  return [...new Set(all)].sort((a, b) => order[a[0]] - order[b[0]] || +a.slice(1) - +b.slice(1));
}

/* ---------- Popover (portal, fixed, keyboard and pointer friendly) ---------- */
const Popover = ({ trigger, className, label, children }) => {
  const [open, setOpen] = React.useState(false);
  const [pos, setPos] = React.useState(null);
  const btn = React.useRef(null);
  const pop = React.useRef(null);
  const timer = React.useRef(0);
  const place = () => {
    const r = btn.current.getBoundingClientRect();
    const w = Math.min(340, window.innerWidth - 24);
    const left = Math.max(12, Math.min(r.left + r.width / 2 - w / 2, window.innerWidth - w - 12));
    const below = window.innerHeight - r.bottom > 260;
    setPos({ left, width: w, top: below ? r.bottom + 10 : null, bottom: below ? null : window.innerHeight - r.top + 10, arrow: r.left + r.width / 2 - left });
  };
  const show = () => { clearTimeout(timer.current); place(); setOpen(true); };
  const hide = (delay = 160) => { clearTimeout(timer.current); timer.current = setTimeout(() => setOpen(false), delay); };
  React.useEffect(() => {
    if (!open) return;
    const onKey = e => { if (e.key === "Escape") { setOpen(false); btn.current?.focus(); } };
    const onDown = e => { if (!pop.current?.contains(e.target) && !btn.current?.contains(e.target)) setOpen(false); };
    const onScroll = () => setOpen(false);
    document.addEventListener("keydown", onKey);
    document.addEventListener("mousedown", onDown);
    window.addEventListener("scroll", onScroll, { passive: true });
    return () => { document.removeEventListener("keydown", onKey); document.removeEventListener("mousedown", onDown); window.removeEventListener("scroll", onScroll); };
  }, [open]);
  React.useEffect(() => () => clearTimeout(timer.current), []);
  const hoverable = () => window.matchMedia?.("(hover: hover)").matches;
  return (
    <>
      <button ref={btn} type="button" className={className} aria-expanded={open} aria-label={label}
              onClick={e => { e.stopPropagation(); open ? setOpen(false) : show(); }}
              onMouseEnter={() => { if (hoverable()) { clearTimeout(timer.current); timer.current = setTimeout(show, 280); } }}
              onMouseLeave={() => hoverable() && hide()}>
        {trigger}
      </button>
      {open && pos && ReactDOM.createPortal(
        <div ref={pop} className="cb-pop" role="dialog" aria-label={label} data-side={pos.top != null ? "below" : "above"}
             style={{ left: pos.left, width: pos.width, top: pos.top ?? "auto", bottom: pos.bottom ?? "auto", "--arrow": `${pos.arrow}px` }}
             onMouseEnter={() => clearTimeout(timer.current)} onMouseLeave={() => hoverable() && hide()}>
          {children}
        </div>,
        document.body
      )}
    </>
  );
};

const RefChip = ({ inner }) => {
  const codes = expandCodes(inner);
  const detail = inner.replace(/^[NIA]\d+(\s*[–-]\s*[NIA]?\d+)?\s*,?\s*/, "");
  return (
    <Popover className="cb-ref" label={`Fuente ${inner}`} trigger={inner}>
      <div className="cb-pop-kicker"><Icons.Book size={13} /> Fuente citada</div>
      <div className="cb-pop-sources">
        {codes.map(c => {
          const s = sourceOf(c);
          return (
            <div key={c} className="cb-pop-source">
              <span className="cb-pop-code" data-type={c[0]}>{c}</span>
              <div>
                <div className="cb-pop-title">{s?.t || "Referencia de la bibliografía del curso"}</div>
                <div className="cb-pop-type">{SOURCE_TYPES[c[0]].label}{detail && codes.length === 1 ? ` · ${detail}` : ""}</div>
                {s?.url && <a className="cb-pop-link" href={s.url} target="_blank" rel="noopener noreferrer">Leer el texto oficial <Icons.ArrowUpRight size={13} /></a>}
              </div>
            </div>
          );
        })}
      </div>
    </Popover>
  );
};

const GlossTerm = ({ entry, text }) => {
  const ctx = React.useContext(CourseCtx);
  const page = entry.page && ctx?.ids.has(entry.page) && entry.page !== ctx.itemId ? entry.page : null;
  return (
    <Popover className="cb-term" label={`Glosario: ${entry.term}`} trigger={text}>
      <div className="cb-pop-kicker"><Icons.Layers size={13} /> Glosario</div>
      <div className="cb-pop-term">{entry.term}</div>
      <p className="cb-pop-def">{entry.def}</p>
      <div className="cb-pop-actions">
        {page && <a className="cb-pop-link" href={`#/curso/${ctx.course.id}/leccion/${page}`}>Se explica en la página {page.replace("-", ".")} <Icons.ArrowRight size={13} /></a>}
        <button type="button" className="cb-pop-ghost" onClick={() => window.dispatchEvent(new CustomEvent("bh-glossary", { detail: entry.term }))}>Abrir glosario</button>
      </div>
    </Popover>
  );
};

const XREF_RE = /(p[áa]ginas?\s+)(\d\.\d)(?:(\s+y\s+)(\d\.\d))?|(unidad\s+)(\d)(?!\d|\.\d|\s*·\s*p[áa]gina)/i;
const XRef = ({ m, ctx }) => {
  const href = id => `#/curso/${ctx.course.id}/leccion/${id}`;
  if (m[5]) {
    const id = `${m[6]}-1`;
    return ctx.ids.has(id) && +m[6] !== ctx.unitN ? <a className="cb-xref" href={href(id)}>{m[5]}{m[6]}</a> : <>{m[5]}{m[6]}</>;
  }
  const L = (n, children) => {
    const id = n.replace(".", "-");
    return ctx.ids.has(id) && id !== ctx.itemId ? <a className="cb-xref" href={href(id)}>{children}</a> : <>{children}</>;
  };
  return m[4] ? <>{m[1]}{L(m[2], m[2])}{m[3]}{L(m[4], m[4])}</> : L(m[2], <>{m[1]}{m[2]}</>);
};

// `gloss` = { terms, claims }: each term links once per block, in the first text that claims it.
function linkify(s, ctx, gloss, used, whole) {
  if (!ctx) return s;
  const out = [];
  let rest = s, k = 0;
  while (rest) {
    let best = null;
    const x = XREF_RE.exec(rest);
    if (x) best = { idx: x.index, len: x[0].length, x };
    for (const g of gloss?.terms || []) {
      if (used.has(g.term)) continue;
      const claim = gloss.claims[g.term];
      if (claim !== undefined && claim !== whole) continue;
      const m = g.re.exec(rest);
      if (m && (!best || m.index < best.idx)) best = { idx: m.index, len: m[0].length, g, text: m[0] };
    }
    if (!best) { out.push(rest); break; }
    if (best.idx) out.push(rest.slice(0, best.idx));
    if (best.g) { used.add(best.g.term); gloss.claims[best.g.term] = whole; out.push(<GlossTerm key={k++} entry={best.g} text={best.text} />); }
    else out.push(<XRef key={k++} m={best.x} ctx={ctx} />);
    rest = rest.slice(best.idx + best.len);
  }
  return out;
}

function Rich({ text }) {
  const ctx = React.useContext(CourseCtx);
  const gloss = React.useContext(GlossCtx);
  const plain = React.useContext(PlainCtx);
  if (text == null) return null;
  const used = new Set();
  const whole = String(text);
  const link = s => (plain ? s : linkify(s, ctx, gloss, used, whole));
  return String(text).split(REF_RE).map((part, i) => {
    if (!part) return null;
    if (part.startsWith("**") && part.endsWith("**")) return <strong key={i}>{link(part.slice(2, -2))}</strong>;
    if (part.startsWith("[") && part.endsWith("]")) return plain ? <span key={i} className="cb-ref cb-ref--static">{part.slice(1, -1)}</span> : <RefChip key={i} inner={part.slice(1, -1)} />;
    if (part.length > 2 && part.startsWith("*") && part.endsWith("*")) return <em key={i}>{link(part.slice(1, -1))}</em>;
    return <React.Fragment key={i}>{link(part)}</React.Fragment>;
  });
}

/* ---------- Glossary ---------- */
const glossCache = new WeakMap();
function compileGlossary(course) {
  if (!course?.glossary) return [];
  if (glossCache.has(course)) return glossCache.get(course);
  const esc = s => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
  const list = course.glossary.map(g => {
    const aliases = [g.term, ...(g.aliases || [])].sort((a, b) => b.length - a.length).map(esc).join("|");
    return { ...g, re: new RegExp(`(?<![\\p{L}\\p{N}])(?:${aliases})(?![\\p{L}\\p{N}])`, "iu") };
  });
  glossCache.set(course, list);
  return list;
}
// Each term links once per page: on the first linkable block that mentions it.
const PLAIN_KEYS = ["t", "icon", "tag", "label", "tone", "kind", "id", "title", "term", "code", "year"];
const LINKABLE = new Set(["lead", "p", "list", "concepts", "cards", "tabs", "steps", "compare", "table", "callout", "case", "timeline", "summary"]);
function assignGlossary(blocks, course) {
  const gl = compileGlossary(course);
  const taken = new Set();
  return blocks.map(b => {
    if (!LINKABLE.has(b.t)) return null;
    // Only fields rendered as rich text count (titles, terms and tab labels are plain).
    const plainKeys = b.t === "steps" ? PLAIN_KEYS.filter(k => k !== "title") : PLAIN_KEYS;
    const hay = JSON.stringify(b).replace(new RegExp(`"(${plainKeys.join("|")})":"[^"]*"`, "g"), "");
    const mine = gl.filter(g => !taken.has(g.term) && g.re.test(hay));
    mine.forEach(g => taken.add(g.term));
    return mine.length ? { terms: mine, claims: {} } : null;
  });
}

/* ---------- Motion ---------- */
const prefersReducedMotion = () => {
  try { return window.matchMedia("(prefers-reduced-motion: reduce)").matches; } catch { return true; }
};
const canObserve = () => typeof window !== "undefined" && "IntersectionObserver" in window && !prefersReducedMotion();

// Returns [ref, visible]: visible flips to true once the element enters the viewport.
function useInView(threshold = 0.15) {
  const ref = React.useRef(null);
  const [visible, setVisible] = React.useState(() => !canObserve());
  React.useEffect(() => {
    if (visible || !ref.current) return;
    const io = new IntersectionObserver(entries => {
      if (entries.some(e => e.isIntersecting)) { setVisible(true); io.disconnect(); }
    }, { threshold, rootMargin: "0px 0px -6% 0px" });
    io.observe(ref.current);
    return () => io.disconnect();
  }, [visible]);
  return [ref, visible];
}

const Reveal = ({ type, children }) => {
  const [ref, visible] = useInView();
  return <div ref={ref} className={`cb-item cb-item--${type}`} data-visible={visible}>{children}</div>;
};

// Counts from 0 up to the number in `value` ("70 %", "4 meses", "85"). Years and ranges stay static.
const CountUp = ({ value, run }) => {
  const m = /^(\d{1,3})(\s?[^\d–·-].*)?$/.exec(value);
  const target = m ? parseInt(m[1], 10) : null;
  const [n, setN] = React.useState(target == null || !canObserve() ? target : 0);
  React.useEffect(() => {
    if (target == null || !run || !canObserve()) { setN(target); return; }
    let raf, start;
    const step = t => {
      if (!start) start = t;
      const p = Math.min(1, (t - start) / 900);
      setN(Math.round(target * (1 - Math.pow(1 - p, 3))));
      if (p < 1) raf = requestAnimationFrame(step);
    };
    raf = requestAnimationFrame(step);
    return () => cancelAnimationFrame(raf);
  }, [run, target]);
  if (target == null) return value;
  return <>{n}{m[2] || ""}</>;
};

const BLOCK_ICONS = {
  scale: (p) => <Icon {...p}><path d="M12 3v18M5 21h14"/><path d="M5 7h14"/><path d="m5 7-3 7a3 3 0 0 0 6 0L5 7Z"/><path d="m19 7-3 7a3 3 0 0 0 6 0l-3-7Z"/></Icon>,
  heart: (p) => <Icon {...p}><path d="M20.8 4.6a5.5 5.5 0 0 0-7.8 0L12 5.7l-1-1.1a5.5 5.5 0 0 0-7.8 7.8L12 21.2l8.8-8.8a5.5 5.5 0 0 0 0-7.8Z"/></Icon>,
  landmark: (p) => <Icon {...p}><path d="M3 21h18M5 21V10M9 21V10M15 21V10M19 21V10M2 10 12 3l10 7Z"/></Icon>,
  hand: (p) => <Icon {...p}><path d="M18 11V6a2 2 0 0 0-4 0v5M14 10V4a2 2 0 0 0-4 0v6M10 10.5V6a2 2 0 0 0-4 0v8"/><path d="M18 8a2 2 0 1 1 4 0v6a8 8 0 0 1-8 8h-2c-2.8 0-4.5-.9-5.9-2.3l-3.6-3.6a2 2 0 0 1 2.8-2.8L7 15"/></Icon>,
  shield: (p) => <Icons.Shield {...p} />,
  users: (p) => <Icons.Users {...p} />,
  book: (p) => <Icons.Book {...p} />,
  doc: (p) => <Icons.Doc {...p} />,
  globe: (p) => <Icons.Globe {...p} />,
  flag: (p) => <Icons.Flag {...p} />,
  layers: (p) => <Icons.Layers {...p} />,
  search: (p) => <Icons.Search {...p} />,
  eye: (p) => <Icons.Eye {...p} />,
  chat: (p) => <Icons.MessageCircle {...p} />,
  check: (p) => <Icons.CheckCircle {...p} />,
  trend: (p) => <Icons.Trending {...p} />,
  lock: (p) => <Icons.Lock {...p} />,
  award: (p) => <Icons.Award {...p} />,
  spark: (p) => <Icons.Sparkles {...p} />,
};
const BlockIcon = ({ name, size = 20 }) => {
  const I = BLOCK_ICONS[name] || BLOCK_ICONS.doc;
  return <I size={size} />;
};

/* ---------- Text blocks ---------- */
const CbLead = ({ text }) => <p className="cb-lead"><Rich text={text} /></p>;
const CbP = ({ text }) => <p className="cb-p"><Rich text={text} /></p>;
const CbH = ({ text, kicker }) => (
  <div className="cb-h">
    {kicker && <span className="cb-kicker">{kicker}</span>}
    <h3><Rich text={text} /></h3>
  </div>
);
const CbList = ({ items }) => (
  <ul className="cb-list">{items.map((it, i) => <li key={i}><Rich text={it} /></li>)}</ul>
);
const BlockTitle = ({ title }) => title ? <div className="cb-block-title"><Rich text={title} /></div> : null;

/* ---------- Visual blocks ---------- */
const CbConcepts = ({ items }) => (
  <div className={cx("cb-concepts", items.length === 1 && "cb-concepts--single")}>
    {items.map((c, i) => (
      <div key={i} className="cb-concept cb-stagger" style={{ "--i": i }}>
        <div className="cb-concept-term">{c.term}</div>
        <div className="cb-concept-text"><Rich text={c.text} /></div>
      </div>
    ))}
  </div>
);

const CbCards = ({ items, cols }) => {
  const n = cols || Math.min(items.length, 3);
  return (
    <div className={cx("cb-cards", n === 4 && "cb-cards--compact")} style={{ "--cb-cols": n }}>
      {items.map((c, i) => (
        <div key={i} className="cb-card cb-stagger" style={{ "--i": i }}>
          <div className="cb-card-top">
            <span className="cb-card-icon"><BlockIcon name={c.icon} /></span>
            {c.tag && <span className="cb-card-tag">{c.tag}</span>}
          </div>
          <div className="cb-card-title">{c.title}</div>
          <div className="cb-card-text"><Rich text={c.text} /></div>
        </div>
      ))}
    </div>
  );
};

// Interactive: segmented tabs, one panel visible; arrow keys move between tabs.
const CbTabs = ({ items }) => {
  const [active, setActive] = React.useState(0);
  const ids = React.useMemo(() => Math.random().toString(36).slice(2, 8), []);
  const listRef = React.useRef(null);
  const onKey = e => {
    if (e.key !== "ArrowRight" && e.key !== "ArrowLeft") return;
    e.preventDefault();
    const next = (active + (e.key === "ArrowRight" ? 1 : -1) + items.length) % items.length;
    setActive(next);
    listRef.current?.querySelectorAll("[role=tab]")[next]?.focus();
  };
  const it = items[active];
  return (
    <div className="cb-tabs">
      <div className="cb-tabs-list" role="tablist" ref={listRef} onKeyDown={onKey} style={{ "--n": items.length, "--a": active }}>
        <span className="cb-tabs-indicator" aria-hidden="true" />
        {items.map((t, i) => (
          <button key={i} type="button" role="tab" id={`tab-${ids}-${i}`} aria-controls={`panel-${ids}`}
                  aria-selected={i === active} tabIndex={i === active ? 0 : -1}
                  className="cb-tabs-tab" onClick={() => setActive(i)}>
            <BlockIcon name={t.icon} size={16} />
            <span>{t.label || t.tag || t.title}</span>
          </button>
        ))}
      </div>
      <div className="cb-tabs-panel" role="tabpanel" id={`panel-${ids}`} aria-labelledby={`tab-${ids}-${active}`} key={active}>
        <div className="cb-tabs-panel-icon"><BlockIcon name={it.icon} size={26} /></div>
        <div>
          <div className="cb-tabs-panel-kicker">
            {it.tag && it.label && <span>{it.tag}</span>}
            <span className="cb-tabs-panel-count">{active + 1} / {items.length}</span>
          </div>
          <div className="cb-tabs-panel-title">{it.title}</div>
          <div className="cb-tabs-panel-text"><Rich text={it.text} /></div>
        </div>
      </div>
    </div>
  );
};

const CbFigures = ({ items }) => {
  const [ref, visible] = useInView(0.4);
  return (
    <div ref={ref} className="cb-figures" style={{ "--cb-cols": Math.min(items.length, 4) }}>
      {items.map((f, i) => (
        <div key={i} className="cb-figure cb-stagger" style={{ "--i": i }}>
          <div className="cb-figure-value"><CountUp value={f.value} run={visible} /></div>
          <div className="cb-figure-label"><Rich text={f.label} /></div>
        </div>
      ))}
    </div>
  );
};

// Interactive: a rail of years; the selected milestone opens below. Arrows step through.
const CbTimeline = ({ items, title }) => {
  const [active, setActive] = React.useState(0);
  const railRef = React.useRef(null);
  React.useEffect(() => {
    const el = railRef.current?.querySelectorAll(".cb-timeline-stop")[active];
    if (el && railRef.current.scrollWidth > railRef.current.clientWidth) {
      railRef.current.scrollTo({ left: el.offsetLeft - railRef.current.clientWidth / 2 + el.clientWidth / 2, behavior: "smooth" });
    }
  }, [active]);
  const go = d => setActive(a => Math.min(items.length - 1, Math.max(0, a + d)));
  const pct = items.length > 1 ? (active / (items.length - 1)) * 100 : 0;
  return (
    <div className="cb-timeline">
      <div className="cb-timeline-head">
        <BlockTitle title={title} />
        <div className="cb-timeline-nav">
          <button type="button" onClick={() => go(-1)} disabled={active === 0} aria-label="Hito anterior"><Icons.ChevronLeft size={16} /></button>
          <button type="button" onClick={() => go(1)} disabled={active === items.length - 1} aria-label="Hito siguiente"><Icons.ChevronRight size={16} /></button>
        </div>
      </div>
      <div className="cb-timeline-track" style={{ "--n": items.length }}>
        <div className="cb-timeline-rail" ref={railRef}>
          <div className="cb-timeline-line" aria-hidden="true"><span style={{ width: `${pct}%` }} /></div>
          {items.map((it, i) => (
            <button key={i} type="button" className="cb-timeline-stop" data-state={i === active ? "active" : i < active ? "past" : "next"}
                    onClick={() => setActive(i)} aria-pressed={i === active}>
              <span className="cb-timeline-dot" />
              <span className="cb-timeline-year">{it.year}</span>
            </button>
          ))}
        </div>
      </div>
      <div className="cb-timeline-panel" key={active}>
        <span className="cb-timeline-panel-year">{items[active].year}</span>
        <span className="cb-timeline-panel-text"><Rich text={items[active].text} /></span>
      </div>
    </div>
  );
};

const CbSteps = ({ title, items }) => (
  <div>
    <BlockTitle title={title} />
    <ol className="cb-steps">
      {items.map((s, i) => (
        <li key={i} className="cb-stagger" style={{ "--i": i }}>
          <span className="cb-step-num">{i + 1}</span>
          <div>
            <div className="cb-step-title"><Rich text={s.title} /></div>
            {s.text && <div className="cb-step-text"><Rich text={s.text} /></div>}
          </div>
        </li>
      ))}
    </ol>
  </div>
);

const CompareSide = ({ side, mark, i }) => (
  <div className={cx("cb-compare-side", "cb-stagger", mark && `cb-compare-side--${mark}`)} style={{ "--i": i }}>
    <div className="cb-compare-label">
      {mark === "good" && <span className="cb-compare-mark"><Icons.Check size={12} stroke={3} /></span>}
      {mark === "bad" && <span className="cb-compare-mark"><Icons.X size={12} stroke={3} /></span>}
      {side.label}
    </div>
    {side.title && <div className="cb-compare-title">{side.title}</div>}
    {side.text && <div className="cb-compare-text"><Rich text={side.text} /></div>}
    {side.items && <ul className="cb-list cb-list--tight">{side.items.map((it, j) => <li key={j}><Rich text={it} /></li>)}</ul>}
  </div>
);
const CbCompare = ({ left, right, tone = "versus" }) => (
  <div className={cx("cb-compare", `cb-compare--${tone}`)}>
    <CompareSide side={left} mark={tone === "goodbad" ? "bad" : null} i={0} />
    {tone === "versus" && <div className="cb-compare-vs" aria-hidden="true"><span>vs</span></div>}
    <CompareSide side={right} mark={tone === "goodbad" ? "good" : null} i={2} />
  </div>
);

const CbTable = ({ head, rows, title }) => (
  <div>
    <BlockTitle title={title} />
    <div className="cb-table-scroll">
      <table className="cb-table">
        <thead><tr>{head.map((h, i) => <th key={i}>{h}</th>)}</tr></thead>
        <tbody>{rows.map((r, i) => <tr key={i}>{r.map((c, j) => <td key={j}><Rich text={c} /></td>)}</tr>)}</tbody>
      </table>
    </div>
  </div>
);

const CbChips = ({ title, items }) => (
  <div>
    <BlockTitle title={title} />
    <div className="cb-chips">
      {items.map((c, i) => (
        <div key={i} className="cb-chip cb-stagger" style={{ "--i": i }}>
          <span className="cb-chip-code">{c.code}</span>
          <span className="cb-chip-label">{c.label}</span>
        </div>
      ))}
    </div>
  </div>
);

const CbQuote = ({ text, source }) => (
  <figure className="cb-quote">
    <span className="cb-quote-mark" aria-hidden="true">“</span>
    <blockquote><Rich text={text} /></blockquote>
    {source && <figcaption><Rich text={source} /></figcaption>}
  </figure>
);

const CALLOUT = {
  info: { icon: <Icons.Doc size={18} />, label: "Para tener en cuenta" },
  key: { icon: <Icons.Sparkles size={18} />, label: "Idea clave" },
  warn: { icon: <Icons.Flag size={18} />, label: "Error frecuente" },
};
const CbCallout = ({ tone = "info", title, text }) => (
  <aside className={cx("cb-callout", `cb-callout--${tone}`)}>
    <span className="cb-callout-icon">{CALLOUT[tone].icon}</span>
    <div>
      <div className="cb-callout-title">{title || CALLOUT[tone].label}</div>
      <div className="cb-callout-text"><Rich text={text} /></div>
    </div>
  </aside>
);

// Interactive: the analysis keys unfold on demand, so the student thinks first.
const CbCase = ({ title, text, questions }) => {
  const [open, setOpen] = React.useState(false);
  return (
    <section className="cb-case" data-open={open}>
      <div className="cb-case-head">
        <span className="cb-case-tag">E</span>
        <span className="cb-case-label">Caso práctico</span>
        {title && <span className="cb-case-title">{title}</span>}
      </div>
      <p className="cb-case-text"><Rich text={text} /></p>
      {questions && (
        <>
          <button type="button" className="cb-case-toggle" onClick={() => setOpen(o => !o)} aria-expanded={open}>
            <span>{open ? "Ocultar las claves" : "Ver las claves del análisis"}</span>
            <Icons.Chevron size={16} />
          </button>
          <div className="cb-case-keys" aria-hidden={!open}>
            <div>
              <ul className="cb-case-questions">
                {questions.map((q, i) => <li key={i} style={{ "--i": i }}><Rich text={q} /></li>)}
              </ul>
            </div>
          </div>
        </>
      )}
    </section>
  );
};

// Interactive: a common misconception on the front; the card flips to the accurate reading.
const MythCard = ({ myth, fact, i }) => {
  const [open, setOpen] = React.useState(false);
  return (
    <button type="button" className="cb-myth cb-stagger" style={{ "--i": i }} data-open={open}
            onClick={() => setOpen(o => !o)} aria-pressed={open}
            aria-label={open ? "Volver a la idea frecuente" : "Ver la lectura correcta"}>
      <span className="cb-myth-inner">
        <span className="cb-myth-face cb-myth-front" aria-hidden={open}>
          <span className="cb-myth-label">Idea frecuente</span>
          <span className="cb-myth-text">«{myth}»</span>
          <span className="cb-myth-hint"><Icons.Eye size={14} /> Descubre la lectura correcta</span>
        </span>
        <span className="cb-myth-face cb-myth-back" aria-hidden={!open}>
          <span className="cb-myth-label"><Icons.Check size={13} stroke={3} /> Lectura correcta</span>
          <span className="cb-myth-fact"><Rich text={fact} /></span>
          <span className="cb-myth-hint">Volver</span>
        </span>
      </span>
    </button>
  );
};
const CbMyth = ({ items }) => (
  <div className="cb-myths" style={{ "--cb-cols": Math.min(items.length, 2) }}>
    <PlainCtx.Provider value={true}>
      {items.map((m, i) => <MythCard key={i} i={i} {...m} />)}
    </PlainCtx.Provider>
  </div>
);

const CbSummary = ({ items, sources }) => (
  <section className="cb-summary">
    <div className="cb-summary-head"><Icons.Layers size={16} /> Síntesis de la unidad</div>
    <ul>{items.map((it, i) => <li key={i} className="cb-stagger" style={{ "--i": i }}><span className="cb-summary-check"><Icons.Check size={12} stroke={3} /></span><span><Rich text={it} /></span></li>)}</ul>
    {sources && <div className="cb-summary-sources"><span>Fuentes esenciales</span> <Rich text={sources} /></div>}
  </section>
);

const BLOCKS = {
  lead: CbLead, p: CbP, h: CbH, list: CbList,
  concepts: CbConcepts, cards: CbCards, tabs: CbTabs, figures: CbFigures,
  timeline: CbTimeline, steps: CbSteps, compare: CbCompare, table: CbTable, chips: CbChips,
  quote: CbQuote, callout: CbCallout, case: CbCase, myth: CbMyth, summary: CbSummary,
};

const ContentBlocks = ({ blocks }) => {
  const ctx = React.useContext(CourseCtx);
  const gloss = React.useMemo(() => assignGlossary(blocks, ctx?.course), [blocks, ctx?.course]);
  return (
    <div className="cb-flow">
      {blocks.map((b, i) => {
        const C = BLOCKS[b.t];
        if (!C) return null;
        return (
          <GlossCtx.Provider key={i} value={gloss[i]}>
            <Reveal type={b.t}><C {...b} /></Reveal>
          </GlossCtx.Provider>
        );
      })}
    </div>
  );
};

/* Primary sources cited on a page, gathered automatically from its refs. */
const PageSources = ({ blocks }) => {
  const codes = collectCodes(blocks);
  const [ref, visible] = useInView();
  if (!codes.length) return null;
  return (
    <section ref={ref} className="cb-sources cb-item" data-visible={visible}>
      <div className="cb-sources-head">
        <div>
          <div className="cb-kicker">Para profundizar</div>
          <h3>Fuentes citadas en esta página</h3>
        </div>
        <span className="cb-sources-count">{codes.length}</span>
      </div>
      <ul>
        {codes.map((c, i) => {
          const s = sourceOf(c);
          const body = (
            <>
              <span className="cb-pop-code" data-type={c[0]}>{c}</span>
              <span className="cb-sources-copy">
                <span className="cb-sources-title">{s?.t || "Referencia de la bibliografía del curso"}</span>
                <span className="cb-sources-type">{SOURCE_TYPES[c[0]].label}</span>
              </span>
              {s?.url && <span className="cb-sources-go" aria-hidden="true"><Icons.ArrowUpRight size={16} /></span>}
            </>
          );
          return (
            <li key={c} className="cb-stagger" style={{ "--i": Math.min(i, 8) }}>
              {s?.url
                ? <a className="cb-sources-item" href={s.url} target="_blank" rel="noopener noreferrer" title="Abrir el texto oficial en una pestaña nueva">{body}</a>
                : <div className="cb-sources-item">{body}</div>}
            </li>
          );
        })}
      </ul>
    </section>
  );
};

Object.assign(window, { ContentBlocks, PageSources, Rich, BlockIcon, useInView, CountUp, CourseCtx, compileGlossary, sourceOf });
