import * as Sentry from '@sentry/browser';
import { sortBy } from 'lodash';
import {
  BoardType,
  ListType,
  inactiveId,
  flashAnimationDuration,
  ISSUABLE,
  titleQueries,
  subscriptionQueries,
  deleteListQueries,
  listsQuery,
  updateListQueries,
  issuableTypes,
  FilterFields,
  ListTypeTitles,
  DraggableItemTypes,
} from 'ee_else_ce/boards/constants';
import createBoardListMutation from 'ee_else_ce/boards/graphql/board_list_create.mutation.graphql';
import issueMoveListMutation from 'ee_else_ce/boards/graphql/issue_move_list.mutation.graphql';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { queryToObject } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import {
  formatBoardLists,
  formatListIssues,
  formatListsPageInfo,
  formatIssue,
  formatIssueInput,
  updateListPosition,
  moveItemListHelper,
  getMoveData,
  FiltersInfo,
  filterVariables,
} from '../boards_util';
import { gqlClient } from '../graphql';
import boardLabelsQuery from '../graphql/board_labels.query.graphql';
import groupBoardMilestonesQuery from '../graphql/group_board_milestones.query.graphql';
import groupProjectsQuery from '../graphql/group_projects.query.graphql';
import issueCreateMutation from '../graphql/issue_create.mutation.graphql';
import issueSetLabelsMutation from '../graphql/issue_set_labels.mutation.graphql';
import listsIssuesQuery from '../graphql/lists_issues.query.graphql';
import projectBoardMilestonesQuery from '../graphql/project_board_milestones.query.graphql';

import * as types from './mutation_types';

export default {
  setInitialBoardData: ({ commit }, data) => {
    commit(types.SET_INITIAL_BOARD_DATA, data);
  },

  setActiveId({ commit }, { id, sidebarType }) {
    commit(types.SET_ACTIVE_ID, { id, sidebarType });
  },

  unsetActiveId({ dispatch }) {
    dispatch('setActiveId', { id: inactiveId, sidebarType: '' });
  },

  setFilters: ({ commit, state: { issuableType } }, filters) => {
    commit(
      types.SET_FILTERS,
      filterVariables({
        filters,
        issuableType,
        filterInfo: FiltersInfo,
        filterFields: FilterFields,
      }),
    );
  },

  performSearch({ dispatch }) {
    dispatch(
      'setFilters',
      convertObjectPropsToCamelCase(queryToObject(window.location.search, { gatherArrays: true })),
    );
    dispatch('fetchLists');
    dispatch('resetIssues');
  },

  fetchLists: ({ commit, state, dispatch }) => {
    const { boardType, filterParams, fullPath, fullBoardId, issuableType } = state;

    const variables = {
      fullPath,
      boardId: fullBoardId,
      filters: filterParams,
      ...(issuableType === issuableTypes.issue && {
        isGroup: boardType === BoardType.group,
        isProject: boardType === BoardType.project,
      }),
    };

    return gqlClient
      .query({
        query: listsQuery[issuableType].query,
        variables,
      })
      .then(({ data }) => {
        const { lists, hideBacklogList } = data[boardType]?.board;
        commit(types.RECEIVE_BOARD_LISTS_SUCCESS, formatBoardLists(lists));
        // Backlog list needs to be created if it doesn't exist and it's not hidden
        if (!lists.nodes.find((l) => l.listType === ListType.backlog) && !hideBacklogList) {
          dispatch('createList', { backlog: true });
        }
      })
      .catch(() => commit(types.RECEIVE_BOARD_LISTS_FAILURE));
  },

  highlightList: ({ commit, state }, listId) => {
    if ([ListType.backlog, ListType.closed].includes(state.boardLists[listId].listType)) {
      return;
    }

    commit(types.ADD_LIST_TO_HIGHLIGHTED_LISTS, listId);

    setTimeout(() => {
      commit(types.REMOVE_LIST_FROM_HIGHLIGHTED_LISTS, listId);
    }, flashAnimationDuration);
  },

  createList: ({ dispatch }, { backlog, labelId, milestoneId, assigneeId }) => {
    dispatch('createIssueList', { backlog, labelId, milestoneId, assigneeId });
  },

  createIssueList: (
    { state, commit, dispatch, getters },
    { backlog, labelId, milestoneId, assigneeId, iterationId },
  ) => {
    const { fullBoardId } = state;

    const existingList = getters.getListByLabelId(labelId);

    if (existingList) {
      dispatch('highlightList', existingList.id);
      return;
    }

    gqlClient
      .mutate({
        mutation: createBoardListMutation,
        variables: {
          boardId: fullBoardId,
          backlog,
          labelId,
          milestoneId,
          assigneeId,
          iterationId,
        },
      })
      .then(({ data }) => {
        if (data.boardListCreate?.errors.length) {
          commit(types.CREATE_LIST_FAILURE, data.boardListCreate.errors[0]);
        } else {
          const list = data.boardListCreate?.list;
          dispatch('addList', list);
          dispatch('highlightList', list.id);
        }
      })
      .catch((e) => {
        commit(types.CREATE_LIST_FAILURE);
        throw e;
      });
  },

  addList: ({ commit, dispatch, getters }, list) => {
    commit(types.RECEIVE_ADD_LIST_SUCCESS, updateListPosition(list));

    dispatch('fetchItemsForList', {
      listId: getters.getListByTitle(ListTypeTitles.backlog)?.id,
    });
  },

  fetchLabels: ({ state, commit }, searchTerm) => {
    const { fullPath, boardType } = state;

    const variables = {
      fullPath,
      searchTerm,
      isGroup: boardType === BoardType.group,
      isProject: boardType === BoardType.project,
    };

    commit(types.RECEIVE_LABELS_REQUEST);

    return gqlClient
      .query({
        query: boardLabelsQuery,
        variables,
      })
      .then(({ data }) => {
        const labels = data[boardType]?.labels.nodes;

        commit(types.RECEIVE_LABELS_SUCCESS, labels);
        return labels;
      })
      .catch((e) => {
        commit(types.RECEIVE_LABELS_FAILURE);
        throw e;
      });
  },

  fetchMilestones({ state, commit }, searchTerm) {
    commit(types.RECEIVE_MILESTONES_REQUEST);

    const { fullPath, boardType } = state;

    const variables = {
      fullPath,
      searchTerm,
    };

    let query;
    if (boardType === BoardType.project) {
      query = projectBoardMilestonesQuery;
    }
    if (boardType === BoardType.group) {
      query = groupBoardMilestonesQuery;
    }

    if (!query) {
      // eslint-disable-next-line @gitlab/require-i18n-strings
      throw new Error('Unknown board type');
    }

    return gqlClient
      .query({
        query,
        variables,
      })
      .then(({ data }) => {
        const errors = data[boardType]?.errors;
        const milestones = data[boardType]?.milestones.nodes;

        if (errors?.[0]) {
          throw new Error(errors[0]);
        }

        commit(types.RECEIVE_MILESTONES_SUCCESS, milestones);

        return milestones;
      })
      .catch((e) => {
        commit(types.RECEIVE_MILESTONES_FAILURE);
        throw e;
      });
  },

  moveList: (
    { state: { boardLists }, commit, dispatch },
    {
      item: {
        dataset: { listId: movedListId, draggableItemType },
      },
      newIndex,
      to: { children },
    },
  ) => {
    if (draggableItemType !== DraggableItemTypes.list) {
      return;
    }

    const displacedListId = children[newIndex].dataset.listId;
    if (movedListId === displacedListId) {
      return;
    }

    const listIds = sortBy(
      Object.keys(boardLists).filter(
        (listId) =>
          listId !== movedListId &&
          boardLists[listId].listType !== ListType.backlog &&
          boardLists[listId].listType !== ListType.closed,
      ),
      (i) => boardLists[i].position,
    );

    const targetPosition = boardLists[displacedListId].position;
    // When the dragged list moves left, displaced list should shift right.
    const shiftOffset = Number(boardLists[movedListId].position < targetPosition);
    const displacedListIndex = listIds.findIndex((listId) => listId === displacedListId);

    commit(
      types.MOVE_LISTS,
      listIds
        .slice(0, displacedListIndex + shiftOffset)
        .concat([movedListId], listIds.slice(displacedListIndex + shiftOffset))
        .map((listId, index) => ({ listId, position: index })),
    );
    dispatch('updateList', { listId: movedListId, position: targetPosition });
  },

  updateList: (
    { state: { issuableType, boardItemsByListId = {} }, dispatch },
    { listId, position, collapsed },
  ) => {
    gqlClient
      .mutate({
        mutation: updateListQueries[issuableType].mutation,
        variables: {
          listId,
          position,
          collapsed,
        },
      })
      .then(({ data }) => {
        if (data?.updateBoardList?.errors.length) {
          throw new Error();
        }

        // Only fetch when board items havent been fetched on a collapsed list
        if (!boardItemsByListId[listId]) {
          dispatch('fetchItemsForList', { listId });
        }
      })
      .catch(() => {
        dispatch('handleUpdateListFailure');
      });
  },

  handleUpdateListFailure: ({ dispatch, commit }) => {
    dispatch('fetchLists');

    commit(
      types.SET_ERROR,
      s__('Boards|An error occurred while updating the board list. Please try again.'),
    );
  },

  toggleListCollapsed: ({ commit }, { listId, collapsed }) => {
    commit(types.TOGGLE_LIST_COLLAPSED, { listId, collapsed });
  },

  removeList: ({ state: { issuableType, boardLists }, commit, dispatch, getters }, listId) => {
    const listsBackup = { ...boardLists };

    commit(types.REMOVE_LIST, listId);

    return gqlClient
      .mutate({
        mutation: deleteListQueries[issuableType].mutation,
        variables: {
          listId,
        },
      })
      .then(
        ({
          data: {
            destroyBoardList: { errors },
          },
        }) => {
          if (errors.length > 0) {
            commit(types.REMOVE_LIST_FAILURE, listsBackup);
          } else {
            dispatch('fetchItemsForList', {
              listId: getters.getListByTitle(ListTypeTitles.backlog)?.id,
            });
          }
        },
      )
      .catch(() => {
        commit(types.REMOVE_LIST_FAILURE, listsBackup);
      });
  },

  fetchItemsForList: ({ state, commit }, { listId, fetchNext = false }) => {
    if (!listId) return null;

    if (!fetchNext) {
      commit(types.RESET_ITEMS_FOR_LIST, listId);
    }
    commit(types.REQUEST_ITEMS_FOR_LIST, { listId, fetchNext });

    const { fullPath, fullBoardId, boardType, filterParams } = state;

    const variables = {
      fullPath,
      boardId: fullBoardId,
      id: listId,
      filters: filterParams,
      isGroup: boardType === BoardType.group,
      isProject: boardType === BoardType.project,
      first: 10,
      after: fetchNext ? state.pageInfoByListId[listId].endCursor : undefined,
    };

    return gqlClient
      .query({
        query: listsIssuesQuery,
        context: {
          isSingleRequest: true,
        },
        variables,
      })
      .then(({ data }) => {
        const { lists } = data[boardType]?.board;
        const listItems = formatListIssues(lists);
        const listPageInfo = formatListsPageInfo(lists);
        commit(types.RECEIVE_ITEMS_FOR_LIST_SUCCESS, { listItems, listPageInfo, listId });
      })
      .catch(() => commit(types.RECEIVE_ITEMS_FOR_LIST_FAILURE, listId));
  },

  resetIssues: ({ commit }) => {
    commit(types.RESET_ISSUES);
  },

  moveItem: ({ dispatch }, payload) => {
    dispatch('moveIssue', payload);
  },

  moveIssue: ({ dispatch, state }, params) => {
    const moveData = getMoveData(state, params);

    dispatch('moveIssueCard', moveData);
    dispatch('updateMovedIssue', moveData);
    dispatch('updateIssueOrder', { moveData });
  },

  moveIssueCard: ({ commit }, moveData) => {
    const {
      reordering,
      shouldClone,
      itemNotInToList,
      originalIndex,
      itemId,
      fromListId,
      toListId,
      moveBeforeId,
      moveAfterId,
    } = moveData;

    commit(types.REMOVE_BOARD_ITEM_FROM_LIST, { itemId, listId: fromListId });

    if (reordering) {
      commit(types.ADD_BOARD_ITEM_TO_LIST, {
        itemId,
        listId: toListId,
        moveBeforeId,
        moveAfterId,
      });

      return;
    }

    if (itemNotInToList) {
      commit(types.ADD_BOARD_ITEM_TO_LIST, {
        itemId,
        listId: toListId,
        moveBeforeId,
        moveAfterId,
      });
    }

    if (shouldClone) {
      commit(types.ADD_BOARD_ITEM_TO_LIST, { itemId, listId: fromListId, atIndex: originalIndex });
    }
  },

  updateMovedIssue: (
    { commit, state: { boardItems, boardLists } },
    { itemId, fromListId, toListId },
  ) => {
    const updatedIssue = moveItemListHelper(
      boardItems[itemId],
      boardLists[fromListId],
      boardLists[toListId],
    );

    commit(types.UPDATE_BOARD_ITEM, updatedIssue);
  },

  undoMoveIssueCard: ({ commit }, moveData) => {
    const {
      reordering,
      shouldClone,
      itemNotInToList,
      itemId,
      fromListId,
      toListId,
      originalIssue,
      originalIndex,
    } = moveData;

    commit(types.UPDATE_BOARD_ITEM, originalIssue);

    if (reordering) {
      commit(types.REMOVE_BOARD_ITEM_FROM_LIST, { itemId, listId: fromListId });
      commit(types.ADD_BOARD_ITEM_TO_LIST, { itemId, listId: fromListId, atIndex: originalIndex });
      return;
    }

    if (shouldClone) {
      commit(types.REMOVE_BOARD_ITEM_FROM_LIST, { itemId, listId: fromListId });
    }
    if (itemNotInToList) {
      commit(types.REMOVE_BOARD_ITEM_FROM_LIST, { itemId, listId: toListId });
    }

    commit(types.ADD_BOARD_ITEM_TO_LIST, { itemId, listId: fromListId, atIndex: originalIndex });
  },

  updateIssueOrder: async ({ commit, dispatch, state }, { moveData, mutationVariables = {} }) => {
    try {
      const { itemId, fromListId, toListId, moveBeforeId, moveAfterId } = moveData;
      const {
        fullBoardId,
        boardItems: {
          [itemId]: { iid, referencePath },
        },
      } = state;

      const { data } = await gqlClient.mutate({
        mutation: issueMoveListMutation,
        variables: {
          iid,
          projectPath: referencePath.split(/[#]/)[0],
          boardId: fullBoardId,
          fromListId: getIdFromGraphQLId(fromListId),
          toListId: getIdFromGraphQLId(toListId),
          moveBeforeId: moveBeforeId ? getIdFromGraphQLId(moveBeforeId) : undefined,
          moveAfterId: moveAfterId ? getIdFromGraphQLId(moveAfterId) : undefined,
          // 'mutationVariables' allows EE code to pass in extra parameters.
          ...mutationVariables,
        },
      });

      if (data?.issueMoveList?.errors.length || !data.issueMoveList) {
        throw new Error('issueMoveList empty');
      }

      commit(types.MUTATE_ISSUE_SUCCESS, { issue: data.issueMoveList.issue });
    } catch {
      commit(
        types.SET_ERROR,
        s__('Boards|An error occurred while moving the issue. Please try again.'),
      );
      dispatch('undoMoveIssueCard', moveData);
    }
  },

  setAssignees: ({ commit }, { id, assignees }) => {
    commit('UPDATE_BOARD_ITEM_BY_ID', {
      itemId: id,
      prop: 'assignees',
      value: assignees,
    });
  },

  addListItem: ({ commit, dispatch }, { list, item, position, inProgress = false }) => {
    commit(types.ADD_BOARD_ITEM_TO_LIST, {
      listId: list.id,
      itemId: item.id,
      atIndex: position,
      inProgress,
    });
    commit(types.UPDATE_BOARD_ITEM, item);
    if (!inProgress) {
      dispatch('setActiveId', { id: item.id, sidebarType: ISSUABLE });
    }
  },

  removeListItem: ({ commit }, { listId, itemId }) => {
    commit(types.REMOVE_BOARD_ITEM_FROM_LIST, { listId, itemId });
    commit(types.REMOVE_BOARD_ITEM, itemId);
  },

  addListNewIssue: (
    { state: { boardConfig, boardType, fullPath }, dispatch, commit },
    { issueInput, list, placeholderId = `tmp-${new Date().getTime()}` },
  ) => {
    const input = formatIssueInput(issueInput, boardConfig);

    if (boardType === BoardType.project) {
      input.projectPath = fullPath;
    }

    const placeholderIssue = formatIssue({ ...issueInput, id: placeholderId, isLoading: true });
    dispatch('addListItem', { list, item: placeholderIssue, position: 0, inProgress: true });

    gqlClient
      .mutate({
        mutation: issueCreateMutation,
        variables: { input },
      })
      .then(({ data }) => {
        if (data.createIssue.errors.length) {
          throw new Error();
        }

        const rawIssue = data.createIssue?.issue;
        const formattedIssue = formatIssue(rawIssue);
        dispatch('removeListItem', { listId: list.id, itemId: placeholderId });
        dispatch('addListItem', { list, item: formattedIssue, position: 0 });
      })
      .catch(() => {
        dispatch('removeListItem', { listId: list.id, itemId: placeholderId });
        commit(
          types.SET_ERROR,
          s__('Boards|An error occurred while creating the issue. Please try again.'),
        );
      });
  },

  setActiveBoardItemLabels: ({ dispatch }, params) => {
    dispatch('setActiveIssueLabels', params);
  },

  setActiveIssueLabels: async ({ commit, getters }, input) => {
    const { activeBoardItem } = getters;

    if (!gon.features?.labelsWidget) {
      const { data } = await gqlClient.mutate({
        mutation: issueSetLabelsMutation,
        variables: {
          input: {
            iid: input.iid || String(activeBoardItem.iid),
            labelIds: input.labelsId ?? undefined,
            addLabelIds: input.addLabelIds ?? [],
            removeLabelIds: input.removeLabelIds ?? [],
            projectPath: input.projectPath,
          },
        },
      });

      if (data.updateIssue?.errors?.length > 0) {
        throw new Error(data.updateIssue.errors);
      }

      commit(types.UPDATE_BOARD_ITEM_BY_ID, {
        itemId: data.updateIssue?.issue?.id || activeBoardItem.id,
        prop: 'labels',
        value: data.updateIssue?.issue?.labels.nodes,
      });

      return;
    }

    let labels = input?.labels || [];
    if (input.removeLabelIds) {
      labels = activeBoardItem.labels.filter(
        (label) => input.removeLabelIds[0] !== getIdFromGraphQLId(label.id),
      );
    }
    commit(types.UPDATE_BOARD_ITEM_BY_ID, {
      itemId: input.id || activeBoardItem.id,
      prop: 'labels',
      value: labels,
    });
  },

  setActiveItemSubscribed: async ({ commit, getters, state }, input) => {
    const { activeBoardItem, isEpicBoard } = getters;
    const { fullPath, issuableType } = state;
    const workspacePath = isEpicBoard
      ? { groupPath: fullPath }
      : { projectPath: input.projectPath };
    const { data } = await gqlClient.mutate({
      mutation: subscriptionQueries[issuableType].mutation,
      variables: {
        input: {
          ...workspacePath,
          iid: String(activeBoardItem.iid),
          subscribedState: input.subscribed,
        },
      },
    });

    if (data.updateIssuableSubscription?.errors?.length > 0) {
      throw new Error(data.updateIssuableSubscription[issuableType].errors);
    }

    commit(types.UPDATE_BOARD_ITEM_BY_ID, {
      itemId: activeBoardItem.id,
      prop: 'subscribed',
      value: data.updateIssuableSubscription[issuableType].subscribed,
    });
  },

  setActiveItemTitle: async ({ commit, getters, state }, input) => {
    const { activeBoardItem, isEpicBoard } = getters;
    const { fullPath, issuableType } = state;
    const workspacePath = isEpicBoard
      ? { groupPath: fullPath }
      : { projectPath: input.projectPath };
    const { data } = await gqlClient.mutate({
      mutation: titleQueries[issuableType].mutation,
      variables: {
        input: {
          ...workspacePath,
          iid: String(activeBoardItem.iid),
          title: input.title,
        },
      },
    });

    if (data.updateIssuableTitle?.errors?.length > 0) {
      throw new Error(data.updateIssuableTitle.errors);
    }

    commit(types.UPDATE_BOARD_ITEM_BY_ID, {
      itemId: activeBoardItem.id,
      prop: 'title',
      value: data.updateIssuableTitle[issuableType].title,
    });
  },

  setActiveItemConfidential: ({ commit, getters }, confidential) => {
    const { activeBoardItem } = getters;
    commit(types.UPDATE_BOARD_ITEM_BY_ID, {
      itemId: activeBoardItem.id,
      prop: 'confidential',
      value: confidential,
    });
  },

  fetchGroupProjects: ({ commit, state }, { search = '', fetchNext = false }) => {
    commit(types.REQUEST_GROUP_PROJECTS, fetchNext);

    const { fullPath } = state;

    const variables = {
      fullPath,
      search: search !== '' ? search : undefined,
      after: fetchNext ? state.groupProjectsFlags.pageInfo.endCursor : undefined,
    };

    return gqlClient
      .query({
        query: groupProjectsQuery,
        variables,
      })
      .then(({ data }) => {
        const { projects } = data.group;
        commit(types.RECEIVE_GROUP_PROJECTS_SUCCESS, {
          projects: projects.nodes,
          pageInfo: projects.pageInfo,
          fetchNext,
        });
      })
      .catch(() => commit(types.RECEIVE_GROUP_PROJECTS_FAILURE));
  },

  setSelectedProject: ({ commit }, project) => {
    commit(types.SET_SELECTED_PROJECT, project);
  },

  toggleBoardItemMultiSelection: ({ commit, state, dispatch, getters }, boardItem) => {
    const { selectedBoardItems } = state;
    const index = selectedBoardItems.indexOf(boardItem);

    // If user already selected an item (activeBoardItem) without using mult-select,
    // include that item in the selection and unset state.ActiveId to hide the sidebar.
    if (getters.activeBoardItem) {
      commit(types.ADD_BOARD_ITEM_TO_SELECTION, getters.activeBoardItem);
      dispatch('unsetActiveId');
    }

    if (index === -1) {
      commit(types.ADD_BOARD_ITEM_TO_SELECTION, boardItem);
    } else {
      commit(types.REMOVE_BOARD_ITEM_FROM_SELECTION, boardItem);
    }
  },

  setAddColumnFormVisibility: ({ commit }, visible) => {
    commit(types.SET_ADD_COLUMN_FORM_VISIBLE, visible);
  },

  resetBoardItemMultiSelection: ({ commit }) => {
    commit(types.RESET_BOARD_ITEM_SELECTION);
  },

  toggleBoardItem: ({ state, dispatch }, { boardItem, sidebarType = ISSUABLE }) => {
    dispatch('resetBoardItemMultiSelection');

    if (boardItem.id === state.activeId) {
      dispatch('unsetActiveId');
    } else {
      dispatch('setActiveId', { id: boardItem.id, sidebarType });
    }
  },

  setError: ({ commit }, { message, error, captureError = true }) => {
    commit(types.SET_ERROR, message);

    if (captureError) {
      Sentry.captureException(error);
    }
  },

  unsetError: ({ commit }) => {
    commit(types.SET_ERROR, undefined);
  },

  // EE action needs CE empty equivalent
  setActiveItemWeight: () => {},
};
