import {
  CollectionState,
  FiltersMap,
  SelectState,
  TaskState,
  TypedEmitter,
  withoutDefaults,
  WixPatternsContainer,
} from '@wix/bex-core';
import { action, computed, makeObservable, reaction, observable } from 'mobx';
import { ToolbarCollectionState } from './ToolbarCollectionState';
import { Column } from '../model';
import type { CustomColumnsDragAndDrop } from '../components/CustomColumns/CustomColumnsDragAndDrop';
import { CustomColumnsHistoryState } from './CustomColumnsHistoryState';
import { indexOfCompareFn } from './indexOfCompareFn';
import { EventEmitter } from 'events';
import { cairoCustomizeColumnsInSidePanel } from '@wix/bex-core/bi';

type StoredColumn =
  | { id?: string; name?: string; isSelected?: boolean }
  | null
  | undefined;

const asStoredColumnsOrNull = (customColumnsResult: unknown) =>
  Array.isArray(customColumnsResult)
    ? (customColumnsResult as StoredColumn[])
    : null;

export interface CustomColumnsStateParams<T, F extends FiltersMap> {
  readonly table: ToolbarCollectionState<T, F>;
  readonly container: WixPatternsContainer;
  readonly dragAndDrop?: typeof CustomColumnsDragAndDrop;
  readonly modalsContainerRef: {
    current: HTMLElement | null | undefined;
  };
}

export type CustomColumnsActionName = 'visibility' | 'reorder';

export class CustomColumnsState<T, F extends FiltersMap> {
  readonly table;
  readonly container;

  readonly columnsCollection;

  readonly dragAndDrop;

  readonly customColumnsHistory;

  onColumnClick?: (column: Column, checked: boolean) => unknown;

  _didFirstColumnsMerge?: boolean;
  _performedCustomColumnsActions: Map<CustomColumnsActionName, boolean> =
    new Map();

  readonly initTask = new TaskState();

  _touched = false;

  searchValue = '';

  readonly select = new SelectState<{ id: string }>({
    keyGetter: (column) => column.id,
  });

  readonly events = new EventEmitter() as TypedEmitter<{
    beforeInitialFetch: () => void;
  }>;

  constructor(params: CustomColumnsStateParams<T, F>) {
    this.table = params.table;
    this.container = params.container;

    this.columnsCollection = new CollectionState({
      ...this.container,
      events: {},
      queryName: [
        this.container.environment.componentId,
        this.table.collection.queryName,
        'customColumns',
      ]
        .filter(Boolean)
        .join('/'),
      fetchData: async () => {
        const { filteredColumns } = this;

        const items = filteredColumns.filter(
          (column) => column.hiddenFromCustomColumnsSelection !== true,
        );

        return {
          items,
        };
      },
      filters: {},
      itemKey: (item) => item.id,
      itemName: (item) => item.name,
    });

    if (params.dragAndDrop) {
      const { CustomColumnsDragAndDropState } = params.dragAndDrop;
      this.dragAndDrop = new CustomColumnsDragAndDropState({
        customColumns: this,
        components: params.dragAndDrop,
        modalsContainerRef: params.modalsContainerRef,
      });
    }

    this.customColumnsHistory = this.table.collection.persistQueryToUrl
      ? new CustomColumnsHistoryState({
          customColumns: this,
          history: params.container.history,
        })
      : null;

    makeObservable(this, {
      searchValue: observable,
      persistCustomColumns: action.bound,
      setSearchValue: action.bound,
      toSelectedColumns: computed,
      orderedColumns: computed,
      selectedOrderedColumns: computed,
      filteredColumns: computed,
      init: action,
    });
  }

  get orderedColumns(): Column[] {
    const { dragAndDrop, table } = this;
    const { columns, customColumnsState } = table;

    if (customColumnsState?.dragAndDrop && dragAndDrop) {
      return dragAndDrop.orderedColumns;
    }

    return columns;
  }

  get selectedOrderedColumns() {
    const {
      orderedColumns,
      table: {
        selectedColumns: { value: selectedColumns },
      },
    } = this;

    return orderedColumns.filter((column) =>
      selectedColumns.some(
        (selectedColumn) =>
          selectedColumn.id === column.id && selectedColumn.isSelected,
      ),
    );
  }

  async persistCustomColumns() {
    const { table } = this;
    const { dataCapsule, viewsProps } = table;

    // if views enabled columns persists inside the view instead of data capsule
    if (viewsProps) {
      return;
    }

    this._touched = true;
    await dataCapsule.setItem('custom-columns', table.selectedColumns.value);
  }

  private registerSelectedColumnChange() {
    const { table } = this;
    table.selectedColumns.events.on('refresh', this.persistCustomColumns);

    return () => {
      table.selectedColumns.events.off('refresh', this.persistCustomColumns);
    };
  }

  mergeNewColumns() {
    const {
      table: { columns, selectedColumns, _toStoredColumn, customColumnsState },
      _didFirstColumnsMerge,
    } = this;

    const defaultHidden =
      !_didFirstColumnsMerge && !!selectedColumns.value.length;

    const mergeTargetSelectedColumns = [...selectedColumns.value];

    if (customColumnsState?.dragAndDrop == null) {
      return [
        ...mergeTargetSelectedColumns.filter((s) =>
          columns.every((c) => s.id !== c.id),
        ),
        ...columns.map((c) => {
          const existing = selectedColumns.value.find((s) => s.id === c.id);
          if (existing == null) {
            return _toStoredColumn(c);
          }

          return {
            ...existing,
            isSelected:
              c.hideable === false ||
              c.hiddenFromCustomColumnsSelection ||
              existing.isSelected,
          };
        }),
      ];
    }

    const diffColumns = columns
      .filter((c) => selectedColumns.value.every((s) => s.id !== c.id))
      .map((c) =>
        _toStoredColumn({
          ...c,
          defaultHidden: defaultHidden || c.defaultHidden,
        }),
      );

    // place new columns according to their order in the original columns prop
    diffColumns.forEach((d) => {
      const originalIndex = columns.findIndex((c) => c.id === d.id);
      const afterId = originalIndex > 0 ? columns[originalIndex - 1].id : null;
      const afterIndex =
        afterId != null
          ? mergeTargetSelectedColumns.findIndex((c) => c.id === afterId) + 1
          : originalIndex === 0
          ? 0
          : mergeTargetSelectedColumns.length;
      mergeTargetSelectedColumns.splice(afterIndex, 0, d);
    });

    this._didFirstColumnsMerge = true;
    return mergeTargetSelectedColumns;
  }

  onColumnCheckboxToggle = action((column: Column) => {
    const { select, onColumnClick } = this;
    select.toggle(column);
    onColumnClick?.(column, select.isChecked(column.id));
  });

  getStickyColumnsCount({ stickyColumns }: { stickyColumns: number }) {
    const {
      dragAndDrop,
      table: {
        selectedColumns: { value: selectedColumns },
      },
    } = this;

    if (dragAndDrop == null) {
      return stickyColumns;
    }

    const { reorderDisabled: reorderDisabledColumns } =
      dragAndDrop.orderedColumnsByReorderDisabled;

    if (!reorderDisabledColumns.length) {
      return stickyColumns;
    }

    return dragAndDrop.orderedColumnsByReorderDisabled.reorderDisabled.filter(
      (column) =>
        selectedColumns.some(
          (selectedColumn) =>
            selectedColumn.id === column.id && selectedColumn.isSelected,
        ),
    ).length;
  }

  get toSelectedColumns() {
    const {
      select,
      table: {
        selectedColumns: { value },
      },
    } = this;

    return value.map(({ id }) => ({
      id,
      isSelected: select.isChecked(id),
    }));
  }

  get selectionChanged() {
    const {
      toSelectedColumns,
      table: {
        selectedColumns: { value },
      },
    } = this;

    if (value.length !== toSelectedColumns.length) {
      return true;
    }

    for (let i = 0; i < value.length; i++) {
      const a = value[i];
      const b = toSelectedColumns[i];

      if (a.id !== b.id || a.isSelected !== b.isSelected) {
        return true;
      }
    }

    return false;
  }

  init() {
    const { table } = this;
    table.customColumnsStateInitialized = true;

    const disposers = [
      reaction(
        () => this.table.selectedColumns.value,
        async (selectedColumns) => {
          const { select, columnsCollection } = this;

          select.clear();
          select.forceSetMany(
            selectedColumns.map(({ id, isSelected }) => ({
              value: { id },
              status: isSelected ? select.finalStatus : select.startStatus,
            })),
          );
          await columnsCollection.refreshAllPages();
        },
        {
          fireImmediately: true,
        },
      ),
      reaction(
        () => this.select.selectedValues,
        () => {
          const {
            table: { selectedColumns },
          } = this;

          if (!this.selectionChanged) {
            return;
          }

          selectedColumns.setValue(this.toSelectedColumns, {
            emitEvents: ['change', 'beforeRefresh', 'refresh'],
          });
        },
      ),

      this.registerSelectedColumnChange(),
      this.columnsCollection.init(),
      this.dragAndDrop?.init(),
      this.customColumnsHistory?.init(),
    ];

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

  async _getStoredCustomColumns() {
    const { dataCapsule } = this.table;
    try {
      const storedValue = await asStoredColumnsOrNull(
        await dataCapsule.getItem('custom-columns'),
      );
      this._touched = storedValue != null;
      return storedValue;
    } catch (e) {
      if (e instanceof Error && e.message === 'Key was not found in capsule') {
        return null;
      }

      throw e;
    }
  }

  _tableInit() {
    const {
      initTask,
      table: {
        multi: { collections },
      },
    } = this;
    initTask.runOnce(async () => {
      try {
        const beforeInitialFetchListeners =
          this.events.listeners('beforeInitialFetch');
        await Promise.all(beforeInitialFetchListeners.map((l) => l()));
        await this._fetchFromStorage();
      } catch (e) {
        console.error(e);
      }
    });

    collections.forEach((collection) => {
      if (collection.query.refreshOnColumnsChange) {
        collection.hooks.addListener(
          'beforeInitialFetch',
          () => initTask.status.promise,
        );
      }
    });
  }

  async _fetchFromStorage() {
    const {
      table: {
        viewsProps,
        columns,
        selectedColumns,
        customColumnsState,
        _toStoredColumn,
      },
    } = this;

    if (viewsProps) {
      return;
    }

    if (customColumnsState?.customColumnsHistory?._selectedColumnsFromUrl) {
      return;
    }

    const storedCustomColumns = await this._getStoredCustomColumns();

    if (storedCustomColumns == null) {
      return;
    }

    const selectedColumnsFromStorage = columns.map((tableColumn) => {
      const existing = storedCustomColumns.find(
        (c) => c?.id === tableColumn.id,
      );

      // If the column is not found in the stored columns, it means it was added after the last visit, so its "isSelected" state should be calculated based on the column prop passed via code
      if (existing == null) {
        return _toStoredColumn(tableColumn);
      }

      // If the column is found in the stored columns, it means it was already configured by the user, so its  "isSelected" state should be taken from the stored columns
      const { id, name, hideable, hiddenFromCustomColumnsSelection } =
        tableColumn;

      return {
        id,
        name,
        isSelected:
          storedCustomColumns.some(
            (c) => c?.id === id && c?.isSelected !== false,
          ) ||
          !hideable ||
          hiddenFromCustomColumnsSelection === true,
      };
    });

    selectedColumns.setValue(
      customColumnsState?.dragAndDrop
        ? selectedColumnsFromStorage.sort(
            indexOfCompareFn(storedCustomColumns, (e) => e?.id),
          )
        : selectedColumnsFromStorage,
    );
  }

  customizeColumnsInSidePanelBi({
    actionName,
  }: {
    actionName: CustomColumnsActionName;
  }) {
    if (this._performedCustomColumnsActions.get(actionName)) {
      return;
    }
    this._performedCustomColumnsActions.set(actionName, true);
    this.table.reportBi(
      withoutDefaults(cairoCustomizeColumnsInSidePanel)({
        actionName,
      }),
    );
  }

  get filteredColumns(): Column[] {
    const { orderedColumns, searchValue } = this;

    if (!searchValue.trim()) {
      return orderedColumns;
    }

    const searchLower = searchValue.toLowerCase();
    return orderedColumns.filter((column) => {
      const name =
        column.name ||
        (typeof column.title === 'string' ? column.title : column.id);
      return name.toLowerCase().includes(searchLower);
    });
  }

  setSearchValue = action((value: string) => {
    this.searchValue = value;
    this.columnsCollection.refreshAllPages();
  });
}
