/* 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). */

const REWRITE_MODES = [
  { id: "polish",   label: "Polish",      icon: "sparkles", desc: "Clean up the wording, keep the same idea" },
  { id: "ideate",   label: "Ideate",      icon: "spark",    desc: "Try a different take on this entry" },
  { id: "simplify", label: "Simplify",    icon: "scroll",   desc: "Trim it down, cut anything redundant" },
  { id: "expand",   label: "Expand",      icon: "plus",     desc: "Flesh it out with more usable detail" },
  { id: "tone",     label: "Adjust tone", icon: "spark",    desc: "Shift the mood or feel (describe below)" },
];

// ── 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 ideas in this direction: "${steer.trim()}".`
    : "";

  const prompt = `You are helping a GM prep a tabletop RPG session. Write in a natural, practical style. These are working notes, not published prose. Stay system-neutral. Do not use em dashes (—) anywhere in your output.

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

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

Generate ${n} ideas for this step that fit this specific campaign. Keep them concrete and usable at the table, not generic. Where it makes sense, tie one to a player character.${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: revise an existing entry with a chosen mode and optional direction ────
async function rewriteContent(step, item, ctx, mode = "polish", 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 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 instructions = {
    polish:   `Clean up the wording and fix any awkward phrasing. Keep the same idea and proper names. Don't change what it is, just make it read more naturally. Fill in any empty fields if you can infer something reasonable.`,
    simplify: `Trim it down. Remove anything redundant or wordy. Keep every concrete detail, proper name, and the core idea. Don't add anything new.`,
    expand:   `Add more usable detail. Something concrete the GM can actually use at the table. Keep the same idea and proper names. Fill in any empty fields.`,
    ideate:   `Come up with a different take on this entry that still fits the same prep slot. You can change the specifics, but keep it grounded in this campaign.`,
    tone:     `Rewrite this entry to shift the mood as the GM describes below. Keep the same facts and proper names. Just change the feel of it.`,
  };
  const instruction = instructions[mode] || instructions.polish;

  const steerLine = steer && steer.trim()
    ? `\n\nThe GM also wants: "${steer.trim()}".`
    : "";

  const prompt = `You are helping a GM revise one prep entry for a tabletop RPG session. Write in a natural, practical style. These are working notes, not published prose. Do not use em dashes (—) anywhere in your output.

Campaign: "${ctx.campaign}"${ctx.blurb ? ` (${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)}

${instruction}${steerLine}

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, onCardLinkClick, cardIndex }) {
  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} wrap 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 })}
      onCardLinkClick={onCardLinkClick} cardIndex={cardIndex} />;
  });
}

// ── 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, dragProps, isDragging, onRewrite, thinking, onImageOpen, onCardLinkClick, cardIndex, highlighted }) {
  const [lightboxOpen, setLightboxOpen] = React.useState(false);
  const [blobUrl, setBlobUrl] = React.useState(null);
  const [expanded, setExpanded] = React.useState(false);

  React.useEffect(() => {
    if (!lightboxOpen || !item.image) return;
    // Card images are data URIs offline but become Supabase storage URLs once
    // synced — only the former needs (and can be) converted to a blob URL.
    const isData = item.image.startsWith("data:");
    const url = isData ? dataUriToBlobUrl(item.image) : item.image;
    setBlobUrl(url);
    return () => { if (isData) 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]);

  const imageSize = item.imageSize || "thumb";

  const SIZE_OPTS = [["thumb","S","Small"],["medium","M","Medium"],["full","L","Large"],["hidden","—","Hidden (expanded only)"]];

  const imgControls = (
    <div className="card-image-controls" onClick={(e) => e.stopPropagation()}>
      <div className="card-image-sizes">
        {SIZE_OPTS.map(([s,label,ttl]) => (
          <button key={s} type="button"
            className={`card-img-size-btn${imageSize===s?" active":""}`}
            onClick={() => patch({ imageSize: s })} title={ttl}>{label}</button>
        ))}
      </div>
      <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>
  );

  const imgThumb = item.image && imageSize !== "hidden" ? (
    <div className="card-image-thumb" onClick={() => setLightboxOpen(true)}>
      <img src={item.image} alt="" loading="lazy" />
      {imgControls}
    </div>
  ) : null;

  return (
    <div className={`prep-card${isDragging ? " dragging" : ""}${thinking ? " thinking" : ""}${highlighted ? " card-highlight" : ""}`}>
      {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="expand" title="Expand card" onClick={() => setExpanded(true)} />
        <window.IconButton name="image" title="Add / change image" onClick={onImageOpen} active={!!item.image} />
        <window.IconButton name="sparkles" title="Rewrite with AI" onClick={onRewrite} />
        <window.ConfirmDelete onConfirm={remove} label="Delete card" />
      </div>
      <div className={`card-content-area${item.image && imageSize !== "hidden" ? ` pci--${imageSize}` : ""}`}>
        {item.image && imageSize === "medium" ? (
          <>
            <div className="card-m-title">
              <window.CardFields step={{...step, fields: step.fields.slice(0, 1)}} item={item} patch={patch} onCardLinkClick={onCardLinkClick} cardIndex={cardIndex} />
            </div>
            <div className="card-m-body-row">
              <div className="card-main">
                <window.CardFields step={{...step, fields: step.fields.slice(1)}} item={item} patch={patch} onCardLinkClick={onCardLinkClick} cardIndex={cardIndex} />
              </div>
              {imgThumb}
            </div>
          </>
        ) : (
          <>
            {imgThumb}
            <div className="card-main">
              <window.CardFields step={step} item={item} patch={patch} onCardLinkClick={onCardLinkClick} cardIndex={cardIndex} />
            </div>
          </>
        )}
      </div>
      {lightboxOpen && blobUrl && (
        <div className="img-lightbox" onClick={() => setLightboxOpen(false)}>
          <img src={blobUrl} alt="" className="img-lightbox-img" onClick={(e) => e.stopPropagation()} />
        </div>
      )}
      {expanded && (
        <window.Modal title={item.title || "Untitled"} onClose={() => setExpanded(false)} width={620}>
          <div className="card-expand-body">
            {item.image && (
              <div className="card-expand-image-wrap">
                <div className="card-expand-image" onClick={() => setLightboxOpen(true)} title="Tap to view full size">
                  <img src={item.image} alt="" loading="lazy" />
                </div>
                <div className="card-expand-img-controls">
                  <div className="card-image-sizes">
                    {SIZE_OPTS.map(([s,label,ttl]) => (
                      <button key={s} type="button"
                        className={`card-img-size-btn${imageSize===s?" active":""}`}
                        onClick={() => patch({ imageSize: s })} title={ttl}>{label}</button>
                    ))}
                  </div>
                  <button type="button" className="card-image-remove" title="Remove image"
                    onClick={() => { patch({ image: null }); setExpanded(false); }}>
                    <window.Icon name="x" size={11} sw={2.4} />
                  </button>
                </div>
              </div>
            )}
            <window.CardFields step={{ ...step, fields: step.fields.filter(f => f.key !== "title") }} item={item} patch={patch} onCardLinkClick={onCardLinkClick} cardIndex={cardIndex} />
          </div>
        </window.Modal>
      )}
    </div>
  );
}

// ── AI suggestion strip ────────────────────────────────────────────────────────
function SparkPanel({ step, ctx, onAdd, onAddMany, onClose, containerRef }) {
  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(e.message || "Nothing came back. 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="Steer the ideas: a theme, a character, a twist to build on (optional)"
          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}
          {/api key/i.test(err) && (
            <> — <button type="button" className="spark-settings-link"
              onClick={() => window.postMessage({ type: '__activate_edit_mode' }, '*')}>
              Open Settings
            </button></>
          )}
        </div>
      )}
      {state === "ok" &&
        <div className="spark-list">
          {ideas.length === 0 && <div className="spark-empty">No ideas came back. Try sparking again.</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(() => {
                    // Scope to this step's pane — in board layout every step's list
                    // is in the document, so a bare querySelector hits the wrong one.
                    const scope = (containerRef && containerRef.current) || document;
                    const list = scope.querySelector('.card-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 && step.kind !== "single" && ideas.some((_, i) => !added.has(i)) &&
            <button type="button" className="spark-addall"
              onClick={() => {
                const remaining = ideas.filter((_, i) => !added.has(i));
                onAddMany(remaining);
                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: 'pen and ink illustration', label: 'Pen & Ink' },
  { value: 'pixel art style', label: 'Pixel Art' },
  { value: 'digital painting', label: 'Digital Painting' },
];

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>
  );
}

// ── Rewrite with AI modal: mode picker → generate → before/after preview ──────
function RewriteModal({ step, item, ctx, onApply, onClose }) {
  const [mode, setMode] = React.useState("polish");
  const [steer, setSteer] = React.useState("");
  const steerRef = React.useRef("");
  const [state, setState] = React.useState("idle");
  const [result, setResult] = React.useState(null);
  const [err, setErr] = React.useState("");

  const selectMode = (m) => {
    setMode(m);
    if (state !== "loading") { setState("idle"); setResult(null); }
  };

  const run = React.useCallback(async () => {
    setState("loading"); setErr("");
    try {
      const obj = await rewriteContent(step, item, ctx, mode, steerRef.current);
      setResult(obj); setState("done");
    } catch (e) {
      setErr(e.message || "Nothing came back. Try again."); setState("error");
    }
  }, [step, item, ctx, mode]);

  const fieldRows = React.useMemo(() => {
    if (!result) return [];
    return step.fields.map((f) => {
      const fmt = (v) => f.kind === "aspects"
        ? (Array.isArray(v) ? v.join(" · ") : "")
        : (v == null ? "" : String(v));
      const before = fmt(item[f.key]);
      const after = result[f.key] !== undefined ? fmt(result[f.key]) : undefined;
      return { key: f.key, label: f.label, before, after, present: after !== undefined };
    }).filter((r) => r.present && (r.before || r.after));
  }, [result, step, item]);

  const hasChanges = fieldRows.some((r) => r.before !== r.after);

  const applyChanges = () => {
    const out = {};
    step.fields.forEach((f) => { if (result && result[f.key] !== undefined) out[f.key] = result[f.key]; });
    onApply(out);
  };

  const title = item.title || (item.body || "").slice(0, 40) || step.itemNoun || "entry";
  const steerPlaceholder = mode === "tone"
    ? "How should the tone change? (e.g. darker, more comedic, unsettling)"
    : "Anything specific you want? Leave blank to let the mode decide.";

  return (
    <window.Modal title={`Rewrite: ${title}`} onClose={onClose} width={560}>
      <div className="rw-modes">
        {REWRITE_MODES.map((m) => (
          <button key={m.id} type="button"
            className={`rw-mode-chip${mode === m.id ? " on" : ""}`}
            title={m.desc}
            onClick={() => selectMode(m.id)}>
            <window.Icon name={m.icon} size={14} />
            {m.label}
          </button>
        ))}
      </div>

      <div className="spark-steer">
        <input className="spark-steer-input" value={steer}
          placeholder={steerPlaceholder}
          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" ? "…" : "Generate"}
        </button>
      </div>

      {state === "idle" && (
        <div className="rw-empty">Choose a mode above, then hit Generate. You'll see the changes before anything is applied.</div>
      )}
      {state === "loading" && (
        <div className="rw-loading"><span className="spin" /> Rewriting…</div>
      )}
      {state === "error" && (
        <div className="rw-empty">
          {err}
          {/api key/i.test(err) && (
            <> — <button type="button" className="spark-settings-link"
              onClick={() => window.postMessage({ type: '__activate_edit_mode' }, '*')}>
              Open Settings
            </button></>
          )}
        </div>
      )}
      {state === "done" && (
        <div className="rw-preview">
          {fieldRows.map((r) => (
            <div key={r.key} className={`rw-field${r.before === r.after ? " unchanged" : ""}`}>
              <div className="rw-field-label">
                {r.label}
                {r.before === r.after && <span className="rw-field-tag">unchanged</span>}
              </div>
              <div className="rw-diff">
                <div className="rw-before">{r.before || <em style={{ opacity: 0.4 }}>empty</em>}</div>
                {r.before !== r.after && <div className="rw-after">{r.after || <em style={{ opacity: 0.4 }}>empty</em>}</div>}
              </div>
            </div>
          ))}
        </div>
      )}

      {state === "done" && (
        <div className="rw-footer">
          <button type="button" className="btn btn-sm" onClick={onClose}>Cancel</button>
          <button type="button" className="btn btn-ghost btn-sm" onClick={run}>Regenerate</button>
          <button type="button" className="btn btn-accent btn-sm" onClick={applyChanges} disabled={!hasChanges}>Apply</button>
        </div>
      )}
    </window.Modal>
  );
}

// ── Step editor (the whole right pane for the active step) ────────────────────
function StepEditor({ step, items, setItems, ctx, sessions, sessionId, onCardLinkClick, cardIndex, highlightedCard }) {
  const [spark, setSpark] = React.useState(false);
  const [importOpen, setImportOpen] = React.useState(false);
  const [thinkingId, setThinkingId] = React.useState(null);
  const [rewriteId, setRewriteId] = React.useState(null);
  const rewriteItem = rewriteId ? items.find((it) => it.id === rewriteId) || null : null;
  const [imageModalId, setImageModalId] = React.useState(null);
  const imageModalItem = imageModalId ? items.find((it) => it.id === imageModalId) || null : null;
  // Unified pointer-based drag (mouse + touch). We deliberately avoid native
  // HTML5 drag-and-drop: live-reordering the list mid-drag relocates the
  // dragged DOM node, which cancels a native drag (drop never fires). Driving
  // everything from pointer events sidesteps that entirely.
  const [dragFrom, setDragFrom] = React.useState(null);
  const [dragOver, setDragOver] = React.useState(null);
  const [dragging, setDragging] = React.useState(false);
  const dragFromRef = React.useRef(null);
  const dragOverRef = React.useRef(null);
  const listRef = React.useRef(null);
  // Root of this step's pane. In board layout all 8 editors mount at once, so any
  // "find the last card" lookup MUST be scoped here, not to the whole document.
  const paneRef = React.useRef(null);
  const pending = React.useRef(null); // pre-drag gesture (long-press / grip grab)
  const longPressTimer = React.useRef(null);

  const hoverOver = (i) => { if (i == null) return; dragOverRef.current = i; setDragOver(i); };

  const beginDrag = (i) => {
    dragFromRef.current = i; dragOverRef.current = i;
    setDragFrom(i); setDragOver(i); setDragging(true);
    if (navigator.vibrate) navigator.vibrate(40);
  };

  const cancelDrag = () => {
    clearTimeout(longPressTimer.current);
    pending.current = null;
    dragFromRef.current = null; dragOverRef.current = null;
    setDragFrom(null); setDragOver(null); setDragging(false);
  };

  const commitDrop = () => {
    const from = dragFromRef.current;
    const to = dragOverRef.current;
    cancelDrag();
    if (from === null || to === null || from === to) return;
    const next = [...items];
    const [moved] = next.splice(from, 1);
    next.splice(to, 0, moved);
    setItems(next);
  };

  const findCardAtY = (y) => {
    const children = listRef.current ? Array.from(listRef.current.children) : [];
    for (let ci = 0; ci < children.length; ci++) {
      const rect = children[ci].getBoundingClientRect();
      if (y >= rect.top && y <= rect.bottom) return ci;
    }
    // past the ends: clamp to first/last
    if (children.length) {
      if (y < children[0].getBoundingClientRect().top) return 0;
      return children.length - 1;
    }
    return null;
  };

  const visualItems = React.useMemo(() => {
    if (dragFrom === null || dragOver === null || dragFrom === dragOver) return items;
    const next = [...items];
    const [moved] = next.splice(dragFrom, 1);
    next.splice(dragOver, 0, moved);
    return next;
  }, [items, dragFrom, dragOver]);

  // While a drag is active, listen on window so the pointer can roam anywhere
  // (and keep moving even if the captured node is re-rendered into a new slot).
  React.useEffect(() => {
    if (!dragging) return;
    const onMove = (e) => {
      e.preventDefault();
      hoverOver(findCardAtY(e.clientY));
    };
    const onUp = () => commitDrop();
    const onCancel = () => cancelDrag();
    const blockScroll = (e) => e.preventDefault();
    window.addEventListener('pointermove', onMove, { passive: false });
    window.addEventListener('pointerup', onUp);
    window.addEventListener('pointercancel', onCancel);
    document.addEventListener('touchmove', blockScroll, { passive: false });
    document.body.style.userSelect = 'none';
    return () => {
      window.removeEventListener('pointermove', onMove);
      window.removeEventListener('pointerup', onUp);
      window.removeEventListener('pointercancel', onCancel);
      document.removeEventListener('touchmove', blockScroll);
      document.body.style.userSelect = '';
    };
  }, [dragging]);

  // Pre-drag gesture ("arm" then commit). Mouse: press the grip handle, then
  // move to start dragging (a plain click never triggers a drag). Touch:
  // long-press anywhere on the card (cancelled if the finger scrolls first).
  const onCardPointerDown = (e, i) => {
    const tag = e.target.tagName;
    if (tag === 'INPUT' || tag === 'TEXTAREA') return;
    const onGrip = !!e.target.closest('.card-rail');
    if (e.pointerType === 'mouse') {
      if (!onGrip || e.button !== 0) return; // mouse drags only from the grip
      pending.current = { index: i, startX: e.clientX, startY: e.clientY, mouse: true };
      return;
    }
    if (tag === 'BUTTON' && !onGrip) return; // let card buttons work on touch
    pending.current = { index: i, startX: e.clientX, startY: e.clientY, mouse: false };
    longPressTimer.current = setTimeout(() => {
      if (!pending.current) return;
      beginDrag(pending.current.index);
      pending.current = null;
    }, 350);
  };

  const onCardPointerMove = (e) => {
    const p = pending.current;
    if (dragging || !p) return;
    const moved = Math.abs(e.clientX - p.startX) > 6 || Math.abs(e.clientY - p.startY) > 6;
    if (!moved) return;
    if (p.mouse) { beginDrag(p.index); pending.current = null; }   // mouse: move starts drag
    else { clearTimeout(longPressTimer.current); pending.current = null; } // touch: move = scroll, abort
  };

  const onCardPointerUp = () => {
    clearTimeout(longPressTimer.current);
    pending.current = null;
  };

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

  const patch = (id, changes) => {
    if ('image' in changes && !changes.image) {
      const old = items.find((it) => it.id === id);
      if (old && old.image) window.RPGStorage?.remove(old.image);
    }
    setItems(items.map((it) => it.id === id ? { ...it, ...changes } : it));
  };
  const remove = (id) => setItems(items.filter((it) => it.id !== id));
  const addBlank = () => {
    const fresh = { id: window.RPG.uid() };
    if (step.revealable) fresh.revealed = false;
    setItems([...items, fresh]);
    requestAnimationFrame(() => {
      const scope = paneRef.current || document;
      const cards = scope.querySelectorAll(".prep-card");
      const last = cards[cards.length - 1];
      if (!last) return;
      const el = last.querySelector(".card-title") || last.querySelector("input, 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 = (it) => setRewriteId(it.id);

  // 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" />
          <div style={{ flex: 1, minWidth: 0 }}>
            <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>
      </div>
    );
  }

  return (
    <div className="step-pane" ref={paneRef}>
      <StepHeader step={step} count={items.length} onSpark={() => setSpark((s) => !s)} sparkOn={spark}
        onImport={canImport ? () => setImportOpen(true) : null} />
      {spark && <SparkPanel step={step} ctx={{ ...ctx, items }} containerRef={paneRef} 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={async (uri) => {
            const url = window.RPGStorage ? await window.RPGStorage.ensureUploaded(uri) : uri;
            patch(imageModalId, { image: url });
          }}
          onClose={() => setImageModalId(null)} />
      )}
      {rewriteItem && (
        <RewriteModal
          step={step} item={rewriteItem} ctx={ctx}
          onApply={(changes) => {
            if (changes && Object.keys(changes).length) patch(rewriteId, changes);
            setRewriteId(null);
          }}
          onClose={() => setRewriteId(null)} />
      )}

      <div className="card-list" ref={listRef}>
        {visualItems.map((it, visIdx) => (
          <div key={it.id} id={`card-${it.id}`}
            onPointerDown={(e) => onCardPointerDown(e, visIdx)}
            onPointerMove={onCardPointerMove}
            onPointerUp={onCardPointerUp}>
            <PrepCard
              step={step} item={it} index={visIdx}
              patch={(c) => patch(it.id, c)}
              remove={() => remove(it.id)}
              onRewrite={() => rewrite(it)}
              onImageOpen={() => setImageModalId(it.id)}
              thinking={thinkingId === it.id}
              isDragging={dragging && it.id === items[dragFrom]?.id}
              dragProps={{ "data-drag-grip": true }}
              onCardLinkClick={onCardLinkClick} cardIndex={cardIndex}
              highlighted={!!(highlightedCard && highlightedCard.cardId === it.id)} />
          </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>
        {step.id === "secrets" && items.length > 0 && items.length < 10 && (
          <span className="add-hint">{10 - items.length} more to reach the classic ten.</span>
        )}
      </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, dataUriToBlobUrl });
