"use client";

import {
  Alert,
  Badge,
  Card,
  Group,
  Loader,
  NumberInput,
  Pagination,
  Select,
  Skeleton,
  Stack,
  Switch,
  Text,
  TextInput,
  Title,
} from "@mantine/core";
import { notifications } from "@mantine/notifications";
import { IconSearch } from "@tabler/icons-react";
import { useEffect, useMemo, useRef, useState } from "react";
import { useNavigate } from "react-router";
import {
  getMemberReferralAnalytics,
  getSignupAggregate,
  listCountriesFromDB,
  type SignupAggregateRow,
} from "@/lib/features/promo-code-campaigns/query";
import {
  getPromoCodeByCode,
  listPromoCodes,
} from "@/lib/features/promo-codes/query";
import type { PromoCode } from "@/lib/features/promo-codes/types";
import type { CampaignAnalyticsResponse } from "@/lib/features/promo-code-campaigns/types";
import { PromoCodesTable } from "@/routes/promo-codes/PromoCodesTable";
import { CampaignAnalyticsCard } from "@/routes/promo-code-campaigns/_components/CampaignAnalyticsCard";
import {
  ActiveFilterChips,
  type FilterChip,
  FilterDrawer,
  FilterSection,
  FilterTrigger,
} from "@/routes/promo-code-campaigns/_components/FilterDrawer";
import {
  StatTiles,
  type StatTileSpec,
} from "@/routes/promo-code-campaigns/_components/StatTiles";
import {
  AnimatedNumber,
  PageArrival,
  PageSection,
} from "@/routes/promo-code-campaigns/_components/motion";
import { usePromoTheme } from "@/routes/promo-code-campaigns/_components/appearance";
import {
  IconCalendarStats,
  IconLogin,
  IconTicket,
  IconUsers,
  IconWorld,
} from "@tabler/icons-react";
import { AnalyticsCardSkeleton } from "@/routes/promo-code-campaigns/_components/AnalyticsCardSkeleton";
import {
  CountryMultiSelect,
  DEFAULT_SIGNUP_WINDOW,
  isWindowActive,
  resolveSignupWindow,
  SignupWindowControl,
  type SignupWindowState,
} from "@/routes/reports/_shared";
import { useSessionStorageState } from "@/hooks/useSessionStorageState";
import { ExportButton } from "@/lib/export/ExportButton";
import { InfoHint } from "@/lib/components/InfoHint";
import { buildAnalyticsSheets } from "@/lib/export/analyticsSheets";
import {
  ExportLimitError,
  MAX_EXPORT_ROWS,
  withTimeout,
} from "@/lib/export/exportLimits";

type RefCode = { promo_code_id: string; promo_code_name: string };

const PAGE_SIZE_OPTIONS = ["10", "25", "50", "100"];

// Session cache so switching away and back to this tab is instant: the cached
// analytics render immediately while a fresh copy revalidates in the
// background. Lives for the SPA session (cleared on full reload).
let cachedReferralAnalytics: CampaignAnalyticsResponse | null = null;

const fmtDate = (value: unknown) => {
  if (!value) return "";
  const d = new Date(value as string);
  return Number.isFinite(d.getTime()) ? d.toLocaleDateString() : "";
};

async function mapWithConcurrency<T, R>(
  items: T[],
  limit: number,
  fn: (item: T) => Promise<R>,
  signal?: AbortSignal,
): Promise<R[]> {
  const out: R[] = [];
  for (let i = 0; i < items.length; i += limit) {
    if (signal?.aborted)
      throw new DOMException("Export cancelled", "AbortError");
    out.push(...(await Promise.all(items.slice(i, i + limit).map(fn))));
  }
  return out;
}

function unwrapPromo(raw: unknown): PromoCode | null {
  if (!raw || typeof raw !== "object") return null;
  const obj = raw as Record<string, unknown>;
  if (
    "data" in obj &&
    obj.data &&
    typeof obj.data === "object" &&
    !Array.isArray(obj.data) &&
    !("name" in obj || "code" in obj || "rewards" in obj)
  ) {
    return obj.data as PromoCode;
  }
  return obj as PromoCode;
}

export default function MemberReferralsClientPage() {
  const navigate = useNavigate();

  /**
   * The detail URL for one code.
   *
   * Shared by the table rows and the Analytics donut/legend so both land in the
   * same place. The `from` sends Back to this page rather than the promo code
   * list, which is where the detail route would otherwise return.
   */
  const promoDetailPath = (slug: string) =>
    `/admin/promo-codes/${encodeURIComponent(slug)}?from=${encodeURIComponent(
      "/admin/member-referrals",
    )}`;

  // The whole referral universe + signups comes from one aggregate call. It can
  // be large/slow, so we fetch it in the background and render a loader; the
  // table then paginates this set client-side and lazy-loads per-row detail.
  const [analytics, setAnalytics] = useState<CampaignAnalyticsResponse | null>(
    cachedReferralAnalytics,
  );
  // Only block with the spinner on the very first load; cached visits render
  // instantly and revalidate silently.
  const [analyticsLoading, setAnalyticsLoading] = useState(
    !cachedReferralAnalytics,
  );
  const [analyticsError, setAnalyticsError] = useState<string | null>(null);

  const [promoDetailsById, setPromoDetailsById] = useState<
    Record<string, PromoCode>
  >({});
  const pcFetchingRef = useRef<Set<string>>(new Set());
  const [pcDetailsLoading, setPcDetailsLoading] = useState(false);

  // ---- committed filters ----
  const [search, setSearch] = useSessionStorageState(
    "memberReferrals.search",
    "",
  );
  /*
   * On by default: referral codes are generated per member, so most of them have
   * no signups at all and an unfiltered list is mostly empty rows. Reset restores
   * this default rather than clearing it, so "reset" still means the useful view.
   */
  /*
   * Key is versioned deliberately.
   *
   * `useSessionStorageState` prefers any stored value over the default, so
   * flipping the default alone did nothing for anyone whose session already held
   * `false` — which is everyone who had opened this page before. The `.v2`
   * suffix retires those entries so the new default actually applies.
   */
  /**
   * Narrows the codes by the login state of the members behind them.
   *
   * This page lists codes, not members, so the filter is about what a code's members
   * have done: `logged_in` keeps codes with at least one member who has signed in,
   * `pending` keeps codes with at least one who has signed up but never has. A code can
   * satisfy both at once — most do — so these are not mutually exclusive buckets and
   * the counts will not sum to the code total.
   *
   * "Has signups" stays its own switch below, since it already existed and reads as a
   * different question.
   */
  const [loginState, setLoginState] = useSessionStorageState<
    "any" | "logged_in" | "pending"
  >("memberReferrals.loginState", "any");

  const [onlyWithSignups, setOnlyWithSignups] = useSessionStorageState(
    "memberReferrals.onlyWithSignups.v2",
    true,
  );
  const [minSignups, setMinSignups] = useSessionStorageState<number | null>(
    "memberReferrals.minSignups",
    null,
  );
  const [maxSignups, setMaxSignups] = useSessionStorageState<number | null>(
    "memberReferrals.maxSignups",
    null,
  );
  const [sortBy, setSortBy] = useSessionStorageState<string>(
    "memberReferrals.sort",
    "signups-desc",
  );

  // ---- signup window + country (business filters, on top) ----
  const [signupWindow, setSignupWindow] =
    useSessionStorageState<SignupWindowState>(
      "referrals.window",
      DEFAULT_SIGNUP_WINDOW,
    );
  const [countrySel, setCountrySel] = useSessionStorageState<string[]>(
    "referrals.country",
    [],
  );
  const windowRange = useMemo(
    () => resolveSignupWindow(signupWindow),
    [signupWindow],
  );
  // Per-(code,country) aggregate over the whole referral set, within the window.
  const [aggRows, setAggRows] = useState<SignupAggregateRow[]>([]);
  // Aggregating signups for thousands of referral codes takes a moment, so we
  // surface a loader whenever the window changes.
  const [aggLoading, setAggLoading] = useState(false);

  const [page, setPage] = useState(1);
  const [pageSize, setPageSize] = useState(25);
  const [dbCountryOptions, setDbCountryOptions] = useState<string[]>([]);

  // Fetch all countries from the DB `countries` table once on mount.
  // This drives the CountryMultiSelect options independently of the signup data.
  useEffect(() => {
    let cancelled = false;
    void (async () => {
      const res = await listCountriesFromDB();
      if (cancelled || !res.success) return;
      setDbCountryOptions(res.data.map((c) => c.name));
    })();
    return () => {
      cancelled = true;
    };
  }, []);

  useEffect(() => {
    let cancelled = false;
    // Show the blocking loader only when we have nothing cached to display.
    if (!cachedReferralAnalytics) setAnalyticsLoading(true);
    (async () => {
      try {
        const res = await getMemberReferralAnalytics({
          from: windowRange.from,
          to: windowRange.to,
        });
        if (cancelled) return;
        if (res.success) {
          // Only cache the all-time view for instant revisits; windowed views
          // are transient.
          if (!isWindowActive(signupWindow)) cachedReferralAnalytics = res.data;
          setAnalytics(res.data);
          setAnalyticsError(null);
        } else {
          setAnalyticsError(res.message || "Failed to load referral analytics");
          notifications.show({
            color: "red",
            message: res.message || "Failed to load referral analytics",
          });
        }
      } catch (e) {
        if (cancelled) return;
        setAnalyticsError(
          e instanceof Error ? e.message : "Failed to load referral analytics",
        );
      } finally {
        if (!cancelled) setAnalyticsLoading(false);
      }
    })();
    return () => {
      cancelled = true;
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [windowRange.from, windowRange.to]);

  const master: RefCode[] = useMemo(
    () =>
      analytics?.promo_codes.map((p) => ({
        promo_code_id: p.promo_code_id,
        promo_code_name: p.promo_code_name,
      })) ?? [],
    [analytics],
  );

  // Per-code, per-country signup aggregate for the whole referral set within the
  // window — used for the Country filter options + country-scoped counts.
  useEffect(() => {
    let cancelled = false;
    const names = master.map((m) => m.promo_code_name).filter(Boolean);
    if (names.length === 0) {
      setAggRows([]);
      return;
    }
    setAggLoading(true);
    void (async () => {
      try {
        const res = await getSignupAggregate(names, {
          from: windowRange.from,
          to: windowRange.to,
        });
        if (cancelled || !res.success) return;
        setAggRows(res.data);
      } finally {
        if (!cancelled) setAggLoading(false);
      }
    })();
    return () => {
      cancelled = true;
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [master, windowRange.from, windowRange.to]);

  // countryOptions are now fetched from the DB at mount via dbCountryOptions.
  // The useMemo below is kept for statsByName which still needs aggRows.
  const countryOptions = dbCountryOptions;

  // Country-scoped signup totals keyed by UPPER(code name).
  const statsByName = useMemo(() => {
    const m = new Map<string, { signups: number; loggedIn: number }>();
    const set = countrySel.length ? new Set(countrySel) : null;
    for (const r of aggRows) {
      if (set && !(r.country && set.has(r.country))) continue;
      const k = String(r.code).toUpperCase();
      const cur = m.get(k) ?? { signups: 0, loggedIn: 0 };
      cur.signups += Number(r.signups || 0);
      cur.loggedIn += Number(r.loggedIn || 0);
      m.set(k, cur);
    }
    return m;
  }, [aggRows, countrySel]);

  // Counts come from the analytics byPromoCode (window-aware) unless a country
  // is selected, in which case the per-country aggregate scopes them.
  const signupsById = useMemo(() => {
    if (countrySel.length === 0) {
      return new Map(
        analytics?.analytics.byPromoCode.map(
          (r) => [r.promo_code_id, r.signups] as const,
        ) ?? [],
      );
    }
    return new Map(
      master.map(
        (m) =>
          [
            m.promo_code_id,
            statsByName.get(m.promo_code_name.toUpperCase())?.signups ?? 0,
          ] as const,
      ),
    );
  }, [analytics, countrySel, master, statsByName]);
  const loggedInById = useMemo(() => {
    if (countrySel.length === 0) {
      return new Map(
        analytics?.analytics.byPromoCode.map(
          (r) => [r.promo_code_id, r.logged_in] as const,
        ) ?? [],
      );
    }
    return new Map(
      master.map(
        (m) =>
          [
            m.promo_code_id,
            statsByName.get(m.promo_code_name.toUpperCase())?.loggedIn ?? 0,
          ] as const,
      ),
    );
  }, [analytics, countrySel, master, statsByName]);
  const internalBookingsById = useMemo(() => {
    return new Map(
      analytics?.analytics.byPromoCode.map(
        (r) => [r.promo_code_id, r.internal_bookings ?? 0] as const,
      ) ?? [],
    );
  }, [analytics]);
  const externalBookingsById = useMemo(() => {
    return new Map(
      analytics?.analytics.byPromoCode.map(
        (r) => [r.promo_code_id, r.external_bookings ?? 0] as const,
      ) ?? [],
    );
  }, [analytics]);

  const signupsOfId = (id: string) => signupsById.get(id) ?? 0;

  const activeReferralCount = useMemo(() => {
    let count = 0;
    for (const val of signupsById.values()) {
      if (Number(val || 0) > 0) count += 1;
    }
    return count;
  }, [signupsById]);

  // Maps keyed by uppercase code name — what PromoCodesTable expects.
  const signupsByCode = useMemo<Record<string, number>>(() => {
    const m: Record<string, number> = {};
    for (const p of master)
      m[p.promo_code_name.toUpperCase()] = signupsOfId(p.promo_code_id);
    return m;
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [master, signupsById]);
  const loggedInByCode = useMemo<Record<string, number>>(() => {
    const m: Record<string, number> = {};
    for (const p of master)
      m[p.promo_code_name.toUpperCase()] =
        loggedInById.get(p.promo_code_id) ?? 0;
    return m;
  }, [master, loggedInById]);
  const internalBookingsByCode = useMemo<Record<string, number>>(() => {
    const m: Record<string, number> = {};
    for (const p of master)
      m[p.promo_code_name.toUpperCase()] =
        internalBookingsById.get(p.promo_code_id) ?? 0;
    return m;
  }, [master, internalBookingsById]);
  const externalBookingsByCode = useMemo<Record<string, number>>(() => {
    const m: Record<string, number> = {};
    for (const p of master)
      m[p.promo_code_name.toUpperCase()] =
        externalBookingsById.get(p.promo_code_id) ?? 0;
    return m;
  }, [master, externalBookingsById]);

  // A time window or country selection only makes sense for codes with signups
  // in it, so they imply "has signups".
  const requireSignups =
    onlyWithSignups ||
    loginState !== "any" ||
    countrySel.length > 0 ||
    isWindowActive(signupWindow);

  const filterAndSort = (list: RefCode[]) => {
    const q = search.trim().toLowerCase();
    const filtered = list.filter((p) => {
      if (q && !p.promo_code_name.toLowerCase().includes(q)) return false;
      const s = signupsOfId(p.promo_code_id);
      if (requireSignups && s <= 0) return false;
      if (minSignups !== null && s < minSignups) return false;
      if (maxSignups !== null && s > maxSignups) return false;

      if (loginState !== "any") {
        const loggedIn = loggedInById.get(p.promo_code_id) ?? 0;
        // Pending is derived, not stored: signups minus those who have signed in.
        // Clamped at zero so a stale count can never make a code look pending.
        const pending = Math.max(0, s - loggedIn);
        if (loginState === "logged_in" && loggedIn <= 0) return false;
        if (loginState === "pending" && pending <= 0) return false;
      }
      return true;
    });
    return [...filtered].sort((a, b) => {
      switch (sortBy) {
        case "signups-asc":
          return signupsOfId(a.promo_code_id) - signupsOfId(b.promo_code_id);
        case "loggedin-desc":
          return (
            (loggedInById.get(b.promo_code_id) ?? 0) -
            (loggedInById.get(a.promo_code_id) ?? 0)
          );
        case "name-asc":
          return a.promo_code_name.localeCompare(b.promo_code_name);
        case "name-desc":
          return b.promo_code_name.localeCompare(a.promo_code_name);
        case "signups-desc":
        default:
          return signupsOfId(b.promo_code_id) - signupsOfId(a.promo_code_id);
      }
    });
  };

  const filtered = useMemo(
    () => filterAndSort(master),
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [
      master,
      search,
      onlyWithSignups,
      loginState,
      minSignups,
      maxSignups,
      sortBy,
      signupsById,
      requireSignups,
    ],
  );

  useEffect(() => {
    setPage(1);
  }, [
    search,
    onlyWithSignups,
    minSignups,
    maxSignups,
    sortBy,
    pageSize,
    countrySel,
    windowRange.from,
    windowRange.to,
  ]);

  const pageCount = Math.max(1, Math.ceil(filtered.length / pageSize));
  const safePage = Math.min(page, pageCount);
  const pageItems = useMemo(
    () => filtered.slice((safePage - 1) * pageSize, safePage * pageSize),
    [filtered, safePage, pageSize],
  );

  useEffect(() => {
    const toFetch = pageItems.filter(
      (r) => !pcFetchingRef.current.has(r.promo_code_id),
    );
    if (toFetch.length === 0) return;
    toFetch.forEach((r) => pcFetchingRef.current.add(r.promo_code_id));
    setPcDetailsLoading(true);
    (async () => {
      const fetchRow = async (row: RefCode) => {
        try {
          const res = await getPromoCodeByCode(row.promo_code_name);
          if (res?.success && res.data) {
            const promo = unwrapPromo(res.data);
            if (promo) return { id: row.promo_code_id, promo };
          }
        } catch {
          /* ignore */
        }
        pcFetchingRef.current.delete(row.promo_code_id);
        return null;
      };
      for (let i = 0; i < toFetch.length; i += 6) {
        const batch = await Promise.all(toFetch.slice(i, i + 6).map(fetchRow));
        const found = batch.filter((r): r is { id: string; promo: PromoCode } =>
          Boolean(r),
        );
        if (found.length > 0) {
          setPromoDetailsById((prev) => {
            const next = { ...prev };
            for (const r of found) next[r.id] = r.promo;
            return next;
          });
        }
      }
      setPcDetailsLoading(false);
    })();
  }, [pageItems]);

  const visiblePromoCodeIds = useMemo(
    () => new Set(filtered.map((p) => p.promo_code_id)),
    [filtered],
  );

  const tableRows: PromoCode[] = pageItems.map(
    (p) =>
      promoDetailsById[p.promo_code_id] ?? {
        id: p.promo_code_id,
        code: p.promo_code_name,
        name: p.promo_code_name,
      },
  );

  const getExportData = async (signal: AbortSignal) => {
    const list = filterAndSort(master);
    // Block oversized exports up front (before fetching details for every code).
    if (list.length > MAX_EXPORT_ROWS) {
      throw new ExportLimitError(list.length);
    }
    const details: Record<string, PromoCode> = { ...promoDetailsById };
    let missing = list.filter((p) => !details[p.promo_code_id]);
    if (missing.length > 0) {
      const missingByName = new Map<string, string>(); // code name (uppercase) -> promo_code_id
      for (const p of list) {
        if (!details[p.promo_code_id]) {
          missingByName.set(p.promo_code_name.toUpperCase(), p.promo_code_id);
        }
      }
      let offset = 0;
      const pageSize = 250;
      while (missing.length > 0) {
        if (signal.aborted)
          throw new DOMException("Export cancelled", "AbortError");
        const res = await withTimeout(
          listPromoCodes({ limit: pageSize, offset }),
        );
        if (!res.success) break;
        const items = res.data?.data || [];
        if (items.length === 0) break;

        for (const item of items) {
          const promo = unwrapPromo(item);
          if (!promo) continue;
          const itemCode = String(promo.code || promo.name || "").toUpperCase();
          const targetId = missingByName.get(itemCode);
          if (targetId) {
            details[targetId] = promo;
          }
          // Also check by ID directly
          const itemId = promo.id || promo.promoCodeId || promo.promo_code_id;
          if (itemId && list.some((p) => p.promo_code_id === itemId)) {
            details[itemId as string] = promo;
          }
        }

        missing = list.filter((p) => !details[p.promo_code_id]);
        if (items.length < pageSize) break;
        offset += pageSize;
      }
    }
    const rows = list.map((p, i) => {
      const d = details[p.promo_code_id] as Record<string, unknown> | undefined;
      const up = p.promo_code_name.toUpperCase();
      return {
        "#": i + 1,
        Name: String(d?.name ?? p.promo_code_name),
        Code: p.promo_code_name,
        Registered: signupsByCode[up] ?? 0,
        "Logged In": loggedInByCode[up] ?? 0,
        Status: d ? ((d.isActive ?? d.is_active) ? "Active" : "Inactive") : "",
        "Expires At": fmtDate(d?.expiresAt ?? d?.expires_at),
        "Created At": fmtDate(d?.createdAt ?? d?.created_at),
      };
    });
    return {
      sheetName: "Member Referrals",
      columns: [
        { key: "#" as const, label: "#", width: 6 },
        { key: "Name" as const, label: "Name", width: 28 },
        { key: "Code" as const, label: "Code", width: 22 },
        { key: "Registered" as const, label: "Registered", width: 12 },
        { key: "Logged In" as const, label: "Logged In", width: 12 },
        { key: "Status" as const, label: "Status", width: 12 },
        { key: "Expires At" as const, label: "Expires At", width: 14 },
        { key: "Created At" as const, label: "Created At", width: 14 },
      ],
      rows,
      extraSheets: buildAnalyticsSheets(analytics, visiblePromoCodeIds),
    };
  };

  const [filtersOpen, setFiltersOpen] = useState(false);
  const { series: seriesPalette } = usePromoTheme();

  /* One chip per applied filter, so a narrowed list is never mistaken for the
   * full one once the drawer is closed. */
  const filterChips = useMemo<FilterChip[]>(() => {
    const chips: FilterChip[] = [];
    if (isWindowActive(signupWindow)) {
      chips.push({
        key: "window",
        label: "Signup window",
        onRemove: () => setSignupWindow(DEFAULT_SIGNUP_WINDOW),
      });
    }
    for (const country of countrySel) {
      chips.push({
        key: `country:${country}`,
        label: `Country: ${country}`,
        onRemove: () => setCountrySel(countrySel.filter((c) => c !== country)),
      });
    }
    if (minSignups !== null || maxSignups !== null) {
      const lo = minSignups ?? 0;
      chips.push({
        key: "signup-range",
        label:
          maxSignups === null
            ? `Signups ≥ ${lo}`
            : `Signups ${lo}–${maxSignups}`,
        onRemove: () => {
          setMinSignups(null);
          setMaxSignups(null);
        },
      });
    }
    if (loginState !== "any") {
      chips.push({
        key: "login-state",
        label:
          loginState === "logged_in"
            ? "Has logged-in members"
            : "Has pending logins",
        onRemove: () => setLoginState("any"),
      });
    }

    if (onlyWithSignups) {
      chips.push({
        key: "withSignups",
        label: "With signups only",
        onRemove: () => setOnlyWithSignups(false),
      });
    }
    return chips;
  }, [
    signupWindow,
    countrySel,
    minSignups,
    maxSignups,
    onlyWithSignups,
    loginState,
    setLoginState,
  ]);

  /*
   * Headline figures for the referral codes in scope.
   *
   * Summed from signupsByCode/loggedInByCode, which already respect the country
   * filter, so these track the same rows the table and charts show.
   */
  const statTiles = useMemo<StatTileSpec[]>(() => {
    const signups = Object.values(signupsByCode).reduce(
      (sum, n) => sum + Number(n || 0),
      0,
    );
    const loggedIn = Object.values(loggedInByCode).reduce(
      (sum, n) => sum + Number(n || 0),
      0,
    );
    const pending = Math.max(0, signups - loggedIn);
    const paint = seriesPalette(5);
    const tiles: StatTileSpec[] = [
      {
        key: "codes",
        label: "Referral codes",
        value: filtered.length,
        icon: <IconTicket size={17} />,
        color: paint[0].from,
        colorTo: paint[0].to,
        hint:
          filtered.length === activeReferralCount
            ? "All referral codes"
            : `of ${activeReferralCount} total`,
      },
      {
        key: "signups",
        label: "Signups",
        value: signups,
        icon: <IconUsers size={17} />,
        color: paint[1].from,
        colorTo: paint[1].to,
        hint: isWindowActive(signupWindow) ? "In selected window" : "All time",
      },
      {
        key: "logged-in",
        label: "Logged in",
        value: loggedIn,
        icon: <IconLogin size={17} />,
        color: paint[2].from,
        colorTo: paint[2].to,
        hint: "Rewards granted",
        percent: signups > 0 ? (loggedIn / signups) * 100 : 0,
      },
      {
        key: "pending",
        label: "Pending login",
        value: pending,
        icon: <IconCalendarStats size={17} />,
        color: paint[3].from,
        colorTo: paint[3].to,
        hint: "Nothing granted yet",
        percent: signups > 0 ? (pending / signups) * 100 : 0,
      },
    ];
    // Only meaningful under an active country filter.
    if (countrySel.length > 0) {
      tiles.push({
        key: "countries",
        label: "Countries",
        value: countrySel.length,
        icon: <IconWorld size={17} />,
        color: paint[4].from,
        colorTo: paint[4].to,
        hint: "In current filter",
      });
    }
    return tiles;
  }, [
    signupsByCode,
    loggedInByCode,
    filtered.length,
    activeReferralCount,
    countrySel,
    signupWindow,
    seriesPalette,
  ]);

  const resetFilters = () => {
    // Back to the default view, which has this on — not to an unfiltered list.
    setOnlyWithSignups(true);
    setMinSignups(null);
    setMaxSignups(null);
    setSignupWindow(DEFAULT_SIGNUP_WINDOW);
    setCountrySel([]);
    setLoginState("any");
  };

  const rangeStart = filtered.length === 0 ? 0 : (safePage - 1) * pageSize + 1;
  const rangeEnd = Math.min(safePage * pageSize, filtered.length);

  return (
    <PageArrival>
      <Stack px={28} py={20} gap="lg">
        <Group justify="space-between" align="center">
          <Group gap="sm">
            <Title order={3}>Member Referrals</Title>
            <InfoHint
              ariaLabel="About member-referral codes"
              label="This section automatically includes every member-referral promo code. Codes are attached implicitly — there is nothing to attach or detach here."
            />
            {analyticsLoading ? (
              <Skeleton height={26} width={96} radius="sm" />
            ) : (
              /* No explicit colour — follows the theme accent. */
              <Badge variant="light" size="md" radius="sm">
                <AnimatedNumber value={activeReferralCount} /> member referrals
              </Badge>
            )}
            {aggLoading && <Loader size="xs" />}
          </Group>
          <Group>
            <FilterTrigger
              activeCount={filterChips.length}
              onClick={() => setFiltersOpen(true)}
            />
            <ExportButton
              label="Export Promo Codes"
              filename="member-referral-promo-codes"
              getData={getExportData}
              section="memberReferrals"
            />
          </Group>
        </Group>

        <ActiveFilterChips chips={filterChips} onReset={resetFilters} />

        <PageSection>
          <StatTiles tiles={statTiles} loading={analyticsLoading} />
        </PageSection>

        <FilterDrawer
          opened={filtersOpen}
          onClose={() => setFiltersOpen(false)}
          activeCount={filterChips.length}
          onReset={resetFilters}
        >
          <FilterSection
            icon={<IconCalendarStats size={15} />}
            title="Signup window"
            description="Limits signups counted in the tiles and charts."
          >
            <SignupWindowControl
              value={signupWindow}
              onChange={setSignupWindow}
            />
          </FilterSection>

          <FilterSection
            icon={<IconWorld size={15} />}
            title="Country"
            description="Keeps only codes with signups from these countries."
          >
            <CountryMultiSelect
              value={countrySel}
              onChange={setCountrySel}
              options={countryOptions}
              disabled={analyticsLoading}
            />
          </FilterSection>

          <FilterSection
            icon={<IconUsers size={15} />}
            title="Signup volume"
            description="Bounds on a code's signup count."
            withDivider={false}
          >
            <Group gap="xs" grow align="flex-end">
              <NumberInput
                label="Min signups"
                placeholder="Any"
                min={0}
                value={minSignups ?? ""}
                onChange={(v) =>
                  setMinSignups(typeof v === "number" ? v : null)
                }
              />
              <NumberInput
                label="Max signups"
                placeholder="Any"
                min={0}
                value={maxSignups ?? ""}
                onChange={(v) =>
                  setMaxSignups(typeof v === "number" ? v : null)
                }
              />
            </Group>
            <Switch
              label="Only codes with signups in window"
              checked={onlyWithSignups}
              onChange={(e) => setOnlyWithSignups(e.currentTarget.checked)}
            />
            {/*
              Login state of the members behind each code.
              
              A code usually has both logged-in and pending members, so these are not
              exclusive buckets and the two counts will not add up to the code total —
              the description says so rather than leaving it to be inferred from
              numbers that look wrong.
            */}
            <Select
              label="Member login state"
              value={loginState}
              onChange={(v) =>
                v && setLoginState(v as "any" | "logged_in" | "pending")
              }
              data={[
                { value: "any", label: "Any" },
                { value: "logged_in", label: "Has logged-in members" },
                { value: "pending", label: "Has pending logins" },
              ]}
              allowDeselect={false}
              description={
                loginState === "any"
                  ? "Every code, whatever its members have done."
                  : loginState === "logged_in"
                    ? "Codes with at least one member who has signed in."
                    : "Codes with at least one member who signed up but never signed in."
              }
            />
          </FilterSection>
        </FilterDrawer>

        {analyticsError && (
          <Alert color="red" variant="light">
            {analyticsError}
          </Alert>
        )}

        {analyticsLoading ? (
          <AnalyticsCardSkeleton />
        ) : (
          <CampaignAnalyticsCard
            analytics={analytics}
            visiblePromoCodeIds={visiblePromoCodeIds}
            countryFilter={countrySel}
            showBookingsAnalytics={false}
            /* Same destination as a table row below. */
            onPromoCodeClick={(promo) => navigate(promoDetailPath(promo.name))}
          />
        )}

        <Card withBorder radius="lg" p="lg">
          <Text fw={700} size="lg" mb="sm">
            Promo Codes ({filtered.length})
          </Text>
          <Group
            gap="sm"
            align="center"
            justify="space-between"
            wrap="wrap"
            mb="md"
          >
            <TextInput
              placeholder="Quick search by code…"
              leftSection={<IconSearch size={16} />}
              value={search}
              onChange={(e) => setSearch(e.currentTarget.value)}
              style={{ flex: 1, minWidth: 240, maxWidth: 360 }}
            />
            <Select
              size="sm"
              variant="filled"
              w={180}
              value={sortBy}
              onChange={(v) => setSortBy(v || "signups-desc")}
              data={[
                { value: "signups-desc", label: "Most signups" },
                { value: "signups-asc", label: "Least signups" },
                { value: "loggedin-desc", label: "Most logged in" },
                { value: "name-asc", label: "Code (A–Z)" },
                { value: "name-desc", label: "Code (Z–A)" },
              ]}
            />
          </Group>

          {analyticsLoading ? (
            <Stack gap="xs">
              {Array.from({ length: 8 }).map((_, i) => (
                <Skeleton key={i} height={44} radius="sm" />
              ))}
            </Stack>
          ) : (
            <PromoCodesTable
              promoCodes={tableRows}
              signupsByCode={signupsByCode}
              signupsLabel="Registered"
              loggedInByCode={loggedInByCode}
              internalBookingsByCode={internalBookingsByCode}
              externalBookingsByCode={externalBookingsByCode}
              rowOffset={(safePage - 1) * pageSize}
              onRowClick={(p) => navigate(promoDetailPath(p.code || ""))}
              canUpdate={false}
              canDelete={false}
              emptyMessage="No member-referral promo codes match the filters."
            />
          )}

          {filtered.length > 0 && (
            <Group justify="space-between" mt="md" align="center">
              <Group gap="xs" align="center">
                <Select
                  size="sm"
                  variant="filled"
                  w={90}
                  value={String(pageSize)}
                  onChange={(v) => setPageSize(Number(v) || 25)}
                  data={PAGE_SIZE_OPTIONS}
                  allowDeselect={false}
                  searchable={false}
                  aria-label="Entries per page"
                />
                <Text size="sm" c="dimmed">
                  Entries per page
                </Text>
                <Text size="sm" c="dimmed">
                  · Showing {rangeStart}–{rangeEnd} of {filtered.length}
                </Text>
                {pcDetailsLoading && <Loader size="xs" />}
              </Group>
              {pageCount > 1 && (
                <Pagination
                  value={safePage}
                  onChange={setPage}
                  total={pageCount}
                  size="sm"
                />
              )}
            </Group>
          )}
        </Card>
      </Stack>
    </PageArrival>
  );
}
