"use client";

import {
  ActionIcon,
  Badge,
  Button,
  Checkbox,
  Group,
  Popover,
  Stack,
  Text,
  Tooltip,
} from "@mantine/core";
import {
  IconArrowUp,
  IconColumns3,
  IconEye,
  IconInbox,
  IconTrash,
} from "@tabler/icons-react";
import {
  createColumnHelper,
  flexRender,
  getCoreRowModel,
  useReactTable,
  type ColumnDef,
  type SortingState,
  type VisibilityState,
} from "@tanstack/react-table";
import { useMemo } from "react";
import { useSessionStorageState } from "@/hooks/useSessionStorageState";
import { HScrollTable } from "@/lib/components/HScrollTable";
import type { PromoCodeCampaignWithCount } from "@/lib/features/promo-code-campaigns/types";
import type { AccessScope } from "@/lib/features/types";
import { EASE_OUT, motion, useReducedMotion } from "./motion";
import styles from "./promo.module.css";

/**
 * Sort keys understood by the parent's `applyCampaignFilters`. Header clicks map
 * to these instead of tanstack doing the sorting, because the parent already
 * sorts the whole dataset (which can be wider than the current page).
 */
export type CampaignSortKey =
  | "newest"
  | "oldest"
  | "most-codes"
  | "most-signups"
  | "most-internal"
  | "most-curated"
  | "recently-updated"
  | "name";

/** Column id → the sort key for each direction. Columns absent here can't sort. */
const SORT_MAP: Record<
  string,
  { asc: CampaignSortKey; desc: CampaignSortKey }
> = {
  name: { asc: "name", desc: "name" },
  promo_code_count: { asc: "most-codes", desc: "most-codes" },
  signups: { asc: "most-signups", desc: "most-signups" },
  internal: { asc: "most-internal", desc: "most-internal" },
  curated: { asc: "most-curated", desc: "most-curated" },
  created_at: { asc: "oldest", desc: "newest" },
  updated_at: { asc: "recently-updated", desc: "recently-updated" },
};

/** Inverse of SORT_MAP — turns the parent's sort key back into tanstack state. */
const SORT_STATE: Record<CampaignSortKey, SortingState> = {
  newest: [{ id: "created_at", desc: true }],
  oldest: [{ id: "created_at", desc: false }],
  "most-codes": [{ id: "promo_code_count", desc: true }],
  "most-signups": [{ id: "signups", desc: true }],
  "most-internal": [{ id: "internal", desc: true }],
  "most-curated": [{ id: "curated", desc: true }],
  "recently-updated": [{ id: "updated_at", desc: true }],
  name: [{ id: "name", desc: false }],
};

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

interface CampaignsTableProps {
  rows: PromoCodeCampaignWithCount[];
  /** Index of the first row within the whole result set, for the "#" column. */
  rowOffset: number;
  loading: boolean;
  accessScope: AccessScope;
  sortBy: CampaignSortKey;
  onSortChange: (key: CampaignSortKey) => void;
  signupsOf: (id: string) => number;
  internalBookingsOf: (id: string) => number;
  externalBookingsOf: (id: string) => number;
  onView: (id: string) => void;
  onDelete: (row: { id: string; name: string }) => void;
  /** Identity of the current page/filter set — restarts the row cascade. */
  animationKey: string;
}

/**
 * Campaigns table built on headless @tanstack/react-table with hand-rolled
 * Mantine markup: sortable headers, a column-visibility menu, hover-revealed
 * row actions and a staggered row cascade.
 */
export function CampaignsTable({
  rows,
  rowOffset,
  loading,
  accessScope,
  sortBy,
  onSortChange,
  signupsOf,
  internalBookingsOf,
  externalBookingsOf,
  onView,
  onDelete,
  animationKey,
}: CampaignsTableProps) {
  const reduce = useReducedMotion();
  const [columnVisibility, setColumnVisibility] =
    useSessionStorageState<VisibilityState>("campaignList.columns", {});

  const columns = useMemo<ColumnDef<PromoCodeCampaignWithCount, any>[]>(() => {
    const col = createColumnHelper<PromoCodeCampaignWithCount>();
    return [
      col.display({
        id: "index",
        header: "#",
        cell: (ctx) => (
          <Text size="sm" c="dimmed" className={styles.num}>
            {rowOffset + ctx.row.index + 1}
          </Text>
        ),
      }),
      col.accessor("name", {
        id: "name",
        header: "Name",
        cell: (ctx) => <Text fw={600}>{ctx.getValue()}</Text>,
      }),
      col.accessor((r) => Number(r.promo_code_count || 0), {
        id: "promo_code_count",
        header: "Promo Codes",
        cell: (ctx) => {
          const count = ctx.getValue() as number;
          return count > 0 ? (
            <Text fw={600} className={styles.num}>
              {count}
            </Text>
          ) : (
            <Text c="dimmed" className={styles.num}>
              0
            </Text>
          );
        },
      }),
      col.display({
        id: "signups",
        header: "Signups",
        cell: (ctx) => (
          <Text fw={600} className={styles.num}>
            {signupsOf(ctx.row.original.id)}
          </Text>
        ),
      }),
      // Booking badges take the same two role colours as the bookings donut, so
      // the table and the chart agree on what "internal" and "curated" look like.
      col.display({
        id: "internal",
        header: "Karma Subito Bookings",
        cell: (ctx) => (
          <span
            className={`${styles.dataBadge} ${styles.num}`}
            data-role="internal"
          >
            {internalBookingsOf(ctx.row.original.id)}
          </span>
        ),
      }),
      col.display({
        id: "curated",
        header: "Curated Bookings",
        cell: (ctx) => (
          <span
            className={`${styles.dataBadge} ${styles.num}`}
            data-role="curated"
          >
            {externalBookingsOf(ctx.row.original.id)}
          </span>
        ),
      }),
      col.accessor("created_at", {
        id: "created_at",
        header: "Created At",
        cell: (ctx) => (
          <Text size="sm" c="dimmed">
            {fmtDateTime(ctx.getValue())}
          </Text>
        ),
      }),
      col.accessor("updated_at", {
        id: "updated_at",
        header: "Updated At",
        cell: (ctx) => (
          <Text size="sm" c="dimmed">
            {fmtDateTime(ctx.getValue())}
          </Text>
        ),
      }),
      col.accessor((r) => r.updated_by || r.created_by || "", {
        id: "updated_by",
        header: "Updated By",
        cell: (ctx) => (
          <Text size="sm" lineClamp={1}>
            {(ctx.getValue() as string) || "-"}
          </Text>
        ),
      }),
      col.display({
        id: "actions",
        header: "Actions",
        cell: (ctx) => {
          const row = ctx.row.original;
          const isEmpty = Number(row.promo_code_count || 0) === 0;
          return (
            <Group
              gap={4}
              wrap="nowrap"
              className={styles.rowActions}
              onClick={(e) => e.stopPropagation()}
            >
              <Tooltip label="View campaign" withArrow openDelay={300}>
                <ActionIcon
                  variant="light"
                  size="sm"
                  aria-label={`View ${row.name}`}
                  onClick={() => onView(row.id)}
                >
                  <IconEye size={15} />
                </ActionIcon>
              </Tooltip>
              {isEmpty && accessScope.delete && (
                <Tooltip label="Delete campaign" withArrow openDelay={300}>
                  <ActionIcon
                    variant="light"
                    color="red"
                    size="sm"
                    aria-label={`Delete ${row.name}`}
                    onClick={() => onDelete({ id: row.id, name: row.name })}
                  >
                    <IconTrash size={15} />
                  </ActionIcon>
                </Tooltip>
              )}
            </Group>
          );
        },
      }),
    ];
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [
    rowOffset,
    signupsOf,
    internalBookingsOf,
    externalBookingsOf,
    accessScope,
  ]);

  const table = useReactTable({
    data: rows,
    columns,
    state: { sorting: SORT_STATE[sortBy] ?? [], columnVisibility },
    onColumnVisibilityChange: setColumnVisibility,
    // The parent sorts (and may sort across the whole dataset, not just this
    // page), so tanstack only reflects the direction in the header.
    manualSorting: true,
    getCoreRowModel: getCoreRowModel(),
  });

  const leafColumns = table.getVisibleLeafColumns();
  const colCount = leafColumns.length;

  /** Cycles a column's header between its two sort keys. */
  const handleHeaderClick = (columnId: string) => {
    const entry = SORT_MAP[columnId];
    if (!entry) return;
    const current = SORT_STATE[sortBy]?.[0];
    if (current?.id !== columnId) {
      onSortChange(entry.desc);
      return;
    }
    // Same column clicked again — flip direction where the column has two keys.
    onSortChange(current.desc ? entry.asc : entry.desc);
  };

  return (
    <>
      <Group justify="flex-end" mb={6}>
        <Popover position="bottom-end" withArrow shadow="md" width={220}>
          <Popover.Target>
            <Button
              variant="subtle"
              color="gray"
              size="compact-sm"
              leftSection={<IconColumns3 size={15} />}
            >
              Columns
            </Button>
          </Popover.Target>
          <Popover.Dropdown>
            <Stack gap={8}>
              <Text size="xs" fw={700} tt="uppercase" c="dimmed">
                Visible columns
              </Text>
              {table
                .getAllLeafColumns()
                .filter((c) => c.id !== "index" && c.id !== "actions")
                .map((column) => (
                  <Checkbox
                    key={column.id}
                    size="xs"
                    label={String(column.columnDef.header)}
                    checked={column.getIsVisible()}
                    onChange={column.getToggleVisibilityHandler()}
                  />
                ))}
            </Stack>
          </Popover.Dropdown>
        </Popover>
      </Group>

      <HScrollTable minWidth={820}>
        <table
          style={{
            minWidth: 820,
            width: "100%",
            borderCollapse: "separate",
            borderSpacing: 0,
          }}
        >
          <thead className={styles.tableHead}>
            {table.getHeaderGroups().map((group) => (
              <tr key={group.id}>
                {group.headers.map((header) => {
                  const sortable = Boolean(SORT_MAP[header.column.id]);
                  const active =
                    SORT_STATE[sortBy]?.[0]?.id === header.column.id;
                  const desc = SORT_STATE[sortBy]?.[0]?.desc ?? false;
                  return (
                    <th
                      key={header.id}
                      className={`${styles.th} ${sortable ? styles.thSortable : ""}`}
                      onClick={
                        sortable
                          ? () => handleHeaderClick(header.column.id)
                          : undefined
                      }
                      aria-sort={
                        active ? (desc ? "descending" : "ascending") : undefined
                      }
                    >
                      <span className={styles.thInner}>
                        {flexRender(
                          header.column.columnDef.header,
                          header.getContext(),
                        )}
                        {sortable && (
                          <span
                            className={`${styles.sortIcon} ${
                              active ? styles.sortIconActive : ""
                            } ${active && desc ? styles.sortIconDesc : ""}`}
                          >
                            <IconArrowUp size={13} stroke={2.5} />
                          </span>
                        )}
                      </span>
                    </th>
                  );
                })}
              </tr>
            ))}
          </thead>

          <tbody key={animationKey}>
            {loading ? (
              Array.from({ length: 6 }).map((_, i) => (
                <tr
                  key={`sk-${i}`}
                  className={styles.row}
                  style={{ cursor: "default" }}
                >
                  {Array.from({ length: colCount }).map((__, j) => (
                    <td key={j}>
                      <div
                        className={styles.shimmer}
                        style={{ width: j === 1 ? "70%" : "45%" }}
                      />
                    </td>
                  ))}
                </tr>
              ))
            ) : table.getRowModel().rows.length === 0 ? (
              <tr>
                <td colSpan={colCount} className={styles.emptyCell}>
                  <Stack align="center" gap={6}>
                    <IconInbox size={38} stroke={1.2} opacity={0.3} />
                    <Text c="dimmed" size="sm">
                      No campaigns match the current filters
                    </Text>
                  </Stack>
                </td>
              </tr>
            ) : (
              table.getRowModel().rows.map((row, i) => (
                <motion.tr
                  key={row.id}
                  className={styles.row}
                  initial={reduce ? false : { opacity: 0, y: 6 }}
                  animate={{ opacity: 1, y: 0 }}
                  transition={{
                    duration: 0.26,
                    // Cap the cascade so a 100-row page still settles quickly.
                    delay: Math.min(i * 0.022, 0.45),
                    ease: EASE_OUT,
                  }}
                  onClick={() => onView(row.original.id)}
                >
                  {row.getVisibleCells().map((cell) => (
                    <td key={cell.id}>
                      {flexRender(
                        cell.column.columnDef.cell,
                        cell.getContext(),
                      )}
                    </td>
                  ))}
                </motion.tr>
              ))
            )}
          </tbody>
        </table>
      </HScrollTable>
    </>
  );
}
