import differenceWith from "lodash/differenceWith";
import isEqual from "lodash/isEqual";
import remove from "lodash/remove";
import { action, computed, makeObservable, observable, runInAction } from "mobx";
import { computedFn } from "mobx-utils";
// hooks
import { TEntityDetails } from "@/hooks/use-multiple-select";
// services
import { IssueService } from "@/services/issue";

export type IMultipleSelectStore = {
  // computed functions
  isSelectionActive: boolean;
  selectedEntityIds: string[];
  // helper actions
  getIsEntitySelected: (entityID: string) => boolean;
  getIsEntityActive: (entityID: string) => boolean;
  getLastSelectedEntityDetails: () => TEntityDetails | null;
  getPreviousActiveEntity: () => TEntityDetails | null;
  getNextActiveEntity: () => TEntityDetails | null;
  getActiveEntityDetails: () => TEntityDetails | null;
  getEntityDetailsFromEntityID: (entityID: string) => TEntityDetails | null;
  // entity actions
  updateSelectedEntityDetails: (entityDetails: TEntityDetails, action: "add" | "remove") => void;
  bulkUpdateSelectedEntityDetails: (entitiesList: TEntityDetails[], action: "add" | "remove") => void;
  updateLastSelectedEntityDetails: (entityDetails: TEntityDetails | null) => void;
  updatePreviousActiveEntity: (entityDetails: TEntityDetails | null) => void;
  updateNextActiveEntity: (entityDetails: TEntityDetails | null) => void;
  updateActiveEntityDetails: (entityDetails: TEntityDetails | null) => void;
  clearSelection: () => void;
};

/**
 * @description the MultipleSelectStore manages multiple selection states by keeping track of the selected entities and providing a bunch of helper functions and actions to maintain the selected states
 * @description use the useMultipleSelectStore custom hook to access the observables
 * @description use the useMultipleSelect custom hook for added functionality on top of the store, including-
 * 1. Keyboard and mouse interaction
 * 2. Clear state on route change
 */
export class MultipleSelectStore implements IMultipleSelectStore {
  // observables
  selectedEntityDetails: TEntityDetails[] = [];
  lastSelectedEntityDetails: TEntityDetails | null = null;
  previousActiveEntity: TEntityDetails | null = null;
  nextActiveEntity: TEntityDetails | null = null;
  activeEntityDetails: TEntityDetails | null = null;
  // service
  issueService;

  constructor() {
    makeObservable(this, {
      // observables
      selectedEntityDetails: observable,
      lastSelectedEntityDetails: observable,
      previousActiveEntity: observable,
      nextActiveEntity: observable,
      activeEntityDetails: observable,
      // computed functions
      isSelectionActive: computed,
      selectedEntityIds: computed,
      // actions
      updateSelectedEntityDetails: action,
      bulkUpdateSelectedEntityDetails: action,
      updateLastSelectedEntityDetails: action,
      updatePreviousActiveEntity: action,
      updateNextActiveEntity: action,
      updateActiveEntityDetails: action,
      clearSelection: action,
    });

    this.issueService = new IssueService();
  }

  get isSelectionActive() {
    return this.selectedEntityDetails.length > 0;
  }

  get selectedEntityIds() {
    return this.selectedEntityDetails.map((en) => en.entityID);
  }

  // helper actions
  /**
   * @description returns if the entity is selected or not
   * @param {string} entityID
   * @returns {boolean}
   */
  getIsEntitySelected = computedFn((entityID: string): boolean =>
    this.selectedEntityDetails.some((en) => en.entityID === entityID)
  );

  /**
   * @description returns if the entity is active or not
   * @param {string} entityID
   * @returns {boolean}
   */
  getIsEntityActive = computedFn((entityID: string): boolean => this.activeEntityDetails?.entityID === entityID);

  /**
   * @description get the last selected entity details
   * @returns {TEntityDetails}
   */
  getLastSelectedEntityDetails = computedFn(() => this.lastSelectedEntityDetails);

  /**
   * @description get the details of the entity preceding the active entity
   * @returns {TEntityDetails}
   */
  getPreviousActiveEntity = computedFn(() => this.previousActiveEntity);

  /**
   * @description get the details of the entity succeeding the active entity
   * @returns {TEntityDetails}
   */
  getNextActiveEntity = computedFn(() => this.nextActiveEntity);

  /**
   * @description get the active entity details
   * @returns {TEntityDetails}
   */
  getActiveEntityDetails = computedFn(() => this.activeEntityDetails);

  /**
   * @description get the entity details from entityID
   * @param {string} entityID
   * @returns {TEntityDetails | null}
   */
  getEntityDetailsFromEntityID = computedFn(
    (entityID: string): TEntityDetails | null =>
      this.selectedEntityDetails.find((en) => en.entityID === entityID) ?? null
  );

  // entity actions
  /**
   * @description add or remove entities
   * @param {TEntityDetails} entityDetails
   * @param {"add" | "remove"} action
   */
  updateSelectedEntityDetails = (entityDetails: TEntityDetails, action: "add" | "remove") => {
    if (action === "add") {
      runInAction(() => {
        if (this.getIsEntitySelected(entityDetails.entityID)) {
          remove(this.selectedEntityDetails, (en) => en.entityID === entityDetails.entityID);
        }
        this.selectedEntityDetails.push(entityDetails);
        this.updateLastSelectedEntityDetails(entityDetails);
      });
    } else {
      let currentSelection = [...this.selectedEntityDetails];
      currentSelection = currentSelection.filter((en) => en.entityID !== entityDetails.entityID);
      runInAction(() => {
        remove(this.selectedEntityDetails, (en) => en.entityID === entityDetails.entityID);
        this.updateLastSelectedEntityDetails(currentSelection[currentSelection.length - 1] ?? null);
      });
    }
  };

  /**
   * @description add or remove multiple entities
   * @param {TEntityDetails[]} entitiesList
   * @param {"add" | "remove"} action
   */
  bulkUpdateSelectedEntityDetails = (entitiesList: TEntityDetails[], action: "add" | "remove") => {
    if (action === "add") {
      runInAction(() => {
        let newEntities: TEntityDetails[] = [];
        newEntities = differenceWith(this.selectedEntityDetails, entitiesList, isEqual);
        newEntities = newEntities.concat(entitiesList);
        this.selectedEntityDetails = newEntities;
        if (entitiesList.length > 0) this.updateLastSelectedEntityDetails(entitiesList[entitiesList.length - 1]);
      });
    } else {
      const newEntities = differenceWith(this.selectedEntityDetails, entitiesList, (obj1, obj2) =>
        isEqual(obj1.entityID, obj2.entityID)
      );
      runInAction(() => {
        this.selectedEntityDetails = newEntities;
      });
    }
  };

  /**
   * @description update last selected entity
   * @param {TEntityDetails} entityDetails
   */
  updateLastSelectedEntityDetails = (entityDetails: TEntityDetails | null) => {
    runInAction(() => {
      this.lastSelectedEntityDetails = entityDetails;
    });
  };

  /**
   * @description update previous active entity
   * @param {TEntityDetails} entityDetails
   */
  updatePreviousActiveEntity = (entityDetails: TEntityDetails | null) => {
    runInAction(() => {
      this.previousActiveEntity = entityDetails;
    });
  };

  /**
   * @description update next active entity
   * @param {TEntityDetails} entityDetails
   */
  updateNextActiveEntity = (entityDetails: TEntityDetails | null) => {
    runInAction(() => {
      this.nextActiveEntity = entityDetails;
    });
  };

  /**
   * @description update active entity
   * @param {TEntityDetails} entityDetails
   */
  updateActiveEntityDetails = (entityDetails: TEntityDetails | null) => {
    runInAction(() => {
      this.activeEntityDetails = entityDetails;
    });
  };

  /**
   * @description clear selection and reset all the observables
   */
  clearSelection = () => {
    runInAction(() => {
      this.selectedEntityDetails = [];
      this.lastSelectedEntityDetails = null;
      this.previousActiveEntity = null;
      this.nextActiveEntity = null;
      this.activeEntityDetails = null;
    });
  };
}