import {
  WixPatternsContainer,
  CollectionOptimisticActions,
  CollectionState,
  FiltersMap,
  ModalState,
  ReportBI,
  TaskState,
} from '@wix/bex-core';
import { action, computed, makeObservable, observable } from 'mobx';
import { EventEmitter, TypedEmitter } from '@wix/bex-core/events';
import { ViewsService } from '../services';
import { v4 as uuid } from 'uuid';
import {
  ALL_ITEMS_SORT_IND,
  ALL_ITEMS_VIEW_ID,
  AllItemsViewProps,
  BasicCategory,
  Category,
  PREDEFINED_VIEWS_SORT_IND,
  PrivateView,
  SAVED_VIEWS_ID,
  SAVED_VIEWS_SORT_IND,
  View,
  ViewActionNameBI,
  ViewBase,
} from '../model';
import { AutoCompleteReadonlyState } from '../components/CollectionViewsDropdown/AutoCompleteReadonlyState';
import { ViewsLocalSearchState } from './ViewsLocalSearchState';

const MAX_SAVED_VIEWS = 50;

const updatePresetViews = <F extends FiltersMap>(
  presets: Category<F>[],
  hiddenPresetIds: string[],
): PrivateView<F>[] => {
  return presets.reduce(
    (views, cat, ind) => [
      ...views,
      ...(cat.views
        ?.filter((p) => !hiddenPresetIds.includes(p.id))
        .map(({ isDefaultView, ...p }) => ({
          ...p,
          categoryId: cat.id,
          categorySortInd: `${ind + +PREDEFINED_VIEWS_SORT_IND}`,
          isPreset: true,
        })) ?? []),
    ],
    [] as PrivateView<F>[],
  );
};

const toNonPresetViews = <F extends FiltersMap>(
  views: View<F>[],
  categoryId: string,
): PrivateView<F>[] => {
  return views.map((v) => ({
    ...v,
    categoryId,
    isPreset: false,
    categorySortInd: SAVED_VIEWS_SORT_IND,
  }));
};

const getBasicCategories = <F extends FiltersMap>(
  presets: Category<F>[],
): Record<string, BasicCategory> => {
  return {
    [SAVED_VIEWS_ID]: {
      id: SAVED_VIEWS_ID,
      name: '',
    },
    ...presets.reduce(
      (prev, { views, ...basicCategory }) => ({
        ...prev,
        [basicCategory.id]: basicCategory,
      }),
      {},
    ),
  };
};

export interface ViewsStateParams<F extends FiltersMap> {
  oldNamespace: string;
  presets?: Category<F>[];
  container: WixPatternsContainer;
  fqdn: string;
  initialSelect?: true | ((item: View<F>, index: number) => boolean);
  allItemsViewProps?: AllItemsViewProps<F>;
  reportBI: ReportBI;
}

export class ViewsState<F extends FiltersMap> {
  currentView: PrivateView<F> | null;
  defaultViewId: string | null;
  _initialViewIdFromUrl?: string | null;

  readonly initialSelect?: true | ((item: View<F>, index: number) => boolean);
  readonly viewsService: ViewsService<F>;
  readonly collection: CollectionState<PrivateView<F>>;
  readonly optimisticActions: CollectionOptimisticActions<
    PrivateView<F>,
    FiltersMap
  >;
  readonly saveViewModal = new ModalState();
  readonly renameViewModal = new ModalState();

  readonly autoCompleteReadonlyState = new AutoCompleteReadonlyState();

  readonly localSearchState;
  readonly reportBI;

  readonly events = new EventEmitter() as TypedEmitter<{
    selectAndRefresh: (needFiltersReset?: boolean) => unknown;
    beforeSelectView: () => unknown;
    afterSelectView: (params: { view: PrivateView<F> }) => unknown;
    undoAction: (params: {
      actionName: ViewActionNameBI;
      viewId: string;
      newName?: string;
    }) => unknown;
    tryAgainAction: (params: { actionName: ViewActionNameBI }) => unknown;
    errorAction: (params: {
      actionName: ViewActionNameBI;
      createdAt: number;
      viewId: string;
    }) => unknown;
  }>;

  readonly presets: Category<F>[];
  readonly allItemsViewProps: AllItemsViewProps<F>;
  readonly basicCategories: Record<string, BasicCategory>;
  readonly initTask = new TaskState();
  readonly container;

  _migrateOrdersViews?: (state: ViewsState<F>) => unknown;
  _fixMissingContactsViews?: (state: ViewsState<F>) => unknown;

  constructor(params: Readonly<ViewsStateParams<F>>) {
    const { container, fqdn, reportBI } = params;
    this.container = container;
    this.reportBI = reportBI;
    this.initialSelect = params.initialSelect;
    this.presets = params.presets ?? [];
    this.allItemsViewProps = params.allItemsViewProps ?? {};

    const viewsStorage = container._createComponentSettings({
      namespace: 'cairo',
    });

    const { environment } = container;

    const defaultViewStorage =
      environment.providerType === 'bm'
        ? viewsStorage
        : container.createEssentials({
            dataCapsule: {
              namespace: 'cairo',
              scope: environment.componentId,
              storageStrategy:
                environment.providerType === 'giza'
                  ? 'userPreferences'
                  : undefined,
            },
          }).dataCapsule;

    this.viewsService = new ViewsService({
      viewsStorage,
      defaultViewStorage,
      fqdn,
      monitor: container.internalMonitor,
      errorHandler: container.errorHandler,
      translate: container.translate,
    });

    this.collection = new CollectionState<PrivateView<F>>({
      ...container,
      events: {},
      queryName: [fqdn, 'views'].join('/'),
      fetchData: async () => {
        const { viewsService, collection, allItemsViewProps } = this;

        if (collection.initTask.status.isLoading) {
          await viewsService.migrateFromLocalStorage();
          await this._migrateOrdersViews?.(this);
          await this._fixMissingContactsViews?.(this);
        }

        const result = await viewsService.fetchViews();

        const { views = [], hiddenPresetIds = [] } = result ?? {};
        const items = [] as PrivateView<F>[];

        if (
          !allItemsViewProps.hidden ||
          // we ignore the hidden flag if there are no presets
          this.presets.every((pr) => !pr.views?.length)
        ) {
          items.push(await this.getAllItemsView(this.allItemsViewProps));
        }

        items.push(...(toNonPresetViews(views, SAVED_VIEWS_ID) ?? []));
        items.push(...updatePresetViews(this.presets, hiddenPresetIds));

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

    this.localSearchState = new ViewsLocalSearchState({ views: this });

    this.optimisticActions = new CollectionOptimisticActions({
      container,
      collection: this.collection,
      orderBy: ({ sort }) => [
        ...(sort || []),
        {
          fieldName: (item) => item.categorySortInd,
          order: 'asc',
        },
      ],
    });

    this.basicCategories = getBasicCategories(this.presets);

    this.currentView = null;
    this.defaultViewId = null;

    makeObservable(this, {
      isReady: computed,
      isDefaultView: computed,
      currentView: observable.ref,
      defaultViewId: observable.ref,
      selectView: action,
      unselectView: action,
      setDefaultViewId: action,
      _onActionUndo: action,
      reachedViewsLimit: computed,
      isCurrentViewSelected: computed,
    });
  }

  _getAllItemsAndPresetViews() {
    const { allItemsViewProps } = this;
    const items = [] as PrivateView<F>[];

    if (
      !allItemsViewProps.hidden ||
      // we ignore the hidden flag if there are no presets
      this.presets.every((pr) => !pr.views?.length)
    ) {
      items.push(this.getAllItemsViewBase(this.allItemsViewProps));
    }

    items.push(...updatePresetViews(this.presets, []));

    return items;
  }

  _getInitialSelectItem(items: PrivateView<F>[]): PrivateView<F> | undefined {
    const { initialSelect, _initialViewIdFromUrl, defaultViewId } = this;

    if (initialSelect == null) {
      if (_initialViewIdFromUrl) {
        return items.find((item) => item.id === _initialViewIdFromUrl);
      }
      if (defaultViewId) {
        return items.find((item) => item.id === defaultViewId);
      }
      const defaultPresetId = this.getDefaultPreset() ?? ALL_ITEMS_VIEW_ID;

      return items.find((item) => item.id === defaultPresetId);
    }

    if (initialSelect === true) {
      return items[0];
    }
    return items.find((item, index) => initialSelect(item, index));
  }

  _onActionUndo(
    id: string,
    actionName: ViewActionNameBI,
    newName?: string,
  ): void {
    this.events.emit('undoAction', { actionName, viewId: id, newName });
    this.selectView({ id, needFiltersReset: false });
  }

  _onTryAgain(actionName: ViewActionNameBI): void {
    this.events.emit('tryAgainAction', { actionName });
  }

  _onError(
    actionName: ViewActionNameBI,
    createdAt: number,
    oldViewId?: string,
    viewId?: string,
  ): void {
    if (oldViewId) {
      this.selectView({ id: oldViewId, needFiltersReset: false });
    } else {
      this.unselectView();
    }
    this.events.emit('errorAction', {
      actionName,
      createdAt,
      viewId: viewId ?? oldViewId ?? '',
    });
  }

  init() {
    const { collection, localSearchState } = this;

    const disposers = [
      collection.init(),
      this.optimisticActions.init(),
      localSearchState.init(),
    ];

    this.initTask.runOnce(async () => {
      await collection.initTask.status.promise;
      this.setDefaultViewId(await this.viewsService.fetchDefaultViewId());
    });

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

  getViews(): PrivateView<F>[] {
    return this.collection.result.items.slice();
  }

  getView(viewId: string): PrivateView<F> | undefined {
    return this.getViews().find(({ id }) => id === viewId);
  }

  getDefaultPreset(): string | undefined {
    return this.presets
      .reduce((prev, cur) => [...prev, ...cur.views], [] as View<F>[])
      .find((view) => view.isDefaultView)?.id;
  }

  get isDefaultView() {
    return this.currentView?.id === this.defaultViewId;
  }

  async getAllItemsView(view: AllItemsViewProps<F>): Promise<PrivateView<F>> {
    const { container } = this;
    await container.initTask.status.promise;
    return {
      ...this.getAllItemsViewBase(view),
      name:
        view.name ?? container.translate('cairo.viewsDropdown.allItems.label'),
    };
  }

  getAllItemsViewBase(view: AllItemsViewProps<F>): PrivateView<F> {
    return {
      categorySortInd: ALL_ITEMS_SORT_IND,
      id: ALL_ITEMS_VIEW_ID,
      name: view.name ?? '',
      selectedColumns: view.selectedColumns,
      sortDirections: view.sortDirections,
      isPreset: true,
    };
  }

  async saveViewChanges(viewBase: ViewBase<F>): Promise<PrivateView<F> | null> {
    if (!this.currentView) {
      return null;
    }

    const updatedView = {
      ...this.currentView,
      ...viewBase,
    };

    return this.saveView({
      newView: updatedView,
      hasUndo: true,
      oldViewId: this.currentView.id,
    });
  }

  createNewView(name: string, viewBase: ViewBase<F>): PrivateView<F> {
    return {
      ...viewBase,
      categorySortInd: SAVED_VIEWS_SORT_IND,
      categoryId: SAVED_VIEWS_ID,
      isPreset: false,
      name,
      id: uuid(),
      extra: this.currentView?.extra,
    };
  }

  async saveView({
    newView,
    hasUndo = false,
    isNew = false,
    oldViewId,
  }: {
    newView: PrivateView<F>;
    hasUndo?: boolean;
    isNew?: boolean;
    oldViewId?: string;
  }): Promise<PrivateView<F>> {
    const createdAt = Date.now();
    const immediateAction = () => {
      this.selectView({ id: newView.id });
    };

    this.optimisticActions.createOne(newView, {
      submit: async () => {
        await this.viewsService.saveView(newView as View<F>);
        return [newView];
      },
      showUndoToast: hasUndo,
      onUndo: () => {
        this._onActionUndo(
          newView.id,
          isNew ? 'view-save-new' : 'view-save-changes',
        );
      },
      onTryAgain: () => {
        immediateAction();
        this._onTryAgain(isNew ? 'view-save-new' : 'view-save-changes');
      },
      onError: (_: unknown) => {
        this._onError(
          isNew ? 'view-save-new' : 'view-save-changes',
          createdAt,
          oldViewId,
          newView.id,
        );
      },
      successToast: {
        biName: isNew
          ? 'cairo-save-new-view-success'
          : 'cairo-save-view-changes-success',
        message: isNew
          ? this.collection.translate('cairo.saveView.Success.toast', {
              viewName: newView.name,
            })
          : this.collection.translate(
              'cairo.manageView.saveChanges.Success.toast',
              {
                viewName: newView.name,
              },
            ),
      },
      errorToast: (err) => {
        const resolvedError =
          this.container.errorHandler?.getResolvedError?.(err);
        return {
          biName: isNew
            ? 'cairo-save-new-view-error'
            : 'cairo-save-view-changes-error',
          message: resolvedError?.message!,
          ...(resolvedError?.action ? { action: resolvedError?.action } : {}),
          type: 'ERROR',
        };
      },
    });

    immediateAction();
    return newView;
  }

  public async deleteView(view: PrivateView<F>): Promise<void> {
    const { viewsService, currentView } = this;
    const { id, isPreset } = view;
    const isCurrentViewSelected = currentView?.id === id;
    const idForUndo = currentView?.id;
    const prevDefaultView = this.defaultViewId;

    const createdAt = Date.now();
    const immediateAction = async () => {
      if (id === this.defaultViewId) {
        await this.viewsService.updateDefaultView(undefined);
        this.setDefaultViewId(null);
      }
      if (isCurrentViewSelected) {
        this.unselectView();
      }
    };
    this.optimisticActions.deleteOne(view, {
      submit: async () => {
        if (isPreset) {
          await viewsService.updateHiddenPreset(id);
        } else {
          await viewsService.deleteView(view);
        }
      },
      onUndo: () => {
        if (!idForUndo) {
          return;
        }
        this.setDefaultViewId(prevDefaultView);
        this._onActionUndo(idForUndo, 'view-delete');
      },
      onTryAgain: async () => {
        await immediateAction();
        this._onTryAgain('view-delete');
      },
      onError: (_: unknown) => {
        this.setDefaultViewId(prevDefaultView);
        this._onError('view-delete', createdAt, idForUndo);
      },
      showUndoToast: true,
      successToast: {
        biName: 'cairo-delete-view-success',
        message: this.collection.translate('cairo.deleteView.Success.toast', {
          viewName: view.name,
        }),
      },
      errorToast: (err) => {
        const resolvedError =
          this.container.errorHandler?.getResolvedError?.(err);
        return {
          biName: 'cairo-delete-view-error',
          message: resolvedError?.message!,
          ...(resolvedError?.action ? { action: resolvedError?.action } : {}),
          type: 'ERROR',
        };
      },
    });

    immediateAction();
    return;
  }

  async renameView(
    newView: PrivateView<F>,
    oldName: string,
  ): Promise<PrivateView<F>> {
    const createdAt = Date.now();
    const immediateAction = () => {
      this.selectView({ id: newView.id });
    };
    this.optimisticActions.createOne(newView, {
      submit: async () => {
        await this.viewsService.saveView(newView as View<F>, {
          isRename: true,
        });
        return [newView];
      },
      onUndo: () => {
        this._onActionUndo(newView.id, 'view-rename', newView.name);
      },
      onTryAgain: () => {
        immediateAction();
        this._onTryAgain('view-rename');
      },
      onError: (_: unknown) => {
        this._onError('view-rename', createdAt, newView.id);
      },
      showUndoToast: true,
      successToast: {
        biName: 'cairo-rename-view-success',
        message: this.collection.translate(
          'cairo.manageView.Rename.Success.toast',
          {
            oldViewName: oldName,
            newViewName: newView.name,
          },
        ),
      },
      errorToast: (err) => {
        const resolvedError =
          this.container.errorHandler?.getResolvedError?.(err);
        return {
          biName: 'cairo-rename-view-error',
          message: resolvedError?.message!,
          ...(resolvedError?.action ? { action: resolvedError?.action } : {}),
          type: 'ERROR',
        };
      },
    });

    immediateAction();
    return newView;
  }

  async setAsDefaultView(
    newView: PrivateView<F>,
    isDefault: boolean,
  ): Promise<PrivateView<F>> {
    const createdAt = Date.now();
    const prevDefaultView = this.defaultViewId;
    const biName = isDefault ? 'view-set-as-default' : 'view-unset-as-default';
    const immediateAction = () => {
      this.setDefaultViewId(isDefault ? newView.id : null);
    };

    this.optimisticActions.createOne(newView, {
      submit: async () => {
        await this.viewsService.updateDefaultView(
          isDefault ? newView : undefined,
        );
        return [newView];
      },
      onUndo: () => {
        this.setDefaultViewId(prevDefaultView);
        this._onActionUndo(newView.id, biName);
      },
      onTryAgain: () => {
        immediateAction();
        this._onTryAgain(biName);
      },
      onError: (_: unknown) => {
        this.setDefaultViewId(prevDefaultView);
        this._onError(biName, createdAt, newView.id);
      },
      showUndoToast: isDefault,
      successToast: isDefault
        ? {
            biName: 'cairo-set-default-view-success',
            message: this.collection.translate(
              'cairo.manageView.setAsDefault.success.toast',
              {
                viewName: newView.name,
              },
            ),
          }
        : undefined,
      errorToast: (err) => {
        const resolvedError =
          this.container.errorHandler?.getResolvedError?.(err);
        return {
          biName: 'cairo-set-default-view-error',
          message:
            resolvedError?.message ??
            this.collection.translate(
              'cairo.manageView.setAsDefault.error.toast',
              {
                viewName: newView.name,
              },
            ),
          ...(resolvedError?.action ? { action: resolvedError?.action } : {}),
          type: 'ERROR',
        };
      },
    });
    immediateAction();
    return newView;
  }

  get isReady(): boolean {
    const { isError, isFetching, isSuccess } = this.collection.result.status;
    return !isError && !isFetching && isSuccess;
  }

  setDefaultViewId(defaultViewId: string | null) {
    this.defaultViewId = defaultViewId;
  }

  unselectView() {
    this.currentView = null;
  }

  selectView({
    id,
    needFiltersReset,
  }: {
    id: string;
    needFiltersReset?: boolean;
  }): void {
    const view = this.getView(id);
    this.currentView = view ?? null;
    this.events.emit('selectAndRefresh', needFiltersReset);
  }

  get reachedViewsLimit(): boolean {
    return (
      this.getViews().filter(({ isPreset }) => !isPreset).length >=
      MAX_SAVED_VIEWS
    );
  }

  get isCurrentViewSelected(): boolean {
    return !!this.currentView?.id;
  }

  hasViewName(viewName: string): boolean {
    return this.getViews()
      .map(({ name }) => name)
      .includes(viewName);
  }
}
