"use client";

import {
  ActionIcon,
  Badge,
  Button,
  Card,
  Checkbox,
  Chip,
  Collapse,
  Group,
  MultiSelect,
  Paper,
  Skeleton,
  Stack,
  Table,
  Text,
} from "@mantine/core";
import { DatePickerInput } from "@mantine/dates";
import { IconChevronDown, IconGripVertical, IconX } from "@tabler/icons-react";
import { useEffect, useRef, useState, type ReactNode } from "react";
import { useSessionStorageState } from "@/hooks/useSessionStorageState";
import TablePaginationFooter from "@/components/TablePaginationFooter";
import { HScrollTable } from "@/lib/components/HScrollTable";
import promoStyles from "@/routes/promo-code-campaigns/_components/promo.module.css";
import type { ExportColumn } from "@/lib/export/exportXlsx";

// ---- Signup time-window filter (shared across all report types + the promo
// codes list page). "Signups in the last N days" is the primary business
// filter; it resolves to a {from,to} that the server uses against
// members.created_at. Presets are recomputed against "now" on every render so
// "Today" stays correct across day boundaries. ----
export type WindowPreset = "all" | "today" | "7d" | "30d" | "month" | "custom";

export type SignupWindowState = {
  preset: WindowPreset;
  customFrom: string | null;
  customTo: string | null;
};

export const DEFAULT_SIGNUP_WINDOW: SignupWindowState = {
  preset: "all",
  customFrom: null,
  customTo: null,
};

const startOfDay = (d: Date) => {
  const x = new Date(d);
  x.setHours(0, 0, 0, 0);
  return x;
};

// Resolve a window state to the {from,to} ISO strings the API expects.
export function resolveSignupWindow(s: SignupWindowState): {
  from?: string;
  to?: string;
} {
  const now = new Date();
  switch (s.preset) {
    case "today":
      return { from: startOfDay(now).toISOString() };
    case "7d": {
      const f = startOfDay(now);
      f.setDate(f.getDate() - 6);
      return { from: f.toISOString() };
    }
    case "30d": {
      const f = startOfDay(now);
      f.setDate(f.getDate() - 29);
      return { from: f.toISOString() };
    }
    case "month":
      return {
        from: new Date(now.getFullYear(), now.getMonth(), 1).toISOString(),
      };
    case "custom": {
      let fromIso: string | undefined = undefined;
      let toIso: string | undefined = undefined;

      if (s.customFrom) {
        const str = String(s.customFrom).trim();
        if (/^\d{4}-\d{2}-\d{2}$/.test(str)) {
          const [y, m, d] = str.split("-").map(Number);
          fromIso = new Date(y, m - 1, d, 0, 0, 0, 0).toISOString();
        } else {
          const d = new Date(str);
          if (Number.isFinite(d.getTime())) {
            const start = new Date(d);
            start.setHours(0, 0, 0, 0);
            fromIso = start.toISOString();
          }
        }
      }

      if (s.customTo) {
        const str = String(s.customTo).trim();
        if (/^\d{4}-\d{2}-\d{2}$/.test(str)) {
          const [y, m, d] = str.split("-").map(Number);
          toIso = new Date(y, m - 1, d, 23, 59, 59, 999).toISOString();
        } else {
          const d = new Date(str);
          if (Number.isFinite(d.getTime())) {
            const end = new Date(d);
            end.setHours(23, 59, 59, 999);
            toIso = end.toISOString();
          }
        }
      }

      return {
        from: fromIso,
        to: toIso,
      };
    }
    case "all":
    default:
      return {};
  }
}

// True when the window actually narrows the data (i.e. not "all time", and not
// an empty custom range).
export function isWindowActive(s: SignupWindowState): boolean {
  if (s.preset === "all") return false;
  if (s.preset === "custom") return Boolean(s.customFrom || s.customTo);
  return true;
}

const WINDOW_PRESETS: { value: WindowPreset; label: string }[] = [
  { value: "all", label: "All time" },
  { value: "today", label: "Today" },
  { value: "7d", label: "Last 7 days" },
  { value: "30d", label: "Last 30 days" },
  { value: "month", label: "This month" },
  { value: "custom", label: "Custom" },
];

export function SignupWindowControl({
  value,
  onChange,
  label = "Signups in",
}: {
  value: SignupWindowState;
  onChange: (s: SignupWindowState) => void;
  label?: string;
}) {
  const fromDate = value.customFrom ? new Date(value.customFrom as any) : null;
  const validFromDate =
    fromDate && Number.isFinite(fromDate.getTime()) ? fromDate : null;

  const toDate = value.customTo ? new Date(value.customTo as any) : null;
  const validToDate =
    toDate && Number.isFinite(toDate.getTime()) ? toDate : null;

  /*
   * No future dates.
   *
   * Every window here filters on when a member signed up or a booking was made,
   * so a range extending past today can only ever return the same rows as one
   * ending today — it looks like a valid query and quietly isn't. Capping the
   * calendar is clearer than validating after the fact.
   *
   * The two ends are also bounded by each other so an inverted range (From after
   * To) can't be entered, which would return nothing at all.
   */
  const today = new Date();
  const fromMax = validToDate && validToDate < today ? validToDate : today;

  return (
    <Stack gap={6}>
      <Text size="sm" fw={600}>
        {label}
      </Text>
      <Group gap="sm" wrap="wrap" align="center">
        <Chip.Group
          multiple={false}
          value={value.preset}
          onChange={(v) =>
            onChange({ ...value, preset: (v as WindowPreset) || "all" })
          }
        >
          <Group gap={6}>
            {WINDOW_PRESETS.map((p) => (
              <Chip key={p.value} value={p.value} variant="light" size="sm">
                {p.label}
              </Chip>
            ))}
          </Group>
        </Chip.Group>
        {value.preset === "custom" && (
          <Group gap="xs" align="center">
            <DatePickerInput
              size="sm"
              placeholder="From"
              maxDate={fromMax}
              value={validFromDate}
              onChange={(v: any) => {
                const dateStr = v
                  ? v instanceof Date
                    ? v.toISOString()
                    : new Date(v).toISOString()
                  : null;
                onChange({ ...value, customFrom: dateStr });
              }}
              clearable
              valueFormat="DD MMM YYYY"
            />
            <DatePickerInput
              size="sm"
              placeholder="To"
              minDate={validFromDate ?? undefined}
              maxDate={today}
              value={validToDate}
              onChange={(v: any) => {
                const dateStr = v
                  ? v instanceof Date
                    ? v.toISOString()
                    : new Date(v).toISOString()
                  : null;
                onChange({ ...value, customTo: dateStr });
              }}
              clearable
              valueFormat="DD MMM YYYY"
            />
          </Group>
        )}
      </Group>
    </Stack>
  );
}

const SELECT_ALL_SENTINEL = "__select_all__";

const COLLAPSE_THRESHOLD = 4;

export function SelectAllMultiSelect({
  label,
  placeholder,
  value,
  onChange,
  options,
  disabled,
}: {
  label?: string;
  placeholder?: string;
  value: string[];
  onChange: (v: string[]) => void;
  options: string[];
  disabled?: boolean;
}) {
  const [expanded, setExpanded] = useState(false);
  const inputRef = useRef<HTMLInputElement>(null);
  const allSelected = options.length > 0 && value.length === options.length;
  const collapsed = !expanded && value.length > COLLAPSE_THRESHOLD;

  useEffect(() => {
    if (expanded) inputRef.current?.focus();
  }, [expanded]);

  if (collapsed) {
    return (
      <Stack gap={4}>
        {label && (
          <Text size="sm" fw={500}>
            {label}
          </Text>
        )}
        <Group
          gap="xs"
          wrap="nowrap"
          justify="space-between"
          onClick={() => !disabled && setExpanded(true)}
          style={{
            border: "1px solid var(--mantine-color-gray-4)",
            borderRadius: "var(--mantine-radius-sm)",
            padding: "6px 10px",
            cursor: disabled ? "default" : "pointer",
          }}
        >
          <Badge variant="light" size="lg" radius="sm">
            {value.length} selected
          </Badge>
          <Group gap={4} wrap="nowrap">
            <ActionIcon
              variant="subtle"
              color="gray"
              size="sm"
              disabled={disabled}
              onClick={(e) => {
                e.stopPropagation();
                onChange([]);
              }}
              aria-label={`Clear ${label ?? "selection"}`}
            >
              <IconX size={14} />
            </ActionIcon>
            <IconChevronDown size={14} style={{ opacity: 0.6 }} />
          </Group>
        </Group>
      </Stack>
    );
  }

  return (
    <MultiSelect
      ref={inputRef}
      label={label}
      placeholder={placeholder}
      value={value}
      onChange={(next) => {
        if (next.includes(SELECT_ALL_SENTINEL)) {
          onChange(allSelected ? [] : options);
          return;
        }
        onChange(next);
      }}
      data={[
        {
          value: SELECT_ALL_SENTINEL,
          label: allSelected ? "Deselect all" : "Select all",
        },
        ...options.map((c) => ({
          value: c,
          label: c,
        })),
      ]}
      clearable
      searchable
      hidePickedOptions
      disabled={disabled}
      styles={{
        input: {
          minHeight: 42,
          height: 42,
          overflow: "hidden",
        },
        pillsList: {
          display: "flex",
          flexWrap: "nowrap",
          overflowX: "auto",
          overflowY: "hidden",
          scrollbarWidth: "thin", // Firefox
        },
      }}
    />
  );
}

export function CountryMultiSelect({
  label = "Country (signups from)",
  value,
  onChange,
  options,
  disabled,
}: {
  label?: string;
  value: string[];
  onChange: (v: string[]) => void;
  options: string[];
  disabled?: boolean;
}) {
  return (
    <SelectAllMultiSelect
      label={label}
      placeholder="Any country"
      value={value}
      onChange={onChange}
      options={options}
      disabled={disabled}
    />
  );
}

// A toggleable, reorderable report column over rows of type T. Shared by every
// report type (Members / Promo Codes / Campaigns) so the column-picker, the
// preview table and the export all read from one definition.
export type ReportColumn<T> = {
  key: string;
  label: string;
  /** Export column width in characters. */
  width: number;
  /** Whether the column is selected by default. */
  defaultOn: boolean;
  get: (row: T) => string | number;
};

export const defaultColumnKeys = <T,>(columns: ReportColumn<T>[]): string[] =>
  columns.filter((c) => c.defaultOn).map((c) => c.key);

// Run `fn` over `items` with bounded concurrency so a large export doesn't fire
// hundreds of requests at once. Aborts early when the signal trips.
export 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");
    const slice = items.slice(i, i + limit);
    out.push(...(await Promise.all(slice.map(fn))));
  }
  return out;
}

// Resolve the user's chosen keys to columns in their chosen order, dropping
// anything unknown (e.g. a stored key that no longer exists).
export const resolveColumns = <T,>(
  columns: ReportColumn<T>[],
  selected: string[],
): ReportColumn<T>[] => {
  const byKey = new Map(columns.map((c) => [c.key, c] as const));
  return selected
    .map((k) => byKey.get(k))
    .filter((c): c is ReportColumn<T> => Boolean(c));
};

// Build the ExportButton `getData` payload from the chosen columns + the full
// (all-pages) row set the caller has already fetched.
export function buildExportSheet<T>(
  sheetName: string,
  columns: ReportColumn<T>[],
  rows: T[],
) {
  const records = rows.map((row, i) => {
    const record: Record<string, string | number> = { "#": i + 1 };
    for (const col of columns) record[col.key] = col.get(row);
    return record;
  });
  return {
    sheetName,
    columns: [
      { key: "#", label: "#", width: 6 },
      ...columns.map((c) => ({ key: c.key, label: c.label, width: c.width })),
    ] as ExportColumn<Record<string, string | number>>[],
    rows: records,
  };
}

// A collapsible filter card with a chevron toggle, an active-count badge and an
// optional Reset. Shared by every filterable section so they look/behave the
// same and can be shrunk away when not needed.
export function FilterPanel({
  title = "Filters",
  activeCount = 0,
  onReset,
  defaultOpen = false,
  storageKey,
  mb = "md",
  children,
}: {
  title?: string;
  activeCount?: number;
  onReset?: () => void;
  defaultOpen?: boolean;
  // When set, the open/closed state persists across navigation (sessionStorage).
  storageKey?: string;
  mb?: string | number;
  children: ReactNode;
}) {
  const [open, setOpen] = useSessionStorageState<boolean>(
    storageKey ?? `filterPanel.open:${title}`,
    defaultOpen,
  );
  return (
    <Card withBorder radius="md" p="md" mb={mb}>
      <Group justify="space-between" align="center" wrap="nowrap">
        <Group
          gap="sm"
          align="center"
          style={{ cursor: "pointer", flex: 1 }}
          onClick={() => setOpen((o) => !o)}
        >
          <ActionIcon
            variant="subtle"
            color="gray"
            aria-label={open ? "Collapse filters" : "Expand filters"}
          >
            <IconChevronDown
              size={18}
              style={{
                transform: open ? "rotate(180deg)" : "none",
                transition: "transform 150ms ease",
              }}
            />
          </ActionIcon>
          <Text fw={700} size="lg">
            {title}
          </Text>
          {activeCount > 0 && (
            <Badge variant="light" color="dark" size="sm">
              {activeCount} active
            </Badge>
          )}
        </Group>
        {onReset && (
          <Button
            variant="subtle"
            color="gray"
            size="compact-sm"
            onClick={onReset}
            disabled={activeCount === 0}
          >
            Reset
          </Button>
        )}
      </Group>
      <Collapse in={open}>
        <Stack gap="md" mt="md">
          {children}
        </Stack>
      </Collapse>
    </Card>
  );
}

// "Choose columns" checkbox group + "Arrange columns" drag-to-reorder list.
// `selected` is the ordered list of chosen keys; `onChange` is a setState-style
// setter (functional updates are used for the reorder).
export function ColumnArranger<T>({
  columns,
  selected,
  onChange,
}: {
  columns: ReportColumn<T>[];
  selected: string[];
  onChange: React.Dispatch<React.SetStateAction<string[]>>;
}) {
  const active = resolveColumns(columns, selected);
  const [dragIndex, setDragIndex] = useState<number | null>(null);
  const [dragOverIndex, setDragOverIndex] = useState<number | null>(null);
  const resetDrag = () => {
    setDragIndex(null);
    setDragOverIndex(null);
  };

  const reorder = (from: number, to: number) => {
    onChange((prev) => {
      if (
        from === to ||
        from < 0 ||
        to < 0 ||
        from >= prev.length ||
        to >= prev.length
      ) {
        return prev;
      }
      const next = [...prev];
      const [moved] = next.splice(from, 1);
      next.splice(to, 0, moved);
      return next;
    });
  };

  return (
    <>
      <Stack gap={8}>
        <div>
          <Text size="sm" fw={600}>
            Choose columns
          </Text>
          <Text size="xs" c="dimmed">
            Tick the fields to include in the report.
          </Text>
        </div>
        <Checkbox.Group value={selected} onChange={onChange}>
          <Group gap="md" wrap="wrap">
            {columns.map((c) => (
              <Checkbox key={c.key} value={c.key} label={c.label} />
            ))}
          </Group>
        </Checkbox.Group>
        {active.length === 0 && (
          <Text size="xs" c="red">
            Select at least one column.
          </Text>
        )}
      </Stack>

      {active.length > 0 && (
        <Stack gap={8}>
          <div>
            <Text size="sm" fw={600}>
              Arrange columns
            </Text>
            <Text size="xs" c="dimmed">
              Drag to reorder — top is the first (leftmost) column in the
              export.
            </Text>
          </div>
          <Group
            gap={6}
            wrap="wrap"
            style={{
              maxHeight: 180,
              overflowY: "auto",
              padding: 4,
              border: "1px solid var(--mantine-color-gray-3)",
              borderRadius: "var(--mantine-radius-md)",
            }}
          >
            {active.map((c, i) => {
              const isDragging = dragIndex === i;
              const isDropTarget = dragOverIndex === i && dragIndex !== i;
              return (
                <Paper
                  key={c.key}
                  withBorder
                  radius="sm"
                  px={8}
                  py={4}
                  draggable
                  onDragStart={() => setDragIndex(i)}
                  onDragOver={(e) => {
                    e.preventDefault();
                    if (dragOverIndex !== i) setDragOverIndex(i);
                  }}
                  onDrop={(e) => {
                    e.preventDefault();
                    if (dragIndex !== null) reorder(dragIndex, i);
                    resetDrag();
                  }}
                  onDragEnd={resetDrag}
                  style={{
                    cursor: "grab",
                    userSelect: "none",
                    opacity: isDragging ? 0.5 : 1,
                    borderColor: isDropTarget
                      ? "var(--mantine-color-blue-5)"
                      : undefined,
                    backgroundColor: isDropTarget
                      ? "var(--mantine-color-blue-0)"
                      : undefined,
                    transition: "background-color 120ms, border-color 120ms",
                  }}
                >
                  <Group gap={6} wrap="nowrap">
                    <IconGripVertical
                      size={14}
                      color="var(--mantine-color-gray-5)"
                    />
                    <Badge size="xs" radius="sm" variant="light" color="gray">
                      {i + 1}
                    </Badge>
                    <Text size="sm" fw={500}>
                      {c.label}
                    </Text>
                  </Group>
                </Paper>
              );
            })}
          </Group>
        </Stack>
      )}
    </>
  );
}

// Preview table for a report: sticky-header HScrollTable driven by the active
// columns, plus an optional pagination footer.
export function ReportPreview<T>({
  activeColumns,
  rows,
  loading,
  rowKey,
  emptyMessage,
  total,
  page,
  totalPages,
  pageSize,
  onPageChange,
  onPageSizeChange,
  pageSizeOptions,
  showFooter,
  onRowClick,
}: {
  activeColumns: ReportColumn<T>[];
  rows: T[];
  loading: boolean;
  rowKey: (row: T, idx: number) => string;
  emptyMessage: string;
  total: number;
  page: number;
  totalPages: number;
  pageSize: number;
  onPageChange: (page: number) => void;
  onPageSizeChange: (pageSize: number) => void;
  pageSizeOptions: string[];
  showFooter: boolean;
  onRowClick?: (row: T, idx: number) => void;
}) {
  const minWidth = Math.max(600, activeColumns.length * 160);
  const skeleton = Array.from({ length: 8 }).map((_, i) => (
    <Table.Tr key={`sk-${i}`}>
      {Array.from({ length: Math.max(1, activeColumns.length) }).map(
        (__, j) => (
          <Table.Td key={j}>
            <Skeleton height={16} radius="sm" />
          </Table.Td>
        ),
      )}
    </Table.Tr>
  ));

  return (
    <>
      <HScrollTable minWidth={minWidth}>
        {/* Hover and cell borders come from promo.module.css: the accent-tinted
            row hover with a leading rail is what striping would fight. No outer
            frame — the caller already renders this inside a bordered Card. */}
        <Table stickyHeader style={{ minWidth }}>
          <Table.Thead className={promoStyles.tableHead}>
            <Table.Tr>
              {activeColumns.map((c) => (
                <Table.Th key={c.key} className={promoStyles.th}>
                  {c.label}
                </Table.Th>
              ))}
            </Table.Tr>
          </Table.Thead>
          <Table.Tbody>
            {loading ? (
              skeleton
            ) : rows.length === 0 ? (
              <Table.Tr>
                <Table.Td colSpan={activeColumns.length}>
                  <Text c="dimmed" ta="center" py="lg">
                    {emptyMessage}
                  </Text>
                </Table.Td>
              </Table.Tr>
            ) : (
              rows.map((row, idx) => (
                <Table.Tr
                  key={rowKey(row, idx)}
                  className={promoStyles.row}
                  /* `.row` assumes a clickable row; put the default cursor back
                   * when there is no click handler. */
                  style={onRowClick ? undefined : { cursor: "default" }}
                  onClick={onRowClick ? () => onRowClick(row, idx) : undefined}
                >
                  {activeColumns.map((c) => (
                    <Table.Td key={c.key}>{c.get(row) || "—"}</Table.Td>
                  ))}
                </Table.Tr>
              ))
            )}
          </Table.Tbody>
        </Table>
      </HScrollTable>

      {showFooter && (
        <TablePaginationFooter
          total={total}
          page={page}
          totalPages={totalPages}
          pageSize={pageSize}
          onPageChange={onPageChange}
          onPageSizeChange={onPageSizeChange}
          pageSizeOptions={pageSizeOptions}
        />
      )}
    </>
  );
}
