import { forwardRef, useEffect, useLayoutEffect, useMemo, useRef, useState, type CSSProperties, type ReactNode } from "react";
import { createPortal } from "react-dom";
import { ArrowDown, ArrowUp, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, ChevronsUpDown, Filter, GripVertical, Plus, Trash2, X } from "lucide-react";
import StatusBadge from "../StatusBadge";
import TableActionGroup from "./TableActionGroup";
import { usePlatformLanguage, i18nMessage } from "../../i18n/LanguageContext";
import {
  DATA_GRID_MAX_TRACK_WIDTH,
  dataGridColumnPixelWidth as columnPixelWidth,
  dataGridColumnTrackWithMinimum as columnTrackWithMinimum,
  dataGridLayoutSignature,
  dataGridWidthsForLayout,
  effectiveDataGridColumnMaxWidth as effectiveColumnMaxWidth,
  effectiveDataGridColumnMinWidth as effectiveColumnMinWidth,
  fitDataGridColumns,
  isFlexibleDataGridWidth as isFlexibleWidth,
  resizeDataGridColumn,
  roundDataGridWidths as roundWidthRecord
} from "./dataGridSizing";

export type DataGridSortDirection = "asc" | "desc";
export type DataGridFilterType = "text" | "number" | "integer" | "boolean" | "date" | "list";
export type DataGridInitialFit = "content" | "container";
export type DataGridResizeBehavior = "free" | "cover" | "constrained";

export type DataGridListOption = {
  value: string;
  label: string;
  disabled?: boolean;
};

export type DataGridListConfig<T> = {
  options: DataGridListOption[];
  display?: "text" | "pill";
  editable?: boolean;
  onChange?: (row: T, index: number, value: string) => void;
  optionsEditable?: boolean;
  onOptionsChange?: (options: DataGridListOption[]) => void;
};

export type DataGridQueryState = {
  sort: {columnId: string;direction: DataGridSortDirection;} | null;
  filters: Record<string, string>;
};

type DataGridPaginationBase = {
  page: number;
  pageSize: number;
  pageSizeOptions?: number[];
  onPageChange: (page: number) => void;
  onPageSizeChange?: (pageSize: number) => void;
  disabled?: boolean;
};

export type DataGridClientPagination = DataGridPaginationBase & {
  /**
   * Client mode filters and sorts before slicing. `rows` must therefore contain
   * the complete logical result set, never an already paginated backend page.
   */
  mode?: "client";
  totalRows?: never;
};

export type DataGridServerPagination = DataGridPaginationBase & {
  /**
   * Server mode treats `rows` as the current backend page. The query callback
   * must apply every emitted sort/filter to the backend before pagination.
   */
  mode: "server";
  /** Total rows after the backend has applied the current filters. */
  totalRows: number;
};

export type DataGridPagination = DataGridClientPagination | DataGridServerPagination;

type TypedFilterOperator = "contains" | "eq" | "gt" | "gte" | "lt" | "lte" | "before" | "after";

export type DataGridColumn<T> = {
  id: string;
  header: ReactNode;
  /**
   * Preferred track width. Pixel and percentage values are preferences;
   * fractional values share residual container space by their declared weight.
   * A `minmax()` lower pixel bound is also enforced during manual resizing.
   */
  width?: number | string;
  /** Hard lower bound. Header controls may require a larger accessible minimum. */
  minWidth?: number;
  /**
   * Upper bound for direct resizing. A cover layout may exceed preferred
   * maxima only when that is required to consume the complete container.
   */
  maxWidth?: number;
  resizable?: boolean;
  /** @deprecated Kept for source compatibility. Resize space is now distributed across resizable columns. */
  fill?: boolean;
  sortable?: boolean;
  filterable?: boolean;
  filterType?: DataGridFilterType;
  columnType?: "default" | "from-list";
  list?: DataGridListConfig<T>;
  sticky?: "start" | "end";
  align?: "left" | "center" | "right";
  className?: string;
  headerClassName?: string;
  render?: (row: T, index: number) => ReactNode;
  value?: (row: T, index: number) => unknown;
  sortValue?: (row: T, index: number) => unknown;
  filterValue?: (row: T, index: number) => unknown;
};

type DataGridState = {
  sort?: {columnId: string;direction: DataGridSortDirection;} | null;
  filters?: Record<string, string>;
  /** Current in-memory pixel layout. This is deliberately not persisted. */
  widths?: Record<string, number>;
  /** Pixel widths explicitly selected by a user with a resize handle. */
  userWidths?: Record<string, number>;
  layoutSignature?: string;
  /** Legacy layout state; removed when persisted state is sanitized. */
  fillColumnId?: string | null;
};

type DataGridBaseProps<T> = {
  id: string;
  rows: T[];
  columns: DataGridColumn<T>[];
  getRowKey: (row: T, index: number) => string;
  emptyText?: ReactNode;
  filteredEmptyText?: ReactNode;
  emptyAction?: ReactNode;
  emptyActionColumnId?: string;
  /** Determines how the configured tracks are fitted when the grid is first laid out. */
  initialFit?: DataGridInitialFit;
  /** @deprecated Use initialFit instead. */
  fit?: DataGridInitialFit;
  /**
   * Cover prevents underflow while allowing horizontal overflow. Shrinking
   * consumes existing overflow and then grows eligible columns to the right;
   * it stops when no such capacity remains. Free permits both underflow and
   * overflow. Free is automatically promoted to cover when a right-sticky
   * column exists.
   */
  resizeBehavior?: DataGridResizeBehavior;
  className?: string;
  rowClassName?: (row: T, index: number) => string | undefined;
  storageKey?: string;
  initialFilters?: Record<string, string | string[]>;
  initialSort?: {columnId: string;direction: DataGridSortDirection;};
  /**
   * Synchronize a query selected outside the grid, for example by a summary
   * count shortcut. Server grids should feed `onQueryChange` back into this
   * value so the header controls and backend query remain in agreement.
   */
  query?: DataGridQueryState;
};

/**
 * Query ownership is deliberately explicit: client grids receive all rows and
 * DataGrid queries them locally, while optional `onQueryChange` observers can
 * explain the current match. Server grids receive one page and must handle the
 * complete query through `onQueryChange`.
 */
export type DataGridProps<T> = DataGridBaseProps<T> & (
  | {
    pagination?: DataGridClientPagination;
    /** Observe client-side filters and sorting without taking ownership of them. */
    onQueryChange?: (query: DataGridQueryState) => void;
  }
  | {pagination: DataGridServerPagination;onQueryChange: (query: DataGridQueryState) => void;}
);

export type DataGridRowActionsProps = {
  disabled?: boolean;
  removeDisabled?: boolean;
  /** Set to false only when row ordering is not part of this table's action set. */
  reorderable?: boolean;
  onAddBelow: () => void;
  onRemove: () => void;
  onMoveUp?: () => void;
  onMoveDown?: () => void;
  addLabel?: string;
  removeLabel?: string;
  moveUpLabel?: string;
  moveDownLabel?: string;
};

type FilterPosition = {
  top: number;
  left: number;
  width: number;
};

type ColumnResizeState = {
  columnId: string;
  startX: number;
  baseWidths: Record<string, number>;
  uncompensatedShrinkRoom: number;
  behavior: DataGridResizeBehavior;
};

const STORAGE_PREFIX = "govoplan.datagrid.";
const FILTER_POPOVER_WIDTH = 320;
const FILTER_POPOVER_MARGIN = 12;

export default function DataGrid<T>({
  id,
  rows,
  columns,
  getRowKey,
  emptyText = "i18n:govoplan-core.no_rows_found.0c1a8d34",
  filteredEmptyText = "i18n:govoplan-core.filter_yields_an_empty_result.8bfe7c90",
  emptyAction,
  emptyActionColumnId = "actions",
  initialFit,
  fit,
  resizeBehavior = "cover",
  className = "",
  rowClassName,
  storageKey,
  initialFilters = {},
  initialSort,
  query,
  pagination,
  onQueryChange
}: DataGridProps<T>) {
  const { translateText } = usePlatformLanguage();
  const resolvedInitialFit = initialFit ?? fit ?? "container";
  const hasRightStickyColumn = columns.some((column) => column.sticky === "end");
  const effectiveResizeBehavior: DataGridResizeBehavior = resizeBehavior === "free" && hasRightStickyColumn ?
  "cover" :
  resizeBehavior;
  const layoutSignature = dataGridLayoutSignature(columns, resolvedInitialFit, effectiveResizeBehavior);

  const localStorageKey = storageKey ?? `${STORAGE_PREFIX}${id}`;
  const initialFiltersKey = JSON.stringify(initialFilters);
  const normalizedInitialFilters = useMemo(() => normalizeInitialFilters(initialFilters), [initialFiltersKey]);
  const initialSortKey = initialSort ? `${initialSort.columnId}:${initialSort.direction}` : "";
  const normalizedInitialSort = useMemo(() => initialSort ? { ...initialSort } : undefined, [initialSortKey]);
  const externalQueryKey = query ? JSON.stringify(query) : "";
  const normalizedExternalQuery = useMemo(
    () => query ? { sort: query.sort ? { ...query.sort } : null, filters: { ...query.filters } } : undefined,
    [externalQueryKey]
  );
  const [state, setState] = useState<DataGridState>(() => mergeExternalQuery(
    mergeInitialSort(mergeInitialFilters(loadState(localStorageKey, layoutSignature), normalizedInitialFilters), normalizedInitialSort),
    normalizedExternalQuery
  ));
  const [resizeState, setResizeState] = useState<ColumnResizeState | null>(null);
  const [openFilterColumnId, setOpenFilterColumnId] = useState<string | null>(null);
  const [filterPosition, setFilterPosition] = useState<FilterPosition | null>(null);
  const scrollRegionRef = useRef<HTMLDivElement | null>(null);
  const headerCellRefs = useRef<Record<string, HTMLDivElement | null>>({});
  const filterButtonRefs = useRef<Record<string, HTMLButtonElement | null>>({});
  const filterPopoverRef = useRef<HTMLDivElement | null>(null);
  const serverQueryMode = pagination?.mode === "server";
  const onQueryChangeRef = useRef(onQueryChange);
  const serverPaginationRef = useRef<DataGridServerPagination | null>(serverQueryMode ? pagination : null);
  const lastQueryRef = useRef<DataGridQueryState | null>(null);
  const [measuredWidths, setMeasuredWidths] = useState<Record<string, number>>({});

  useEffect(() => {onQueryChangeRef.current = onQueryChange;}, [onQueryChange]);

  useEffect(() => {
    serverPaginationRef.current = serverQueryMode ? pagination as DataGridServerPagination : null;
  }, [pagination, serverQueryMode]);

  useEffect(() => {
    setState((current) => sanitizePersistedColumnState(
      columns,
      current,
      effectiveResizeBehavior,
      layoutSignature
    ));
  }, [columns, effectiveResizeBehavior, layoutSignature]);

  useEffect(() => {
    setState((current) => mergeInitialFilters(current, normalizedInitialFilters));
  }, [normalizedInitialFilters]);

  useEffect(() => {
    setState((current) => mergeInitialSort(current, normalizedInitialSort));
  }, [normalizedInitialSort]);

  useEffect(() => {
    setState((current) => mergeExternalQuery(current, normalizedExternalQuery));
  }, [normalizedExternalQuery]);

  useEffect(() => {
    try {
      window.localStorage.setItem(localStorageKey, JSON.stringify(persistedState(state, layoutSignature)));
    } catch {
      // Local storage is an enhancement only.
    }
  }, [localStorageKey, state, layoutSignature]);

  useEffect(() => {
    const params = new URLSearchParams(window.location.search);
    const filtersFromUrl: Record<string, string> = {};
    for (const column of columns) {
      const exact = params.get(`grid.${id}.${column.id}`) ?? params.get(`filter.${id}.${column.id}`);
      if (exact) filtersFromUrl[column.id] = exact;
    }
    const table = params.get("table");
    const filter = params.get("filter");
    if (table === id && filter?.includes(":")) {
      const [columnId, ...parts] = filter.split(":");
      const value = parts.join(":");
      if (columnId && value) filtersFromUrl[columnId] = value;
    }
    if (Object.keys(filtersFromUrl).length > 0) {
      setState((current) => ({ ...current, filters: { ...(current.filters ?? {}), ...filtersFromUrl } }));
    }
  }, [id, columns]);

  useLayoutEffect(() => {
    function measure() {
      const next: Record<string, number> = {};
      for (const column of columns) {
        const element = headerCellRefs.current[column.id];
        if (!element) continue;
        const width = Math.round(element.getBoundingClientRect().width);
        if (width > 0) next[column.id] = width;
      }
      setMeasuredWidths((current) => shallowEqualNumberRecords(current, next) ? current : next);
    }

    measure();
    if (typeof ResizeObserver === "undefined") {
      window.addEventListener("resize", measure);
      return () => window.removeEventListener("resize", measure);
    }

    const observer = new ResizeObserver(measure);
    for (const column of columns) {
      const element = headerCellRefs.current[column.id];
      if (element) observer.observe(element);
    }
    return () => observer.disconnect();
  }, [columns, state.widths]);

  useLayoutEffect(() => {
    const element = scrollRegionRef.current;
    if (!element || resizeState) return undefined;
    const scrollElement = element;
    let animationFrame = 0;

    function reconcileContainerWidth() {
      window.cancelAnimationFrame(animationFrame);
      animationFrame = window.requestAnimationFrame(() => {
        const nextContainerWidth = Math.round(scrollElement.clientWidth);
        if (nextContainerWidth <= 0) return;

        setState((current) => {
          const signatureMatches = current.layoutSignature === layoutSignature;
          const userWidths = signatureMatches ? current.userWidths ?? {} : {};
          const mustFit = effectiveResizeBehavior !== "free" || resolvedInitialFit === "container";
          if (!mustFit) {
            if (signatureMatches && current.widths === undefined) return current;
            return {
              ...current,
              widths: undefined,
              userWidths,
              layoutSignature,
              fillColumnId: undefined
            };
          }

          const layout = fitDataGridColumns(
            columns,
            nextContainerWidth,
            measuredWidths,
            userWidths,
            effectiveResizeBehavior
          );
          if (
            signatureMatches
            && shallowEqualNumberRecords(current.widths ?? {}, layout.widths)
          ) return current;
          return {
            ...current,
            widths: layout.widths,
            userWidths,
            layoutSignature,
            fillColumnId: undefined
          };
        });
      });
    }

    reconcileContainerWidth();
    if (typeof ResizeObserver === "undefined") {
      window.addEventListener("resize", reconcileContainerWidth);
      return () => {
        window.cancelAnimationFrame(animationFrame);
        window.removeEventListener("resize", reconcileContainerWidth);
      };
    }

    const observer = new ResizeObserver(reconcileContainerWidth);
    observer.observe(scrollElement);
    return () => {
      window.cancelAnimationFrame(animationFrame);
      observer.disconnect();
    };
  }, [columns, layoutSignature, effectiveResizeBehavior, resolvedInitialFit, measuredWidths, resizeState]);

  useEffect(() => {
    if (!resizeState) return;
    const activeResize = resizeState;

    function onMove(event: MouseEvent) {
      const rawDelta = event.clientX - activeResize.startX;
      const resized = resizeDataGridColumn(
        columns,
        activeResize.baseWidths,
        activeResize.columnId,
        rawDelta,
        activeResize.behavior,
        activeResize.uncompensatedShrinkRoom
      );
      setState((current) => ({
        ...current,
        widths: resized.widths,
        userWidths: activeResize.behavior === "free"
          ? {
            ...(current.layoutSignature === layoutSignature ? current.userWidths ?? {} : {}),
            [activeResize.columnId]: resized.widths[activeResize.columnId]
          }
          : resized.widths,
        layoutSignature,
        fillColumnId: undefined
      }));
    }

    function onUp() {
      setState((current) => sanitizePersistedColumnState(
        columns,
        current,
        effectiveResizeBehavior,
        layoutSignature
      ));
      setResizeState(null);
    }

    window.addEventListener("mousemove", onMove);
    window.addEventListener("mouseup", onUp);
    return () => {
      window.removeEventListener("mousemove", onMove);
      window.removeEventListener("mouseup", onUp);
    };
  }, [resizeState, columns, effectiveResizeBehavior, layoutSignature]);

  useEffect(() => {
    if (!openFilterColumnId) return undefined;
    const activeFilterColumnId = openFilterColumnId;
    function update() {
      updateFilterPosition(activeFilterColumnId);
    }
    update();
    window.addEventListener("resize", update);
    window.addEventListener("scroll", update, true);
    return () => {
      window.removeEventListener("resize", update);
      window.removeEventListener("scroll", update, true);
    };
  }, [openFilterColumnId]);

  useEffect(() => {
    if (!openFilterColumnId) return undefined;
    const activeFilterColumnId = openFilterColumnId;
    function onPointerDown(event: MouseEvent) {
      const target = event.target as Node | null;
      const popover = filterPopoverRef.current;
      const trigger = filterButtonRefs.current[activeFilterColumnId];
      if (target && (popover?.contains(target) || trigger?.contains(target))) return;
      setOpenFilterColumnId(null);
    }
    function onKeyDown(event: KeyboardEvent) {
      if (event.key === "Escape") setOpenFilterColumnId(null);
    }
    window.addEventListener("mousedown", onPointerDown);
    window.addEventListener("keydown", onKeyDown);
    return () => {
      window.removeEventListener("mousedown", onPointerDown);
      window.removeEventListener("keydown", onKeyDown);
    };
  }, [openFilterColumnId]);

  useEffect(() => {
    const nextQuery = {
      sort: state.sort ?? null,
      filters: { ...(state.filters ?? {}) }
    };
    const previousQuery = lastQueryRef.current;
    lastQueryRef.current = nextQuery;
    const serverPagination = serverPaginationRef.current;
    if (
      serverQueryMode &&
      previousQuery &&
      !dataGridQueriesEqual(previousQuery, nextQuery) &&
      serverPagination &&
      serverPagination.page !== 1
    ) {
      serverPagination.onPageChange(1);
    }
    onQueryChangeRef.current?.(nextQuery);
  }, [serverQueryMode, state.sort, state.filters]);

  const filterTypes = useMemo(() => {
    const result: Record<string, DataGridFilterType> = {};
    for (const column of columns) {
      result[column.id] = column.columnType === "from-list" ? "list" : column.filterType ?? inferFilterType(column, rows);
    }
    return result;
  }, [columns, rows]);

  const visibleRows = useMemo(() => {
    if (serverQueryMode) return rows;
    const filters = state.filters ?? {};
    const filtered = rows.filter((row, rowIndex) => columns.every((column) => {
      const filterValue = filters[column.id] ?? "";
      if (!filterValue.trim()) return true;
      const rawValue = valueForFilter(column, row, rowIndex);
      return matchesFilter(rawValue, filterValue, filterTypes[column.id]);
    }));
    if (!state.sort) return filtered;
    const sortColumn = columns.find((column) => column.id === state.sort?.columnId);
    if (!sortColumn) return filtered;
    return [...filtered].sort((a, b) => {
      const aIndex = rows.indexOf(a);
      const bIndex = rows.indexOf(b);
      const aValue = sortColumn.sortValue?.(a, aIndex) ?? sortColumn.value?.(a, aIndex) ?? "";
      const bValue = sortColumn.sortValue?.(b, bIndex) ?? sortColumn.value?.(b, bIndex) ?? "";
      const result = compareValues(aValue, bValue);
      return state.sort?.direction === "desc" ? -result : result;
    });
  }, [rows, columns, state.filters, state.sort, filterTypes, serverQueryMode]);

  const paginationMode = pagination?.mode ?? "client";
  const paginationTotal = pagination ?
  paginationMode === "server" ? Math.max(0, pagination.totalRows ?? rows.length) : visibleRows.length :
  visibleRows.length;
  const paginationPageSize = Math.max(1, pagination?.pageSize ?? Math.max(1, visibleRows.length));
  const paginationPageCount = Math.max(1, Math.ceil(paginationTotal / paginationPageSize));
  const paginationPage = pagination ? Math.min(paginationPageCount, Math.max(1, pagination.page)) : 1;
  const renderedRows = pagination && paginationMode === "client" ?
  visibleRows.slice((paginationPage - 1) * paginationPageSize, paginationPage * paginationPageSize) :
  visibleRows;

  useEffect(() => {
    if (pagination && pagination.page !== paginationPage) pagination.onPageChange(paginationPage);
  }, [pagination, paginationPage]);

  const actualTracks = columns.map((column) => widthForColumn(column, state.widths?.[column.id]));
  const templateColumns = actualTracks.join(" ");
  const hasFlexibleColumns = columns.some((column) => !state.widths?.[column.id] && isFlexibleColumn(column));
  const stickyOffsets = useMemo(() => computeStickyOffsets(columns, state.widths, measuredWidths), [columns, state.widths, measuredWidths]);
  const gridClassName = [
  "data-grid",
  `data-grid-fit-${resolvedInitialFit}`,
  `data-grid-initial-fit-${resolvedInitialFit}`,
  `data-grid-resize-${effectiveResizeBehavior}`,
  hasFlexibleColumns ? "data-grid-has-flex" : "data-grid-fixed-only"].
  join(" ");
  const activeFilterColumn = openFilterColumnId ? columns.find((column) => column.id === openFilterColumnId) : undefined;
  const hasActiveFilters = columns.some((column) => Boolean((state.filters?.[column.id] ?? "").trim()));
  const translatedEmptyText = translateNode(emptyText, translateText);
  const translatedFilteredEmptyText = translateNode(filteredEmptyText, translateText);

  function toggleSort(column: DataGridColumn<T>) {
    if (!column.sortable) return;
    setState((current) => {
      if (current.sort?.columnId !== column.id) return { ...current, sort: { columnId: column.id, direction: "asc" } };
      if (current.sort.direction === "asc") return { ...current, sort: { columnId: column.id, direction: "desc" } };
      return { ...current, sort: null };
    });
  }

  function patchFilter(columnId: string, value: string) {
    setState((current) => ({ ...current, filters: { ...(current.filters ?? {}), [columnId]: value } }));
  }

  function clearFilter(columnId: string, defaults: Record<string, string>) {
    setState((current) => {
      const nextFilters = { ...(current.filters ?? {}) };
      if (Object.prototype.hasOwnProperty.call(defaults, columnId)) nextFilters[columnId] = "";else
      delete nextFilters[columnId];
      return { ...current, filters: nextFilters };
    });
  }

  function updateFilterPosition(columnId: string) {
    const element = filterButtonRefs.current[columnId];
    if (!element) return;
    const rect = element.getBoundingClientRect();
    const width = Math.min(FILTER_POPOVER_WIDTH, Math.max(240, window.innerWidth - FILTER_POPOVER_MARGIN * 2));
    const left = Math.min(Math.max(FILTER_POPOVER_MARGIN, rect.left), window.innerWidth - width - FILTER_POPOVER_MARGIN);
    const top = Math.min(rect.bottom + 8, window.innerHeight - 120);
    setFilterPosition({ top, left, width });
  }

  function toggleFilterPopover(columnId: string) {
    setOpenFilterColumnId((current) => {
      if (current === columnId) return null;
      window.requestAnimationFrame(() => updateFilterPosition(columnId));
      return columnId;
    });
  }

  return (
    <div
      className={`data-grid-shell data-grid-${resolvedInitialFit} data-grid-shell-resize-${effectiveResizeBehavior} ${className}`.trim()}
      data-resize-behavior={effectiveResizeBehavior}
      data-requested-resize-behavior={resizeBehavior}>
      
      <div className="data-grid-scroll-region" ref={scrollRegionRef}>
        <div className={gridClassName} role="table" aria-label={id} style={{ gridTemplateColumns: templateColumns }}>
          {columns.map((column, columnIndex) => {
            const sorted = state.sort?.columnId === column.id ? state.sort.direction : undefined;
            const hasFilter = Boolean((state.filters?.[column.id] ?? "").trim());
            return (
              <div
                key={`header-${column.id}`}
                role="columnheader"
                ref={(element) => {headerCellRefs.current[column.id] = element;}}
                className={`data-grid-cell data-grid-header-cell ${column.headerClassName ?? ""} ${column.sortable ? "is-sortable" : ""} ${sorted ? "is-sorted" : ""} ${stickyClass(column)}`.trim()}
                style={stickyStyle(column, stickyOffsets[columnIndex])}>
                
                {column.sortable ?
                <button type="button" className="data-grid-header-button" onClick={() => toggleSort(column)}>
                    <span>{translateNode(column.header, translateText)}</span>
                    <SortIcon direction={sorted} />
                  </button> :

                <div className="data-grid-header-label"><span>{translateNode(column.header, translateText)}</span></div>
                }
                {column.filterable &&
                <button
                  type="button"
                  ref={(element) => {filterButtonRefs.current[column.id] = element;}}
                  className={`data-grid-filter-trigger${hasFilter ? " has-filter" : ""}${openFilterColumnId === column.id ? " is-open" : ""}`}
                  aria-label={i18nMessage("i18n:govoplan-core.value_value.dca59cc0", { value0: translateText("i18n:govoplan-core.filter.d7decf1a"), value1: translateHeaderLabel(column.header, translateText) })}
                  aria-expanded={openFilterColumnId === column.id}
                  onClick={(event) => {
                    event.stopPropagation();
                    toggleFilterPopover(column.id);
                  }}>
                  
                    <Filter size={14} aria-hidden="true" />
                  </button>
                }
                {column.resizable &&
                <button
                  type="button"
                  className="data-grid-resize-handle"
                  aria-label={i18nMessage("i18n:govoplan-core.value_value.dca59cc0", { value0: translateText("i18n:govoplan-core.resize.f52dc753"), value1: translateHeaderLabel(column.header, translateText) })}
                  onMouseDown={(event) => {
                    event.preventDefault();
                    event.stopPropagation();
                    const baseWidths = measuredColumnWidths(columns, headerCellRefs.current, state.widths, measuredWidths);
                    const activeColumnIndex = columns.findIndex((candidate) => candidate.id === column.id);
                    const isLastResizableColumn = !columns.
                    slice(activeColumnIndex + 1).
                    some(isResizeCompensationColumn);
                    const scrollElement = scrollRegionRef.current;
                    const totalHorizontalOverflow = scrollElement ?
                    Math.max(0, scrollElement.scrollWidth - scrollElement.clientWidth) :
                    0;
                    const shrinkRoomWithoutScroll = scrollElement && !isLastResizableColumn ?
                    Math.max(0, totalHorizontalOverflow - scrollElement.scrollLeft) :
                    totalHorizontalOverflow;

                    setState((current) => ({
                      ...current,
                      widths: roundWidthRecord(baseWidths),
                      fillColumnId: undefined
                    }));
                    setResizeState({
                      columnId: column.id,
                      startX: event.clientX,
                      baseWidths,
                      uncompensatedShrinkRoom: shrinkRoomWithoutScroll,
                      behavior: effectiveResizeBehavior
                    });
                  }}>
                  
                    <GripVertical size={14} aria-hidden="true" />
                  </button>
                }
              </div>
            );
          })}
          {renderedRows.length === 0 ? (() => {
            const filteredEmpty = rows.length > 0 && hasActiveFilters;
            const actionColumnIndex = columns.findIndex((column) => column.id === emptyActionColumnId);
            const actionColumn = actionColumnIndex >= 0 ? columns[actionColumnIndex] : undefined;
            if (!filteredEmpty && emptyAction && actionColumn && actionColumnIndex > 0) {
              return (
                <>
                  <div
                    className="data-grid-cell data-grid-body-cell data-grid-empty-message data-grid-row-even is-last-row"
                    role="cell"
                    style={{ gridColumn: `1 / ${actionColumnIndex + 1}` }}>
                    
                    {translatedEmptyText}
                  </div>
                  <div
                    className={`data-grid-cell data-grid-body-cell data-grid-empty-action-cell data-grid-row-even is-last-row ${stickyClass(actionColumn)}`.trim()}
                    role="cell"
                    style={{ ...stickyStyle(actionColumn, stickyOffsets[actionColumnIndex]), gridColumn: `${actionColumnIndex + 1} / ${actionColumnIndex + 2}` }}>
                    
                    {emptyAction}
                  </div>
                </>);

            }
            return (
              <div className="data-grid-empty" role="row">
                <div role="cell">{filteredEmpty ? translatedFilteredEmptyText : translatedEmptyText}</div>
              </div>);

          })() : renderedRows.map((row, visibleIndex) => {
            const originalIndex = rows.indexOf(row);
            const rowKey = getRowKey(row, originalIndex);
            const rowClass = rowClassName?.(row, originalIndex);
            const parityClass = visibleIndex % 2 === 0 ? "data-grid-row-even" : "data-grid-row-odd";
            const lastRowClass = visibleIndex === renderedRows.length - 1 ? "is-last-row" : "";
            const cells: ReactNode[] = [];
            columns.forEach((column, columnIndex) => {
              cells.push(
                <div
                  key={`${rowKey}-${column.id}`}
                  role="cell"
                  className={`data-grid-cell data-grid-body-cell ${parityClass} ${lastRowClass} ${column.align ? `align-${column.align}` : ""} ${column.className ?? ""} ${rowClass ?? ""} ${stickyClass(column)}`.trim()}
                  style={stickyStyle(column, stickyOffsets[columnIndex])}>
                  
                  {renderCell(column, row, originalIndex, translateText)}
                </div>
              );
            });
            return cells;
          })}
        </div>
      </div>
      {pagination &&
      <DataGridPaginationBar
        page={paginationPage}
        pageSize={paginationPageSize}
        totalRows={paginationTotal}
        pageCount={paginationPageCount}
        pageSizeOptions={pagination.pageSizeOptions ?? [10, 25, 50, 100]}
        disabled={pagination.disabled}
        onPageChange={pagination.onPageChange}
        onPageSizeChange={pagination.onPageSizeChange} />

      }
      {activeFilterColumn && filterPosition && createPortal(
        <FilterPopover
          ref={filterPopoverRef}
          column={activeFilterColumn}
          filterType={filterTypes[activeFilterColumn.id]}
          value={state.filters?.[activeFilterColumn.id] ?? ""}
          listOptions={listOptionsForColumn(activeFilterColumn, rows)}
          listDisplay={activeFilterColumn.list?.display ?? "text"}
          optionsEditable={Boolean(activeFilterColumn.list?.optionsEditable && activeFilterColumn.list?.onOptionsChange)}
          position={filterPosition}
          onChange={(value) => patchFilter(activeFilterColumn.id, value)}
          onClear={() => clearFilter(activeFilterColumn.id, normalizedInitialFilters)}
          onOptionsChange={activeFilterColumn.list?.onOptionsChange}
          onClose={() => setOpenFilterColumnId(null)} />,

        document.body
      )}
    </div>);

}

export type DataGridPaginationBarProps = {
  page: number;
  pageSize: number;
  totalRows: number;
  pageCount?: number;
  pageSizeOptions?: number[];
  disabled?: boolean;
  className?: string;
  ariaLabel?: string;
  onPageChange: (page: number) => void;
  onPageSizeChange?: (pageSize: number) => void;
};

export function DataGridPaginationBar({
  page,
  pageSize,
  totalRows,
  pageCount = Math.max(1, Math.ceil(totalRows / pageSize)),
  pageSizeOptions = [10, 25, 50, 100],
  disabled = false,
  className = "",
  ariaLabel = "i18n:govoplan-core.table_pagination.3665bd76",
  onPageChange,
  onPageSizeChange









}: DataGridPaginationBarProps) {
  const first = totalRows === 0 ? 0 : (page - 1) * pageSize + 1;
  const last = Math.min(totalRows, page * pageSize);
  const options = [...new Set([...pageSizeOptions, pageSize])].filter((value) => value > 0).sort((a, b) => a - b);
  const { translateText } = usePlatformLanguage();
  return (
    <div className={`data-grid-pagination ${className}`.trim()} aria-label={translateText(ariaLabel)}>
      <div className="data-grid-pagination-summary">{first}–{last} {translateText("i18n:govoplan-core.of")} {totalRows}</div>
      <label className="data-grid-page-size">
        <span>{translateText("i18n:govoplan-core.rows_per_page.af2f9c1b")}</span>
        <select
          value={pageSize}
          disabled={disabled || !onPageSizeChange}
          onChange={(event) => onPageSizeChange?.(Number(event.target.value))}>
          
          {options.map((value) => <option key={value} value={value}>{value}</option>)}
        </select>
      </label>
      <div className="data-grid-page-controls">
        <button type="button" aria-label={translateText("i18n:govoplan-core.first_page.49d74b49")} disabled={disabled || page <= 1} onClick={() => onPageChange(1)}><ChevronsLeft size={16} /></button>
        <button type="button" aria-label={translateText("i18n:govoplan-core.previous_page.81f54719")} disabled={disabled || page <= 1} onClick={() => onPageChange(page - 1)}><ChevronLeft size={16} /></button>
        <span>{translateText("i18n:govoplan-core.page.fb06270f")} {page} {translateText("i18n:govoplan-core.of")} {pageCount}</span>
        <button type="button" aria-label={translateText("i18n:govoplan-core.next_page.4bfc194b")} disabled={disabled || page >= pageCount} onClick={() => onPageChange(page + 1)}><ChevronRight size={16} /></button>
        <button type="button" aria-label={translateText("i18n:govoplan-core.last_page.b01f16ae")} disabled={disabled || page >= pageCount} onClick={() => onPageChange(pageCount)}><ChevronsRight size={16} /></button>
      </div>
    </div>);

}

export function DataGridRowActions({
  disabled = false,
  removeDisabled = false,
  reorderable = true,
  onAddBelow,
  onRemove,
  onMoveUp,
  onMoveDown,
  addLabel = "i18n:govoplan-core.add_row_below.1aef2881",
  removeLabel = "i18n:govoplan-core.remove_row.bd7f4b48",
  moveUpLabel = "i18n:govoplan-core.move_row_up.b4715a19",
  moveDownLabel = "i18n:govoplan-core.move_row_down.8e2b5957"
}: DataGridRowActionsProps) {
  return (
    <TableActionGroup
      className="data-grid-row-actions"
      actions={[
        {
          id: "add",
          label: addLabel,
          icon: <Plus size={16} aria-hidden="true" />,
          variant: "primary",
          disabled,
          onClick: onAddBelow
        },
        reorderable && {
          id: "move-up",
          label: moveUpLabel,
          icon: <ArrowUp size={16} aria-hidden="true" />,
          disabled: disabled || !onMoveUp,
          onClick: () => onMoveUp?.()
        },
        reorderable && {
          id: "move-down",
          label: moveDownLabel,
          icon: <ArrowDown size={16} aria-hidden="true" />,
          disabled: disabled || !onMoveDown,
          onClick: () => onMoveDown?.()
        },
        {
          id: "remove",
          label: removeLabel,
          icon: <Trash2 size={16} aria-hidden="true" />,
          variant: "danger",
          disabled: disabled || removeDisabled,
          onClick: onRemove
        }
      ]}
    />);

}

export function DataGridEmptyAction({
  disabled = false,
  reorderable = true,
  onAdd,
  label = "i18n:govoplan-core.add_first_row.c82c15f2"




}: {disabled?: boolean;reorderable?: boolean;onAdd: () => void;label?: string;}) {
  return (
    <TableActionGroup
      className="data-grid-row-actions data-grid-empty-row-actions"
      minimumSlots={reorderable ? 4 : 2}
      actions={[{
        id: "add",
        label,
        icon: <Plus size={16} aria-hidden="true" />,
        variant: "primary",
        disabled,
        onClick: onAdd
      }]}
    />);

}

type FilterPopoverProps = {
  column: Pick<DataGridColumn<unknown>, "id" | "header">;
  filterType: DataGridFilterType;
  value: string;
  listOptions: DataGridListOption[];
  listDisplay: "text" | "pill";
  optionsEditable: boolean;
  position: FilterPosition;
  onChange: (value: string) => void;
  onClear: () => void;
  onOptionsChange?: (options: DataGridListOption[]) => void;
  onClose: () => void;
};

const FilterPopover = forwardRef<HTMLDivElement, FilterPopoverProps>(function FilterPopover(
{ column, filterType, value, listOptions, listDisplay, optionsEditable, position, onChange, onClear, onOptionsChange, onClose },
ref)
{
  const { translateText } = usePlatformLanguage();
  const parsed = parseTypedFilter(value, filterType);
  const operatorOptions = filterType === "date" ? DATE_OPERATORS : NUMBER_OPERATORS;
  const headerLabel = translateHeaderLabel(column.header, translateText);
  return (
    <div
      ref={ref}
      className="data-grid-filter-popover"
      style={{ top: position.top, left: position.left, width: position.width }}
      role="dialog"
      aria-label={i18nMessage("i18n:govoplan-core.value_value.dca59cc0", { value0: translateText("i18n:govoplan-core.filter.d7decf1a"), value1: headerLabel })}>
      
      <div className="data-grid-filter-popover-header">
        <strong>{translateText("i18n:govoplan-core.filter.d7decf1a")} {translateNode(column.header, translateText)}</strong>
        <button type="button" aria-label={translateText("i18n:govoplan-core.close_filter.3a281c3f")} onClick={onClose}><X size={15} /></button>
      </div>
      {filterType === "list" ?
      <ListFilterEditor
        options={listOptions}
        display={listDisplay}
        value={value}
        editable={optionsEditable}
        onChange={onChange}
        onOptionsChange={onOptionsChange} /> :

      filterType === "boolean" ?
      <ListFilterEditor
        options={BOOLEAN_FILTER_OPTIONS}
        display="text"
        value={value}
        editable={false}
        onChange={onChange} /> :

      filterType === "number" || filterType === "integer" || filterType === "date" ?
      <div className="data-grid-filter-stack">
          <label className="data-grid-filter-field">
            <span>{translateText("i18n:govoplan-core.match.0335207f")}</span>
            <select
            value={parsed.operator}
            onChange={(event) => onChange(formatTypedFilter(event.target.value as TypedFilterOperator, parsed.value))}>
            
              {operatorOptions.map((option) => <option key={option.value} value={option.value}>{translateText(option.label)}</option>)}
            </select>
          </label>
          <label className="data-grid-filter-field">
            <span>{translateText("i18n:govoplan-core.value.8dce170d")}</span>
            <div className="data-grid-filter-input-wrap">
              <input
              type={filterType === "date" ? "date" : "number"}
              step={filterType === "integer" ? 1 : undefined}
              value={parsed.value}
              onChange={(event) => onChange(formatTypedFilter(parsed.operator, event.target.value))}
              autoFocus />
            
              {value && <button type="button" aria-label={translateText("i18n:govoplan-core.clear_filter.b667d6f9")} onClick={onClear}><X size={14} /></button>}
            </div>
          </label>
        </div> :

      <label className="data-grid-filter-field">
          <span>{translateText("i18n:govoplan-core.contains.33e15d00")}</span>
          <div className="data-grid-filter-input-wrap">
            <input value={value} placeholder={translateText("i18n:govoplan-core.filter.d7decf1a")} onChange={(event) => onChange(event.target.value)} autoFocus />
            {value && <button type="button" aria-label={translateText("i18n:govoplan-core.clear_filter.b667d6f9")} onClick={onClear}><X size={14} /></button>}
          </div>
        </label>
      }
    </div>);

});

function ListFilterEditor({
  options,
  display,
  value,
  editable,
  onChange,
  onOptionsChange







}: {options: DataGridListOption[];display: "text" | "pill";value: string;editable: boolean;onChange: (value: string) => void;onOptionsChange?: (options: DataGridListOption[]) => void;}) {
  const { translateText } = usePlatformLanguage();
  const [newValue, setNewValue] = useState("");
  const selected = parseListFilter(value, options.map((option) => option.value));
  const selectedSet = new Set(selected);

  function toggleOption(optionValue: string) {
    const next = new Set(selectedSet);
    if (next.has(optionValue)) next.delete(optionValue);else
    next.add(optionValue);
    onChange(formatListSelection([...next], options));
  }

  function addOption() {
    const normalized = newValue.trim();
    if (!normalized || !onOptionsChange || options.some((option) => option.value === normalized)) return;
    onOptionsChange([...options, { value: normalized, label: humanizeListValue(normalized) }]);
    setNewValue("");
  }

  function removeOption(optionValue: string) {
    if (!onOptionsChange) return;
    onOptionsChange(options.filter((option) => option.value !== optionValue));
    if (selectedSet.has(optionValue)) {
      const nextOptions = options.filter((option) => option.value !== optionValue);
      onChange(formatListSelection(selected.filter((candidate) => candidate !== optionValue), nextOptions));
    }
  }

  return (
    <div className="data-grid-list-filter">
      <div className="data-grid-list-filter-actions">
        <button type="button" onClick={() => onChange("")}>{translateText("i18n:govoplan-core.select_all.913afff1")}</button>
        <button type="button" onClick={() => onChange(formatListFilter([]))}>{translateText("i18n:govoplan-core.deselect_all.85cce1e1")}</button>
      </div>
      <div className="data-grid-list-filter-options" role="group" aria-label={translateText("i18n:govoplan-core.allowed_values.495fcf3a")}>
        {options.length === 0 ?
        <p className="muted small-note">{translateText("i18n:govoplan-core.no_values_are_configured_for_this_column.16e935e8")}</p> :
        options.map((option) =>
        <div className="data-grid-list-filter-row" key={option.value}>
            <label>
              <input
              type="checkbox"
              checked={selectedSet.has(option.value)}
              disabled={option.disabled}
              onChange={() => toggleOption(option.value)} />
            
              {display === "pill" ? <StatusBadge status={option.value} label={option.label} /> : <span>{translateText(option.label)}</span>}
            </label>
            {editable &&
          <button type="button" className="data-grid-list-option-remove" aria-label={i18nMessage("i18n:govoplan-core.value_value.dca59cc0", { value0: translateText("i18n:govoplan-core.remove.e963907d"), value1: translateText(option.label) })} onClick={() => removeOption(option.value)}>
                <Trash2 size={14} aria-hidden="true" />
              </button>
          }
          </div>
        )}
      </div>
      {editable &&
      <div className="data-grid-list-option-add">
          <input
          value={newValue}
          placeholder={translateText("i18n:govoplan-core.add_value.187d35c5")}
          onChange={(event) => setNewValue(event.target.value)}
          onKeyDown={(event) => {
            if (event.key === "Enter") {
              event.preventDefault();
              addOption();
            }
          }} />
        
          <button type="button" aria-label={translateText("i18n:govoplan-core.add_list_value.5e49e01b")} onClick={addOption} disabled={!newValue.trim()}>
            <Plus size={15} aria-hidden="true" />
          </button>
        </div>
      }
    </div>);

}

function SortIcon({ direction }: {direction?: DataGridSortDirection;}) {
  if (direction === "asc") return <ArrowUp size={14} aria-label="i18n:govoplan-core.sorted_ascending.bb4892e9" />;
  if (direction === "desc") return <ArrowDown size={14} aria-label="i18n:govoplan-core.sorted_descending.30a95b88" />;
  return <ChevronsUpDown size={14} aria-hidden="true" />;
}

function renderCell<T>(column: DataGridColumn<T>, row: T, index: number, translateText: (value: string) => string): ReactNode {
  if (column.render) return column.render(row, index);
  if (column.columnType === "from-list" && column.list) {
    const rawValue = stringifyCell(column.value?.(row, index));
    const option = column.list.options.find((candidate) => candidate.value === rawValue);
    const label = option?.label ?? humanizeListValue(rawValue);
    if (column.list.editable && column.list.onChange) {
      const cellOptions = option || !rawValue ?
      column.list.options :
      [...column.list.options, { value: rawValue, label }];
      return (
        <select
          className="data-grid-list-select"
          value={rawValue}
          onChange={(event) => column.list?.onChange?.(row, index, event.target.value)}
          aria-label={i18nMessage("i18n:govoplan-core.select_value.a9ef046e", { value0: String(column.header) })}>
          
          {cellOptions.map((candidate) =>
          <option key={candidate.value} value={candidate.value} disabled={candidate.disabled}>{translateText(candidate.label)}</option>
          )}
        </select>);

    }
    return column.list.display === "pill" ? <StatusBadge status={rawValue} label={label} /> : translateText(label);
  }
  return stringifyCell(column.value?.(row, index));
}

function translateNode(node: ReactNode, translateText: (value: string) => string): ReactNode {
  return typeof node === "string" ? translateText(node) : node;
}

function translateHeaderLabel(node: ReactNode, translateText: (value: string) => string): string {
  if (typeof node === "string") return translateText(node);
  if (typeof node === "number") return String(node);
  return String(node);
}

function loadState(key: string, layoutSignature: string): DataGridState {
  try {
    const value = window.localStorage.getItem(key);
    if (!value) return { layoutSignature };
    const parsed = JSON.parse(value) as DataGridState;
    const userWidths = dataGridWidthsForLayout(parsed.layoutSignature, layoutSignature, parsed.widths);
    return {
      sort: parsed.sort,
      filters: parsed.filters,
      widths: userWidths,
      userWidths,
      layoutSignature
    };
  } catch {
    return { layoutSignature };
  }
}

function persistedState(state: DataGridState, layoutSignature: string): DataGridState {
  return {
    sort: state.sort,
    filters: state.filters,
    widths: state.userWidths,
    layoutSignature
  };
}

function normalizeInitialFilters(filters: Record<string, string | string[]>): Record<string, string> {
  return Object.fromEntries(
    Object.entries(filters).map(([columnId, value]) => [
    columnId,
    Array.isArray(value) ? formatListFilter(value) : value]
    )
  );
}

function mergeInitialFilters(state: DataGridState, initialFilters: Record<string, string>): DataGridState {
  const currentFilters = state.filters ?? {};
  let changed = false;
  const nextFilters = { ...currentFilters };
  for (const [columnId, value] of Object.entries(initialFilters)) {
    if (!Object.prototype.hasOwnProperty.call(currentFilters, columnId)) {
      nextFilters[columnId] = value;
      changed = true;
    }
  }
  if (!changed) return state;
  return { ...state, filters: nextFilters };
}

function mergeInitialSort(
state: DataGridState,
initialSort?: {columnId: string;direction: DataGridSortDirection;})
: DataGridState {
  if (!initialSort || state.sort !== undefined) return state;
  return { ...state, sort: initialSort };
}

function mergeExternalQuery(state: DataGridState, query?: DataGridQueryState): DataGridState {
  if (!query) return state;
  const current = {
    sort: state.sort ?? null,
    filters: state.filters ?? {}
  };
  if (dataGridQueriesEqual(current, query)) return state;
  return {
    ...state,
    sort: query.sort ? { ...query.sort } : null,
    filters: { ...query.filters }
  };
}

function formatListFilter(values: string[]): string {
  return `list:${JSON.stringify([...new Set(values)])}`;
}

function formatListSelection(values: string[], options: DataGridListOption[]): string {
  const optionValues = [...new Set(options.map((option) => option.value))];
  const selected = [...new Set(values)].filter((value) => optionValues.includes(value));
  if (optionValues.length > 0 && optionValues.every((value) => selected.includes(value))) return "";
  return formatListFilter(selected);
}

function parseListFilter(value: string, allValues: string[]): string[] {
  if (!value.trim()) return [...allValues];
  if (value.startsWith("list:")) {
    try {
      const parsed = JSON.parse(value.slice(5));
      return Array.isArray(parsed) ? parsed.filter((item): item is string => typeof item === "string") : [];
    } catch {
      return [];
    }
  }
  return value.split(",").map((item) => item.trim()).filter(Boolean);
}

function listOptionsForColumn<T>(column: DataGridColumn<T>, rows: T[]): DataGridListOption[] {
  if (column.columnType !== "from-list" && column.filterType !== "list") return [];
  const configured = column.list?.options ?? [];
  const known = new Set(configured.map((option) => option.value));
  const observed = rows.
  map((row, index) => stringifyCell(valueForFilter(column, row, index)).trim()).
  filter((value) => value && !known.has(value)).
  map((value) => ({ value, label: humanizeListValue(value) }));
  return [...configured, ...observed.filter((option, index, all) => all.findIndex((candidate) => candidate.value === option.value) === index)];
}

function humanizeListValue(value: string): string {
  if (!value) return "—";
  return value.replace(/_/g, " ").replace(/-/g, " ").replace(/\b\w/g, (character) => character.toUpperCase());
}

function sanitizePersistedColumnState<T>(
columns: DataGridColumn<T>[],
state: DataGridState,
resizeBehavior: DataGridResizeBehavior,
layoutSignature: string)
: DataGridState {
  const columnMap = new Map(columns.map((column) => [column.id, column]));
  const signatureMatches = state.layoutSignature === layoutSignature;
  const sanitizeWidths = (widths?: Record<string, number>) => Object.fromEntries(
    Object.entries(widths ?? {}).flatMap(([columnId, width]) => {
      const column = columnMap.get(columnId);
      if (!column || !Number.isFinite(width)) return [];
      const minimum = effectiveColumnMinWidth(column);
      const maximum = resizeBehavior === "free"
        ? effectiveColumnMaxWidth(column, minimum)
        : DATA_GRID_MAX_TRACK_WIDTH;
      return [[columnId, Math.min(maximum, Math.max(minimum, width))]];
    })
  );
  const nextWidths = signatureMatches ? sanitizeWidths(state.widths) : {};
  const nextUserWidths = signatureMatches ? sanitizeWidths(state.userWidths) : {};
  const normalizedWidths = Object.keys(nextWidths).length > 0 ? roundWidthRecord(nextWidths) : undefined;
  const normalizedUserWidths = Object.keys(nextUserWidths).length > 0 ? roundWidthRecord(nextUserWidths) : undefined;
  const widthsChanged = !shallowEqualNumberRecords(state.widths ?? {}, normalizedWidths ?? {});
  const userWidthsChanged = !shallowEqualNumberRecords(state.userWidths ?? {}, normalizedUserWidths ?? {});
  const fillChanged = state.fillColumnId !== undefined;
  const signatureChanged = state.layoutSignature !== layoutSignature;
  if (!widthsChanged && !userWidthsChanged && !fillChanged && !signatureChanged) return state;
  return {
    ...state,
    widths: normalizedWidths,
    userWidths: normalizedUserWidths,
    layoutSignature,
    fillColumnId: undefined
  };
}

function widthForColumn<T>(column: DataGridColumn<T>, savedWidth?: number): string {
  const minimum = effectiveColumnMinWidth(column);
  const maximum = effectiveColumnMaxWidth(column, minimum);
  if (savedWidth !== undefined) return `${Math.min(DATA_GRID_MAX_TRACK_WIDTH, Math.max(minimum, savedWidth))}px`;
  if (typeof column.width === "number") return `${Math.min(maximum, Math.max(minimum, column.width))}px`;
  if (column.width) return columnTrackWithMinimum(column.width, minimum);
  return `minmax(${minimum}px, 1fr)`;
}

function isResizeCompensationColumn<T>(column: DataGridColumn<T>): boolean {
  return Boolean(column.resizable && !column.sticky);
}

function measuredColumnWidths<T>(
columns: DataGridColumn<T>[],
elements: Record<string, HTMLDivElement | null>,
savedWidths?: Record<string, number>,
measuredWidths?: Record<string, number>)
: Record<string, number> {
  const result: Record<string, number> = {};
  for (const column of columns) {
    const element = elements[column.id];
    const measured = element ? Math.round(element.getBoundingClientRect().width) : undefined;
    const savedWidth = savedWidths?.[column.id];
    result[column.id] = savedWidth !== undefined
      ? Math.min(DATA_GRID_MAX_TRACK_WIDTH, Math.max(effectiveColumnMinWidth(column), savedWidth))
      : columnPixelWidth(
        column,
        undefined,
        measured && measured > 0 ? measured : measuredWidths?.[column.id]
      );
  }
  return result;
}

function isFlexibleColumn<T>(column: DataGridColumn<T>, savedWidth?: number): boolean {
  if (savedWidth && !isFlexibleWidth(column.width, column.fill)) return false;
  return isFlexibleWidth(column.width, column.fill);
}

function computeStickyOffsets<T>(columns: DataGridColumn<T>[], savedWidths?: Record<string, number>, measuredWidths?: Record<string, number>): Record<number, number> {
  const offsets: Record<number, number> = {};
  let left = 0;
  columns.forEach((column, index) => {
    if (column.sticky === "start") {
      offsets[index] = left;
      left += renderedColumnPixelWidth(column, savedWidths?.[column.id], measuredWidths?.[column.id]);
    }
  });
  let right = 0;
  [...columns].reverse().forEach((column, reverseIndex) => {
    const index = columns.length - 1 - reverseIndex;
    if (column.sticky === "end") {
      offsets[index] = right;
      right += renderedColumnPixelWidth(column, savedWidths?.[column.id], measuredWidths?.[column.id]);
    }
  });
  return offsets;
}

function renderedColumnPixelWidth<T>(
  column: DataGridColumn<T>,
  savedWidth?: number,
  measuredWidth?: number
): number {
  return savedWidth !== undefined
    ? Math.min(DATA_GRID_MAX_TRACK_WIDTH, Math.max(effectiveColumnMinWidth(column), savedWidth))
    : columnPixelWidth(column, undefined, measuredWidth);
}

function stickyClass<T>(column: DataGridColumn<T>): string {
  if (column.sticky === "start") return "is-sticky-start";
  if (column.sticky === "end") return "is-sticky-end";
  return "";
}

function stickyStyle<T>(column: DataGridColumn<T>, offset?: number): CSSProperties | undefined {
  if (!column.sticky) return undefined;
  return column.sticky === "start" ? { left: offset ?? 0 } : { right: offset ?? 0 };
}

function valueForFilter<T>(column: DataGridColumn<T>, row: T, rowIndex: number): unknown {
  return column.filterValue?.(row, rowIndex) ?? column.value?.(row, rowIndex) ?? column.render?.(row, rowIndex) ?? "";
}

function matchesFilter(value: unknown, filterValue: string, filterType: DataGridFilterType): boolean {
  if (!filterValue.trim()) return true;
  if (filterType === "list") {
    const selected = parseListFilter(filterValue, []);
    return selected.includes(stringifyCell(value));
  }
  if (filterType === "boolean") {
    if (filterValue.startsWith("list:")) {
      const selected = parseListFilter(filterValue, BOOLEAN_FILTER_OPTIONS.map((option) => option.value));
      const actual = normalizeBoolean(value);
      if (actual === null) return false;
      return selected.includes(actual ? "true" : "false");
    }
    const expected = parseBooleanFilter(filterValue);
    if (expected === null) return stringifyCell(value).toLowerCase().includes(filterValue.toLowerCase());
    const actual = normalizeBoolean(value);
    return actual === null ? stringifyCell(value).toLowerCase().includes(filterValue.toLowerCase()) : actual === expected;
  }
  if (filterType === "number" || filterType === "integer") {
    const parsed = parseTypedFilter(filterValue, filterType);
    if (!parsed.value.trim()) return true;
    const actual = parseNumberValue(value);
    const expected = Number(parsed.value);
    if (!Number.isFinite(actual) || !Number.isFinite(expected)) return false;
    return compareByOperator(actual, expected, parsed.operator);
  }
  if (filterType === "date") {
    const parsed = parseTypedFilter(filterValue, filterType);
    if (!parsed.value.trim()) return true;
    const actual = parseDateValue(value);
    const expected = parseDateValue(parsed.value);
    if (!Number.isFinite(actual) || !Number.isFinite(expected)) return false;
    return compareByOperator(actual, expected, parsed.operator);
  }
  return stringifyCell(value).toLowerCase().includes(filterValue.trim().toLowerCase());
}

function inferFilterType<T>(column: DataGridColumn<T>, rows: T[]): DataGridFilterType {
  const samples = rows.slice(0, 25).
  map((row, index) => valueForFilter(column, row, index)).
  filter((value) => stringifyCell(value).trim() !== "");
  if (samples.length === 0) return "text";
  if (samples.every((value) => Number.isFinite(parseNumberValue(value)))) {
    return samples.every((value) => Number.isInteger(parseNumberValue(value))) ? "integer" : "number";
  }
  if (samples.every((value) => normalizeBoolean(value) !== null)) return "boolean";
  if (samples.every((value) => Number.isFinite(parseDateValue(value)))) return "date";
  return "text";
}

function parseBooleanFilter(value: string): boolean | null {
  const normalized = value.trim().toLowerCase();
  if (!normalized) return null;
  if (["true", "yes", "1", "active", "on"].includes(normalized)) return true;
  if (["false", "no", "0", "inactive", "off"].includes(normalized)) return false;
  return null;
}

function normalizeBoolean(value: unknown): boolean | null {
  if (typeof value === "boolean") return value;
  if (typeof value === "number") return value !== 0;
  const normalized = stringifyCell(value).trim().toLowerCase();
  if (!normalized) return null;
  if (["true", "yes", "1", "active", "required", "individual", "warn", "on", "enabled", "success", "ok", "can_override", "mock"].includes(normalized)) return true;
  if (["false", "no", "0", "inactive", "optional", "off", "global_only", "locked", "current", "disabled", "none"].includes(normalized)) return false;
  return null;
}

function parseNumberValue(value: unknown): number {
  if (typeof value === "number") return value;
  const text = stringifyCell(value).replace(/[^0-9.,+-]/g, "").replace(",", ".");
  if (!text.trim()) return NaN;
  return Number(text);
}

function parseDateValue(value: unknown): number {
  if (value instanceof Date) return value.getTime();
  const text = stringifyCell(value).trim();
  if (!text) return NaN;
  return Date.parse(text);
}

function compareByOperator(actual: number, expected: number, operator: TypedFilterOperator): boolean {
  if (operator === "gt" || operator === "after") return actual > expected;
  if (operator === "gte") return actual >= expected;
  if (operator === "lt" || operator === "before") return actual < expected;
  if (operator === "lte") return actual <= expected;
  return actual === expected;
}

function parseTypedFilter(value: string, filterType: DataGridFilterType): {operator: TypedFilterOperator;value: string;} {
  if (!value.includes(":")) return { operator: filterType === "text" ? "contains" : "eq", value };
  const [operator, ...parts] = value.split(":");
  const candidate = operator as TypedFilterOperator;
  if (!["contains", "eq", "gt", "gte", "lt", "lte", "before", "after"].includes(candidate)) {
    return { operator: filterType === "text" ? "contains" : "eq", value };
  }
  return { operator: candidate, value: parts.join(":") };
}

function formatTypedFilter(operator: TypedFilterOperator, value: string): string {
  return value ? `${operator}:${value}` : "";
}

const NUMBER_OPERATORS: {value: TypedFilterOperator;label: string;}[] = [
{ value: "eq", label: "i18n:govoplan-core.equals.09b6a6dc" },
{ value: "gt", label: "i18n:govoplan-core.greater_than.599294f2" },
{ value: "gte", label: "i18n:govoplan-core.greater_or_equal.670adf38" },
{ value: "lt", label: "i18n:govoplan-core.less_than.1d3d412a" },
{ value: "lte", label: "i18n:govoplan-core.less_or_equal.2860e695" }];


const DATE_OPERATORS: {value: TypedFilterOperator;label: string;}[] = [
{ value: "eq", label: "i18n:govoplan-core.on.e0049a66" },
{ value: "before", label: "i18n:govoplan-core.before.74f39697" },
{ value: "after", label: "i18n:govoplan-core.after.79ba5e1b" }];


const BOOLEAN_FILTER_OPTIONS: DataGridListOption[] = [
{ value: "true", label: "i18n:govoplan-core.yes_active.b1ef7d60" },
{ value: "false", label: "i18n:govoplan-core.no_inactive.75ddffb2" }];


function compareValues(a: unknown, b: unknown): number {
  if (typeof a === "number" && typeof b === "number") return a - b;
  const aDate = typeof a === "string" ? Date.parse(a) : NaN;
  const bDate = typeof b === "string" ? Date.parse(b) : NaN;
  if (!Number.isNaN(aDate) && !Number.isNaN(bDate)) return aDate - bDate;
  return stringifyCell(a).localeCompare(stringifyCell(b), undefined, { numeric: true, sensitivity: "base" });
}

function dataGridQueriesEqual(left: DataGridQueryState, right: DataGridQueryState): boolean {
  if ((left.sort?.columnId ?? "") !== (right.sort?.columnId ?? "")) return false;
  if ((left.sort?.direction ?? "") !== (right.sort?.direction ?? "")) return false;
  const keys = new Set([...Object.keys(left.filters), ...Object.keys(right.filters)]);
  for (const key of keys) {
    if ((left.filters[key] ?? "") !== (right.filters[key] ?? "")) return false;
  }
  return true;
}

function stringifyCell(value: unknown): string {
  if (value === null || value === undefined) return "";
  if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return String(value);
  if (Array.isArray(value)) return value.map(stringifyCell).join(", ");
  return "";
}

function shallowEqualNumberRecords(a: Record<string, number>, b: Record<string, number>): boolean {
  const aKeys = Object.keys(a);
  const bKeys = Object.keys(b);
  if (aKeys.length !== bKeys.length) return false;
  return aKeys.every((key) => a[key] === b[key]);
}
