import {
  CollectionState,
  CollectionStateParams,
  DataResultRaw,
  FiltersMap,
  OptionalFiltersMap,
  WixPatternsContainer,
  stringFilter,
  CollectionOptimisticActions,
  ComputedQuery,
  parseFqdn,
} from '@wix/bex-core';
import { ICollectionComponentState } from '../ICollectionComponentState';
import { ToolbarCollectionState } from '../ToolbarCollectionState';
import { KanbanDragEndItemEvent } from './KanbanDragEndEvent';
import { KanbanStageState } from './KanbanStageState';
import {
  observable,
  makeObservable,
  runInAction,
  action,
  computed,
  reaction,
  comparer,
} from 'mobx';
import { KanbanDragAndDropState } from '../../components/KanbanDragAndDropDndKit/KanbanCardDragAndDrop';
import { v4 as uuid } from 'uuid';
import { EventEmitter } from 'events';
import { TypedEmitter } from '@wix/bex-core/events';
import { KanbanStateBIReporter } from './KanbanStateBIReporter';

export const KanbanComponentType = 'TableKanbanSwitch';

export type stageType = 'DONE' | 'NONE';

export interface KanbanStateBaseParams<
  T,
  S,
  F extends FiltersMap = OptionalFiltersMap,
> {
  fqdn?: string;
  kanbanId: string;
  stages: {
    fetchData: CollectionStateParams<S, F>['fetchData'];
    stageKey: CollectionStateParams<S, F>['itemKey'];
    stageName: CollectionStateParams<S, F>['itemName'];
    stageType?(stage: S): stageType;
  };
  items: {
    fetchData: CollectionStateParams<T, F>['fetchData'];
    itemKey: CollectionStateParams<T, F>['itemKey'];
    itemName: CollectionStateParams<T, F>['itemName'];
    stageField: string;
    filters?: F;
    fetchAllByStageIds?: (params: {
      ids: string[];
      filters: ComputedQuery<F>['filters'];
      search?: ComputedQuery<F>['search'];
    }) => Promise<Record<string, DataResultRaw<T>>>;
    dragAndDrop?: {
      onSubmit?: (event: KanbanDragEndItemEvent<T, S>) => Promise<T>;
    };
    create?: {
      navigateToEntityPage: ({ stage }: { stage: S }) => void;
      title: string;
    };
  };
}

export interface KanbanStateParams<
  T,
  S,
  F extends FiltersMap = OptionalFiltersMap,
> extends KanbanStateBaseParams<T, S, F> {
  readonly container: WixPatternsContainer;
}

export interface KanbanStateParamsAPI<
  T,
  F extends FiltersMap = OptionalFiltersMap,
> {
  readonly toolbar: ToolbarCollectionState<T, F>;
}

export class KanbanState<T, S, F extends FiltersMap = OptionalFiltersMap>
  implements ICollectionComponentState, KanbanStateParamsAPI<T, F>
{
  readonly container: WixPatternsContainer;
  kanbanStageStates: Record<
    string,
    KanbanStageState<T, S, F & { stageId: ReturnType<typeof stringFilter> }>
  > = {};
  private stageStateDisposers: Record<string, () => void> = {};
  readonly toolbar: ToolbarCollectionState<T, F>;
  readonly stagesCollectionState: CollectionState<S, F>;
  readonly toolbarQueryCollectionState: CollectionState<T, F>;
  readonly dragAndDropState: KanbanDragAndDropState<
    T,
    S,
    F & { stageId: ReturnType<typeof stringFilter> }
  >;
  readonly debouncedStagesRefresh: () => unknown;
  readonly stageType: KanbanStateBaseParams<T, S, F>['stages']['stageType'];
  items: KanbanStateBaseParams<T, S, F>['items'];
  isRefetching = false;
  isChangingFiltersValue = false;
  private stagesOptimisticActions: CollectionOptimisticActions<S, F>;
  readonly events = new EventEmitter() as TypedEmitter<{
    scrollToStage: (params: { stageKey: string }) => void;
    scrollToCardInStage: (params: {
      stageKey: string;
      cardKey: string;
    }) => void;
  }>;
  readonly kanbanStateBIReporter: KanbanStateBIReporter<T, S, F>;
  readonly kanbanId: string;
  private _searchResultsBiEventCallback: (() => void) | null = null;

  constructor(params: KanbanStateParams<T, S, F>) {
    this.container = params.container;
    this.kanbanId = params.kanbanId;
    this.items = params.items;
    this.toolbarQueryCollectionState =
      this.createToolbarQueryCollectionState(params);

    this.stageType = params.stages.stageType;

    this.stagesCollectionState = this.createStagesCollectionState(params);

    this.stagesOptimisticActions = new CollectionOptimisticActions({
      collection: this.stagesCollectionState,
      container: this.container,
    });

    this.toolbar = new ToolbarCollectionState({
      collection: this.toolbarQueryCollectionState,
      container: params.container,
      componentType: KanbanComponentType,
      reportBi: (ev) => {
        // Filter out BI events to avoid duplicates - KanbanStateBIReporter handles them
        const isSearchEvent =
          ev.src === 144 && (ev.evid === 116 || ev.evid === 117); // cairoFilterToggled or cairoSearchResults or cairoLoadEnd
        const isLoadEndEvent = ev.src === 144 && ev.evid === 111; // cairoLoadEnd
        if (!isSearchEvent && !isLoadEndEvent) {
          this.container.reportBi(ev);
        }
      },
    });

    this.dragAndDropState = new KanbanDragAndDropState({
      kanbanState: this as any,
    });

    this.kanbanStateBIReporter = new KanbanStateBIReporter(this);

    makeObservable(this, {
      kanbanStageStates: observable,
      updateStage: action,
      stages: computed,
      isRefetching: observable,
      refetchKanban: action,
      hasStagesError: computed,
      isLoading: computed,
      isLoadingStages: computed,
    });

    this.debouncedStagesRefresh = this.container.lodash.debounce(async () => {
      this.isChangingFiltersValue = false;
      const currentSearchTerm = this.queryComputed.search || '';

      if (currentSearchTerm) {
        this._searchResultsBiEventCallback =
          this.kanbanStateBIReporter.reportSearchTriggered(currentSearchTerm);
      } else {
        this._searchResultsBiEventCallback = null;
      }

      await this.initializeStageStates();
      if (this._searchResultsBiEventCallback) {
        this._searchResultsBiEventCallback();
        this._searchResultsBiEventCallback = null;
      }
    }, 300);
  }

  get query() {
    return this.toolbarQueryCollectionState.query;
  }

  get filters() {
    return this.toolbarQueryCollectionState.filters;
  }

  get queryComputed() {
    return this.query.asComputed;
  }

  triggerDebouncedStagesRefresh() {
    this.isChangingFiltersValue = true;
    this.debouncedStagesRefresh();
  }

  async init() {
    const disposers = [
      this.stagesCollectionState.init(),
      this.stagesOptimisticActions.init(),
      this.kanbanStateBIReporter.initAdditionalFeaturesAvailability(),
      this.toolbar.init({ featuresInitializers: [] }),
      this.dragAndDropState.init(),
      reaction(
        () => ({
          filters: this.queryComputed.filters,
          search: this.queryComputed.search,
        }),
        () => {
          this.triggerDebouncedStagesRefresh();
        },
        {
          fireImmediately: false,
          equals: comparer.structural,
        },
      ),
    ];

    this.kanbanStateBIReporter.init();
    await this.loadStages();
    await this.initializeStageStates();
    this.kanbanStateBIReporter.appLoaded?.();

    return () => {
      Object.keys(this.stageStateDisposers).forEach((stageId) => {
        this.disposeStageState(stageId);
      });

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

  async loadStages() {
    await this.stagesCollectionState?.initTask?.status?.promise?.catch(
      (error) => {
        this.showErrorToast(error);
      },
    );
  }

  async initializeStageStates() {
    if (!this.items.fetchAllByStageIds) {
      return;
    }
    try {
      const stageData = await this.fetchStagesData();
      this.processAllStages(stageData);
    } catch (error) {
      this.showErrorToast(error);
    }
  }

  get stages() {
    return this.stagesCollectionState.result.keyedItems.map(
      (item: any) => item.item,
    );
  }

  getStageState(stage: S) {
    return this.kanbanStageStates[this.stagesCollectionState.itemKey(stage)];
  }

  getStageStateByKey(stageKey: string) {
    return this.kanbanStageStates[stageKey];
  }

  get isLoading() {
    return (
      this.isLoadingStages ||
      Object.values(this.kanbanStageStates).some((stage) => stage.isLoading)
    );
  }

  get isLoadingStages() {
    return (
      this.stagesCollectionState.initTask.status.isLoading ||
      this.isRefetching ||
      Object.keys(this.kanbanStageStates).length !==
        this.stagesCollectionState.result.keyedItems.length
    );
  }

  isInSearchMode() {
    const search = this.queryComputed.search;
    return search && search.length > 0;
  }

  isInFilterMode() {
    return this.queryComputed.hasActiveFilters;
  }

  getIsChangingFiltersValue() {
    return this.isChangingFiltersValue;
  }

  stageKey(stage: S) {
    return this.stagesCollectionState.itemKey(stage);
  }

  get hasStagesError() {
    return this.stagesCollectionState.result.status?.isError;
  }

  // Public methods
  public async refetchKanban() {
    const startTime = performance.now();
    this.toolbar.toolbarBIReporter.loadStart();
    runInAction(() => {
      this.isRefetching = true;
    });
    try {
      await this.stagesCollectionState.clearResultBeforeRefresh();
      await this.stagesCollectionState.refreshAllPages();
      await this.initializeStageStates();
    } catch (error) {
      this.showErrorToast(error);
    } finally {
      // Report refetch completion with timing
      this.kanbanStateBIReporter.reportOnLoadEnd(
        Math.round(performance.now() - startTime),
      );
      runInAction(() => {
        this.isRefetching = false;
      });
    }
  }

  // Private methods
  showErrorToast(error: unknown) {
    const resolvedError =
      this.container.errorHandler?.getResolvedError?.(error);
    resolvedError && this.container.errorHandler?.showError?.(resolvedError);
  }

  private createToolbarQueryCollectionState(
    params: KanbanStateParams<T, S, F>,
  ) {
    return new CollectionState<T, F>({
      errorMonitor: this.container.errorMonitor,
      history: this.container.history,
      lodash: this.container.lodash,
      onlineState: this.container.onlineState,
      queryCache: this.container.queryCache,
      translate: this.container.translate,
      fqdn: params.fqdn ? parseFqdn(params.fqdn) : undefined,
      queryName: `kanban-toolbar-query-items-${this.kanbanId}`,
      fetchData: () =>
        Promise.resolve({
          items: [],
          total: 0,
          cursor: null,
        }),
      itemKey: () => '',
      itemName: () => '',
      filters: params.items.filters ?? ({} as F),
      errorHandler: this.container.errorHandler,
      paginationMode: 'cursor',
    });
  }

  private createStagesCollectionState(params: KanbanStateParams<T, S, F>) {
    return new CollectionState<S, F>({
      errorMonitor: this.container.errorMonitor,
      history: this.container.history,
      lodash: this.container.lodash,
      onlineState: this.container.onlineState,
      queryCache: this.container.queryCache,
      translate: this.container.translate,
      queryName: `kanban-stages-${params.kanbanId}`,
      fetchData: params.stages.fetchData,
      itemKey: params.stages.stageKey,
      itemName: params.stages.stageName,
      filters: {} as F,
      errorHandler: this.container.errorHandler,
      paginationMode: 'cursor',
    });
  }

  private async fetchStagesData() {
    const stageIds = this.getStageIds();

    return this.items.fetchAllByStageIds!({
      ids: stageIds,
      filters: this.queryComputed.filters,
      search: this.queryComputed.search,
    });
  }

  private getStageIds(): string[] {
    return this.stagesCollectionState.result.keyedItems.map((item) =>
      this.stagesCollectionState.itemKey(item.item),
    );
  }

  private processAllStages(stageData: Record<string, DataResultRaw<T>>) {
    this.cleanupUnusedStageStates();
    this.processCurrentStages(stageData);
  }

  private cleanupUnusedStageStates() {
    // Get current stage IDs to identify which ones are no longer needed
    const currentStageIds = new Set(
      this.stagesCollectionState.result.keyedItems.map((item) =>
        this.stagesCollectionState.itemKey(item.item),
      ),
    );

    // Clean up stage states that no longer exist
    Object.keys(this.kanbanStageStates).forEach((stageId) => {
      if (!currentStageIds.has(stageId)) {
        this.disposeStageState(stageId);
      }
    });
  }

  private disposeStageState(stageId: string) {
    // Dispose of the stage state using stored disposer
    const disposer = this.stageStateDisposers[stageId];
    if (disposer) {
      disposer();
      Reflect.deleteProperty(this.stageStateDisposers, stageId);
    }
    runInAction(() => {
      Reflect.deleteProperty(this.kanbanStageStates, stageId);
    });
  }

  private processCurrentStages(stageData: Record<string, DataResultRaw<T>>) {
    this.stagesCollectionState.result.keyedItems.forEach((keyedStage) => {
      const stage = keyedStage.item;
      runInAction(() => {
        const stageId = this.stagesCollectionState.itemKey(stage);
        const hasDataForStage = !!stageData[stageId];

        if (!this.kanbanStageStates[stageId]) {
          this.createNewStageState(
            stage,
            stageId,
            stageData[stageId],
            hasDataForStage,
          );
        } else {
          this.updateExistingStageState(
            stage,
            stageId,
            stageData[stageId],
            hasDataForStage,
          );
        }
      });
    });
  }

  private createNewStageState(
    stage: S,
    stageId: string,
    stageData: DataResultRaw<T>,
    hasDataForStage: boolean,
  ) {
    this.kanbanStageStates[stageId] = new KanbanStageState<
      T,
      S,
      F & { stageId: ReturnType<typeof stringFilter> }
    >({
      container: this.container,
      fetchData: this.items.fetchData,
      itemKey: this.items.itemKey,
      itemName: this.items.itemName,
      initialPage: hasDataForStage
        ? this.getInitialPageData(stageData)
        : {
            items: [],
            total: 0,
            cursor: null,
          },
      name: this.stagesCollectionState.itemName(stage),
      key: this.stagesCollectionState.itemKey(stage),
      kanbanId: this.kanbanId,
      stage,
      query: this.query as any,
    });
    const disposer = this.kanbanStageStates[stageId].init();
    this.stageStateDisposers[stageId] = disposer;
  }

  private updateExistingStageState(
    stage: S,
    stageId: string,
    stageData: DataResultRaw<T>,
    hasDataForStage: boolean,
  ) {
    const stageState = this.kanbanStageStates[stageId];
    stageState.name = this.stagesCollectionState.itemName(stage);

    runInAction(() => {
      const initialPageData = this.getInitialPageData(
        hasDataForStage
          ? stageData
          : {
              items: [],
              total: 0,
              cursor: null,
            },
      );

      stageState.itemsCollectionState.setInitialPage(initialPageData);

      // Directly update QueryResultState to trigger MobX reactivity for keyedItems
      const items = initialPageData.items || [];
      const total = initialPageData.total ?? items.length;
      const cursor = initialPageData.cursor;

      stageState.itemsCollectionState.result.setPages({
        processed: [items],
        unprocessed: [items],
      });

      stageState.itemsCollectionState.result.setTotals({
        processed: total,
        unprocessed: total,
      });

      stageState.setTotal(stageData?.total ?? 0);

      if (cursor != null) {
        stageState.itemsCollectionState.paginationMode.setCursor(cursor);
      }
    });
  }

  private getInitialPageData(stageData: DataResultRaw<T>) {
    return {
      items: stageData?.items,
      total: stageData?.total,
      cursor: stageData?.cursor,
    };
  }

  private getStageIdFromItem(item: T): string {
    return (item as Record<string, any>)[this.items.stageField];
  }

  // optimistic actions

  updateStage(originalStage: S, updatedStage: S) {
    const stageId = this.stagesCollectionState.itemKey(originalStage);
    const stageState = this.getStageStateByKey(stageId);

    this.stagesCollectionState.optimisticActions.updateOne(updatedStage, {
      submit: async (items) => {
        // just return the item that was passed in
        return Promise.resolve(items[0]);
      },
      keepPosition: true,
    });

    runInAction(() => {
      if (
        stageState.name !== this.stagesCollectionState.itemName(updatedStage)
      ) {
        stageState.name = this.stagesCollectionState.itemName(updatedStage);
      }
    });
  }

  setStages(stages: S[]) {
    runInAction(() => {
      const getKey = (stage: S) => this.stagesCollectionState.itemKey(stage);
      const currentStageIds = new Set(Object.keys(this.kanbanStageStates));
      const newStageIds = new Set(stages.map(getKey));

      this.stagesCollectionState.result.setPages({
        processed: [stages],
        unprocessed: [stages],
      });
      this.stagesCollectionState.result.setTotals({
        processed: stages.length,
        unprocessed: stages.length,
      });

      currentStageIds.forEach((stageId) => {
        if (!newStageIds.has(stageId)) {
          this.disposeStageState(stageId);
        }
      });

      stages.forEach((stage) => {
        const stageId = getKey(stage);
        if (!this.kanbanStageStates[stageId]) {
          this.createNewStageState(
            stage,
            stageId,
            { items: [], total: 0, cursor: null },
            false,
          );
        } else {
          this.kanbanStageStates[stageId].name =
            this.stagesCollectionState.itemName(stage);
        }
      });
    });
  }

  createStage(stage: S) {
    this.stagesCollectionState.optimisticActions.createOne(stage, {
      submit: (_items) => Promise.resolve(),
    });
    runInAction(() => {
      const stageId = this.stagesCollectionState.itemKey(stage);

      this.createNewStageState(
        stage,
        stageId,
        {
          items: [],
          total: 0,
          cursor: null,
        },
        false,
      );
    });
  }

  scrollToStage({ stageKey }: { stageKey: string }) {
    this.events.emit('scrollToStage', { stageKey });
  }

  scrollToCardInStage({
    stageKey,
    cardKey,
  }: {
    stageKey: string;
    cardKey: string;
  }) {
    this.events.emit('scrollToCardInStage', { stageKey, cardKey });
  }

  deleteStage(stage: S) {
    const stageId = this.stagesCollectionState.itemKey(stage);

    this.stagesCollectionState.optimisticActions.deleteOne(stage, {
      submit: (_items) => Promise.resolve(),
    });
    this.disposeStageState(stageId);
  }

  reorderStages(fromIndex: number, toIndex: number) {
    const keyedItems = this.stagesCollectionState.result.keyedItems;
    if (fromIndex === toIndex) {
      return;
    }

    const from = keyedItems[fromIndex];
    const over = keyedItems[toIndex];

    let after;
    if (toIndex === 0) {
      after = null;
    } else if (fromIndex < toIndex) {
      after = over;
    } else {
      after = keyedItems[toIndex - 1];
    }

    const actionMove = {
      moveId: uuid(),
      from,
      over,
      after,
      isFromHandle: false,
      filtersKeyHash: this.stagesCollectionState.query.filtersKeyHash,
      query: this.stagesCollectionState.originQuery,
    };

    this.stagesCollectionState.optimisticActions._orderByMode = 'moves';
    this.stagesCollectionState.optimisticActions.move(actionMove, {
      collectionSnapshot: {},
    });
  }

  moveItemBetweenStages(
    originalItem: T,
    updatedItem: T,
    fromStageId: string,
    toStageId: string,
  ) {
    this.getStageStateByKey(fromStageId).deleteItem(originalItem);
    this.getStageStateByKey(toStageId).createItem(updatedItem);
  }

  // optimistic actions card
  deleteCard(card: T) {
    const stageId = this.getStageIdFromItem(card);
    this.getStageStateByKey(stageId).deleteItem(card);
  }

  createCard(card: T) {
    const stageId = this.getStageIdFromItem(card);
    this.getStageStateByKey(stageId).createItem(card);
  }

  updateCard(card: T) {
    const stageId = this.getStageIdFromItem(card);
    this.getStageStateByKey(stageId).updateItem(card);
  }

  // Helper functions for BI reporting
  getStageIndexByKey(stageKey: string): number {
    return this.stages.findIndex(
      (stage) => this.stagesCollectionState.itemKey(stage) === stageKey,
    );
  }

  getStageNameByKey(stageKey: string): string {
    const stage = this.stages.find(
      (s) => this.stagesCollectionState.itemKey(s) === stageKey,
    );
    return stage ? this.stagesCollectionState.itemName(stage) : '';
  }

  getStageById(stageKey: string): S | undefined {
    return this.stages.find(
      (stage) => this.stagesCollectionState.itemKey(stage) === stageKey,
    );
  }
}
