/* global React, ReactDOM */

// v06i — GridDetailModal. Hugo: clicking a grid in the Media library
// should open its OWN popup (mirroring the shot popup) rather than
// either the shot popup or the AssetReviewModal. This component is
// modelled directly on ShotDetailModal but focused on a single grid
// asset_version + its 4 frame candidates ("slices").
//
// Data flow:
//   - Receives one `gridVersion` row (the clicked tile) on open.
//   - Fetches every asset_version for that asset_id (one HTTP round
//     trip) and splits into:
//       grids[]  = kind === "grid"        → version filmstrip
//       slices[] = kind === "frame" and label starts with `<grid>_f`
//         → the four candidate frames for the active grid
//   - User can swap between the full grid and each individual slice
//     in the hero by clicking the slice picker beneath the hero.

function GridDetailModal({ gridVersion, onClose }) {
  // v07zz72 — `if (!gridVersion) return null` USED to live here, above
  // every hook. That made the hook count zero on a null-prop render
  // and seven on a defined-prop render — the same crash class as
  // ScriptView v07zz65 / ScheduleView v07zz72. The fix: read the asset
  // id defensively so all the hooks below see SOMETHING even when the
  // prop is null, then put the early-return AFTER every hook.
  const _safeAssetId = (gridVersion && gridVersion.asset_id) || "__none__";
  const fetcher = window.authFetch || fetch;
  // v07zm — Cache grid asset_versions across opens. Same pattern as
  // ShotDetailModal: window-level Map + sessionStorage mirror so a
  // re-open of the same grid asset is instant (no fetch wait).
  const cacheKey = "grid-asset-versions:" + _safeAssetId;
  const _initialAllVersions = React.useMemo(() => {
    if (!gridVersion) return [];
    const memCached = window.__assetVersionCache && window.__assetVersionCache.get(_safeAssetId);
    if (Array.isArray(memCached) && memCached.length) return memCached;
    try {
      const raw = sessionStorage.getItem(cacheKey);
      if (raw) {
        const parsed = JSON.parse(raw);
        if (Array.isArray(parsed) && parsed.length) {
          window.__assetVersionCache = window.__assetVersionCache || new Map();
          window.__assetVersionCache.set(_safeAssetId, parsed);
          return parsed;
        }
      }
    } catch (_) {}
    return [];
  }, [_safeAssetId]);
  const [allVersions, setAllVersions] = React.useState(_initialAllVersions);
  const [activeBase, setActiveBase]   = React.useState((gridVersion && gridVersion.version_label) || "v001");
  // activeSlice: null = show the full grid; 1-4 = show that f# slice.
  const [activeSlice, setActiveSlice] = React.useState(null);
  // v06m — drag-scroll callback ref + element ref pair.
  const versionEl = React.useRef(null);
  const versionScrollerRef = (window.useDragScroll
    ? window.useDragScroll(versionEl)
    : (el) => { versionEl.current = el; });

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

  const reloadAssetVersions = React.useCallback(() => {
    if (!gridVersion) return;
    fetcher(`/api/asset-versions?asset_id=${encodeURIComponent(_safeAssetId)}`)
      .then(r => r.ok ? r.json() : null)
      .then(d => {
        const versions = (d && d.versions) || [];
        setAllVersions(versions);
        try {
          window.__assetVersionCache = window.__assetVersionCache || new Map();
          window.__assetVersionCache.set(_safeAssetId, versions);
          sessionStorage.setItem(cacheKey, JSON.stringify(versions));
        } catch (_) {}
      })
      .catch(() => setAllVersions([]));
  }, [_safeAssetId, fetcher, cacheKey, gridVersion]);
  React.useEffect(() => { reloadAssetVersions(); }, [reloadAssetVersions]);

  // v07zz72 — Now safe to early-return: every hook above ran on the
  // null-prop render too (with safe fallbacks). React sees the same
  // hook count both ways → no crash on prop toggle.
  if (!gridVersion) return null;

  // v06k — Hugo: previous version was sorting newest-first AND not
  // deduping. The DB has multiple rows per grid version when a grid
  // spans multiple shots (each shot in the SH####-SH#### range gets
  // its own asset_versions row, all pointing at the same physical
  // file). Dedupe by version_label so each version shows once;
  // ascending sort so the strip reads v001 → v002 → … left-to-right.
  const gridsSorted = allVersions
    .filter(v => v.kind === "grid")
    .sort((a, b) => (a.version_number || 0) - (b.version_number || 0));
  const seenGrid = new Set();
  const grids = [];
  for (const g of gridsSorted) {
    const key = g.version_label || `id-${g.id}`;
    if (seenGrid.has(key)) continue;
    seenGrid.add(key);
    grids.push(g);
  }

  const activeGrid = grids.find(g => g.version_label === activeBase) || grids[0] || null;
  // v06r — Hugo: slice mismatch fix. Was filtering only by version_label
  // prefix ("v001_f"), which picked up slices whose label SAID v001_f1
  // but whose underlying file_path was actually from a different grid
  // version (mis-ingested rows). Now we ALSO require the slice's
  // file_path basename to contain the same `_v<N>_` token as the grid's
  // own file_path — so v001's slices come exclusively from v001 files.
  // Falls back to label-only matching when neither has a file_path.
  const gridVerToken = activeGrid
    ? (() => {
        const fp = activeGrid.file_path || "";
        const m = String(fp).match(/_v(\d{1,3})(?:[._-]|$)/i);
        return m ? `_v${m[1]}_` : null; // e.g. "_v001_"
      })()
    : null;
  const slicesRaw = activeGrid
    ? allVersions.filter(v => {
        // v07zw — Slices can be kind="frame" OR kind="hero" (after a
        // promotion). The original filter only matched "frame" so
        // promoted slices vanished from the F1-F4 picker.
        if ((v.kind !== "frame" && v.kind !== "hero") || !v.version_label) return false;
        const labelOk = v.version_label.toLowerCase()
          .startsWith((activeGrid.version_label + "_f").toLowerCase());
        if (!labelOk) return false;
        // Extra: when both have file paths, require version-token agreement.
        if (gridVerToken && v.file_path) {
          // Lowercase compare for case insensitivity.
          if (!v.file_path.toLowerCase().includes(gridVerToken.toLowerCase())) return false;
        }
        return true;
      })
    : [];
  const seenSlice = new Set();
  const slices = [];
  for (const s of slicesRaw.sort((a, b) => (a.version_label || "").localeCompare(b.version_label || ""))) {
    const key = s.version_label;
    if (seenSlice.has(key)) continue;
    seenSlice.add(key);
    slices.push(s);
  }

  // Determine what's shown in the hero area.
  const slicePick = activeSlice
    ? slices.find(s => new RegExp(`_f${activeSlice}$`, "i").test(s.version_label || ""))
    : null;
  const heroVersion = slicePick || activeGrid;
  const heroSrc = heroVersion ? heroVersion.file_path : null;
  // For the reveal-folder button — convert to /local/... if the path
  // isn't already a URL.
  const revealSrc = (() => {
    if (!heroSrc) return null;
    if (/^https?:\/\//i.test(heroSrc)) return null;
    if (heroSrc.startsWith("/local/") || heroSrc.startsWith("/")) return heroSrc;
    if (heroSrc.startsWith("assets/")) return heroSrc;
    return `/local/${heroSrc}`;
  })();
  const RevealBtn = window.FileActionBtns || window.RevealInFolderBtn;

  // Active grid metadata.
  const promptText = activeGrid ? (activeGrid.prompt_text || "") : "";
  const modelStr   = activeGrid ? (activeGrid.model || "—") : "—";
  const resStr     = activeGrid ? (activeGrid.resolution || "—") : "—";
  const refsList   = activeGrid && Array.isArray(activeGrid.reference_paths)
    ? activeGrid.reference_paths : [];

  const NotesPanelComp = window.NotesPanel;
  const portalRoot = document.getElementById("modal-root") || document.body;

  // v07zz418 — Cold-open guard. Opened from the Media library the asset-version cache is
  // pre-warmed, so activeGrid resolves on the first render. Opened from elsewhere (e.g. the
  // Shot Queue's grid rows) there's no cache → the first render has allVersions empty →
  // activeGrid null, and the body below dereferences activeGrid (the F1–F4 picker + hero) and
  // crashed. Render a minimal loading shell until reloadAssetVersions() (mount effect) resolves.
  if (!activeGrid) {
    return ReactDOM.createPortal((
      <div className="modal-backdrop" onClick={onClose}>
        <div className="modal-card glass grid-detail-modal" onClick={(e) => e.stopPropagation()}>
          <button className="modal-close-btn" aria-label="Close" onClick={onClose}>
            <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M5 5l14 14M19 5L5 19"/></svg>
          </button>
          <div className="modal-header">
            <div className="md-id">{gridVersion.asset_id}</div>
            <div className="md-frame">Grid · {activeBase}</div>
          </div>
          <div className="modal-hero-image" style={{ background: "var(--shade-53)" }} />
        </div>
      </div>
    ), portalRoot);
  }

  return ReactDOM.createPortal((
    <div className="modal-backdrop" onClick={onClose}>
      <div className="modal-card glass grid-detail-modal" onClick={(e) => e.stopPropagation()}>
        <button className="modal-close-btn" aria-label="Close" onClick={onClose}>
          <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M5 5l14 14M19 5L5 19"/></svg>
        </button>

        <div className="modal-header">
          <div className="md-id">{gridVersion.asset_id}</div>
          <div className="md-frame">
            Grid · {activeBase}{activeSlice ? ` · F${activeSlice}` : ""}
          </div>
          <div className="md-chip">{gridVersion.shot_frame_title || ""}</div>
        </div>

        {/* v07za — Hero ALWAYS rendered with its 21:9 aspect so the
            modal height is identical whether or not data has loaded.
            When heroSrc is missing we just show the dark backdrop —
            no shrinkage, no "No image available" placeholder text. */}
        <div className="modal-hero-image" style={{background: "var(--shade-53)"}}>
          <div className="mhi-action-cluster">
            {RevealBtn && revealSrc && <RevealBtn src={revealSrc} label="Open in file explorer"/>}
          </div>
          {heroSrc && (() => {
            // v07zz40/41 — same fade gate + warm registry as the shot modal hero
            // (see modal.css .mhi-img): instant when cached/prefetched, fade only
            // when genuinely cold. window.__warmedThumbs is the source of truth.
            const _hu = window.thumbUrl ? window.thumbUrl(heroSrc, 800) : heroSrc;
            const _w = !!(window.__warmedThumbs && window.__warmedThumbs.has(_hu));
            return <img
              className={"mhi-img" + (_w ? " mhi-warm" : "")}
              src={_hu}
              alt={heroVersion.version_label}
              loading="lazy"
              ref={(el) => {
                if (!el) return;
                if (_w || (el.complete && el.naturalWidth > 0)) { el.classList.add("mhi-warm"); return; }
                setTimeout(() => { try { el.classList.add("mhi-loaded"); } catch (_) {} }, 3000);
              }}
              onLoad={(e) => {
                try { (window.__warmedThumbs = window.__warmedThumbs || new Set()).add(_hu); } catch (_) {}
                if (!e.currentTarget.classList.contains("mhi-warm")) e.currentTarget.classList.add("mhi-loaded");
              }}
              onError={(e) => e.currentTarget.classList.add("mhi-warm")}
            />;
          })()}
          <span className="mhi-tag">
            {activeGrid ? activeGrid.version_label : "—"}
            {activeSlice ? ` · F${activeSlice}` : " · GRID"}
          </span>
        </div>

        {/* v06i — slice picker. Tile 0 shows the full grid; tiles 1-4
            show the candidate frames. Clicking switches the hero. */}
        <div className="grid-slice-row">
          <button
            type="button"
            className={"grid-slice" + (activeSlice === null ? " is-active" : "")}
            onClick={() => setActiveSlice(null)}
            title="Show full grid"
          >
            {activeGrid && activeGrid.file_path
              ? <img src={window.thumbUrl ? window.thumbUrl(activeGrid.file_path, 240) : activeGrid.file_path} alt="grid" loading="lazy"/>
              : <span className="grid-slice-placeholder">·</span>}
            <span className="grid-slice-label">GRID</span>
          </button>
          {/* v07perf — Hugo bug train:
              - "all the slices are heroed by default" — legacy
                group-promote left every f# as kind="hero". Boot
                migration now demotes to one-per-version, but for
                rendering we ALSO compute the preferred slice purely
                from saved prefs / kind="hero" so legacy multi-hero
                data still shows just one star.
              - "previously hero-ed slice from the previous version
                returns to its previous state" — preference now lives
                in shots.slice_prefs and persists across version
                heroing. Renders here from shot.slice_prefs lookup
                even when the version isn't the current shot hero,
                so Hugo can see what would be re-promoted later. */}
          {(() => null)()}
          {[1,2,3,4].map(i => {
            const s = slices.find(slc => new RegExp(`_f${i}$`, "i").test(slc.version_label || ""));
            // Determine the preferred slice number for the active
            // grid's base version. Priority:
            //   1. Saved shot.slice_prefs[base] (user picked it
            //      explicitly via the grid modal at some point).
            //   2. Whichever f# currently has kind="hero" (covers
            //      the case where the version IS the shot hero but
            //      no explicit pref was saved).
            //   3. None — no star.
            const shotPrefs = (() => {
              try {
                const data = window.__appData && window.__appData.shots;
                const s2 = Array.isArray(data) ? data.find(sh => sh.id === gridVersion.asset_id) : null;
                return (s2 && s2.slice_prefs) || {};
              } catch (_) { return {}; }
            })();
            const savedPref = shotPrefs[activeGrid.version_label];
            const heroSlice = slices.find(slc => slc.kind === "hero");
            const heroSliceN = heroSlice
              ? parseInt((String(heroSlice.version_label || "").match(/_f([1-4])$/i) || [])[1], 10)
              : null;
            const preferredF = Number.isFinite(savedPref) ? savedPref : heroSliceN;
            const isHeroSlice = preferredF === i;
            // v07perf — Hugo: "I dont like that it ads a little pop up
            // underneath, it should be a star icon the same way it's
            // done in the shot modal for versions." Inline star button
            // on each slice — click promotes (or unsets) THAT slice as
            // the hero. PATCH with the slice-suffixed label so only
            // this F# flips kind="hero" (the other f1-f4 stay as
            // "frame"). After the response, invalidate the cached
            // asset_versions AND dispatch a "paradise-shot-frames-changed"
            // event so the still-open ShotDetailModal refetches and
            // reflects the change without needing a close + reopen.
            const promoteSlice = (ev) => {
              ev.stopPropagation();
              ev.preventDefault();
              if (!s) return;
              if (window.hasPerm && !window.hasPerm("approve_shots")) return;
              const sliceLabel = `${activeGrid.version_label}_f${i}`;
              const shotId = gridVersion.asset_id;
              (window.authFetch || fetch)(`/api/shots/${encodeURIComponent(shotId)}/hero`, {
                method: "PATCH",
                body: JSON.stringify({
                  kind: "frame",
                  version_label: sliceLabel,
                  toggle: isHeroSlice,  // already hero → unset
                }),
              })
                .then(r => r.ok ? r.json() : Promise.reject(new Error(`HTTP ${r.status}`)))
                .then(() => {
                  if (window.__assetVersionCache) {
                    window.__assetVersionCache.delete(shotId);
                  }
                  try {
                    window.dispatchEvent(new CustomEvent("paradise-shot-frames-changed", {
                      detail: { shot_id: shotId },
                    }));
                  } catch (_) {}
                  // Reload our own slice view so the star toggle +
                  // hero pin renders immediately, without needing to
                  // close + reopen the modal.
                  reloadAssetVersions();
                })
                .catch(err => console.warn("[grid] set-hero failed:", err.message));
            };
            return (
              <button
                key={i}
                type="button"
                className={"grid-slice" + (activeSlice === i ? " is-active" : "") + (s ? "" : " is-empty") + (isHeroSlice ? " is-hero-slice" : "")}
                onClick={() => s && setActiveSlice(i)}
                disabled={!s}
                title={s ? `Show F${i}${isHeroSlice ? " · current hero" : ""}` : `F${i} not generated yet`}
                style={{ position: "relative" }}
              >
                {s && s.file_path
                  ? <img src={window.thumbUrl ? window.thumbUrl(s.file_path, 240) : s.file_path} alt={s.version_label} loading="lazy"/>
                  : <span className="grid-slice-placeholder">F{i}</span>}
                <span className="grid-slice-label">F{i}</span>
                {s && (!window.hasPerm || window.hasPerm("approve_shots")) && (
                  <span
                    role="button"
                    tabIndex={0}
                    aria-pressed={isHeroSlice}
                    onMouseDown={(e) => { e.stopPropagation(); e.preventDefault(); }}
                    onClick={promoteSlice}
                    onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") promoteSlice(e); }}
                    title={isHeroSlice
                      ? `F${i} is the chosen frame — click to unset`
                      : `Make F${i} the chosen frame for ${activeGrid.version_label}`}
                    style={{
                      position: "absolute",
                      top: 6,
                      right: 6,
                      width: 22,
                      height: 22,
                      display: "grid",
                      placeItems: "center",
                      borderRadius: "50%",
                      background: isHeroSlice ? "color-mix(in srgb, var(--gold-3) 95%, transparent)" : "color-mix(in srgb, var(--shade-13) 55%, transparent)",
                      color: isHeroSlice ? "var(--ink-on-gold)" : "var(--ink-cream)",
                      cursor: "pointer",
                      transition: "background var(--dur-1) ease, transform 90ms ease",
                      backdropFilter: "blur(4px)",
                      WebkitBackdropFilter: "blur(4px)",
                      boxShadow: isHeroSlice
                        ? "0 0 0 1px color-mix(in srgb, var(--gold-3) 60%, transparent), 0 2px 6px color-mix(in srgb, var(--shadow-ink) 25%, transparent)"
                        : "0 1px 3px rgba(0, 0, 0, 0.35)",
                    }}
                  >
                    <svg viewBox="0 0 24 24" width="11" height="11" fill={isHeroSlice ? "currentColor" : "none"} stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
                      <path d="m12 3 2.6 5.4 5.9.8-4.3 4.1 1.1 5.9L12 16.8 6.7 19.2l1.1-5.9L3.5 9.2l5.9-.8z"/>
                    </svg>
                  </span>
                )}
              </button>
            );
          })}
        </div>

        <div className="meta-block">
          <div className="meta-row">
            <span className="meta-k">Model</span>
            <span className="meta-v mono">{modelStr}</span>
          </div>
          <div className="meta-row">
            <span className="meta-k">Resolution</span>
            <span className="meta-v mono">{resStr}</span>
          </div>
          <div className="meta-row">
            <span className="meta-k">Active version</span>
            <span className="meta-v">
              {activeBase}
              {activeSlice ? <> · F{activeSlice}</> : null}
            </span>
          </div>
          {/* v06j — Hugo wants date under the image rather than on the
              version thumbnails. Show the active grid's created_at.
              v07ze — Always rendered (em-dash when missing) so the
              meta-block height is the same before/after activeGrid
              resolves; the modal no longer grows when data lands. */}
          <div className="meta-row">
            <span className="meta-k">Created</span>
            <span className="meta-v">
              {(() => {
                if (!activeGrid || !activeGrid.created_at) return "—";
                try {
                  const d = new Date(activeGrid.created_at);
                  return Number.isNaN(d.getTime())
                    ? activeGrid.created_at
                    : d.toLocaleDateString(undefined, { day: "numeric", month: "short", year: "numeric" });
                } catch (e) { return activeGrid.created_at; }
              })()}
            </span>
          </div>
          <div className="meta-row">
            <span className="meta-k">References</span>
            <span className="ref-thumbs">
              {refsList.length === 0 ? (
                <span className="ref-empty">No references linked</span>
              ) : refsList.slice(0, 8).map((src, i) => {
                const fname = String(src).split(/[\\/]/).pop() || `ref ${i + 1}`;
                return (
                  <span key={`${src}-${i}`} className="ref-card">
                    <span
                      className="ref-thumb ref-thumb--img"
                      style={{ backgroundImage: `url(${window.thumbUrl ? window.thumbUrl(src, 160) : src})`, backgroundSize: "cover", backgroundPosition: "center" }}
                      aria-label={fname}
                    />
                    <span className="ref-tip" role="tooltip">
                      <img className="ref-tip-img" src={window.thumbUrl ? window.thumbUrl(src, 560) : src} alt={fname} loading="lazy"/>
                      <span className="ref-tip-meta">
                        <span className="ref-tip-name">{fname}</span>
                        <span className="ref-tip-type">image</span>
                      </span>
                    </span>
                  </span>
                );
              })}
            </span>
          </div>
        </div>

        <div className="prompt-block">
          <div className="pb-head">
            <span className="pb-label">PROMPT — {activeBase}</span>
            {promptText && (
              <button className="copy-btn" type="button" onClick={() => {
                try { navigator.clipboard && navigator.clipboard.writeText(promptText); } catch (e) {}
              }}>Copy</button>
            )}
          </div>
          <pre className="pb-code">{promptText || "(no prompt logged for this grid)"}</pre>
        </div>

        <div className="version-section">
          <div className="vs-label">GRID VERSIONS</div>
          {/* v07za — Always render the scroller at its populated
              height; empty state just shows a blank strip of the
              same dimensions. Removes the modal height shift Hugo
              saw between loading and loaded. */}
          <div
            className={"version-scroller" + (grids.length === 0 ? " version-scroller--empty" : "")}
            ref={versionScrollerRef}
          >
            {grids.map((g, i) => (
              <button
                key={g.id}
                type="button"
                className={"version-tile" + (activeBase === g.version_label ? " active" : "")}
                onClick={() => { setActiveBase(g.version_label); setActiveSlice(null); }}
              >
                {/* v06j — version thumbnails just show the grid image
                    + label now; the date moved to the meta block to
                    declutter the strip. Missing file_path renders a
                    minimal placeholder so the tile is still clickable
                    instead of looking like a broken image.
                    v07zw — `.vt-img` starts at opacity 0 in modal.css
                    and only fades in via the .vt-img-loaded class.
                    Without an onLoad handler, the loaded class never
                    got added and every version pill stayed invisible
                    even after the image was fully decoded. */}
                {g.file_path
                  ? <img
                      className="vt-img"
                      src={window.thumbUrl ? window.thumbUrl(g.file_path, 240) : g.file_path}
                      alt={g.version_label}
                      loading="lazy"
                      onLoad={(e) => e.currentTarget.classList.add("vt-img-loaded")}
                      onError={(e) => { e.currentTarget.style.display = "none"; }}
                    />
                  : <span className="vt-placeholder">{g.version_label}</span>}
                <span className="vt-label">{g.version_label}</span>
              </button>
            ))}
          </div>
        </div>

        {/* v07zd — Hugo: panel grew when versions resolved because
            the NotesPanel only mounted once activeGrid was set. Always
            reserve the 320 px slot via a placeholder, then swap in
            the real panel when ready. Modal height is now stable
            from frame one. */}
        {NotesPanelComp && activeGrid ? (
          <NotesPanelComp
            entityType="asset_version"
            entityId={activeGrid.id}
            versionLabel={activeBase}
          />
        ) : (
          <div className="notes-panel notes-panel--placeholder" aria-hidden="true"/>
        )}
      </div>
    </div>
  ), portalRoot);
}

if (typeof window !== "undefined") window.GridDetailModal = GridDetailModal;
