import React, {
  useState, useEffect, useRef, useCallback, memo,
} from 'react';
import ArtworkGallery from '../artwork/ArtworkGallery';
import './MasonryGallery.css';

function getCsrfToken() {
  return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '';
}

async function sendDiscoveryEvent(endpoint, payload) {
  if (!endpoint) return;

  try {
    await fetch(endpoint, {
      method: 'POST',
      credentials: 'same-origin',
      keepalive: true,
      headers: {
        'Content-Type': 'application/json',
        'X-CSRF-TOKEN': getCsrfToken(),
        'X-Requested-With': 'XMLHttpRequest',
      },
      body: JSON.stringify(payload),
    });
  } catch {
    // Discovery telemetry should never block the gallery UX.
  }
}

// ── Masonry helpers ────────────────────────────────────────────────────────
const ROW_SIZE = 8;
const ROW_GAP  = 16;

function applyMasonry(grid) {
  if (!grid) return;
  Array.from(grid.querySelectorAll('.nova-card')).forEach((card) => {
    const media = card.querySelector('.nova-card-media') || card;
    let height  = media.getBoundingClientRect().height || 200;

    // Clamp to the computed max-height so the span never over-reserves rows
    // when CSS max-height kicks in (e.g. portrait images capped to 2×16:9).
    const cssMaxH = parseFloat(getComputedStyle(media).maxHeight);
    if (!isNaN(cssMaxH) && cssMaxH > 0 && cssMaxH < height) {
      height = cssMaxH;
    }

    const span = Math.max(1, Math.ceil((height + ROW_GAP) / (ROW_SIZE + ROW_GAP)));
    card.style.gridRowEnd = `span ${span}`;
  });
}

function waitForImages(el) {
  return Promise.all(
    Array.from(el.querySelectorAll('img')).map((img) =>
      img.decode ? img.decode().catch(() => null) : Promise.resolve(),
    ),
  );
}

// ── Page fetch helpers ─────────────────────────────────────────────────────
/**
 * Fetch the next page of data.
 *
 * The response is either:
 *  - JSON  { artworks: [...], next_cursor: '...' }  when X-Requested-With is
 *    sent and the controller returns JSON (future enhancement)
 *  - HTML  page – we parse [data-react-masonry-gallery] from it and read its
 *    data-artworks / data-next-cursor / data-next-page-url attributes.
 */
async function fetchPageData(url) {
  const res = await fetch(url, {
    credentials: 'same-origin',
    headers: { 'X-Requested-With': 'XMLHttpRequest' },
  });
  if (!res.ok) throw new Error(`HTTP ${res.status}`);

  const ct = res.headers.get('content-type') || '';

  // JSON fast-path (if controller ever returns JSON)
  if (ct.includes('application/json')) {
    const json = await res.json();

    // Support multiple API payload shapes across endpoints.
    const artworks = Array.isArray(json.artworks)
      ? json.artworks
      : Array.isArray(json.data)
        ? json.data
        : Array.isArray(json.items)
          ? json.items
          : Array.isArray(json.results)
            ? json.results
            : [];

    const nextCursor = json.next_cursor
      ?? json.nextCursor
      ?? json.meta?.next_cursor
      ?? null;

    const nextPageUrl = json.next_page_url
      ?? json.nextPageUrl
      ?? json.meta?.next_page_url
      ?? null;

    const hasMore = typeof json.has_more === 'boolean'
      ? json.has_more
      : typeof json.hasMore === 'boolean'
        ? json.hasMore
        : null;

    return {
      artworks: artworks.map(normalizeArtworkItem),
      nextCursor,
      nextPageUrl,
      hasMore,
      meta: json.meta ?? null,
    };
  }

  // HTML: parse and extract mount-container data attributes
  const html = await res.text();
  const doc  = new DOMParser().parseFromString(html, 'text/html');
  const el   = doc.querySelector('[data-react-masonry-gallery]');
  if (!el) return { artworks: [], nextCursor: null, nextPageUrl: null };

  let artworks = [];
  try { artworks = JSON.parse(el.dataset.artworks || '[]'); } catch { /* empty */ }

  return {
    artworks: artworks.map(normalizeArtworkItem),
    nextCursor:  el.dataset.nextCursor  || null,
    nextPageUrl: el.dataset.nextPageUrl || null,
    hasMore: null,
    meta: null,
  };
}

// ── Skeleton row ──────────────────────────────────────────────────────────
function SkeletonCard() {
  return <div className="nova-skeleton-card" aria-hidden="true" />;
}

// ── Ranking API helpers ───────────────────────────────────────────────────
/**
 * Normalize API / Blade artwork payloads into the internal shape used by the
 * gallery layout helpers and ArtworkCard.
 */
function normalizeArtworkItem(item) {
  if (!item || typeof item !== 'object') return item;

  const category = item.category && typeof item.category === 'object' ? item.category : null;
  const author = item.author && typeof item.author === 'object'
    ? item.author
    : (item.creator && typeof item.creator === 'object' ? item.creator : null);
  const publisher = item.publisher && typeof item.publisher === 'object' ? item.publisher : null;
  const w = item.dimensions?.width ?? item.width ?? null;
  const h = item.dimensions?.height ?? item.height ?? null;
  const thumb = item.thumbnail_url ?? item.thumb_url ?? item.thumb ?? item.image ?? null;
  const canonicalUrl = item.canonical_url
    ?? item.urls?.canonical
    ?? item.urls?.direct
    ?? item.url
    ?? item.href
    ?? item.urls?.web
    ?? category?.url
    ?? null;

  return {
    ...item,
    id:            item.id            ?? null,
    name:          item.name ?? item.title ?? null,
    title:         item.title ?? item.name ?? null,
    thumb:         item.thumb ?? thumb,
    thumb_url:     item.thumb_url ?? thumb,
    thumbnail_url: item.thumbnail_url ?? thumb,
    author,
    uname:         item.uname ?? author?.name ?? item.author_name ?? '',
    username:      item.username ?? (publisher?.type === 'group' ? '' : (author?.username ?? '')),
    avatar_url:    item.avatar_url ?? author?.avatar_url ?? null,
    profile_url:   item.profile_url ?? publisher?.profile_url ?? author?.profile_url ?? null,
    published_as_type: item.published_as_type ?? publisher?.type ?? null,
    publisher:     publisher ?? null,
    content_type_name: item.content_type_name ?? category?.content_type_name ?? category?.content_type_slug ?? category?.content_type ?? '',
    content_type_slug: item.content_type_slug ?? category?.content_type_slug ?? category?.content_type ?? '',
    category_name: item.category_name ?? category?.name ?? (typeof item.category === 'string' ? item.category : ''),
    category_slug: item.category_slug ?? category?.slug ?? '',
    category:      typeof item.category === 'string' ? item.category : (category?.name ?? item.category_name ?? ''),
    slug:          item.slug              ?? '',
    canonical_url: item.canonical_url ?? item.urls?.canonical ?? item.urls?.direct ?? null,
    url:           canonicalUrl,
    width:         w,
    height:        h,
  };
}

function mapRankApiArtwork(item) {
  return normalizeArtworkItem(item);
}

/**
 * Fetch ranked artworks from the ranking API.
 * Returns { artworks: [...] } in internal shape, or { artworks: [] } on failure.
 */
async function fetchRankApiArtworks(endpoint, rankType) {
  try {
    const url = new URL(endpoint, window.location.href);
    if (rankType) url.searchParams.set('type', rankType);
    const res = await fetch(url.toString(), {
      credentials: 'same-origin',
      headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' },
    });
    if (!res.ok) return { artworks: [] };
    const json = await res.json();
    const items = Array.isArray(json.data) ? json.data : [];
    return { artworks: items.map(mapRankApiArtwork) };
  } catch {
    return { artworks: [] };
  }
}

const SKELETON_COUNT = 10;

function getMasonryCardProps(art, idx) {
  const title = (art.name || art.title || 'Untitled artwork').trim();
  const hasDimensions = Number(art.width) > 0 && Number(art.height) > 0;
  const aspectRatio = hasDimensions ? Number(art.width) / Number(art.height) : null;
  const categorySlug = (art.category_slug || '').toLowerCase();
  const categoryName = (art.category_name || art.category || '').toLowerCase();
  const wideCategories = ['photography', 'wallpapers', 'photography-digital', 'wallpaper'];
  const wideCategoryNames = ['photography', 'wallpapers'];
  const isWideEligible =
    aspectRatio !== null &&
    aspectRatio > 2.0 &&
    (wideCategories.includes(categorySlug) || wideCategoryNames.includes(categoryName));

  return {
    articleClassName: `nova-card gallery-item artwork relative${isWideEligible ? ' nova-card--wide' : ''}`,
    articleStyle: isWideEligible ? { gridColumn: 'span 2' } : undefined,
    frameClassName: 'rounded-2xl ring-1 ring-white/5 bg-black/20 shadow-lg shadow-black/40 hover:ring-white/15 hover:shadow-[0_8px_30px_rgba(0,0,0,0.6),0_0_0_1px_rgba(255,255,255,0.08)]',
    mediaClassName: 'nova-card-media relative w-full overflow-hidden bg-neutral-900',
    mediaStyle: hasDimensions ? { aspectRatio: `${art.width} / ${art.height}` } : undefined,
    imageSrcSet: art.thumb_srcset || undefined,
    imageSizes: '(max-width: 768px) 50vw, (max-width: 1280px) 33vw, 20vw',
    imageWidth: hasDimensions ? art.width : undefined,
    imageHeight: hasDimensions ? art.height : undefined,
    loading: idx < 8 ? 'eager' : 'lazy',
    decoding: idx < 8 ? 'sync' : 'async',
    fetchPriority: idx === 0 ? 'high' : undefined,
    imageClassName: 'nova-card-main-image absolute inset-0 h-full w-full object-cover group-hover:scale-[1.03]',
    metricBadge: art.recommendation_reason
      ? {
          label: art.recommendation_reason,
          className: 'bg-sky-500/14 text-sky-100 ring-sky-300/30 max-w-[15rem] truncate',
        }
      : null,
  };
}

// ── Main component ────────────────────────────────────────────────────────
/**
 * MasonryGallery
 *
 * Props (all optional – set via data attributes in entry-masonry-gallery.jsx):
 *   artworks           []        Initial artwork objects
 *   galleryType        string    Maps to data-gallery-type (e.g. 'trending')
 *   cursorEndpoint     string|null  Route for cursor-based feeds (e.g. For You)
 *   initialNextCursor  string|null  First cursor token
 *   initialNextPageUrl string|null  First "next page" URL (page-based feeds)
 *   limit              number    Items per page (default 40)
 *   rankApiEndpoint    string|null  /api/rank/* endpoint; used as fallback data
 *                                   source when no SSR artworks are available
 *   rankType           string|null  Ranking API ?type= param (trending|new_hot|best)
 *   gridClassName      string|null  Optional CSS class override for grid columns/gaps
 */
function MasonryGallery({
  artworks: initialArtworks  = [],
  galleryType                = 'discover',
  cursorEndpoint             = null,
  initialNextCursor          = null,
  initialNextPageUrl         = null,
  limit                      = 40,
  rankApiEndpoint            = null,
  rankType                   = null,
  gridClassName              = null,
  discoveryEndpoint          = null,
  algoVersion: initialAlgoVersion = null,
}) {
  const normalizedInitialArtworks = initialArtworks.map(normalizeArtworkItem);
  const [artworks,    setArtworks]    = useState(normalizedInitialArtworks);
  const [nextCursor,  setNextCursor]  = useState(initialNextCursor);
  const [nextPageUrl, setNextPageUrl] = useState(initialNextPageUrl);
  const [loading,     setLoading]     = useState(false);
  const [done,        setDone]        = useState(!initialNextCursor && !initialNextPageUrl);
  const [algoVersion, setAlgoVersion] = useState(initialAlgoVersion);

  const gridRef    = useRef(null);
  const triggerRef = useRef(null);
  const viewedArtworkIdsRef = useRef(new Set());
  const clickedArtworkIdsRef = useRef(new Set());

  // ── Ranking API fallback ───────────────────────────────────────────────
  // When the server-side render provides no initial artworks (e.g. cache miss
  // or empty page result) and a ranking API endpoint is configured, perform a
  // client-side fetch from the ranking API to hydrate the grid.
  // Satisfies spec: "Fallback: Latest if ranking missing".
  useEffect(() => {
    if (normalizedInitialArtworks.length > 0) return; // SSR artworks already present
    if (!rankApiEndpoint)            return; // no API endpoint configured

    let cancelled = false;
    setLoading(true);
    fetchRankApiArtworks(rankApiEndpoint, rankType).then(({ artworks: ranked }) => {
      if (cancelled) return;
      if (ranked.length > 0) {
        setArtworks(ranked);
        setDone(true); // ranking API returns a full list; no further pagination
      }
      setLoading(false);
    });
    return () => { cancelled = true; };
  }, []); // eslint-disable-line react-hooks/exhaustive-deps

  // ── Masonry re-layout ──────────────────────────────────────────────────
  const relayout = useCallback(() => {
    const g = gridRef.current;
    if (!g) return;
    applyMasonry(g);
    waitForImages(g).then(() => applyMasonry(g));
  }, []);

  // Re-layout whenever artworks list changes.
  // Defer by one requestAnimationFrame so the browser has resolved
  // aspect-ratio heights before we measure with getBoundingClientRect().
  useEffect(() => {
    const raf = requestAnimationFrame(() => relayout());
    return () => cancelAnimationFrame(raf);
  }, [artworks, relayout]);

  // Re-layout on container resize (column width changes)
  useEffect(() => {
    const g = gridRef.current;
    if (!g || !('ResizeObserver' in window)) return;
    const ro = new ResizeObserver(relayout);
    ro.observe(g);
    return () => ro.disconnect();
  }, [relayout]);

  // ── Load more ──────────────────────────────────────────────────────────
  const fetchNext = useCallback(async () => {
    if (loading || done) return;

    // Build the URL to fetch
    let fetchUrl = null;
    if (cursorEndpoint && nextCursor) {
      const u = new URL(cursorEndpoint, window.location.href);
      u.searchParams.set('cursor', nextCursor);
      u.searchParams.set('limit', String(limit));
      fetchUrl = u.toString();
    } else if (nextPageUrl) {
      fetchUrl = nextPageUrl;
    }

    if (!fetchUrl) { setDone(true); return; }

    setLoading(true);
    try {
      const { artworks: newItems, nextCursor: nc, nextPageUrl: np, hasMore, meta } =
        await fetchPageData(fetchUrl);

      if (meta?.algo_version) {
        setAlgoVersion(meta.algo_version);
      }

      if (!newItems.length) {
        setDone(true);
      } else {
        setArtworks((prev) => [...prev, ...newItems]);
        if (cursorEndpoint) {
          setNextCursor(nc);
          if (hasMore === false || !nc) setDone(true);
        } else {
          setNextPageUrl(np);
          if (!np) setDone(true);
        }
      }
    } catch {
      setDone(true);
    } finally {
      setLoading(false);
    }
  }, [loading, done, cursorEndpoint, nextCursor, nextPageUrl, limit]);

  // ── Intersection observer for infinite scroll ──────────────────────────
  useEffect(() => {
    if (done) return;
    const trigger = triggerRef.current;
    if (!trigger || !('IntersectionObserver' in window)) return;

    const io = new IntersectionObserver(
      (entries) => { if (entries[0].isIntersecting) fetchNext(); },
      { rootMargin: '900px', threshold: 0 },
    );
    io.observe(trigger);
    return () => io.disconnect();
  }, [done, fetchNext]);

  useEffect(() => {
    if (galleryType !== 'for-you' || !discoveryEndpoint) return undefined;

    const grid = gridRef.current;
    if (!grid || !(window.IntersectionObserver)) return undefined;

    const artworkIndex = new Map(artworks.map((art, index) => [String(art.id), { art, index }]));
    const observer = new IntersectionObserver(
      (entries) => {
        entries.forEach((entry) => {
          if (!entry.isIntersecting || entry.intersectionRatio < 0.65) {
            return;
          }

          const card = entry.target.closest('[data-art-id]');
          const artworkId = card?.getAttribute('data-art-id');
          if (!artworkId || viewedArtworkIdsRef.current.has(artworkId)) {
            return;
          }

          const candidate = artworkIndex.get(artworkId);
          if (!candidate?.art?.id) {
            return;
          }

          viewedArtworkIdsRef.current.add(artworkId);
          observer.unobserve(entry.target);
          sendDiscoveryEvent(discoveryEndpoint, {
            event_type: 'view',
            artwork_id: Number(candidate.art.id),
            algo_version: candidate.art.recommendation_algo_version || algoVersion || undefined,
            meta: {
              gallery_type: galleryType,
              position: candidate.index + 1,
              source: candidate.art.recommendation_source || null,
              reason: candidate.art.recommendation_reason || null,
              score: candidate.art.recommendation_score ?? null,
            },
          });
        });
      },
      { threshold: [0.65] },
    );

    const cards = grid.querySelectorAll('[data-art-id]');
    cards.forEach((card) => observer.observe(card));

    return () => observer.disconnect();
  }, [algoVersion, artworks, discoveryEndpoint, galleryType]);

  useEffect(() => {
    if (galleryType !== 'for-you' || !discoveryEndpoint) return undefined;

    const grid = gridRef.current;
    if (!grid) return undefined;

    const handleClick = (event) => {
      const card = event.target.closest('[data-art-id]');
      if (!card) return;

      const artworkId = card.getAttribute('data-art-id');
      if (!artworkId || clickedArtworkIdsRef.current.has(artworkId)) {
        return;
      }

      const artwork = artworks.find((item) => String(item.id) === artworkId);
      if (!artwork?.id) {
        return;
      }

      clickedArtworkIdsRef.current.add(artworkId);
      sendDiscoveryEvent(discoveryEndpoint, {
        event_type: 'click',
        artwork_id: Number(artwork.id),
        algo_version: artwork.recommendation_algo_version || algoVersion || undefined,
        meta: {
          gallery_type: galleryType,
          source: artwork.recommendation_source || null,
          reason: artwork.recommendation_reason || null,
          score: artwork.recommendation_score ?? null,
          target_url: artwork.url || null,
        },
      });
    };

    grid.addEventListener('click', handleClick, true);
    return () => grid.removeEventListener('click', handleClick, true);
  }, [algoVersion, artworks, discoveryEndpoint, galleryType]);

  // Gallery V2 spec §7: 5 col desktop / 3 tablet / 2 mobile for all gallery pages.
  // Discover feeds (home/discover page) retain the same 5-col layout.
  const gridClass = gridClassName || 'grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-6';

  // ── Render ─────────────────────────────────────────────────────────────
  return (
    <section
      className="pb-10 pt-2 is-enhanced"
      data-nova-gallery
      data-gallery-type={galleryType}
      data-react-masonry-gallery
      data-artworks={JSON.stringify(artworks)}
      data-next-cursor={nextCursor   ?? undefined}
      data-next-page-url={nextPageUrl ?? undefined}
    >
      {artworks.length > 0 ? (
        <>
          <div
            ref={gridRef}
          >
            <ArtworkGallery
              items={artworks}
              layout="masonry"
              className={gridClass}
              containerProps={{ 'data-gallery-grid': true }}
              resolveCardProps={getMasonryCardProps}
            />
          </div>

          {/* Infinite scroll sentinel – placed after the grid */}
          {!done && (
            <div
              ref={triggerRef}
              className="h-px w-full"
              aria-hidden="true"
            />
          )}

          {/* Loading indicator */}
          {loading && (
            <div className="flex justify-center items-center gap-2 mt-8 py-4 text-white/30 text-sm">
              <svg
                className="animate-spin h-4 w-4 shrink-0"
                xmlns="http://www.w3.org/2000/svg"
                fill="none"
                viewBox="0 0 24 24"
                aria-hidden="true"
              >
                <circle
                  className="opacity-25"
                  cx="12" cy="12" r="10"
                  stroke="currentColor"
                  strokeWidth="4"
                />
                <path
                  className="opacity-75"
                  fill="currentColor"
                  d="M4 12a8 8 0 018-8v8H4z"
                />
              </svg>
              Loading more…
            </div>
          )}

          {done && artworks.length > 0 && (
            <p className="text-center text-xs text-white/20 mt-8 py-2">
              All caught up
            </p>
          )}
        </>
      ) : (
        /* Empty state – gallery-type-specific messaging handled by caller */
        <div className="rounded-xl border border-white/[0.06] bg-white/[0.02] px-8 py-12 text-center">
          <p className="text-white/40 text-sm">No artworks found for this section yet.</p>
        </div>
      )}
    </section>
  );
}

export default memo(MasonryGallery);
