import { Group, ScrollArea, Stack, Table, Text } from "@mantine/core";
import { useMemo } from "react";
import styles from "@/routes/promo-code-campaigns/_components/promo.module.css";
import {
  eventTypeLabel,
  FUNNEL_ORDER,
  type SourceFunnel,
} from "@/lib/features/event-analytics/types";

const fmtInt = (n: number) => n.toLocaleString("en-US");

/**
 * Journeys reaching each funnel step, grouped by one dimension.
 *
 * Shared by the campaign and section cards: same question, same columns, same
 * maths — only the grouping differs. Keeping one implementation means a change
 * to how conversion is presented cannot land in one card and miss the other.
 */
export default function ConversionTable({
  rows,
  label,
  emptyMessage = "No journeys in this range.",
  maxHeight = 240,
  formatKey,
}: {
  rows: SourceFunnel[];
  /** Header for the grouping column, e.g. "Source" or "Section". */
  label: string;
  emptyMessage?: string;
  maxHeight?: number;
  /**
   * Turns a row key into what the reader sees.
   *
   * Only some keys need it: a utm_source is whatever marketing typed into a
   * link and must be shown exactly, while a module is an internal identifier
   * that has a name. Defaults to showing the key unchanged.
   */
  formatKey?: (key: string) => string;
}) {
  /*
   * Step counts are cumulative, derived from the "where it stopped" buckets.
   *
   * Core reports each journey's final step, which partitions cleanly but reads
   * backwards as a funnel: a group converting everything would show zero
   * "stopped at hold". Adding the later buckets back in gives "reached", which
   * only ever falls from one step to the next.
   */
  const data = useMemo(
    () =>
      rows.map((row) => ({
        key: row.key,
        events: row.events,
        addedToCart: row.journeys,
        reachedHold: row.stoppedAtHold + row.purchased,
        booked: row.purchased,
        successRatePct: row.conversionPct,
      })),
    [rows],
  );

  /*
   * `stoppedAtCart` / `stoppedAtHold` are positional buckets — the first and
   * middle steps of whatever order core was given — so their labels come from
   * the same place rather than from the names they were first written for.
   */
  const [entryStep, midStep] = FUNNEL_ORDER;
  /*
   * Short headers for the steps we know, full label otherwise.
   *
   * The columns are narrow — "Booking hold initiated" wraps to three lines and
   * pushes the figures out of line — but a header must still follow the order
   * rather than be a fixed word, or a change to the sequence leaves the right
   * numbers under the wrong names.
   */
  const shortLabel = (eventType: string | undefined, fallback: string) => {
    if (!eventType) return fallback;
    if (eventType === "add_to_cart") return "Cart";
    if (eventType === "booking_hold_initiated") return "Hold";
    if (eventType === "page_visited") return "Visits";
    return eventTypeLabel(eventType);
  };
  const entryLabel = shortLabel(entryStep, "Entered");
  const midLabel = shortLabel(midStep, "Reached");

  const totals = useMemo(
    () =>
      data.reduce(
        (acc, r) => ({
          events: acc.events + r.events,
          addedToCart: acc.addedToCart + r.addedToCart,
          reachedHold: acc.reachedHold + r.reachedHold,
          booked: acc.booked + r.booked,
        }),
        { events: 0, addedToCart: 0, reachedHold: 0, booked: 0 },
      ),
    [data],
  );

  if (data.length === 0) {
    return (
      <Text c="dimmed" size="sm">
        {emptyMessage}
      </Text>
    );
  }

  return (
    <Stack gap="xs">
      {/*
       * Scrolls rather than grows.
       *
       * Group counts are open-ended, and this card shares a row with others.
       * Capping the body keeps them aligned however many groups appear, and the
       * header stays put while the rows move.
       */}
      <ScrollArea.Autosize mah={maxHeight} type="auto" offsetScrollbars>
        <Table highlightOnHover verticalSpacing={6} horizontalSpacing="xs" stickyHeader>
          <Table.Thead>
            <Table.Tr>
              <Table.Th>{label}</Table.Th>
              {/* Events first: the raw volume the funnel is drawn from. */}
              <Table.Th ta="right">Events</Table.Th>
              {/*
                * Named from the funnel order, not hardcoded.
                *
                * These two columns are the first and middle steps whatever
                * they are; when the funnel changed from cart/hold/purchase to
                * page-visit/cart/purchase, fixed "Cart" and "Hold" headers
                * would have gone on labelling the right numbers with the wrong
                * names.
                */}
              <Table.Th ta="right">{entryLabel}</Table.Th>
              <Table.Th ta="right">{midLabel}</Table.Th>
              <Table.Th ta="right">Booked</Table.Th>
              <Table.Th ta="right">%</Table.Th>
            </Table.Tr>
          </Table.Thead>
          <Table.Tbody>
            {data.map((row) => (
              <Table.Tr key={row.key}>
                <Table.Td>
                  {/* Raw key in the tooltip, so a labelled row can still be
                      matched back to the value in Firestore. */}
                  <Text size="sm" fw={600} lineClamp={1} title={row.key}>
                    {formatKey ? formatKey(row.key) : row.key}
                  </Text>
                </Table.Td>
                <Table.Td ta="right" className={styles.num}>
                  {fmtInt(row.events)}
                </Table.Td>
                <Table.Td ta="right" className={styles.num}>
                  {fmtInt(row.addedToCart)}
                </Table.Td>
                <Table.Td ta="right" className={styles.num}>
                  {fmtInt(row.reachedHold)}
                </Table.Td>
                <Table.Td ta="right" className={styles.num}>
                  {fmtInt(row.booked)}
                </Table.Td>
                <Table.Td ta="right" className={styles.num}>
                  <Text size="sm" fw={700}>
                    {row.successRatePct}%
                  </Text>
                </Table.Td>
              </Table.Tr>
            ))}
          </Table.Tbody>
        </Table>
      </ScrollArea.Autosize>

      {data.length > 1 && (
        // Outside the scroll area: the total is what people reconcile against,
        // so it must not scroll out of view.
        <Group justify="space-between" gap="xs" className={styles.legendTotal} pt={6}>
          <Text size="sm" fw={700}>
            Total
          </Text>
          <Group gap="md" wrap="nowrap">
            <Text size="xs" c="dimmed" className={styles.num}>
              {fmtInt(totals.events)} events · {fmtInt(totals.addedToCart)} →{" "}
              {fmtInt(totals.reachedHold)} → {fmtInt(totals.booked)}
            </Text>
            <Text size="sm" fw={700} className={styles.num}>
              {totals.addedToCart
                ? ((totals.booked / totals.addedToCart) * 100).toFixed(1)
                : "0.0"}
              %
            </Text>
          </Group>
        </Group>
      )}
    </Stack>
  );
}
