import React, { useCallback, useEffect, useMemo, useState } from "react";
import {
  GroupItem,
  TalentModuleMixItem,
  TalentProfileModuleGroup,
} from "models/talent/talent-profile-module.model";
import { useDispatch } from "react-redux";
import { useTypedSelector } from "redux/rootReducer";
import {
  selectTalentProfileModules,
  selectTalentProfileModulesOrigin,
} from "redux/User/selector";
import {
  addTalentProfileModuleActions,
  createTalentProfileModuleActions,
  deleteTalentProfileModuleActions,
  removeTalentProfileModuleActions,
  revertTalentProfileModuleActions,
  setTalentProfileModuleActions,
  setTalentProfileModuleListActions,
  setTalentProfileModulesOriginActions,
  updateTalentProfileModuleActions,
} from "redux/User/actions";
import { DropResult } from "react-beautiful-dnd";
import { ArrayServices } from "utils/array";
import take from "lodash/take";
import takeRight from "lodash/takeRight";
import sortBy from "lodash/sortBy";
import reduce from "lodash/reduce";
import { v4 as uuidv4 } from "uuid";
import { setActiveModuleAction } from "redux/Talent/actions";
import { useAnalytics } from "hooks/useAnalytics";
import { SEGMENT_EVENTS } from "constants/segment";
import { JSONService } from "utils/json-patch";
import isEmpty from "lodash/isEmpty";
import {
  bandsintownService,
  shopMyShelfService,
  talentService,
  seatedService,
} from "services";
import { getSlugFromUrl } from "utils/url";
import { useAutoSave } from "hooks/useAutoSave";
import moment from "moment";
import pick from "lodash/pick";
import { isModuleGroup } from "utils/modules";
import { SocialProfileLinkTypes } from "redux/User/types";
import { FLAGS, useFeatureIsOn } from "@komi-app/flags-sdk";
import { ModuleType } from "@komi-app/shared-types";
import { useCreatorTracking, CreatorEvent } from "@komi-app/analytics-sdk";

export type IModulesContext = {
  modules: GroupItem[];
  selectedModules?: any[];
  loadingShop?: string[];
  thirdPartyData?: any[];
  scrollActiveId?: string;
  setScrollActiveId?: (value: string) => void;
  cancelSaveModule?: (module: GroupItem) => void;
  saveModule?: (module: GroupItem) => void;
  createModule?: (module: GroupItem) => void;
  deleteModule?: (module: GroupItem) => void;
  setModule?: (module: GroupItem) => void;
  addModule?: (module: GroupItem) => void;
  removeModule?: (module: GroupItem) => void;
  setModuleList?: (modules: GroupItem[]) => void;
  onDropEndModules?: (list: GroupItem[], result: DropResult) => void;
  setSelectedModules?: (value: any[]) => void;
  onChangeSelected?: (module: GroupItem) => (checked: boolean) => void;
  onGrouping?: () => void;
  onUnGroup?: (items: GroupItem[]) => void;
  onMoveGroup?: (group: GroupItem) => void;
  removeSelectedModules?: () => void;
  scrollToId?: (id: string) => void;
};

export const ModulesContext = React.createContext<IModulesContext>({
  modules: [],
  selectedModules: [],
  loadingShop: [],
  thirdPartyData: [],
});
interface OnboardingProviderProps {
  children: React.ReactNode;
}
const ModulesProvider = ({ children }: OnboardingProviderProps) => {
  const dispatch = useDispatch();

  const { localizationSelected } = useAutoSave();

  // TODO: Why is this `any`? Why not `GroupItem`?
  const [selectedModules, setSelectedModules] = useState<any>([]);

  // TOOD: Why is this `{}`? Why not `Record<string, GroupItem>`
  const [currentShopModules, setCurrentShopModules] = useState({});

  const [loadingShop, setLoadingShop] = useState<string[]>([]);
  const [thirdPartyData, setThirdPartyData] = useState<any>([]);
  const [scrollActiveId, setScrollActiveId] = useState<string>();
  const useAnalyticsSDK = useFeatureIsOn(FLAGS.USE_ANALYTICS_SDK_TALENT);
  const { createTracker } = useCreatorTracking();

  const moduleMovedIntroGroup = createTracker(
    CreatorEvent.MODULE_MOVED_INTRO_GROUP
  );
  const groupCreated = createTracker(CreatorEvent.GROUP_CREATED);

  const { sendSegmentEvent } = useAnalytics();

  const talentProfileModules = useTypedSelector(selectTalentProfileModules);
  const talentProfileModulesOrigin = useTypedSelector(
    selectTalentProfileModulesOrigin
  );

  const localizationModuleFixEnabled = useFeatureIsOn(
    FLAGS.LOCALIZATION_MODULE_ADD_FIX
  );

  const modules = talentProfileModules || [];

  /**
   * Memo value that contains a list of third party modules, only returns/sets a value when third party modules are used
   * Runs when defined or if a 3rd party module changes
   * The list consists of an array of module objects considered to be 3rd party
   * 
   * Example value:
    {
      "8d2f2fd4-da0e-4e85-a276-1eff0d54fd0a": {
        "url": "https://www.youtube.com/channel/UC8JfkMtNAp44vmzdtnL4wow",
        "totalSelected": 1,
        "sort": "newest",
        "type": "YOUTUBE_COLLECTION"
      },
      "492361e3-7963-4797-869a-988986b21b32": {
        "url": "https://www.youtube.com/c/TheInfographicsShowOFFICIAL",
        "totalSelected": 2,
        "sort": "newest",
        "type": "YOUTUBE_COLLECTION"
      }
    }
  */
  const thirdPartyModules = useMemo(() => {
    return reduce(
      modules,
      (modules: any, module) => {
        // If the module is of a specific group of types
        if (
          [
            ModuleType.SHOP_MY_SHELF,
            ModuleType.SHOP_LIST,
            ModuleType.BANDSINTOWN,
            ModuleType.SEATED,
            ModuleType.YOUTUBE_COLLECTION,
            ModuleType.PODCAST_AUTOMATION,
          ].includes(module.type as ModuleType)
        ) {
          // If the first element in the module is null, return
          const element = module.items[0] as any;
          if (!element) {
            return modules;
          }
          //refactor here

          // Else, return this batshit crazy generalization of all modules
          // Why would all modules need a `seatedId`???
          /* Generalized third party modules, the unused fields are left undefined */
          return {
            ...modules,
            [module.id as any]: {
              url: element.url,
              links: element.links,
              totalSelected: element.totalSelected,
              numberOfEpisodes: element.numberOfEpisodes,
              sort: element.sort,
              isDifferent: element.isDifferent,
              type: module.type,
              seatedId: element.seatedId,
            },
          };
        }

        // If this is a module group, then the items themselves are modules
        if (ModuleType.GROUP) {
          const items = reduce(
            module.items,
            // This reducer function is performing the same functionality as the above
            (accGroupedModules: any, groupModule: any) => {
              if (
                [
                  ModuleType.SHOP_MY_SHELF,
                  ModuleType.SHOP_LIST,
                  ModuleType.BANDSINTOWN,
                  ModuleType.SEATED,
                  ModuleType.YOUTUBE_COLLECTION,
                  ModuleType.PODCAST_AUTOMATION,
                ].includes(groupModule.type as ModuleType)
              ) {
                const data = groupModule.items[0] as any;
                return {
                  ...accGroupedModules,
                  [groupModule.id as any]: {
                    url: data.url,
                    links: data.links,
                    totalSelected: data.totalSelected,
                    numberOfEpisodes: data.numberOfEpisodes,
                    sort: data.sort,
                    isDifferent: data.isDifferent,
                    type: groupModule.type,
                    seatedId: data.seatedId,
                  },
                };
              }
              return accGroupedModules;
            },
            {}
          );

          return { ...modules, ...items };
        }
        return modules;
      },
      {}
    );
  }, [modules]);

  // The shop module `useEffect`
  useEffect(() => {
    if (!modules.length) {
      return;
    }

    // TODO: Figure out exactly what this does
    // Seems to be checking if the third party modules are different from the current shop modules
    // In terms of `getPatch`, thirdPartyModules is the target, and currentShopModules is the source
    // `modulesPatch` value is the properties added/changed from source to target
    const modulesPatch = JSONService.getPatch(
      thirdPartyModules,
      currentShopModules,
      {
        actions: ["add", "remove", "replace"],
        ignoreFields: [],
      }
    );

    // Fetches data for shopMyShelf
    const fetchData = async (moduleMap: any) => {
      const moduleIds = Object.keys(moduleMap);
      setLoadingShop(moduleIds);

      // For each module, check module type and fetch data
      const data = await Promise.all(
        moduleIds.map(async (moduleId: string) => {
          try {
            const module = moduleMap[moduleId];

            // Fetches metadata for Seated
            if (module.type === ModuleType.SEATED) {
              if (!module.seatedId) {
                return {
                  status: "not-found",
                  events: [],
                  id: moduleId,
                };
              }
              const result: any = await seatedService.getMetadataSeated(
                module.seatedId
              );

              if (result.ok) {
                return {
                  status: "success",
                  events: result.response,
                  id: moduleId,
                };
              }
              return {
                status: result.kind,
                events: [],
                id: moduleId,
              };
            }

            // Fetches metadata for Bandsintown
            if (module.type === ModuleType.BANDSINTOWN) {
              // Find any module or module group that has the current module ID
              const module: any = modules.find(
                (module) =>
                  module.id === moduleId ||
                  (module.type === ModuleType.GROUP &&
                    module.items.some(
                      (groupedModule: any) => groupedModule.id === moduleId
                    ))
              );

              // Get the first element of the module
              let bandsintown = module?.items?.[0];
              // If the module is a group, get the corresponding module, then get the first item.
              // TODO: Why not do this above? Why do the check twice?
              if (module?.type === ModuleType.GROUP) {
                const element = module?.items.find(
                  (groupedModule: any) => groupedModule.id === moduleId
                );
                bandsintown = element?.items?.[0];
              }

              // Validation of the module
              if (!bandsintown || !bandsintown?.metadata) {
                return {
                  status: "not-found",
                  events: [],
                  id: moduleId,
                };
              }

              // Use the Komi Bandsintown API to fetch results for the last year
              const result: any =
                await bandsintownService.getArtistEventsByName(bandsintown, {
                  date: `${moment().utc().format("YYYY-MM-DD")},${moment()
                    .add(1, "year")
                    .utc()
                    .format("YYYY-MM-DD")}`,
                });

              if (result.ok) {
                return {
                  status: "success",
                  events: result.response,
                  id: moduleId,
                };
              }
              return {
                status: result.kind,
                events: [],
                id: moduleId,
              };
            }

            // Fetches metadata for a YouTube collection
            if (module.type === ModuleType.YOUTUBE_COLLECTION) {
              // Find any module or module group that has the current module ID
              const module: any = modules.find(
                (module) =>
                  module.id === moduleId ||
                  (module.type === ModuleType.GROUP &&
                    module.items.some(
                      (groupedModule: any) => groupedModule.id === moduleId
                    ))
              );

              // Get the first element of the module
              let youtube = module?.items?.[0];
              // If the module is a group, get the corresponding module, then get the first item.
              // TODO: Why not do this above? Why do the check twice?
              if (module?.type === ModuleType.GROUP) {
                const element = module?.items.find(
                  (el: any) => el.id === moduleId
                );
                youtube = element?.items?.[0];
              }

              // Validate the element
              if (!youtube) {
                return {
                  status: "not-found",
                  collection: {
                    title: "",
                    items: [],
                  },
                  id: moduleId,
                };
              }

              // Use the Komi YouTube API to fetch results
              const result: any = await talentService().getYoutubeCollection(
                pick(youtube, ["url", "sort", "totalSelected"])
              );

              if (result.ok) {
                return {
                  status: "success",
                  collection: result.response,
                  id: moduleId,
                };
              }
              return {
                status: result.kind,
                collection: {
                  title: "",
                  items: [],
                },
                id: moduleId,
              };
            }

            // Fetches podcast metadata
            if (module.type === ModuleType.PODCAST_AUTOMATION) {
              // Find any module or module group that has the current module ID
              const module: any = modules.find(
                (module) =>
                  module.id === moduleId ||
                  (module.type === ModuleType.GROUP &&
                    module.items.some(
                      (groupedModule: any) => groupedModule.id === moduleId
                    ))
              );

              // Get the first element of the module
              let podcast = module?.items?.[0];
              // If the module is a group, get the corresponding module, then get the first item.
              // TODO: Why not do this above? Why do the check twice?
              if (module?.type === ModuleType.GROUP) {
                const element = module?.items.find(
                  (el: any) => el.id === moduleId
                );
                podcast = element?.items?.[0];
              }

              // Validation of the module
              if (!podcast) {
                return {
                  status: "not-found",
                  collection: {
                    items: [],
                    warning: undefined,
                  },
                  id: moduleId,
                };
              }

              // Add all separate podcast results to the same object
              const syncParams = reduce(
                podcast.links,
                (result, item) => {
                  if (!item.url) {
                    return result;
                  }

                  // Set parameters according the podcast type
                  // TODO: Why do this? Why not set the podcast type and keep this generic?
                  // Seems like the reason for separate names is to add all to the same object
                  // Perhaps there's a better structure, e.g., Record<SocialProfileLinkTypes, { url, active }>
                  //   if the must remain as an object over an array
                  if (item.type === SocialProfileLinkTypes.APPLE_PODCAST) {
                    return {
                      ...result,
                      appleUrl: item.url,
                      appleActive: item.active,
                    };
                  }
                  return {
                    ...result,
                    spotifyUrl: item.url,
                    spotifyActive: item.active,
                  };
                },
                {}
              );

              // Use the Komi latest podcast API to fetch results
              const result: any = await talentService().getSyncLatestPodcasts({
                ...syncParams,
                totalSelected: podcast.numberOfEpisodes,
                isDifferent: podcast.isDifferent,
                firstIndex: podcast.links[0].type,
              });

              if (result.ok) {
                return {
                  status: "success",
                  collection: {
                    items: result.response.episodes,
                    warning: result.response.message,
                  },
                  id: moduleId,
                };
              }
              return {
                status: result.kind,
                collection: {
                  warning: result.response.message,
                  items: [],
                },
                id: moduleId,
              };
            }

            // Get the module ID from the URL
            // TODO: Double check that this is actually the module ID, and not some other form
            //
            const id = getSlugFromUrl(module.url || "");
            if (!id) {
              return;
            }
            const result: any = await shopMyShelfService.getCollectionById(id);

            if (result.ok) {
              return {
                status: "success",
                products: result.response.products,
                id: moduleId,
              };
            }
            return {
              status: result.kind,
              products: [],
              id: moduleId,
            };
          } catch (error) {
            return {
              status: "no-connect",
              products: [],
              id: moduleId,
            };
          }
        })
      );

      //
      setThirdPartyData((values: any) => {
        return reduce(
          data,
          (result: any, item: any) => {
            const index = result.findIndex((el: any) => el.id === item.id);
            if (index !== -1) {
              return ArrayServices.updateWithIndex(result, index, item);
            }
            return [...result, item];
          },
          [...values]
        );
      });

      setLoadingShop([]);
    };

    if (!isEmpty(modulesPatch)) {
      setCurrentShopModules(thirdPartyModules);
      fetchData(modulesPatch);
      return;
    }
  }, [thirdPartyModules, currentShopModules]);

  useEffect(() => {
    if (selectedModules.length) {
      setSelectedModules([]);
    }
  }, [localizationSelected]);

  const createModule = useCallback(
    (moduleAdd: GroupItem) => {
      dispatch(createTalentProfileModuleActions.REQUEST(moduleAdd));
    },
    [dispatch]
  );

  const deleteModule = useCallback(
    (moduleRemove: GroupItem) => {
      dispatch(deleteTalentProfileModuleActions.REQUEST(moduleRemove));
    },
    [dispatch]
  );

  const setModuleList = useCallback(
    (list: GroupItem[]) => {
      // insertAfterLocalization is a fix we're testing that will add
      // modules to the page after localization modules have been loaded
      dispatch(
        setTalentProfileModuleListActions({
          modules: list,
          insertAfterLocalization: localizationModuleFixEnabled,
        })
      );
    },
    [dispatch, localizationModuleFixEnabled]
  );

  const setModuleGroup = (moduleEdit: GroupItem) => {
    const moduleGroup = modules.find((el) => el.id === moduleEdit.groupId);

    if (moduleGroup) {
      // Get a new items array for the group with the module we just updated into it
      const items = ArrayServices.updateWithId(
        moduleGroup.items || [],
        moduleEdit.id as string,
        moduleEdit
      );

      // Set the module group items array to our new updated one
      moduleGroup.items = items;

      dispatch(
        setTalentProfileModuleActions({
          ...moduleGroup,
          isEdit: false,
          // If all modules in the group are hidden, hide the group
          visible:
            moduleGroup.items.filter((i) => i.visible === true).length > 0,
        })
      );
    }

    return;
  };

  const setModule = useCallback(
    (moduleEdit: GroupItem) => {
      // Quick fix to always check new items have visiblity defaulted to true
      moduleEdit.items.forEach((item) => {
        if (item.visible === undefined) {
          item.visible = true;
        }
      });

      let previousVisibility;

      // If the module is part of a group
      if (moduleEdit.groupId) {
        // Find the existing module group
        const group = modules.find(
          (el) => el.id === moduleEdit.groupId
        ) as GroupItem;
        // Then check in the items for that modules previous visibility
        previousVisibility = (group.items as GroupItem[]).find(
          (i) => i.id === moduleEdit.id
        )?.visible;
      } else {
        // Get the previous visibility from modules
        previousVisibility = modules.find(
          (el) => el.id === moduleEdit.id
        )?.visible;
      }

      // If the module wasn't visible but now is
      if (moduleEdit.visible && !previousVisibility) {
        // Show all items
        moduleEdit.items.forEach((i) => {
          i.visible = true;
          i.isUpdate = true;
          // And if we're modifying a group
          if (isModuleGroup(moduleEdit)) {
            // Then show all then items' items
            (i as TalentProfileModuleGroup<TalentModuleMixItem>).items.forEach(
              (item) => {
                item.visible = true;
                i.isUpdate = true;
              }
            );
          }
        });
      } // else if it was visible but now isn't
      else if (!moduleEdit.visible && previousVisibility) {
        // Hide all items
        moduleEdit.items.forEach((i) => {
          i.visible = false;
          i.isUpdate = false;
          // And if we're modifying a group
          if (isModuleGroup(moduleEdit)) {
            // Then hide all then items' items
            (i as GroupItem).items.forEach((item) => {
              item.visible = false;
              item.isUpdate = false;
            });
          }
        });
      } // Else if the module visilibity hasn't changed but all items are hidden
      else if (
        moduleEdit.visible === previousVisibility &&
        moduleEdit.items.filter((x) => x.visible === true).length === 0
      ) {
        // Hide the module
        moduleEdit.visible = false;
      }

      // if the module is in a group, we need to update the actual group
      if (moduleEdit.groupId) {
        setModuleGroup(moduleEdit);
        return;
      }

      dispatch(setTalentProfileModuleActions(moduleEdit));
    },
    [dispatch, modules]
  );

  const addModule = useCallback(
    (moduleAdd: GroupItem) => {
      dispatch(addTalentProfileModuleActions(moduleAdd));
    },
    [dispatch]
  );

  const removeModule = useCallback(
    (moduleRemove: GroupItem) => {
      if (moduleRemove.groupId) {
        const newModules = [...modules];
        const module = newModules.find(
          (item) => item.id === moduleRemove.groupId
        );
        if (module) {
          module.items = [...module.items].filter(
            (el: any) => el.id !== moduleRemove.id
          );
        }
        const data = flattenAndOrderGroupModules(newModules);
        dispatch(setTalentProfileModuleListActions({ modules: data }));
        setSelectedModules([]);
        return;
      }
      dispatch(removeTalentProfileModuleActions(moduleRemove));
    },
    [dispatch, modules]
  );

  const removeSelectedModules = useCallback(() => {
    if (selectedModules.length === 1) {
      removeModule(selectedModules[0]);
      dispatch(setActiveModuleAction(undefined));
      setSelectedModules([]);
      return;
    }
    const currentModules = reduce(
      modules,
      (result: any, item: GroupItem) => {
        if (
          selectedModules.some((el: GroupItem) =>
            [el.groupId, el.id].includes(item.id)
          )
        ) {
          if (item.type === ModuleType.GROUP) {
            const module = {
              ...item,
              items: item.items.filter(
                (module: any) =>
                  !selectedModules.some((el: GroupItem) => el.id === module.id)
              ),
            };
            result.push(module);
          }
          return result;
        }
        result.push(item);
        return result;
      },
      []
    );

    const data = flattenAndOrderGroupModules(currentModules);
    setModuleList(data);
    dispatch(setActiveModuleAction(undefined));
    setSelectedModules([]);
  }, [dispatch, modules, selectedModules, setModuleList]);

  const saveModule = useCallback(
    (moduleSave: GroupItem) => {
      // create first
      if (moduleSave.isCreate) {
        dispatch(createTalentProfileModuleActions.REQUEST(moduleSave));
        return;
      }
      if (moduleSave.isUpdate) {
        dispatch(updateTalentProfileModuleActions.REQUEST(moduleSave));
        return;
      }
    },
    [dispatch]
  );

  const onChangeSelected = useCallback(
    (module: GroupItem) => (checked: boolean) => {
      if (module.type === ModuleType.GROUP) {
        setSelectedModules((values: any) => {
          if (!checked) {
            return values.filter(
              (item: any) => !module.items.some((el: any) => el.id === item.id)
            );
          }
          return [
            ...values.filter(
              (item: any) =>
                !selectedModules.some((el: any) => el.id === item.id)
            ),
            ...module.items,
          ];
        });
        return;
      }
      setSelectedModules((values: any) => {
        if (!checked) {
          return values.filter((item: any) => item.id !== module.id);
        }
        return [...values, module];
      });
    },
    [dispatch]
  );

  const cancelSaveModule = useCallback(
    (moduleRevert: GroupItem) => {
      dispatch(revertTalentProfileModuleActions(moduleRevert));
    },
    [dispatch]
  );

  /**
   * Legacy function that isn't used anywhere, the drag and drop context is used instead to handle drop end operations
   */
  // <LEGACY>
  const onDropEndModules = useCallback(
    (list: GroupItem[], result: DropResult) => {
      const newList = list.map((mod, index) => ({
        ...mod,
        order: index,
      }));

      dispatch(setTalentProfileModuleListActions({ modules: newList }));

      // handle talent profile orgiin
      const { destination, source } = result;
      if (!destination) {
        return;
      }
      const newListOrigin = ArrayServices.reorder(
        talentProfileModulesOrigin || [],
        source.index,
        destination.index
      );
      dispatch(setTalentProfileModulesOriginActions(newListOrigin));
      dispatch(setActiveModuleAction(null));
    },
    [dispatch, talentProfileModulesOrigin]
  );
  // <LEGACY>

  /**
   * Sets selected items by filtering through modules and checking if their're id's match the selected id's,
   * checks groups as well
   */
  const selectedItems = useMemo(() => {
    const sortedItems = sortBy(
      modules.filter((module: any) =>
        selectedModules.some((selectedModule: any) => {
          if (selectedModule.groupId) {
            return selectedModule.groupId === module.id;
          }
          return selectedModule.id === module.id;
        })
      ),
      "order"
    );

    return sortedItems;
  }, [selectedModules, modules]);

  /**
   * Flattens and orders modules in groups when modules are added to a group or modules are ungrouped
   */
  const flattenAndOrderGroupModules = useCallback((data: GroupItem[]) => {
    return reduce(
      data,
      (result: GroupItem[], item: GroupItem) => {
        // No modifications if there are no modules in the group
        if (item.type === ModuleType.GROUP && !item.items?.length) {
          return result;
        }
        // Extract the only item, and order to the bottom, from a group module,
        // if there is only one item. Essentially, flatten single-item group modules
        if (item.type === ModuleType.GROUP && item.items?.length === 1) {
          result.push({
            ...item.items[0],
            groupId: undefined,
            order: result.length,
          } as GroupItem);
          return result;
        }

        // Order group modules with more than one item to the bottom
        result.push({ ...item, order: result.length });
        return result;
      },
      []
    );
  }, []);

  // Scroll to a module by its ID
  const scrollToId = useCallback(
    (id: string) => {
      // TODO: Error handling
      try {
        const scroll = document.getElementById(id);

        scroll?.scrollIntoView({ behavior: "smooth", block: "start" });
      } catch (error) {}
    },
    [document]
  );

  /**
   * Function that handles moving items into groups or groups into groups,
   * occurs when at least 1 group exists to move a module(s) into
   * using the checkbox and 'Move to' button in the popup menu
   */
  const onMoveGroup = useCallback(
    (movingGroup: GroupItem) => () => {
      // Reset the selected modules state
      setSelectedModules([]);

      const moveModules = reduce(
        modules,
        (movingModules: any, module: GroupItem) => {
          // Match any module group with the same ID
          if (
            module.type === ModuleType.GROUP &&
            module.id === movingGroup.id
          ) {
            // Map any selected module so that `showTitle` is true
            // It only happens once, if there's only one module group with the ID
            const data = selectedModules.map((selectedModule: any) => ({
              ...selectedModule,
              showTitle: true,
            }));

            // Add an order and a group ID to each element
            const moduleGroup = {
              ...module,
              items: [...data, ...module.items].map((el, index: number) => ({
                ...el,
                order: index,
                groupId: module.id,
              })),
            };

            movingModules.push(moduleGroup);
            return movingModules;
          }

          // If any currently selected module has the same group or module ID
          if (
            selectedModules.some(
              (selectedModule: GroupItem) =>
                selectedModule.groupId === module.id ||
                selectedModule.id === module.id
            )
          ) {
            if (module.type === ModuleType.GROUP) {
              // If it's a group module,
              const moduleGroup = {
                ...module,
                items: reduce(
                  module.items,
                  (elements: any, element: any) => {
                    // If any previously selected module has an element with the same ID
                    if (
                      !selectedModules.some(
                        (selectedModule: GroupItem) =>
                          selectedModule.id === element.id
                      )
                    ) {
                      // Set the order equal to the end of the elements list
                      return [
                        ...elements,
                        { ...element, order: elements.length },
                      ];
                    }
                    return elements;
                  },
                  []
                ),
              };
              movingModules.push(moduleGroup);
            }
            return movingModules;
          }
          movingModules.push(module);
          return movingModules;
        },
        []
      );

      const data = flattenAndOrderGroupModules(moveModules);

      dispatch(
        setTalentProfileModuleListActions({
          modules: data,
          previousModulesVersion: modules,
        })
      );
      dispatch(setActiveModuleAction(undefined));

      // Send segment event for module grouping
      const method = "Action Bar";
      const types = selectedModules.map((el: any) => el.type).join(", ");

      useAnalyticsSDK
        ? moduleMovedIntroGroup({
            method,
            types,
          })
        : sendSegmentEvent(SEGMENT_EVENTS.MODULE_MOVED_INTRO_GROUP, {
            "Module Types Moved": types,
            "Move Method": method,
          });

      // Scroll to group ID
      setTimeout(() => {
        movingGroup && scrollToId(`${movingGroup.id}`);
      }, 150);
    },
    [
      modules,
      selectedModules,
      setModuleList,
      sendSegmentEvent,
      flattenAndOrderGroupModules,
    ]
  );

  // Event performed when a module is grouped
  const onGrouping = useCallback(() => {
    const id = uuidv4();
    const index = selectedItems[0].order;

    setSelectedModules([]);

    // Sort modules by order
    const items = reduce(
      sortBy(selectedItems, "order"),
      (modules: any, module: any) => {
        if (module.type === ModuleType.GROUP) {
          const moduleGroup = reduce(
            module.items,
            (groupedModules: any, groupedModule) => {
              // If the selected module is equal to the grouped module
              if (
                selectedModules.some(
                  (selectedModule: any) =>
                    selectedModule.id === groupedModule.id
                )
              ) {
                // Add an order property
                return [
                  ...groupedModules,
                  { ...groupedModule, order: groupedModule.length },
                ];
              }
              return groupedModules;
            },
            []
          );
          return [...modules, ...moduleGroup];
        }
        // Concatenate module to results
        return [...modules, module];
      },
      []
    );

    // Default module group properties
    const newModuleGroup = {
      id: id,
      order: 0,
      name: "New Module Group",
      type: ModuleType.GROUP,
      items: items.map((item: any, index: number) => ({
        ...item,
        groupId: id,
        order: index,
        showTitle: true,
      })),
      expand: true,
      isEdit: true,
      isCreate: true,
      isLoading: false,
    };

    // Format/Order rest of the modules(the modules/groups which are not selected in current group)
    const list = reduce(
      modules,
      (results: any, module) => {
        if (
          selectedModules.some(
            (selectedModule: GroupItem) => selectedModule.id === module.id
          )
        ) {
          return results;
        }

        if (
          module.type === ModuleType.GROUP &&
          module.items.some((module: any) =>
            selectedModules.some((el: GroupItem) => el.id === module.id)
          )
        ) {
          const moduleGroup = reduce(
            module.items,
            (moduleItems: any, module: any) => {
              if (
                selectedModules.some(
                  (selectedModule: GroupItem) => selectedModule.id === module.id
                )
              ) {
                return moduleItems;
              }
              return [...moduleItems, { ...module, order: moduleItems.length }];
            },
            []
          );

          return [
            ...results,
            { ...module, items: moduleGroup, order: results.length },
          ];
        }
        return [
          ...results,
          {
            ...module,
            order: results.length,
          },
        ];
      },
      []
    );

    const data = flattenAndOrderGroupModules(
      ArrayServices.reorder([newModuleGroup, ...list], 0, index)
    );
    dispatch(
      setTalentProfileModuleListActions({
        modules: data,
        previousModulesVersion: modules,
      })
    );

    const types = selectedModules.map((el: any) => el.type).join(", ");

    useAnalyticsSDK
      ? groupCreated({ types })
      : sendSegmentEvent(SEGMENT_EVENTS.GROUP_CREATED, {
          "Module Types Grouped": types,
        });

    dispatch(setActiveModuleAction(id));
    setTimeout(() => {
      scrollToId(`${newModuleGroup.id}`);
    }, 150);
  }, [
    selectedModules,
    sendSegmentEvent,
    flattenAndOrderGroupModules,
    selectedItems,
    modules,
    scrollToId,
  ]);

  const onUnGroup = useCallback(
    (items: GroupItem[]) => {
      try {
        setSelectedModules([]);
        const oldModules = modules.slice();
        // remove groupId for the items in group
        const selected = sortBy(items, "order").map((item) => ({
          ...item,
          groupId: null,
        }));

        // get the items in group from all modules list
        const group = oldModules.find(
          (item: GroupItem) => item.id === items[0].groupId
        );
        if (group) {
          //if there are items in group which are not in the selected items(items to be ungrouped), add them to newItems
          const newItems = reduce(
            group.items,
            (result: any, item: any) => {
              if (!items.some((el: GroupItem) => el.id === item.id)) {
                return [...result, { ...item, order: result.length }];
              }
              return result;
            },
            []
          );
          const newGroup = { ...group, items: newItems };
          // Replacing items of existing group module with the newItems from above
          const list = ArrayServices.updateWithId(
            oldModules,
            group.id as string,
            newGroup
          );
          const newModules = [
            ...take(list, group.order + 1),
            ...selected,
            ...takeRight(list, list.length - group.order - 1),
          ];
          const data = flattenAndOrderGroupModules(newModules);
          dispatch(
            setTalentProfileModuleListActions({
              modules: data,
              previousModulesVersion: modules,
            })
          );
          setTimeout(() => {
            dispatch(setActiveModuleAction(undefined));
          }, 50);
        }
      } catch (error) {}
    },
    [modules, flattenAndOrderGroupModules, dispatch]
  );

  return (
    <ModulesContext.Provider
      value={{
        modules,
        cancelSaveModule,
        saveModule,
        createModule,
        deleteModule,
        setModule,
        addModule,
        removeModule,
        setModuleList,
        selectedModules,
        setSelectedModules,
        onDropEndModules,
        onChangeSelected,
        onGrouping,
        onUnGroup,
        onMoveGroup,
        removeSelectedModules,
        scrollToId,
        loadingShop,
        thirdPartyData,
        scrollActiveId,
        setScrollActiveId,
      }}
    >
      {children}
    </ModulesContext.Provider>
  );
};
export default ModulesProvider;
