import { InfiniteData } from 'react-query';

import { AxiosResponse } from 'axios';

import { queryClient } from 'src/config/react-query';
import { Comment } from 'src/types/comments';
import {
  EventOrganizerStatus,
  EventPostType,
  EventTimeRange,
  InvitationStatus,
} from 'src/types/events';
import { DEFAULT_EMOJI } from 'src/types/feed';
import { Updater } from 'src/types/queries';
import { Reaction } from 'src/types/reaction';
import { CursorPagination } from 'src/types/services';
import {
  decreaseCommentCount,
  includesComment,
  increaseCommentCount,
  unshiftChildUpdater,
} from 'src/utils/comments';
import { functionalUpdate } from 'src/utils/queries';
import { sortReactions } from 'src/utils/reactions';

export const eventsKeys = {
  all: () => ['events'],
  eventsList: (
    pagination: { limit: number; page: number },
    timeRange: EventTimeRange,
    invitationStatus: InvitationStatus[],
    onlyInvited: boolean,
    onlyCreatedByUser: boolean,
  ) => [
    ...eventsKeys.all(),
    'list',
    { pagination, timeRange, invitationStatus, onlyInvited, onlyCreatedByUser },
  ],
  event: (id: number | string) => [...eventsKeys.all(), Number(id)],
  postDetail: (eventId: string, postId: string) => [
    ...eventsKeys.all(),
    'postDetail',
    eventId,
    postId,
  ],
  eventUsersStats: (id: number | string) => [
    ...eventsKeys.event(id),
    'eventUsersStats',
  ],
  eventUsers: (
    id: number | string,
    search: string,
    segmentationItemIds: number[],
  ) => [
    ...eventsKeys.event(id),
    'uninvitedUsers',
    { search, segmentationItemIds },
  ],
  eventUsersSelected: (id: number | string, params: object) => [
    ...eventsKeys.event(id),
    'selectedUninvitedUsers',
    { params },
  ],
  eventUninvitedUsersCount: (id: number | string, params: object) => [
    ...eventsKeys.event(id),
    'uninvitedUsersCount',
    { params },
  ],
  eventInvitations: (id: number | string) => [
    ...eventsKeys.event(id),
    'invitations',
  ],
  eventInvitationsFull: (id: number | string, params: object) => [
    ...eventsKeys.eventInvitations(id),
    params,
  ],
  eventPosts: (id: number | string) => [...eventsKeys.event(id), 'posts'],
  eventCalendar: (id: number | string) => [
    ...eventsKeys.event(id),
    'eventCalendar',
  ],
  eventGuests: (id: number | string) => [...eventsKeys.event(id), 'guests'],
  eventSpots: (id: number | string) => [...eventsKeys.event(id), 'spots'],
  segmentations: (id: number | string) => [
    ...eventsKeys.event(id),
    'segmentations',
  ],
  eventImage: (id: number | string) => [...eventsKeys.event(id), 'image'],
  comment: {
    all: () => [...eventsKeys.all(), 'comment'],
    list: (eventId: number | string) => [
      ...eventsKeys.comment.all(),
      'list',
      Number(eventId),
    ],
    detail: (postId: number, eventId: number | string) => [
      ...eventsKeys.comment.list(eventId),
      postId,
    ],
    liveStream: (postId: number) => [
      ...eventsKeys.comment.all(),
      'liveStream',
      postId,
    ],
  },
  childComment: {
    all: () => [...eventsKeys.all(), 'childComment'],
    list: (eventId: number | string) => [
      ...eventsKeys.childComment.all(),
      'list',
      Number(eventId),
    ],
    detail: (commentId: number, eventId: number | string) => [
      ...eventsKeys.childComment.list(eventId),
      commentId,
    ],
  },
  eventOrganizers: (eventId: number, limit?: number) => [
    ...eventsKeys.event(eventId),
    'organizers',
    limit,
  ],
  eventOrganizerBanner: (eventId: number) => [
    ...eventsKeys.event(eventId),
    'organizer-invitation-banner',
  ],
  users: (groupId?: number) => [...eventsKeys.all(), 'users', groupId],
  invitableOrganizers: (eventId: number, filter = '') => [
    ...eventsKeys.event(eventId),
    'invitable-organizers',
    filter,
  ],
  eventOrganizersCount: (eventId: number, statuses: EventOrganizerStatus[]) => [
    ...eventsKeys.eventOrganizers(eventId),
    'organizersCount',
    statuses,
  ],
  eventPostsWithLivestream: (eventId: number) => [
    ...eventsKeys.eventPosts(eventId),
    'withLivestream',
  ],
};

export const invalidateEventPostComment = (postId: number, eventId: number) => {
  queryClient.invalidateQueries(eventsKeys.comment.detail(postId, eventId));
};

export const invalidateEventPosts = (eventId: number) => {
  queryClient.invalidateQueries(eventsKeys.eventPosts(eventId));
};

export const invalidateEventPostsWithLivestream = (eventId: number) => {
  queryClient.invalidateQueries(eventsKeys.eventPostsWithLivestream(eventId));
};

export const invalidateEventPostChildComment = (
  parentId: number,
  eventId: number,
) => {
  queryClient.invalidateQueries(
    eventsKeys.childComment.detail(parentId, eventId),
  );
};

type EventPostsData = InfiniteData<
  AxiosResponse<CursorPagination<EventPostType[]>>
>;

const findEventPostIndexOnPages = (
  postId: number,
  pages: AxiosResponse<CursorPagination<EventPostType[]>>[],
): [pageIndex: number, postIndex: number] => {
  if (!pages) return [-1, -1];

  for (let i = 0; i < pages.length; i++) {
    const postIndex = pages[i]?.data?.items?.findIndex(
      post => post.id === postId,
    );
    if (postIndex !== undefined && postIndex !== -1) {
      return [i, postIndex];
    }
  }

  return [-1, -1];
};

export const setEventPostData = (
  eventId: number,
  postId: number,
  updater: Updater<EventPostType>,
) => {
  queryClient.setQueryData<EventPostsData | undefined>(
    eventsKeys.eventPosts(eventId),
    oldData => {
      if (!oldData) return oldData;

      const [pageIndex, postIndex] = findEventPostIndexOnPages(
        postId,
        oldData.pages,
      );

      if (pageIndex !== -1 && postIndex !== -1) {
        oldData.pages[pageIndex].data.items[postIndex] = functionalUpdate(
          updater,
          oldData.pages[pageIndex].data.items[postIndex],
        );
      }

      return oldData;
    },
  );
};

const dropCommentUpdater = (
  oldPost: EventPostType,
  commentId: number,
): EventPostType => {
  if (!oldPost?.lastComments) return oldPost;

  const commentIndex = oldPost.lastComments.findIndex(
    comment => comment.id === commentId,
  );

  if (commentIndex !== -1) {
    oldPost.commentCount = decreaseCommentCount(oldPost.commentCount);
    oldPost.lastComments.splice(commentIndex, 1);
  }

  return oldPost;
};

export const dropEventPostComment = (
  postId: number,
  eventId: number,
  commentId: number,
) => {
  setEventPostData(eventId, postId, oldPost =>
    oldPost ? dropCommentUpdater(oldPost, commentId) : oldPost,
  );

  dropEventPostCommentFromList(postId, eventId, commentId);

  queryClient.removeQueries(eventsKeys.childComment.detail(commentId, eventId));
};

const dropEventPostCommentFromList = (
  postId: number,
  eventId: number,
  commentId: number,
) => {
  queryClient.setQueryData<
    InfiniteData<AxiosResponse<CursorPagination<Comment[]>>> | undefined
  >(eventsKeys.comment.detail(postId, eventId), oldData => {
    if (!oldData) return oldData;

    for (let i = 0; i < oldData.pages.length; i++) {
      const items = oldData.pages[i].data.items || oldData.pages[i].data;
      const commentIndex = (items as Comment[]).findIndex(
        comment => comment.id === commentId,
      );

      if (commentIndex !== -1) {
        (items as Comment[]).splice(commentIndex, 1);
        break;
      }
    }

    return oldData;
  });
};

const dropChildCommentUpdater = (
  oldComment: Comment,
  childId: number,
): Comment => {
  if (!oldComment?.children) return oldComment;

  const childIndex = oldComment.children.findIndex(
    child => child.id === childId,
  );

  if (childIndex !== -1) {
    oldComment.childrenNumber = decreaseCommentCount(oldComment.childrenNumber);
    oldComment.children.splice(childIndex, 1);
  }

  return oldComment;
};

const setEventPostCommentData = (
  postId: number,
  eventId: number,
  commentId: number,
  updater: Updater<Comment>,
) => {
  queryClient.setQueryData<
    InfiniteData<AxiosResponse<CursorPagination<Comment[]>>> | undefined
  >(eventsKeys.comment.detail(postId, eventId), oldData => {
    if (!oldData) return oldData;

    for (let i = 0; i < oldData.pages.length; i++) {
      const items = oldData.pages[i].data.items || oldData.pages[i].data;
      const commentIndex = (items as Comment[]).findIndex(
        comment => comment.id === commentId,
      );

      if (commentIndex !== -1) {
        (items as Comment[])[commentIndex] = functionalUpdate(
          updater,
          (items as Comment[])[commentIndex],
        );
        break;
      }
    }

    return oldData;
  });
};

const dropChildFromLastComments = (
  postId: number,
  eventId: number,
  parentId: number,
  childId: number,
) => {
  setEventPostData(eventId, postId, oldPost => {
    if (!oldPost?.lastComments) return oldPost;

    const parentIndex = oldPost.lastComments.findIndex(c => c.id === parentId);
    if (parentIndex === -1) return oldPost;

    const parent = oldPost.lastComments[parentIndex];
    if (!parent.children) return oldPost;

    const childIndex = parent.children.findIndex(c => c.id === childId);
    if (childIndex !== -1) {
      parent.childrenNumber = decreaseCommentCount(parent.childrenNumber);
      parent.children.splice(childIndex, 1);
    }

    return oldPost;
  });
};

const dropChildCommentFromChildList = (
  parentId: number,
  eventId: number,
  childId: number,
) => {
  queryClient.setQueryData<
    InfiniteData<AxiosResponse<CursorPagination<Comment[]>>> | undefined
  >(eventsKeys.childComment.detail(parentId, eventId), oldData => {
    if (!oldData) return oldData;

    for (let i = 0; i < oldData.pages.length; i++) {
      const items = oldData.pages[i].data.items || oldData.pages[i].data;
      const childIndex = (items as Comment[]).findIndex(
        child => child.id === childId,
      );

      if (childIndex !== -1) {
        (items as Comment[]).splice(childIndex, 1);
        break;
      }
    }

    return oldData;
  });
};

export const dropEventPostChildComment = (
  postId: number,
  eventId: number,
  parentId: number,
  childId: number,
) => {
  dropChildFromLastComments(postId, eventId, parentId, childId);

  setEventPostCommentData(postId, eventId, parentId, oldComment =>
    oldComment ? dropChildCommentUpdater(oldComment, childId) : oldComment,
  );

  dropChildCommentFromChildList(parentId, eventId, childId);
};

const unshiftCommentUpdater = (
  oldPost: EventPostType,
  comment: Comment,
): EventPostType => {
  if (!oldPost) return oldPost;

  if (!oldPost.lastComments) {
    oldPost.lastComments = [];
  }

  if (!includesComment(oldPost.lastComments, comment)) {
    oldPost.commentCount = increaseCommentCount(oldPost.commentCount);
    oldPost.lastComments.unshift(comment);
  }

  return oldPost;
};

const unshiftCommentToList = (
  postId: number,
  eventId: number,
  comment: Comment,
) => {
  queryClient.setQueryData<
    InfiniteData<AxiosResponse<CursorPagination<Comment[]>>> | undefined
  >(eventsKeys.comment.detail(postId, eventId), oldData => {
    if (!oldData?.pages?.length) return oldData;

    const items = oldData.pages[0].data.items || oldData.pages[0].data;
    if (!includesComment(items as Comment[], comment)) {
      (items as Comment[]).unshift(comment);
    }

    return oldData;
  });
};

const setEventPostDetailData = (
  eventId: number,
  postId: number,
  updater: Updater<EventPostType>,
) => {
  queryClient.setQueryData<AxiosResponse<EventPostType> | undefined>(
    eventsKeys.postDetail(eventId.toString(), postId.toString()),
    oldData => {
      if (!oldData) return oldData;

      oldData.data = functionalUpdate(updater, oldData.data);
      return oldData;
    },
  );
};

export const unshiftEventPostComment = (
  postId: number,
  eventId: number,
  comment: Comment,
) => {
  setEventPostData(eventId, postId, oldPost =>
    oldPost ? unshiftCommentUpdater(oldPost, comment) : oldPost,
  );

  setEventPostDetailData(eventId, postId, oldPost =>
    oldPost ? unshiftCommentUpdater(oldPost, comment) : oldPost,
  );

  unshiftCommentToList(postId, eventId, comment);
  queryClient.invalidateQueries(eventsKeys.comment.liveStream(postId));
};

const unshiftChildToLastComments = (
  postId: number,
  eventId: number,
  parentId: number,
  childComment: Comment,
) => {
  setEventPostData(eventId, postId, oldPost => {
    if (!oldPost?.lastComments) return oldPost;

    const parentIndex = oldPost.lastComments.findIndex(c => c.id === parentId);
    if (parentIndex !== -1) {
      oldPost.lastComments[parentIndex] = unshiftChildUpdater(
        oldPost.lastComments[parentIndex],
        childComment,
      ) as Comment;
    }

    return oldPost;
  });
};

const unshiftChildToCommentList = (
  postId: number,
  eventId: number,
  parentId: number,
  childComment: Comment,
) => {
  setEventPostCommentData(postId, eventId, parentId, oldComment =>
    oldComment
      ? (unshiftChildUpdater(oldComment, childComment) as Comment)
      : oldComment,
  );
};

const unshiftChildToChildList = (
  parentId: number,
  eventId: number,
  childComment: Comment,
) => {
  queryClient.setQueryData<
    InfiniteData<AxiosResponse<CursorPagination<Comment[]>>> | undefined
  >(eventsKeys.childComment.detail(parentId, eventId), oldData => {
    if (!oldData?.pages?.length) return oldData;

    const items = oldData.pages[0].data.items || oldData.pages[0].data;
    if (!includesComment(items as Comment[], childComment)) {
      (items as Comment[]).unshift(childComment);
    }

    return oldData;
  });
};

export const unshiftEventPostChildComment = (
  postId: number,
  eventId: number,
  parentId: number,
  childComment: Comment,
) => {
  unshiftChildToLastComments(postId, eventId, parentId, childComment);
  unshiftChildToCommentList(postId, eventId, parentId, childComment);
  unshiftChildToChildList(parentId, eventId, childComment);
};

const updateCommentUpdater = (
  oldComment: Comment,
  newComment: Comment,
): Comment => {
  return { ...oldComment, ...newComment };
};

const updateLastCommentData = (
  postId: number,
  eventId: number,
  commentId: number,
  updater: Updater<Comment>,
) => {
  setEventPostData(eventId, postId, oldPost => {
    if (!oldPost?.lastComments) return oldPost;

    const commentIndex = oldPost.lastComments.findIndex(
      c => c.id === commentId,
    );
    if (commentIndex !== -1) {
      oldPost.lastComments[commentIndex] = functionalUpdate(
        updater,
        oldPost.lastComments[commentIndex],
      );
    }

    return oldPost;
  });
};

export const updateEventPostComment = (
  postId: number,
  eventId: number,
  comment: Comment,
) => {
  updateLastCommentData(postId, eventId, comment.id, oldComment =>
    oldComment ? updateCommentUpdater(oldComment, comment) : oldComment,
  );

  setEventPostCommentData(postId, eventId, comment.id, oldComment =>
    oldComment ? updateCommentUpdater(oldComment, comment) : oldComment,
  );
};

const updateChildInLastComments = (
  postId: number,
  eventId: number,
  parentId: number,
  childComment: Comment,
) => {
  setEventPostData(eventId, postId, oldPost => {
    if (!oldPost?.lastComments) return oldPost;

    const parentIndex = oldPost.lastComments.findIndex(c => c.id === parentId);
    if (parentIndex === -1) return oldPost;

    const parent = oldPost.lastComments[parentIndex];
    if (!parent.children) return oldPost;

    const childIndex = parent.children.findIndex(c => c.id === childComment.id);
    if (childIndex !== -1) {
      parent.children[childIndex] = updateCommentUpdater(
        parent.children[childIndex],
        childComment,
      );
    }

    return oldPost;
  });
};

const updateChildInCommentList = (
  postId: number,
  eventId: number,
  parentId: number,
  childComment: Comment,
) => {
  setEventPostCommentData(postId, eventId, parentId, oldComment => {
    if (!oldComment?.children) return oldComment;

    const childIndex = oldComment.children.findIndex(
      c => c.id === childComment.id,
    );
    if (childIndex !== -1) {
      oldComment.children[childIndex] = updateCommentUpdater(
        oldComment.children[childIndex],
        childComment,
      );
    }

    return oldComment;
  });
};

const updateChildInChildList = (
  parentId: number,
  eventId: number,
  childComment: Comment,
) => {
  queryClient.setQueryData<
    InfiniteData<AxiosResponse<CursorPagination<Comment[]>>> | undefined
  >(eventsKeys.childComment.detail(parentId, eventId), oldData => {
    if (!oldData) return oldData;

    for (let i = 0; i < oldData.pages.length; i++) {
      const items = oldData.pages[i].data.items || oldData.pages[i].data;
      const childIndex = (items as Comment[]).findIndex(
        child => child.id === childComment.id,
      );

      if (childIndex !== -1) {
        (items as Comment[])[childIndex] = updateCommentUpdater(
          (items as Comment[])[childIndex],
          childComment,
        );
        break;
      }
    }

    return oldData;
  });
};

export const updateEventPostChildComment = (
  postId: number,
  eventId: number,
  parentId: number,
  childComment: Comment,
) => {
  updateChildInLastComments(postId, eventId, parentId, childComment);
  updateChildInCommentList(postId, eventId, parentId, childComment);
  updateChildInChildList(parentId, eventId, childComment);
};

const reactionCommentUpdater = (
  oldComment: Comment,
  updater: Updater<Reaction[]>,
): Comment => {
  if (!oldComment) return oldComment;

  oldComment.reactions = functionalUpdate(updater, oldComment.reactions);
  sortReactions(oldComment.reactions, DEFAULT_EMOJI.emoji);

  return oldComment;
};

const updateReactionInLastComments = (
  postId: number,
  eventId: number,
  commentId: number,
  updater: Updater<Reaction[]>,
) => {
  setEventPostData(eventId, postId, oldPost => {
    if (!oldPost?.lastComments) return oldPost;

    const commentIndex = oldPost.lastComments.findIndex(
      c => c.id === commentId,
    );
    if (commentIndex !== -1) {
      oldPost.lastComments[commentIndex] = reactionCommentUpdater(
        oldPost.lastComments[commentIndex],
        updater,
      );
    }

    return oldPost;
  });
};

export const updateEventPostCommentReaction = (
  postId: number,
  eventId: number,
  commentId: number,
  updater: Updater<Reaction[]>,
) => {
  updateReactionInLastComments(postId, eventId, commentId, updater);

  setEventPostCommentData(postId, eventId, commentId, oldComment =>
    oldComment ? reactionCommentUpdater(oldComment, updater) : oldComment,
  );
};

const updateReactionChildInLastComments = (
  postId: number,
  eventId: number,
  parentId: number,
  childId: number,
  updater: Updater<Reaction[]>,
) => {
  setEventPostData(eventId, postId, oldPost => {
    if (!oldPost?.lastComments) return oldPost;

    const parentIndex = oldPost.lastComments.findIndex(c => c.id === parentId);
    if (parentIndex === -1) return oldPost;

    const parent = oldPost.lastComments[parentIndex];
    if (!parent.children) return oldPost;

    const childIndex = parent.children.findIndex(c => c.id === childId);
    if (childIndex !== -1) {
      parent.children[childIndex] = reactionCommentUpdater(
        parent.children[childIndex],
        updater,
      );
    }

    return oldPost;
  });
};

const updateReactionChildInCommentList = (
  postId: number,
  eventId: number,
  parentId: number,
  childId: number,
  updater: Updater<Reaction[]>,
) => {
  setEventPostCommentData(postId, eventId, parentId, oldComment => {
    if (!oldComment?.children) return oldComment;

    const childIndex = oldComment.children.findIndex(c => c.id === childId);
    if (childIndex !== -1) {
      oldComment.children[childIndex] = reactionCommentUpdater(
        oldComment.children[childIndex],
        updater,
      );
    }

    return oldComment;
  });
};

const updateReactionChildInChildList = (
  parentId: number,
  eventId: number,
  childId: number,
  updater: Updater<Reaction[]>,
) => {
  queryClient.setQueryData<
    InfiniteData<AxiosResponse<CursorPagination<Comment[]>>> | undefined
  >(eventsKeys.childComment.detail(parentId, eventId), oldData => {
    if (!oldData) return oldData;

    for (let i = 0; i < oldData.pages.length; i++) {
      const items = oldData.pages[i].data.items || oldData.pages[i].data;
      const childIndex = (items as Comment[]).findIndex(
        child => child.id === childId,
      );

      if (childIndex !== -1) {
        (items as Comment[])[childIndex] = reactionCommentUpdater(
          (items as Comment[])[childIndex],
          updater,
        );
        break;
      }
    }

    return oldData;
  });
};

export const updateEventPostChildCommentReaction = (
  postId: number,
  eventId: number,
  parentId: number,
  childId: number,
  updater: Updater<Reaction[]>,
) => {
  updateReactionChildInLastComments(
    postId,
    eventId,
    parentId,
    childId,
    updater,
  );
  updateReactionChildInCommentList(postId, eventId, parentId, childId, updater);
  updateReactionChildInChildList(parentId, eventId, childId, updater);
};

const reactionPostUpdater = (
  oldPost: EventPostType,
  updater: Updater<Reaction[]>,
): EventPostType => {
  if (!oldPost) return oldPost;

  oldPost.reactions = functionalUpdate(updater, oldPost.reactions);
  sortReactions(oldPost.reactions, DEFAULT_EMOJI.emoji);

  return oldPost;
};

export const updateEventPostReaction = (
  eventId: number,
  postId: number,
  updater: Updater<Reaction[]>,
) => {
  setEventPostData(eventId, postId, oldPost =>
    oldPost ? reactionPostUpdater(oldPost, updater) : oldPost,
  );
  setEventPostDetailData(eventId, postId, oldPost =>
    oldPost ? reactionPostUpdater(oldPost, updater) : oldPost,
  );
  updateLivestreamPostReactions(eventId, postId, updater);
};

export const updateLivestreamPostReactions = (
  eventId: number,
  postId: number,
  updater: Updater<Reaction[]>,
) => {
  const old = queryClient.getQueryData<
    AxiosResponse<CursorPagination<EventPostType[]>>
  >(eventsKeys.eventPostsWithLivestream(eventId));
  if (!old) return;
  queryClient.setQueryData(eventsKeys.eventPostsWithLivestream(eventId), {
    ...old,
    data: {
      ...old.data,
      items: old.data.items.map(item => {
        if (item.id !== postId) return item;
        const updatedReactions = functionalUpdate(updater, item.reactions);
        sortReactions(updatedReactions, DEFAULT_EMOJI.emoji);
        return { ...item, reactions: updatedReactions };
      }),
    },
  });
};

export const invalidateEventOrganizers = (eventId: number, limit?: number) => {
  queryClient.invalidateQueries(eventsKeys.eventOrganizers(eventId, limit));
};

export const invalidateEventOrganizerBanner = (eventId: number) => {
  queryClient.invalidateQueries(eventsKeys.eventOrganizerBanner(eventId));
};
