import {
  CollectionState,
  FiltersMap,
  KeyedItem,
  MultiCollectionSupport,
  TaskState,
} from '@wix/bex-core';
import { NestedCollectionNodeState } from './NestedCollectionNodeState';
import {
  action,
  computed,
  createAtom,
  makeObservable,
  reaction,
  runInAction,
} from 'mobx';
import { TableColumn } from '../../model';
import { NestedTableNestedModeState } from './NestedTableNestedModeState';
import { ReactNode } from 'react';
import { NestedTableNodeStateBIReporter } from './NestedTableNodeStateBIReporter';
import { NestedTableLevelDescriptor } from './NestedTableLevelDescriptor';
import { NestedTableNodeStatePublicAPI } from './NestedTableNodeStatePublicAPI';
import { getNodeOrLeaf } from './getNodeOrLeaf';
import { NestedTableNodeStateOptimisticActions } from './NestedTableNodeStateOptimisticActions';

export interface NestedTableDataItem<
  C extends string,
  T,
  F extends FiltersMap,
> {
  state: NestedTableNodeState<C, T, F>;
  originalKeyedItem: KeyedItem<T>;
}

export interface NestedTableSkeletonItem<
  C extends string,
  T,
  F extends FiltersMap,
> {
  state: NestedTableNodeState<C, T, F>;
  originalKeyedItem: undefined;
}

export type NestedTableItem<C extends string, T, F extends FiltersMap> =
  | NestedTableDataItem<C, T, F>
  | NestedTableSkeletonItem<C, T, F>;

export type NestedTableKeyedItem<
  C extends string,
  T,
  F extends FiltersMap,
> = KeyedItem<NestedTableItem<C, T, F>>;

export type RenderMainColumn<T> = (item: T) => {
  title?: string;
  subtitle?: string;
  image?: ReactNode;
};

export type NestedTableNestColumns<C extends string, T> = Partial<
  Record<C, TableColumn<T>['render']>
>;

export interface NestedTableNodeStateParams<
  C extends string,
  T,
  F extends FiltersMap,
> {
  readonly nestedTable: NestedTableNestedModeState<C>;
  readonly node: NestedCollectionNodeState<T, F>;
  readonly levelDescriptor: NestedTableLevelDescriptor<C, T, F>;
  readonly parentNode: NestedTableNodeState<C, any, any> | null;
  readonly parent: KeyedItem<unknown> | null;
  readonly preRegister?: boolean;
}

export class NestedTableNodeState<C extends string, T, F extends FiltersMap>
  implements NestedTableNodeStatePublicAPI<C, T, F>
{
  readonly nestedTable;
  readonly node;

  readonly levelDescriptor;
  readonly parentNode;
  readonly parent;
  readonly bi: NestedTableNodeStateBIReporter<C, T, F>;

  readonly multi;

  _rowNum?: number;

  readonly initTask = new TaskState();

  readonly parentKeyToChildrenMap = new Map<
    string,
    NestedTableNodeState<C, any, any>
  >();

  readonly parentKeyToChildrenMapAtom = createAtom(
    'parentKeyToChildrenMapAtom',
  );

  constructor(params: NestedTableNodeStateParams<C, T, F>) {
    this.nestedTable = params.nestedTable;
    this.node = params.node;
    this.parentNode = params.parentNode;
    this.parent = params.parent;
    this.levelDescriptor = params.levelDescriptor;
    this.bi = new NestedTableNodeStateBIReporter(this);

    this.multi = new MultiCollectionSupport({
      collections: [this.node.collection],
    });

    makeObservable(this, {
      keyedItems: computed,
      _keyedItemsMap: computed,
      _allKeyedItemsMap: computed,
      _allKeyedItems: computed,
      _keyedItemsIfExpanded: computed,
      _isNotExpandableWhenEmpty: computed,
      init: action,
      createNode: action,
      _preRegisterAll: action,
      _toggleExpand: action,
      _notify: action,
      _notifyAll: action,
      deepestBranchDepth: computed,
      unprocessedMap: computed,
      getKeyedItem: action,
      hasExpandableItems: computed,
      _childrenCount: computed,
    });

    this._register();
  }

  get collection() {
    return this.node.collection;
  }

  get unprocessedMap() {
    const {
      collection: { result },
    } = this;
    const { _pages, itemKey, _pagesAtom } = result;
    _pagesAtom.reportObserved();
    return new Map(
      _pages.unprocessed.flatMap((page) =>
        page.map((item) => {
          const key = itemKey(item);
          return [key, item];
        }),
      ),
    );
  }

  get children() {
    return this.collection;
  }

  get optimisticActions() {
    return new NestedTableNodeStateOptimisticActions(this);
  }

  get flatModeCollection(): CollectionState<T, F> | undefined {
    return this.nestedTable.wrapper.flat.sections.find((section) =>
      section.levelDescriptors.includes(this.levelDescriptor),
    )?.collection;
  }

  get depth() {
    return this.parentNode?.childrenDepth ?? 0;
  }

  get childrenDepth() {
    return this.levelDescriptor.depth;
  }

  get deepestBranchDepth(): number {
    const { node, parentKeyToChildrenMap, parentKeyToChildrenMapAtom } = this;
    const {
      collection: {
        result: { keyedItems },
      },
    } = node;

    parentKeyToChildrenMapAtom.reportObserved();

    return (
      keyedItems.reduce((acc, keyedItem) => {
        return Math.max(
          acc,
          parentKeyToChildrenMap.get(keyedItem.id)?.deepestBranchDepth ?? 0,
        );
      }, 0) + (keyedItems.length ? 1 : 0)
    );
  }

  get hasExpandableItems() {
    const { parentKeyToChildrenMap, parentKeyToChildrenMapAtom } = this;
    parentKeyToChildrenMapAtom.reportObserved();

    return this.node.collection.result.keyedItems.some(
      (item) => (parentKeyToChildrenMap.get(item.id)?._childrenCount ?? 0) > 0,
    );
  }

  get _childrenCount() {
    return this.node.collection.result.keyedItems.length ?? 0;
  }

  get _expanded() {
    return this.node._expanded;
  }

  get columns() {
    return this.levelDescriptor.columns;
  }

  get renderMainColumn() {
    return this.levelDescriptor.renderMainColumn;
  }

  get showHoverHighlight() {
    return this.node.hoveredOrParentHovered;
  }

  _changeHover(hovered: boolean) {
    this.node._changeHover(hovered);
  }

  get showEmptyState() {
    return this.node.collection.showEmptyState;
  }

  get showLoadingState() {
    return (
      this.node.collection.showInitialLoader ||
      this.node.collection.showRefreshLoader
    );
  }

  get showErrorState() {
    return this.node.collection.showErrorState;
  }

  get errorStatus() {
    return this.node.collection.result.errorStatus;
  }

  retryErrorState = () => this.node.collection.retryErrorState();

  get _isNotExpandableWhenEmpty() {
    const {
      nestedTable: { isFetchAllAborted },
      node: {
        collection: { showEmptyState },
      },
    } = this;
    return !isFetchAllAborted && showEmptyState;
  }

  _isExpandableActionDisabled(item: T) {
    return this.levelDescriptor.expandable?.(item) === false;
  }

  isNotExpandable(item: T) {
    return (
      this._isNotExpandableWhenEmpty || this._isExpandableActionDisabled(item)
    );
  }

  get _keyedItemsIfExpanded(): NestedTableKeyedItem<C, any, any>[] {
    const {
      parent,
      nestedTable: { isFetchAllAborted },
      _expanded,
      node: {
        collection: { showRefreshLoader, showEmptyState, showErrorState },
      },
    } = this;

    if (!_expanded) {
      return [];
    }

    return [
      ...this.keyedItems,
      ...(parent &&
      isFetchAllAborted &&
      (showRefreshLoader || showEmptyState || showErrorState)
        ? new Array(1).fill(null).map((_, index) => ({
            id: `placeholder-states-${parent.key}`,
            key: `placeholder-states-${parent.key}`,
            index,
            pageIndex: 0,
            indexWithinPage: index,
            item: {
              state: this,
              originalKeyedItem: undefined,
            },
          }))
        : []),
    ];
  }

  get _keyedItemsMap(): Map<string, NestedTableKeyedItem<C, any, any>> {
    return new Map(
      this.keyedItems.map((keyedItem) => [keyedItem.key, keyedItem]),
    );
  }

  get _allKeyedItemsMap(): Map<string, NestedTableKeyedItem<C, any, any>> {
    return new Map(
      this._getKeyedItems({ onlyExpanded: false }).map((keyedItem) => [
        keyedItem.key,
        keyedItem,
      ]),
    );
  }

  get _allKeyedItems() {
    return Array.from(this._allKeyedItemsMap.values());
  }

  _getKeyedItems({
    onlyExpanded,
  }: {
    onlyExpanded?: boolean;
  }): NestedTableKeyedItem<C, any, any>[] {
    const { node, parentKeyToChildrenMap, parentKeyToChildrenMapAtom } = this;
    const {
      collection: {
        result: { keyedItems },
      },
    } = node;

    parentKeyToChildrenMapAtom.reportObserved();

    return keyedItems
      .flatMap((keyedItem) => {
        const childrenNode = parentKeyToChildrenMap.get(keyedItem.key);

        return [
          {
            ...keyedItem,
            item: {
              state: this,
              originalKeyedItem: keyedItem,
            },
          },
          ...(childrenNode
            ? onlyExpanded
              ? childrenNode._keyedItemsIfExpanded
              : childrenNode._allKeyedItems
            : []),
        ];
      })
      .map((keyedItem, index) => ({ ...keyedItem, index }));
  }

  get keyedItems(): NestedTableKeyedItem<C, any, any>[] {
    return this._getKeyedItems({ onlyExpanded: true });
  }

  getKeyedItem(key: string) {
    return this._keyedItemsMap.get(key);
  }

  getDescendants<TD, FD extends FiltersMap>(): NestedTableNodeStatePublicAPI<
    C,
    TD,
    FD
  >[] {
    const { collection } = this;

    return collection.result.keyedItems.flatMap((keyedItem) => {
      const node = getNodeOrLeaf(this, keyedItem);
      return [node, ...node.getDescendants<TD, FD>()];
    });
  }

  _getNode<TD, FD extends FiltersMap>(key: string) {
    const node = this._keyedItemsMap.get(key);
    return node?.item.state.parentKeyToChildrenMap.get(key) as
      | NestedTableNodeState<C, TD, FD>
      | undefined;
  }

  getParent<TD, FD extends FiltersMap>(): NestedTableNodeStatePublicAPI<
    C,
    TD,
    FD
  > | null {
    return this.parentNode;
  }

  getData<TD>(): TD | null {
    const { parent, parentNode } = this;
    if (parent == null || parentNode == null) {
      return null;
    }
    return parentNode.collection.getItem(parent.key);
  }

  toggleExpand() {
    this._toggleExpand();
  }

  get expanded() {
    return this._expanded;
  }

  getKeyedItemByIndex(ind: number) {
    return this.keyedItems[ind];
  }

  _register() {
    const { parent, parentNode } = this;

    if (parent == null || parentNode == null) {
      return;
    }

    parentNode.parentKeyToChildrenMap.set(parent.key, this);

    return () => {
      parentNode.parentKeyToChildrenMap.delete(parent.key);
    };
  }

  async _refreshAll() {
    const { collection } = this;

    collection._resetReactQuery();
    await collection.moveToFirstPage();
    const keyedItems = collection.result.keyedItems;

    for (const keyedItem of keyedItems) {
      const node = this.createNode({ parent: keyedItem });

      if (node == null) {
        continue;
      }

      await node._refreshAll();
    }

    if (this.childrenDepth === 0 || this.node._notifyAll) {
      this._notifyAll();
    }
  }

  async _toggleExpand() {
    const promise = this.node._toggleExpand();

    this.parentNode?.parentKeyToChildrenMapAtom.reportChanged();
    await promise;
    runInAction(() => {
      this.parentNode?.parentKeyToChildrenMapAtom.reportChanged();
    });
  }

  async _preRegisterAll() {
    const { node } = this;
    const {
      collection: {
        result: { keyedItems },
      },
    } = node;

    await Promise.all(
      keyedItems.map(async (keyedItem) => {
        const state = this.createNode({ parent: keyedItem });
        if (state == null) {
          return;
        }
        const dispose = state.init();
        await state.initTask.status.promise;
        dispose();
      }),
    );
  }

  _notify() {
    const { parentKeyToChildrenMapAtom } = this;
    parentKeyToChildrenMapAtom.reportChanged();
  }

  _notifyAll() {
    const { parentKeyToChildrenMap } = this;
    this._notify();
    parentKeyToChildrenMap.forEach((state) => {
      state._notifyAll();
    });
  }

  _cleanupStaleNodes() {
    return reaction(
      () => this.collection.result.keyedItems,
      (keyedItems) => {
        const { parentKeyToChildrenMap } = this;
        // remove children that are not in the collection
        const keys = new Set(keyedItems.map((item) => item.key));
        const mapKeys = Array.from(parentKeyToChildrenMap.keys());

        for (const key of mapKeys) {
          if (keys.has(key)) {
            continue;
          }

          parentKeyToChildrenMap.delete(key);
        }
      },
    );
  }

  init({ skipCollectionInit }: { skipCollectionInit?: boolean } = {}) {
    const { node, initTask, nestedTable, collection } = this;

    const { nestedTableDragAndDrop } = nestedTable;

    const disposers = [
      node.init({ skipCollectionInit }),
      nestedTableDragAndDrop?.initCollection(collection),
      this._cleanupStaleNodes(),
    ];

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

      await runInAction(async () => {
        if (!nestedTable.isFetchAllAborted) {
          await this._preRegisterAll();
          if (this.childrenDepth === 0 || node._notifyAll) {
            this._notifyAll();
          }
        }
        return;
      });
    });

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

  createNode({
    parent,
    notifyAll,
  }: {
    parent: KeyedItem<any>;
    notifyAll?: boolean;
  }): NestedTableNodeState<C, any, any> | null {
    const { nestedTable, node, childrenDepth, parentKeyToChildrenMap } = this;

    const existing = parentKeyToChildrenMap.get(parent.key);

    if (existing != null) {
      return existing;
    }

    const nextLevelDescriptor = nestedTable.levels[childrenDepth + 1];

    if (nextLevelDescriptor == null) {
      return null;
    }

    const nextLevelNode = node._createNode(parent, {
      levelDescriptor: nextLevelDescriptor,
      notifyAll,
    });

    if (nextLevelNode == null) {
      return null;
    }

    return new NestedTableNodeState({
      parent,
      parentNode: this,
      nestedTable,
      node: nextLevelNode,
      levelDescriptor: nextLevelDescriptor,
    });
  }
}
