// Vanday DAM — single asset preview
// PDF / EPS / AI have no browser-renderable pixels, so the <img> would break.
// Show the server-rasterized preview (an 800px PNG) instead. SVG renders
// natively in an <img> and stays crisp when zoomed, so it keeps its raw URL.
function assetNeedsRasterPreview(asset) {
  const n = (asset?.name || asset?.url || "").toLowerCase();
  const m = (asset?.mime || "").toLowerCase();
  return (
    m === "application/pdf" || m === "application/postscript" || m === "application/illustrator" ||
    /\.(pdf|eps|ai)$/.test(n)
  );
}
function mainPreviewSrc(asset) {
  if (assetNeedsRasterPreview(asset) && asset?.id && window.VandayAPI?.thumbUrl) {
    return window.VandayAPI.thumbUrl(asset.id, 800);
  }
  return asset?.url || "";
}

// Editable tag chips. For real (uploaded) assets, changes are persisted via
// PATCH /api/assets/:id. For mocked/demo assets we update locally only —
// they don't exist on the server so we can't save.
function TagEditor({ asset, onPickTag }) {
  // Read directly off the asset; bumping a state forces re-renders after a save.
  const [, bump] = React.useState(0);
  const [draft, setDraft] = React.useState("");
  const tags = Array.isArray(asset.tags) ? asset.tags : [];
  // Viewers see tags but can't edit them (the server rejects the PATCH too).
  const canWrite = (typeof window.useVandayFeatures === "function")
    ? window.useVandayFeatures().canWrite : true;

  const persist = async (next) => {
    asset.tags = next;
    bump((n) => n + 1);
    if (asset.isServer && !asset.isServerVariant) {
      try { await VandayAPI.patchAsset(asset.id, { tags: next }); } catch {}
    }
  };
  const removeTag = (t) => persist(tags.filter((x) => x !== t));
  const addTag = (raw) => {
    const t = raw.toLowerCase().trim().replace(/^#/, "").slice(0, 32);
    if (!t || tags.includes(t)) return;
    persist([...tags, t]);
  };
  const onKey = (e) => {
    if (e.key === "Enter" || e.key === ",") {
      e.preventDefault();
      addTag(draft);
      setDraft("");
    } else if (e.key === "Backspace" && !draft && tags.length) {
      removeTag(tags[tags.length - 1]);
    }
  };

  return (
    <div style={{ display: "flex", flexWrap: "wrap", gap: 6, alignItems: "center" }}>
      {tags.map((t) => (
        <span
          key={t}
          className="kw"
          onClick={onPickTag ? () => onPickTag(t) : undefined}
          style={{ display: "inline-flex", alignItems: "center", gap: 4, cursor: onPickTag ? "pointer" : "default" }}
          title={onPickTag ? `Search for “${t}”` : "Click × to remove"}
        >
          {t}
          {canWrite && (
            <button
              onClick={(e) => { e.stopPropagation(); removeTag(t); }}
              style={{
                background: "transparent", border: 0, color: "inherit",
                opacity: 0.55, cursor: "pointer", padding: "0 2px", lineHeight: 1,
                fontSize: 11,
              }}
              title="Remove tag"
            >×</button>
          )}
        </span>
      ))}
      {!canWrite && tags.length === 0 && (
        <span style={{ color: "var(--text-faint)", fontSize: 11.5, fontStyle: "italic" }}>No tags</span>
      )}
      {canWrite && (
        <input
          value={draft}
          onChange={(e) => setDraft(e.target.value)}
          onKeyDown={onKey}
          onBlur={() => { if (draft) { addTag(draft); setDraft(""); } }}
          placeholder={tags.length ? "+ add" : "Add a tag…"}
          style={{
            flex: "1 1 80px", minWidth: 60, background: "transparent",
            border: "1px dashed var(--border)", color: "var(--text)",
            borderRadius: 6, padding: "3px 8px", fontSize: 11.5,
            fontFamily: "inherit", outline: "none",
          }}
        />
      )}
    </div>
  );
}

// Inline-editable asset title. Double-click the h3 → input swaps in; Enter
// or blur saves, Escape cancels. For real (server) assets the change is
// persisted via PATCH /api/assets/:id { name }. For demo/mock assets we
// update locally only — they don't exist on the server.
function NameEditor({ asset }) {
  const [, bump] = React.useState(0);
  const [editing, setEditing] = React.useState(false);
  const [draft, setDraft] = React.useState("");
  const canWrite = (typeof window.useVandayFeatures === "function")
    ? window.useVandayFeatures().canWrite : true;
  const editable = canWrite && !!asset && !asset.isCrop;

  const startEdit = () => {
    if (!editable) return;
    setDraft(asset.name || "");
    setEditing(true);
  };
  const commit = async () => {
    const next = draft.trim().slice(0, 200);
    setEditing(false);
    if (!next || next === asset.name) return;
    const prev = asset.name;
    asset.name = next;
    bump((n) => n + 1);
    try { window.VandayServer?.bumpRev?.(); } catch {}
    if (asset.isServer && !asset.isServerVariant) {
      try {
        await VandayAPI.patchAsset(asset.id, { name: next });
      } catch {
        asset.name = prev;
        bump((n) => n + 1);
        try { window.VandayServer?.bumpRev?.(); } catch {}
      }
    }
  };
  const cancel = () => { setEditing(false); setDraft(""); };

  if (editing) {
    return (
      <h3 className="rp-title">
        <input
          autoFocus
          value={draft}
          onChange={(e) => setDraft(e.target.value)}
          onKeyDown={(e) => {
            if (e.key === "Enter") { e.preventDefault(); commit(); }
            else if (e.key === "Escape") { e.preventDefault(); cancel(); }
          }}
          onBlur={commit}
          onFocus={(e) => e.target.select()}
          maxLength={200}
          style={{
            width: "100%", background: "var(--bg, #fff)",
            border: "1px dashed var(--accent)", color: "var(--text)",
            borderRadius: 6, padding: "2px 8px",
            font: "inherit", outline: "none",
          }}
        />
      </h3>
    );
  }
  return (
    <h3 className="rp-title rp-title-row">
      <span
        className="rp-title-text"
        onDoubleClick={startEdit}
        title={editable ? "Rename file" : undefined}
        style={editable ? { cursor: "text" } : undefined}
      >
        {asset.name}
      </span>
      {editable && (
        <button
          type="button"
          className="rp-rename-btn"
          onClick={startEdit}
          title="Rename file"
          aria-label="Rename file"
        >
          <IcPencil size={14} />
        </button>
      )}
    </h3>
  );
}

// ============================================================================
// IPTC Metadata editor — top-20 industry-standard fields, grouped into
// readable sections. Loads from /api/assets/:id/metadata on mount, persists
// via PATCH. Edits are merged server-side so unspecified fields stay put.
//
// For demo / non-server assets we render the panel read-only with a hint —
// IPTC only makes sense on real uploaded files we can re-stamp on download.
// ============================================================================

// Canonical 20-field shape. Mirrors EMPTY_METADATA in server/iptc.js — keep
// in sync if either side changes. Used both to seed the form and to know
// what keys to send back on PATCH.
const EMPTY_METADATA = {
  title: "",
  description: "",
  keywords: [],
  headline: "",
  creator: "",
  creatorJobTitle: "",
  creditLine: "",
  source: "",
  copyrightNotice: "",
  rightsUsageTerms: "",
  contactEmail: "",
  contactUrl: "",
  city: "",
  state: "",
  country: "",
  countryCode: "",
  sublocation: "",
  dateCreated: "",
  instructions: "",
  jobId: "",
};

// Group the 20 fields into UI sections. Each entry: [section title, [field key, label, type]]
// `type` is "text" | "textarea" | "keywords" | "date".
const METADATA_SECTIONS = [
  ["Identity", [
    ["title",        "Title",        "text"],
    ["headline",     "Headline",     "text"],
    ["description",  "Description",  "textarea"],
    ["keywords",     "Keywords",     "keywords"],
  ]],
  ["Author", [
    ["creator",         "Creator",     "text"],
    ["creatorJobTitle", "Job title",   "text"],
    ["creditLine",      "Credit",      "text"],
    ["source",          "Source",      "text"],
  ]],
  ["Rights", [
    ["copyrightNotice", "Copyright",     "text"],
    ["rightsUsageTerms","Usage terms",   "textarea"],
    ["contactEmail",    "Contact email", "text"],
    ["contactUrl",      "Contact URL",   "text"],
  ]],
  ["Location", [
    ["city",        "City",         "text"],
    ["state",       "State",        "text"],
    ["country",     "Country",      "text"],
    ["countryCode", "Country code", "text"],
    ["sublocation", "Sublocation",  "text"],
  ]],
  ["Other", [
    ["dateCreated",  "Date created", "date"],
    ["instructions", "Instructions", "textarea"],
    ["jobId",        "Job ID",       "text"],
  ]],
];

// IPTC keyword fields sometimes arrive as a single comma/semicolon-joined
// string (common with AI-written metadata), which renders as one giant run-on
// chip. Split + trim + dedupe so each keyword becomes its own chip.
function normalizeKeywords(kw) {
  const arr = Array.isArray(kw) ? kw : (typeof kw === "string" ? [kw] : []);
  const out = [];
  const seen = new Set();
  for (const item of arr) {
    for (const part of String(item == null ? "" : item).split(/[,;]+/)) {
      const t = part.trim();
      if (t && !seen.has(t.toLowerCase())) { seen.add(t.toLowerCase()); out.push(t); }
    }
  }
  return out;
}

function MetadataEditor({ asset }) {
  const canWrite = (typeof window.useVandayFeatures === "function")
    ? window.useVandayFeatures().canWrite : true;

  const isServer = !!(asset && asset.isServer && !asset.isServerVariant);
  const editable = canWrite && isServer;

  const [draft, setDraft]   = React.useState(EMPTY_METADATA);
  const [saved, setSaved]   = React.useState(EMPTY_METADATA);
  const [state, setState]   = React.useState(isServer ? "loading" : "ready");
  const [status, setStatus] = React.useState("");
  // Polling indicator — non-zero while we're waiting on the background
  // IPTC queue to populate the row after a fresh upload. The exact value is
  // the attempt count, used to show progress to the user.
  const [pollAttempt, setPollAttempt] = React.useState(0);
  // Whether the server has finished its background metadata read for this
  // asset. Drives the "Reading metadata…" spinner. We rely on this explicit
  // signal rather than "are the fields empty?" — a file can legitimately have
  // no embedded metadata (e.g. an exported PNG logo), and that's "done, empty",
  // not "still reading".
  const [read, setRead] = React.useState(false);

  // Pull metadata from the server on mount / asset change.
  React.useEffect(() => {
    if (!isServer || !asset?.id) {
      setState("ready");
      return;
    }
    let cancelled = false;
    setState("loading");
    setPollAttempt(0);
    setRead(false);
    VandayAPI.getAssetMetadata(asset.id)
      .then((data) => {
        if (cancelled) return;
        const m = { ...EMPTY_METADATA, ...(data.metadata || {}) };
        // Normalize keywords — server might send array of strings.
        m.keywords = normalizeKeywords(m.keywords);
        setDraft(m);
        setSaved(m);
        setRead(!!data.read);
        setState("ready");
      })
      .catch(() => {
        if (cancelled) return;
        setState("error");
      });
    return () => { cancelled = true; };
  }, [asset?.id, isServer]);

  // Background-queue polling. After the initial fetch, if the server hasn't
  // finished its metadata read AND the user hasn't started editing, we
  // re-fetch every 2s for up to 30s. As soon as the server reports the read
  // is done (read === true) — or the user starts editing, or we time out — we
  // stop.
  //
  // The dirty flag is captured via a ref so changes mid-poll cancel
  // gracefully without restarting the whole effect from scratch.
  const dirtyRef = React.useRef(false);
  React.useEffect(() => {
    if (state !== "ready") return;
    if (!isServer || !asset?.id) return;
    if (read) return;
    if (dirtyRef.current) return;

    const MAX_ATTEMPTS = 15; // 15 × 2s = 30s
    let cancelled = false;
    let attempts = 0;
    let timer = null;

    const tick = async () => {
      if (cancelled) return;
      attempts += 1;
      setPollAttempt(attempts);
      if (attempts > MAX_ATTEMPTS) {
        setPollAttempt(0);
        return;
      }
      try {
        const data = await VandayAPI.getAssetMetadata(asset.id);
        const m = { ...EMPTY_METADATA, ...(data.metadata || {}) };
        m.keywords = normalizeKeywords(m.keywords);
        if (data.read && !cancelled && !dirtyRef.current) {
          setSaved(m);
          setDraft(m);
          setRead(true);
          setPollAttempt(0);
          return;
        }
      } catch {/* network blip — keep trying */}
      if (!cancelled && !dirtyRef.current) {
        timer = window.setTimeout(tick, 2000);
      } else {
        setPollAttempt(0);
      }
    };

    setPollAttempt(1);
    timer = window.setTimeout(tick, 2000);
    return () => {
      cancelled = true;
      if (timer) window.clearTimeout(timer);
    };
    // asset.id + state + read are the triggers — dirty changes use the ref
    // above so they cancel in-flight loops without restarting.
  }, [asset?.id, state, isServer, read]);

  const dirty = React.useMemo(() => {
    const ks = Object.keys(EMPTY_METADATA);
    for (const k of ks) {
      if (k === "keywords") {
        const a = (draft[k] || []).slice().sort().join("\n");
        const b = (saved[k] || []).slice().sort().join("\n");
        if (a !== b) return true;
      } else if ((draft[k] || "") !== (saved[k] || "")) {
        return true;
      }
    }
    return false;
  }, [draft, saved]);

  // Keep the polling effect's view of "is the user editing?" current.
  // The polling effect (defined above) reads this ref each tick so that
  // typing mid-poll cancels the loop without restarting from scratch.
  React.useEffect(() => { dirtyRef.current = dirty; }, [dirty]);

  const setField = (k, v) => setDraft((d) => ({ ...d, [k]: v }));

  // Track save lifecycle separately from the inline status text so we can
  // change the BUTTON label too — that's where the user's eye is when they
  // click save, so a button-state change is more visible than a tiny status
  // span next to it.
  const [saveState, setSaveState] = React.useState("idle"); // idle | saving | saved | error

  const save = async () => {
    if (!editable) return;
    setSaveState("saving");
    setStatus("");
    try {
      const data = await VandayAPI.patchAssetMetadata(asset.id, draft);
      const m = { ...EMPTY_METADATA, ...(data.metadata || {}) };
      m.keywords = Array.isArray(m.keywords) ? m.keywords : [];
      setSaved(m);
      setDraft(m);
      setSaveState("saved");
      setStatus("Changes saved.");
      // Reset the visible "Saved" pill after a beat so the button returns
      // to its normal disabled-clean state. The status text fades along.
      setTimeout(() => {
        setSaveState("idle");
        setStatus("");
      }, 2500);
    } catch (err) {
      console.error("[metadata] save failed", err);
      setSaveState("error");
      setStatus("Save failed — try again.");
    }
  };

  const discard = () => setDraft(saved);

  if (state === "loading") {
    return <div className="rp-body"><div style={{ color: "var(--text-faint)", fontSize: 13 }}>Loading metadata…</div></div>;
  }
  if (state === "error") {
    return <div className="rp-body"><div style={{ color: "var(--text-faint)", fontSize: 13 }}>Couldn't load metadata.</div></div>;
  }

  if (!isServer) {
    return (
      <div className="rp-body">
        <div style={{ color: "var(--text-faint)", fontSize: 13, fontStyle: "italic" }}>
          Metadata editing is available on uploaded files. Demo / sample assets don't have IPTC fields to edit.
        </div>
      </div>
    );
  }

  return (
    <div className="rp-body">

      {pollAttempt > 0 && (
        <div
          role="status"
          aria-live="polite"
          style={{
            display: "flex",
            alignItems: "center",
            gap: 10,
            padding: "10px 12px",
            marginBottom: 16,
            background: "var(--accent-soft, #fbe6df)",
            border: "1px solid color-mix(in oklch, var(--accent, #c8553d) 25%, transparent)",
            borderRadius: 8,
            fontSize: 12.5,
            color: "var(--text)",
            lineHeight: 1.5,
          }}
        >
          <span
            aria-hidden="true"
            style={{
              display: "inline-block",
              width: 14,
              height: 14,
              border: "2px solid color-mix(in oklch, var(--accent, #c8553d) 30%, transparent)",
              borderTopColor: "var(--accent, #c8553d)",
              borderRadius: "50%",
              animation: "vanday-spin 0.8s linear infinite",
              flexShrink: 0,
            }}
          />
          <span>
            <strong style={{ fontWeight: 500 }}>Reading metadata from the file…</strong>
            {" "}This will fill in automatically in a moment. You can start editing now and your changes won't be overwritten.
          </span>
          {/* Local keyframes — small CSS injection so we don't need to
              touch the global stylesheet for one component's spinner. */}
          <style>{`@keyframes vanday-spin { to { transform: rotate(360deg); } }`}</style>
        </div>
      )}

      {METADATA_SECTIONS.map(([sectionTitle, fields]) => (
        <div key={sectionTitle} style={{ marginBottom: 20 }}>
          <div style={{
            fontSize: 11, textTransform: "uppercase", letterSpacing: "0.08em",
            color: "var(--text-faint)", fontWeight: 500, margin: "0 0 10px",
            paddingBottom: 4, borderBottom: "1px solid var(--border)",
          }}>{sectionTitle}</div>

          {fields.map(([key, label, type]) => (
            <div className="field-block" key={key} style={{ marginTop: 12 }}>
              <div className="field-label">{label}</div>
              <MetadataField
                type={type}
                value={draft[key]}
                disabled={!editable}
                onChange={(v) => setField(key, v)}
              />
            </div>
          ))}
        </div>
      ))}

      {editable && (
        <div style={{
          position: "sticky", bottom: 0,
          marginTop: 20, padding: "12px 0",
          background: "linear-gradient(to bottom, transparent, var(--bg) 40%)",
          display: "flex", alignItems: "center", gap: 10,
        }}>
          <button
            className="btn sm"
            onClick={discard}
            disabled={!dirty || saveState === "saving"}
            style={{ opacity: (dirty && saveState !== "saving") ? 1 : 0.5 }}
          >
            Discard
          </button>
          <button
            className="btn sm primary"
            onClick={save}
            disabled={!dirty || saveState === "saving"}
            style={{
              opacity: (dirty || saveState === "saved") ? 1 : 0.5,
              minWidth: 130,
              transition: "background 0.18s ease",
              background: saveState === "saved" ? "var(--success, #1f8a3a)" : undefined,
              borderColor: saveState === "saved" ? "var(--success, #1f8a3a)" : undefined,
              color: saveState === "saved" ? "#fff" : undefined,
            }}
          >
            {saveState === "saving" && "Saving…"}
            {saveState === "saved"  && "✓ Saved"}
            {saveState === "error"  && "Try again"}
            {saveState === "idle"   && "Save changes"}
          </button>
          {status && (
            <span style={{
              fontSize: 12.5,
              color: saveState === "error" ? "var(--danger, #b53b22)" : "var(--text-faint)",
              fontWeight: saveState === "saved" ? 500 : 400,
            }}>{status}</span>
          )}
        </div>
      )}
    </div>
  );
}

// Per-field input component. Picks the right shape based on `type`.
function MetadataField({ type, value, disabled, onChange }) {
  const baseStyle = {
    width: "100%", boxSizing: "border-box",
    background: "var(--bg, #fff)",
    border: "1px solid var(--border)", borderRadius: 6,
    color: "var(--text)", font: "inherit", fontSize: 13,
    padding: "6px 8px", outline: "none",
  };

  if (type === "textarea") {
    return (
      <textarea
        value={value || ""}
        onChange={(e) => onChange(e.target.value)}
        disabled={disabled}
        rows={3}
        maxLength={500}
        style={{ ...baseStyle, resize: "vertical", minHeight: 56 }}
      />
    );
  }

  if (type === "date") {
    // Two-part date field: a clear "Not set" indicator when empty (so the
    // browser's calendar picker defaulting to today doesn't mislead the
    // user into thinking today is stored), plus a "Clear" affordance once
    // a value IS set so users can wipe a date back to empty. The native
    // <input type="date"> has no built-in clear mechanism that's
    // consistent across browsers.
    const hasValue = !!(value && /^\d{4}-\d{2}-\d{2}$/.test(value));
    return (
      <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
        <input
          type="date"
          value={hasValue ? value : ""}
          onChange={(e) => onChange(e.target.value)}
          disabled={disabled}
          style={{ ...baseStyle, flex: "1 1 auto", colorScheme: "light" }}
        />
        {!hasValue && (
          <span style={{ fontSize: 11.5, color: "var(--text-faint)", fontStyle: "italic" }}>
            Not set
          </span>
        )}
        {hasValue && !disabled && (
          <button
            onClick={() => onChange("")}
            style={{
              background: "transparent", border: 0,
              color: "var(--text-faint)", cursor: "pointer",
              fontSize: 11.5, padding: "2px 6px",
            }}
            title="Clear date"
          >
            Clear
          </button>
        )}
      </div>
    );
  }

  if (type === "keywords") {
    return (
      <KeywordChipsInput
        value={Array.isArray(value) ? value : []}
        disabled={disabled}
        onChange={onChange}
      />
    );
  }

  return (
    <input
      type="text"
      value={value || ""}
      onChange={(e) => onChange(e.target.value)}
      disabled={disabled}
      maxLength={500}
      style={baseStyle}
    />
  );
}

// Keyword chips: Enter / comma to add, backspace on empty input to remove
// last. Same UX as the TagEditor at the top of this file, but tailored for
// the metadata form (no API persistence on each chip — saved with the rest
// of the form on Save).
function KeywordChipsInput({ value, disabled, onChange }) {
  const [draft, setDraft] = React.useState("");
  const add = (raw) => {
    const t = String(raw || "").trim().slice(0, 60);
    if (!t) return;
    if (value.some((x) => x.toLowerCase() === t.toLowerCase())) return;
    onChange([...value, t]);
  };
  const remove = (i) => onChange(value.filter((_, idx) => idx !== i));
  const onKey = (e) => {
    if (e.key === "Enter" || e.key === ",") {
      e.preventDefault();
      add(draft);
      setDraft("");
    } else if (e.key === "Backspace" && !draft && value.length) {
      remove(value.length - 1);
    }
  };

  // Explicit inline pill style — not relying on the .kw class, which had
  // surprising interactions with the inline-flex layout and made imported
  // keywords look like run-on text. This gives each keyword a clear
  // background pill + visible spacing.
  const chipStyle = {
    display: "inline-flex",
    alignItems: "center",
    gap: 4,
    background: "var(--accent-soft, #fbe6df)",
    color: "var(--accent, #c8553d)",
    fontFamily: "var(--font-mono, ui-monospace, monospace)",
    fontSize: 11.5,
    lineHeight: 1.4,
    padding: "3px 8px",
    borderRadius: 999,
    border: "1px solid color-mix(in oklch, var(--accent, #c8553d) 25%, transparent)",
    cursor: "default",
    userSelect: "none",
    whiteSpace: "nowrap",
  };

  return (
    <div style={{
      display: "flex", flexWrap: "wrap", gap: 6, alignItems: "center",
      padding: 6,
      border: "1px solid var(--border)", borderRadius: 6,
      background: "var(--bg, #fff)",
      minHeight: 34,
    }}>
      {value.map((t, i) => (
        <span
          key={`${t}-${i}`}
          style={chipStyle}
          title={disabled ? t : "Click × to remove"}
        >
          {t}
          {!disabled && (
            <button
              onClick={() => remove(i)}
              style={{
                background: "transparent", border: 0, color: "inherit",
                opacity: 0.55, cursor: "pointer",
                padding: 0, marginLeft: 2, lineHeight: 1,
                fontSize: 13, fontWeight: 500,
              }}
              onMouseEnter={(e) => (e.currentTarget.style.opacity = "1")}
              onMouseLeave={(e) => (e.currentTarget.style.opacity = "0.55")}
              title="Remove keyword"
            >×</button>
          )}
        </span>
      ))}
      {!disabled && (
        <input
          value={draft}
          onChange={(e) => setDraft(e.target.value)}
          onKeyDown={onKey}
          onBlur={() => { if (draft) { add(draft); setDraft(""); } }}
          placeholder={value.length ? "+ add" : "Add a keyword…"}
          style={{
            flex: "1 1 80px", minWidth: 60, background: "transparent",
            border: 0, color: "var(--text)", padding: "3px 4px", fontSize: 12.5,
            fontFamily: "inherit", outline: "none",
          }}
        />
      )}
    </div>
  );
}

function DownloadMenu({ asset }) {
  const [open, setOpen] = React.useState(false);
  const [busy, setBusy] = React.useState(false);
  const ref = React.useRef(null);

  React.useEffect(() => {
    if (!open) return;
    const onClick = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    const onKey = (e) => { if (e.key === "Escape") setOpen(false); };
    window.addEventListener("mousedown", onClick);
    window.addEventListener("keydown", onKey);
    return () => {
      window.removeEventListener("mousedown", onClick);
      window.removeEventListener("keydown", onKey);
    };
  }, [open]);

  const pick = async (format) => {
    setOpen(false);
    setBusy(true);
    try { await VandayServer.downloadAsset(asset, format); }
    finally { setBusy(false); }
  };

  const origExt = (() => {
    const m = /\.([^.]+)$/.exec(asset && asset.name ? asset.name : "");
    return m ? m[1].toUpperCase() : "file";
  })();

  return (
    <div ref={ref} style={{ position: "relative" }}>
      <button className="btn" disabled={busy} onClick={() => setOpen((v) => !v)}>
        <IcDownload size={14} /> {busy ? "Preparing…" : "Download"}
        <IcChevD size={12} style={{ marginLeft: 2 }} />
      </button>
      {open && (
        <div className="send-menu" style={{ left: 0, right: "auto" }}>
          <div className="send-menu-h">Download as</div>
          <button className="send-menu-item" onClick={() => pick("original")}>
            <div style={{ flex: 1 }}>
              <div className="send-menu-name">Original</div>
              <div className="send-menu-account">Keep the source file ({origExt})</div>
            </div>
          </button>
          <button className="send-menu-item" onClick={() => pick("jpg")}>
            <div style={{ flex: 1 }}>
              <div className="send-menu-name">JPG</div>
              <div className="send-menu-account">Smaller file, no transparency</div>
            </div>
          </button>
          <button className="send-menu-item" onClick={() => pick("png")}>
            <div style={{ flex: 1 }}>
              <div className="send-menu-name">PNG</div>
              <div className="send-menu-account">Lossless, keeps transparency</div>
            </div>
          </button>
        </div>
      )}
    </div>
  );
}

// --- Workflow label picker ---------------------------------------------
// Lets any org member change an asset's workflow status. Server assets POST
// to /api/assets/:id; mock/demo assets just update locally.
function WorkflowLabelPicker({ asset }) {
  const { labels } = window.VandayServer.useWorkflowLabels();
  const [open, setOpen] = React.useState(false);
  const [currentId, setCurrentId] = React.useState(asset.workflowLabelId || null);
  const ref = React.useRef(null);
  // Viewers see the status but can't change it.
  const canWrite = (typeof window.useVandayFeatures === "function")
    ? window.useVandayFeatures().canWrite : true;

  React.useEffect(() => { setCurrentId(asset.workflowLabelId || null); }, [asset.id, asset.workflowLabelId]);

  React.useEffect(() => {
    if (!open) return;
    const onClick = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    const onKey = (e) => { if (e.key === "Escape") setOpen(false); };
    window.addEventListener("mousedown", onClick);
    window.addEventListener("keydown", onKey);
    return () => {
      window.removeEventListener("mousedown", onClick);
      window.removeEventListener("keydown", onKey);
    };
  }, [open]);

  const current = (labels || []).find((l) => l.id === currentId) || null;
  const colorClass = current ? `wf-color-${current.color}` : "";

  // Read-only badge for viewers: show the current status, nothing clickable.
  if (!canWrite) {
    if (!current) return null;
    return (
      <span className={`wf-pill ${colorClass}`}>
        <span className="wf-dot" />
        {current.name}
      </span>
    );
  }

  const choose = async (labelId) => {
    setOpen(false);
    setCurrentId(labelId);
    asset.workflowLabelId = labelId;
    if (asset.isServer && !asset.isServerVariant) {
      try {
        await VandayAPI.patchAsset(asset.id, { workflowLabelId: labelId });
      } catch (e) {
        console.warn("Failed to set workflow label", e);
      }
    }
  };

  return (
    <div ref={ref} style={{ position: "relative", display: "inline-block" }}>
      <button
        type="button"
        className={`wf-pill is-button ${current ? colorClass : "is-empty"}`}
        onClick={() => setOpen((v) => !v)}
        title="Change status"
      >
        {current ? (
          <>
            <span className="wf-dot" />
            {current.name}
          </>
        ) : (
          <>+ Set status</>
        )}
      </button>
      {open && (
        <div className="wf-picker-pop">
          {(labels || []).length === 0 && (
            <div style={{ padding: "8px 10px", fontSize: 12, color: "var(--text-faint)" }}>
              No labels yet
            </div>
          )}
          {(labels || []).map((l) => (
            <button
              key={l.id}
              className="wf-picker-opt"
              onClick={() => choose(l.id)}
            >
              <span className={`wf-swatch wf-color-${l.color}`} />
              <span style={{ flex: 1 }}>{l.name}</span>
              {currentId === l.id && <IcCheck size={11} />}
            </button>
          ))}
          {current && (
            <button className="wf-picker-opt is-clear" onClick={() => choose(null)}>
              Clear status
            </button>
          )}
        </div>
      )}
    </div>
  );
}

// --- Server-backed comments + @mention autocomplete --------------------
function ServerComments({ asset, embedded }) {
  const [comments, setComments] = React.useState([]);
  const [draft, setDraft] = React.useState("");
  const [loading, setLoading] = React.useState(true);
  const [mentionState, setMentionState] = React.useState(null); // {query, start, active}
  const [mentioned, setMentioned] = React.useState({}); // displayName -> userId
  const members = window.VandayServer.useOrgMembers();
  const memberDisplay = window.VandayServer.memberDisplayName;
  const inputRef = React.useRef(null);
  const me = window.VandaySession && window.VandaySession.getActiveUser
    ? window.VandaySession.getActiveUser() : null;
  // Viewers can read the thread but can't post (server rejects it too).
  const canWrite = (typeof window.useVandayFeatures === "function")
    ? window.useVandayFeatures().canWrite : true;

  React.useEffect(() => {
    let cancelled = false;
    setLoading(true);
    VandayAPI.listComments(asset.id)
      .then((r) => { if (!cancelled) setComments(r.comments || r || []); })
      .catch(() => { if (!cancelled) setComments([]); })
      .finally(() => { if (!cancelled) setLoading(false); });
    return () => { cancelled = true; };
  }, [asset.id]);

  // Build a quick lookup so we can show author name + initials for each comment.
  const memberById = React.useMemo(() => {
    const m = new Map();
    for (const u of members || []) m.set(u.id, u);
    return m;
  }, [members]);

  const authorFor = (c) => {
    const u = memberById.get(c.userId);
    return u ? memberDisplay(u) : (c.userId ? c.userId.slice(0, 6) : "Unknown");
  };
  const initialsFor = (c) => {
    const n = authorFor(c);
    return n.split(/[\s._-]+/).filter(Boolean).slice(0, 2).map((s) => s[0].toUpperCase()).join("") || "?";
  };
  const whenFor = (c) => {
    if (!c.createdAt) return "";
    const d = new Date(c.createdAt);
    const diff = Date.now() - d.getTime();
    if (diff < 60 * 1000) return "just now";
    if (diff < 3600 * 1000) return `${Math.floor(diff / 60000)}m ago`;
    if (diff < 86400 * 1000) return `${Math.floor(diff / 3600000)}h ago`;
    return d.toLocaleDateString();
  };

  // Detect @mention as the user types.
  const updateDraft = (value, caret) => {
    setDraft(value);
    const upToCaret = value.slice(0, caret);
    const m = upToCaret.match(/(^|\s)@([\w.-]*)$/);
    if (m) {
      setMentionState({ query: m[2].toLowerCase(), start: caret - m[2].length - 1 });
    } else {
      setMentionState(null);
    }
  };

  const filteredMembers = React.useMemo(() => {
    if (!mentionState) return [];
    const q = mentionState.query;
    return (members || [])
      .filter((u) => !me || u.id !== me.id)
      .filter((u) => {
        if (!q) return true;
        const name = memberDisplay(u).toLowerCase();
        const email = (u.email || "").toLowerCase();
        return name.includes(q) || email.includes(q);
      })
      .slice(0, 6);
  }, [mentionState, members, me, memberDisplay]);

  const insertMention = (user) => {
    const name = memberDisplay(user);
    const before = draft.slice(0, mentionState.start);
    const after = draft.slice(mentionState.start + 1 + mentionState.query.length);
    const next = `${before}@${name} ${after}`;
    setDraft(next);
    setMentioned((m) => ({ ...m, [name]: user.id }));
    setMentionState(null);
    requestAnimationFrame(() => {
      if (inputRef.current) {
        inputRef.current.focus();
        const pos = (before + "@" + name + " ").length;
        try { inputRef.current.setSelectionRange(pos, pos); } catch {}
      }
    });
  };

  const submit = async () => {
    const body = draft.trim();
    if (!body) return;
    // Only include mentions that still appear in the body.
    const ids = Object.entries(mentioned)
      .filter(([name]) => body.includes(`@${name}`))
      .map(([, id]) => id);
    try {
      const created = await VandayAPI.postComment(asset.id, body, ids);
      setComments((cs) => [...cs, created]);
      setDraft("");
      setMentioned({});
      setMentionState(null);
      // After the new comment renders, scroll it into view. On mobile a tall
      // comment thread can push the new entry below the bottom-nav / keyboard;
      // without this you can't see what you just posted. Block:'end' keeps the
      // input visible too, so a follow-up reply is one tap away.
      requestAnimationFrame(() => {
        try {
          inputRef.current?.scrollIntoView({ behavior: "smooth", block: "end" });
        } catch {}
      });
    } catch (e) {
      console.warn("Failed to post comment", e);
    }
  };

  const onKeyDown = (e) => {
    if (mentionState && filteredMembers.length && (e.key === "Enter" || e.key === "Tab")) {
      e.preventDefault();
      insertMention(filteredMembers[0]);
      return;
    }
    if (e.key === "Enter" && !e.shiftKey && !mentionState) {
      e.preventDefault();
      submit();
    }
    if (e.key === "Escape" && mentionState) {
      setMentionState(null);
    }
  };

  // Render comment body with @mentions visually highlighted.
  const renderBody = (body) => {
    const parts = body.split(/(@[\w.-]+)/g);
    return parts.map((p, i) =>
      p.startsWith("@")
        ? <strong key={i} style={{ color: "var(--accent)", fontWeight: 600 }}>{p}</strong>
        : <span key={i}>{p}</span>
    );
  };

  return (
    <div className={embedded ? "" : "rp-body"} style={{ color: "var(--text-muted)", fontSize: 13, ...(embedded ? { paddingTop: 4 } : {}) }}>
      {loading && (
        <div style={{ padding: "16px 0", fontSize: 12.5, color: "var(--text-faint)", fontStyle: "italic" }}>
          Loading comments…
        </div>
      )}
      {!loading && comments.length === 0 && (
        <div style={{ padding: "16px 0", fontSize: 12.5, color: "var(--text-faint)", fontStyle: "italic" }}>
          No comments yet.
        </div>
      )}
      {!loading && comments.map((c) => (
        <div key={c.id} style={{ display: "flex", gap: 10, marginBottom: 18 }}>
          <span className="pill-user" style={{ background: "transparent", padding: 0 }}>
            <span className="av">{initialsFor(c)}</span>
          </span>
          <div style={{ flex: 1 }}>
            <div style={{ fontSize: 12.5, color: "var(--text)" }}>
              <strong style={{ fontWeight: 600 }}>{authorFor(c)}</strong> · {whenFor(c)}
              {me && c.userId === me.id && (
                <button
                  onClick={async () => {
                    if (!window.confirm("Delete this comment?")) return;
                    try {
                      await VandayAPI.deleteComment(asset.id, c.id);
                      setComments((cs) => cs.filter((x) => x.id !== c.id));
                    } catch (e) { console.warn(e); }
                  }}
                  style={{
                    marginLeft: 6, background: "transparent", border: 0,
                    color: "var(--text-faint)", cursor: "pointer", fontSize: 11,
                  }}
                  title="Delete comment"
                >×</button>
              )}
            </div>
            <p style={{ margin: "4px 0 0", color: "var(--text)" }}>{renderBody(c.body)}</p>
          </div>
        </div>
      ))}

      <div style={{ position: "relative", display: canWrite ? "block" : "none" }}>
        <input
          ref={inputRef}
          value={draft}
          onChange={(e) => updateDraft(e.target.value, e.target.selectionStart || e.target.value.length)}
          onKeyDown={onKeyDown}
          placeholder="Add a comment… type @ to mention"
          style={{
            width: "100%",
            background: "var(--surface)",
            border: "1px solid var(--border)",
            borderRadius: 8,
            padding: "10px 12px",
            fontSize: 13,
            outline: "none",
          }}
        />
        {mentionState && filteredMembers.length > 0 && (
          <div className="mention-pop" style={{ bottom: "100%", left: 0, marginBottom: 4 }}>
            {filteredMembers.map((u, i) => (
              <button
                key={u.id}
                className={`mention-opt ${i === 0 ? "is-active" : ""}`}
                onClick={() => insertMention(u)}
              >
                <span className="av">
                  {memberDisplay(u).split(/[\s._-]+/).filter(Boolean).slice(0, 2).map((s) => s[0].toUpperCase()).join("") || "?"}
                </span>
                <span style={{ flex: 1 }}>
                  <div style={{ color: "var(--text)" }}>{memberDisplay(u)}</div>
                  {u.email && <div style={{ fontSize: 11, color: "var(--text-faint)" }}>{u.email}</div>}
                </span>
              </button>
            ))}
          </div>
        )}
      </div>
    </div>
  );
}

// Org-level field visibility (Settings → Fields). Admins can hide built-in
// Details fields (status, date_uploaded, file_size, dimensions, ai_keywords);
// the detail panel must respect that. Fetched per mount; shows all until loaded
// and on any error, so a fetch hiccup never hides everything.
function useHiddenFields() {
  const [hidden, setHidden] = React.useState(null);
  React.useEffect(() => {
    let alive = true;
    Promise.resolve(window.VandayAPI && window.VandayAPI.listFields && window.VandayAPI.listFields())
      .then((d) => {
        if (!alive) return;
        setHidden(new Set((d && d.fields ? d.fields : []).filter((f) => f.visible === false).map((f) => f.key)));
      })
      .catch(() => { if (alive) setHidden(new Set()); });
    return () => { alive = false; };
  }, []);
  return hidden || new Set();
}

// Whether this workspace has a Shopify store connected. Fetched once, cached
// for the tab so every preview-open doesn't re-hit the endpoint. { value: null
// } = unknown yet; true/false once known.
const _shopifyConnCache = { value: null };

// Real "Send to Shopify" flow: pick one product, confirm (this writes to the
// live store), push the current image onto it. Uses the Admin API under the
// hood via VandayAPI.shopifyPush.
// Turn a thrown API error into a friendly sentence (never a raw code like
// "insufficient_role"). jfetch errors carry .data.error + .message + .status.
function shopifyErrText(e, fallback) {
  const code = e?.data?.error || "";
  const msg = e?.message || "";
  if (code === "insufficient_role" || /insufficient_role/.test(msg))
    return "You need editor or admin access to send images to Shopify.";
  if (code === "not_connected" || /not_connected/.test(msg))
    return "Shopify isn’t connected. Ask a workspace admin to connect it in Settings → Integrations.";
  // Already a human sentence (has a space, not a bare code) → use it as-is.
  if (msg && /\s/.test(msg) && !/^[a-z0-9_]+$/i.test(msg.trim())) return msg;
  return fallback || "Something went wrong with Shopify. Please try again.";
}
window.shopifyErrText = shopifyErrText;

function ShopifyPushModal({ asset, onClose }) {
  const [products, setProducts] = React.useState(null); // null=loading, []=none
  const [loadErr, setLoadErr] = React.useState("");     // couldn't load products
  const [err, setErr] = React.useState("");             // couldn't send
  const [picked, setPicked] = React.useState(null);
  const [busy, setBusy] = React.useState(false);
  const [done, setDone] = React.useState(false);
  const [doneUrl, setDoneUrl] = React.useState(null);

  React.useEffect(() => {
    let cancelled = false;
    VandayAPI.shopifyProducts()
      .then((j) => { if (!cancelled) setProducts(j.products || []); })
      .catch((e) => { if (!cancelled) setLoadErr(shopifyErrText(e, "Couldn’t load your Shopify products — please try again.")); });
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    window.addEventListener("keydown", onKey);
    return () => { cancelled = true; window.removeEventListener("keydown", onKey); };
  }, [onClose]);

  const send = async () => {
    if (busy || !picked) return;
    setErr(""); setBusy(true);
    try {
      const resp = await VandayAPI.shopifyPush(asset.id, picked.id, picked.title);
      setDoneUrl(resp?.productUrl || null);
      // Reflect it in the asset's Publications panel right away (server also
      // persists this record, so it survives a reload).
      try {
        asset.publications = [
          ...(asset.publications || []),
          { site: "shopify", status: "live", url: resp?.productUrl || null, postedAt: new Date().toLocaleString(), caption: picked.title },
        ];
        window.VandayServer?.bumpRev?.();
      } catch {}
      setDone(true);
    } catch (e) { setErr(shopifyErrText(e, "Couldn’t send the image — please try again.")); }
    finally { setBusy(false); }
  };

  return (
    <div className="modal-backdrop" onClick={onClose}>
      <div className="modal" onClick={(e) => e.stopPropagation()} style={{ maxWidth: 500 }}>
        <header className="modal-head">
          <h2 className="modal-title">Send to Shopify</h2>
          <button className="btn ghost sm" onClick={onClose} title="Close"><IcClose size={16} /></button>
        </header>
        <div className="modal-body" style={{ display: "block", padding: 20, maxHeight: "58vh", overflowY: "auto" }}>
          {done ? (
            <div style={{ fontSize: 13.5, lineHeight: 1.6 }}>
              <div style={{ fontWeight: 600, marginBottom: 4 }}>Added to Shopify.</div>
              <div><span style={{ fontFamily: "var(--font-mono)" }}>{asset.name}</span> is now on <b>{picked?.title}</b>.</div>
              <div style={{ color: "var(--text-faint)", marginTop: 6 }}>It’s an additional product image — set it as the featured image in Shopify if you want it first.</div>
            </div>
          ) : loadErr ? (
            <div style={{ padding: "10px 12px", borderRadius: 8, background: "oklch(0.97 0.02 24)", border: "1px solid oklch(0.85 0.07 24)", color: "oklch(0.45 0.16 24)", fontSize: 13 }}>
              {loadErr}
            </div>
          ) : (
            <>
              <p className="rp-sub" style={{ marginTop: 0 }}>Pick the product to add this image to. It’s added to your <b>live</b> store as an additional product image.</p>
              {products === null ? (
                <div style={{ color: "var(--text-muted)", fontSize: 13 }}>Loading your products…</div>
              ) : products.length === 0 ? (
                <div style={{ color: "var(--text-muted)", fontSize: 13 }}>No products found in your store yet.</div>
              ) : (
                <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
                  {products.map((p) => (
                    <label key={p.id} style={{ display: "flex", alignItems: "center", gap: 11, padding: "8px 10px", border: "1px solid var(--border)", borderRadius: 9, cursor: "pointer",
                      background: picked?.id === p.id ? "var(--accent-soft)" : "transparent" }}>
                      <input type="radio" name="shopify-product" checked={picked?.id === p.id} onChange={() => setPicked(p)} />
                      {p.thumb
                        ? <img src={p.thumb} alt="" style={{ width: 40, height: 40, objectFit: "cover", borderRadius: 6, flexShrink: 0 }} />
                        : <div style={{ width: 40, height: 40, borderRadius: 6, background: "var(--surface-sunken)", flexShrink: 0 }} />}
                      <div style={{ minWidth: 0, flex: 1 }}>
                        <div style={{ fontSize: 13, fontWeight: 500, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{p.title}</div>
                        <div style={{ fontSize: 11.5, color: "var(--text-faint)" }}>{p.imageCount} image{p.imageCount === 1 ? "" : "s"}</div>
                      </div>
                    </label>
                  ))}
                </div>
              )}
            </>
          )}
          {err && (
            <div style={{ marginTop: 14, padding: "9px 11px", borderRadius: 8, background: "oklch(0.97 0.02 24)", border: "1px solid oklch(0.85 0.07 24)", color: "oklch(0.45 0.16 24)", fontSize: 12.5 }}>
              {err}
            </div>
          )}
        </div>
        <footer className="modal-foot">
          {done ? (
            <>
              {doneUrl && (
                <a className="btn" href={doneUrl} target="_blank" rel="noreferrer">
                  <IcExternal size={13} /> Open in Shopify
                </a>
              )}
              <button className="btn accent" onClick={onClose}>Done</button>
            </>
          ) : loadErr ? (
            <button className="btn accent" onClick={onClose}>Close</button>
          ) : (
            <>
              <button className="btn" onClick={onClose}>Cancel</button>
              <button className="btn accent" onClick={send} disabled={busy || !picked}>
                {busy ? "Sending…" : "Send to product"}
              </button>
            </>
          )}
        </footer>
      </div>
    </div>
  );
}

function PreviewPage({ asset, onClose, onNav, onPrev, onNext, index, total, onPublish, onOpenAsset, onFindSimilar, onShare, backStack, onBack, onQuickUpload }) {
  // Plan limits drive which action buttons we render. Default to "off" while
  // /api/me is loading so the UI doesn't briefly flash beta-disallowed items.
  const limits = (typeof window.useVandayLimits === "function")
    ? window.useVandayLimits()
    : { sharingEnabled: true, publishingEnabled: true };
  const features = (typeof window.useVandayFeatures === "function")
    ? window.useVandayFeatures()
    : { comments: true, versions: true };
  // Role gates: viewers can't publish/share/delete; contributors can't publish.
  const showPublish = limits.publishingEnabled !== false && features.canPublish;
  const showShare = limits.sharingEnabled !== false && features.canWrite;
  // Server-backed assets always get the comments tab (real DB-backed thread)
  // regardless of the prototype feature flag.
  const showComments = features.comments || (asset && asset.isServer && !asset.isServerVariant);
  const showVersions = features.versions;
  const [tab, setTab] = React.useState("fields");
  const [zoom, setZoom] = React.useState(100);
  // Which built-in Details fields the admin has hidden in Settings → Fields.
  const hiddenFields = useHiddenFields();
  // Right-panel collapse state + the pop-up Comments modal.
  const [panelOpen, setPanelOpen] = React.useState(true);
  const [commentsModalOpen, setCommentsModalOpen] = React.useState(false);
  // Bumped whenever the comments modal closes so the Details-tab comment
  // list (a separate ServerComments instance) remounts and re-fetches —
  // otherwise comments added in the modal wouldn't appear until refresh.
  const [commentsRev, setCommentsRev] = React.useState(0);
  const closeCommentsModal = React.useCallback(() => {
    setCommentsModalOpen(false);
    setCommentsRev((r) => r + 1);
  }, []);
  React.useEffect(() => {
    if (!commentsModalOpen) return;
    const onKey = (e) => { if (e.key === "Escape") closeCommentsModal(); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [commentsModalOpen, closeCommentsModal]);
  // The Crops tab is hidden for videos; if we navigate onto a video while it
  // was the active tab (e.g. arrow keys between assets), fall back to Details.
  // Same for Info, which only makes sense on server-backed image assets, and
  // is also gated behind the iptcMetadata feature flag — if the flag flips
  // off (admin disable, account demotion) we fall back too.
  React.useEffect(() => {
    if (tab === "crops" && assetIsVideo(asset)) setTab("fields");
    if (tab === "info" && (
      assetIsVideo(asset)
      || !(asset && asset.isServer && !asset.isServerVariant)
      || !features.iptcMetadata
    )) {
      setTab("fields");
    }
  }, [asset && asset.id, tab, features.iptcMetadata]);
  const [overlay, setOverlay] = React.useState(null);
  const [sendTarget, setSendTarget] = React.useState(null);
  // "Send to Shopify" — only shown when a store is actually connected.
  const [shopifyPushOpen, setShopifyPushOpen] = React.useState(false);
  const [shopifyConnected, setShopifyConnected] = React.useState(_shopifyConnCache.value === true);
  React.useEffect(() => {
    if (_shopifyConnCache.value !== null) { setShopifyConnected(_shopifyConnCache.value === true); return; }
    let cancelled = false;
    VandayAPI.shopifyStatus?.()
      .then((s) => { _shopifyConnCache.value = !!(s && s.connected); if (!cancelled) setShopifyConnected(_shopifyConnCache.value); })
      .catch(() => { _shopifyConnCache.value = false; });
    return () => { cancelled = true; };
  }, []);
  const [commentDraft, setCommentDraft] = React.useState("");
  // Comments start empty for free beta — fake "Maya / Devon" seed comments
  // were a prototype affordance. Real comment threads will arrive with team
  // workspaces.
  const [comments, setComments] = React.useState(() => (
    Array.isArray(asset.comments) ? asset.comments : []
  ));
  const addComment = () => {
    const body = commentDraft.trim();
    if (!body) return;
    const me = window.VandaySession.getActiveUser();
    const next = [
      ...comments,
      {
        id: `c-${Date.now()}`,
        author: (me && me.name) || "Unknown",
        initials: window.VandaySession.initials(me),
        color: (me && me.color) || null,
        when: "Just now",
        body,
      },
    ];
    setComments(next);
    asset.comments = next;
    setCommentDraft("");
  };

  if (!asset) return null;

  const isCrop = asset.kind === "crop";
  // Videos get no auto-crops, so the Crops tab is hidden for them.
  const isVideo = assetIsVideo(asset);
  const parent = isCrop ? ASSETS.find((p) => p.id === asset.parentId) : asset;
  const siblings = VARIANTS.filter((v) => v.parentId === parent.id);
  const cropAspect = isCrop ? `${asset.w} / ${asset.h}` : null;

  // Shared comment thread (list + input). Used both in the Details tab and
  // the pop-up Comments modal. Server-backed assets get the live thread;
  // sample assets fall back to local in-memory comments.
  const renderCommentsBody = () => (
    asset.isServer && !asset.isServerVariant ? (
      <ServerComments key={`sc-${commentsRev}`} asset={asset} embedded />
    ) : (
      <div style={{ color: "var(--text-muted)", fontSize: 13 }}>
        {comments.length === 0 && (
          <div style={{ padding: "8px 0", fontSize: 12.5, color: "var(--text-faint)", fontStyle: "italic" }}>
            No comments yet.
          </div>
        )}
        {comments.map((c) => (
          <div key={c.id} style={{ display: "flex", gap: 10, marginBottom: 18 }}>
            <span className="pill-user" style={{ background: "transparent", padding: 0 }}>
              <span className="av" style={c.color ? { background: c.color } : undefined}>{c.initials}</span>
            </span>
            <div>
              <div style={{ fontSize: 12.5, color: "var(--text)" }}>
                <strong style={{ fontWeight: 600 }}>{c.author}</strong> · {c.when}
              </div>
              <p style={{ margin: "4px 0 0", color: "var(--text)" }}>{c.body}</p>
            </div>
          </div>
        ))}
        <input
          value={commentDraft}
          onChange={(e) => setCommentDraft(e.target.value)}
          onKeyDown={(e) => {
            if (e.key === "Enter" && !e.shiftKey) {
              e.preventDefault();
              addComment();
            }
          }}
          placeholder="Add a comment…"
          style={{
            width: "100%",
            background: "var(--surface)",
            border: "1px solid var(--border)",
            borderRadius: 8,
            padding: "10px 12px",
            fontSize: 13,
            outline: "none",
          }}
        />
      </div>
    )
  );

  return (
    <div className={"app preview-shell" + (panelOpen ? "" : " panel-collapsed")}>
      <Rail active="library" onNav={onNav} onLogout={() => onNav("login")} onSearch={() => onNav("library")} onNotifs={() => onNav("library")} onHelp={() => onNav("library")} onQuickUpload={onQuickUpload} />

      <main className="main">
        <header className="topbar preview-topbar">
          <div className="preview-topbar-row">
            {backStack && backStack.length > 0 ? (
              <button className="btn ghost sm preview-back" onClick={onBack} title={`Back to ${backStack[backStack.length - 1].name}`}>
                <IcChevL size={14} />
                <span className="preview-back-label">{backStack[backStack.length - 1].name}</span>
              </button>
            ) : (
              <button className="btn ghost sm" onClick={onClose} title="Back to library">
                <IcChevL size={16} />
                <span style={{ fontSize: 12.5 }}>Library</span>
              </button>
            )}
            <div className="crumbs preview-crumbs">
              <span className="sep">/</span>
              <span>{(FOLDERS.find((f) => f.id === asset.folder) || {}).name}</span>
              <span className="sep">/</span>
              {isCrop ? (
                <>
                  <a
                    href="#"
                    onClick={(e) => { e.preventDefault(); onOpenAsset && onOpenAsset(parent); }}
                    className="crumb-file"
                    title={`Open original — ${parent.name}`}
                  >
                    {parent.name}
                  </a>
                  <span className="sep">/</span>
                  <span className="here crumb-file">{asset.ratio} {asset.ratioName}</span>
                </>
              ) : (
                <span className="here crumb-file" title={asset.name}>{asset.name}</span>
              )}
            </div>
            <div className="preview-nav">
              <button className="btn ghost sm" onClick={onPrev} title="Previous"><IcChevL size={14} /></button>
              <span className="preview-nav-counter">
                {index + 1} <span style={{ color: "var(--text-faint)" }}>of</span> {total}
              </span>
              <button className="btn ghost sm" onClick={onNext} title="Next"><IcChevR size={14} /></button>
            </div>
          </div>
          <div className="preview-topbar-row preview-actions">
            <DownloadMenu asset={asset} />
            <SendToMenu asset={parent} onSend={(integration) => setSendTarget(integration)} />
            <button className="btn" onClick={() => onFindSimilar && onFindSimilar(parent)}>
              <IcSparkles size={14} /> Find similar
            </button>
            {asset.isServer && features.canWrite && (
              <button
                className="btn"
                title="Delete this asset"
                onClick={async () => {
                  if (!window.confirm(`Delete "${asset.name}" permanently?`)) return;
                  try { await VandayAPI.deleteAsset(asset.id); } catch {}
                  if (onClose) onClose();
                }}
              >
                <IcTrash size={14} /> Delete
              </button>
            )}
            <span style={{ flex: 1 }} />
            {shopifyConnected && features.canPublish && parent?.isServer && !assetIsVideo(asset) && (
              <button className="btn" onClick={() => setShopifyPushOpen(true)} title="Add this image to a Shopify product">
                <IcExternal size={14} /> Send to Shopify
              </button>
            )}
            {showPublish && (
              <button className="btn" onClick={() => onPublish && onPublish(asset)}>
                <IcShare size={14} /> Publish
              </button>
            )}
            {showShare && (
              <button className="btn accent" onClick={() => onShare && onShare({ asset: parent })}>
                <IcLink size={14} /> Share
              </button>
            )}
          </div>
        </header>

        <div className="preview-canvas">
          {assetIsVideo(asset) ? (
            <video
              src={asset.url}
              controls
              playsInline
              preload="metadata"
            />
          ) : isCrop ? (
            <div
              style={{
                aspectRatio: cropAspect,
                maxHeight: "78%",
                maxWidth: "78%",
                background: "black",
                borderRadius: 4,
                boxShadow: "var(--shadow-lg)",
                overflow: "hidden",
                transform: `scale(${zoom / 100})`,
                transition: "transform 160ms",
              }}
            >
              <img
                src={asset.url}
                alt={asset.name}
                style={{ width: "100%", height: "100%", objectFit: "cover", objectPosition: "center", display: "block" }}
              />
            </div>
          ) : (
            <img
              src={mainPreviewSrc(asset)}
              alt={asset.name}
              className={asset.hasAlpha ? "preview-alpha" : ""}
              style={{ transform: `scale(${zoom / 100})`, transition: "transform 160ms" }}
            />
          )}
          {!isVideo && (
          <div className="preview-toolbar">
            <button onClick={() => setZoom((z) => Math.max(25, z - 25))} title="Zoom out">
              <IcZoomOut size={16} />
            </button>
            <span className="zoom-val">{zoom}%</span>
            <button onClick={() => setZoom((z) => Math.min(400, z + 25))} title="Zoom in">
              <IcZoomIn size={16} />
            </button>
            <span className="sep" />
            <button onClick={() => setZoom(100)} title="Fit"><IcFullscreen size={16} /></button>
            <span className="sep" />
            {showComments && (
              <button title="Comments" onClick={() => setCommentsModalOpen(true)}><IcInfo size={16} /></button>
            )}
          </div>
          )}
        </div>
      </main>

      {!panelOpen && (
        <button
          className="rp-reopen"
          title="Show details panel"
          onClick={() => setPanelOpen(true)}
        >
          <IcChevL size={16} />
        </button>
      )}

      <aside className="right-panel" style={panelOpen ? undefined : { display: "none" }}>
        <div className="rp-tabs">
          <button className={`rp-tab ${tab === "fields" ? "active" : ""}`} onClick={() => setTab("fields")}>
            Details
          </button>
          {!isVideo && asset && asset.isServer && !asset.isServerVariant && features.iptcMetadata && (
            <button className={`rp-tab ${tab === "info" ? "active" : ""}`} onClick={() => setTab("info")}>
              Info
            </button>
          )}
          {!isVideo && (
            <button className={`rp-tab ${tab === "crops" ? "active" : ""}`} onClick={() => setTab("crops")}>
              Crops
            </button>
          )}
          <button className={`rp-tab ${tab === "related" ? "active" : ""}`} onClick={() => setTab("related")}>
            Related
          </button>
          {showVersions && (
            <button className={`rp-tab ${tab === "versions" ? "active" : ""}`} onClick={() => setTab("versions")}>
              Versions
            </button>
          )}
          <button
            className="rp-collapse"
            title="Hide panel"
            onClick={() => setPanelOpen(false)}
          >
            <IcChevR size={16} />
          </button>
        </div>

        {tab === "fields" && (
          <div className="rp-body">
            {isCrop && (
              <div style={{
                background: "var(--surface-soft)",
                border: "1px solid var(--border)",
                borderRadius: 10,
                padding: 12,
                marginBottom: 18,
                display: "flex",
                gap: 12,
                alignItems: "center",
              }}>
                <div style={{
                  width: 44, height: 44, borderRadius: 6,
                  backgroundImage: `url(${parent.url})`,
                  backgroundSize: "cover", backgroundPosition: "center",
                  flexShrink: 0,
                }} />
                <div style={{ minWidth: 0, flex: 1 }}>
                  <div style={{ display: "inline-flex", alignItems: "center", gap: 6, fontSize: 10.5, color: "var(--text-faint)", textTransform: "uppercase", letterSpacing: "0.06em", fontWeight: 500 }}>
                    <IcLink size={11} /> Crop of
                  </div>
                  <a
                    href="#"
                    onClick={(e) => { e.preventDefault(); onOpenAsset && onOpenAsset(parent); }}
                    style={{
                      display: "block",
                      fontFamily: "var(--font-mono)", fontSize: 12,
                      color: "var(--text)", textDecoration: "none",
                      overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap",
                      marginTop: 2,
                    }}
                  >
                    {parent.name}
                  </a>
                </div>
                <button className="btn sm" onClick={() => onOpenAsset && onOpenAsset(parent)}>
                  Open original
                </button>
              </div>
            )}
            {isCrop ? (
              <h3 className="rp-title">{asset.ratioName} · {asset.ratio}</h3>
            ) : (
              <NameEditor asset={asset} />
            )}
            <p className="rp-sub">
              {asset.w} × {asset.h} · {asset.size}
              {!isCrop && ` · uploaded ${asset.date}`}
              {isCrop && asset.sites && (
                <span style={{ display: "inline-flex", gap: 4, marginLeft: 8, verticalAlign: "middle" }}>
                  {asset.sites.map((sid) => <SiteMark key={sid} site={sid} size={12} />)}
                </span>
              )}
            </p>

            <div className="field-grid">
              {!hiddenFields.has("status") && (
              <div className="field-block">
                <div className="field-label"><IcEye size={11} /> Status</div>
                <WorkflowLabelPicker asset={asset} />
              </div>
              )}
              {!hiddenFields.has("date_uploaded") && (
              <div className="field-block">
                <div className="field-label">Date uploaded</div>
                <span className="field-value field-mono">{asset.date}</span>
              </div>
              )}
              {!hiddenFields.has("file_size") && (
              <div className="field-block">
                <div className="field-label">File size</div>
                <span className="field-value field-mono">{asset.size}</span>
              </div>
              )}
              {!hiddenFields.has("dimensions") && (
              <div className="field-block">
                <div className="field-label">Dimensions</div>
                <span className="field-value field-mono">{asset.w} × {asset.h}</span>
              </div>
              )}
            </div>

            {!hiddenFields.has("ai_keywords") && (
            <div className="field-block">
              <div className="field-label">
                <IcSparkles size={11} /> Auto-tagged keywords
                <span className="tag ai" style={{ marginLeft: "auto", fontSize: 9 }}>AI</span>
              </div>
              <TagEditor
                asset={asset}
                onPickTag={(t) => {
                  // Stash the keyword so the library (which remounts when the
                  // preview closes) runs the search as soon as it mounts.
                  window.__vandayPendingSearch = t;
                  if (onClose) onClose();
                }}
              />
            </div>
            )}

            <div className="field-block">
              <div className="field-label">Folder</div>
              <span className="role-pill">
                {(FOLDERS.find((f) => f.id === asset.folder) || {}).name}
              </span>
            </div>

            {(showPublish || (asset.publications && asset.publications.length > 0)) && (
              <div className="field-block">
                <div className="field-label">
                  <IcSend size={11} /> Publications
                  {showPublish && (
                    <button
                      className="btn ghost sm"
                      style={{ marginLeft: "auto", padding: "2px 8px", fontSize: 11 }}
                      onClick={() => onPublish && onPublish(asset)}
                    >
                      <IcShare size={11} /> Publish
                    </button>
                  )}
                </div>
                <PublicationList asset={asset} />
              </div>
            )}

            {/* Comments now live inside Details (no separate tab). */}
            <div className="field-block" style={{ borderTop: "1px solid var(--border)", paddingTop: 16, marginTop: 4 }}>
              <div className="field-label">Comments</div>
              {renderCommentsBody()}
            </div>
          </div>
        )}

        {tab === "info" && <MetadataEditor asset={asset} />}

        {tab === "crops" && (
          <div className="rp-body">
            <h3 className="rp-title" style={{ fontFamily: "var(--font-sans)", fontSize: 14 }}>
              {isCrop ? "Other crops of this asset" : "Auto-generated crops"}
            </h3>
            <p className="rp-sub">
              <IcSparkles size={11} style={{ verticalAlign: "middle", marginRight: 4 }} />
              Subject-aware reframes — click any crop to open it.
            </p>
            <CropsGrid
              asset={asset}
              onOpenVariant={(v) => onOpenAsset && onOpenAsset(v)}
              highlightId={isCrop ? asset.id : null}
            />
            {showPublish && (
              <p className="rp-sub" style={{ marginTop: 16, color: "var(--text-faint)" }}>
                You'll pick which crop to publish (or let Vanday choose) when you hit Publish.
              </p>
            )}
          </div>
        )}

        {tab === "related" && (
          <RelatedTab
            asset={parent}
            onOpenAsset={onOpenAsset}
            onPeek={(a, siblings) => setOverlay({ siblings, idx: siblings.findIndex((s) => s.id === a.id) })}
          />
        )}

        {tab === "versions" && (
          <div className="rp-body" style={{ fontSize: 13, color: "var(--text-muted)" }}>
            {[3, 2, 1].map((v) => {
              const isCurrent = v === 3;
              const baseName = asset.name.replace(/\.[^.]+$/, "");
              const ext = (asset.name.match(/\.[^.]+$/) || [".jpg"])[0];
              const versionAsset = isCurrent ? asset : {
                ...asset,
                id: `${asset.id}-v${v}`,
                name: `${baseName}-v${v}${ext}`,
                date: `May ${v + 6}, 2026`,
                comments: [],
                publications: [],
                isVersionOf: asset.id,
              };
              const open = () => {
                if (isCurrent) return;
                onOpenAsset && onOpenAsset(versionAsset);
              };
              return (
                <button
                  key={v}
                  onClick={open}
                  disabled={isCurrent}
                  title={isCurrent ? "You're viewing this version" : `Open Version ${v}`}
                  style={{
                    display: "flex", gap: 12, padding: "12px 0",
                    width: "100%", textAlign: "left",
                    background: "transparent", border: 0,
                    borderBottom: v > 1 ? "1px solid var(--border)" : 0,
                    cursor: isCurrent ? "default" : "pointer",
                    color: "inherit",
                  }}
                >
                  <div style={{
                    width: 46, height: 46, borderRadius: 6,
                    backgroundImage: `url(${asset.url})`,
                    backgroundSize: "cover", backgroundPosition: "center",
                    flexShrink: 0,
                    outline: isCurrent ? "1.5px solid var(--accent)" : "1px solid var(--border)",
                    outlineOffset: -1,
                  }} />
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div style={{ color: "var(--text)", fontSize: 13, fontWeight: 500 }}>
                      Version {v}{isCurrent ? " (current)" : ""}
                    </div>
                    <div style={{ fontSize: 12, marginTop: 2 }}>{asset.uploader} · May {v + 6}, 2026</div>
                  </div>
                  {!isCurrent && (
                    <span style={{ alignSelf: "center", color: "var(--text-faint)" }}>
                      <IcChevR size={14} />
                    </span>
                  )}
                </button>
              );
            })}
          </div>
        )}
      </aside>

      {overlay && (
        <PreviewOverlay
          siblings={overlay.siblings}
          startIdx={overlay.idx}
          sourceAsset={parent}
          onClose={() => setOverlay(null)}
          onOpenFull={(a) => { setOverlay(null); onOpenAsset && onOpenAsset(a); }}
        />
      )}

      {sendTarget && (
        <SendToModal
          asset={parent}
          integration={sendTarget}
          onClose={() => setSendTarget(null)}
        />
      )}

      {shopifyPushOpen && (
        <ShopifyPushModal asset={parent} onClose={() => setShopifyPushOpen(false)} />
      )}

      {commentsModalOpen && (
        <div className="modal-backdrop" onClick={closeCommentsModal}>
          <div className="modal" onClick={(e) => e.stopPropagation()} style={{ maxWidth: 480 }}>
            <header className="modal-head">
              <div>
                <h2 className="modal-title">Comments</h2>
                <div style={{ fontSize: 12.5, color: "var(--text-muted)", marginTop: 2, fontFamily: "var(--font-mono)" }}>
                  {asset.name}
                </div>
              </div>
              <button className="btn ghost sm" onClick={closeCommentsModal} title="Close">
                <IcClose size={16} />
              </button>
            </header>
            <div className="modal-body" style={{ display: "block", padding: 20, maxHeight: "62vh", overflowY: "auto" }}>
              {renderCommentsBody()}
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

// FT-11 \u2014 Related assets. Stored per-asset in module-scope so the
// relationship persists when you navigate between asset previews
// (without a real backend). In production this would round-trip to
// the API and be bidirectional.
const RELATIONSHIP_KINDS = [
  { id: "alt",     label: "Alternate angle" },
  { id: "ba",      label: "Before \u2192 after" },
  { id: "variant", label: "Variant"          },
  { id: "version", label: "Version"          },
  { id: "master",  label: "Master / derivative" },
  { id: "other",   label: "Other"              },
];

const _relatedStore = {};
function getRelated(assetId) {
  if (_relatedStore[assetId]) return _relatedStore[assetId];
  const self = ASSETS.find((a) => a.id === assetId);
  if (!self) return (_relatedStore[assetId] = []);
  // Real (server) uploads must NEVER auto-seed — the user expects
  // an empty Related list until they manually link something. The
  // demo-data seed below only runs for the prototype's hard-coded
  // mock assets so the empty state isn't every preview's experience.
  if (self.isServer) {
    _relatedStore[assetId] = [];
    return _relatedStore[assetId];
  }
  // Seed with two examples per mock asset on first read so the demo
  // shows what the relationship UI looks like without making the user
  // wire it up by hand.
  const seeds = ASSETS
    .filter((a) => a.id !== assetId && a.folder === self.folder && !a.isServer)
    .slice(0, 2)
    .map((a, i) => ({ id: a.id, kind: i === 0 ? "alt" : "variant" }));
  _relatedStore[assetId] = seeds;
  return seeds;
}
function setRelated(assetId, list) {
  _relatedStore[assetId] = list;
}

// Asset browser modal — used by Related (and reusable for bulk move,
// publish picker, etc). Big grid + search + folder rail + "Find similar"
// toggle so a 10k-asset library is still navigable.
function AssetBrowserModal({
  title,
  subtitle,
  source,         // optional — asset that "Find similar" ranks against
  excludeIds,     // Set of ids to hide (already linked, etc)
  relationshipKinds,
  initialKind,
  onClose,
  onConfirm,
  confirmLabel = "Add",
}) {
  const [query, setQuery] = React.useState("");
  const [folder, setFolder] = React.useState("all");
  const [orient, setOrient] = React.useState("all");
  const [type, setType] = React.useState("all"); // all | images | videos | docs
  const [similar, setSimilar] = React.useState(false);
  const [picked, setPicked] = React.useState(new Set());
  const [kind, setKind] = React.useState(initialKind || (relationshipKinds && relationshipKinds[0].id));

  // Lock body scroll while open
  React.useEffect(() => {
    const prev = document.body.style.overflow;
    document.body.style.overflow = "hidden";
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    window.addEventListener("keydown", onKey);
    return () => {
      document.body.style.overflow = prev;
      window.removeEventListener("keydown", onKey);
    };
  }, [onClose]);

  let pool = ASSETS.filter((a) => !excludeIds || !excludeIds.has(a.id));
  if (source) pool = pool.filter((a) => a.id !== source.id);
  if (folder !== "all") pool = pool.filter((a) => a.folder === folder);
  if (orient !== "all") {
    pool = pool.filter((a) => {
      const r = a.w / a.h;
      const o = Math.abs(r - 1) < 0.06 ? "square" : r > 1 ? "landscape" : "portrait";
      return o === orient;
    });
  }
  if (query) {
    const q = query.toLowerCase();
    pool = pool.filter((a) =>
      a.name.toLowerCase().includes(q) ||
      (a.tags || []).some((t) => t.toLowerCase().includes(q)) ||
      (a.uploader || "").toLowerCase().includes(q)
    );
  }

  // "Find similar" — rank by tag overlap + folder match with source.
  if (similar && source) {
    const srcTags = new Set(source.tags || []);
    pool = pool
      .map((a) => {
        const overlap = (a.tags || []).filter((t) => srcTags.has(t)).length;
        const folderBonus = a.folder === source.folder ? 1 : 0;
        return { a, score: overlap * 2 + folderBonus };
      })
      .filter((x) => x.score > 0)
      .sort((x, y) => y.score - x.score)
      .map((x) => x.a);
  }

  const togglePick = (id, withShift) => {
    setPicked((s) => {
      const next = new Set(s);
      if (next.has(id)) next.delete(id);
      else next.add(id);
      return next;
    });
  };

  const folderTotals = React.useMemo(() => {
    const m = { all: 0 };
    ASSETS.forEach((a) => {
      m.all += 1;
      m[a.folder] = (m[a.folder] || 0) + 1;
    });
    return m;
  }, []);

  return (
    <div className="modal-backdrop" onClick={onClose}>
      <div
        className="modal browser-modal"
        onClick={(e) => e.stopPropagation()}
      >
        <div className="modal-head">
          <div>
            <h2 className="modal-title">{title}</h2>
            {subtitle && (
              <div style={{ fontSize: 12, color: "var(--text-muted)", marginTop: 4 }}>
                {subtitle}
              </div>
            )}
          </div>
          <button className="btn ghost sm" onClick={onClose}>
            <IcClose size={14} />
          </button>
        </div>

        {/* Toolbar */}
        <div className="browser-toolbar">
          <div className="browser-search">
            <IcSearch size={14} />
            <input
              autoFocus
              placeholder="Search by filename, tag, or uploader…"
              value={query}
              onChange={(e) => setQuery(e.target.value)}
            />
            {query && (
              <button className="browser-clear" onClick={() => setQuery("")} title="Clear">
                <IcClose size={11} />
              </button>
            )}
          </div>

          {source && (
            <button
              className={`browser-similar ${similar ? "is-on" : ""}`}
              onClick={() => setSimilar((v) => !v)}
              title="Rank by visual similarity to the source asset"
            >
              <div
                className="browser-similar-thumb"
                style={{ backgroundImage: `url(${source.url})` }}
              />
              <span>
                <span className="browser-similar-h">
                  <IcSparkles size={11} /> Find similar
                </span>
                <span className="browser-similar-sub">
                  {similar ? "Ranked by visual + tag match" : `to ${source.name}`}
                </span>
              </span>
              <span className={`toggle ${similar ? "on" : ""}`} style={{ marginLeft: "auto" }} />
            </button>
          )}

          <div className="browser-segments">
            {[
              { id: "all",    label: "All"       },
              { id: "images", label: "Images"    },
              { id: "videos", label: "Videos"    },
              { id: "docs",   label: "Documents" },
            ].map((t) => (
              <button
                key={t.id}
                className={`browser-seg ${type === t.id ? "is-on" : ""}`}
                onClick={() => setType(t.id)}
              >
                {t.label}
              </button>
            ))}
          </div>
          <div className="orient-group">
            {[
              { id: "all",       svg: <span style={{ display: "inline-block", width: 12, height: 12, border: "1.5px solid currentColor", borderRadius: 2 }} />, label: "Any orientation" },
              { id: "landscape", svg: <span style={{ display: "inline-block", width: 14, height: 10, border: "1.5px solid currentColor", borderRadius: 2 }} />, label: "Landscape — wider than tall" },
              { id: "portrait",  svg: <span style={{ display: "inline-block", width: 10, height: 14, border: "1.5px solid currentColor", borderRadius: 2 }} />, label: "Portrait — taller than wide" },
              { id: "square",    svg: <span style={{ display: "inline-block", width: 12, height: 12, border: "1.5px solid currentColor", borderRadius: 2 }} />, label: "Square — within ±5%" },
            ].map((o) => (
              <button
                key={o.id}
                className={`orient-btn ${orient === o.id ? "is-on" : ""}`}
                onClick={() => setOrient(o.id)}
                title={o.label}
                aria-label={o.label}
              >
                {o.svg}
                <span className="orient-tip">{o.label}</span>
              </button>
            ))}
          </div>
        </div>

        {/* Body: folder rail + result grid */}
        <div className="browser-body">
          <aside className="browser-rail">
            <div className="browser-rail-h">Folders</div>
            <button
              className={`browser-folder ${folder === "all" ? "is-on" : ""}`}
              onClick={() => setFolder("all")}
            >
              <span><IcGrid size={13} /> All assets</span>
              <span className="browser-folder-count">{folderTotals.all}</span>
            </button>
            {FOLDERS.slice(1).map((f) => (
              <button
                key={f.id}
                className={`browser-folder ${folder === f.id ? "is-on" : ""}`}
                onClick={() => setFolder(f.id)}
              >
                <span><IcFolder size={13} /> {f.name}</span>
                <span className="browser-folder-count">{folderTotals[f.id] || 0}</span>
              </button>
            ))}
          </aside>

          <div className="browser-grid-wrap">
            {similar && source && (
              <div className="browser-similar-bar">
                <IcSparkles size={12} />
                Showing visually similar to <strong>{source.name}</strong> · ranked by combined embedding + tag overlap
                <button onClick={() => setSimilar(false)}>Turn off</button>
              </div>
            )}
            {pool.length === 0 ? (
              <div className="browser-empty">
                <div style={{ marginBottom: 8 }}><IcSearch size={22} /></div>
                Nothing matches these filters.
                <button
                  className="btn sm"
                  style={{ marginTop: 12 }}
                  onClick={() => { setQuery(""); setFolder("all"); setOrient("all"); setSimilar(false); }}
                >
                  Reset filters
                </button>
              </div>
            ) : (
              <div className="browser-grid">
                {pool.map((a) => {
                  const isOn = picked.has(a.id);
                  return (
                    <button
                      key={a.id}
                      className={`browser-card ${isOn ? "is-on" : ""}`}
                      onClick={(e) => togglePick(a.id, e.shiftKey)}
                    >
                      <div className="browser-card-thumb">
                        <img src={a.url} alt="" loading="lazy" />
                        <span className={`browser-card-check ${isOn ? "is-on" : ""}`}>
                          {isOn && <IcCheck size={11} />}
                        </span>
                      </div>
                      <div className="browser-card-body">
                        <div className="browser-card-name">{a.name}</div>
                        <div className="browser-card-meta">
                          <span>{a.uploader}</span>
                          <span>·</span>
                          <span style={{ fontFamily: "var(--font-mono)" }}>
                            {a.w}×{a.h}
                          </span>
                        </div>
                      </div>
                    </button>
                  );
                })}
              </div>
            )}
          </div>
        </div>

        <div className="modal-foot browser-foot">
          <div style={{ fontSize: 12.5, color: "var(--text-muted)", display: "flex", alignItems: "center", gap: 14 }}>
            <span>
              <strong style={{ color: "var(--text)", fontFamily: "var(--font-mono)" }}>
                {picked.size}
              </strong>{" "}
              selected
            </span>
            <span style={{ color: "var(--text-faint)" }}>·</span>
            <span>{pool.length.toLocaleString()} of {ASSETS.length.toLocaleString()} shown</span>
            {picked.size > 0 && (
              <button
                className="bulk-link"
                style={{ color: "var(--text-muted)", padding: "2px 6px" }}
                onClick={() => setPicked(new Set())}
              >
                Clear
              </button>
            )}
          </div>
          <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
            {relationshipKinds && (
              <>
                <span style={{ fontSize: 12, color: "var(--text-muted)" }}>{confirmLabel}</span>
                <select
                  className="related-kind-select is-large"
                  value={kind}
                  onChange={(e) => setKind(e.target.value)}
                >
                  {relationshipKinds.map((k) => (
                    <option key={k.id} value={k.id}>{k.label}</option>
                  ))}
                </select>
              </>
            )}
            <button className="btn" onClick={onClose}>Cancel</button>
            <button
              className="btn accent"
              disabled={picked.size === 0}
              style={picked.size === 0 ? { opacity: 0.5, cursor: "not-allowed" } : undefined}
              onClick={() => onConfirm(Array.from(picked), kind)}
            >
              {relationshipKinds ? `Link ${picked.size || ""}` : `${confirmLabel} ${picked.size || ""}`}
            </button>
          </div>
        </div>
      </div>
    </div>
  );
}

window.AssetBrowserModal = AssetBrowserModal;

function RelatedTab({ asset, onOpenAsset, onPeek }) {
  const [items, setItems] = React.useState(() => getRelated(asset.id));
  const [picking, setPicking] = React.useState(false);
  const [pendingKind, setPendingKind] = React.useState("alt");

  // Sync back to store whenever items change (so other previews see updates).
  React.useEffect(() => { setRelated(asset.id, items); }, [items, asset.id]);

  // Reset when switching to a different asset.
  React.useEffect(() => {
    setItems(getRelated(asset.id));
    setPicking(false);
  }, [asset.id]);

  const linkedIds = new Set(items.map((r) => r.id));

  const removeOne = (id) => setItems((xs) => xs.filter((r) => r.id !== id));
  const addMany = (ids, kind) => {
    setPendingKind(kind);
    setItems((xs) => [
      ...xs,
      ...ids.filter((id) => !linkedIds.has(id)).map((id) => ({ id, kind })),
    ]);
    setPicking(false);
  };
  const changeKind = (id, kind) => {
    setItems((xs) => xs.map((r) => (r.id === id ? { ...r, kind } : r)));
  };

  // Download this asset plus everything linked to it — with the same
  // format-conversion choices (Original / JPG / PNG) as individual and batch
  // downloads, routed through the same VandayServer.downloadMany helper.
  const [dlBusy, setDlBusy] = React.useState(false);
  const [dlOpen, setDlOpen] = React.useState(false);
  const dlRef = React.useRef(null);
  React.useEffect(() => {
    if (!dlOpen) return;
    const onClick = (e) => { if (dlRef.current && !dlRef.current.contains(e.target)) setDlOpen(false); };
    const onKey = (e) => { if (e.key === "Escape") setDlOpen(false); };
    window.addEventListener("mousedown", onClick);
    window.addEventListener("keydown", onKey);
    return () => { window.removeEventListener("mousedown", onClick); window.removeEventListener("keydown", onKey); };
  }, [dlOpen]);
  const downloadAll = async (format) => {
    setDlOpen(false);
    setDlBusy(true);
    try {
      const all = [asset, ...items.map((r) => ASSETS.find((x) => x.id === r.id)).filter(Boolean)];
      await VandayServer.downloadMany(all, format);
    } finally { setDlBusy(false); }
  };

  return (
    <div className="rp-body" style={{ fontSize: 13 }}>
      <div style={{ display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: 12 }}>
        <div style={{ minWidth: 0 }}>
          <h3 className="rp-title" style={{ fontFamily: "var(--font-sans)", fontSize: 14 }}>
            Related assets
          </h3>
          <p className="rp-sub">
            Manually linked variants, alts and before/afters. Linked both ways.
          </p>
        </div>
        {items.length > 0 && (
          <div ref={dlRef} style={{ position: "relative", flexShrink: 0 }}>
            <button className="btn sm" disabled={dlBusy} onClick={() => setDlOpen((v) => !v)} title="Download this asset and all related assets">
              <IcDownload size={13} /> {dlBusy ? "Preparing…" : `Download all (${items.length + 1})`}
              <IcChevD size={11} style={{ marginLeft: 2 }} />
            </button>
            {dlOpen && (
              <div className="send-menu" style={{ right: 0, left: "auto" }}>
                <div className="send-menu-h">Download as</div>
                <button className="send-menu-item" onClick={() => downloadAll("original")}>
                  <div style={{ flex: 1 }}>
                    <div className="send-menu-name">Original</div>
                    <div className="send-menu-account">Keep each file's source format</div>
                  </div>
                </button>
                <button className="send-menu-item" onClick={() => downloadAll("jpg")}>
                  <div style={{ flex: 1 }}>
                    <div className="send-menu-name">JPG</div>
                    <div className="send-menu-account">Smaller files, no transparency</div>
                  </div>
                </button>
                <button className="send-menu-item" onClick={() => downloadAll("png")}>
                  <div style={{ flex: 1 }}>
                    <div className="send-menu-name">PNG</div>
                    <div className="send-menu-account">Lossless, keeps transparency</div>
                  </div>
                </button>
              </div>
            )}
          </div>
        )}
      </div>

      {items.length === 0 && !picking && (
        <div style={{
          padding: 24, textAlign: "center",
          background: "var(--surface-soft)",
          border: "1px dashed var(--border-strong)",
          borderRadius: 10,
          color: "var(--text-faint)",
          fontSize: 12.5,
          marginBottom: 10,
        }}>
          <div style={{ marginBottom: 6 }}>
            <IcLink size={20} style={{ color: "var(--text-faint)" }} />
          </div>
          No related assets yet. Use <strong style={{ color: "var(--text)", fontWeight: 600 }}>+ Add related</strong> to link one.
        </div>
      )}

      {items.map((rel) => {
        const a = ASSETS.find((x) => x.id === rel.id);
        if (!a) return null;
        const kind = RELATIONSHIP_KINDS.find((k) => k.id === rel.kind) || RELATIONSHIP_KINDS[0];
        return (
          <div key={rel.id} className="related-row">
            <button
              className="related-row-main"
              onClick={() => onPeek ? onPeek(a, items.map((r) => ASSETS.find((x) => x.id === r.id)).filter(Boolean)) : onOpenAsset && onOpenAsset(a)}
              title="Peek at related asset"
            >
              <div className="related-thumb" style={{ backgroundImage: `url(${a.url})` }} />
              <div style={{ minWidth: 0, flex: 1 }}>
                <div className="related-kind-row">
                  <IcLink size={10} />
                  <select
                    className="related-kind-select"
                    value={rel.kind}
                    onClick={(e) => e.stopPropagation()}
                    onChange={(e) => { e.stopPropagation(); changeKind(rel.id, e.target.value); }}
                  >
                    {RELATIONSHIP_KINDS.map((k) => (
                      <option key={k.id} value={k.id}>{k.label}</option>
                    ))}
                  </select>
                </div>
                <div className="related-name">{a.name}</div>
                <div className="related-meta">Linked by {a.uploader} · {a.date}</div>
              </div>
            </button>
            <button
              className="related-remove"
              onClick={() => removeOne(rel.id)}
              title="Remove relationship"
              aria-label={`Remove relationship to ${a.name}`}
            >
              <IcClose size={13} />
            </button>
          </div>
        );
      })}

      <button
        className="btn btn-block"
        style={{ marginTop: 14, justifyContent: "center", borderStyle: "dashed", color: "var(--text-muted)" }}
        onClick={() => setPicking(true)}
      >
        <IcPlus size={13} /> Add related asset
      </button>

      {picking && (
        <AssetBrowserModal
          title="Link related assets"
          subtitle={`Browse, filter, multi-select. ${ASSETS.length.toLocaleString()} assets in this workspace.`}
          source={asset}
          excludeIds={linkedIds}
          relationshipKinds={RELATIONSHIP_KINDS}
          initialKind={pendingKind}
          onClose={() => setPicking(false)}
          onConfirm={(ids, kind) => addMany(ids, kind)}
          confirmLabel="Link as"
        />
      )}

      <div style={{ marginTop: 22, padding: "14px 16px", background: "var(--surface-soft)", borderRadius: 10, fontSize: 12, color: "var(--text-muted)", lineHeight: 1.5 }}>
        <strong style={{ color: "var(--text)", fontWeight: 600 }}>Tip.</strong> In the browser, toggle <strong style={{ color: "var(--text)", fontWeight: 600 }}>Find similar</strong> to pre-rank by visual similarity to this asset — useful when your library has thousands of items.
      </div>
    </div>
  );
}

function PreviewOverlay({ siblings, startIdx, sourceAsset, onClose, onOpenFull }) {
  const [idx, setIdx] = React.useState(Math.max(0, startIdx));
  const safeIdx = Math.max(0, Math.min(idx, siblings.length - 1));
  const current = siblings[safeIdx];

  // Keyboard navigation \u2014 arrows + esc, scoped while overlay is open.
  React.useEffect(() => {
    const onKey = (e) => {
      if (e.key === "Escape") { e.stopPropagation(); onClose(); }
      else if (e.key === "ArrowLeft" && safeIdx > 0) { e.stopPropagation(); setIdx((i) => i - 1); }
      else if (e.key === "ArrowRight" && safeIdx < siblings.length - 1) { e.stopPropagation(); setIdx((i) => i + 1); }
    };
    // Capture phase so we beat the parent PreviewPage's own arrow handlers.
    window.addEventListener("keydown", onKey, true);
    document.body.style.overflow = "hidden";
    return () => {
      window.removeEventListener("keydown", onKey, true);
      document.body.style.overflow = "";
    };
  }, [safeIdx, siblings.length, onClose]);

  if (!current) return null;

  const prev = () => safeIdx > 0 && setIdx((i) => i - 1);
  const next = () => safeIdx < siblings.length - 1 && setIdx((i) => i + 1);

  return (
    <div className="overlay-backdrop" onClick={onClose}>
      <header className="overlay-topbar" onClick={(e) => e.stopPropagation()}>
        <div className="overlay-crumbs">
          <span className="overlay-source-thumb" style={{ backgroundImage: `url(${sourceAsset.url})` }} />
          <span className="overlay-crumb-text">
            <span style={{ color: "var(--text-faint)" }}>Peeking from</span>
            <span className="overlay-source-name">{sourceAsset.name}</span>
          </span>
        </div>
        <div className="overlay-position">
          <button
            className="overlay-nav-btn"
            onClick={prev}
            disabled={safeIdx === 0}
            title="Previous related (\u2190)"
          >
            <IcChevL size={16} />
          </button>
          <span className="overlay-counter">
            Related <strong>{safeIdx + 1}</strong> of {siblings.length}
          </span>
          <button
            className="overlay-nav-btn"
            onClick={next}
            disabled={safeIdx === siblings.length - 1}
            title="Next related (\u2192)"
          >
            <IcChevR size={16} />
          </button>
        </div>
        <div className="overlay-actions">
          <button className="btn sm" onClick={() => onOpenFull(current)}>
            Open full preview
          </button>
          <button className="overlay-close" onClick={onClose} title="Close (Esc)">
            <IcClose size={16} />
          </button>
        </div>
      </header>

      <div className="overlay-stage" onClick={onClose}>
        <button
          className="overlay-side-nav left"
          onClick={(e) => { e.stopPropagation(); prev(); }}
          disabled={safeIdx === 0}
          title="Previous (\u2190)"
        >
          <IcChevL size={22} />
        </button>
        <img
          className="overlay-image"
          src={current.url}
          alt={current.name}
          onClick={(e) => e.stopPropagation()}
        />
        <button
          className="overlay-side-nav right"
          onClick={(e) => { e.stopPropagation(); next(); }}
          disabled={safeIdx === siblings.length - 1}
          title="Next (\u2192)"
        >
          <IcChevR size={22} />
        </button>
      </div>

      <div className="overlay-meta" onClick={(e) => e.stopPropagation()}>
        <div>
          <div className="overlay-asset-name">{current.name}</div>
          <div className="overlay-asset-sub">
            {current.w} × {current.h} · {current.size} · by {current.uploader}
          </div>
        </div>
        <div className="overlay-tags">
          {(current.tags || []).slice(0, 6).map((t) => (
            <span key={t} className="tag ai">
              <IcSparkles size={9} style={{ verticalAlign: "middle", marginRight: 3 }} />
              {t}
            </span>
          ))}
        </div>
      </div>

      <div className="overlay-filmstrip" onClick={(e) => e.stopPropagation()}>
        {siblings.map((s, i) => (
          <button
            key={s.id}
            className={`overlay-strip-thumb ${i === safeIdx ? "is-on" : ""}`}
            style={{ backgroundImage: `url(${s.url})` }}
            onClick={() => setIdx(i)}
            title={s.name}
          />
        ))}
      </div>
    </div>
  );
}

function captionFor(asset) {
  const captions = {
    a01: "Wide harbor scene at dawn, low fog over moored boats, soft cool color palette with muted grays and blues.",
    a02: "Studio headshot of a woman against a neutral gray backdrop, soft key light from camera left.",
    a03: "Flatlay of handmade ceramic bowls and cups arranged on a linen surface, natural overhead light.",
    a04: "Group of colleagues walking together outdoors, candid stride, golden-hour backlight.",
    a05: "Overhead view of a wooden desk with an open laptop, coffee mug and notebook.",
  };
  return captions[asset.id] || `Auto-generated description of ${asset.name.replace(/\.[^.]+$/, "")}. Subjects, colors and composition detected by Vanday's vision model.`;
}

window.PreviewPage = PreviewPage;
