import { warnOnce } from './app-dom.js';
import { confirmDialog } from './app-confirm-dialog.js';
import { gridFiltersFromSchema, withCurrentListReturn } from '../pages/app-list-utils.js';
import { initListFilterExperience } from '../pages/app-list-filter-experience.js';
import { buildChipsFromMeta, removeChipFromMetaState, clearMetaState } from '../pages/app-list-filter-state.js';

const DEFAULT_PAGE_SIZE_OPTIONS = [10, 25, 50, 100];
const PAGE_SIZE_STORAGE_PREFIX = 'grid:page-size';

const parsePositiveInt = (value) => {
  const parsed = Number.parseInt(String(value ?? ''), 10);
  if (!Number.isFinite(parsed) || parsed <= 0) {
    return null;
  }
  return parsed;
};

const normalizePageSizeOptions = (options) => {
  const source = Array.isArray(options) && options.length
    ? options
    : DEFAULT_PAGE_SIZE_OPTIONS;
  const normalized = [];
  source.forEach((option) => {
    const parsed = parsePositiveInt(option);
    if (parsed !== null && !normalized.includes(parsed)) {
      normalized.push(parsed);
    }
  });
  return normalized.length ? normalized : [...DEFAULT_PAGE_SIZE_OPTIONS];
};

const safeStorageGet = (key) => {
  const storageKey = String(key || '').trim();
  if (storageKey === '') {
    return null;
  }
  try {
    return window.localStorage.getItem(storageKey);
  } catch {
    return null;
  }
};

const safeStorageSet = (key, value) => {
  const storageKey = String(key || '').trim();
  if (storageKey === '') {
    return;
  }
  try {
    window.localStorage.setItem(storageKey, value);
  } catch {
    // localStorage can be unavailable; fail safe.
  }
};

const escapeHtmlAttr = (value) => String(value ?? '')
  .replaceAll('&', '&amp;')
  .replaceAll('<', '&lt;')
  .replaceAll('>', '&gt;')
  .replaceAll('"', '&quot;');

const normalizeColumnIdPart = (value) => String(value ?? '')
  .trim()
  .toLowerCase()
  .replaceAll(/[^a-z0-9]+/g, '_')
  .replaceAll(/^_+|_+$/g, '');

const uniqueColumnId = (candidate, usedIds, fallbackPrefix = 'col') => {
  const base = normalizeColumnIdPart(candidate) || fallbackPrefix;
  let next = base;
  let counter = 2;
  while (usedIds.has(next)) {
    next = `${base}_${counter}`;
    counter += 1;
  }
  usedIds.add(next);
  return next;
};

const normalizeGridColumns = (columns, sortColumns) => {
  const source = Array.isArray(columns) ? columns : [];
  const sortKeys = Array.isArray(sortColumns) ? sortColumns : [];
  const usedIds = new Set();

  return source.map((column, index) => {
    const baseColumn = (column && typeof column === 'object' && !Array.isArray(column))
      ? column
      : { name: String(column ?? '') };
    const explicitId = typeof baseColumn.id === 'string' ? baseColumn.id : '';
    const sortKey = typeof sortKeys[index] === 'string' ? sortKeys[index] : '';
    const label = typeof baseColumn.name === 'string' ? baseColumn.name : '';
    const fallback = `col_${index + 1}`;
    const id = uniqueColumnId(explicitId || sortKey || label || fallback, usedIds, fallback);
    return { ...baseColumn, id };
  });
};

export function createServerGrid(options) {
  const {
    container,
    dataUrl,
    appBase = '/',
    columns,
    mapData,
    total = (data) => data.total,
    sortColumns = [],
    paginationLimit = 10,
    language = {},
    search = null,
    filters = [],
    filterBindingMode = 'live',
    urlSync = true,
    urlState = null,
    gridjs,
    rowDblClick = null,
    actions = null,
    rowInteraction = null,
    rowDataset = null,
    selection = null,
    pageSize = null
  } = options || {};

  const normalizedFilterBindingMode = String(filterBindingMode || 'live').trim().toLowerCase() === 'manual'
    ? 'manual'
    : 'live';

  const gridjsLib = gridjs || window.gridjs;
  const containerEl = typeof container === 'string' ? document.querySelector(container) : container;
  if (!containerEl || !gridjsLib) {
    if (!containerEl) {
      warnOnce('UI_EL_MISSING', `Missing grid container: ${container}`, { module: 'grid-factory' });
    }
    if (!gridjsLib) {
      warnOnce('UI_LIB_MISSING', 'Grid.js not available', { module: 'grid-factory' });
    }
    return null;
  }

  const normalizedColumns = normalizeGridColumns(columns, sortColumns);

  const params = new URLSearchParams(window.location.search);
  const urlStateConfig = {
    pageParam: 'page',
    sortOrderParam: 'order',
    sortDirParam: 'dir',
    limitParam: 'limit',
    cleanFirstPage: true,
    cleanDefaultLimit: true,
    syncPage: true,
    syncSort: true,
    syncLimit: true,
    ...(urlState || {})
  };
  const pageParam = urlStateConfig.pageParam || 'page';
  const sortOrderParam = urlStateConfig.sortOrderParam || 'order';
  const sortDirParam = urlStateConfig.sortDirParam || 'dir';
  const limitParam = urlStateConfig.limitParam || 'limit';
  const resolveUrl = (path) => new URL(path, appBase).toString();
  const addParam = (url, key, value) => {
    const nextUrl = new URL(url);
    nextUrl.searchParams.set(key, value);
    return nextUrl.toString();
  };
  const deleteParam = (url, key) => {
    const nextUrl = new URL(url);
    nextUrl.searchParams.delete(key);
    return nextUrl.toString();
  };
  const clearSortParams = (url) => deleteParam(deleteParam(url, sortOrderParam), sortDirParam);
  const parsePage = (rawValue) => {
    const parsed = Number.parseInt(String(rawValue ?? ''), 10);
    if (!Number.isFinite(parsed) || parsed < 1) {
      return 1;
    }
    return parsed;
  };
  const validSortKeys = new Set(
    (sortColumns || [])
      .filter((key) => typeof key === 'string')
      .map((key) => key.trim())
      .filter(Boolean)
  );
  const normalizeDir = (rawValue) => {
    const normalized = String(rawValue ?? '').trim().toLowerCase();
    return normalized === 'asc' || normalized === 'desc' ? normalized : '';
  };
  const pageSizeConfig = pageSize && typeof pageSize === 'object'
    ? pageSize
    : {};
  const pageSizeEnabled = pageSizeConfig.enabled !== false;
  const pageSizeOptions = normalizePageSizeOptions(pageSizeConfig.options);
  const defaultLimit = pageSizeEnabled
    ? (
      pageSizeOptions.includes(parsePositiveInt(paginationLimit))
        ? Number(parsePositiveInt(paginationLimit))
        : pageSizeOptions[0]
    )
    : (parsePositiveInt(paginationLimit) ?? 10);
  const syncLimit = pageSizeEnabled && urlStateConfig.syncLimit !== false;
  const cleanDefaultLimit = urlStateConfig.cleanDefaultLimit !== false;
  const dataUrlPath = (() => {
    try {
      return new URL(dataUrl, appBase).pathname || String(dataUrl || '');
    } catch {
      return String(dataUrl || '');
    }
  })();
  const containerKey = typeof container === 'string'
    ? container
    : (containerEl.id ? `#${containerEl.id}` : 'grid');
  const autoPageSizeStorageKey = [
    PAGE_SIZE_STORAGE_PREFIX,
    encodeURIComponent(window.location.pathname || ''),
    encodeURIComponent(dataUrlPath || ''),
    encodeURIComponent(containerKey || '')
  ].join(':');
  const pageSizeStorageKey = pageSizeEnabled
    ? (String(pageSizeConfig.storageKey || autoPageSizeStorageKey).trim() || autoPageSizeStorageKey)
    : '';
  const parseAllowedLimit = (value) => {
    if (!pageSizeEnabled) {
      return parsePositiveInt(value);
    }
    const parsed = parsePositiveInt(value);
    if (parsed === null) {
      return null;
    }
    return pageSizeOptions.includes(parsed) ? parsed : null;
  };
  const pageSizeLabel = String(
    pageSizeConfig.label
      || language?.pagination?.rowsPerPage
      || 'Rows per page'
  ).trim() || 'Rows per page';
  const pageSizeCompactLabel = String(
    pageSizeConfig.compactLabel
      || language?.pagination?.rowsPerPageShort
      || 'Per page'
  ).trim() || 'Per page';

  let currentLimit = defaultLimit;
  const urlLimit = syncLimit ? parseAllowedLimit(params.get(limitParam)) : null;
  if (urlLimit !== null) {
    currentLimit = urlLimit;
  } else if (pageSizeEnabled) {
    const storedLimit = parseAllowedLimit(safeStorageGet(pageSizeStorageKey));
    if (storedLimit !== null) {
      currentLimit = storedLimit;
    }
  }
  if (pageSizeEnabled) {
    safeStorageSet(pageSizeStorageKey, String(currentLimit));
  }
  let currentPage = urlStateConfig.syncPage ? parsePage(params.get(pageParam)) : 1;
  let currentSort = null;
  if (urlStateConfig.syncSort) {
    const initialOrder = String(params.get(sortOrderParam) ?? '').trim();
    const initialDir = normalizeDir(params.get(sortDirParam));
    if (initialOrder !== '' && initialDir !== '' && validSortKeys.has(initialOrder)) {
      currentSort = { order: initialOrder, dir: initialDir };
    }
  }
  const rowInteractionConfig = rowInteraction && typeof rowInteraction === 'object'
    ? rowInteraction
    : {};
  const hasRowOpenHandler = Boolean(rowDblClick && typeof rowDblClick.getUrl === 'function');
  const rowInteractionEnabled = hasRowOpenHandler && rowInteractionConfig.enabled !== false;
  const keepDblClick = rowInteractionEnabled && rowInteractionConfig.keepDblClick !== false;
  const enableEnterOnRow = rowInteractionEnabled && rowInteractionConfig.enableEnterOnRow !== false;
  const showRowActionButton = rowInteractionEnabled && rowInteractionConfig.showActionButton !== false;
  const rowActionColumnLabel = String(
    rowInteractionConfig.actionColumnLabel
      || language?.actions?.column
      || 'Actions'
  ).trim() || 'Actions';
  const rowActionOpenLabel = String(language?.actions?.open || 'Open').trim() || 'Open';
  const rowActionEditLabel = String(language?.actions?.edit || 'Edit').trim() || 'Edit';
  const rowEnterHint = String(
    rowInteractionConfig.rowEnterHint
      || language?.actions?.rowEnterHint
      || 'Press Enter to open row'
  ).trim() || 'Press Enter to open row';
  const gridRetryLabel = String(language?.errorRetry || 'Retry').trim() || 'Retry';
  const defaultResolveRowActionLabel = (url) => (
    /(^|\/)edit(\/|$|\?)/i.test(String(url || '').trim())
      ? rowActionEditLabel
      : rowActionOpenLabel
  );
  const resolveActionLabel = typeof rowInteractionConfig.resolveActionLabel === 'function'
    ? rowInteractionConfig.resolveActionLabel
    : defaultResolveRowActionLabel;
  const resolveRowOpenUrl = (rowData, rowIndex = -1) => {
    if (!rowInteractionEnabled) {
      return '';
    }
    const url = rowDblClick.getUrl(rowData, rowIndex);
    const normalized = String(url || '').trim();
    if (normalized === '') {
      return '';
    }
    return withCurrentListReturn(normalized);
  };

  const getInput = (input) => {
    if (!input) {return null;}
    return typeof input === 'string' ? document.querySelector(input) : input;
  };

  const getInputValue = (input) => {
    if (!input) {return '';}
    if (input.type === 'checkbox') {
      return input.checked ? (input.value || '1') : '';
    }
    if (input.tagName === 'SELECT' && input.multiple) {
      return Array.from(input.selectedOptions || []).map((option) => option.value).filter(Boolean);
    }
    return (input.value ?? '').toString();
  };

  const setInputValue = (input, value) => {
    if (!input) {return;}
    if (input.type === 'checkbox') {
      input.checked = value === true || value === 'true' || value === '1';
    } else if (input.tagName === 'SELECT' && input.multiple) {
      const values = Array.isArray(value)
        ? value.map((item) => item.toString())
        : value
          ? value.toString().split(',').map((item) => item.trim()).filter(Boolean)
          : [];
      Array.from(input.options || []).forEach((option) => {
        option.selected = values.includes(option.value);
      });
    } else {
      input.value = value;
    }
    if (input._multiSelectInstance && typeof input._multiSelectInstance.setValues === 'function') {
      const values = Array.isArray(value)
        ? value.map((item) => item.toString())
        : value
          ? value.toString().split(',').map((item) => item.trim()).filter(Boolean)
          : [];
      input._multiSelectInstance.setValues(values);
    }
  };

  let searchValue = '';
  let searchParam = 'search';
  let searchInput = null;
  let searchDebounce = 250;
  if (search) {
    searchInput = getInput(search.input);
    searchParam = search.param || searchParam;
    searchDebounce = search.debounce ?? searchDebounce;
    const initial = params.get(searchParam) ?? search.default ?? '';
    searchValue = initial || '';
    if (searchInput) {
      setInputValue(searchInput, initial);
    }
  }

  const filterState = filters.map((filter) => {
    const input = getInput(filter.input);
    const initial = params.get(filter.param);
    const value = initial !== null ? initial : (filter.default ?? '');
    if (input) {
      const parsed = typeof filter.parse === 'function' ? filter.parse(value) : value;
      setInputValue(input, parsed);
    }
    return { ...filter, input, value };
  });

  const normalizeValue = (filter, value) => {
    if (typeof filter.normalize === 'function') {
      return filter.normalize(value);
    }
    return value;
  };

  const resolveFilterRawValue = (filter) => {
    if (!filter) {
      return '';
    }
    if (normalizedFilterBindingMode === 'manual') {
      return filter.value ?? '';
    }
    if (!filter.input) {
      return filter.value ?? '';
    }
    return getInputValue(filter.input);
  };

  const buildDataParams = () => {
    const query = new URLSearchParams();
    if (searchValue) {
      query.set(searchParam, searchValue);
    }
    filterState.forEach((filter) => {
      const raw = resolveFilterRawValue(filter);
      const normalized = normalizeValue(filter, raw);
      if (Array.isArray(normalized)) {
        const list = normalized.map((item) => item.toString()).filter(Boolean);
        if (list.length) {
          query.set(filter.param, list.join(','));
        }
        return;
      }
      if (normalized !== '' && normalized !== null && typeof normalized !== 'undefined') {
        query.set(filter.param, normalized);
      }
    });
    if (currentSort?.order && currentSort?.dir) {
      query.set(sortOrderParam, currentSort.order);
      query.set(sortDirParam, currentSort.dir);
    }
    return query;
  };

  const buildUrlParams = () => {
    const query = buildDataParams();
    if (!urlStateConfig.syncSort) {
      query.delete(sortOrderParam);
      query.delete(sortDirParam);
    }
    if (urlStateConfig.syncPage) {
      if (urlStateConfig.cleanFirstPage && currentPage <= 1) {
        query.delete(pageParam);
      } else {
        query.set(pageParam, String(currentPage));
      }
    } else {
      query.delete(pageParam);
    }
    if (syncLimit) {
      if (!cleanDefaultLimit || currentLimit !== defaultLimit) {
        query.set(limitParam, String(currentLimit));
      } else {
        query.delete(limitParam);
      }
    } else {
      query.delete(limitParam);
    }
    return query;
  };

  const baseUrl = () => {
    const url = new URL(dataUrl, appBase);
    const query = buildDataParams();
    query.forEach((value, key) => url.searchParams.set(key, value));
    return url.toString();
  };

  const updateUrl = (options = {}) => {
    if (!urlSync) {return;}
    const historyMode = String(options?.historyMode || 'replace').trim().toLowerCase() === 'push'
      ? 'push'
      : 'replace';
    const url = new URL(window.location.href);
    if (searchParam) { url.searchParams.delete(searchParam); }
    filterState.forEach((filter) => url.searchParams.delete(filter.param));
    url.searchParams.delete(sortOrderParam);
    url.searchParams.delete(sortDirParam);
    url.searchParams.delete(pageParam);
    url.searchParams.delete(limitParam);
    const query = buildUrlParams();
    query.forEach((value, key) => url.searchParams.set(key, value));
    if (historyMode === 'push') {
      window.history.pushState({}, '', url.toString());
      return;
    }
    window.history.replaceState({}, '', url.toString());
  };

  const explicitActionConfig = actions && actions.enabled ? {
    id: String(actions.id || 'row_actions').trim() || 'row_actions',
    label: actions.label || 'Actions',
    index: Number.isInteger(actions.index) ? actions.index : null,
    items: Array.isArray(actions.items) ? actions.items : [],
    getRowId: typeof actions.getRowId === 'function' ? actions.getRowId : null,
    wrapperClass: actions.wrapperClass || 'grid-actions',
    onAction: typeof actions.onAction === 'function' ? actions.onAction : null
  } : null;
  const rowOpenActionConfig = !explicitActionConfig && showRowActionButton ? {
    id: 'row_actions',
    label: gridjsLib.html(
      `<span class="app-grid-actions-header" data-tooltip="${escapeHtmlAttr(rowEnterHint)}" data-tooltip-pos="top" aria-label="${escapeHtmlAttr(rowEnterHint)}">${escapeHtmlAttr(rowActionColumnLabel)}</span>`
    ),
    index: null,
    items: [{
      key: 'row-open',
      type: 'link',
      className: 'outline secondary app-grid-row-open',
      label: (rowData, url) => {
        const dynamicLabel = resolveActionLabel(url, rowData);
        const normalizedLabel = String(dynamicLabel || '').trim();
        return normalizedLabel || defaultResolveRowActionLabel(url);
      },
      ariaLabel: (_rowData, _url, label) => `${String(label || '').trim()}. ${rowEnterHint}`,
      title: (_rowData, _url) => rowEnterHint,
      href: ({ id }) => id
    }],
    // For row-open actions the URL itself is the stable row identifier.
    getRowId: (rowData) => resolveRowOpenUrl(rowData),
    wrapperClass: 'grid-actions grid-actions-row-open',
    onAction: null
  } : null;
  const actionConfig = explicitActionConfig || rowOpenActionConfig;
  const selectionConfig = selection && selection.enabled ? {
    id: selection.id || 'selectRow',
    index: Number.isInteger(selection.index) ? selection.index : 0,
    component: selection.component || null,
    props: selection.props || null,
    onChange: typeof selection.onChange === 'function' ? selection.onChange : null,
    getSelectedIds: typeof selection.getSelectedIds === 'function' ? selection.getSelectedIds : null,
    selectAllLabel: selection.selectAllLabel || 'Select all',
    selectAllEnabled: selection.selectAllEnabled !== false
  } : null;

  if (selectionConfig) {
    const usedColumnIds = new Set(
      normalizedColumns
        .map((column) => (column && typeof column === 'object' ? String(column.id || '').trim() : ''))
        .filter(Boolean)
    );
    selectionConfig.id = uniqueColumnId(selectionConfig.id, usedColumnIds, 'select_row');
  }

  const insertActionColumn = (cols) => {
    if (!actionConfig) {return cols;}
    const usedColumnIds = new Set(
      cols
        .map((column) => (column && typeof column === 'object' ? String(column.id || '').trim() : ''))
        .filter(Boolean)
    );
    const actionColumnId = uniqueColumnId(actionConfig.id, usedColumnIds, 'row_actions');
    const actionColumn = {
      id: actionColumnId,
      name: actionConfig.label,
      sort: false,
      formatter: (_cell, row) => {
        const id = actionConfig.getRowId ? actionConfig.getRowId(row) : null;
        if (!id) {
          return gridjsLib.html('');
        }
        const buttons = actionConfig.items.map((item) => {
          if (!item || !item.key) {return '';}
          const label = typeof item.label === 'function' ? item.label(row, id) : item.label;
          if (!label) {return '';}
          const ariaLabelRaw = typeof item.ariaLabel === 'function'
            ? item.ariaLabel(row, id, label)
            : item.ariaLabel;
          const titleRaw = typeof item.title === 'function'
            ? item.title(row, id, label)
            : item.title;
          const ariaAttr = ariaLabelRaw ? ` aria-label="${escapeHtmlAttr(ariaLabelRaw)}"` : '';
          const titleAttr = titleRaw ? ` title="${escapeHtmlAttr(titleRaw)}"` : '';
          const className = item.className ? ` ${item.className}` : '';
          const type = item.type || 'button';
          if (type === 'link') {
            const href = typeof item.href === 'function' ? item.href({ row, id }) : item.href;
            const url = href || '#';
            return `<a role="button" class="${className.trim()}" data-grid-action="${item.key}" href="${url}"${ariaAttr}${titleAttr}>${label}</a>`;
          }
          return `<button type="button" class="${className.trim()}" data-grid-action="${item.key}"${ariaAttr}${titleAttr}>${label}</button>`;
        }).join('');
        return gridjsLib.html(`<div class="${actionConfig.wrapperClass}" role="group">${buttons}</div>`);
      }
    };

    const next = [...cols];
    const index = actionConfig.index === null ? next.length : Math.min(Math.max(actionConfig.index, 0), next.length);
    next.splice(index, 0, actionColumn);
    return next;
  };

  const insertSelectionColumn = (cols) => {
    if (!selectionConfig) {return cols;}
    const headerLabel = selectionConfig.selectAllLabel || 'Select all';
    const header = selectionConfig.selectAllEnabled
      ? gridjsLib.html(`<input type="checkbox" data-grid-select-all aria-label="${headerLabel}">`)
      : '';
    const selectionColumn = {
      id: selectionConfig.id,
      name: header,
      sort: false,
      plugin: {
        id: selectionConfig.id
      }
    };

    const next = [...cols];
    const index = Math.min(Math.max(selectionConfig.index ?? 0, 0), next.length);
    next.splice(index, 0, selectionColumn);
    return next;
  };

  const insertActionSort = (cols) => {
    if (!actionConfig || !cols.length) {return cols;}
    const next = [...cols];
    const index = actionConfig.index === null ? next.length : Math.min(Math.max(actionConfig.index, 0), next.length);
    next.splice(index, 0, null);
    return next;
  };

  const insertSelectionSort = (cols) => {
    if (!selectionConfig || !cols.length) {return cols;}
    const next = [...cols];
    const index = Math.min(Math.max(selectionConfig.index ?? 0, 0), next.length);
    next.splice(index, 0, null);
    return next;
  };

  const mapDataWithActions = (data) => {
    const rows = mapData ? mapData(data) : [];
    if (!actionConfig || !Array.isArray(rows)) {
      return rows;
    }
    const index = actionConfig.index === null ? null : Math.min(Math.max(actionConfig.index, 0), Infinity);
    return rows.map((row) => {
      if (!Array.isArray(row)) {return row;}
      const next = [...row];
      const insertAt = index === null ? next.length : Math.min(index, next.length);
      next.splice(insertAt, 0, null);
      return next;
    });
  };

  const serverConfig = {
    url: baseUrl(),
    then: mapDataWithActions,
    total
  };

  let finalSortColumns = sortColumns;
  if (selectionConfig) {
    finalSortColumns = insertSelectionSort(finalSortColumns);
  }
  if (actionConfig) {
    finalSortColumns = insertActionSort(finalSortColumns);
  }
  const sortConfig = finalSortColumns.length ? {
    enabled: true,
    server: {
      url: (prev, columns) => {
        if (!columns.length) {
          // Keep URL/bootstrap sort when grid has no active sort columns yet (e.g. initial load from deep-link).
          if (currentSort?.order && currentSort?.dir) {
            return addParam(addParam(clearSortParams(prev), sortOrderParam, currentSort.order), sortDirParam, currentSort.dir);
          }
          currentSort = null;
          return clearSortParams(prev);
        }
        const column = columns[0];
        const key = finalSortColumns[column.index];
        if (!key) {
          if (currentSort?.order && currentSort?.dir) {
            return addParam(addParam(clearSortParams(prev), sortOrderParam, currentSort.order), sortDirParam, currentSort.dir);
          }
          currentSort = null;
          return clearSortParams(prev);
        }
        const direction = column.direction === 1 ? 'asc' : 'desc';
        currentSort = { order: key, dir: direction };
        if (urlSync && urlStateConfig.syncSort) {
          updateUrl();
        }
        return addParam(addParam(clearSortParams(prev), sortOrderParam, key), sortDirParam, direction);
      }
    }
  } : { enabled: false };

  const paginationConfig = {
    limit: currentLimit,
    page: Math.max(currentPage - 1, 0),
    // We control page resets explicitly (search/filter/reset actions) to keep deep-link pages stable.
    resetPageOnUpdate: false,
    server: {
      url: (prev, page, limit) => {
        const parsedPage = Number.parseInt(String(page ?? ''), 10);
        const parsedLimit = parseAllowedLimit(limit);
        const nextPage = Number.isFinite(parsedPage) && parsedPage >= 0 ? parsedPage : 0;
        const nextLimit = parsedLimit ?? currentLimit;
        currentLimit = nextLimit;
        if (pageSizeEnabled) {
          safeStorageSet(pageSizeStorageKey, String(currentLimit));
        }
        currentPage = nextPage + 1;
        if (urlSync && (urlStateConfig.syncPage || syncLimit)) {
          updateUrl();
        }
        const offset = nextPage * nextLimit;
        return addParam(addParam(prev, 'limit', nextLimit), 'offset', offset);
      }
    }
  };

  const plugins = [];
  if (selectionConfig?.component) {
    plugins.push({
      id: selectionConfig.id,
      component: selectionConfig.component,
      props: selectionConfig.props || {}
    });
  }

  const grid = new gridjsLib.Grid({
    columns: insertActionColumn(insertSelectionColumn(normalizedColumns)),
    server: serverConfig,
    search: false,
    sort: sortConfig,
    pagination: paginationConfig,
    language,
    plugins
  });

  grid.render(containerEl);
  setTimeout(() => {
    initSelectAll();
  }, 0);

  // Prevent empty-state flash before data arrives.
  let hasLoadedOnce = false;
  const markLoadedOnce = () => {
    if (hasLoadedOnce) {return;}
    hasLoadedOnce = true;
    containerEl.classList.add('gridjs-has-loaded');
  };
  const setUpdating = () => containerEl.classList.add('gridjs-is-updating');
  const clearUpdating = () => containerEl.classList.remove('gridjs-is-updating');
  const ensureErrorRetryControl = () => {
    const errorMessage = containerEl.querySelector('.gridjs-message.gridjs-error');
    if (!errorMessage) {
      return;
    }
    if (errorMessage.querySelector('[data-grid-retry]')) {
      return;
    }
    const actionsWrap = document.createElement('div');
    actionsWrap.className = 'app-grid-error-actions';

    const retryButton = document.createElement('button');
    retryButton.type = 'button';
    retryButton.className = 'outline secondary app-grid-retry';
    retryButton.setAttribute('data-grid-retry', '1');
    retryButton.textContent = gridRetryLabel;

    actionsWrap.appendChild(retryButton);
    errorMessage.appendChild(actionsWrap);
  };

  const store = grid?.config?.store;
  if (store?.subscribe) {
    store.subscribe((state) => {
      if (!state) {return;}
      // Grid.js status: 0=Init, 1=Loading, 2=Loaded, 3=Rendered, 4=Error
      if (state.status === 1) {
        setUpdating();
      } else {
        clearUpdating();
      }
      if (state.status === 4) {
        setTimeout(() => {
          ensureErrorRetryControl();
        }, 0);
      }
      if (state.status >= 2) {
        markLoadedOnce();
      }
      if (state.status >= 3) {
        initSelectAll();
      }
    });
  }

  const applyRowDataset = () => {
    const shouldApplyCustomDataset = typeof rowDataset === 'function';
    if (!shouldApplyCustomDataset && !rowInteractionEnabled) {return;}
    const rowEls = containerEl.querySelectorAll('.gridjs-tbody .gridjs-tr');
    const dataRows = grid.config.store.getState().data?.rows || [];
    rowEls.forEach((rowEl, index) => {
      const rowData = dataRows[index] || null;
      if (!rowData) {return;}
      if (rowInteractionEnabled) {
        const rowOpenUrl = resolveRowOpenUrl(rowData, index);
        if (rowOpenUrl !== '') {
          rowEl.setAttribute('data-row-open-url', rowOpenUrl);
          rowEl.setAttribute('tabindex', '-1');
        } else {
          rowEl.removeAttribute('data-row-open-url');
          if (rowEl.getAttribute('tabindex') === '-1') {
            rowEl.removeAttribute('tabindex');
          }
        }
      }
      if (shouldApplyCustomDataset) {
        const attrs = rowDataset(rowData, index) || {};
        Object.keys(attrs).forEach((key) => {
          const value = attrs[key];
          const attr = `data-${key}`;
          if (value === null || typeof value === 'undefined' || value === '') {
            rowEl.removeAttribute(attr);
          } else {
            rowEl.setAttribute(attr, String(value));
          }
        });
      }
    });
  };

  const initSelectAll = () => {
    if (!selectionConfig?.component || !selectionConfig.selectAllEnabled) {return;}
    const header = containerEl.querySelector(`th[data-column-id="${selectionConfig.id}"] input[data-grid-select-all]`);
    if (!header) {return;}
    if (header.dataset.bound === '1') {
      return;
    }
    header.dataset.bound = '1';

    const getRowCheckboxes = () => Array.from(
      containerEl.querySelectorAll(`td[data-column-id="${selectionConfig.id}"] input[type="checkbox"]`)
    );

    const updateHeaderState = () => {
      const boxes = getRowCheckboxes();
      const total = boxes.length;
      const checked = boxes.filter((box) => box.checked).length;
      header.indeterminate = total > 0 && checked > 0 && checked < total;
      header.checked = total > 0 && checked === total;
      header.disabled = total === 0;
    };

    header.addEventListener('change', () => {
      const boxes = getRowCheckboxes();
      boxes.forEach((box) => {
        if (box.checked !== header.checked) {
          box.click();
        }
      });
      updateHeaderState();
    });

    updateHeaderState();
  };

  if (typeof rowDataset === 'function' || rowInteractionEnabled) {
    const observer = new MutationObserver(() => applyRowDataset());
    observer.observe(containerEl, { childList: true, subtree: true });
    applyRowDataset();
  }

  const getRowDataByElement = (rowEl) => {
    if (!rowEl) {return null;}
    const rows = Array.from(rowEl.parentElement?.children || []);
    const rowIndex = rows.indexOf(rowEl);
    if (rowIndex < 0) {return null;}
    const dataRows = grid.config.store.getState().data?.rows || [];
    const rowData = dataRows[rowIndex] || null;
    if (!rowData) {return null;}
    return { rowData, rowIndex };
  };

  const resolveRowOpenUrlByElement = (rowEl) => {
    if (!rowInteractionEnabled || !rowEl) {
      return '';
    }
    const storedUrl = String(rowEl.getAttribute('data-row-open-url') || '').trim();
    if (storedUrl !== '') {
      return storedUrl;
    }
    const rowInfo = getRowDataByElement(rowEl);
    if (!rowInfo) {
      return '';
    }
    return resolveRowOpenUrl(rowInfo.rowData, rowInfo.rowIndex);
  };

  const navigateToRow = (rowEl) => {
    const url = resolveRowOpenUrlByElement(rowEl);
    if (url === '') {
      return false;
    }
    window.location.href = url;
    return true;
  };

  if (keepDblClick && rowDblClick) {
    const excludeSelector = rowDblClick.exclude || '.grid-actions, button, a, input, select, textarea';
    containerEl.addEventListener('dblclick', (event) => {
      if (excludeSelector && event.target.closest(excludeSelector)) {
        return;
      }
      const rowEl = event.target.closest('.gridjs-tr');
      navigateToRow(rowEl);
    });
  }

  if (enableEnterOnRow) {
    const interactiveSelector = '.grid-actions, button, a, input, select, textarea, [role="button"], [contenteditable="true"]';
    containerEl.addEventListener('click', (event) => {
      const target = event.target instanceof Element ? event.target : null;
      if (!target || target.closest(interactiveSelector)) {
        return;
      }
      const rowEl = target.closest('.gridjs-tr[data-row-open-url]');
      if (!rowEl) {
        return;
      }
      rowEl.focus({ preventScroll: true });
    });
    containerEl.addEventListener('keydown', (event) => {
      if (event.key !== 'Enter' || event.defaultPrevented) {
        return;
      }
      if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {
        return;
      }
      const target = event.target instanceof Element ? event.target : null;
      if (!target || target.closest(interactiveSelector)) {
        return;
      }
      const rowEl = target.closest('.gridjs-tr');
      if (!rowEl) {
        return;
      }
      if (navigateToRow(rowEl)) {
        event.preventDefault();
      }
    });
  }

  if (actionConfig) {
    containerEl.addEventListener('click', async (event) => {
      const actionEl = event.target.closest('[data-grid-action]');
      if (!actionEl) {
        return;
      }
      const rowEl = actionEl.closest('.gridjs-tr');
      const rowInfo = getRowDataByElement(rowEl);
      if (!rowInfo) {
        return;
      }
      const { rowData, rowIndex } = rowInfo;
      const actionKey = actionEl.getAttribute('data-grid-action') || '';
      const actionItem = actionConfig.items.find((item) => item && item.key === actionKey);
      if (!actionItem) {
        return;
      }
      const id = actionConfig.getRowId ? actionConfig.getRowId(rowData, rowIndex) : null;
      if (!id) {
        return;
      }
      const confirmText = typeof actionItem.confirm === 'function'
        ? actionItem.confirm({ row: rowData, id })
        : actionItem.confirm;
      if (confirmText) {
        const confirmOptionsFromItem = typeof actionItem.confirmOptions === 'function'
          ? (actionItem.confirmOptions({ row: rowData, id, action: actionKey }) || {})
          : (actionItem.confirmOptions || {});
        const approved = await confirmDialog.confirm({
          ...confirmOptionsFromItem,
          message: String(confirmText),
          actionKind: String(confirmOptionsFromItem.actionKind || actionKey || '')
        });
        if (!approved) {
          return;
        }
      }
      if (actionItem.type === 'link') {
        const href = typeof actionItem.href === 'function'
          ? actionItem.href({ row: rowData, id })
          : actionItem.href;
        if (href) {
          event.preventDefault();
          window.location.href = href;
        }
        return;
      }
      if (actionConfig.onAction) {
        event.preventDefault();
        await actionConfig.onAction({ action: actionKey, id, row: rowData, event, grid, container: containerEl });
      }
    });
  }

  containerEl.addEventListener('click', (event) => {
    const retryButton = event.target.closest('[data-grid-retry]');
    if (!retryButton) {
      return;
    }
    event.preventDefault();
    retryButton.setAttribute('aria-busy', 'true');
    updateGrid({ resetPage: false });
  });

  let pageSizeSelect = null;
  const syncPageSizeControl = () => {
    if (!pageSizeSelect) {
      return;
    }
    pageSizeSelect.value = String(currentLimit);
  };

  const resolvePageSizeToolbar = () => {
    const selector = String(pageSizeConfig.toolbarSelector || '.app-list-toolbar').trim() || '.app-list-toolbar';
    const allToolbars = Array.from(document.querySelectorAll(selector));
    if (!allToolbars.length) {
      return null;
    }
    const precedingToolbars = allToolbars.filter((toolbar) => (
      Boolean(toolbar.compareDocumentPosition(containerEl) & Node.DOCUMENT_POSITION_FOLLOWING)
    ));
    const pool = precedingToolbars.length ? precedingToolbars : allToolbars;
    const withFilterButton = pool.filter((toolbar) => toolbar.querySelector('[data-filter-drawer-open]'));
    const targetPool = withFilterButton.length ? withFilterButton : pool;
    return targetPool[targetPool.length - 1] || null;
  };

  const mountPageSizeControl = () => {
    if (!pageSizeEnabled) {
      return;
    }
    const toolbar = resolvePageSizeToolbar();
    if (!toolbar) {
      warnOnce(
        'UI_PAGE_SIZE_HOST_MISSING',
        `Missing page-size toolbar near grid: ${containerKey || 'unknown'}`,
        { module: 'grid-factory' }
      );
      return;
    }
    const controlKey = encodeURIComponent(`${containerKey}:${dataUrlPath}`);
    const existing = toolbar.querySelector(`.app-grid-page-size[data-grid-page-size-key="${controlKey}"]`);
    if (existing) {
      pageSizeSelect = existing.querySelector('select');
      syncPageSizeControl();
      return;
    }

    const field = document.createElement('label');
    field.className = 'app-field app-grid-page-size';
    field.dataset.gridPageSizeKey = controlKey;

    const caption = document.createElement('span');
    caption.textContent = pageSizeCompactLabel;

    const select = document.createElement('select');
    select.setAttribute('aria-label', pageSizeLabel);
    select.setAttribute('title', pageSizeLabel);
    pageSizeOptions.forEach((option) => {
      const optionEl = document.createElement('option');
      optionEl.value = String(option);
      optionEl.textContent = String(option);
      select.appendChild(optionEl);
    });
    select.value = String(currentLimit);
    select.addEventListener('change', () => {
      const nextLimit = parseAllowedLimit(select.value);
      if (nextLimit === null || nextLimit === currentLimit) {
        syncPageSizeControl();
        return;
      }
      currentLimit = nextLimit;
      currentPage = 1;
      safeStorageSet(pageSizeStorageKey, String(currentLimit));
      syncPageSizeControl();
      updateGrid({ resetPage: true, urlHistoryMode: 'push' });
    });

    field.appendChild(caption);
    field.appendChild(select);

    const filterButton = toolbar.querySelector(':scope > [data-filter-drawer-open]');
    if (filterButton) {
      filterButton.insertAdjacentElement('afterend', field);
    } else {
      toolbar.appendChild(field);
    }
    pageSizeSelect = select;
    syncPageSizeControl();
  };

  const updateGrid = (options = {}) => {
    const resetPage = options && options.resetPage === true;
    const urlHistoryMode = options?.urlHistoryMode || 'replace';
    if (resetPage) {
      currentPage = 1;
    }
    updateUrl({ historyMode: urlHistoryMode });
    const paginationState = grid?.config?.pagination && typeof grid.config.pagination === 'object'
      ? grid.config.pagination
      : paginationConfig;
    const nextPagination = {
      ...paginationState,
      limit: currentLimit,
      page: resetPage ? 0 : Math.max(currentPage - 1, 0)
    };
    const nextConfig = {
      server: { ...serverConfig, url: baseUrl() },
      pagination: nextPagination
    };

    // Use Grid.js reconfiguration API so server pipeline is refreshed reliably on filter/search changes.
    if (typeof grid?.updateConfig === 'function') {
      grid.updateConfig(nextConfig).forceRender();
      return;
    }

    if (grid?.config) {
      grid.config.server = nextConfig.server;
      grid.config.pagination = nextConfig.pagination;
    }
    grid.forceRender();
  };

  mountPageSizeControl();

  const debounce = (fn, delay = 250) => {
    let timer;
    return (...args) => {
      clearTimeout(timer);
      timer = setTimeout(() => fn(...args), delay);
    };
  };

  if (searchInput) {
    searchInput.addEventListener('input', debounce((event) => {
      searchValue = event.target.value.trim();
      updateGrid({ resetPage: true });
    }, searchDebounce));
  }

  if (normalizedFilterBindingMode === 'live') {
    filterState.forEach((filter) => {
      if (!filter.input) {return;}
      const eventName = filter.event || 'change';
      filter.input.addEventListener(eventName, () => {
        filter.value = getInputValue(filter.input);
        updateGrid({ resetPage: true });
      });
    });
  }

  const commitFilterInputValues = () => {
    filterState.forEach((filter) => {
      if (!filter.input) {return;}
      filter.value = getInputValue(filter.input);
    });
  };

  const applyFilters = (options = {}) => {
    const resetPage = options?.resetPage !== false;
    const urlHistoryMode = options?.urlHistoryMode || 'replace';
    commitFilterInputValues();
    updateGrid({ resetPage, urlHistoryMode });
  };

  const resetFilters = (options = {}) => {
    const preserveFilterParams = Array.isArray(options?.preserveFilterParams)
      ? options.preserveFilterParams.map((item) => String(item ?? '').trim()).filter(Boolean)
      : [];
    const preserveFilterParamSet = new Set(preserveFilterParams);
    const preserveSearch = options?.preserveSearch === true;
    const resetPage = options?.resetPage !== false;
    const urlHistoryMode = options?.urlHistoryMode || 'replace';

    if (search && !preserveSearch) {
      const defaultValue = search.default ?? '';
      searchValue = defaultValue;
      if (searchInput) {
        setInputValue(searchInput, defaultValue);
      }
    }
    filterState.forEach((filter) => {
      if (preserveFilterParamSet.has(filter.param)) {
        if (filter.input) {
          filter.value = getInputValue(filter.input);
        }
        return;
      }
      const defaultValue = filter.default ?? '';
      filter.value = defaultValue;
      if (filter.input) {
        setInputValue(filter.input, defaultValue);
      }
    });
    updateGrid({ resetPage, urlHistoryMode });
  };

  const captureAppliedState = () => {
    const filterValues = {};
    filterState.forEach((filter) => {
      filterValues[filter.param] = filter.value ?? filter.default ?? '';
    });
    return {
      search: searchValue,
      filters: filterValues,
      sort: currentSort ? { ...currentSort } : null,
      page: currentPage,
      limit: currentLimit
    };
  };

  const captureDraftState = () => {
    const filterValues = {};
    filterState.forEach((filter) => {
      const raw = filter.input ? getInputValue(filter.input) : (filter.value ?? filter.default ?? '');
      filterValues[filter.param] = raw;
    });
    const draftSearch = searchInput
      ? String(getInputValue(searchInput) ?? '').trim()
      : searchValue;
    return {
      search: draftSearch,
      filters: filterValues,
      sort: currentSort ? { ...currentSort } : null,
      page: currentPage,
      limit: currentLimit
    };
  };

  const restoreAppliedState = (state = {}) => {
    if (search) {
      const nextSearch = typeof state?.search === 'string' ? state.search : (search.default ?? '');
      searchValue = nextSearch;
      if (searchInput) {
        setInputValue(searchInput, nextSearch);
      }
    }

    const nextFilters = state?.filters && typeof state.filters === 'object'
      ? state.filters
      : {};
    filterState.forEach((filter) => {
      const nextValue = Object.prototype.hasOwnProperty.call(nextFilters, filter.param)
        ? nextFilters[filter.param]
        : (filter.default ?? '');
      filter.value = nextValue;
      if (filter.input) {
        setInputValue(filter.input, nextValue);
      }
    });

    if (state?.sort && typeof state.sort === 'object') {
      const order = String(state.sort.order ?? '').trim();
      const dir = normalizeDir(state.sort.dir ?? '');
      if (order !== '' && dir !== '' && validSortKeys.has(order)) {
        currentSort = { order, dir };
      } else {
        currentSort = null;
      }
    } else if (state?.sort === null) {
      currentSort = null;
    }

    if (Number.isFinite(Number(state?.page))) {
      const parsedPage = parsePage(state.page);
      currentPage = parsedPage;
    }
    const nextLimit = parseAllowedLimit(state?.limit);
    if (nextLimit !== null) {
      currentLimit = nextLimit;
      safeStorageSet(pageSizeStorageKey, String(currentLimit));
      syncPageSizeControl();
    }
  };

  const getSortParams = () => (currentSort ? { ...currentSort } : null);
  const selectionApi = selectionConfig && selectionConfig.component ? {
    getSelectedIds: () => {
      if (selectionConfig.getSelectedIds) {
        return selectionConfig.getSelectedIds({ grid, container: containerEl });
      }
      const plugin = grid?.config?.plugin?.get?.(selectionConfig.id);
      const store = plugin?.props?.store;
      const state = store?.state ?? store?.getState?.();
      if (!state) {return [];}
      if (Array.isArray(state.selected)) {return state.selected;}
      if (Array.isArray(state.ids)) {return state.ids;}
      if (state.rows && typeof state.rows === 'object') {
        return Object.keys(state.rows).filter((key) => state.rows[key]);
      }
      if (state instanceof Set) {return Array.from(state);}
      if (Array.isArray(state)) {return state;}
      if (typeof state === 'object') {
        return Object.keys(state).filter((key) => state[key]);
      }
      return [];
    },
    clear: () => {
      const plugin = grid?.config?.plugin?.get?.(selectionConfig.id);
      const store = plugin?.props?.store;
      if (store?.dispatch) {
        store.dispatch('reset');
      } else if (store?.reset) {
        store.reset();
      }
    }
  } : null;

  if (selectionConfig?.onChange && selectionConfig.component) {
    grid.on('ready', () => {
      const plugin = grid?.config?.plugin?.get?.(selectionConfig.id);
      const store = plugin?.props?.store;
      const sync = () => {
        selectionConfig.onChange(selectionApi?.getSelectedIds?.() || []);
      };
      if (store?.on) {store.on('updated', sync);}
      if (store?.subscribe) {store.subscribe(sync);}
      sync();
    });
  }

  grid.on('ready', () => {
    initSelectAll();
    setTimeout(() => {
      if (typeof window.requestFsLightboxRefresh === 'function') {
        window.requestFsLightboxRefresh();
        return;
      }
      if (typeof window.refreshFsLightbox === 'function') {
        window.refreshFsLightbox();
      } else {
        window.__fsLightboxRefreshPending = true;
      }
    }, 0);
  });

  return {
    grid,
    container: containerEl,
    updateGrid,
    updateUrl,
    baseUrl,
    resolveUrl,
    getSortParams,
    resetFilters,
    applyFilters,
    captureAppliedState,
    captureDraftState,
    restoreAppliedState,
    selection: selectionApi
  };
}

const DEFAULT_LIST_FILTER_LABELS = {
  chipsClearAll: 'Clear all filters',
  chipsRemove: 'Remove filter',
  filtersApplied: 'Filters applied',
  filterRemoved: 'Filter removed',
  filtersCleared: 'All filters cleared'
};

const resolveDomQueryElement = (target, root = document) => {
  if (!target) {
    return null;
  }
  if (typeof target === 'string') {
    if (root && typeof root.querySelector === 'function') {
      return root.querySelector(target) || document.querySelector(target);
    }
    return document.querySelector(target);
  }
  return target;
};

const readDomLabelDefaults = (options = {}) => {
  const drawerRoot = options.drawerRoot && typeof options.drawerRoot.querySelector === 'function'
    ? options.drawerRoot
    : document;
  const chipsSelector = options.chipsSelector || '[data-active-filter-chips]';
  const liveSelector = options.liveSelector || '[data-filter-live-region]';
  const chipsEl = resolveDomQueryElement(chipsSelector, drawerRoot);
  const liveEl = resolveDomQueryElement(liveSelector, drawerRoot);

  const chipsClearAll = String(chipsEl?.dataset?.filterChipsClearAllLabel || '').trim();
  const chipsRemove = String(chipsEl?.dataset?.filterChipsRemoveLabel || '').trim();
  const filtersApplied = String(liveEl?.dataset?.filterLiveAppliedMessage || '').trim();
  const filterRemoved = String(liveEl?.dataset?.filterLiveRemovedMessage || '').trim();
  const filtersCleared = String(liveEl?.dataset?.filterLiveClearedMessage || '').trim();

  return {
    ...(chipsClearAll !== '' ? { chipsClearAll } : {}),
    ...(chipsRemove !== '' ? { chipsRemove } : {}),
    ...(filtersApplied !== '' ? { filtersApplied } : {}),
    ...(filterRemoved !== '' ? { filterRemoved } : {}),
    ...(filtersCleared !== '' ? { filtersCleared } : {})
  };
};

const normalizeListMode = (value) => {
  const mode = String(value || '').trim().toLowerCase();
  if (mode === 'drawer' || mode === 'search-only' || mode === 'none') {
    return mode;
  }
  return 'drawer';
};

/**
 * Standardized list-page bootstrap:
 * - builds grid filters from schema (or uses explicit filters)
 * - initializes server grid
 * - optionally wires drawer/chip filter experience
 */
export function initStandardListPage(options = {}) {
  const gridOptions = options?.grid && typeof options.grid === 'object'
    ? options.grid
    : {};
  const filterOptions = options?.filters && typeof options.filters === 'object'
    ? options.filters
    : {};
  const hooks = options?.hooks && typeof options.hooks === 'object'
    ? options.hooks
    : {};

  const mode = normalizeListMode(filterOptions.mode);
  const filterSchema = Array.isArray(gridOptions.filterSchema) ? gridOptions.filterSchema : [];
  const extraFilters = Array.isArray(gridOptions.extraFilters) ? gridOptions.extraFilters : [];
  const resolvedFilters = Array.isArray(gridOptions.filters)
    ? gridOptions.filters
    : [...gridFiltersFromSchema(filterSchema), ...extraFilters];

  const hasExplicitBindingMode = Object.prototype.hasOwnProperty.call(gridOptions, 'filterBindingMode');
  const defaultBindingMode = mode === 'drawer' ? 'manual' : 'live';

  const createOptions = {
    ...gridOptions,
    filters: resolvedFilters,
    filterBindingMode: hasExplicitBindingMode ? gridOptions.filterBindingMode : defaultBindingMode
  };
  delete createOptions.filterSchema;
  delete createOptions.extraFilters;

  const gridConfig = createServerGrid(createOptions);

  if (typeof hooks.onGridReady === 'function') {
    hooks.onGridReady({ gridConfig, mode });
  }

  let filterExperience = null;
  if (mode === 'drawer' && gridConfig) {
    const chipMeta = filterOptions.chipMeta && typeof filterOptions.chipMeta === 'object'
      ? filterOptions.chipMeta
      : {};
    const drawerConfig = filterOptions.drawer && typeof filterOptions.drawer === 'object'
      ? filterOptions.drawer
      : {};
    const liveConfig = filterOptions.live && typeof filterOptions.live === 'object'
      ? filterOptions.live
      : {};
    const chipUiConfig = filterOptions.chips && typeof filterOptions.chips === 'object'
      ? filterOptions.chips
      : {};
    const drawerRoot = filterOptions.drawerRoot || document;
    const domLabels = readDomLabelDefaults({
      drawerRoot,
      chipsSelector: chipUiConfig.container || '[data-active-filter-chips]',
      liveSelector: liveConfig.regionSelector || '[data-filter-live-region]'
    });
    const labels = {
      ...DEFAULT_LIST_FILTER_LABELS,
      ...domLabels,
      ...(filterOptions.labels && typeof filterOptions.labels === 'object' ? filterOptions.labels : {})
    };
    const preserveFilterParamsOnReset = Array.isArray(filterOptions.preserveFilterParamsOnReset)
      ? filterOptions.preserveFilterParamsOnReset
      : [];
    const watchInputs = Array.isArray(filterOptions.watchInputs)
      ? filterOptions.watchInputs
      : [];
    const clearMetaOptions = filterOptions.clearMetaOptions && typeof filterOptions.clearMetaOptions === 'object'
      ? filterOptions.clearMetaOptions
      : {};

    const buildChips = typeof filterOptions.buildChips === 'function'
      ? filterOptions.buildChips
      : (state = {}) => buildChipsFromMeta(state, chipMeta);
    const removeChip = typeof filterOptions.removeChip === 'function'
      ? filterOptions.removeChip
      : (chip, state = {}) => removeChipFromMetaState(chip, state, chipMeta);
    const clearAllState = typeof filterOptions.clearAllState === 'function'
      ? filterOptions.clearAllState
      : (state = {}) => clearMetaState(state, chipMeta, clearMetaOptions);

    filterExperience = initListFilterExperience({
      gridConfig,
      drawerRoot,
      drawer: drawerConfig,
      live: {
        regionSelector: '[data-filter-live-region]',
        appliedMessage: labels.filtersApplied,
        removedMessage: labels.filterRemoved,
        clearedMessage: labels.filtersCleared,
        ...liveConfig
      },
      chips: {
        container: '[data-active-filter-chips]',
        clearAllLabel: labels.chipsClearAll,
        removeButtonLabel: labels.chipsRemove,
        ...chipUiConfig
      },
      preserveFilterParamsOnReset,
      buildChips,
      removeChip,
      clearAllState,
      watchInputs
    });
  }

  if (typeof hooks.onFilterExperienceReady === 'function') {
    hooks.onFilterExperienceReady({ gridConfig, filterExperience, mode });
  }

  return {
    gridConfig,
    filterExperience
  };
}
