/* global React, ReactDOM */
// v07zz255 — Read-only deck REVIEW viewer (Edits-style inline layout): a cream MAIN
// panel scrolling the whole locked deck + a persistent RIGHT-HAND comment panel.
// Clicking an image SELECTS it → you comment in the right panel. A hover ⤢ button
// on each image opens it FULL SCREEN in the shared assets Lightbox (zoom + pan, no
// comment box). Comments post to the deck thread AND mirror to the source shot/asset
// Notes (the server resolves the image URL → entity, even for freeform images).
function PresentationDeckViewer({ presentationId, onClose }) {
  const fetcher = window.authFetch || fetch;
  // v07zz278 — hide the comment composer for roles without comment_on_shots
  // (the deck comment endpoint mirrors to shot/asset notes, gated on that
  // key). The existing comment thread stays readable.
  const canComment = !window.hasPerm || window.hasPerm("comment_on_shots");
  const [data, setData] = React.useState(null);
  const [loading, setLoading] = React.useState(true);
  const [error, setError] = React.useState(null);
  const [comments, setComments] = React.useState([]);
  const [target, setTarget] = React.useState(null);     // selected image → drawer composer
  const [zoomSrc, setZoomSrc] = React.useState(null);    // image open full-screen in the Lightbox
  const [draft, setDraft] = React.useState("");
  const [saving, setSaving] = React.useState(false);
  const [copied, setCopied] = React.useState(false);
  const [editingId, setEditingId] = React.useState(null);   // comment being edited
  const [editDraft, setEditDraft] = React.useState("");
  const [confirmDelId, setConfirmDelId] = React.useState(null);  // comment pending delete-confirm
  const [copiedId, setCopiedId] = React.useState(null);          // v965 — "Copied" flash on the Copy button
  const [replyingTo, setReplyingTo] = React.useState(null);      // v07zz426 — the comment a reply is being written for
  const [activeCommentId, setActiveCommentId] = React.useState(null);  // v07zz284 — note clicked → its image stays gold-outlined
  const [hoveredUrl, setHoveredUrl] = React.useState(null);            // v07zz291 — note hovered → its image gets a ring (no glow)
  const [slidesReady, setSlidesReady] = React.useState(false);   // deck images preloaded → fade the deck in
  const slideRefs = React.useRef({});
  const taRef = React.useRef(null);
  // v07zz279 — a user can edit/delete ONLY their OWN comment (no admin override) —
  // it's their words in a shared review thread. The server enforces the same.
  const _me = (typeof window !== "undefined" && window.__currentUser) || null;
  const canManage = (c) => !!(_me && c && String(_me.id) === String(c.user_id));
  // v07zz479 — ADMIN: replace a slide image afterwards (broken/renamed source,
  // or the moved-file fallback landed on the wrong successor). Select the image
  // (click it) → "⇄ Replace image" in the composer → pick from the slot's
  // entity's current images (or type any shot id / asset slug).
  const isAdmin = (window.__effectiveRole === "admin");
  const [swap, setSwap] = React.useState(null);            // {slide_key, url, entity_id, label} — slot being replaced
  const [swapCands, setSwapCands] = React.useState(null);  // null = not loaded, [] = none
  const [swapQuery, setSwapQuery] = React.useState("");
  const [swapBusy, setSwapBusy] = React.useState(false);
  const [swapErr, setSwapErr] = React.useState(null);
  const loadSwapCands = React.useCallback((q) => {
    const id = String(q || "").trim();
    if (!id) { setSwapCands([]); return; }
    setSwapCands(null); setSwapErr(null);
    fetcher(`/api/presentations/image-candidates?entity_id=${encodeURIComponent(id)}`)
      .then(r => r.ok ? r.json() : Promise.reject(new Error("HTTP " + r.status)))
      .then(j => setSwapCands((j && j.candidates) || []))
      .catch(e => { setSwapCands([]); setSwapErr(e.message); });
  }, [fetcher]);
  const openSwap = () => {
    if (!target) return;
    const q = target.entity_id || "";
    setSwap({ slide_key: target.slide_key, url: target.url, entity_id: target.entity_id, label: target.label });
    setSwapQuery(q);
    if (q) loadSwapCands(q); else setSwapCands([]);
  };
  const doSwap = (newUrl) => {
    if (!swap || swapBusy) return;
    setSwapBusy(true); setSwapErr(null);
    fetcher(`/api/presentations/${presentationId}/replace-image`, {
      method: "POST", headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ slide_key: swap.slide_key, old_url: swap.url, new_url: newUrl }),
    })
      .then(r => r.json().then(j => ({ ok: r.ok, j })).catch(() => ({ ok: r.ok, j: null })))
      .then(({ ok, j }) => {
        if (!ok) throw new Error((j && j.error) || "Replace failed");
        // Refetch the deck so the swapped image renders (and stays on reload).
        return fetcher(`/api/presentations/${presentationId}`).then(r => r.ok ? r.json() : null)
          .then(d => { if (d) setData(d); setSwap(null); setTarget(null); setActiveCommentId(null); });
      })
      .catch(e => setSwapErr(e.message))
      .finally(() => setSwapBusy(false));
  };

  React.useEffect(() => {
    let alive = true; setLoading(true); setError(null);
    fetcher(`/api/presentations/${presentationId}`)
      .then(r => r.ok ? r.json() : Promise.reject(new Error("HTTP " + r.status)))
      .then(j => { if (alive) { setData(j); setLoading(false); } })
      .catch(e => { if (alive) { setError(e.message); setLoading(false); } });
    return () => { alive = false; };
  }, [presentationId]);
  const loadComments = React.useCallback(() => {
    fetcher(`/api/presentation-comments?presentation_id=${presentationId}`)
      .then(r => r.ok ? r.json() : null).then(j => setComments((j && j.comments) || [])).catch(() => {});
  }, [presentationId]);
  React.useEffect(() => { loadComments(); }, [loadComments]);
  // Esc: close the full-screen viewer first, then clear the selection, then leave.
  React.useEffect(() => {
    const h = (e) => { if (e.key === "Escape") { if (zoomSrc) setZoomSrc(null); else if (target) { setTarget(null); setActiveCommentId(null); } else onClose(); } };
    document.addEventListener("keydown", h); return () => document.removeEventListener("keydown", h);
  }, [zoomSrc, target, onClose]);
  React.useEffect(() => { if (target && taRef.current) taRef.current.focus(); }, [target]);

  const deck = data && data.deck;
  const thumbU = (u, w) => (window.thumbUrl ? window.thumbUrl(u, w) : u);
  // v07zz284 — A comment stores the image URL from the env it was made in
  // (/local/… locally, the R2 cloud URL on Railway). To gold-outline the right
  // deck image regardless of env, resolve a stored image_url to the CURRENT
  // deck's matching cell URL: exact match first, else by filename (the file is
  // the same across envs, only the URL prefix differs).
  const _baseName = (u) => { try { return decodeURIComponent(String(u || "").split("?")[0].split("/").pop() || ""); } catch (_) { return String(u || "").split("?")[0].split("/").pop() || ""; } };
  const resolveDeckUrl = (imageUrl) => {
    if (!imageUrl || !deck) return imageUrl || null;
    const allUrls = [];
    (deck.order || []).forEach(k => { const s = (deck.slides || {})[k]; if (s && Array.isArray(s.imgs)) s.imgs.forEach(u => { if (u) allUrls.push(u); }); });
    if (allUrls.includes(imageUrl)) return imageUrl;          // exact (same env)
    const bn = _baseName(imageUrl);
    const hit = bn && allUrls.find(u => _baseName(u) === bn);  // env-robust by filename
    return hit || imageUrl;
  };
  // v07zz267 — preload every deck image (at both widths the slides request: 800 grid
  // cells / 1600 full-bleed) BEFORE revealing the deck, so the slides + images don't
  // pop in one by one. A spinner shows until all are loaded (or a 6s safety timeout),
  // then the whole deck fades in.
  // v07zz287 — idempotent reveal: key the preload off the deck's CONTENT (its
  // slide order), not the object identity. A re-render that produces a new `deck`
  // reference with the same content must NOT slam slidesReady false→true→false
  // again (that bounce re-fired the fade = a flicker). We early-return as already
  // ready when the content key is unchanged.
  const _preppedKey = React.useRef(null);
  React.useEffect(() => {
    if (!deck) { _preppedKey.current = null; setSlidesReady(false); return; }
    const key = (deck.order || []).join(",");
    if (_preppedKey.current === key) { setSlidesReady(true); return; }
    _preppedKey.current = key;
    setSlidesReady(false);
    const urls = new Set();
    (deck.order || []).forEach(k => { const s = (deck.slides || {})[k]; if (s && Array.isArray(s.imgs)) s.imgs.forEach(u => { if (u) urls.add(u); }); });
    const list = [...urls];
    if (!list.length) { setSlidesReady(true); return; }
    let done = false; const finish = () => { if (!done) { done = true; setSlidesReady(true); } };
    const proms = [];
    list.forEach(u => [800, 1600].forEach(w => { proms.push(new Promise(res => { const im = new Image(); im.onload = im.onerror = () => res(); im.src = thumbU(u, w); })); }));
    Promise.allSettled(proms).then(finish);
    const t = setTimeout(finish, 6000);
    return () => { done = true; clearTimeout(t); };
  }, [deck]);   // eslint-disable-line react-hooks/exhaustive-deps
  const _meta = (et) => et === "shot" ? { kind: "Shot", word: "shot" }
    : et === "asset_character" ? { kind: "Character", word: "character" }
    : et === "asset_location" ? { kind: "Location", word: "location" }
    : et === "asset_prop" ? { kind: "Prop", word: "prop" }
    : et ? { kind: "Asset", word: "asset" } : { kind: "Image", word: "image" };
  const imgSource = (slideKey, labels, url) => {
    if (labels && labels[url]) { const sid = labels[url]; return { entity_type: "shot", entity_id: sid, label: sid, kind: "Shot", word: "shot" }; }
    const src = (deck && deck.src_by_key && deck.src_by_key[slideKey]) || null;
    if (!src || !src.entity_type) return null;
    const m = _meta(src.entity_type);
    return { entity_type: src.entity_type, entity_id: src.entity_id, label: src.label || src.entity_id, kind: m.kind, word: m.word };
  };
  // v07zz425 — match by filename, not strict URL equality, so the per-cell comment-count badge
  // shows across environments (a comment made on Railway stores a cloud URL; the same deck opened
  // locally has /local cell URLs). Mirrors resolveDeckUrl / the gold-outline path.
  const commentsForImage = (url) => comments.filter(c => _baseName(c.image_url) === _baseName(url));
  const cellBadge = (key, url) => { const n = commentsForImage(url).length; return n > 0 ? <span className="pdv-cbadge" title={n + " comment" + (n === 1 ? "" : "s")}>{n}</span> : null; };

  // Click an image → select it for commenting in the right panel.
  const openComment = (slideKey, labels, url) => {
    const src = imgSource(slideKey, labels, url) || { entity_type: null, entity_id: null, label: "This image", kind: "Image", word: "image" };
    setActiveCommentId(null);   // clicking an image is a fresh selection, not a note
    setTarget({ slide_key: slideKey, url, ...src }); setDraft("");
  };
  // Hover ⤢ → open the image full screen in the shared assets Lightbox.
  const openZoom = (url) => setZoomSrc(thumbU(url, 1600));
  // Click a note → keep its image gold-outlined (resolve to the current deck URL)
  // and scroll it into view, so it's obvious which image the note refers to.
  const fromComment = (c) => {
    const m = _meta(c.entity_type);
    const url = resolveDeckUrl(c.image_url);
    setActiveCommentId(c.id);
    setTarget({ slide_key: c.slide_key, url, entity_type: c.entity_type || null, entity_id: c.entity_id || null, label: c.entity_id || "This image", kind: m.kind, word: m.word });
    const el = c.slide_key && slideRefs.current[c.slide_key];
    if (el && el.scrollIntoView) el.scrollIntoView({ behavior: "smooth", block: "center" });
  };
  const submit = () => {
    const body = draft.trim(); if (!body || saving) return;
    // v07zz426 — a REPLY inherits the parent comment's image/entity + flattens to its top-level
    // ancestor (1-level threads); a top-level comment still needs a selected image (target).
    const parent = replyingTo;
    const tgt = parent
      ? { slide_key: parent.slide_key, url: parent.image_url, entity_type: parent.entity_type, entity_id: parent.entity_id }
      : target;
    if (!parent && !tgt) return;
    const parentId = parent ? (parent.parent_comment_id || parent.id) : null;
    setSaving(true);
    fetcher("/api/presentation-comments", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ presentation_id: presentationId, slide_key: tgt && tgt.slide_key, image_url: tgt && tgt.url, entity_type: tgt && tgt.entity_type, entity_id: tgt && tgt.entity_id, body, parent_comment_id: parentId }) })
      .then(r => r.json().then(j => ({ ok: r.ok, j })).catch(() => ({ ok: r.ok, j: null })))
      .then(({ ok, j }) => { if (ok && j && j.id) { setComments(c => [...c, j]); setDraft(""); setReplyingTo(null); } })
      .catch(() => {}).finally(() => setSaving(false));
  };
  const onKey = (e) => { if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) { e.preventDefault(); submit(); } };
  const saveEdit = (c) => {
    const body = editDraft.trim(); if (!body) return;
    fetcher(`/api/presentation-comments/${c.id}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ body }) })
      .then(r => r.ok ? r.json() : null)
      .then(j => { setEditingId(null); setEditDraft(""); if (j && j.id) setComments(list => list.map(x => x.id === j.id ? j : x)); else loadComments(); })
      .catch(() => {});
  };
  const doDelete = (c) => {
    fetcher(`/api/presentation-comments/${c.id}`, { method: "DELETE" })
      .then(r => r.ok ? r.json() : null)
      .then(() => { setConfirmDelId(null); if (activeCommentId === c.id) { setActiveCommentId(null); setTarget(null); } setComments(list => list.filter(x => x.id !== c.id)); })
      .catch(() => {});
  };
  const share = () => {
    // Point the review link at the PUBLIC app (Railway), not Hugo's localhost.
    // The server hands us share_base = RAILWAY_URL when we're on local; on Railway
    // it's null, so we use this origin (which is already the public Railway URL).
    const base = (data && data.share_base) || location.origin;
    // 24 Sep 2026 - the link carries its project as the path (/trope?deck=4), src/projectUrl.js
    const url = `${base}${window.__projectUrl ? window.__projectUrl.appPath(window.__activeProjectId) : "/"}?deck=${presentationId}`;
    const done = () => { setCopied(true); setTimeout(() => setCopied(false), 1800); };
    try { (navigator.clipboard ? navigator.clipboard.writeText(url) : Promise.reject()).then(done).catch(() => { window.prompt && window.prompt("Copy this review link:", url); }); }
    catch (_) { try { window.prompt("Copy this review link:", url); } catch (e) {} }
  };

  // v07zz426 — one row renderer for both top-level comments and their nested replies.
  const renderComment = (c, isReply) => {
    const editing = editingId === c.id; const confirming = confirmDelId === c.id;
    return (
      <div key={c.id} className={"pdv-cmt" + (isReply ? " pdv-cmt--reply" : "") + (activeCommentId === c.id ? " is-active" : "") + (editing ? " is-editing" : "")}
        role="button" tabIndex={0}
        /* v965 — Hugo: "why cant i copy notes from the presentations??" The whole row is
           a button, so dragging across the text fired the row click and the browser threw
           the selection away. If the user has actually selected something, the click is
           theirs, not the row's. */
        onClick={() => {
          let sel = "";
          try { sel = String(window.getSelection ? window.getSelection().toString() : ""); } catch (_) {}
          if (sel.trim()) return;
          if (!editing && !confirming) fromComment(c);
        }}
        onMouseEnter={() => setHoveredUrl(resolveDeckUrl(c.image_url))} onMouseLeave={() => setHoveredUrl(null)}>
        {!isReply && (c.image_url ? <div className="pdv-cmt-thumb" style={{ backgroundImage: `url(${thumbU(c.image_url, 240)})` }} /> : <div className="pdv-cmt-thumb is-none" />)}
        <div className="pdv-cmt-main">
          {editing
            ? <textarea className="pdv-cmt-editinput" rows={2} value={editDraft} autoFocus onClick={e => e.stopPropagation()} onChange={e => setEditDraft(e.target.value)} onKeyDown={e => { if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) { e.preventDefault(); saveEdit(c); } if (e.key === "Escape") { e.stopPropagation(); setEditingId(null); } }} />
            : <div className="pdv-cmt-text">{c.body}</div>}
          <div className="pdv-cmt-meta">{c.user_name || "Someone"}{c.entity_id && !isReply ? ` · ${c.entity_id}` : ""} · {(c.created_at || "").slice(0, 10)}
            {canComment && !editing && !confirming && (
              <button type="button" className="pdv-cmt-reply" onClick={e => { e.stopPropagation(); setReplyingTo(c); setEditingId(null); setConfirmDelId(null); try { taRef.current && taRef.current.focus(); } catch (_) {} }}>↩ Reply</button>
            )}
          </div>
        </div>
        {/* v965 — Copy the note text with one click, for everyone (the edit/delete
            cluster below stays owner-only). */}
        <div className={"pdv-cmt-copywrap" + (canManage(c) ? " pdv-cmt-copywrap--owner" : "")} onClick={e => e.stopPropagation()}>
          <button type="button" className="pdv-cmt-act pdv-cmt-copy" title="Copy this note"
            aria-label="Copy this note"
            onClick={() => {
              const t = String(c.body || "");
              try {
                if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(t);
                else { const ta = document.createElement("textarea"); ta.value = t; document.body.appendChild(ta); ta.select(); document.execCommand("copy"); document.body.removeChild(ta); }
                setCopiedId(c.id); setTimeout(() => setCopiedId(v => (v === c.id ? null : v)), 1200);
              } catch (_) {}
            }}>{copiedId === c.id ? "Copied" : "Copy"}</button>
        </div>
        {canManage(c) && (
          <div className="pdv-cmt-actions" onClick={e => e.stopPropagation()}>
            {editing ? (<>
              <button type="button" className="pdv-cmt-act is-go" title="Save" onClick={() => saveEdit(c)}>Save</button>
              <button type="button" className="pdv-cmt-act" title="Cancel" onClick={() => { setEditingId(null); setEditDraft(""); }}>Cancel</button>
            </>) : confirming ? (<>
              <span className="pdv-cmt-confirm">Delete?</span>
              <button type="button" className="pdv-cmt-act is-danger" title="Confirm delete" onClick={() => doDelete(c)}>Yes</button>
              <button type="button" className="pdv-cmt-act" title="Cancel" onClick={() => setConfirmDelId(null)}>No</button>
            </>) : (<>
              <button type="button" className="pdv-cmt-act" title="Edit comment" aria-label="Edit comment" onClick={() => { setEditingId(c.id); setEditDraft(c.body); setConfirmDelId(null); }}>✎</button>
              <button type="button" className="pdv-cmt-act" title="Delete comment" aria-label="Delete comment" onClick={() => { setConfirmDelId(c.id); setEditingId(null); }}>✕</button>
            </>)}
          </div>
        )}
      </div>
    );
  };

  const ctx = deck && window.PresHelpers && window.PresHelpers.buildViewCtx
    ? window.PresHelpers.buildViewCtx(deck, { mode: "view", hiRes: true, onImageClick: openComment, onImageZoom: openZoom, imgSource, cellBadge, selectedUrl: target && target.url, hoveredUrl, thumbU })
    : null;
  const tplOf = (key) => { const maps = (deck && deck.maps) || {}; return (maps.slideTemplate && maps.slideTemplate[key]) || (deck && deck.template) || "dark-cinematic"; };
  const slidesArr = deck ? (deck.order || []).map(k => (deck.slides || {})[k]).filter(Boolean) : [];
  const noDeck = !loading && !error && deck === null;
  const pdfLink = data && data.open_url ? <a href={data.open_url} target="_blank" rel="noopener noreferrer"> Open the PDF ↗</a> : null;
  const LB = window.Lightbox;

  return (
    <div className="review-detail pdv-detail">
      <div className="review-detail-head">
        <button type="button" className="review-back-btn" onClick={onClose} aria-label="Back to presentations">←</button>
        <div className="review-detail-titles">
          <div className="review-detail-eyebrow">PRESENTATION{data && data.version_label ? ` · ${data.version_label}` : ""}</div>
          <div className="review-detail-title">{(data && data.title) || "Presentation"}</div>
          <div className="review-detail-sub">Review mode — click any image to comment; hover an image and tap ⤢ to view it full screen.</div>
        </div>
        <div className="review-detail-tools">
          <button type="button" className="review-version-picker" onClick={share} title="Copy a link that opens this deck for review">{copied ? "Link copied ✓" : "Share link"}</button>
          {data && data.open_url ? <a className="review-version-picker" href={data.open_url} target="_blank" rel="noopener noreferrer">Open PDF ↗</a> : null}
        </div>
      </div>
      <div className="review-detail-body pdv-detail-body">
        <div className="pdv-stage-col">
          {!error && !noDeck && (
            <div className={"pdv-loader" + (slidesReady ? " is-hidden" : "")} aria-hidden={slidesReady}>
              <span className="pdv-loader-spin" /><span className="pdv-loader-txt">Loading deck…</span>
            </div>
          )}
          <div className="pdv-slides-scroll">
            {error && <div className="pdv-empty">Couldn't load this presentation ({error}).{pdfLink}</div>}
            {noDeck && <div className="pdv-empty">This presentation was saved before in-app review existed, so there's no live deck to comment on.{pdfLink}</div>}
            {!loading && deck && !ctx && <div className="pdv-empty">Renderer still loading — hard-refresh (Ctrl+Shift+R).</div>}
            {!loading && deck && ctx && (
              <div className={"pdv-slides-fade" + (slidesReady ? " is-ready" : "")}>
                {slidesArr.map(s => (
                  <div key={s.key} ref={el => { slideRefs.current[s.key] = el; }} className={"pres-slide pdv-slide pres-tpl-" + tplOf(s.key)}>
                    <div className="pres-slide-canvas">{window.renderPresentationSlideBody ? window.renderPresentationSlideBody(s, ctx) : null}</div>
                  </div>
                ))}
              </div>
            )}
          </div>
        </div>
        <aside className="review-side pdv-side">
          <div className="review-side-head">
            <div className="review-side-title">Comments{comments.length ? ` · ${comments.length}` : ""}</div>
          </div>
          <div className="review-comments-scroll">
            {comments.length === 0
              ? <div className="pdv-cmt-empty">No comments yet. Click any image in the deck to leave feedback.</div>
              : (() => {
                  // v07zz426 — group replies under their top-level comment (1-level threads).
                  const byParent = {};
                  comments.forEach(c => { if (c.parent_comment_id) { (byParent[c.parent_comment_id] = byParent[c.parent_comment_id] || []).push(c); } });
                  return comments.filter(c => !c.parent_comment_id).map(c => {
                    const replies = (byParent[c.id] || []).slice().sort((a, b) => String(a.created_at || "").localeCompare(String(b.created_at || "")));
                    return (
                      <div key={"thread-" + c.id} className="pdv-cmt-thread">
                        {renderComment(c, false)}
                        {replies.length > 0 && <div className="pdv-cmt-replies">{replies.map(r => renderComment(r, true))}</div>}
                      </div>
                    );
                  });
                })()}
          </div>
          {/* composer ALWAYS rendered (disabled until an image is picked) → constant height, no shift.
              v07zz278 — entirely hidden for roles without comment_on_shots. */}
          {canComment && (
          <div className="pdv-composer">
            {replyingTo && (
              <div className="pdv-replybar">
                <span className="pdv-replybar-txt">↩ Replying to {replyingTo.user_name || "comment"}</span>
                <button type="button" className="pdv-replybar-x" title="Cancel reply" onClick={() => { setReplyingTo(null); setDraft(""); }}>✕</button>
              </div>
            )}
            <textarea ref={taRef} className="pdv-composer-input" rows={2} value={draft} disabled={!target && !replyingTo}
              placeholder={replyingTo ? ("Reply to " + (replyingTo.user_name || "comment") + "…") : (target ? "Comment on " + (target.label || "this image") + "…" : "Click an image in the deck to comment on it")}
              onChange={e => setDraft(e.target.value)} onKeyDown={onKey} />
            <div className="pdv-composer-foot">
              <span className="pdv-composer-hint">{replyingTo ? "Reply stays on this deck thread" : (target ? (target.entity_type ? `Also posts to this ${target.word}'s Notes` : "Routed to its source shot/asset Notes when known") : "Select an image above")}</span>
              {/* v07zz479 — admin-only, ALWAYS rendered (disabled until an image is
                  selected) so toggling selection never shifts the composer height. */}
              {isAdmin && (
                <button type="button" className="pdv-composer-send pdv-swap-open" disabled={!target || !!replyingTo}
                  title={target ? "Swap this slide image for another (fixes a broken or wrong image)" : "Click an image in the deck first"}
                  onClick={openSwap}>⇄ Replace image</button>
              )}
              <button type="button" className="pdv-composer-send" disabled={(!target && !replyingTo) || saving || !draft.trim()} onClick={submit}>{saving ? "Posting…" : (replyingTo ? "Reply" : "Comment")}</button>
            </div>
          </div>
          )}
        </aside>
      </div>
      {zoomSrc && LB ? React.createElement(LB, { src: zoomSrc, onClose: () => setZoomSrc(null) }) : null}
      {/* v07zz479 — admin image-replace picker (styled confirm-modal pattern, invariant #22) */}
      {swap && ReactDOM.createPortal(
        <div className="confirm-delete-backdrop" onClick={() => !swapBusy && setSwap(null)}>
          <div className="confirm-delete-modal pdv-swap-modal" onClick={e => e.stopPropagation()}>
            <div className="pdv-swap-head">
              <div className="pdv-swap-title">Replace image · {swap.label || swap.slide_key}</div>
              <button type="button" className="pdv-replybar-x" title="Close" onClick={() => setSwap(null)}>✕</button>
            </div>
            <div className="pdv-swap-current">
              <div className="pdv-swap-thumb" style={{ backgroundImage: `url(${thumbU(swap.url, 240)})` }} />
              <div className="pdv-swap-cursub">Current image — pick its replacement below. It saves into this locked deck only.</div>
            </div>
            <div className="pdv-swap-search">
              <input className="pdv-swap-input" type="text" value={swapQuery} placeholder="Shot id (SH0140) or asset slug (columbus)…"
                onChange={e => setSwapQuery(e.target.value)}
                onKeyDown={e => { if (e.key === "Enter") loadSwapCands(swapQuery); }} />
              <button type="button" className="pdv-composer-send" onClick={() => loadSwapCands(swapQuery)}>Load</button>
            </div>
            {swapErr && <div className="pdv-swap-err">{swapErr}</div>}
            <div className="pdv-swap-grid">
              {swapCands === null && <div className="pdv-swap-empty">Loading images…</div>}
              {Array.isArray(swapCands) && swapCands.length === 0 && <div className="pdv-swap-empty">No images found — try a shot id (SH0140) or an asset slug above.</div>}
              {Array.isArray(swapCands) && swapCands.map((c, i) => (
                <button key={i} type="button" className="pdv-swap-cell" disabled={swapBusy} title={c.label}
                  onClick={() => doSwap(c.url)}>
                  <span className="pdv-swap-cellimg" style={{ backgroundImage: `url(${thumbU(c.url, 320)})` }} />
                  <span className="pdv-swap-celllabel">{c.label}</span>
                </button>
              ))}
            </div>
            {swapBusy && <div className="pdv-swap-empty">Saving…</div>}
          </div>
        </div>,
        document.getElementById("modal-root") || document.body
      )}
    </div>
  );
}
window.PresentationDeckViewer = PresentationDeckViewer;
