import {
  action,
  computed,
  makeObservable,
  observable,
  reaction,
  runInAction,
} from 'mobx';
import type {
  ModalState,
  QueryPendingState,
  SortOrder,
  WixPatternsContainer,
} from '@wix/bex-core';
import {
  CollectionBIReporter,
  CollectionState,
  CustomField,
  FedopsReporter,
  Filter,
  FiltersMap,
  idArrayFilter,
  MultiCollectionSupport,
  ReportBI,
  TaskState,
  ValueBox,
} from '@wix/bex-core';
import {
  Column,
  FilterProps,
  PrivateView,
  TableColumn,
  ViewType,
} from '../model';

export { ViewType };
import { CustomColumnsProps } from '../components/CustomColumns';
import type {
  LiveRegionRefType,
  LiveRegionState,
  TableProps,
} from '@wix/design-system';
import type { AppliedFiltersTagListState } from '../components/AppliedFiltersTagList/AppliedFiltersTagListState';
import { ToolbarFeatureInitializer } from './toolbarFeatureInitializer';
import { simplifyColumn, simplifyColumns } from './simplifyColumns';
import { indexOfCompareFn } from './indexOfCompareFn';
import type { CollectionViewsState } from './CollectionViewsState';
import { ReactElement } from 'react';
import { ToolbarBIReporter } from './ToolbarBIReporter';
import { DragAndDropState } from '../components/DragAndDrop';
import { CollectionViewsDropdownProps } from '../components/CollectionViewsDropdown';
import { ActionCellProps } from '../components/ActionCell';
import { ActionCellAPI } from './ItemActionsState';
import { CustomColumnsState } from './CustomColumnsState';
import { SidePanelsState } from './SidePanelsState';
import { CollectionToolbarFiltersState } from '../components/CollectionToolbarFilters/CollectionToolbarFiltersState';
import { MultiLevelSortingState } from './MultiLevelSortingState';
import { RectState } from './RectState';
import { BulkActionToolbarState } from './BulkActionToolbarState';
import { addEventListener } from '@wix/bex-core/util';
import { simplifyExtendedColumns } from './simplifyExtendedColumns';
import type { AddApplyFiltersButton } from '../components/FiltersPanel/AddApplyFiltersButton';
import { TabsFilterState } from '../components/TabsFilter/TabsFilterState';
import { CollectionDataExtensionState } from '../components/DataExtension';
import { TagsCollectionState } from './TagsCollectionState';
import {
  AdditionalFeaturesAvailabilityBIParams,
  FeaturesAvailabilityBIParams,
} from './FeaturesAvailabilityBIParams';
import { ExtensionMenuItemsState } from './ExtensionMenuItemsState';
import { ToolbarState } from '../components/CollectionToolbars/ToolbarState';
import { MoreActionItemFactory } from '../components/MoreActions/MoreActionsBase';
import { ColumnSizesState } from './ColumnSizesState';

export type Component<C> = () => C;

export interface ToolbarCollectionStateParamsInitParams {
  featuresInitializers: (ToolbarFeatureInitializer | undefined)[];
  hasMultiLevelSorting?: boolean;
  skipFilterListeners?: boolean;
}

export interface LayoutState {
  viewType: ViewType;
}

export interface ToolbarCollectionStateParams<T, F extends FiltersMap> {
  readonly container: WixPatternsContainer;
  readonly collection: CollectionState<T, F>;
  readonly componentType: string;
  readonly reportBi?: ReportBI;
}

export class ToolbarCollectionState<T, F extends FiltersMap = FiltersMap> {
  readonly collection: CollectionState<T, F>;
  readonly selectedColumns = idArrayFilter<{
    id: string;
    isSelected: boolean;
  }>();
  readonly initTask = new TaskState();
  readonly fedopsReporter;
  readonly dataCapsule;
  readonly toolbar;
  readonly container: WixPatternsContainer;
  readonly namespace;
  readonly reportBi: ReportBI;
  readonly featuredComponents = {
    moreFiltersButton: new ValueBox<Component<{}> | null>(null),
    appliedFiltersTagList: new ValueBox<{
      component: Component<{}>;
      state: AppliedFiltersTagListState<T, F>;
    } | null>(null),
    exportButton: new ValueBox<ReactElement | null>(null),
    exportModal: new ValueBox<ReactElement<{}> | null>(null),
    importButton: new ValueBox<ReactElement | null>(null),
    importModal: new ValueBox<ReactElement<{}> | null>(null),
  };
  readonly toolbarRect;
  readonly itemsContentRect;
  readonly panels;
  readonly bulk;
  readonly multi;
  readonly componentType;

  _syncedProps?: {
    useResponsiveLayout?: boolean;
  };

  _userColumns: Column[] = []; // columns propagated from user
  _featureColumns: Column[] = observable.array(); // columns propagated from installed features
  _extendedColumns: Column[] = []; // columns propagated from data-extension (can be merged with above?)
  internalScroll = undefined as boolean | undefined;
  customColumnsState: CustomColumnsState<T, F> | null = null;
  customColumnsStateInitialized = false;
  columnSizesState: ColumnSizesState | null = null;
  multiLevelSorting: MultiLevelSortingState<T, F> | null = null;
  liveRegionRef: { current: null | LiveRegionRefType } = {
    current: null,
  };
  collectionDataExtension: CollectionDataExtensionState<T, F> | null = null;
  menuItemExtensions = new Map<string, ExtensionMenuItemsState>();
  tagsCollectionState: TagsCollectionState<T, F> | null = null;
  toolbarFiltersState: CollectionToolbarFiltersState<T, F> | null = null;
  toolbarFiltersStateInitialized = false;
  _dnd: DragAndDropState<T, F> | null = null;
  toolbarBIReporter: ToolbarBIReporter<T, F>;
  filtersProps: FilterProps<any>[] | null = null;
  viewsProps: CollectionViewsDropdownProps<F> | null | undefined = null;
  dragAndDropCategories: (keyof F)[][] | null | undefined = null;
  customColumnsProps: CustomColumnsProps | null | undefined = null;
  selectionProps = new ValueBox<{ showSelection?: boolean }>({});
  featuresAvailability: FeaturesAvailabilityBIParams = {};
  additionalFeaturesAvailability: AdditionalFeaturesAvailabilityBIParams = {};
  actionCell = new ValueBox<
    | undefined
    | ActionCellProps
    | ((item: T, index: number, api: ActionCellAPI<T, F>) => ActionCellProps)
  >(undefined);
  contentPadding = 24;
  contentPaddingPx = `${this.contentPadding}px`;

  scrollElementRect = observable<{
    height?: number | null;
    top?: number | null;
  }>({
    height: null,
    top: null,
  });
  scrollTopBeforeUnmount: number | null = null;
  minimizedHeaderHeight = 0;
  tabsFilterProps?: {
    filter: Filter<any>;
  } = undefined;
  viewsState?: CollectionViewsState<T, F>;
  exportState?: ModalState;
  importState?: ModalState;
  pendingQuery?: QueryPendingState;
  AddApplyFiltersButton?: typeof AddApplyFiltersButton;
  tabs?: TabsFilterState<T, F>;
  layoutState?: LayoutState;

  // Per-layout in-memory scratchpad of uncommitted column edits.
  _layoutColumnsCache: Partial<
    Record<ViewType, { id: string; isSelected: boolean }[]>
  > = {};
  // Set by the coordinator on cache-hit so per-flow reactions skip the same transition.
  _cacheRestoredLayout?: ViewType;
  // Set by `_onSelectViewAndRefresh` when a view-select changes layout, so the coordinator
  // skips flush/restore — the view-select already loaded the correct columns.
  _skipNextLayoutCoordinator = false;

  _searchExpanded = false;

  forceRenderIndexes: number[] | undefined = undefined;

  constructor(params: ToolbarCollectionStateParams<T, F>) {
    this.collection = params.collection;
    this.container = params.container;
    this.componentType = params.componentType;

    this.toolbar = new ToolbarState(this);

    this.fedopsReporter = new FedopsReporter({
      componentName: this.componentType,
      collection: this.collection,
      internalMonitor: this.container.internalMonitor,
    });

    this.multi = new MultiCollectionSupport<F>({
      collections: [this.collection as CollectionState<unknown, F>],
    });

    this.itemsContentRect = new RectState(params);
    this.toolbarRect = new RectState(params);

    this.panels = new SidePanelsState({
      container: this.container,
      panelGroupName: this.collection.queryName,
    });

    this.toolbarBIReporter = new ToolbarBIReporter<T, F>({
      toolbar: this,
      collection: this.collection,
      reportBi: (ev) => {
        this.reportBi({
          ...ev,
          params: {
            ...this._commonDynamicBiParams(),
            ...ev.params,
          },
        });
      },
    });

    this.bulk = new BulkActionToolbarState({
      toolbarBIReporter: this.toolbarBIReporter,
      panels: this.panels,
      multi: this.multi,
      translate: this.container.translate,
      scrollToTop: () => {
        this.collection.scrollableContent?.scrollTo({
          top: 0,
          left: 0,
        });
      },
    });

    this.namespace = [
      this.container.environment.componentId,
      this.collection.queryName,
    ].join('.');

    const { dataCapsule } = this.container.createEssentials({
      dataCapsule: {
        namespace: this.namespace,
      },
    });

    this.dataCapsule = dataCapsule;

    const reportBiBase = params.reportBi ?? this.container.reportBi;

    this.reportBi = (ev) => {
      reportBiBase({
        ...ev,
        params: {
          ...this._commonDynamicBiParams(),
          ...ev.params,
        },
      });
    };

    this.columnSizesState = new ColumnSizesState({
      container: this.container,
      dataCapsule: this.dataCapsule,
      fqdn: this.namespace,
    });

    makeObservable(this, {
      forceRenderIndexes: observable.ref,
      _userColumns: observable.ref,
      _extendedColumns: observable.ref,
      columns: computed,
      customColumnsStateInitialized: observable.ref,
      viewsProps: observable.ref,
      dragAndDropCategories: observable.ref,
      customColumnsProps: observable.ref,
      filtersProps: observable.ref,
      shouldDisplayFiltersTags: computed,
      totalStickyContentHeight: computed,
      showInitialLoader: computed,
      showEmptyState: computed,
      showErrorState: computed,
      hasSubToolbarComponents: computed,
      containerRect: computed,
      setFilterTagsPropsIfChanged: action,
      _initDefaultSortableColumn: action,
      minimizedHeaderHeight: observable.ref,
      tabsFilterProps: observable.ref,
      toolbarFiltersStateInitialized: observable.ref,
      hasToolbarTabs: computed,
      itemsContentHeight: computed,
      setNewColumns: action,
      addFeatureColumn: action,
      setNewExtendedColumns: action,
      _commonDynamicBiParams: action,
      _adjustSortWithNewColumns: action,
      setToolbarFiltersStateInitialized: action,
      activeFiltersEntries: computed,
      _searchExpanded: observable.ref,
      showFieldTypeIcons: observable.ref,
    });
  }

  get containerRect() {
    const { itemsContentRect, toolbarRect, internalScroll } = this;

    if (internalScroll && itemsContentRect.rect.height) {
      return {
        ...itemsContentRect.rect,
      };
    }

    if (itemsContentRect.rect.left && toolbarRect.rect.left) {
      const top = toolbarRect.rect.bottom;
      const bottom = top + itemsContentRect.rect.height;
      const left = toolbarRect.rect.left;
      const right = left + toolbarRect.rect.width;
      return {
        left,
        right,
        top,
        bottom,
        height: bottom - top,
        width: right - left,
      };
    }

    return undefined;
  }

  get containerTop() {
    return this.containerRect?.top;
  }

  /**
   * How much height left for the content inside the viewport
   */
  get itemsContentHeight() {
    const {
      toolbarRect,
      scrollElementRect,
      contentPadding,
      internalScroll,
      itemsContentRect,
    } = this;

    if (internalScroll && itemsContentRect.rect.height) {
      return itemsContentRect.rect.height;
    }

    if (
      toolbarRect.rect.height == null ||
      toolbarRect.rect.top == null ||
      scrollElementRect.height == null ||
      scrollElementRect.top == null
    ) {
      return 0;
    }

    if (process.env.NODE_ENV === 'test') {
      return 796;
    }

    const toolbarOffsetTop = toolbarRect.rect.top - scrollElementRect.top;

    return (
      scrollElementRect.height -
      toolbarOffsetTop -
      toolbarRect.rect.height -
      contentPadding
    );
  }

  get totalStickyContentHeight() {
    const { toolbarRect, minimizedHeaderHeight } = this;
    if (toolbarRect.rect.height == null) {
      return null;
    }

    return toolbarRect.rect.height + minimizedHeaderHeight;
  }

  get sidePanel() {
    return this.panels.sidePanel;
  }

  get sidePanelComponents() {
    return this.panels.sidePanelComponents;
  }

  get showInitialLoader() {
    const { multi, initTask } = this;

    return (
      multi.showInitialLoader ||
      initTask.status.isLoading ||
      initTask.status.isIdle
    );
  }

  get showEmptyState() {
    const {
      showInitialLoader,
      multi: { showRefreshLoader, showEmptyState },
    } = this;
    return showEmptyState && !showInitialLoader && !showRefreshLoader;
  }

  get errorStatus() {
    const { initTask, multi } = this;
    return initTask.errorStatus ?? multi.errorStatus;
  }

  get showErrorState() {
    const {
      showInitialLoader,
      multi: { showRefreshLoader, showErrorState },
      initTask,
    } = this;

    return (
      (showErrorState || initTask.status.isError) &&
      !showInitialLoader &&
      !showRefreshLoader
    );
  }

  get hasSubToolbarComponents() {
    const { appliedFiltersTagList } = this.featuredComponents;
    return appliedFiltersTagList.value != null;
  }

  get reorderModeState() {
    return this._dnd?.reorderModeState;
  }

  get shouldDisplayFiltersTags() {
    const { collectionDataExtension, featuredComponents } = this;

    if (collectionDataExtension?.initTask.status.isLoading) {
      return false;
    }

    return !!featuredComponents.appliedFiltersTagList.value?.state.tags.length;
  }

  get hasToolbarTabs() {
    return this.tabsFilterProps != null;
  }

  get isReorderModeToolbarVisible() {
    return this.reorderModeState?.isReorderModeToolbarVisible;
  }

  get isReorderModePending() {
    return this.reorderModeState?.isReorderModePending;
  }

  get columns(): Column[] {
    const { _userColumns, _featureColumns, _extendedColumns } = this;
    return [..._userColumns, ..._featureColumns, ..._extendedColumns];
  }

  /**
   * Whether to show field-type prefix icons across the collection UI.
   * Controlled by the `showFieldTypeIcons` prop on the collection component (Table, Grid, etc.).
   * @default false
   */
  showFieldTypeIcons = false;

  get selectedOrderedColumnsOrAll() {
    const { columns, customColumnsState, columnSizesState } = this;

    const baseColumns = customColumnsState
      ? customColumnsState.selectedOrderedColumns
      : columns;

    return columnSizesState?.mergeSizes(baseColumns) ?? baseColumns;
  }

  _commonDynamicBiParams(query = this.collection.result.originQuery) {
    const { collection, viewsState } = this;

    return {
      componentType: this.componentType,
      ...collection._commonDynamicBiParams(query),
      currentTab: this.getCurrentTabName(),
      ...(viewsState && { currentView: viewsState.views.currentView?.id }),
      ...(viewsState?.views?.currentView && {
        isPredefined: viewsState.views.currentView.isPreset,
      }),
      ...(viewsState?.views?.currentView?.filters && {
        numFiltersInView: this._numberFiltersInView(
          viewsState.views.currentView,
        ),
      }),
      visibleColumnsOrder: this.selectedOrderedColumnsOrAll
        .map((col) => col.id ?? col.title ?? col.name)
        .join(','),
      numColumns: this.selectedOrderedColumnsOrAll.length,
      numFiltersActive: collection.query.activeFiltersCount,
    };
  }

  _numberFiltersInView(view: PrivateView<F>) {
    let numFiltersInView = 0;

    if (view.filters) {
      for (const filterName of Object.keys(view.filters)) {
        const filter = view.filters[filterName];

        if (filter && filter.value) {
          if (Array.isArray(filter.value)) {
            numFiltersInView += filter.value.length;
          } else if (filter.value) {
            numFiltersInView += 1;
          }
        }
      }
    }

    return numFiltersInView;
  }

  getCurrentTabName() {
    const {
      tabsFilterProps,
      collection: {
        result: { originQuery: query },
      },
    } = this;

    const tabs =
      tabsFilterProps &&
      Object.entries(query.rawFilters)
        .filter(([key]) => key === tabsFilterProps.filter.name)
        .flatMap(([, value]) =>
          Array.isArray(value)
            ? value.map(tabsFilterProps.filter.itemKey)
            : value,
        );

    return tabs?.[0] as string;
  }

  getCollectionSnapshot = (): {
    [key: string]: string | number | undefined;
  } => {
    const {
      collection: {
        result: { originQuery },
      },
    } = this;

    return {
      viewShown: this.viewsState?.views.currentView?.id,
      tabShown: this.getCurrentTabName(),
      sortOrderApplied: originQuery.sort?.length
        ? JSON.stringify(originQuery.sort)
        : undefined,
      filtersApplied: JSON.stringify(originQuery.filtersKey),
      filteredListSizeShown: this.collection.result.total,
    };
  };

  getPendingFilter<A>(filter: A) {
    return this.pendingQuery?.getApplicableFilter(filter) ?? filter;
  }

  init({
    featuresInitializers,
    hasMultiLevelSorting,
    skipFilterListeners,
  }: ToolbarCollectionStateParamsInitParams) {
    const {
      collection,
      multi,
      initTask,
      panels,
      sidePanel,
      collectionDataExtension,
      tagsCollectionState,
      bulk,
      customColumnsState,
      viewsState,
      columnSizesState,
    } = this;

    const biReporters = multi.collections.map(
      (collection) =>
        new CollectionBIReporter({
          collection,
          reportBi: (ev) => {
            this.reportBi({
              ...ev,
              params: {
                ...collection._commonDynamicBiParams(),
                ...ev.params,
              },
            });
          },
        }),
    );

    // must call `initOptionalFeatures` before collection init because we want to allow features to customize the collection before collection.init
    const tableFeatures = featuresInitializers
      .filter((t): t is ToolbarFeatureInitializer => t != null)
      .map((tableFeatInit) => tableFeatInit(this));

    // should be initialized before collection.init() so the sort query will be updated before the time of the first request.
    this._initDefaultSortableColumn(hasMultiLevelSorting);

    const disposers = [
      // Must be registered before the per-flow layout reactions so the
      // `_cacheRestoredLayout` gate they read is set first (mobx fires
      // reactions in creation order).
      reaction(
        () => this.layoutState?.viewType,
        (next, prev) => {
          if (!next || !prev || next === prev) {
            return;
          }
          if (this._skipNextLayoutCoordinator) {
            this._skipNextLayoutCoordinator = false;
            this._cacheRestoredLayout = next;
            return;
          }
          this._flushColumnsToCache(prev);
          this._cacheRestoredLayout = this._tryRestoreColumnsFromCache(next)
            ? next
            : undefined;
        },
      ),
      collectionDataExtension?.init(), // dataExtensionState should be initialized and beforeInitialFetch promise listener registered before customColumnsState?._tableInit() because custom columns might use data extensions
      customColumnsState?._tableInit(),
      viewsState?.init(),
      columnSizesState?.init(),
      // register table feature first as they might register to the collection `beforeInitialFetch`
      ...tableFeatures.map((t) => t.init()),
      reaction(
        () => this.selectedOrderedColumnsOrAll,
        (columns) => {
          collection.query._columns.replace(columns.map((c) => c.id));
        },
        {
          fireImmediately: true,
        },
      ),
      reaction(
        () => collectionDataExtension?.dataExtension.availableCustomFields,
        (customFields) => {
          this.setNewExtendedColumns(customFields ?? []);
        },
      ),
      multi.init({ skipFilterListeners }),
      ...biReporters.map((b) => b.init()),
      panels.init(),
      sidePanel.init(),
      bulk.init(),
      tagsCollectionState?.init(),
      addEventListener(
        collection.bulkSelect.events,
        'openModal',
        this.bulk.modals.actionModal.modal.open,
      ),
      addEventListener(
        collection.bulkSelect.events,
        'closeModal',
        this.bulk.modals.actionModal.modal.close,
      ),
      addEventListener(this.collection.emitter, 'resultUpdated', () => {
        this.broadcast({
          message: this.container.translate(
            'cairo.a11y.liveRegion.resultsFound',
            { num: this.collection.result.total },
          ),
        });
      }),
    ];

    const startTime = performance.now();

    initTask.runOnce(async () => {
      this.toolbarBIReporter.loadStart();

      multi.initTask.runOnce();
      customColumnsState?.initTask.runOnce();

      tableFeatures.forEach(({ initTask }) => {
        if (initTask) {
          initTask.runOnce();
        }
      });

      try {
        await Promise.all([
          multi.initTask.status.promise,
          tagsCollectionState?.initTask.status.promise?.catch(() => null), // resilience to failed request
          collectionDataExtension?.initTask.status.promise?.catch(() => null), // resilience to failed request
          customColumnsState?.initTask.status.promise,
          columnSizesState?.initTask.status.promise,
          ...tableFeatures.map((t) => t.initTask?.status.promise),
        ]);

        this.toolbarBIReporter.loadEnd(startTime);
      } catch (e) {
        throw e;
      }
    });

    return () => {
      disposers.forEach((d) => d?.());
    };
  }

  setFilterTagsPropsIfChanged(filtersProps: FilterProps<any>[] | null) {
    if (this.filtersProps !== filtersProps) {
      this.filtersProps = filtersProps;
    }
  }

  _toStoredColumn = (column: Column) => {
    const { id, defaultHidden, hideable, hiddenFromCustomColumnsSelection } =
      column;

    return {
      id,
      isSelected:
        !hideable || hiddenFromCustomColumnsSelection || !defaultHidden,
    };
  };

  setNewColumns(tableColumns: TableColumn<T>[]) {
    this._userColumns = simplifyColumns(tableColumns);
  }

  addFeatureColumn(featureColumn: TableColumn<T>) {
    this._featureColumns.push(simplifyColumn(featureColumn));

    this._adjustSelectedColumns();
  }

  setNewExtendedColumns(
    fields: CustomField[] = [],
    { defaultHidden = false }: { defaultHidden?: boolean } = {},
  ) {
    if (
      !this.collectionDataExtension?.dataExtension.initTask.status.isSuccess
    ) {
      return;
    }

    const dataExtension = this.collectionDataExtension?.dataExtension;
    const getSchemaField = dataExtension
      ? (namespace: string, fieldId: string) => {
          const schemas = [
            dataExtension.userSchema,
            ...(dataExtension.appSchemas || []),
          ];
          const schema = schemas.find((s) => s?.namespace === namespace);
          return schema?.jsonSchema?.properties?.[fieldId];
        }
      : undefined;

    this._extendedColumns = simplifyExtendedColumns<T>({
      fields,
      toExtendedFields: this.collection.toExtendedFields,
      defaultHidden,
      getSchemaField,
    });

    this._adjustSelectedColumns();
  }

  broadcast(params: LiveRegionState) {
    this.liveRegionRef.current?.broadcast(params);
  }

  setToolbarFiltersStateInitialized(value: boolean) {
    this.toolbarFiltersStateInitialized = value;
  }

  updateColumnWidth(columnId: string, width: number) {
    this.columnSizesState?.updateColumnWidth(columnId, width);
  }

  _initDefaultSortableColumn(hasMultiLevelSorting = false) {
    const {
      columns,
      collection: { query },
    } = this;

    const defaultSortableColumns = columns.filter(
      (col): col is Column & { defaultSortOrder: SortOrder } =>
        col.sortable === true && col.id != null && col.defaultSortOrder != null,
    );

    if (defaultSortableColumns.length && !query.sort.value.length) {
      const sortQuery = defaultSortableColumns.map((column) => ({
        field: column.id,
        direction: column.defaultSortOrder,
      }));

      query.sort.setSortQuery(
        hasMultiLevelSorting ? sortQuery : sortQuery.slice(0, 1),
      );
    }
  }

  _adjustSelectedColumns = () => {
    const {
      selectedColumns,
      customColumnsState,
      customColumnsProps,
      customColumnsStateInitialized,
    } = this;

    // as long as consumer rendered `<CustomColumns/>` at least once, use `mergeNewColumns` strategy when re-rendering new columns prop
    if (customColumnsProps != null || customColumnsStateInitialized) {
      // if `customColumns` is not set yet - do nothing
      if (customColumnsState) {
        const { initTask } = customColumnsState;
        const mergedColumns = customColumnsState.mergeNewColumns();
        runInAction(() => {
          selectedColumns.defaultValue = this.columns.map((col) => {
            const { id, isSelected } = this._toStoredColumn(col);
            return { id, isSelected };
          });
        });
        if (initTask.status.isSuccess && customColumnsState._touched) {
          selectedColumns.refresh(mergedColumns);
        } else {
          selectedColumns.setValue(mergedColumns);
        }
      }
    } else {
      const allColumns = this.columns.map(({ id }) => ({
        id,
        isSelected: true,
      }));
      selectedColumns.defaultValue = allColumns;
      selectedColumns.setValue(allColumns);
    }
  };

  _flushColumnsToCache(layout: ViewType) {
    this._layoutColumnsCache[layout] = this.selectedColumns.value.map(
      ({ id, isSelected }) => ({ id, isSelected }),
    );
  }

  _tryRestoreColumnsFromCache(layout: ViewType): boolean {
    const cached = this._layoutColumnsCache[layout];
    if (!cached) {
      return false;
    }
    // Merge against current columns — mirrors `_fetchFromStorage`.
    const merged = this.columns.map((column) => {
      const cachedEntry = cached.find((c) => c.id === column.id);
      if (cachedEntry == null) {
        return this._toStoredColumn(column);
      }
      const { id, hideable, hiddenFromCustomColumnsSelection } = column;
      return {
        id,
        isSelected:
          cachedEntry.isSelected ||
          !hideable ||
          hiddenFromCustomColumnsSelection === true,
      };
    });
    this.selectedColumns.setValue(
      this.customColumnsState?.dragAndDrop
        ? merged.sort(indexOfCompareFn(cached, (e) => e?.id))
        : merged,
    );
    return true;
  }

  _clearColumnsCache() {
    this._layoutColumnsCache = {};
  }

  _adjustSortWithNewColumns(tableColumns: TableColumn<T>[]) {
    const {
      collection: { query },
    } = this;
    const tableColumnsIds = new Set(tableColumns.map((column) => column.id));

    query.sort.value
      .filter(({ field }) => !tableColumnsIds.has(field))
      .forEach((s) => {
        query.sort.value.remove(s);
      });
    query.sort.events.emit('change');
    this.collection.scheduleSort({ clearResult: true });
  }

  closeSidePanel = () => {
    this.sidePanel.close();
  };

  openFiltersPanel() {
    this.panels.openSidePanel('filters');
  }

  retryErrorState = action(() => {
    const { initTask, multi } = this;

    if (initTask.status.isError) {
      initTask.runOnce();
    } else {
      multi.retryErrorState();
    }
  });

  onSelectionStarted = () => {
    this.bulk._selectionStartTime = performance.now();
  };

  _onSelectAllCheckboxClick() {}

  get activeFiltersEntries() {
    return Array.from(
      this.multi.collections
        .flatMap(({ query }) =>
          query.activeFiltersEntries.filter(
            ([_, filter]) => filter !== query.search,
          ),
        )
        .reduce(
          (map, entry) => map.set(entry[0], entry),
          new Map<string, [string, Filter<unknown>]>(),
        )
        .values(),
    );
  }

  onSelectionChanged: Required<TableProps>['onSelectionChanged'] = action(
    (_selectedIds, change) => {
      if (change == null) {
        return;
      }

      const { panels, bulk } = this;

      panels.closeSidePanel();

      switch (change.type) {
        case 'ALL':
          bulk.toggleSelectAll({ location: 'Checkbox' });
          break;
        case 'NONE':
          bulk.deselectAll({ location: 'Checkbox' });
          break;
        case 'SINGLE_TOGGLE':
          if (change.value) {
            bulk.selectOne(change.id);
          } else {
            bulk.deselectOne(change.id);
          }
          break;
      }
    },
  );

  openCustomFieldModal = ({ customOrigin }: { customOrigin?: string } = {}) => {
    return this.collectionDataExtension?.customFieldsPanel.openCustomFieldModal(
      {
        origin: 'external',
        customOrigin,
      },
    );
  };

  openManageTagsModal = () => {
    return this.tagsCollectionState?.openManageTagsModal();
  };

  onAddItemActionComplete = async (items: T[]) => {
    await this._resetQueryAndAddItems(items);

    this.toolbarBIReporter.newItemCreationEnd(items);
  };

  _resetQueryAndAddItems = async (items: T[]) => {
    const { collection } = this;
    const { query } = collection;

    query.resetFilters();
    query.resetSorting();

    await collection.clearResultAndMoveToStart({ force: true });

    collection._optimisticActions?.createMany(items, {
      submit: async () => items,
    });

    collection.scrollableContent?.scrollTo?.({
      top: 0,
      behavior: 'smooth',
    });
  };

  getPageFeaturedActions(): MoreActionItemFactory[] {
    const { collectionDataExtension, tagsCollectionState } = this;
    return [collectionDataExtension, tagsCollectionState]
      .filter(
        (
          e,
        ): e is
          | CollectionDataExtensionState<T, F>
          | TagsCollectionState<T, F> => e != null,
      )
      .filter((e) => !e.props?.disableDefaultPageAction)
      .flatMap((e) => e.featuredPageActions);
  }
}
