/* ============================================================
   Article environment — full-page reader + inline editor.
   Six regions: (1) title+desc, (2) linked/alternative media,
   (3) body, (4) sources, (5) next article, (6) side metadata
   (date, read time, rating, tags, AI-transparency, feedback).
   Plus a multi-sensory media panel (video / voice / AI voice / music).
   ============================================================ */
const { useState: uSa2, useEffect: uEa2, useRef: uRa2, useMemo: uMa2 } = React;

/* ---------- AI transparency badge ---------- */
const AI_LABEL = {
  generated:  { label: "AI-created", note: "Created with substantial AI assistance, reviewed by the author." },
  augmented:  { label: "AI-augmented", note: "Written by the author, with AI used for drafting or editing." },
  translated: { label: "AI-translated", note: "Originally written in another language, translated with AI." },
};
function AiBadge({ mode, compact }) {
  if (!mode || !AI_LABEL[mode]) return null;
  const a = AI_LABEL[mode];
  return (
    <span className={"ai-badge" + (compact ? " compact" : "")} title={a.note}>
      <span className="ai-spark" aria-hidden="true">✦</span>
      {a.label}
    </span>
  );
}

/* ---------- simulated media player (voice / music) ---------- */
function MiniPlayer({ kind, label, onClose }) {
  const [playing, setPlaying] = uSa2(true);
  const [t, setT] = uSa2(0);
  const dur = kind === "music" ? 184 : 326;
  uEa2(() => {
    if (!playing) return;
    const id = setInterval(() => setT(x => (x + 1 >= dur ? (clearInterval(id), dur) : x + 1)), 1000);
    return () => clearInterval(id);
  }, [playing]);
  const fmt = (s) => `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`;
  const bars = uMa2(() => Array.from({ length: 56 }, () => 0.25 + Math.random() * 0.75), []);
  return (
    <div className="mini-player">
      <button className="mp-play" onClick={() => setPlaying(p => !p)}><Icon name={playing ? "close" : "play"} size={playing ? 16 : 20} /></button>
      <div className="mp-main">
        <div className="row between" style={{ marginBottom: 5 }}>
          <span className="mp-title">{kind === "music" ? "♪ " : ""}{label}</span>
          <span className="mp-time mono">{fmt(t)} / {fmt(dur)}</span>
        </div>
        <div className="mp-wave">
          {bars.map((h, i) => <span key={i} style={{ height: `${h * 100}%`, opacity: i / bars.length <= t / dur ? 1 : .28 }} />)}
        </div>
      </div>
      <span className="mp-demo">demo</span>
      <button className="iconbtn" style={{ flex: "none" }} onClick={onClose}><Icon name="close" size={15} /></button>
    </div>
  );
}

/* ---------- AI read-aloud (real Web Speech API) ---------- */
function useReadAloud() {
  const [speaking, setSpeaking] = uSa2(false);
  const supported = typeof window !== "undefined" && "speechSynthesis" in window;
  const speak = (text) => {
    if (!supported) return;
    window.speechSynthesis.cancel();
    const u = new SpeechSynthesisUtterance(text.slice(0, 4500));
    u.rate = 1; u.pitch = 1;
    u.onend = () => setSpeaking(false);
    u.onerror = () => setSpeaking(false);
    window.speechSynthesis.speak(u);
    setSpeaking(true);
  };
  const stop = () => { if (supported) window.speechSynthesis.cancel(); setSpeaking(false); };
  uEa2(() => () => { if (supported) window.speechSynthesis.cancel(); }, []);
  return { supported, speaking, speak, stop };
}

/* ---------- multi-sensory media panel ---------- */
const ALT_META = {
  video:   { icon: "play",    label: "Watch the video" },
  read:    { icon: "edit",    label: "Read the article" },
  podcast: { icon: "msg",     label: "Listen to the podcast" },
  page:    { icon: "compass", label: "Open" },
  voice:   { icon: "msg",     label: "Listen — my voice" },
  aivoice: { icon: "bell",    label: "AI read-aloud" },
  music:   { icon: "compass", label: "Reading soundtrack" },
};
function AltMediaPanel({ item, bodyText }) {
  const { db } = useStore();
  const { go } = useNav();
  const alt = item.alt || [];
  const [player, setPlayer] = uSa2(null);   // { kind, label, src }
  const read = useReadAloud();
  if (alt.length === 0) return null;

  const NAV_KINDS = ["video", "read", "podcast", "page"];
  const altSrc = (a) => { if (a.url) return a.url; const m = window.assetById ? window.assetById(db, a.assetId) : null; return m ? (window.assetSrc ? window.assetSrc(m) : m.url) : ""; };
  const targetOf = (a) => a.kind === "page" ? (db.otherPages || []).find(p => p.id === a.ref) : db.content.find(c => c.ref === a.ref);
  const refRoute = (a) => {
    if (a.kind === "page") { const p = (db.otherPages || []).find(x => x.id === a.ref); return p ? p.route : null; }
    const target = db.content.find(c => c.ref === a.ref);
    if (!target) return null;
    const sec = db.sections.find(s => s.kind === target.type);
    return sec ? `${sec.route}/${target.ref}` : null;
  };
  const onChip = (a) => {
    if (NAV_KINDS.includes(a.kind)) { const r = refRoute(a); if (r) go(r); return; }
    if (a.kind === "aivoice") { read.speaking ? read.stop() : read.speak(bodyText); return; }
    setPlayer(p => (p && p.kind === a.kind) ? null : { kind: a.kind, label: a.label || ALT_META[a.kind].label, src: altSrc(a) });
  };

  return (
    <div className="altmedia">
      <div className="am-head">
        <span className="am-kicker"><Icon name="globe" size={13} /> Experience this your way</span>
        <span className="dim mono am-sub">same story · different senses</span>
      </div>
      <div className="am-chips">
        {alt.map((a, i) => {
          const m = ALT_META[a.kind] || { icon: "arrow", label: a.label };
          const tgt = NAV_KINDS.includes(a.kind) ? targetOf(a) : null;
          const label = a.label || (tgt && (tgt.title || tgt.label)) || m.label;
          const active = (a.kind === "aivoice" && read.speaking) || (player && player.kind === a.kind);
          return (
            <button key={i} className={"am-chip" + (active ? " on" : "")} onClick={() => onChip(a)}>
              <span className="am-ic">{tgt && tgt.cover ? <img src={tgt.cover} alt="" style={{ width: "100%", height: "100%", objectFit: "cover", borderRadius: "inherit" }} /> : <Icon name={m.icon} size={18} />}</span>
              <span className="am-l">{label}</span>
              {NAV_KINDS.includes(a.kind) && <Icon name="arrow" size={14} />}
              {a.kind === "aivoice" && <span className="am-state">{read.speaking ? "stop" : read.supported ? "play" : "n/a"}</span>}
            </button>
          );
        })}
      </div>
      {player && (player.src
        ? <div className="am-audio"><span className="am-audio-l">{player.label}</span><audio className="audio-player" controls autoPlay src={player.src} /><button className="iconbtn" onClick={() => setPlayer(null)}><Icon name="close" size={15} /></button></div>
        : <MiniPlayer kind={player.kind} label={player.label} onClose={() => setPlayer(null)} />)}
    </div>
  );
}

/* ============================================================
   Body block model — reader + editor
   ============================================================ */
/* a code block with an optional header: a language tag (</> bash) and/or a
   filename (test.bash), plus a copy button. Header hidden when both are empty. */
function CodeBlock({ b }) {
  const code = b.code || b.text || "";
  const [copied, setCopied] = uSa2(false);
  const copy = () => {
    try { navigator.clipboard.writeText(code); } catch (e) {}
    setCopied(true); setTimeout(() => setCopied(false), 1400);
  };
  const hasBar = !!(b.lang || b.file);
  return (
    <figure data-bid={b.id} className="ab-code-wrap">
      {hasBar && (
        <figcaption className="ab-code-bar">
          {b.lang && <span className="cb-lang"><span className="cb-chev">&lt;/&gt;</span>{b.lang}</span>}
          {b.file && <span className="cb-file">{b.file}</span>}
          <button type="button" className="cb-copy" onClick={copy} title="copy code">{copied ? "copied" : "copy"}</button>
        </figcaption>
      )}
      <pre className="ab-code"><code>{code}</code></pre>
    </figure>
  );
}

/* ---- image framing (blog-friendly: choose the frame shape, then fit/zoom/crop inside it) ----
   Fields on an img block: ratio (""=original | "16/9"|"4/3"|"1/1"|"3/2"|"21/9"),
   fit ("cover"|"contain"), zoom (1–3), posX/posY (focal %, 0–100), size ("full"|"inset"|"wide"). */
const IMG_RATIOS = [{ v: "", l: "Original" }, { v: "16/9", l: "16:9" }, { v: "3/2", l: "3:2" }, { v: "4/3", l: "4:3" }, { v: "1/1", l: "Square" }, { v: "21/9", l: "Wide" }, { v: "4/5", l: "4:5 portrait" }, { v: "3/4", l: "3:4 portrait" }, { v: "2/3", l: "2:3 tall" }, { v: "9/16", l: "9:16 tall" }];
const IMG_SIZES = [{ v: "full", l: "Full" }, { v: "inset", l: "Inset" }, { v: "wide", l: "Bleed" }];
function imgFrameParts(b) {
  const ratio = b.ratio || "";
  if (!ratio) return { framed: false, frameStyle: {}, imgStyle: {} };
  const fit = b.fit || "cover";
  const zoom = fit === "cover" ? (b.zoom || 1) : 1;
  const px = b.posX == null ? 50 : b.posX, py = b.posY == null ? 50 : b.posY;
  return {
    framed: true,
    frameStyle: { aspectRatio: ratio.replace("/", " / ") },
    imgStyle: { width: "100%", height: "100%", objectFit: fit, objectPosition: `${px}% ${py}%`, transform: zoom !== 1 ? `scale(${zoom})` : undefined },
  };
}
/* inner-image style for a FIXED-frame slot (hero cover, portrait, presentation image):
   the container owns the box + overflow:hidden; the image fits/zooms/repositions inside it. */
function imgFitStyle(o) {
  o = o || {};
  const fit = o.fit || "cover";
  const zoom = fit === "cover" ? (o.zoom || 1) : 1;
  const px = o.posX == null ? 50 : o.posX, py = o.posY == null ? 50 : o.posY;
  return { width: "100%", height: "100%", objectFit: fit, objectPosition: `${px}% ${py}%`, transform: zoom !== 1 ? `scale(${zoom})` : undefined };
}
function ArticleFigure({ src, b }) {
  const { framed, frameStyle, imgStyle } = imgFrameParts(b);
  return (
    <figure data-bid={b.id} className={"ab-fig ab-fig--" + (b.size || "full")}>
      {framed
        ? <div className="ab-imgframe" style={frameStyle}><img src={src} alt={b.caption || ""} style={imgStyle} /></div>
        : <img src={src} alt={b.caption || ""} />}
      {b.caption ? <figcaption>{b.caption}</figcaption> : null}
    </figure>
  );
}

/* ---- editor: pick the frame shape, then fit / zoom / reposition inside it ---- */
function ImageFrameControls({ block, src, onChange, hideSize }) {
  const b = block;
  const ratio = b.ratio || "";
  const fit = b.fit || "cover";
  const framePrev = uRa2(null);
  const drag = (e) => {
    if (fit !== "cover" || !framePrev.current) return;
    const r = framePrev.current.getBoundingClientRect();
    const cx = (e.touches ? e.touches[0].clientX : e.clientX) - r.left;
    const cy = (e.touches ? e.touches[0].clientY : e.clientY) - r.top;
    onChange({ posX: Math.max(0, Math.min(100, Math.round(cx / r.width * 100))), posY: Math.max(0, Math.min(100, Math.round(cy / r.height * 100))) });
  };
  const { framed, frameStyle, imgStyle } = imgFrameParts(b);
  return (
    <div className="col gap-8 imgframe-ctl">
      <div className="row gap-8 wrap" style={{ alignItems: "center" }}>
        <span className="ifc-lbl">Frame</span>
        <select value={ratio} onChange={e => onChange({ ratio: e.target.value })}>
          {IMG_RATIOS.map(o => <option key={o.v} value={o.v}>{o.l}</option>)}
        </select>
        {!hideSize && <span className="ifc-lbl">Width</span>}
        <select value={b.size || "full"} onChange={e => onChange({ size: e.target.value })} style={hideSize ? { display: "none" } : undefined}>
          {IMG_SIZES.map(o => <option key={o.v} value={o.v}>{o.l}</option>)}
        </select>
        {ratio && (
          <div className="seg">
            <button type="button" className={fit === "cover" ? "on" : ""} onClick={() => onChange({ fit: "cover" })}>Crop</button>
            <button type="button" className={fit === "contain" ? "on" : ""} onClick={() => onChange({ fit: "contain" })}>Fit</button>
          </div>
        )}
      </div>
      {ratio && src && (
        <div className="row gap-8" style={{ alignItems: "stretch" }}>
          <div ref={framePrev} className={"ab-imgframe ifc-prev" + (fit === "cover" ? " grab" : "")} style={{ ...frameStyle, maxWidth: 220 }}
            onMouseDown={fit === "cover" ? drag : undefined} onMouseMove={e => e.buttons === 1 && fit === "cover" && drag(e)}>
            <img src={src} alt="" style={imgStyle} draggable={false} />
            {fit === "cover" && <span className="ifc-focal" style={{ left: `${b.posX == null ? 50 : b.posX}%`, top: `${b.posY == null ? 50 : b.posY}%` }} />}
          </div>
          {fit === "cover" && (
            <div className="col gap-8" style={{ justifyContent: "center", flex: 1, minWidth: 120 }}>
              <label className="ifc-lbl" style={{ margin: 0 }}>Zoom · {(b.zoom || 1).toFixed(1)}×</label>
              <input type="range" min="1" max="3" step="0.1" value={b.zoom || 1} onChange={e => onChange({ zoom: parseFloat(e.target.value) })} />
              <div className="dim mono" style={{ fontSize: 10.5, lineHeight: 1.5 }}>Click / drag the preview to move the crop focus.</div>
            </div>
          )}
        </div>
      )}
    </div>
  );
}

/* ===================== ARTICLE BODY (reader) ===================== */
function BlockReader({ blocks }) {
  const { db } = useStore();
  const imgSrcOf = (b) => b.src || (b.assetId && window.assetById ? window.assetSrc(window.assetById(db, b.assetId)) : "");
  return (
    <div className="article-body">
      {(blocks || []).map(b => {
        if (b.type === "h") return b.level === 1
          ? <h2 key={b.id} data-bid={b.id} className="ab-h ab-h1">{b.text}</h2>
          : <h3 key={b.id} data-bid={b.id} className="ab-h">{b.text}</h3>;
        if (b.type === "quote") return (
          <blockquote key={b.id} data-bid={b.id} className={"pullquote" + (b.bar ? " has-bar" : "")}>
            <span className="pq-text"><span className="pq-mark open" aria-hidden="true">“</span>{renderInline(b.text)}<span className="pq-mark close" aria-hidden="true">”</span></span>
            {b.author && <cite className="pq-author">— {b.author}</cite>}
          </blockquote>
        );
        if (b.type === "callout") return (
          <div key={b.id} data-bid={b.id} className={"ab-callout tone-" + (b.tone || "info")}>
            <span className="cal-ic"><Icon name={b.tone === "warn" ? "bell" : b.tone === "tip" ? "star" : "globe"} size={18} /></span>
            <div className="cal-body">{renderInline(b.text)}</div>
          </div>
        );
        if (b.type === "table") {
          const rows = b.rows || [];
          return (
            <div key={b.id} data-bid={b.id} className="ab-table-wrap">
              <table className="ab-table">
                {b.header && rows.length > 0 && <thead><tr>{rows[0].map((c, ci) => <th key={ci}>{renderInline(c)}</th>)}</tr></thead>}
                <tbody>
                  {rows.slice(b.header ? 1 : 0).map((row, ri) => <tr key={ri}>{row.map((c, ci) => <td key={ci}>{renderInline(c)}</td>)}</tr>)}
                </tbody>
              </table>
            </div>
          );
        }
        if (b.type === "list") return (
          <ul key={b.id} data-bid={b.id} className="ab-list">
            {(b.text || "").split("\n").map(s => s.replace(/^[-*]\s+/, "").trim()).filter(Boolean).map((li, i) => <li key={i}>{renderInline(li)}</li>)}
          </ul>
        );
        if (b.type === "hr") return <hr key={b.id} data-bid={b.id} className="ab-sep" />;
        if (b.type === "accordion") return (
          <details key={b.id} data-bid={b.id} className="ab-acc" {...(b.open ? { open: true } : {})}>
            <summary className="ab-acc-sum">
              <span className="ab-acc-chev" aria-hidden="true"><Icon name="right" size={14} /></span>
              <span className="ab-acc-title">{b.summary || "Details"}</span>
            </summary>
            <div className="ab-acc-body">{(b.blocks && b.blocks.length) ? <BlockReader blocks={b.blocks} /> : renderInline(b.text)}</div>
          </details>
        );
        if (b.type === "code") return <CodeBlock key={b.id} b={b} />;
        if (b.type === "embed") return <div key={b.id} data-bid={b.id} className="ab-embed" dangerouslySetInnerHTML={{ __html: b.html || "" }} />;
        if (b.type === "img") { const _s = imgSrcOf(b); return _s
          ? <ArticleFigure key={b.id} src={_s} b={b} />
          : <div key={b.id} data-bid={b.id}><Placeholder label={b.caption || "figure / diagram"} tag="image" style={{ width: "100%", aspectRatio: b.ratio ? b.ratio.replace("/", " / ") : "16/9", margin: "8px 0" }} /></div>; }
        return <div key={b.id} data-bid={b.id}><RichText as="p" text={b.text} /></div>;
      })}
    </div>
  );
}

const BLOCK_KINDS = [
  { type: "h", label: "Heading", icon: "edit" },
  { type: "p", label: "Paragraph", icon: "msg" },
  { type: "list", label: "List", icon: "menu" },
  { type: "quote", label: "Quote", icon: "star" },
  { type: "callout", label: "Callout", icon: "bell" },
  { type: "table", label: "Table", icon: "chart" },
  { type: "img", label: "Image", icon: "compass" },
  { type: "code", label: "Code", icon: "chart" },
  { type: "embed", label: "Embed / HTML", icon: "globe" },
  { type: "accordion", label: "Collapsible", icon: "right" },
  { type: "hr", label: "Separator", icon: "down" },
];

function blankBlock(type) {
  return { id: uid("b"), type, text: "", code: "", lang: "", file: "", html: "", src: null, caption: "", author: "", tone: "info", header: true, summary: "", open: false, blocks: type === "accordion" ? [] : undefined, ratio: "", fit: "cover", zoom: 1, posX: 50, posY: 50, size: "full", rows: type === "table" ? [["Column A", "Column B"], ["", ""]] : undefined, level: type === "h" ? 2 : undefined };
}

/* a small free-form syntax hint shown under text inputs */
function InlineHint() {
  return (
    <div className="dim mono inline-hint" style={{ fontSize: 11, marginTop: 5, lineHeight: 1.6 }}>
      <b>**bold**</b> · <b>__underline__</b> · <span style={{ color: "var(--accent-ink)" }}>[link](https://…)</span> · internal <span style={{ color: "var(--accent-ink)" }}>[Blog](/blog)</span> · <span style={{ borderBottom: "2px dotted var(--accent)" }}>[[term|definition]]</span> bubble · new line = line break
    </div>
  );
}

/* table editor — add / remove rows & columns, toggle header */
function TableEditor({ block, onChange }) {
  const rows = block.rows && block.rows.length ? block.rows : [["", ""], ["", ""]];
  const cols = rows[0] ? rows[0].length : 2;
  const setCell = (r, c, v) => { const n = rows.map(row => row.slice()); n[r][c] = v; onChange({ rows: n }); };
  const addRow = () => onChange({ rows: [...rows.map(r => r.slice()), Array.from({ length: cols }, () => "")] });
  const delRow = (r) => onChange({ rows: rows.length > 1 ? rows.filter((_, i) => i !== r) : rows });
  const addCol = () => onChange({ rows: rows.map(r => [...r, ""]) });
  const delCol = (c) => onChange({ rows: cols > 1 ? rows.map(r => r.filter((_, i) => i !== c)) : rows });
  return (
    <div className="table-editor">
      <div className="row gap-8" style={{ marginBottom: 8, flexWrap: "wrap" }}>
        <button type="button" className={"chip" + (block.header ? " on" : "")} onClick={() => onChange({ header: !block.header })}><Icon name="check" size={12} style={{ verticalAlign: "-2px", marginRight: 3 }} />Header row</button>
        <span className="grow" />
        <button type="button" className="btn ghost sm" onClick={addRow}><Icon name="plus" size={13} /> Row</button>
        <button type="button" className="btn ghost sm" onClick={addCol}><Icon name="plus" size={13} /> Column</button>
      </div>
      <div className="te-grid" style={{ overflowX: "auto" }}>
        <table className="te-table">
          <tbody>
            {rows.map((row, r) => (
              <tr key={r}>
                {row.map((cell, c) => (
                  <td key={c} className={block.header && r === 0 ? "te-head" : ""}>
                    <input value={cell} onChange={e => setCell(r, c, e.target.value)} placeholder={block.header && r === 0 ? "Heading" : "…"} />
                  </td>
                ))}
                <td className="te-rowtool"><button type="button" className="iconbtn sm danger" onClick={() => delRow(r)} title="delete row"><Icon name="trash" size={13} /></button></td>
              </tr>
            ))}
            <tr>
              {Array.from({ length: cols }).map((_, c) => (
                <td key={c} className="te-coltool"><button type="button" className="iconbtn sm danger" onClick={() => delCol(c)} title="delete column"><Icon name="trash" size={12} /></button></td>
              ))}
              <td />
            </tr>
          </tbody>
        </table>
      </div>
    </div>
  );
}

function BlockEditor({ blocks, onChange, nested }) {
  const { db } = useStore();
  const [imgPick, setImgPick] = uSa2(null); // index of the img block awaiting a media-library pick
  const [imgUpload, setImgUpload] = uSa2(null); // { i, file } awaiting the R2 upload dialog
  const list = blocks || [];
  // inside a collapsible, every block type is allowed EXCEPT another collapsible (no nesting loops).
  const kinds = nested ? BLOCK_KINDS.filter(k => k.type !== "accordion") : BLOCK_KINDS;
  const setBlock = (i, patch) => onChange(list.map((b, j) => j === i ? { ...b, ...patch } : b));
  const move = (i, dir) => {
    const j = i + dir; if (j < 0 || j >= list.length) return;
    const n = list.slice(); const [m] = n.splice(i, 1); n.splice(j, 0, m); onChange(n);
  };
  const remove = (i) => onChange(list.filter((_, j) => j !== i));
  const add = (type) => onChange([...list, blankBlock(type)]);
  const addBelow = (i) => { const n = list.slice(); n.splice(i + 1, 0, blankBlock("p")); onChange(n); };
  const changeType = (i, type) => setBlock(i, { type, level: type === "h" ? (list[i].level || 2) : undefined });
  return (
    <div className="block-editor">
      {list.map((b, i) => (
        <div key={b.id} data-bid={b.id} className="be-block">
          <div className="be-bar">
            <select className="be-type-sel" value={b.type} onChange={e => changeType(i, e.target.value)} title="block type">
              {kinds.map(k => <option key={k.type} value={k.type}>{k.label}</option>)}
            </select>
            {b.type === "h" && (
              <div className="row gap-8" style={{ marginLeft: 2 }}>
                {[1, 2].map(L => <button key={L} type="button" className={"chip" + ((b.level || 2) === L ? " on" : "")} onClick={() => setBlock(i, { level: L })}>H{L}</button>)}
              </div>
            )}
            <span className="grow" />
            <button className="iconbtn sm" onClick={() => move(i, -1)} disabled={i === 0}><Icon name="up" size={14} /></button>
            <button className="iconbtn sm" onClick={() => move(i, 1)} disabled={i === list.length - 1}><Icon name="down" size={14} /></button>
            <button className="iconbtn sm danger" onClick={() => remove(i)}><Icon name="trash" size={14} /></button>
          </div>
          {b.type === "img" ? (
            <div className="col gap-8">
              <ImageUpload value={b.src || (b.assetId && window.assetById ? window.assetSrc(window.assetById(db, b.assetId)) : "")} onChange={v => setBlock(i, { src: v, assetId: undefined })} onFile={f => setImgUpload({ i, file: f })} label="drop or click — uploads to the library (R2)" aspect="16/9" />
              <div className="row gap-8 wrap" style={{ alignItems: "center" }}>
                <button type="button" className="chip" onClick={() => setImgPick(i)}><Icon name="compass" size={12} style={{ verticalAlign: "-2px", marginRight: 3 }} />From library</button>
                {b.assetId && window.AssetRef && <window.AssetRef assetId={b.assetId} onClear={() => setBlock(i, { assetId: undefined })} />}
              </div>
              <input value={b.caption || ""} onChange={e => setBlock(i, { caption: e.target.value })} placeholder="Caption (optional)" />
              <ImageFrameControls block={b} src={b.src || (b.assetId && window.assetById ? window.assetSrc(window.assetById(db, b.assetId)) : "")} onChange={patch => setBlock(i, patch)} />
            </div>
          ) : b.type === "code" ? (
            <div className="col gap-8">
              <div className="row gap-8" style={{ flexWrap: "wrap" }}>
                <input className="mono" value={b.lang || ""} onChange={e => setBlock(i, { lang: e.target.value })} placeholder="language — e.g. bash, python, sql" style={{ flex: "1 1 150px", fontSize: 12 }} />
                <input className="mono" value={b.file || ""} onChange={e => setBlock(i, { file: e.target.value })} placeholder="filename (optional) — e.g. test.bash" style={{ flex: "1 1 180px", fontSize: 12 }} />
              </div>
              <textarea rows={4} className="mono" value={b.code || ""} onChange={e => setBlock(i, { code: e.target.value })} placeholder="paste code…" style={{ fontSize: 13 }} />
              <div className="dim mono" style={{ fontSize: 11 }}>Shown above the block as <b>&lt;/&gt; language</b> and/or the <b>filename</b>.</div>
            </div>
          ) : b.type === "embed" ? (
            <div className="col gap-8">
              <textarea rows={3} className="mono" value={b.html || ""} onChange={e => setBlock(i, { html: e.target.value })} placeholder="<iframe …>, any embed HTML, or a link to another media on the site" style={{ fontSize: 12 }} />
              <div className="dim mono" style={{ fontSize: 11 }}>Tip: paste an <b>&lt;iframe&gt;</b>, or an <b>&lt;a href=\"#/blog/…\"&gt;</b> link to another piece on the site.</div>
            </div>
          ) : b.type === "table" ? (
            <TableEditor block={b} onChange={patch => setBlock(i, patch)} />
          ) : b.type === "callout" ? (
            <div className="col gap-8">
              <div className="row gap-8">
                {[{ id: "info", l: "Info" }, { id: "tip", l: "Tip" }, { id: "warn", l: "Warning" }].map(o => (
                  <button key={o.id} type="button" className={"chip" + ((b.tone || "info") === o.id ? " on" : "")} onClick={() => setBlock(i, { tone: o.id })}>{o.l}</button>
                ))}
              </div>
              <textarea rows={3} value={b.text || ""} onChange={e => setBlock(i, { text: e.target.value })} placeholder="Highlight a tip, a warning or an aside…" />
              <InlineHint />
            </div>
          ) : b.type === "hr" ? (
            <div className="be-hr-note">— horizontal separator —</div>
          ) : b.type === "accordion" ? (
            <div className="col gap-8">
              <input value={b.summary || ""} onChange={e => setBlock(i, { summary: e.target.value })} placeholder="Summary / clickable title — e.g. “Show the full method”" style={{ fontFamily: "var(--font-display)", fontWeight: 700 }} />
              <div className="row gap-8" style={{ alignItems: "center" }}>
                <button type="button" className={"chip" + (b.open ? " on" : "")} onClick={() => setBlock(i, { open: !b.open })}><Icon name="check" size={12} style={{ verticalAlign: "-2px", marginRight: 3 }} />Open by default</button>
                <span className="dim mono" style={{ fontSize: 11 }}>readers click the arrow to expand · nest any block below (except another collapsible)</span>
              </div>
              <div className="be-nested">
                <BlockEditor blocks={b.blocks || []} onChange={nb => setBlock(i, { blocks: nb })} nested />
              </div>
            </div>
          ) : b.type === "list" ? (
            <textarea rows={4} value={b.text || ""} onChange={e => setBlock(i, { text: e.target.value })} placeholder={"One item per line…\n- first point\n- second point"} />
          ) : b.type === "h" ? (
            <input value={b.text || ""} onChange={e => setBlock(i, { text: e.target.value })} placeholder="Heading…" style={{ fontFamily: "var(--font-display)", fontWeight: 800, fontSize: b.level === 1 ? 22 : 18 }} />
          ) : b.type === "quote" ? (
            <div className="col gap-8">
              <textarea rows={2} value={b.text || ""} onChange={e => setBlock(i, { text: e.target.value })} placeholder="“ A quote… ”" />
              <input value={b.author || ""} onChange={e => setBlock(i, { author: e.target.value })} placeholder="Author / source (optional)" />
              <div className="row gap-8" style={{ alignItems: "center" }}>
                <button type="button" className={"chip" + (b.bar ? " on" : "")} onClick={() => setBlock(i, { bar: !b.bar })}><Icon name="check" size={12} style={{ verticalAlign: "-2px", marginRight: 3 }} />Left accent bar</button>
                <span className="dim mono" style={{ fontSize: 11 }}>off = quote marks only</span>
              </div>
            </div>
          ) : (
            <div className="col gap-8">
              <textarea rows={4} value={b.text || ""} onChange={e => setBlock(i, { text: e.target.value })} placeholder="Write… (**bold**, __underline__, [link](url), [[term|definition]] supported)" />
              <InlineHint />
            </div>
          )}
          <button className="be-addbelow" onClick={() => addBelow(i)} title="add a block below"><Icon name="plus" size={12} /> add block below</button>
        </div>
      ))}
      <div className="be-add">
        <span className="dim mono" style={{ fontSize: 11, alignSelf: "center", marginRight: 2 }}>add block:</span>
        {kinds.map(k => <button key={k.type} className="chip" onClick={() => add(k.type)}><Icon name={k.icon} size={12} style={{ verticalAlign: "-2px", marginRight: 3 }} />{k.label}</button>)}
      </div>
      {imgPick != null && window.MediaPicker && <window.MediaPicker type="image" title="Search media" onPick={(id) => { const m = window.assetById(db, id); setBlock(imgPick, { assetId: id, src: (m && window.assetSrc(m)) || null }); }} onClose={() => setImgPick(null)} />}
      {imgUpload != null && window.MediaPicker && <window.MediaPicker type="image" title="Upload figure to the library" initialTab="upload" initialFile={imgUpload.file} onPick={(id) => { const m = window.assetById(db, id); setBlock(imgUpload.i, { assetId: id, src: (m && window.assetSrc(m)) || null }); }} onClose={() => setImgUpload(null)} />}
    </div>
  );
}

/* ---------- body <-> markup (export / import for API publishing) ---------- */
function bodyToMarkup(blocks) {
  return (blocks || []).map(b => {
    if (b.type === "h") return (b.level === 1 ? "# " : "## ") + (b.text || "");
    if (b.type === "quote") return "> " + (b.text || "") + (b.author ? `\n> — ${b.author}` : "");
    if (b.type === "callout") return ":::" + (b.tone || "info") + "\n" + (b.text || "") + "\n:::";
    if (b.type === "table") return (b.rows || []).map(r => "| " + r.join(" | ") + " |").join("\n");
    if (b.type === "list") return (b.text || "").split("\n").map(s => s.replace(/^[-*]\s+/, "").trim()).filter(Boolean).map(s => "- " + s).join("\n");
    if (b.type === "hr") return "---";
    if (b.type === "accordion") return "+++ " + (b.summary || "") + "\n" + ((b.blocks && b.blocks.length) ? bodyToMarkup(b.blocks) : (b.text || "")) + "\n+++";
    if (b.type === "code") return "```" + (b.lang || "") + (b.file ? " " + b.file : "") + "\n" + (b.code || "") + "\n```";
    if (b.type === "embed") return "<embed>\n" + (b.html || "") + "\n</embed>";
    if (b.type === "img") return `![${b.caption || ""}](${b.src || ""})`;
    return b.text || "";
  }).join("\n\n");
}
function markupToBody(text) {
  const out = [];
  const chunks = (text || "").split(/\n{2,}/);
  chunks.forEach(raw => {
    const block = raw.replace(/\r/g, "");
    const t = block.trim();
    if (!t) return;
    if (/^```/.test(t)) { const info = ((t.match(/^```([^\n]*)/) || ["", ""])[1] || "").trim().split(/\s+/); const lang = info[0] || ""; const file = info.slice(1).join(" "); out.push({ ...blankBlock("code"), lang, file, code: t.replace(/^```[^\n]*\n?/, "").replace(/```$/, "").trim() }); return; }
    if (/^:::/.test(t)) { const tn = t.match(/^:::(\w+)/); out.push({ ...blankBlock("callout"), tone: tn ? tn[1] : "info", text: t.replace(/^:::\w*\s*/, "").replace(/\n?:::\s*$/, "").trim() }); return; }
    if (/^\+\+\+/.test(t)) { const m = t.match(/^\+\+\+\s*([^\n]*)\n?([\s\S]*?)\n?\+\+\+\s*$/); const inner = m ? m[2].trim() : t.replace(/^\+\+\+\s*/, "").replace(/\+\+\+\s*$/, "").trim(); out.push({ ...blankBlock("accordion"), summary: m ? m[1].trim() : "", blocks: inner ? markupToBody(inner) : [] }); return; }
    if (/^\|.*\|/.test(t)) {
      const rows = t.split("\n").map(l => l.trim()).filter(Boolean)
        .map(l => l.replace(/^\|/, "").replace(/\|$/, "").split("|").map(c => c.trim()))
        .filter(r => !r.every(c => /^:?-+:?$/.test(c)));
      out.push({ ...blankBlock("table"), header: true, rows: rows.length ? rows : [["", ""]] }); return;
    }
    if (/^<embed>/i.test(t)) { out.push({ ...blankBlock("embed"), html: t.replace(/^<embed>\s*/i, "").replace(/<\/embed>\s*$/i, "").trim() }); return; }
    if (/^---+$/.test(t)) { out.push(blankBlock("hr")); return; }
    if (/^!\[/.test(t)) { const m = t.match(/^!\[(.*?)\]\((.*?)\)/); out.push({ ...blankBlock("img"), caption: m ? m[1] : "", src: m && m[2] ? m[2] : null }); return; }
    if (/^#\s/.test(t)) { out.push({ ...blankBlock("h"), level: 1, text: t.replace(/^#\s+/, "") }); return; }
    if (/^##\s/.test(t)) { out.push({ ...blankBlock("h"), level: 2, text: t.replace(/^##\s+/, "") }); return; }
    if (/^>\s/.test(t)) {
      const lines = t.split("\n").map(l => l.replace(/^>\s?/, ""));
      const authorLine = lines.find(l => /^—|^-\s/.test(l));
      const body = lines.filter(l => l !== authorLine).join(" ").trim();
      out.push({ ...blankBlock("quote"), text: body, author: authorLine ? authorLine.replace(/^[—-]\s*/, "") : "" });
      return;
    }
    if (/^[-*]\s/.test(t)) { out.push({ ...blankBlock("list"), text: t.split("\n").map(l => l.replace(/^[-*]\s+/, "")).join("\n") }); return; }
    out.push({ ...blankBlock("p"), text: t });
  });
  return out.length ? out : [blankBlock("p")];
}

function ExportImportModal({ blocks, onClose, onImport }) {
  const [text, setText] = uSa2(() => bodyToMarkup(blocks));
  const [copied, setCopied] = uSa2(false);
  const copy = () => { try { navigator.clipboard.writeText(text); setCopied(true); setTimeout(() => setCopied(false), 1400); } catch (e) {} };
  return (
    <Modal title="Export / import body" onClose={onClose}
      footer={<>
        <button className="btn ghost" style={{ marginRight: "auto" }} onClick={copy}><Icon name="save" size={15} /> {copied ? "Copied!" : "Copy markup"}</button>
        <button className="btn ghost" onClick={onClose}>Close</button>
        <button className="btn accent" onClick={() => { onImport(markupToBody(text)); onClose(); }}><Icon name="check" size={16} /> Apply to article</button>
      </>}>
      <p className="dim" style={{ fontSize: 13, marginTop: -4 }}>A simple markup an API or another tool can read &amp; write. <b># / ##</b> headings, <b>-</b> lists, <b>&gt;</b> quotes, <b>---</b> separators, <code>```</code> code, <b>![caption](url)</b> images, <b>&lt;embed&gt;…&lt;/embed&gt;</b> HTML.</p>
      <textarea rows={14} className="mono" style={{ fontSize: 12.5, lineHeight: 1.5 }} value={text} onChange={e => setText(e.target.value)} />
    </Modal>
  );
}

Object.assign(window, { ExportImportModal, bodyToMarkup, markupToBody, TableEditor, InlineHint });

/* ---------- sources editor ---------- */
function SourcesEditor({ sources, onChange }) {
  const list = sources || [];
  const set = (i, k, v) => onChange(list.map((s, j) => j === i ? { ...s, [k]: v } : s));
  return (
    <div className="col gap-8">
      {list.map((s, i) => (
        <div className="field-row" key={i}>
          <input placeholder="Label" value={s.label} onChange={e => set(i, "label", e.target.value)} />
          <input placeholder="https://…" value={s.url} onChange={e => set(i, "url", e.target.value)} />
          <button className="iconbtn danger" style={{ flex: "none" }} onClick={() => onChange(list.filter((_, j) => j !== i))}><Icon name="trash" size={15} /></button>
        </div>
      ))}
      <button className="btn ghost sm" style={{ alignSelf: "flex-start" }} onClick={() => onChange([...list, { label: "", url: "" }])}><Icon name="plus" size={14} /> Add source</button>
    </div>
  );
}

/* ---------- alternative-media editor ---------- */
const ALT_KINDS = [
  { kind: "video", label: "Video" }, { kind: "read", label: "Read (article)" },
  { kind: "podcast", label: "Podcast" }, { kind: "page", label: "Other page (game, app…)" },
  { kind: "voice", label: "My voice" }, { kind: "aivoice", label: "AI read-aloud" }, { kind: "music", label: "Music" },
];
const ALT_NEEDS_REF = ["video", "read", "podcast", "page"];
const ALT_NEEDS_ASSET = ["voice", "music"];
const ALT_DEFAULT_FOLDER = { voice: "readloud-blog", music: "soundtrack" };
function AltEditor({ alt, content, pages = [], item, onChange }) {
  const { db } = useStore();
  const list = alt || [];
  const [pick, setPick] = uSa2(null);   // index of the row awaiting an audio asset
  const set = (i, patch) => onChange(list.map((a, j) => j === i ? { ...a, ...patch } : a));
  const refsFor = (kind) => {
    if (kind === "video")   return content.filter(c => c.type === "video" && c.id !== item.id).map(c => ({ id: c.ref, title: c.title, cover: c.cover }));
    if (kind === "read")    return content.filter(c => c.type === "blog" && c.id !== item.id).map(c => ({ id: c.ref, title: c.title, cover: c.cover }));
    if (kind === "podcast") return content.filter(c => c.type === "podcast" && c.id !== item.id).map(c => ({ id: c.ref, title: c.title, cover: c.cover }));
    if (kind === "page")    return (pages || []).filter(p => p.kind !== "secret").map(p => ({ id: p.id, title: p.label, cover: null }));
    return [];
  };
  const targetOf = (a) => {
    if (a.kind === "page") return (pages || []).find(p => p.id === a.ref);
    return content.find(c => c.ref === a.ref);
  };
  return (
    <div className="col gap-8">
      {list.map((a, i) => {
        const opts = refsFor(a.kind);
        const tgt = ALT_NEEDS_REF.includes(a.kind) ? targetOf(a) : null;
        const m = ALT_META[a.kind] || { icon: "arrow" };
        return (
          <div key={i} className="alt-edit-row2">
            <span className="aer-thumb">
              {tgt && tgt.cover ? <img src={tgt.cover} alt="" /> : <Icon name={m.icon} size={16} />}
            </span>
            <div className="col gap-8" style={{ flex: 1, minWidth: 0 }}>
              <div className="row gap-8">
                <select value={a.kind} onChange={e => set(i, { kind: e.target.value, ref: "" })} style={{ flex: ".9" }}>
                  {ALT_KINDS.map(k => <option key={k.kind} value={k.kind}>{k.label}</option>)}
                </select>
                {ALT_NEEDS_REF.includes(a.kind) && (
                  <select value={a.ref || ""} onChange={e => set(i, { ref: e.target.value })} style={{ flex: 1 }}>
                    <option value="">{opts.length ? "— choose —" : "none available"}</option>
                    {opts.map(o => <option key={o.id} value={o.id}>{(o.title || "").slice(0, 32)}</option>)}
                  </select>
                )}
                <button className="iconbtn danger" style={{ flex: "none" }} onClick={() => onChange(list.filter((_, j) => j !== i))}><Icon name="trash" size={15} /></button>
              </div>
              <input placeholder={tgt ? `Label — optional (defaults to “${(tgt.title || "").slice(0, 24)}”)` : "Label shown on the reader's chip — optional"} value={a.label || ""} onChange={e => set(i, { label: e.target.value })} />
              {ALT_NEEDS_ASSET.includes(a.kind) && (
                <div className="row gap-8 wrap" style={{ alignItems: "center" }}>
                  <button type="button" className="chip" onClick={() => setPick(i)}><Icon name="compass" size={12} style={{ verticalAlign: "-2px", marginRight: 3 }} />{a.assetId ? "Change media" : "Search media"}</button>
                  {a.assetId && window.AssetRef && <window.AssetRef assetId={a.assetId} onClear={() => set(i, { assetId: undefined })} />}
                  {!a.assetId && <span className="dim mono" style={{ fontSize: 11 }}>no file linked — plays a demo</span>}
                </div>
              )}
            </div>
          </div>
        );
      })}
      <button className="btn ghost sm" style={{ alignSelf: "flex-start" }} onClick={() => onChange([...list, { kind: "voice", label: "" }])}><Icon name="plus" size={14} /> Link an alternative</button>
      {pick != null && window.MediaPicker && <window.MediaPicker type="audio" defaultFolder={ALT_DEFAULT_FOLDER[(list[pick] || {}).kind] || "all"} title="Search media" onPick={(id) => set(pick, { assetId: id })} onClose={() => setPick(null)} />}
    </div>
  );
}

Object.assign(window, { AiBadge, AltMediaPanel, MiniPlayer, BlockReader, BlockEditor, SourcesEditor, AltEditor, useReadAloud, AI_LABEL, ALT_META, ImageFrameControls, imgFrameParts, imgFitStyle, ArticleFigure });
