"use client";

import { useEffect, useMemo, useRef, useState } from "react";
import { useProducts } from "../../lib/products";
import { Reveal } from "../../components/Reveal";
import { ProductCard } from "../../components/ProductCard";
import { FEEDBACK_MS } from "../../components/AddToCartInlineButton";
import { useCart } from "../../lib/cart";

const DISPLAY_COUNT = 3;

function pickRandom(allIds: string[], excludeIds: string[], count: number): string[] {
  const pool = allIds.filter((id) => !excludeIds.includes(id));
  const shuffled = [...pool].sort(() => Math.random() - 0.5);
  return shuffled.slice(0, count);
}

// Picks up to `count` active products not already in the cart, on top of
// whatever's already in `keep`. Deliberately does NOT fall back to
// re-suggesting a cart item when the non-cart pool runs short (e.g. 2
// active products total, 1 already in the cart) — the grid just renders
// fewer, genuinely-relevant cards instead (see the centering logic in the
// component below), rather than padding itself out with something the
// shopper has already added.
function pickAvailable(allIds: string[], excludeIds: string[], keep: string[], count: number): string[] {
  const missing = count - keep.length;
  if (missing <= 0) return keep;
  return [...keep, ...pickRandom(allIds, [...excludeIds, ...keep], missing)];
}

export function RelatedProducts({
  defaultTaxRate,
  kleinunternehmer,
  wishlistEnabled,
}: {
  defaultTaxRate: number;
  kleinunternehmer: boolean;
  wishlistEnabled: boolean;
}) {
  const cart = useCart();
  const products = useProducts();
  // Cart/checkout resolve any product regardless of `active` (see
  // Product's own comment in lib/payload.ts) — this is the one discovery
  // surface among the useProducts() consumers, so it filters here itself.
  const activeProducts = useMemo(() => products.filter((p) => p.active), [products]);
  const hasItems = cart.length > 0;
  const cartKey = cart
    .map((i) => i.id)
    .sort()
    .join(",");
  // useMemo, not a plain .map() — .map() would return a new array
  // reference on every render regardless of whether `activeProducts` itself
  // changed, which would make the effect below re-run (and re-pick) every
  // single render if `productIds` were listed as its dependency.
  const productIds = useMemo(() => activeProducts.map((p) => p.id), [activeProducts]);

  // Starts empty — the catalog itself is now fetched (useProducts()), so
  // there's nothing to pick a random set from until that resolves. The
  // effect below picks as soon as products arrive, same idea as
  // CartContent's productsLoading guard.
  const [displayIds, setDisplayIds] = useState<string[]>([]);

  // First run once products have loaded: pick the initial random set.
  // After that, don't reshuffle everything on every cart change — one of
  // these cards' own AddToCartInlineButton adds to the cart without
  // navigating away, and a full reshuffle would yank the other two cards
  // out from under the user mid-browse. Instead, only swap out whichever
  // displayed card just became "already in the cart" (no longer relevant
  // to recommend) and top that one slot back up — the rest stay exactly
  // where they were.
  const pickedRef = useRef(false);
  const swapTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);

  useEffect(() => () => clearTimeout(swapTimeoutRef.current), []);

  useEffect(() => {
    if (productIds.length === 0) return;
    const cartIds = cartKey ? cartKey.split(",") : [];

    if (!pickedRef.current) {
      pickedRef.current = true;
      setDisplayIds(pickAvailable(productIds, cartIds, [], DISPLAY_COUNT));
      return;
    }

    // Delayed by FEEDBACK_MS, not immediate — the add usually came from
    // clicking one of THESE cards' own AddToCartInlineButton, which shows
    // a ~2s green "Hinzugefügt ✓" state. Swapping the card out right away
    // unmounts that button before the confirmation is ever visible.
    clearTimeout(swapTimeoutRef.current);
    swapTimeoutRef.current = setTimeout(() => {
      setDisplayIds((prev) => {
        const stillRelevant = prev.filter((id) => !cartIds.includes(id));
        return pickAvailable(productIds, cartIds, stillRelevant, DISPLAY_COUNT);
      });
    }, FEEDBACK_MS);
  }, [cartKey, productIds]);

  const displayProducts = displayIds
    .map((id) => activeProducts.find((p) => p.id === id))
    .filter((p): p is NonNullable<typeof p> => Boolean(p));

  // Section-wide gate, independent of cart contents: a "related products"
  // section makes no sense with fewer than 2 active products total to
  // ever offer, even before considering what's already in the cart.
  if (activeProducts.length < 2 || displayProducts.length === 0) return null;

  return (
    <section className="w-full bg-bg-base flex flex-col gap-8 items-center py-12 md:py-16 px-[var(--layout-padding-x)]">
      <Reveal className="flex flex-col gap-2 items-center text-center">
        <p
          className="font-semibold text-h-section text-text-primary"
          style={{ fontFamily: "var(--font-lora)" }}
        >
          {hasItems ? "Passt perfekt dazu" : "Beliebte Produkte"}
        </p>
        <p className="text-body text-text-muted">
          {hasItems ? "Weitere Tools für deinen klaren Alltag." : "Entdecke, was zu dir passt."}
        </p>
      </Reveal>

      {/* Plain divs, not RevealGroup/RevealItem — this is the one grid on
          the site whose items get swapped after the initial mount (see
          the swap-in-place effect above). RevealItem has no viewport
          trigger of its own; it only ever renders visible because it
          inherits the parent RevealGroup's already-resolved "show" state
          at first mount. A card swapped in later doesn't get a fresh
          trigger and can end up stuck at its hidden variant
          (opacity: 0) — invisible. Not worth chasing a fix for a
          scroll-reveal nicety on a list that mutates; a static grid
          renders correctly with no animation risk. */}
      {/* No items-start, and grid-auto-rows + subgrid on each card — see
          ProductGrid.tsx's own comment on why both are needed for
          ProductCard's internal rows (price, etc.) to actually line up
          across cards, not just overall card height. No RevealItem
          wrapper here (see comment above), so the subgrid classes go
          straight onto ProductCard's own className prop instead. */}
      <div className="grid grid-cols-1 sm:grid-cols-12 gap-6 sm:gap-[var(--layout-grid-gap)] [grid-auto-rows:auto_auto_auto_auto_auto_minmax(0,1fr)] w-full max-w-[75rem]">
        {displayProducts.map((product, i) => (
          <ProductCard
            key={product.id}
            product={product}
            defaultTaxRate={defaultTaxRate}
            kleinunternehmer={kleinunternehmer}
            wishlistEnabled={wishlistEnabled}
            wishlistRevealOnHover
            className={
              "sm:col-span-4 [grid-row:span_6] " +
              // Center the row when there are fewer than 3 cards to show
              // (e.g. only 1 active product left once the others are
              // already in the cart) — only the first card needs an
              // explicit start column, later ones auto-flow right after
              // it. 3-card case keeps the default left-to-right flow.
              (i === 0
                ? displayProducts.length === 1
                  ? "sm:col-start-5"
                  : displayProducts.length === 2
                    ? "sm:col-start-3"
                    : ""
                : "")
            }
          />
        ))}
      </div>
    </section>
  );
}
