"use client";

import { useId, useMemo, useState, type ReactNode } from "react";
import {
  FlexRender,
  useTable,
  type ColumnDef,
  type ColumnFiltersState,
  type ColumnVisibilityState,
  type RowData,
  type RowSelectionState,
  type SortingState,
} from "@tanstack/react-table";
import {
  ChevronDownIcon,
  ChevronLeftIcon,
  ChevronRightIcon,
  ChevronsLeftIcon,
  ChevronsRightIcon,
  ChevronsUpDownIcon,
  ChevronUpIcon,
  Columns3Icon,
  SearchIcon,
} from "lucide-react";
import { dataTableFeatures, type DataTableFeatures } from "@/components/data-table/features";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import {
  DropdownMenu,
  DropdownMenuCheckboxItem,
  DropdownMenuContent,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
  Select,
  SelectContent,
  SelectGroup,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select";
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table";
import { pageKey, visibleSelection } from "@/lib/table-selection";
import { cn } from "@/lib/utils";

/** Filtre par colonne rendu en select ; les options sont fournies par l'appelant. */
export type DataTableFilter = {
  columnId: string;
  label: string;
  options: { value: string; label: string }[];
};

type DataTableProps<TData extends RowData> = {
  columns: ColumnDef<DataTableFeatures, TData>[];
  data: TData[];
  /** Libellé accessible et placeholder du champ de recherche globale. */
  searchLabel?: string;
  filters?: DataTableFilter[];
  emptyMessage?: string;
  pageSize?: number;
  /**
   * **Mode « page serveur »** : `data` est déjà *la* page, filtrée et triée par l'api.
   *
   * La table cesse alors de proposer ce qu'elle ne peut pas tenir — une recherche, des
   * filtres et un tri qui ne verraient que les 50 lignes chargées sur les 500 000 du
   * réservoir. Les contrôles viennent de `toolbar` (ils pilotent l'URL, donc la requête) et
   * la navigation de `footer` (curseur api). Reste actif : le masquage de colonnes, qui est
   * purement local et donc toujours honnête.
   */
  server?: boolean;
  /** Contrôles rendus dans la barre d'outils — filtres pilotés par l'URL en mode serveur. */
  toolbar?: ReactNode;
  /** Pied de table — pagination par curseur en mode serveur. */
  footer?: ReactNode;
  /** Identité stable d'une ligne : indispensable dès qu'on sélectionne (défaut : l'index). */
  getRowId?: (row: TData) => string;
  /**
   * Portée de la sélection — la cible du réservoir, par exemple. Deux pages de portées
   * différentes ne partagent jamais une sélection, **même si elles montrent les mêmes
   * lignes** : les mots-clés sont mutualisés à l'organisation (décision n°2), un même
   * identifiant peut donc apparaître sur deux cibles sans désigner le même geste.
   */
  selectionKey?: string;
  /**
   * Actions de masse (§1.8). Fournir cette fonction fait apparaître la colonne de cases à
   * cocher ; elle reçoit les identifiants sélectionnés et de quoi vider la sélection après
   * l'action. Sans elle, aucune sélection n'existe et rien ne change pour les autres tables.
   */
  bulkActions?: (selectedIds: string[], clearSelection: () => void) => ReactNode;
  /**
   * Identifiants des colonnes dont l'en-tête **et** les cellules s'alignent à droite : les
   * valeurs numériques. Aligner les cellules sans aligner l'en-tête laisserait un titre
   * flottant au-dessus d'une colonne de chiffres cadrés à droite.
   */
  alignRight?: string[];
  /**
   * Ouvre une ligne. La ligne entière devient cliquable à la souris ; les clics nés d'un
   * élément interactif (case à cocher, bouton, lien, menu) sont ignorés — ils ont déjà leur
   * propre geste, et l'appelant doit offrir une cible clavier dans la ligne (un bouton sur
   * la colonne principale) plutôt que de rendre le `<tr>` focusable.
   */
  onRowClick?: (row: TData) => void;
};

/** Ce dont un clic ne doit jamais déclencher l'ouverture d'une ligne. */
const INTERACTIVE = 'button, a, input, select, textarea, [role="checkbox"], [role="menuitem"]';

// Valeur sentinelle : un select Base UI a besoin d'une valeur présente dans ses options,
// « toutes » ne peut donc pas être la chaîne vide du filtre absent.
const ALL = "__all";

const PAGE_SIZES = [10, 20, 30, 50];

function columnLabel(header: unknown, id: string): string {
  return typeof header === "string" ? header : id;
}

/**
 * Colonne de sélection, construite ici et pas dans les définitions de colonnes de chaque
 * module : c'est le même geste partout, et elle doit rester impossible à masquer ou à trier.
 */
function selectionColumn<TData extends RowData>(): ColumnDef<DataTableFeatures, TData> {
  return {
    id: "__select",
    enableHiding: false,
    enableSorting: false,
    enableGlobalFilter: false,
    header: ({ table }) => (
      <Checkbox
        aria-label="Tout sélectionner"
        checked={table.getIsAllRowsSelected()}
        indeterminate={table.getIsSomeRowsSelected()}
        onCheckedChange={(checked) => table.toggleAllRowsSelected(checked === true)}
      />
    ),
    cell: ({ row }) => (
      <Checkbox
        aria-label="Sélectionner la ligne"
        checked={row.getIsSelected()}
        onCheckedChange={(checked) => row.toggleSelected(checked === true)}
      />
    ),
  };
}

export function DataTable<TData extends RowData>({
  columns,
  data,
  searchLabel = "Rechercher",
  filters = [],
  emptyMessage = "Aucun résultat.",
  pageSize = 10,
  server = false,
  toolbar,
  footer,
  getRowId,
  selectionKey,
  bulkActions,
  alignRight = [],
  onRowClick,
}: DataTableProps<TData>) {
  const id = useId();
  const [globalFilter, setGlobalFilter] = useState("");
  const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
  const [columnVisibility, setColumnVisibility] = useState<ColumnVisibilityState>({});
  const [sorting, setSorting] = useState<SortingState>([]);
  const [pagination, setPagination] = useState({ pageIndex: 0, pageSize });
  const [rowSelection, setRowSelection] = useState<RowSelectionState>({});

  const selectable = bulkActions !== undefined;
  const allColumns = useMemo(
    () => (selectable ? [selectionColumn<TData>(), ...columns] : columns),
    [columns, selectable],
  );

  // Identité de la page affichée. Le composant reste monté quand la page change (changer de
  // cible, charger la suivante, poser un filtre : même route, même arbre React), donc la
  // sélection survivrait à des données entièrement renouvelées — et une action de masse
  // s'appliquerait à des lignes invisibles, voire d'une autre cible.
  // Pas de mémoïsation : `getRowId` est presque toujours une lambda, la dépendance serait
  // fausse à chaque rendu — et parcourir une page (≤ 100 lignes) coûte moins que la garder.
  const rowIds = getRowId ? data.map((row) => getRowId(row)) : [];
  const key = pageKey(selectionKey ?? "", rowIds);
  const [renderedKey, setRenderedKey] = useState(key);
  if (selectable && renderedKey !== key) {
    // Ajustement d'état pendant le rendu (pattern React) plutôt qu'un effet : la barre de
    // sélection ne doit pas s'afficher, même une frame, sur une page qui n'est plus la sienne.
    setRenderedKey(key);
    setRowSelection({});
  }

  const table = useTable({
    features: dataTableFeatures,
    data,
    columns: allColumns,
    state: {
      globalFilter,
      columnFilters,
      columnVisibility,
      sorting,
      // En mode serveur, la page rendue est la page reçue : on neutralise la pagination
      // locale plutôt que de découper une seconde fois ce qui l'a déjà été côté api.
      pagination: server ? { pageIndex: 0, pageSize: Math.max(data.length, 1) } : pagination,
      rowSelection,
    },
    globalFilterFn: "includesString",
    // Trier 50 lignes sur 500 000 donnerait un classement faux avec l'apparence du vrai.
    enableSorting: !server,
    ...(getRowId ? { getRowId: (row: TData) => getRowId(row) } : {}),
    onGlobalFilterChange: setGlobalFilter,
    onColumnFiltersChange: setColumnFilters,
    onColumnVisibilityChange: setColumnVisibility,
    onSortingChange: setSorting,
    onPaginationChange: setPagination,
    onRowSelectionChange: setRowSelection,
  });

  const pageCount = Math.max(table.getPageCount(), 1);
  // Filet de sécurité : ce qui sort vers une action de masse est **l'intersection** avec la
  // page affichée. Même si la remise à zéro ci-dessus manquait un cas, un geste ne peut pas
  // porter sur une ligne que l'utilisateur ne voit pas.
  const selectedIds = visibleSelection(rowSelection, rowIds);
  const clearSelection = () => setRowSelection({});

  return (
    // `min-w-0` de haut en bas : la table défile **dans son cadre** (`overflow-x-auto` plus
    // bas), jamais en poussant la page. Sans cette contrainte, un conteneur flex prend la
    // largeur minimale de son contenu et le débordement remonte jusqu'à la fenêtre.
    <div className="flex min-w-0 flex-col gap-4">
      <div className="flex min-w-0 flex-wrap items-end gap-3">
        {server ? null : (
          <div className="min-w-0 grow space-y-1 sm:max-w-xs">
            <Label htmlFor={`${id}-search`}>{searchLabel}</Label>
            <div className="relative">
              <SearchIcon className="text-muted-foreground pointer-events-none absolute top-1/2 left-2.5 size-4 -translate-y-1/2" />
              <Input
                id={`${id}-search`}
                className="pl-8"
                value={globalFilter}
                placeholder={searchLabel}
                onChange={(event) => {
                  setGlobalFilter(event.target.value);
                  setPagination((state) => ({ ...state, pageIndex: 0 }));
                }}
              />
            </div>
          </div>
        )}

        {toolbar}

        {filters.map((filter) => {
          const column = table.getColumn(filter.columnId);
          const current = (column?.getFilterValue() as string | undefined) ?? "";
          const items = [
            { value: ALL, label: "Toutes" },
            ...filter.options.filter((option) => option.value !== ALL),
          ];
          return (
            <div
              key={filter.columnId}
              className="min-w-0 flex-1 basis-40 space-y-1 sm:flex-none sm:basis-auto"
            >
              <Label htmlFor={`${id}-${filter.columnId}`}>{filter.label}</Label>
              <Select
                items={items}
                value={current === "" ? ALL : current}
                onValueChange={(value) => {
                  column?.setFilterValue(value === ALL || value === null ? undefined : value);
                  setPagination((state) => ({ ...state, pageIndex: 0 }));
                }}
              >
                <SelectTrigger id={`${id}-${filter.columnId}`} className="h-9 w-full sm:w-44">
                  <SelectValue />
                </SelectTrigger>
                <SelectContent>
                  <SelectGroup>
                    {items.map((option) => (
                      <SelectItem key={option.value} value={option.value}>
                        {option.label}
                      </SelectItem>
                    ))}
                  </SelectGroup>
                </SelectContent>
              </Select>
            </div>
          );
        })}

        <DropdownMenu>
          <DropdownMenuTrigger render={<Button variant="outline" className="h-9" />}>
            <Columns3Icon data-icon="inline-start" />
            Colonnes
            <ChevronDownIcon data-icon="inline-end" />
          </DropdownMenuTrigger>
          <DropdownMenuContent align="end" className="w-44">
            {table
              .getAllColumns()
              // Seules les colonnes de données sont masquables : une colonne d'affichage
              // (actions, sélection) n'a rien à faire dans ce menu.
              .filter((column) => column.accessorFn !== undefined && column.getCanHide())
              .map((column) => (
                <DropdownMenuCheckboxItem
                  key={column.id}
                  checked={column.getIsVisible()}
                  onCheckedChange={(value) => column.toggleVisibility(!!value)}
                >
                  {columnLabel(column.columnDef.header, column.id)}
                </DropdownMenuCheckboxItem>
              ))}
          </DropdownMenuContent>
        </DropdownMenu>
      </div>

      {/* Le **seul** scroll horizontal légitime de l'application : celui de la table, dans
          son propre cadre. `min-w-0` lui interdit de repousser la page pour l'obtenir. */}
      <div className="min-w-0 overflow-x-auto rounded-lg border">
        <Table>
          <TableHeader className="bg-muted">
            {table.getHeaderGroups().map((headerGroup) => (
              <TableRow key={headerGroup.id}>
                {headerGroup.headers.map((header) => {
                  if (header.isPlaceholder) return <TableHead key={header.id} />;
                  const sorted = header.column.getCanSort() ? header.column.getIsSorted() : false;
                  return (
                    <TableHead
                      key={header.id}
                      colSpan={header.colSpan}
                      // `text-foreground-faint` posé ici et pas dans `TableHead` : le
                      // composant force `text-foreground`, et une règle d'élément ne peut pas
                      // gagner contre une classe utilitaire. Un en-tête de colonne est une
                      // étiquette, pas une donnée — il passe au troisième niveau de texte.
                      className={cn(
                        "text-foreground-faint font-normal",
                        alignRight.includes(header.column.id) && "text-right",
                      )}
                    >
                      {header.column.getCanSort() ? (
                        <button
                          type="button"
                          className="hover:text-foreground -mx-2 inline-flex h-8 items-center gap-1 rounded-md px-2"
                          aria-label={`Trier par ${columnLabel(header.column.columnDef.header, header.column.id)}`}
                          onClick={header.column.getToggleSortingHandler()}
                        >
                          <FlexRender header={header} />
                          {sorted === "asc" ? (
                            <ChevronUpIcon className="size-3.5" />
                          ) : sorted === "desc" ? (
                            <ChevronDownIcon className="size-3.5" />
                          ) : (
                            <ChevronsUpDownIcon className="text-muted-foreground size-3.5" />
                          )}
                        </button>
                      ) : (
                        <FlexRender header={header} />
                      )}
                    </TableHead>
                  );
                })}
              </TableRow>
            ))}
          </TableHeader>
          <TableBody>
            {table.getRowModel().rows.length > 0 ? (
              table.getRowModel().rows.map((row) => (
                <TableRow
                  key={row.id}
                  // Le voile de marque marque la ligne sélectionnée et, quand la ligne
                  // s'ouvre, celle que le curseur survole : c'est le même jeton que la
                  // navigation active, jamais une teinte propre à cet écran.
                  className={cn(
                    row.getIsSelected() && "bg-brand-surface",
                    onRowClick && "hover:bg-brand-surface cursor-pointer",
                  )}
                  onClick={
                    onRowClick
                      ? (event) => {
                          // Un clic né d'un élément interactif a déjà son geste : cocher une
                          // case ou ouvrir un menu ne doit pas, en plus, ouvrir la ligne.
                          if (
                            event.target instanceof Element &&
                            event.target.closest(INTERACTIVE) !== null
                          ) {
                            return;
                          }
                          onRowClick(row.original);
                        }
                      : undefined
                  }
                >
                  {row.getVisibleCells().map((cell) => (
                    <TableCell
                      key={cell.id}
                      className={alignRight.includes(cell.column.id) ? "text-right" : undefined}
                    >
                      <FlexRender cell={cell} />
                    </TableCell>
                  ))}
                </TableRow>
              ))
            ) : (
              <TableRow>
                <TableCell
                  colSpan={table.getVisibleLeafColumns().length}
                  className="h-24 text-center"
                >
                  {emptyMessage}
                </TableCell>
              </TableRow>
            )}
          </TableBody>
        </Table>
      </div>

      {/* Pied de table : à gauche ce qui porte sur la **sélection**, à droite ce qui porte sur
          la **page**. Les deux enveloppent — sur un écran étroit, la pagination passe sous les
          actions de masse plutôt que de sortir du cadre. */}
      <div className="flex min-w-0 flex-wrap items-center gap-x-4 gap-y-3">
        {selectable && selectedIds.length > 0 ? (
          <div className="flex min-w-0 flex-wrap items-center gap-2">
            <span className="num text-sm font-medium">
              {selectedIds.length} sélectionné{selectedIds.length > 1 ? "s" : ""}
            </span>
            {bulkActions(selectedIds, clearSelection)}
            <Button variant="ghost" size="sm" onClick={clearSelection}>
              Tout désélectionner
            </Button>
          </div>
        ) : null}

        {server ? (
          <div className="ml-auto min-w-0">{footer}</div>
        ) : (
          <div className="ml-auto flex flex-wrap items-center justify-end gap-4">
            <div className="hidden items-center gap-2 lg:flex">
              <Label htmlFor={`${id}-page-size`} className="text-sm font-medium">
                Lignes par page
              </Label>
              <Select
                items={PAGE_SIZES.map((size) => ({ label: `${size}`, value: `${size}` }))}
                value={`${table.state.pagination.pageSize}`}
                onValueChange={(value) => {
                  if (value !== null) table.setPageSize(Number(value));
                }}
              >
                <SelectTrigger id={`${id}-page-size`} className="h-9 w-20">
                  <SelectValue />
                </SelectTrigger>
                <SelectContent side="top">
                  <SelectGroup>
                    {PAGE_SIZES.map((size) => (
                      <SelectItem key={size} value={`${size}`}>
                        {size}
                      </SelectItem>
                    ))}
                  </SelectGroup>
                </SelectContent>
              </Select>
            </div>
            <p className="text-sm font-medium">
              Page {table.state.pagination.pageIndex + 1} sur {pageCount}
            </p>
            <div className="flex items-center gap-2">
              <Button
                variant="outline"
                size="icon"
                className="hidden size-8 lg:flex"
                onClick={() => table.setPageIndex(0)}
                disabled={!table.getCanPreviousPage()}
              >
                <span className="sr-only">Première page</span>
                <ChevronsLeftIcon />
              </Button>
              <Button
                variant="outline"
                size="icon"
                className="size-8"
                onClick={() => table.previousPage()}
                disabled={!table.getCanPreviousPage()}
              >
                <span className="sr-only">Page précédente</span>
                <ChevronLeftIcon />
              </Button>
              <Button
                variant="outline"
                size="icon"
                className="size-8"
                onClick={() => table.nextPage()}
                disabled={!table.getCanNextPage()}
              >
                <span className="sr-only">Page suivante</span>
                <ChevronRightIcon />
              </Button>
              <Button
                variant="outline"
                size="icon"
                className="hidden size-8 lg:flex"
                onClick={() => table.setPageIndex(pageCount - 1)}
                disabled={!table.getCanNextPage()}
              >
                <span className="sr-only">Dernière page</span>
                <ChevronsRightIcon />
              </Button>
            </div>
          </div>
        )}
      </div>
    </div>
  );
}
