// Shared utilities, image atoms, and theme tokens.
// Exposes via window: MediaImage, justifyRows, applyTheme, themeVars, fontStacks.

function installProfileMediaProtection() {
  if (typeof window === "undefined" || typeof document === "undefined") return;
  if (window.__ELARA_PROFILE_MEDIA_PROTECTION__) return;
  window.__ELARA_PROFILE_MEDIA_PROTECTION__ = true;

  const protectedSelector = "img, video, picture, canvas, .mi";
  const markMedia = (root = document) => {
    if (!root || typeof root.querySelectorAll !== "function") return;
    root.querySelectorAll("img, video").forEach((el) => {
      if ("draggable" in el) el.draggable = false;
      el.setAttribute("draggable", "false");
    });
  };
  const protectedTarget = (event) => (
    event.target && typeof event.target.closest === "function"
      ? event.target.closest(protectedSelector)
      : null
  );
  const blockMediaEvent = (event) => {
    if (!protectedTarget(event)) return;
    event.preventDefault();
    if (event.type !== "contextmenu") event.stopPropagation();
  };

  document.addEventListener("contextmenu", blockMediaEvent, true);
  document.addEventListener("dragstart", blockMediaEvent, true);
  document.addEventListener("selectstart", blockMediaEvent, true);
  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", () => markMedia(), { once: true });
  } else {
    markMedia();
  }
  if (typeof MutationObserver === "function") {
    new MutationObserver((mutations) => {
      mutations.forEach((mutation) => {
        mutation.addedNodes.forEach((node) => {
          if (node && node.nodeType === 1) markMedia(node);
        });
      });
    }).observe(document.documentElement, { childList: true, subtree: true });
  }
}

installProfileMediaProtection();

// ─── Real media tile (image or video) ──────────────────────────────────
// One canonical way to render any portfolio item. `crop` forces cover-fit
// to its container (used for the hero); the gallery never passes crop=true,
// so images keep their true aspect ratio. Videos render a poster frame
// with a play badge — clicking is handled upstream (opens the lightbox).
function mediaUrl(item, variant = "public") {
  if (!item) return "";
  const urls = item.urls || {};

  return (
    urls[variant] ||
    urls.public ||
    urls.modal ||
    urls.card ||
    item.signedUrl ||
    item.previewUrl ||
    item.url ||
    item.src ||
    ""
  );
}

function posterUrl(item) {
  if (!item) return "";
  return item.poster || item.urls?.poster || mediaUrl(item, "poster");
}

function streamUid(item) {
  const direct = String(item?.id || item?.uid || "").trim();
  if (/^[a-f0-9]{32}$/i.test(direct)) return direct;

  const candidates = [
    item?.src,
    item?.url,
    item?.poster,
    item?.urls?.public,
    item?.urls?.poster,
  ].filter(Boolean);

  for (const value of candidates) {
    const match = String(value).match(/(?:videodelivery\.net|cloudflarestream\.com)\/([a-f0-9]{32})(?:\/|$)/i);
    if (match) return match[1];
  }
  return "";
}

function streamIframeUrl(item) {
  if (item?.provider !== "cloudflare_stream" && !streamUid(item)) return "";
  const uid = streamUid(item);
  if (!uid) return "";
  const url = new URL(`https://iframe.videodelivery.net/${uid}`);
  url.searchParams.set("autoplay", "true");
  url.searchParams.set("controls", "true");
  return url.toString();
}

function mediaAspect(item) {
  const ratio = Number(item?.aspectRatio);
  if (Number.isFinite(ratio) && ratio > 0) return ratio;

  const w = Number(item?.w);
  const h = Number(item?.h);
  if (Number.isFinite(w) && Number.isFinite(h) && w > 0 && h > 0) return w / h;

  return 3 / 4;
}

function mediaSrcSet(item) {
  if (!item?.urls) return undefined;
  const pairs = [
    item.urls.card && `${item.urls.card} 720w`,
    item.urls.modal && `${item.urls.modal} 1440w`,
    item.urls.public && `${item.urls.public} 1600w`,
  ].filter(Boolean);

  return pairs.length ? pairs.join(", ") : undefined;
}

function MediaPlaceholder({ label = "Image pending" }) {
  return (
    <div
      aria-hidden="true"
      style={{
        position: "absolute",
        inset: 0,
        display: "grid",
        placeItems: "center",
        overflow: "hidden",
        background:
          "linear-gradient(135deg, rgba(243,239,233,0.10), rgba(243,239,233,0.025) 42%, rgba(231,117,76,0.09))",
      }}
    >
      <span style={{
        position: "relative",
        zIndex: 1,
        color: "rgba(246,240,226,0.52)",
        fontFamily: "var(--mono)",
        fontSize: 10,
        letterSpacing: ".18em",
        textTransform: "uppercase",
      }}>
        {label}
      </span>
      <span style={{
        position: "absolute",
        width: "52%",
        height: "1px",
        background: "rgba(246,240,226,0.24)",
      }}/>
    </div>
  );
}

function MediaItem({ item, crop = false, style = {}, className = "", priority = false, alt }) {
  const isVideo = item?.kind === "video";
  const [loaded, setLoaded] = React.useState(false);
  const [failed, setFailed] = React.useState(false);
  const src = isVideo ? mediaUrl(item) : mediaUrl(item, priority ? "modal" : "card");
  const poster = isVideo ? posterUrl(item) : "";
  const label = item?.provider === "cloudflare_images" ? "Secure image loading" : "Image loading";
  const canRender = Boolean(src || poster);

  // Video without a separate poster image (e.g. user upload) — render the
  // <video> element itself with preload=metadata so the browser shows the
  // first frame.
  if (!canRender) {
    return (
      <div className={"mi " + className} style={{ position:"relative", width:"100%", height: crop ? "100%" : "auto", aspectRatio: crop ? undefined : mediaAspect(item), ...style }}>
        <MediaPlaceholder label="Media pending"/>
      </div>
    );
  }

  if (isVideo && !poster) {
    return (
      <div className={"mi " + className} style={{ position:"relative", width:"100%", height: crop ? "100%" : "auto", ...style }}>
        {!loaded && <MediaPlaceholder label="Video loading"/>}
        <video
          src={src}
          muted
          playsInline
          preload="metadata"
          draggable={false}
          onLoadedMetadata={() => setLoaded(true)}
          onError={() => setFailed(true)}
          style={{
            display: "block",
            width: "100%",
            height: crop ? "100%" : "auto",
            objectFit: crop ? "cover" : "fill",
            background: "#1a1816",
            opacity: failed ? 0 : loaded ? 1 : 0,
            transition: "opacity .45s cubic-bezier(0.16, 1, 0.3, 1)",
            userSelect: "none",
          }}
        />
        {failed && <MediaPlaceholder label="Video unavailable"/>}
        <VideoBadge/>
      </div>
    );
  }

  const imgSrc = isVideo ? poster : src;
  return (
    <div
      className={"mi " + className}
      style={{
        position: "relative",
        width: "100%",
        height: crop ? "100%" : "auto",
        background: "#1a1816",
        ...style,
      }}
    >
      {!loaded && !failed && <MediaPlaceholder label={label}/>}
      <img
        src={imgSrc}
        srcSet={mediaSrcSet(item)}
        sizes={crop ? "(max-width: 760px) 100vw, 960px" : "(max-width: 760px) 100vw, 50vw"}
        alt={alt ?? item?.alt ?? ""}
        loading={priority ? "eager" : "lazy"}
        decoding="async"
        draggable={false}
        onLoad={() => setLoaded(true)}
        onError={() => setFailed(true)}
        style={{
          display: "block",
          width: "100%",
          height: crop ? "100%" : "auto",
          objectFit: crop ? "cover" : "fill",
          opacity: failed ? 0 : loaded ? 1 : 0,
          transition: "opacity .55s cubic-bezier(0.16, 1, 0.3, 1)",
          userSelect: "none",
        }}
      />
      {failed && <MediaPlaceholder label="Image unavailable"/>}
      {isVideo && <VideoBadge/>}
    </div>
  );
}

// Subtle play badge overlaid on video posters in the gallery.
function VideoBadge({ size = 56 }) {
  return (
    <div style={{
      position:"absolute", inset:0,
      display:"flex", alignItems:"center", justifyContent:"center",
      pointerEvents:"none",
    }}>
      <div style={{
        width: size, height: size, borderRadius: "50%",
        background: "rgba(10,10,10,0.32)",
        border: "1px solid rgba(246,240,226,0.8)",
        backdropFilter: "blur(6px)",
        WebkitBackdropFilter: "blur(6px)",
        display: "flex", alignItems: "center", justifyContent: "center",
      }}>
        <svg width={size * 0.34} height={size * 0.34} viewBox="0 0 24 24" fill="#f6f0e2" aria-hidden="true">
          <path d="M8 5v14l11-7z"/>
        </svg>
      </div>
    </div>
  );
}

// Back-compat alias — old call sites still import MediaImage.
const MediaImage = MediaItem;

// ─── Justified gallery packer ──────────────────────────────────────────
// Row-based justified layout (Flickr/Google-Photos style):
//
//   1. Each row starts with a target height.
//   2. Items are added at that height (width = aspect * height) until the
//      summed width plus gutters exceeds the container width.
//   3. The whole row is then scaled so widths sum exactly to the container
//      width — every image keeps its true aspect ratio, no crops, no gaps.
//   4. The trailing partial row is left at the target height (its natural
//      width may be less than the container, so it's optionally left-aligned
//      or extended depending on caller preference).
//
// Inputs:
//   items           — [{ w, h, ... }] in source order
//   containerWidth  — px available for the row
//   targetHeight    — preferred row height before justification (px)
//   gap             — gutter between items in a row, AND between rows (px)
//   maxHeightRatio  — cap how much an under-filled row can grow (e.g. 1.5×)
//
// Output: [{ rowHeight, items: [{ ...item, w: scaledW, h: scaledH }] }]
function justifyRows(items, containerWidth, targetHeight, gap = 8, maxHeightRatio = 1.4) {
  // ── Pass 1: greedy pack item indices into rows ────────────────────
  const packs = []; // each: array of indices into `items`
  let cur = [];
  let curW = 0;
  for (let i = 0; i < items.length; i++) {
    const aspect = mediaAspect(items[i]);
    cur.push(i);
    curW += aspect * targetHeight;
    const projected = curW + gap * (cur.length - 1);
    if (projected >= containerWidth) { packs.push(cur); cur = []; curW = 0; }
  }
  if (cur.length) packs.push(cur);

  // ── Pass 2: rebalance a lonely trailing item ──────────────────────
  // If the last row has just 1 item, it would scale up to maxHeightRatio
  // and tower over the gallery (and leave a big void to its right). If the
  // previous row has at least 3 items, lend it one — the result is two
  // tidy rows of similar height.
  if (packs.length >= 2) {
    const last = packs[packs.length - 1];
    const prev = packs[packs.length - 2];
    if (last.length === 1 && prev.length >= 3) {
      last.unshift(prev.pop());
    }
  }

  // ── Pass 3: compute per-row scaling and emit ──────────────────────
  return packs.map((indices, ri) => {
    const isLast = ri === packs.length - 1;
    const rowItems = indices.map((idx) => items[idx]);
    const sumAtTarget = rowItems.reduce((s, it) => s + mediaAspect(it) * targetHeight, 0);
    const totalGap = gap * (rowItems.length - 1);
    const available = containerWidth - totalGap;
    let scale = available / sumAtTarget;
    if (isLast && scale > maxHeightRatio) scale = maxHeightRatio;
    const h = targetHeight * scale;
    return {
      rowHeight: h,
      items: rowItems.map((it) => ({
        ...it,
        _w: mediaAspect(it) * h,
        _h: h,
      })),
    };
  });
}

// ─── Theme + Type tokens ───────────────────────────────────────────────
const themeVars = {
  light: {
    "--bg":           "#f6f4ef",  // bone, not pure white — printed feel
    "--bg-deep":      "#fbfaf6",
    "--fg":           "#15110d",
    "--fg-soft":      "rgba(21,17,13,0.62)",
    "--fg-faint":     "rgba(21,17,13,0.38)",
    "--rule":         "rgba(21,17,13,0.14)",
    "--rule-strong":  "rgba(21,17,13,0.30)",
    "--chip-bg":      "rgba(21,17,13,0.04)",
  },
  dark: {
    "--bg":           "#0d0b09",
    "--bg-deep":      "#090706",
    "--fg":           "#f1ecdd",
    "--fg-soft":      "rgba(241,236,221,0.62)",
    "--fg-faint":     "rgba(241,236,221,0.38)",
    "--rule":         "rgba(241,236,221,0.14)",
    "--rule-strong":  "rgba(241,236,221,0.30)",
    "--chip-bg":      "rgba(241,236,221,0.06)",
  },
  sepia: {
    "--bg":           "#efe7d8",
    "--bg-deep":      "#f6f0e2",
    "--fg":           "#2a221a",
    "--fg-soft":      "rgba(42,34,26,0.62)",
    "--fg-faint":     "rgba(42,34,26,0.38)",
    "--rule":         "rgba(42,34,26,0.18)",
    "--rule-strong":  "rgba(42,34,26,0.34)",
    "--chip-bg":      "rgba(42,34,26,0.06)",
  },
};

const fontStacks = {
  "serif-led": {
    "--font-display": "\"minion-3\", \"freight-text-pro\", Iowan Old Style, Georgia, serif",
    "--font-body":    "\"minion-3\", \"freight-text-pro\", Iowan Old Style, Georgia, serif",
    "--display-weight": "400",
    "--display-tracking": "-0.025em",
    "--display-leading": "0.92",
    "--name-tracking": "0",
  },
  "grotesque-led": {
    "--font-display": "\"freight-sans-pro\", system-ui, -apple-system, Helvetica, Arial, sans-serif",
    "--font-body":    "\"freight-sans-pro\", system-ui, -apple-system, Helvetica, Arial, sans-serif",
    "--display-weight": "600",
    "--display-tracking": "0",
    "--display-leading": "0.86",
    "--name-tracking": "0",
  },
};

function applyTheme(el, { theme = "light", type = "serif-led" } = {}) {
  if (!el) return;
  const vars = { ...themeVars[theme], ...fontStacks[type] };
  for (const k in vars) el.style.setProperty(k, vars[k]);
  el.style.background = "var(--bg)";
  el.style.color = "var(--fg)";
  el.style.fontFamily = "var(--font-body)";
}

// ─── Window measure hook ───────────────────────────────────────────────
// Returns a stable number that updates on resize, so the justified gallery
// can repack at the right container width on viewport change.
function useElementWidth(ref) {
  const [w, setW] = React.useState(0);
  React.useEffect(() => {
    if (!ref.current) return;
    const ro = new ResizeObserver((entries) => {
      for (const e of entries) setW(e.contentRect.width);
    });
    ro.observe(ref.current);
    return () => ro.disconnect();
  }, [ref]);
  return w;
}

// ─── Lightbox ──────────────────────────────────────────────────────────
// Modal viewer. Image/video is sized explicitly from the item's true aspect
// ratio + the viewport so it fills the available space (no tiny images on
// big screens), and the prev/next arrows sit immediately beside the media
// rather than pinned to the viewport edges.

function useViewportSize() {
  const [s, setS] = React.useState(() => ({
    w: typeof window !== "undefined" ? window.innerWidth  : 1440,
    h: typeof window !== "undefined" ? window.innerHeight : 900,
  }));
  React.useEffect(() => {
    const r = () => setS({ w: window.innerWidth, h: window.innerHeight });
    window.addEventListener("resize", r);
    return () => window.removeEventListener("resize", r);
  }, []);
  return s;
}

function useLightbox(items) {
  const [idx, setIdx] = React.useState(-1);
  const open  = React.useCallback((i) => setIdx(i), []);
  const close = React.useCallback(() => setIdx(-1), []);
  const next  = React.useCallback(() => setIdx((i) => items.length ? (i + 1) % items.length : -1), [items.length]);
  const prev  = React.useCallback(() => setIdx((i) => items.length ? (i - 1 + items.length) % items.length : -1), [items.length]);
  return { idx, open, close, next, prev, isOpen: idx >= 0 };
}

function Lightbox({ items, idx, onClose, onPrev, onNext }) {
  const vp = useViewportSize();
  const closeRef = React.useRef(null);

  React.useEffect(() => {
    if (idx < 0) return;
    const k = (e) => {
      if (e.key === "Escape")     onClose();
      if (e.key === "ArrowLeft")  onPrev();
      if (e.key === "ArrowRight") onNext();
    };
    document.addEventListener("keydown", k);
    const prev = document.body.style.overflow;
    document.body.style.overflow = "hidden";
    return () => {
      document.removeEventListener("keydown", k);
      document.body.style.overflow = prev;
    };
  }, [idx, onClose, onPrev, onNext]);

  React.useEffect(() => {
    if (idx >= 0) closeRef.current?.focus();
  }, [idx]);

  if (idx < 0) return null;
  const item = items[idx];
  if (!item) return null;
  const isVideo = item.kind === "video";

  // Compute display size from item aspect + viewport. Reserve room above
  // for the top bar and on either side for the arrow buttons so the media
  // never collides with chrome.
  const aspect = mediaAspect(item);
  const reservedW = 168; // 2 × (52 arrow + 24 gap + 8 breathing)
  const reservedH = 96;  // top bar
  const maxW = Math.max(280, Math.min(1600, vp.w - reservedW));
  const maxH = Math.max(280, vp.h - reservedH);
  let dispH = maxH;
  let dispW = dispH * aspect;
  if (dispW > maxW) { dispW = maxW; dispH = dispW / aspect; }

  return ReactDOM.createPortal(
    <div
      onClick={onClose}
      role="dialog"
      aria-modal="true"
      aria-label="Media viewer"
      style={{
        position:"fixed", inset:0, zIndex: 2147483640,
        background:"rgba(8,7,6,0.62)",
        backdropFilter:"blur(1.5px)", WebkitBackdropFilter:"blur(1.5px)",
        cursor:"zoom-out",
      }}
    >
      {/* Top bar */}
      <div style={{
        position:"absolute", top:0, left:0, right:0, padding:"20px 28px",
        display:"flex", justifyContent:"space-between", alignItems:"center",
        color:"rgba(246,240,226,0.82)",
        fontFamily:"var(--font-body)",
        fontSize: 11, letterSpacing:".18em", textTransform:"uppercase",
        pointerEvents:"none", zIndex: 2,
      }}>
        <span style={{pointerEvents:"auto"}}>
          {String(item.i).padStart(2, "0")} / {String(items.length).padStart(2, "0")}
        </span>
        <button
          ref={closeRef}
          aria-label="Close"
          onClick={(e) => { e.stopPropagation(); onClose(); }}
          style={{
            pointerEvents:"auto", appearance:"none", border:"1px solid rgba(246,240,226,0.24)", background:"rgba(12,10,8,0.68)",
            color:"rgba(246,240,226,0.85)",
            minWidth:86, height:36, borderRadius:0, cursor:"pointer",
            display:"inline-flex", alignItems:"center", justifyContent:"center",
            fontSize:10.5, lineHeight:1, letterSpacing:".18em", textTransform:"uppercase",
          }}>Close</button>
      </div>

      {/* Media row — arrows flank the media at a tight gap */}
      <div style={{
        position:"absolute", inset:0,
        display:"flex", alignItems:"center", justifyContent:"center",
        gap: 24,
      }}>
        <LbArrow dir="left"  onClick={(e) => { e.stopPropagation(); onPrev(); }}/>
        <div
          onClick={(e) => e.stopPropagation()}
          style={{
            width: dispW, height: dispH,
            flex: "0 0 auto",
            background: "#0a0908",
            cursor: "auto",
          }}
        >
          {isVideo ? (
            streamIframeUrl(item) ? (
              <iframe
                key={streamIframeUrl(item)}
                src={streamIframeUrl(item)}
                title={item.title || item.alt || "Profile film"}
                loading="eager"
                allow="accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture"
                allowFullScreen
                style={{ width:"100%", height:"100%", display:"block", border:0, background:"#0d0b09" }}
              />
            ) : (
              <video
                key={mediaUrl(item)}
                src={mediaUrl(item)}
                poster={posterUrl(item)}
                controls
                controlsList="nodownload"
                autoPlay
                playsInline
                draggable={false}
                style={{ width:"100%", height:"100%", display:"block", objectFit:"contain", background:"#0d0b09", userSelect:"none" }}
              />
            )
          ) : (
            <img
              src={mediaUrl(item, "modal")}
              srcSet={mediaSrcSet(item)}
              sizes="100vw"
              alt={item.alt || ""}
              decoding="async"
              draggable={false}
              style={{ width:"100%", height:"100%", display:"block", objectFit:"contain" }}
            />
          )}
        </div>
        <LbArrow dir="right" onClick={(e) => { e.stopPropagation(); onNext(); }}/>
      </div>
    </div>,
    document.body,
  );
}

function LbArrow({ dir, onClick }) {
  return (
    <button
      onClick={onClick}
      aria-label={dir === "left" ? "Previous" : "Next"}
      style={{
        flex: "0 0 auto",
        appearance:"none", border:0, cursor:"pointer",
        background:"rgba(246,240,226,0.10)",
        width:56, height:56, borderRadius:28,
        color:"#f6f0e2",
        display:"flex", alignItems:"center", justifyContent:"center",
        transition:"background .15s, transform .15s",
      }}
      onMouseEnter={(e) => { e.currentTarget.style.background = "rgba(246,240,226,0.22)"; e.currentTarget.style.transform = "scale(1.04)"; }}
      onMouseLeave={(e) => { e.currentTarget.style.background = "rgba(246,240,226,0.10)"; e.currentTarget.style.transform = "scale(1)"; }}
    >
      <svg width="22" height="22" viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round">
        <path d={dir === "left" ? "M12 4l-6 6 6 6" : "M8 4l6 6-6 6"}/>
      </svg>
    </button>
  );
}

Object.assign(window, {
  MediaItem, MediaImage, justifyRows, applyTheme, themeVars, fontStacks,
  useElementWidth, Lightbox, useLightbox, VideoBadge, mediaUrl, mediaAspect,
  streamUid, streamIframeUrl,
});
