import {
  addEventListener,
  CollectionOptimisticSequenceExecutor,
  FiltersMap,
  OptionalFiltersMap,
  QueryState,
  ReportBI,
  syncWithSharedQuery,
  TaskState,
  WixPatternsContainer,
} from '@wix/bex-core';
import { ToolbarCollectionState } from '../ToolbarCollectionState';
import { TableState } from '../TableState';
import { TableColumn } from '../../model';
import { NestedTableColumn } from './NestedTableColumn';
import { NestedTableNestedModeState } from './NestedTableNestedModeState';
import { NestedTableFlatModeState } from './NestedTableFlatModeState';
import { NestedCollectionFetchAllState } from './NestedCollectionFetchAllState';
import { NestedTableLevelDescriptor } from './NestedTableLevelDescriptor';
import { EagerLazyModeOptions } from './types';
import { reaction } from 'mobx';
import { ICollectionComponentState } from '../ICollectionComponentState';
import { NestedTableNodeStatePublicAPI } from './NestedTableNodeStatePublicAPI';
import { createNestedOptimisticActions } from './createNestedOptimisticActions';

export interface NestedTableStateBaseParams<
  C extends string,
  F extends FiltersMap = OptionalFiltersMap,
> {
  /**
   * If the total items in the server are less or equal to this threshold, all items will be fetched and rendered at once, in expanded state (referred to as "Eager" mode).
   * @default 1000
   */
  fetchAllLimit?: number;
  /**
   * @deprecated Use `fetchAllLimit` instead.
   */
  fetchAllThreshold?: number;
  /**
   * In "Eager" mode, items will be in expanded state. This threshold allows to use collapsed state if the total items in the server are greater than this threshold.
   * @default `fetchAllThreshold`
   */
  expandAllThreshold?: number;
  /**
   * Columns to be rendered in the nested table. Same as [TableColumn](./?path=/story/common-types--tablecolumn) - just without rendering logic of cells (`render` prop will be defined for each level individually in the `levels` prop).
   * @overrideType [TableColumn](./?path=/story/common-types--tablecolumn)[]
   */
  columns: NestedTableColumn<C>[];
  /**
   * Main column of the nested table.
   * An object with the following properties:
   *  - `title`: The title of the column. @default ''
   *  - `width`: The width of the column. Can be a number (in pixels) or a string (percentage). @default '326px'
   */
  mainColumn?: {
    title?: string;
    width?: number | string;
  };
  /**
   * Configuration of the nested table levels. Use one of the helper function to create the levels configuration:
   * - [createNestedTableLevel](./?path=/story/base-components-collections-nestedtable--createnestedtablelevel) - for cases where each level if of a different entity type.
   * - [createNestedTableSingleEntityLevels](./?path=/story/base-components-collections-nestedtable--createnestedtablesingleentitylevels) - for cases where all levels are of the same entity type.
   * @overrideType [NestedTableLevel](./?path=/story/base-components-collections-nestedtable--nestedtablelevel)[]
   */
  levels: Omit<NestedTableLevelDescriptor<C, any, any>, 'depth'>[];

  filters?: F;
}

export interface NestedTableStateParams<
  C extends string,
  F extends FiltersMap = OptionalFiltersMap,
> extends NestedTableStateBaseParams<C, F> {
  container: WixPatternsContainer;
}

export interface NestedTableStatePublicAPI<C extends string> {
  /**
   * Underlying [ToolbarCollectionState](./?path=/story/common-state--toolbarcollectionstate) instance
   * @overrideType [ToolbarCollectionState](./?path=/story/common-state--toolbarcollectionstate)
   */
  readonly toolbar: ToolbarCollectionState<unknown, {}>;
  /**
   * Underlying [TableState](./?path=/story/base-components-collections-table-tablestate--tablestate) instance
   * @overrideType [TableState](./?path=/story/base-components-collections-table-tablestate--tablestate)
   */
  readonly tableState: TableState<unknown, {}>;

  /**
   * Get the node this item is the parent of. i.e. this node's collection items' parent id is the `key`
   * @param key
   * @overrideType (key: string) => [NestedTableNodeState](./?path=/story/base-components-collections-nestedtable--nestedtablenodestate) | undefined
   */
  getNode<TD, FD extends FiltersMap>(
    key: string,
  ): NestedTableNodeStatePublicAPI<C, TD, FD> | undefined;

  /**
   * Get the root node of the nested table, this node's collection items' parent id is `null`, the items are the top level items
   */
  getRootNode<TD, FD extends FiltersMap>(): NestedTableNodeStatePublicAPI<
    C,
    TD,
    FD
  >;

  /**
   * Get all the nodes in the table
   */
  getAllNodes<TD, FD extends FiltersMap>(): NestedTableNodeStatePublicAPI<
    C,
    TD,
    FD
  >[];

  /**
   * An API to refresh all items in the nested table.
   */
  readonly refreshAll: () => void;
}

export class NestedTableState<
  C extends string = string,
  F extends FiltersMap = OptionalFiltersMap,
> implements ICollectionComponentState, NestedTableStatePublicAPI<C>
{
  readonly container;
  readonly reportBi: ReportBI;
  readonly toolbar;
  readonly tableState;
  readonly mainColumn;
  readonly options: EagerLazyModeOptions;

  readonly initTask = new TaskState();

  readonly levels: NestedTableLevelDescriptor<C, any, any>[];

  readonly rootNodeCollection;
  readonly query;

  readonly nested: NestedTableNestedModeState<C>;
  readonly flat: NestedTableFlatModeState<C>;

  readonly sequences = new Map<string, CollectionOptimisticSequenceExecutor>();

  constructor(params: NestedTableStateParams<C, F>) {
    this.container = params.container;

    this.query = new QueryState({
      filters: params.filters || {},
    });

    this.levels = params.levels.map((level, i) => ({
      ...level,
      depth: i,
    }));

    const fetchAllLimit = params.fetchAllLimit ?? params.fetchAllThreshold;

    this.options = {
      fetchAllThreshold: fetchAllLimit ?? 1000,
      expandAllThreshold: params.expandAllThreshold ?? fetchAllLimit ?? 1000,
    };

    this.mainColumn = params.mainColumn;
    const rootLevelDescriptor = this.levels[0];

    if (rootLevelDescriptor == null) {
      throw new Error('NestedTableState must have at least one level');
    }

    const nestedOptimisticActionsState = {
      parentId: '',
      sequences: this.sequences,
      query: this.query,
      levelDescriptor: rootLevelDescriptor,
    };

    this.rootNodeCollection = rootLevelDescriptor.createCollection(
      nestedOptimisticActionsState,
    );

    createNestedOptimisticActions(
      this.container,
      this.rootNodeCollection.collection,
      nestedOptimisticActionsState,
    );

    const { collection } = this.rootNodeCollection;

    syncWithSharedQuery(collection.query, this.query);

    this.reportBi = (reportProps) => {
      const { isFetchAllAborted } = this;
      this.container.reportBi({
        ...reportProps,
        params: {
          ...reportProps.params,
          ...(isFetchAllAborted != null && {
            componentStatus: isFetchAllAborted
              ? 'All items collapsed'
              : 'All items expanded',
          }),
        },
      });
    };

    this.toolbar = new ToolbarCollectionState({
      collection: this.rootNodeCollection.collection,
      container: params.container,
      componentType: 'NestedTable',
      reportBi: this.reportBi,
    });

    this.toolbar.setNewColumns(
      params.columns.map((column) => ({
        ...column,
        render: () => null,
      })) as TableColumn<unknown>[],
    );

    this.tableState = new TableState({
      collection: this.rootNodeCollection.collection,
      container: params.container,
      toolbar: this.toolbar,
    });

    this.nested = new NestedTableNestedModeState({
      wrapper: this,
      reportBi: this.reportBi,
    });

    this.flat = new NestedTableFlatModeState({
      wrapper: this,
    });
  }

  get nestedMode() {
    return this.nested;
  }

  get flatMode() {
    return this.flat;
  }

  getNode<TD, FD extends FiltersMap>(key: string) {
    return this.nested.getNode<TD, FD>(key);
  }

  getRootNode<TD, FD extends FiltersMap>(): NestedTableNodeStatePublicAPI<
    C,
    TD,
    FD
  > {
    return this.nested.root;
  }

  /**
   * Get all the nodes in the table
   */
  getAllNodes<TD, FD extends FiltersMap>() {
    return this.nested.getAllNodes<TD, FD>();
  }

  get isFetchAllAborted() {
    return this.nested.isFetchAllAborted;
  }

  get expandAll() {
    return this.nested.expandAll;
  }

  get nestedCollection() {
    return this.nested.nestedCollection;
  }

  _attemptFetchAllAndPopulateCache() {
    const initTask = new TaskState();
    return {
      initTask,
      init: (
        { invalidate }: { invalidate: boolean } = { invalidate: false },
      ) => {
        initTask.runOnce(async () => {
          const fetchAllState = new NestedCollectionFetchAllState({
            options: this.options,
            query: this.nested.query,
            nestedCollectionState: this.nested.nestedCollection,
          });

          const disposers = [fetchAllState.init({ invalidate })];

          try {
            await fetchAllState.initTask.status.promise;
          } finally {
            disposers.forEach((d) => d());
          }
        });

        return () => {};
      },
    };
  }

  refreshAll({ invalidate = true }: { invalidate?: boolean } = {}) {
    const initTask = this.isFlatMode
      ? this.flat.initTask
      : this.nested.initTask;
    initTask.reset();
    initTask.runOnce(async () => {
      const fetchAll = this._attemptFetchAllAndPopulateCache();
      fetchAll.init({ invalidate });
      await fetchAll.initTask.status.promise;

      await (this.isFlatMode
        ? this.flatMode._refreshAll()
        : this.nestedMode.root._refreshAll());
      this.rootNodeCollection.collection.query.events.emit('refresh');
    });
  }

  get isFlatMode() {
    return !!this.query.search.value;
  }

  _addFilterListeners() {
    return Object.values(this.query.filtersEntries).map(([_, filter]) => {
      return addEventListener(filter.events, 'refresh', () => {
        this.refreshAll({
          invalidate: false,
        });
      });
    });
  }

  init() {
    const { initTask, tableState, query } = this;

    const disposers = [
      query.init({ skipFilterListeners: true }),
      ...this._addFilterListeners(),
      reaction(
        () => this.isFlatMode,
        (isFlatMode) => {
          const {
            query: { _fields },
          } = this;
          if (isFlatMode) {
            _fields.add('breadcrumbs');
          } else {
            _fields.delete('breadcrumbs');
          }
        },
        { fireImmediately: true },
      ),
      tableState.init({
        featuresInitializers: [],
        skipFilterListeners: true,
      }),
    ];

    initTask.runOnce(async () => {
      tableState.initTask.runOnce();
      await tableState.initTask.status.promise;
    });

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