/* steps.jsx — prep-step editor: inline-editable cards, quick-add, drag reorder,
   reveal toggles, and per-step AI "spark ideas". Depends on ui.jsx (window). */

// ── AI: ask Claude for step-appropriate suggestions, parsed into card shapes ──
async function sparkIdeas(step, ctx, n = 3, steer = "") {
  const fieldSpec = step.fields
    .filter((f) => f.kind !== "aspects")
    .map((f) => `"${f.key}": ${f.kind === "line" ? "short string" : "1–2 sentence string"} (${f.label})`)
    .join(", ");
  const hasAspects = step.fields.some((f) => f.key === "aspects");
  const aspectSpec = hasAspects ? `, "aspects": array of 3 short evocative sensory phrases` : "";

  const existing = (ctx.items || [])
    .map((it) => it.title || it.body || "")
    .filter(Boolean).slice(0, 12).join("; ");
  const pcs = (ctx.pcs || []).map((p) => `${p.title} (${p.sub || ""})`).join("; ");
  const steerLine = steer && steer.trim()
    ? `\n\nThe GM wants the ideas to follow this specific direction: "${steer.trim()}". Honor it closely while keeping the campaign tone.`
    : "";

  const prompt = `You are a creative co-GM helping prep a tabletop RPG session using the "Lazy Dungeon Master" method. Stay system-neutral.

Campaign: "${ctx.campaign}" — ${ctx.blurb || ""}
${ctx.tone ? `Tone & themes: ${ctx.tone}` : ""}
Session: "${ctx.session}"
Player characters: ${pcs || "unknown"}

Prep section: "${step.name}". ${step.tagline}
${existing ? `Already noted (avoid repeating): ${existing}` : ""}

Generate ${n} fresh, vivid, usable ideas for this step. Each must be evocative and specific to THIS campaign's tone${ctx.tone ? ` (${ctx.tone})` : ""}, not generic. Tie at least one to a player character's drive when natural.${steerLine}

Respond with ONLY a JSON array of ${n} objects, each shaped: { ${fieldSpec}${aspectSpec} }. No prose, no markdown fences.`;

  const raw = await window.claude.complete(prompt);
  let txt = (raw || "").trim().replace(/^```(json)?/i, "").replace(/```$/, "").trim();
  const a = txt.indexOf("["), b = txt.lastIndexOf("]");
  if (a !== -1 && b !== -1) txt = txt.slice(a, b + 1);
  const arr = JSON.parse(txt);
  return Array.isArray(arr) ? arr : [];
}

// ── AI: polish/rewrite an existing entry, keeping its intent ──────────────────
async function rewriteContent(step, item, ctx) {
  const fieldSpec = step.fields
    .filter((f) => f.kind !== "aspects")
    .map((f) => `"${f.key}": ${f.kind === "line" ? "short string" : "1–2 sentence string"} (${f.label})`)
    .join(", ");
  const hasAspects = step.fields.some((f) => f.key === "aspects");
  const aspectSpec = hasAspects ? `, "aspects": array of 3 short evocative sensory phrases` : "";

  const current = {};
  step.fields.forEach((f) => {
    const v = item[f.key];
    if (v != null && (Array.isArray(v) ? v.length : String(v).trim())) current[f.key] = v;
  });

  const prompt = `You are a co-GM polishing one prep entry for a tabletop RPG session ("Lazy Dungeon Master" method, system-neutral).

Campaign: "${ctx.campaign}" — ${ctx.blurb || ""}
${ctx.tone ? `Tone & themes: ${ctx.tone}` : ""}
Session: "${ctx.session}"
Prep section: "${step.name}". ${step.tagline}

Here is the GM's current draft for one ${step.itemNoun || "entry"} (JSON):
${JSON.stringify(current)}

Rewrite it to be sharper, more vivid, and specific to the campaign's tone${ctx.tone ? ` (${ctx.tone})` : ""}. Keep the same intent and any proper names — elevate and tighten the prose, don't invent a wholly different entry. If a field is empty, you may fill it in so the entry feels complete.

Respond with ONLY a JSON object: { ${fieldSpec}${aspectSpec} }. No prose, no markdown fences.`;

  const raw = await window.claude.complete(prompt);
  let txt = (raw || "").trim().replace(/^```(json)?/i, "").replace(/```$/, "").trim();
  const a = txt.indexOf("{"), b = txt.lastIndexOf("}");
  if (a !== -1 && b !== -1) txt = txt.slice(a, b + 1);
  const obj = JSON.parse(txt);
  return obj && typeof obj === "object" ? obj : {};
}

// ── Card field renderer ──────────────────────────────────────────────────────
function CardFields({ step, item, patch }) {
  return step.fields.map((f) => {
    if (f.key === "aspects") {
      return <window.AspectsEditor key={f.key} value={item.aspects} placeholder={f.placeholder}
        onChange={(v) => patch({ aspects: v })} />;
    }
    if (f.kind === "line") {
      const isTitle = f.key === "title";
      return <window.EditableLine key={f.key} big={isTitle} value={item[f.key]} placeholder={f.placeholder}
        className={isTitle ? "card-title" : "card-sub"} onChange={(v) => patch({ [f.key]: v })} />;
    }
    return <window.EditableArea key={f.key} value={item[f.key]} placeholder={f.placeholder}
      onChange={(v) => patch({ [f.key]: v })} />;
  });
}

// ── One editable card ─────────────────────────────────────────────────────────
function dataUriToBlobUrl(dataUri) {
  const [header, b64] = dataUri.split(',');
  const mime = header.match(/:(.*?);/)[1];
  const bytes = atob(b64);
  const arr = new Uint8Array(bytes.length);
  for (let i = 0; i < bytes.length; i++) arr[i] = bytes.charCodeAt(i);
  return URL.createObjectURL(new Blob([arr], { type: mime }));
}

function PrepCard({ step, item, patch, remove, duplicate, dragProps, isDragging, onRewrite, thinking, onImageOpen }) {
  const [lightboxOpen, setLightboxOpen] = React.useState(false);
  const [blobUrl, setBlobUrl] = React.useState(null);

  React.useEffect(() => {
    if (!lightboxOpen || !item.image) return;
    const url = dataUriToBlobUrl(item.image);
    setBlobUrl(url);
    return () => { URL.revokeObjectURL(url); setBlobUrl(null); };
  }, [lightboxOpen, item.image]);

  React.useEffect(() => {
    if (!lightboxOpen) return;
    const onKey = (e) => { if (e.key === 'Escape') setLightboxOpen(false); };
    document.addEventListener('keydown', onKey);
    return () => document.removeEventListener('keydown', onKey);
  }, [lightboxOpen]);

  return (
    <div className={`prep-card${isDragging ? " dragging" : ""}${thinking ? " thinking" : ""}`}>
      {thinking && <div className="card-thinking"><span className="spin" /> Rewriting…</div>}
      <div className="card-rail" {...dragProps} title="Drag to reorder">
        <window.Icon name="grip" size={15} sw={1.6} />
      </div>
      <div className="card-tools">
        <window.IconButton name="image" title="Add / change image" onClick={onImageOpen} active={!!item.image} />
        <window.IconButton name="sparkles" title="Rewrite with AI" onClick={onRewrite} />
        <window.IconButton name="copy" title="Duplicate" onClick={duplicate} />
        <window.ConfirmDelete onConfirm={remove} label="Delete card" />
      </div>
      {item.image && (
        <div className="card-image-thumb" onClick={() => setLightboxOpen(true)}>
          <img src={item.image} alt="" />
          <button type="button" className="card-image-remove" title="Remove image"
            onClick={(e) => { e.stopPropagation(); patch({ image: null }); }}>
            <window.Icon name="x" size={11} sw={2.4} />
          </button>
        </div>
      )}
      <div className="card-main">
        <window.CardFields step={step} item={item} patch={patch} />
      </div>
      {lightboxOpen && blobUrl && (
        <div className="img-lightbox" onClick={() => setLightboxOpen(false)}>
          <img src={blobUrl} alt="" className="img-lightbox-img" onClick={(e) => e.stopPropagation()} />
        </div>
      )}
    </div>
  );
}

// ── Compact secret row (for the secrets step only) ────────────────────────────
function SecretRow({ item, patch, remove, onRewrite, thinking }) {
  return (
    <li className={`secret-row${thinking ? " thinking" : ""}`}>
      <div className="secret-body">
        {thinking
          ? <span className="secret-thinking"><span className="spin" /> Rewriting…</span>
          : <window.EditableArea value={item.body} minRows={1}
              placeholder="A truth the party might uncover…"
              onChange={(v) => patch({ body: v })} />
        }
      </div>
      <div className="secret-actions">
        <window.IconButton name="sparkles" title="Rewrite with AI" onClick={onRewrite} />
        <window.ConfirmDelete onConfirm={remove} label="Delete secret" />
      </div>
    </li>
  );
}

// ── AI suggestion strip ────────────────────────────────────────────────────────
function SparkPanel({ step, ctx, onAdd, onAddMany, onClose }) {
  const [state, setState] = React.useState("idle");
  const [ideas, setIdeas] = React.useState([]);
  const [added, setAdded] = React.useState(new Set());
  const [err, setErr] = React.useState("");
  const [steer, setSteer] = React.useState("");
  const steerRef = React.useRef("");

  const run = React.useCallback(async () => {
    setState("loading"); setErr("");
    try {
      const arr = await sparkIdeas(step, ctx, 3, steerRef.current);
      setIdeas(arr); setState("ok");
    } catch (e) {
      setErr("The muse is quiet — try again."); setState("error");
    }
  }, [step, ctx]);

  const preview = (idea) => idea.title || idea.body || Object.values(idea)[0] || "";
  const detail = (idea) => idea.title && idea.body ? idea.body : "";

  return (
    <div className="spark">
      <div className="spark-hd">
        <span className="spark-title"><window.Icon name="sparkles" size={15} /> Sparked ideas</span>
        <window.IconButton name="x" title="Dismiss" onClick={onClose} />
      </div>
      <div className="spark-steer">
        <input className="spark-steer-input" value={steer}
          placeholder="Optional: steer the ideas — a theme, a twist, a name to build on…"
          onChange={(e) => { setSteer(e.target.value); steerRef.current = e.target.value; }}
          onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); run(); } }} />
        <button type="button" className="spark-go" onClick={run} disabled={state === "loading"}>
          <window.Icon name="sparkles" size={14} /> {state === "loading" ? "…" : "Spark"}
        </button>
      </div>
      {state === "idle" &&
        <div className="spark-empty">Optionally steer the muse above, then hit Spark for three fresh ideas in this campaign's voice.</div>
      }
      {state === "loading" &&
        <div className="spark-list">
          {[0, 1, 2].map((i) => <div className="spark-skel" key={i} style={{ animationDelay: i * 0.12 + "s" }} />)}
        </div>
      }
      {state === "error" && <div className="spark-empty">{err}</div>}
      {state === "ok" &&
        <div className="spark-list">
          {ideas.length === 0 && <div className="spark-empty">No ideas came back — re-spark?</div>}
          {ideas.map((idea, i) => {
            const isAdded = added.has(i);
            return (
              <button type="button"
                className={`spark-idea${isAdded ? " spark-added" : ""}`}
                key={i}
                onClick={() => {
                  if (isAdded) return;
                  onAdd(idea);
                  setAdded(prev => new Set([...prev, i]));
                  setTimeout(() => {
                    const list = document.querySelector('.card-list, .secret-list');
                    if (!list) return;
                    const last = list.lastElementChild;
                    if (!last) return;
                    let sc = last.parentElement;
                    while (sc) {
                      const oy = getComputedStyle(sc).overflowY;
                      if ((oy === 'auto' || oy === 'scroll') && sc.scrollHeight > sc.clientHeight + 1) break;
                      sc = sc.parentElement;
                    }
                    const offset = 80;
                    if (sc) {
                      const top = last.getBoundingClientRect().top - sc.getBoundingClientRect().top + sc.scrollTop - offset;
                      sc.scrollTo({ top: Math.max(0, top), behavior: 'smooth' });
                    } else {
                      window.scrollTo({ top: last.getBoundingClientRect().top + window.scrollY - offset, behavior: 'smooth' });
                    }
                    last.classList.add('card-flash');
                    setTimeout(() => last.classList.remove('card-flash'), 900);
                  }, 80);
                }}
                title={isAdded ? "Already added" : "Add to prep"}>
                <span className="spark-idea-status">
                  {isAdded
                    ? <window.Icon name="check" size={13} sw={2.8} />
                    : <window.Icon name="plus" size={14} sw={2.2} />}
                </span>
                <span className="spark-idea-txt">
                  <strong>{preview(idea)}</strong>
                  {detail(idea) && <em>{detail(idea)}</em>}
                </span>
                {isAdded && <span className="spark-added-label">Added</span>}
              </button>
            );
          })}
          {ideas.length > 0 && ideas.some((_, i) => !added.has(i)) &&
            <button type="button" className="spark-addall"
              onClick={() => {
                const remaining = ideas.filter((_, i) => !added.has(i));
                remaining.forEach(onAdd);
                setAdded(new Set(ideas.map((_, i) => i)));
              }}>
              Add all remaining {ideas.filter((_, i) => !added.has(i)).length}
            </button>
          }
        </div>
      }
    </div>
  );
}

// ── Import items from another session ────────────────────────────────────────
function ImportModal({ step, sessions, sessionId, onImport, onClose }) {
  const others = sessions.filter((s) => s.id !== sessionId);
  const [fromId, setFromId] = React.useState(others[others.length - 1]?.id || "");
  const [selected, setSelected] = React.useState(new Set());

  const fromSess = others.find((s) => s.id === fromId);
  const candidates = (fromSess?.data[step.id] || []).filter(
    (it) => it.title?.trim() || it.body?.trim() || it.aspects?.length
  );

  React.useEffect(() => setSelected(new Set()), [fromId]);

  const toggle = (id) => setSelected((prev) => {
    const next = new Set(prev);
    if (next.has(id)) next.delete(id); else next.add(id);
    return next;
  });

  const allSelected = candidates.length > 0 && candidates.every((it) => selected.has(it.id));
  const toggleAll = () => setSelected(allSelected ? new Set() : new Set(candidates.map((it) => it.id)));

  const doImport = () => {
    const toAdd = candidates
      .filter((it) => selected.has(it.id))
      .map((it) => ({ ...it, id: window.RPG.uid() }));
    onImport(toAdd);
    onClose();
  };

  return (
    <window.Modal title={`Import ${step.itemNoun}s`} onClose={onClose} width={500}>
      <div className="import-modal">
        <div className="import-from">
          <label className="import-from-label">From session</label>
          <select className="import-select" value={fromId} onChange={(e) => setFromId(e.target.value)}>
            {others.map((s) => (
              <option key={s.id} value={s.id}>#{s.number} — {s.name}</option>
            ))}
          </select>
        </div>

        {candidates.length === 0 ? (
          <p className="import-empty">No {step.itemNoun}s in that session.</p>
        ) : (
          <>
            <div className="import-list-hd">
              <span className="import-list-hd-count">{candidates.length} {step.itemNoun}{candidates.length !== 1 ? "s" : ""}</span>
              <button type="button" className="import-selall" onClick={toggleAll}>
                {allSelected ? "Deselect all" : "Select all"}
              </button>
            </div>
            <div className="import-list">
              {candidates.map((it) => (
                <label key={it.id} className={`import-row${selected.has(it.id) ? " sel" : ""}`}>
                  <input type="checkbox" checked={selected.has(it.id)} onChange={() => toggle(it.id)} />
                  <span className="import-row-main">
                    <span className="import-row-name">{it.title || it.body || "(untitled)"}</span>
                    {it.sub && <span className="import-row-sub">{it.sub}</span>}
                  </span>
                </label>
              ))}
            </div>
          </>
        )}

        <div className="import-footer">
          <button type="button" className="btn btn-sm" onClick={onClose}>Cancel</button>
          <button type="button" className="btn btn-sm btn-accent"
            disabled={selected.size === 0} onClick={doImport}>
            Import {selected.size > 0 ? selected.size : ""} selected
          </button>
        </div>
      </div>
    </window.Modal>
  );
}

// ── Image upload / generation modal ──────────────────────────────────────────
const IMG_STYLES = [
  { value: '', label: 'Default' },
  { value: 'fantasy art style', label: 'Fantasy Art' },
  { value: 'detailed watercolor illustration', label: 'Watercolor' },
  { value: 'oil painting style', label: 'Oil Painting' },
  { value: 'pencil sketch', label: 'Pencil Sketch' },
  { value: 'dark gritty concept art', label: 'Dark / Gritty' },
  { value: 'pixel art style', label: 'Pixel Art' },
];

const IMG_RATIOS = [
  { value: '1024x1024', label: 'Square (1:1)' },
  { value: '1024x1536', label: 'Portrait (2:3)' },
  { value: '1536x1024', label: 'Landscape (3:2)' },
];

function ImageModal({ item, step, ctx, onUse, onClose }) {
  const [tab, setTab] = React.useState('generate');
  const [prompt, setPrompt] = React.useState(() => {
    const parts = [item.title, item.sub, item.body].filter(Boolean);
    const content = parts.join(' — ');
    const contextStr = [ctx.campaign, ctx.tone].filter(Boolean).join(', ');
    return `Fantasy tabletop RPG illustration: ${content}${contextStr ? ` (${contextStr})` : ''}. No text, no words, no letters, no labels.`;
  });
  const [imgStyle, setImgStyle] = React.useState('');
  const [imgRatio, setImgRatio] = React.useState('1024x1024');
  const [genState, setGenState] = React.useState('idle');
  const [genImage, setGenImage] = React.useState(null);
  const [genError, setGenError] = React.useState('');
  const [uploadPreview, setUploadPreview] = React.useState(null);
  const [dragOver, setDragOver] = React.useState(false);
  const fileRef = React.useRef();

  const generate = async () => {
    setGenState('loading');
    setGenError('');
    try {
      const base = imgStyle ? `${prompt}, ${imgStyle}` : prompt;
      const fullPrompt = `${base} No text, no words, no letters, no labels.`;
      const uri = await window.imageGen.generate(fullPrompt, { size: imgRatio });
      setGenImage(uri);
      setGenState('done');
    } catch (e) {
      setGenError(e.message || 'Generation failed — try again.');
      setGenState('error');
    }
  };

  const handleFile = (file) => {
    if (!file || !file.type.startsWith('image/')) return;
    const reader = new FileReader();
    reader.onload = (e) => setUploadPreview(e.target.result);
    reader.readAsDataURL(file);
  };

  const onDrop = (e) => {
    e.preventDefault();
    setDragOver(false);
    handleFile(e.dataTransfer.files[0]);
  };

  return (
    <window.Modal title={`Image — ${item.title || step.itemNoun}`} onClose={onClose} width={520}>
      <div className="img-tabs">
        <button className={`img-tab${tab === 'generate' ? ' active' : ''}`} onClick={() => setTab('generate')}>
          <window.Icon name="sparkles" size={14} /> Generate
        </button>
        <button className={`img-tab${tab === 'upload' ? ' active' : ''}`} onClick={() => setTab('upload')}>
          <window.Icon name="import" size={14} /> Upload
        </button>
      </div>

      {tab === 'generate' && (
        <div className="img-form">
          <div>
            <label className="img-prompt-label">Image prompt</label>
            <textarea className="img-prompt" value={prompt} rows={3}
              placeholder="Describe the image — style, subject, mood…"
              onChange={(e) => setPrompt(e.target.value)} />
          </div>
          <div className="img-selects">
            <div className="img-select-wrap">
              <label className="img-prompt-label">Style</label>
              <select className="img-select" value={imgStyle} onChange={(e) => setImgStyle(e.target.value)}>
                {IMG_STYLES.map((s) => <option key={s.value} value={s.value}>{s.label}</option>)}
              </select>
            </div>
            <div className="img-select-wrap">
              <label className="img-prompt-label">Aspect ratio</label>
              <select className="img-select" value={imgRatio} onChange={(e) => setImgRatio(e.target.value)}>
                {IMG_RATIOS.map((r) => <option key={r.value} value={r.value}>{r.label}</option>)}
              </select>
            </div>
          </div>
          {genState !== 'done' && (
            <div className="img-row">
              <button type="button" className="btn btn-accent btn-sm"
                disabled={genState === 'loading' || !prompt.trim()}
                onClick={generate}>
                {genState === 'loading'
                  ? <><span style={{width:13,height:13,border:'2px solid rgba(255,255,255,0.35)',borderTopColor:'white',borderRadius:'50%',animation:'spin 0.7s linear infinite',display:'inline-block',verticalAlign:'middle',marginRight:6}} />Generating…</>
                  : <><window.Icon name="sparkles" size={14} /> Generate</>}
              </button>
              {genState === 'error' && <span className="img-error">{genError}</span>}
            </div>
          )}
          {genState === 'done' && genImage && (
            <div className="img-preview-wrap">
              <img src={genImage} alt="Generated preview" className="img-preview" />
              <div className="img-preview-actions">
                <button type="button" className="btn btn-ghost btn-sm"
                  onClick={() => { setGenState('idle'); setGenImage(null); }}>
                  Regenerate
                </button>
                <button type="button" className="btn btn-accent btn-sm"
                  onClick={() => { onUse(genImage); onClose(); }}>
                  Use this
                </button>
              </div>
            </div>
          )}
        </div>
      )}

      {tab === 'upload' && (
        <div className="img-form">
          {!uploadPreview ? (
            <div
              className={`img-dropzone${dragOver ? ' drag-over' : ''}`}
              onClick={() => fileRef.current && fileRef.current.click()}
              onDragOver={(e) => { e.preventDefault(); setDragOver(true); }}
              onDragLeave={() => setDragOver(false)}
              onDrop={onDrop}>
              <input ref={fileRef} type="file" accept="image/*" style={{ display: 'none' }}
                onChange={(e) => handleFile(e.target.files[0])} />
              <window.Icon name="import" size={28} sw={1.4} />
              <span className="img-dropzone-label">Click or drop an image</span>
              <span className="img-dropzone-hint">PNG, JPG, WebP — large files may fill local storage</span>
            </div>
          ) : (
            <div className="img-preview-wrap">
              <img src={uploadPreview} alt="Upload preview" className="img-preview" />
              <div className="img-preview-actions">
                <button type="button" className="btn btn-ghost btn-sm"
                  onClick={() => setUploadPreview(null)}>
                  Choose different
                </button>
                <button type="button" className="btn btn-accent btn-sm"
                  onClick={() => { onUse(uploadPreview); onClose(); }}>
                  Use this
                </button>
              </div>
            </div>
          )}
        </div>
      )}
    </window.Modal>
  );
}

// ── Step editor (the whole right pane for the active step) ────────────────────
function StepEditor({ step, items, setItems, ctx, sessions, sessionId }) {
  const [spark, setSpark] = React.useState(false);
  const [importOpen, setImportOpen] = React.useState(false);
  const [thinkingId, setThinkingId] = React.useState(null);
  const [imageModalId, setImageModalId] = React.useState(null);
  const imageModalItem = imageModalId ? items.find((it) => it.id === imageModalId) || null : null;
  const dragIndex = React.useRef(null);
  const [dragOver, setDragOver] = React.useState(null);

  const canImport = step.kind !== "single" && (sessions || []).filter((s) => s.id !== sessionId).length > 0;
  const doImport = (newItems) => setItems([...items, ...newItems]);

  const patch = (id, changes) => setItems(items.map((it) => it.id === id ? { ...it, ...changes } : it));
  const remove = (id) => setItems(items.filter((it) => it.id !== id));
  const duplicate = (it) => {
    const copy = { ...it, id: window.RPG.uid() };
    const idx = items.findIndex((x) => x.id === it.id);
    const next = items.slice(); next.splice(idx + 1, 0, copy); setItems(next);
  };
  const addBlank = () => {
    const fresh = { id: window.RPG.uid() };
    if (step.revealable) fresh.revealed = false;
    setItems([...items, fresh]);
    requestAnimationFrame(() => {
      const el = document.querySelector(".prep-card:last-of-type input, .prep-card:last-of-type textarea");
      if (el) el.focus();
    });
  };
  const makeItem = (idea) => {
    const fresh = { id: window.RPG.uid(), ...idea };
    if (step.revealable) fresh.revealed = false;
    return fresh;
  };
  const addIdea = (idea) => setItems([...items, makeItem(idea)]);
  const addIdeas = (arr) => setItems([...items, ...arr.map(makeItem)]);

  const rewrite = async (it) => {
    setThinkingId(it.id);
    try {
      const changes = await rewriteContent(step, it, ctx);
      if (changes && Object.keys(changes).length) patch(it.id, changes);
    } catch (e) { /* leave the draft untouched */ } finally {
      setThinkingId(null);
    }
  };

  const onDrop = (toIdx) => {
    const from = dragIndex.current;
    if (from == null || from === toIdx) { setDragOver(null); dragIndex.current = null; return; }
    const next = items.slice();
    const [moved] = next.splice(from, 1);
    next.splice(toIdx, 0, moved);
    setItems(next); setDragOver(null); dragIndex.current = null;
  };

  // SINGLE (strong start)
  if (step.kind === "single") {
    const item = items[0] || { id: window.RPG.uid() };
    const ensure = (changes) => {
      if (items.length === 0) setItems([{ ...item, ...changes }]);
      else patch(item.id, changes);
    };
    return (
      <div className="step-pane">
        <StepHeader step={step} count={null} onSpark={() => setSpark((s) => !s)} sparkOn={spark} />
        {spark && <SparkPanel step={step} ctx={{ ...ctx, items }} onClose={() => setSpark(false)}
          onAdd={(idea) => { ensure({ body: idea.body || idea.title }); setSpark(false); }}
          onAddMany={(arr) => { if (arr[0]) ensure({ body: arr[0].body || arr[0].title }); setSpark(false); }} />}
        <div className="single-start">
          <span className="drop-cap-rule" />
          <window.EditableArea value={item.body} minRows={4}
            placeholder="Open in the action. One vivid scene that drops the table straight into the world…"
            onChange={(v) => ensure({ body: v })} className="start-text" />
        </div>
      </div>
    );
  }

  // LIST — secrets get a compact row design; everything else uses PrepCard
  const isSecrets = step.id === "secrets";
  return (
    <div className="step-pane">
      <StepHeader step={step} count={items.length} onSpark={() => setSpark((s) => !s)} sparkOn={spark}
        onImport={canImport ? () => setImportOpen(true) : null} />
      {spark && <SparkPanel step={step} ctx={{ ...ctx, items }} onClose={() => setSpark(false)} onAdd={addIdea} onAddMany={addIdeas} />}
      {importOpen && <ImportModal step={step} sessions={sessions} sessionId={sessionId}
        onImport={doImport} onClose={() => setImportOpen(false)} />}
      {imageModalItem && (
        <ImageModal
          item={imageModalItem}
          step={step}
          ctx={ctx}
          onUse={(uri) => patch(imageModalId, { image: uri })}
          onClose={() => setImageModalId(null)} />
      )}

      {isSecrets ? (
        <>
          {items.length === 0 ? (
            <div className="empty-step">
              <window.Icon name={step.icon} size={30} sw={1.3} />
              <p>No secrets yet.</p>
              <span>Add one below or spark some ideas.</span>
            </div>
          ) : (
            <ul className="secret-list">
              {items.map((it, i) => (
                <SecretRow key={it.id} item={it} index={i}
                  patch={(c) => patch(it.id, c)}
                  remove={() => remove(it.id)}
                  onRewrite={() => rewrite(it)}
                  thinking={thinkingId === it.id} />
              ))}
            </ul>
          )}
          <div className="add-row">
            <button type="button" className="add-card" onClick={addBlank}>
              <window.Icon name="plus" size={16} sw={2.2} /> Add secret
            </button>
            {items.length > 0 && items.length < 10 && (
              <span className="add-hint">{10 - items.length} more to reach the classic ten.</span>
            )}
          </div>
        </>
      ) : (
        <>
          <div className="card-list">
            {items.map((it, i) => (
              <div key={it.id}
                className={dragOver === i ? "drop-target" : ""}
                onDragOver={(e) => { e.preventDefault(); if (dragOver !== i) setDragOver(i); }}
                onDrop={() => onDrop(i)}>
                <PrepCard
                  step={step} item={it} index={i}
                  patch={(c) => patch(it.id, c)}
                  remove={() => remove(it.id)}
                  duplicate={() => duplicate(it)}
                  onRewrite={() => rewrite(it)}
                  onImageOpen={() => setImageModalId(it.id)}
                  thinking={thinkingId === it.id}
                  isDragging={dragIndex.current === i}
                  dragProps={{
                    draggable: true,
                    onDragStart: (e) => { dragIndex.current = i; e.dataTransfer.effectAllowed = "move"; },
                    onDragEnd: () => { dragIndex.current = null; setDragOver(null); }
                  }} />
              </div>
            ))}
          </div>

          {items.length === 0 && (
            <div className="empty-step">
              <window.Icon name={step.icon} size={30} sw={1.3} />
              <p>No {step.itemNoun}s yet.</p>
              <span>Add one by hand, or let the muse spark a few.</span>
            </div>
          )}

          <div className="add-row">
            <button type="button" className="add-card" onClick={addBlank}>
              <window.Icon name="plus" size={16} sw={2.2} /> Add {step.itemNoun}
            </button>
          </div>
        </>
      )}
    </div>
  );
}

function StepHeader({ step, count, onSpark, sparkOn, onImport }) {
  return (
    <div className="step-head">
      <div className="step-head-l">
        <h2 className="step-title">
          {step.name}
          {count != null && <span className="step-count">{count}</span>}
        </h2>
        <p className="step-tagline">{step.tagline}</p>
      </div>
      <div className="step-head-r">
        {onImport && (
          <button type="button" className="import-btn" onClick={onImport}>
            <window.Icon name="import" size={15} /> Import
          </button>
        )}
        <button type="button" className={`spark-btn${sparkOn ? " on" : ""}`} onClick={onSpark}>
          <window.Icon name="sparkles" size={16} /> Spark ideas
        </button>
      </div>
    </div>
  );
}

Object.assign(window, { StepEditor, CardFields, sparkIdeas, ImageModal });
