import { computed, type Signal } from '@angular/core';
import {
  createAngularTable,
  getCoreRowModel,
  getFilteredRowModel,
  getPaginationRowModel,
  getSortedRowModel,
  type PaginationState,
  type Row,
  type SortingState,
  type Table,
} from '@tanstack/angular-table';
import {
  applySortHeaders,
  derivePinningState,
} from '../../components/data-table/lib/column-helpers';
import type { DataTableColumnDef } from '../../components/data-table/types';
import {
  useTableColumnOrder,
  type TableColumnOrderAtom,
} from '../atoms/data-table/use-table-column-order';
import {
  useTableColumnPinning,
  type TableColumnPinningAtom,
} from '../atoms/data-table/use-table-column-pinning';
import {
  useTableColumnVisibility,
  type TableColumnVisibilityAtom,
} from '../atoms/data-table/use-table-column-visibility';
import {
  useTablePagination,
  type TablePaginationAtom,
} from '../atoms/data-table/use-table-pagination';
import {
  useTableSelection,
  type TableSelectionAtom,
} from '../atoms/data-table/use-table-selection';
import { useTableSort, type TableSortAtom } from '../atoms/data-table/use-table-sort';

export interface DataTableConfig<T> {
  readonly data: () => T[];
  /**
   * Columns. Accepts either a static array or a function/signal — the
   * function form allows computed columns whose values depend on inputs
   * that aren't ready at field-initialization time (e.g. column lists
   * derived from a required input via `computed(() => [...])`).
   */
  readonly columns: DataTableColumnDef<T>[] | (() => DataTableColumnDef<T>[]);
  readonly totalRows?: () => number;
  readonly pageSize?: number;
  readonly manualSorting?: boolean;
  readonly manualPagination?: boolean;
  readonly getRowId?: (row: T, index: number) => string;
  readonly enableRowSelection?: boolean | ((row: Row<T>) => boolean);

  /**
   * Optional pre-instantiated atoms. When omitted, the preset instantiates
   * each one with sensible defaults. Pass an atom when you need to share its
   * state across composables (e.g. `usePaginatedDataTable` instantiates the
   * atoms once and passes them in here so they remain the single source of
   * truth).
   */
  readonly sort?: TableSortAtom;
  readonly pagination?: TablePaginationAtom;
  readonly columnVisibility?: TableColumnVisibilityAtom;
  readonly columnOrder?: TableColumnOrderAtom;
  readonly columnPinning?: TableColumnPinningAtom;
  readonly selection?: TableSelectionAtom;

  /**
   * Callbacks invoked AFTER the corresponding atom signal has been updated
   * by a TanStack-driven change. Useful for triggering refetch in a server-
   * paginated orchestrator.
   */
  readonly onSortingChange?: (sorting: SortingState) => void;
  readonly onPaginationChange?: (pagination: PaginationState) => void;
}

export interface DataTableResult<T> {
  readonly table: Table<T>;
  readonly hidableColumns: () => ReturnType<Table<T>['getAllColumns']>;
  readonly sort: TableSortAtom;
  readonly pagination: TablePaginationAtom;
  readonly columnVisibility: TableColumnVisibilityAtom;
  readonly columnOrder: TableColumnOrderAtom;
  readonly columnPinning: TableColumnPinningAtom;
  readonly selection: TableSelectionAtom;
}

/**
 * Binding-layer preset. Takes data-table atoms (or instantiates them with
 * defaults) and produces a TanStack `Table<T>` whose `state` is enlazado a
 * las señales de los atoms — no there is no parallel state managed inside
 * this preset. The atoms are the single source of truth.
 *
 * Pass `getRowId: r => r.id` to enable cross-page selection persistence: the
 * `useTableSelection` atom keeps a `Set<string>` keyed by entity id; the
 * `state.rowSelection` of TanStack is derived per-render from that Set + the
 * currently visible rows.
 */
export function useDataTable<T>(config: DataTableConfig<T>): DataTableResult<T> {
  const sort = config.sort ?? useTableSort();
  const pagination = config.pagination ?? useTablePagination(config.pageSize);
  const columnVisibility = config.columnVisibility ?? useTableColumnVisibility();
  const columnOrder = config.columnOrder ?? useTableColumnOrder();
  const selection = config.selection ?? useTableSelection();

  const resolveColumns = (): DataTableColumnDef<T>[] =>
    typeof config.columns === 'function' ? config.columns() : config.columns;

  // Seed column pinning from declared `sticky` fields. Done eagerly so
  // the table renders with the correct pinning on first frame. Wrapped in
  // try/catch because `config.columns` may be a reactive function that
  // depends on a required input not yet set (the case in component tests
  // that instantiate the host before binding inputs); in that scenario we
  // start with empty pinning and the column-toggle (or external setter)
  // can apply the right state once columns become available.
  const seedPinningState = (): ReturnType<typeof derivePinningState<T>> => {
    try {
      return derivePinningState(resolveColumns());
    } catch {
      return { left: [], right: [] };
    }
  };
  const columnPinning = config.columnPinning ?? useTableColumnPinning(seedPinningState());

  // Default to index-based ids (TanStack's default) when host doesn't provide
  // an entity-id-based `getRowId`. Cross-page selection persistence kicks in
  // only when host opts in via `getRowId: r => r.id` (or similar entity key).
  const getRowIdEffective = config.getRowId ?? ((_: T, index: number) => String(index));

  const visibleIds: Signal<string[]> = computed(() =>
    config.data().map((row, index) => getRowIdEffective(row, index)),
  );
  const rowSelectionState = selection.toRowSelectionState(visibleIds);

  const table = createAngularTable<T>(() => ({
    data: config.data(),
    columns: applySortHeaders(resolveColumns()),
    manualSorting: config.manualSorting ?? false,
    manualPagination: config.manualPagination ?? false,
    rowCount: config.totalRows?.() ?? undefined,
    getRowId: config.getRowId,
    enableRowSelection: config.enableRowSelection,
    enableColumnPinning: true,
    getCoreRowModel: getCoreRowModel(),
    getPaginationRowModel: config.manualPagination ? undefined : getPaginationRowModel(),
    getSortedRowModel: config.manualSorting ? undefined : getSortedRowModel(),
    getFilteredRowModel: getFilteredRowModel(),
    state: {
      sorting: sort.state(),
      pagination: pagination.state(),
      columnVisibility: columnVisibility.state(),
      columnOrder: columnOrder.state(),
      columnPinning: columnPinning.state(),
      rowSelection: rowSelectionState(),
    },
    onSortingChange: (updater) => {
      const next = updater instanceof Function ? updater(sort.state()) : updater;
      sort.set(next);
      config.onSortingChange?.(next);
    },
    onPaginationChange: (updater) => {
      const next = updater instanceof Function ? updater(pagination.state()) : updater;
      pagination.set(next);
      config.onPaginationChange?.(next);
    },
    onColumnVisibilityChange: (updater) => {
      const next = updater instanceof Function ? updater(columnVisibility.state()) : updater;
      columnVisibility.set(next);
    },
    onColumnOrderChange: (updater) => {
      const next = updater instanceof Function ? updater(columnOrder.state()) : updater;
      columnOrder.set(next);
    },
    onColumnPinningChange: (updater) => {
      const next = updater instanceof Function ? updater(columnPinning.state()) : updater;
      columnPinning.set(next);
    },
    onRowSelectionChange: (updater) => {
      const current = rowSelectionState();
      const next = updater instanceof Function ? updater(current) : updater;
      const visible = visibleIds();
      const newSet = new Set(selection.ids());
      for (const id of visible) {
        if (next[id]) newSet.add(id);
        else newSet.delete(id);
      }
      selection.setIds(newSet);
    },
  }));

  const hidableColumns = () => table.getAllColumns().filter((column) => column.getCanHide());

  return {
    table,
    hidableColumns,
    sort,
    pagination,
    columnVisibility,
    columnOrder,
    columnPinning,
    selection,
  };
}
