'use client';

import {
  InfiniteData,
  QueryClient,
  QueryKey,
  QueryObserverOptions,
  useInfiniteQuery,
  useMutation,
  useQuery,
  useQueryClient,
} from '@tanstack/react-query';
import { WritableDraft, produce } from 'immer';
import { useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { deepCamelKeys } from 'string-ts';

import { useStores } from '@/app/(root)/AppProviders';
import { toast } from '@/components/toast/Toast';
import { useApiClient } from '@/lib/apiClient';
import { CommentEntityType, formatErrorResponse } from '@/state/clipStore';
import {
  CommentEntity,
  CommentReplyEntity,
  CommentSortBy,
} from '@/state/clipStore';

type CommentsResponse = {
  results: CommentEntity[];
  nextCursor?: string | null;
  totalCount?: number;
  allowComment?: boolean;
  disableReason?: string;
};

type RepliesResponse = {
  replies?: CommentReplyEntity[] | null;
  replyContinuationToken?: string | null;
  totalCount?: number;
};

export const commentsKeys = {
  all: [{ scope: 'comments' }] as const,
  commentCount: ({
    entityId,
    entityType,
    deeplinkedCommentId,
  }: {
    entityId: string;
    entityType: CommentEntityType;
    deeplinkedCommentId?: string;
  }) =>
    [
      {
        ...commentsKeys.all[0],
        entity: 'commentCount',
        entityType,
        entityId,
        deeplinkedCommentId,
      },
    ] as const,
  comments: ({
    entityId,
    entityType,
    sortBy,
    deeplinkedCommentId,
  }: {
    entityId: string;
    entityType: CommentEntityType;
    sortBy?: CommentSortBy | null;
    deeplinkedCommentId?: string;
  }) =>
    [
      {
        ...commentsKeys.all[0],
        entity: 'comments',
        entityType,
        entityId,
        sortBy,
        deeplinkedCommentId,
      },
    ] as const,
  replies: ({
    entityId,
    entityType,
    commentId,
    deeplinkedCommentId,
  }: {
    entityId: string;
    entityType: CommentEntityType;
    commentId: string;
    deeplinkedCommentId?: string;
  }) =>
    [
      {
        ...commentsKeys.all[0],
        entity: 'replies',
        entityType,
        entityId,
        commentId,
        deeplinkedCommentId,
      },
    ] as const,
  topReplies: ({
    entityId,
    entityType,
    commentId,
    deeplinkedCommentId,
  }: {
    entityId: string;
    entityType: CommentEntityType;
    commentId: string;
    deeplinkedCommentId?: string;
  }) =>
    [
      {
        ...commentsKeys.all[0],
        entity: 'topReplies',
        entityType,
        entityId,
        commentId,
        deeplinkedCommentId,
      },
    ] as const,
  mentionSearch: ({
    query,
    boostedUserHandles,
  }: {
    query?: string | null;
    boostedUserHandles?: string[];
  }) =>
    [
      {
        scope: 'userSearch',
        entity: 'user',
        query,
        boostedUserHandles,
      },
    ] as const,
};

export const mutualFollowersKeys = {
  all: [{ scope: 'mutualFollowers' }] as const,
  search: (query?: string | null) =>
    [
      {
        ...mutualFollowersKeys.all[0],
        entity: 'search',
        query,
      },
    ] as const,
};

/**
 * Updates a specific comment using the given mutation function
 */
function updateCommentData(
  queryClient: QueryClient,
  queryKey: QueryKey,
  commentId: string | null,
  mutationFnComment: (comment: WritableDraft<CommentEntity>) => void,
  mutationFn?: (
    data: WritableDraft<InfiniteData<CommentsResponse>>,
    hasEdit: boolean
  ) => void
) {
  const prevData =
    queryClient.getQueryData<InfiniteData<CommentsResponse>>(queryKey);
  queryClient.setQueryData<InfiniteData<CommentsResponse>>(
    queryKey,
    (prevData) => {
      if (!prevData?.pages) return prevData;
      // Update the comment
      return produce(prevData, (nextData) => {
        let hasEdit = false;
        for (const page of nextData.pages) {
          for (const comment of page.results) {
            if (comment.id === commentId || commentId === null) {
              mutationFnComment(comment);
              hasEdit = true;
            }
          }
        }
        mutationFn?.(nextData, hasEdit);
      });
    }
  );
  return prevData;
}

function updateCommentReplyData(
  queryClient: QueryClient,
  queryKey: QueryKey,
  commentId: string | null,
  mutationFnComment: (comment: WritableDraft<CommentReplyEntity>) => void,
  mutationFn?: (
    data: WritableDraft<InfiniteData<RepliesResponse>>,
    hasEdit: boolean
  ) => void
) {
  const prevData =
    queryClient.getQueryData<InfiniteData<RepliesResponse>>(queryKey);
  queryClient.setQueryData<InfiniteData<RepliesResponse>>(
    queryKey,
    (prevData) => {
      if (!prevData?.pages) return prevData;
      // Update the comment
      return produce(prevData, (nextData) => {
        let hasEdit = false;
        for (const page of nextData.pages) {
          for (const comment of page.replies || []) {
            if (comment.id === commentId || commentId === null) {
              mutationFnComment(comment);
              hasEdit = true;
            }
          }
        }
        mutationFn?.(nextData, hasEdit);
      });
    }
  );
  return prevData;
}

function updateCommentTopReplyData(
  queryClient: QueryClient,
  queryKey: QueryKey,
  commentId: string | null,
  mutationFnComment: (comment: WritableDraft<CommentReplyEntity>) => void,
  mutationFn?: (data: WritableDraft<RepliesResponse>, hasEdit: boolean) => void
) {
  const prevData = queryClient.getQueryData<RepliesResponse>(queryKey);
  queryClient.setQueryData<RepliesResponse>(queryKey, (prevData) => {
    if (!prevData?.replies) return prevData;
    // Update the comment
    return produce(prevData, (nextData) => {
      let hasEdit = false;
      for (const comment of nextData.replies || []) {
        if (comment.id === commentId || commentId === null) {
          mutationFnComment(comment);
          hasEdit = true;
        }
      }
      mutationFn?.(nextData, hasEdit);
    });
  });
  return prevData;
}

export function useCommentCount(
  options: {
    entityId: string;
    entityType: CommentEntityType;
    initialCount?: number;
    initialDataUpdatedAt?: number;
  } & Pick<QueryObserverOptions<any>, 'enabled' | 'refetchOnMount'>
) {
  const {
    entityId,
    entityType,
    enabled,
    refetchOnMount,
    initialCount = null,
    initialDataUpdatedAt = null,
  } = options;

  const apiClient = useApiClient();

  const commentCountQuery = useQuery({
    enabled,
    refetchOnMount,
    queryKey: commentsKeys.commentCount({
      entityType,
      entityId,
      deeplinkedCommentId: undefined,
    }),
    queryFn: async () => {
      if (entityType !== 'hook') {
        const response = await apiClient.GET(
          '/api/gen/{clip_id}/comments/count',
          {
            params: {
              path: {
                clip_id: entityId,
              },
            },
          }
        );
        return deepCamelKeys(response.data || {});
      } else {
        const response = await apiClient.GET(
          '/api/video/hooks/comments/{hook_id}/comments/count',
          {
            params: {
              path: {
                hook_id: entityId,
              },
            },
          }
        );
        return deepCamelKeys(response.data || {});
      }
    },
    initialData: initialCount != null ? { count: initialCount } : undefined,
    initialDataUpdatedAt: initialDataUpdatedAt ?? undefined,
    staleTime: 5 * 60 * 1000,
  });

  return useMemo(
    () => ({
      numComments: (commentCountQuery.data as any)?.count ?? 0,
      query: commentCountQuery,
    }),
    [commentCountQuery]
  );
}

export function useComments(
  options: {
    entityId: string;
    entityType: CommentEntityType;
    sortBy?: CommentSortBy;
    syncClipCommentCount?: boolean;
    deeplinkedCommentId?: string;
  } & Pick<QueryObserverOptions<any>, 'enabled' | 'refetchOnMount'>
) {
  const {
    entityId,
    entityType,
    enabled,
    refetchOnMount,
    sortBy = null,
    syncClipCommentCount = true,
    deeplinkedCommentId,
  } = options;

  const { t } = useTranslation();

  const apiClient = useApiClient();
  const { clips: clipsStore } = useStores();

  const queryClient = useQueryClient();

  const commentsQuery = useInfiniteQuery({
    enabled,
    refetchOnMount,
    queryKey: commentsKeys.comments({
      entityType,
      entityId,
      sortBy,
      deeplinkedCommentId,
    }),
    queryFn: async ({ pageParam }) => {
      if (entityType !== 'hook') {
        const { data, response, error } = await apiClient.GET(
          '/api/gen/{clip_id}/comments',
          {
            params: {
              query: {
                cursor: pageParam,
                order: sortBy,
                id: deeplinkedCommentId,
                deeplink: deeplinkedCommentId ? true : undefined,
              },
              path: {
                clip_id: entityId,
              },
            },
          }
        );
        if (error) {
          throw formatErrorResponse(error, response);
        }
        return deepCamelKeys(data) as CommentsResponse;
      } else {
        const { data, response, error } = await apiClient.GET(
          '/api/video/hooks/comments/{hook_id}/comments',
          {
            params: {
              query: {
                cursor: pageParam,
                order: sortBy,
                id: deeplinkedCommentId,
                deeplink: deeplinkedCommentId ? true : undefined,
              },
              path: {
                hook_id: entityId,
              },
            },
          }
        );
        if (error) {
          throw formatErrorResponse(error, response);
        }
        return deepCamelKeys(data) as CommentsResponse;
      }
    },
    initialPageParam: null as string | null,
    getNextPageParam: (lastPage) => lastPage?.nextCursor,
    select(data) {
      return {
        ...data.pages[data.pages.length - 1],
        results: data.pages
          .flatMap((data) => data?.results)
          .filter((v) => v != null),
      } as (typeof data.pages)[number];
    },
    staleTime: 5 * 60 * 1000,
  });

  const commentReactionMutation = useMutation({
    mutationFn: async ({
      commentId,
      commentEntityType,
      isLike,
    }: {
      commentId: string;
      commentEntityType: CommentEntityType;
      isLike: boolean;
    }) => {
      if (entityType !== 'hook') {
        const { data, response, error } = await apiClient.POST(
          '/api/comment/{comment_id}/reaction',
          {
            body: {
              entity_type: commentEntityType,
              reaction: isLike ? 'LIKE' : 'DISLIKE',
            },
            params: {
              path: {
                comment_id: commentId,
              },
            },
          }
        );
        if (error) {
          throw formatErrorResponse(error, response);
        }
        return data;
      } else {
        const { data, response, error } = await apiClient.POST(
          '/api/video/hooks/comments/{comment_id}/reaction',
          {
            body: {
              entity_type: commentEntityType,
              reaction: isLike ? 'LIKE' : 'DISLIKE',
            },
            params: {
              path: {
                comment_id: commentId,
              },
            },
          }
        );
        if (error) {
          throw formatErrorResponse(error, response);
        }
        return data;
      }
    },
    onMutate: async ({ commentId, isLike }) => {
      const prevData = queryClient.getQueryData<InfiniteData<CommentsResponse>>(
        commentsKeys.comments({
          entityType,
          entityId,
          sortBy,
          deeplinkedCommentId,
        })
      );
      queryClient.setQueryData<InfiniteData<CommentsResponse>>(
        commentsKeys.comments({
          entityType,
          entityId,
          sortBy,
          deeplinkedCommentId,
        }),
        (prevData) => {
          if (!prevData?.pages) return prevData;
          // Update the comment
          return produce(prevData, (nextData) => {
            for (const page of nextData.pages) {
              for (const comment of page.results) {
                if (comment.id === commentId) {
                  comment.numLikes = isLike
                    ? comment.numLikes + 1
                    : Math.max(0, comment.numLikes - 1);
                  comment.reactionType = isLike ? 'like' : null;
                }
              }
            }
          });
        }
      );

      return { prevData };
    },
    onError: (_err, _variables, context) => {
      if (context?.prevData) {
        queryClient.setQueryData(
          commentsKeys.comments({
            entityType,
            entityId,
            sortBy,
            deeplinkedCommentId,
          }),
          context.prevData
        );
      }
    },
  });

  const commentDeleteMutation = useMutation({
    mutationFn: async ({
      commentId,
      commentEntityType,
    }: {
      commentId: string;
      commentEntityType: CommentEntityType;
    }) => {
      if (entityType !== 'hook') {
        const { data, response, error } = await apiClient.DELETE(
          '/api/comment/{comment_id}',
          {
            params: {
              path: {
                comment_id: commentId,
              },
            },
            // @TODO uncomment when the schema gets updated
            // body: {
            //   entity_type: commentEntityType,
            // },
          }
        );
        if (error) {
          throw formatErrorResponse(error, response);
        }
        return data;
      } else {
        const { data, response, error } = await apiClient.DELETE(
          '/api/video/hooks/comments/{comment_id}',
          {
            params: {
              path: {
                comment_id: commentId,
              },
            },
            body: {
              entity_type: commentEntityType,
            },
          }
        );
        if (error) {
          throw formatErrorResponse(error, response);
        }
        return data;
      }
    },
    onMutate: async ({ commentId }) => {
      const prevData = updateCommentData(
        queryClient,
        commentsKeys.comments({
          entityType,
          entityId,
          sortBy,
          deeplinkedCommentId,
        }),
        commentId,
        (comment) => {
          comment.isDeleted = true;
        },
        (nextData, hasEdit = true) => {
          if (hasEdit) {
            for (const page of nextData.pages) {
              if (typeof page.totalCount === 'number') {
                page.totalCount = Math.max(0, page.totalCount - 1);
              }
            }
          }
        }
      );
      return { prevData };
    },
    onSuccess: async () => {
      toast({
        title: t('comments.deleteSuccess'),
        status: 'info',
        duration: 5000,
        isClosable: true,
      });
      await queryClient.invalidateQueries({
        queryKey: commentsKeys.commentCount({
          entityType,
          entityId,
          deeplinkedCommentId,
        }),
        type: 'active',
      });
    },
    onError: (_err, _variables, context) => {
      if (context?.prevData) {
        queryClient.setQueryData(
          commentsKeys.comments({
            entityType,
            entityId,
            sortBy,
            deeplinkedCommentId,
          }),
          context.prevData
        );
      }
      toast({
        title: t('comments.deleteError'),
        status: 'error',
        duration: 5000,
        isClosable: true,
      });
    },
  });

  const commentReportMutation = useMutation({
    mutationFn: async ({
      commentId,
      commentEntityType,
      reason,
    }: {
      commentId: string;
      commentEntityType: CommentEntityType;
      reason?: string;
    }) => {
      if (entityType !== 'hook') {
        const { data, response, error } = await apiClient.POST(
          '/api/comment/{comment_id}/report',
          {
            body: {
              entity_type: commentEntityType,
              reason,
            },
            params: {
              path: {
                comment_id: commentId,
              },
            },
          }
        );
        if (error) {
          throw formatErrorResponse(error, response);
        }
        return deepCamelKeys(data);
      } else {
        const { data, response, error } = await apiClient.POST(
          '/api/video/hooks/comments/{comment_id}/report',
          {
            body: {
              entity_type: commentEntityType,
              reason,
            },
            params: {
              path: {
                comment_id: commentId,
              },
            },
          }
        );
        if (error) {
          throw formatErrorResponse(error, response);
        }
        return deepCamelKeys(data);
      }
    },
    onMutate: async ({ commentId }) => {
      const prevData = updateCommentData(
        queryClient,
        commentsKeys.comments({
          entityType,
          entityId,
          sortBy,
          deeplinkedCommentId,
        }),
        commentId,
        (comment) => {
          comment.isReported = true;
        }
      );
      return { prevData };
    },
    onSuccess: (data) => {
      toast({
        title: t('comments.reportSuccess'),
        status: 'info',
        duration: 5000,
        isClosable: true,
      });
      // @TODO: Use response to determine whether to hide comment
      const shouldDelete = false;
      if (shouldDelete) {
        const commentId = data.commentId;
        updateCommentData(
          queryClient,
          commentsKeys.comments({
            entityType,
            entityId,
            sortBy,
            deeplinkedCommentId,
          }),
          commentId,
          (comment) => {
            comment.isDeleted = true;
          },
          (nextData, hasEdit = true) => {
            if (hasEdit) {
              for (const page of nextData.pages) {
                if (typeof page.totalCount === 'number') {
                  page.totalCount = Math.max(0, page.totalCount - 1);
                }
              }
            }
          }
        );
      }
    },
    onError: (_err, _variables, context) => {
      if (context?.prevData) {
        queryClient.setQueryData(
          commentsKeys.comments({
            entityType,
            entityId,
            sortBy,
            deeplinkedCommentId,
          }),
          context.prevData
        );
      }
      toast({
        title: t('comments.reportError'),
        status: 'error',
        duration: 5000,
        isClosable: true,
      });
    },
  });

  const commentBlockMutation = useMutation({
    mutationFn: async ({
      handle,
      reason,
    }: {
      handle: string;
      reason?: string;
    }) => {
      const { data, response, error } = await apiClient.POST(
        '/api/comment/block-user',
        {
          body: {
            handle,
            reason,
          },
        }
      );
      if (error) {
        throw formatErrorResponse(error, response);
      }
      return deepCamelKeys(data);
    },
    onSuccess: async (_data, { handle }) => {
      toast({
        title: handle
          ? t('comments.blockSuccessName', { name: handle })
          : t('comments.blockSuccess'),
        status: 'info',
        duration: 5000,
        isClosable: true,
      });

      // Update current query optimistically
      updateCommentData(
        queryClient,
        commentsKeys.comments({
          entityType,
          entityId,
          sortBy,
          deeplinkedCommentId,
        }),
        null,
        (comment) => {
          if (comment.userHandle === handle) {
            comment.userCommentsBlocked = true;
          }
        }
      );

      // Invalidate all comment-related queries for this entity to ensure consistency
      await queryClient.invalidateQueries({
        queryKey: commentsKeys.all,
        type: 'active',
      });
    },
    onError: () => {
      toast({
        title: t('comments.blockError'),
        status: 'error',
        duration: 5000,
        isClosable: true,
      });
    },
  });

  const commentUnblockMutation = useMutation({
    mutationFn: async ({ handle }: { handle: string }) => {
      const { data, response, error } = await apiClient.POST(
        '/api/comment/unblock-user',
        {
          body: {
            handle,
          },
        }
      );
      if (error) {
        throw formatErrorResponse(error, response);
      }
      return deepCamelKeys(data);
    },
    onSuccess: async (_data, { handle }) => {
      toast({
        title: handle
          ? t('comments.unblockSuccessName', { name: handle })
          : t('comments.unblockSuccess'),
        status: 'info',
        duration: 5000,
        isClosable: true,
      });

      // Update current query optimistically
      updateCommentData(
        queryClient,
        commentsKeys.comments({
          entityType,
          entityId,
          sortBy,
          deeplinkedCommentId,
        }),
        null,
        (comment) => {
          if (comment.userHandle === handle) {
            comment.userCommentsBlocked = false;
          }
        }
      );

      // Invalidate all comment-related queries for this entity to ensure consistency
      await queryClient.invalidateQueries({
        queryKey: commentsKeys.all,
        type: 'active',
      });
    },
    onError: () => {
      toast({
        title: t('comments.unblockError'),
        status: 'error',
        duration: 5000,
        isClosable: true,
      });
    },
  });

  const commentPostMutation = useMutation({
    mutationFn: async ({
      content,
      commentId = null,
      commentEntityType,
      trackTimestamp = null,
    }: {
      content:
        | string
        | {
            content: string;
            userMentions?: Array<{
              handle: string;
              displayName?: string;
              start: number;
              end: number;
            }>;
          };
      commentId?: string | null;
      commentEntityType?: CommentEntityType;
      trackTimestamp?: number | null;
    }) => {
      if (entityType !== 'hook') {
        const { data, response, error } = await apiClient.POST(
          '/api/gen/{clip_id}/comment',
          {
            body:
              typeof content === 'string'
                ? {
                    content,
                    parent_id: commentId,
                    entity_type: commentEntityType,
                    track_timestamp: trackTimestamp || null,
                  }
                : {
                    content: content.content,
                    parent_id: commentId,
                    entity_type: commentEntityType,
                    user_mentions: content.userMentions?.map((m) => ({
                      handle: m.handle,
                      display_name: m.displayName,
                      start: m.start,
                      end: m.end,
                    })),
                    track_timestamp: trackTimestamp || null,
                  },
            params: {
              path: {
                clip_id: entityId,
              },
            },
          }
        );
        if (error) {
          throw formatErrorResponse(error, response);
        }
        return deepCamelKeys(data);
      } else {
        const { data, response, error } = await apiClient.POST(
          '/api/video/hooks/comments/{hook_id}/comment',
          {
            body:
              typeof content === 'string'
                ? {
                    content,
                    parent_id: commentId,
                    entity_type: commentEntityType,
                    track_timestamp: trackTimestamp || null,
                  }
                : {
                    content: content.content,
                    parent_id: commentId,
                    entity_type: commentEntityType,
                    user_mentions: content.userMentions?.map((m) => ({
                      handle: m.handle,
                      display_name: m.displayName,
                      start: m.start,
                      end: m.end,
                    })),
                    track_timestamp: trackTimestamp || null,
                  },
            params: {
              path: {
                hook_id: entityId,
              },
            },
          }
        );
        if (error) {
          throw formatErrorResponse(error, response);
        }
        return deepCamelKeys(data);
      }
    },
    onSuccess: async (newComment) => {
      // Update the current sort order cache with the new comment
      queryClient.setQueryData<InfiniteData<CommentsResponse>>(
        commentsKeys.comments({
          entityType,
          entityId,
          sortBy,
          deeplinkedCommentId,
        }),
        (prevData) => {
          if (!prevData?.pages) return prevData;
          return produce(prevData, (nextData) => {
            nextData.pages[0].results.unshift(newComment as CommentEntity);
            for (const page of nextData.pages) {
              if (typeof page.totalCount === 'number') {
                ++page.totalCount;
              }
            }
          });
        }
      );

      // Mark all other sort order queries as stale so they refetch when accessed
      await queryClient.invalidateQueries({
        queryKey: commentsKeys.comments({
          entityType,
          entityId,
        }),
        type: 'active',
      });

      await queryClient.invalidateQueries({
        queryKey: commentsKeys.commentCount({
          entityType,
          entityId,
          deeplinkedCommentId,
        }),
        type: 'active',
      });
    },
    onError: (error) => {
      toast({
        title: error.message || t('comments.commentPostError'),
        status: 'error',
        duration: 5000,
        isClosable: true,
      });
    },
  });

  const numComments = commentsQuery.data?.totalCount ?? 0;
  useEffect(() => {
    if (syncClipCommentCount && numComments != null && entityType === 'clip') {
      const prevClip = clipsStore.clipById[entityId];
      if (prevClip) {
        prevClip.comment_count = numComments;
      }
    }
  }, [entityId, clipsStore, numComments, syncClipCommentCount, entityType]);

  return useMemo(
    () => ({
      comments: commentsQuery.data?.results || [],
      numComments,
      allowComments: commentsQuery.data?.allowComment,
      disableCommentReason: commentsQuery.data?.disableReason,
      updatedTime: new Date(commentsQuery.dataUpdatedAt).toUTCString(),

      query: commentsQuery,
      reactionMutation: commentReactionMutation,
      commentMutation: commentPostMutation,
      deleteMutation: commentDeleteMutation,
      reportMutation: commentReportMutation,
      blockMutation: commentBlockMutation,
      unblockMutation: commentUnblockMutation,
    }),
    [
      numComments,
      commentsQuery,
      commentReactionMutation,
      commentPostMutation,
      commentDeleteMutation,
      commentReportMutation,
      commentBlockMutation,
      commentUnblockMutation,
    ]
  );
}

export function useCommentReplies(
  options: {
    entityId: string;
    entityType: CommentEntityType;
    commentId: string;
    initialData?: RepliesResponse;
    deeplinkedCommentId?: string;
  } & Pick<QueryObserverOptions<any>, 'enabled' | 'refetchOnMount'>
) {
  const {
    entityId,
    entityType,
    commentId,
    enabled,
    refetchOnMount,
    initialData,
    deeplinkedCommentId,
  } = options;

  const { t } = useTranslation();

  const apiClient = useApiClient();

  const queryClient = useQueryClient();

  const repliesQuery = useInfiniteQuery({
    enabled,
    refetchOnMount,
    queryKey: commentsKeys.replies({
      entityType,
      entityId,
      commentId,
      deeplinkedCommentId,
    }),
    queryFn: async ({ pageParam }) => {
      if (entityType !== 'hook') {
        const { data, response, error } = await apiClient.GET(
          '/api/comment/{comment_id}/replies',
          {
            params: {
              query: {
                cursor: pageParam,
                page_size: 10,
                deeplinked_comment_id: deeplinkedCommentId,
              },
              path: {
                comment_id: commentId,
              },
            },
          }
        );
        if (error) {
          throw formatErrorResponse(error, response);
        }
        return deepCamelKeys(data) as RepliesResponse;
      } else {
        const { data, response, error } = await apiClient.GET(
          '/api/video/hooks/comments/{comment_id}/replies',
          {
            params: {
              query: {
                cursor: pageParam,
                page_size: 10,
                deeplinked_comment_id: deeplinkedCommentId,
              },
              path: {
                comment_id: commentId,
              },
            },
          }
        );
        if (error) {
          throw formatErrorResponse(error, response);
        }
        return deepCamelKeys(data) as RepliesResponse;
      }
    },
    initialPageParam: null as string | null,
    getNextPageParam: (lastPage) => lastPage?.replyContinuationToken,
    select(data) {
      return {
        ...data.pages[data.pages.length - 1],
        replies: data.pages
          .flatMap((data) => data?.replies)
          .filter((v) => v != null),
      } as (typeof data.pages)[number];
    },
    staleTime: 5 * 60 * 1000,
  });

  // Treating top replies as its own query that we bootstrap with data from the
  // top-level comment. Wonky though it is, it's at least more explicit than
  // treating top replies as the "initial data" for the infinite query.
  const topRepliesQuery = useQuery({
    queryKey: commentsKeys.topReplies({
      entityType,
      entityId,
      commentId,
      deeplinkedCommentId,
    }),
    queryFn: async () => {
      // This query is bootstrapped with initialData, so we return that data
      // or null if no initial data is provided
      return initialData || null;
    },
    initialData,
  });

  const replyReactionMutation = useMutation({
    mutationFn: async ({
      commentId: replyId,
      commentEntityType: replyEntityType,
      isLike,
    }: {
      commentId: string;
      commentEntityType: CommentEntityType;
      isLike: boolean;
    }) => {
      if (entityType !== 'hook') {
        const { data, response, error } = await apiClient.POST(
          '/api/comment/{comment_id}/reaction',
          {
            body: {
              entity_type: replyEntityType,
              reaction: isLike ? 'LIKE' : 'DISLIKE',
            },
            params: {
              path: {
                comment_id: replyId,
              },
            },
          }
        );
        if (error) {
          throw formatErrorResponse(error, response);
        }
        return data;
      } else {
        const { data, response, error } = await apiClient.POST(
          '/api/video/hooks/comments/{comment_id}/reaction',
          {
            body: {
              entity_type: replyEntityType,
              reaction: isLike ? 'LIKE' : 'DISLIKE',
            },
            params: {
              path: {
                comment_id: replyId,
              },
            },
          }
        );
        if (error) {
          throw formatErrorResponse(error, response);
        }
        return data;
      }
    },
    onMutate: async ({ commentId: replyId, isLike }) => {
      const prevData = queryClient.getQueryData<InfiniteData<RepliesResponse>>(
        commentsKeys.replies({
          entityType,
          entityId,
          commentId,
        })
      );
      queryClient.setQueryData<InfiniteData<RepliesResponse>>(
        commentsKeys.replies({
          entityType,
          entityId,
          commentId,
        }),
        (prevData) => {
          if (!prevData?.pages) return prevData;
          // Update the comment
          return produce(prevData, (nextData) => {
            for (const page of nextData.pages) {
              for (const comment of page.replies || []) {
                if (comment.id === replyId) {
                  comment.numLikes = isLike
                    ? comment.numLikes + 1
                    : Math.max(0, comment.numLikes - 1);
                  comment.reactionType = isLike ? 'like' : null;
                }
              }
            }
          });
        }
      );

      // Mirror write to preview top replies
      const prevDataTopReplies = queryClient.getQueryData<
        typeof topRepliesQuery.data
      >(
        commentsKeys.topReplies({
          entityType,
          entityId,
          commentId,
        })
      );
      queryClient.setQueryData<typeof topRepliesQuery.data>(
        commentsKeys.topReplies({
          entityType,
          entityId,
          commentId,
        }),
        (prevData) => {
          if (!prevData?.replies) return prevData;
          // Update the comment
          return produce(prevData, (nextData) => {
            for (const comment of nextData.replies || []) {
              if (comment.id === replyId) {
                comment.numLikes = isLike
                  ? comment.numLikes + 1
                  : Math.max(0, comment.numLikes - 1);
                comment.reactionType = isLike ? 'like' : null;
              }
            }
          });
        }
      );

      return { prevData, prevDataTopReplies };
    },
    onError: (_err, _variables, context) => {
      if (context?.prevData) {
        queryClient.setQueryData(
          commentsKeys.replies({
            entityType,
            entityId,
            commentId,
          }),
          context.prevData
        );
      }
      if (context?.prevDataTopReplies) {
        queryClient.setQueryData(
          commentsKeys.topReplies({
            entityType,
            entityId,
            commentId,
          }),
          context.prevDataTopReplies
        );
      }
    },
  });

  const replyPostMutation = useMutation({
    mutationFn: async ({
      content,
      commentId = null,
      commentEntityType,
    }: {
      content:
        | string
        | {
            content: string;
            userMentions?: Array<{
              handle: string;
              displayName?: string;
              start: number;
              end: number;
            }>;
          };
      commentId?: string | null;
      commentEntityType?: CommentEntityType;
    }) => {
      if (entityType !== 'hook') {
        const { data, response, error } = await apiClient.POST(
          '/api/gen/{clip_id}/comment',
          {
            body:
              typeof content === 'string'
                ? {
                    content,
                    parent_id: commentId,
                    entity_type: commentEntityType,
                  }
                : {
                    content: content.content,
                    parent_id: commentId,
                    entity_type: commentEntityType,
                    user_mentions: content.userMentions?.map((m) => ({
                      handle: m.handle,
                      display_name: m.displayName,
                      start: m.start,
                      end: m.end,
                    })),
                  },
            params: {
              path: {
                clip_id: entityId,
              },
            },
          }
        );
        if (error) {
          throw formatErrorResponse(error, response);
        }
        return deepCamelKeys(data);
      } else {
        const { data, response, error } = await apiClient.POST(
          '/api/video/hooks/comments/{hook_id}/comment',
          {
            body:
              typeof content === 'string'
                ? {
                    content,
                    parent_id: commentId,
                    entity_type: commentEntityType,
                  }
                : {
                    content: content.content,
                    parent_id: commentId,
                    entity_type: commentEntityType,
                    user_mentions: content.userMentions?.map((m) => ({
                      handle: m.handle,
                      display_name: m.displayName,
                      start: m.start,
                      end: m.end,
                    })),
                  },
            params: {
              path: {
                hook_id: entityId,
              },
            },
          }
        );
        if (error) {
          throw formatErrorResponse(error, response);
        }
        return deepCamelKeys(data);
      }
    },
    onSuccess: async (newComment) => {
      queryClient.setQueryData<InfiniteData<RepliesResponse>>(
        commentsKeys.replies({
          entityType,
          entityId,
          commentId,
        }),
        (prevData) => {
          if (!prevData?.pages) return prevData;
          return produce(prevData, (nextData) => {
            nextData.pages[0].replies?.unshift(newComment as CommentEntity);
            for (const page of nextData.pages) {
              if (typeof page.totalCount === 'number') {
                ++page.totalCount;
              }
            }
          });
        }
      );
      await queryClient.invalidateQueries({
        queryKey: commentsKeys.commentCount({
          entityType,
          entityId,
        }),
        type: 'active',
      });
      /**
       * Invalidating would refetch ALL pages of the infinite query, so we will
       * just prepend the comment that was just posted and hope for the best.
       * This has the benefit of ignoring the sort order so that the user will
       * sees their comment at the top.
       */
      // await queryClient.invalidateQueries({
      //   queryKey: commentsKeys.replies({ entityId, entityType, commentId }),
      //   type: 'active',
      // });
      // And again, we need to write to the top replies query
      if (newComment) {
        queryClient.setQueryData<typeof topRepliesQuery.data>(
          commentsKeys.topReplies({
            entityType,
            entityId,
            commentId,
          }),
          (prevData) => {
            const nextData = {
              ...prevData,
              replies: [...(prevData?.replies || [])],
            };
            // Prepend the new reply to the list
            nextData.replies.unshift(newComment as CommentEntity);
            return nextData;
          }
        );
      }
    },
    onError: async (error) => {
      toast({
        title: error.message || t('comments.commentPostError'),
        status: 'error',
        duration: 5000,
        isClosable: true,
      });
    },
  });

  const replyDeleteMutation = useMutation({
    mutationFn: async ({
      commentId: replyId,
      commentEntityType: replyEntityType,
    }: {
      commentId: string;
      commentEntityType: CommentEntityType;
    }) => {
      if (entityType !== 'hook') {
        const { data, response, error } = await apiClient.DELETE(
          '/api/comment/{comment_id}',
          {
            params: {
              path: {
                comment_id: replyId,
              },
            },
            // @TODO uncomment when the schema gets updated
            // body: {
            //   entity_type: replyEntityType,
            // },
          }
        );
        if (error) {
          throw formatErrorResponse(error, response);
        }
        return data;
      } else {
        const { data, response, error } = await apiClient.DELETE(
          '/api/video/hooks/comments/{comment_id}',
          {
            params: {
              path: {
                comment_id: replyId,
              },
            },
            body: {
              entity_type: replyEntityType,
            },
          }
        );
        if (error) {
          throw formatErrorResponse(error, response);
        }
        return data;
      }
    },
    onMutate: async ({ commentId: replyId }) => {
      const prevData = updateCommentReplyData(
        queryClient,
        commentsKeys.replies({
          entityType,
          entityId,
          commentId,
        }),
        replyId,
        (reply) => {
          reply.isDeleted = true;
        },
        (nextData, hasEdit = true) => {
          if (hasEdit) {
            for (const page of nextData.pages) {
              if (typeof page.totalCount === 'number') {
                page.totalCount = Math.max(0, page.totalCount - 1);
              }
            }
          }
        }
      );
      const prevDataTopReplies = updateCommentTopReplyData(
        queryClient,
        commentsKeys.topReplies({
          entityType,
          entityId,
          commentId,
        }),
        replyId,
        (reply) => {
          reply.isDeleted = true;
        },
        (nextData, hasEdit = true) => {
          if (hasEdit) {
            if (typeof nextData.totalCount === 'number') {
              nextData.totalCount = Math.max(0, nextData.totalCount - 1);
            }
          }
        }
      );
      return { prevData, prevDataTopReplies };
    },
    onSuccess: async () => {
      toast({
        title: t('comments.deleteSuccess'),
        status: 'info',
        duration: 5000,
        isClosable: true,
      });
      await queryClient.invalidateQueries({
        queryKey: commentsKeys.commentCount({
          entityType,
          entityId,
        }),
        type: 'active',
      });
    },
    onError: (_err, _variables, context) => {
      if (context?.prevData) {
        queryClient.setQueryData(
          commentsKeys.replies({
            entityType,
            entityId,
            commentId,
          }),
          context.prevData
        );
      }
      if (context?.prevDataTopReplies) {
        queryClient.setQueryData(
          commentsKeys.topReplies({
            entityType,
            entityId,
            commentId,
          }),
          context.prevDataTopReplies
        );
      }
      toast({
        title: t('comments.deleteError'),
        status: 'error',
        duration: 5000,
        isClosable: true,
      });
    },
  });

  const replyReportMutation = useMutation({
    mutationFn: async ({
      commentId: replyId,
      commentEntityType: replyEntityType,
      reason,
    }: {
      commentId: string;
      commentEntityType: CommentEntityType;
      reason?: string;
    }) => {
      if (entityType !== 'hook') {
        const { data, response, error } = await apiClient.POST(
          '/api/comment/{comment_id}/report',
          {
            body: {
              entity_type: replyEntityType,
              reason,
            },
            params: {
              path: {
                comment_id: replyId,
              },
            },
          }
        );
        if (error) {
          throw formatErrorResponse(error, response);
        }
        return deepCamelKeys(data);
      } else {
        const { data, response, error } = await apiClient.POST(
          '/api/video/hooks/comments/{comment_id}/report',
          {
            body: {
              entity_type: replyEntityType,
              reason,
            },
            params: {
              path: {
                comment_id: replyId,
              },
            },
          }
        );
        if (error) {
          throw formatErrorResponse(error, response);
        }
        return deepCamelKeys(data);
      }
    },
    onMutate: async ({ commentId: replyId }) => {
      const prevData = updateCommentReplyData(
        queryClient,
        commentsKeys.replies({
          entityType,
          entityId,
          commentId,
        }),
        replyId,
        (reply) => {
          reply.isReported = true;
        }
      );
      const prevDataTopReplies = updateCommentTopReplyData(
        queryClient,
        commentsKeys.topReplies({
          entityType,
          entityId,
          commentId,
        }),
        replyId,
        (reply) => {
          reply.isReported = true;
        }
      );
      return { prevData, prevDataTopReplies };
    },
    onSuccess: (data) => {
      toast({
        title: t('comments.reportSuccess'),
        status: 'info',
        duration: 5000,
        isClosable: true,
      });
      // @TODO: Use response to determine whether to hide comment
      const shouldDelete = false;
      if (shouldDelete) {
        const replyId = data.commentId;
        updateCommentReplyData(
          queryClient,
          commentsKeys.replies({
            entityType,
            entityId,
            commentId,
          }),
          replyId,
          (reply) => {
            reply.isDeleted = true;
          },
          (nextData, hasEdit = true) => {
            if (hasEdit) {
              for (const page of nextData.pages) {
                if (typeof page.totalCount === 'number') {
                  page.totalCount = Math.max(0, page.totalCount - 1);
                }
              }
            }
          }
        );
        updateCommentTopReplyData(
          queryClient,
          commentsKeys.topReplies({
            entityType,
            entityId,
            commentId,
          }),
          replyId,
          (reply) => {
            reply.isDeleted = true;
          },
          (nextData, hasEdit = true) => {
            if (hasEdit) {
              if (typeof nextData.totalCount === 'number') {
                nextData.totalCount = Math.max(0, nextData.totalCount - 1);
              }
            }
          }
        );
      }
    },
    onError: (_err, _variables, context) => {
      if (context?.prevData) {
        queryClient.setQueryData(
          commentsKeys.replies({
            entityType,
            entityId,
            commentId,
          }),
          context.prevData
        );
      }
      if (context?.prevDataTopReplies) {
        queryClient.setQueryData(
          commentsKeys.topReplies({
            entityType,
            entityId,
            commentId,
          }),
          context.prevDataTopReplies
        );
      }
      toast({
        title: t('comments.reportError'),
        status: 'error',
        duration: 5000,
        isClosable: true,
      });
    },
  });

  const replyBlockMutation = useMutation({
    mutationFn: async ({
      handle,
      reason,
    }: {
      handle: string;
      reason?: string;
    }) => {
      const { data, response, error } = await apiClient.POST(
        '/api/comment/block-user',
        {
          body: {
            handle,
            reason,
          },
        }
      );
      if (error) {
        throw formatErrorResponse(error, response);
      }
      return deepCamelKeys(data);
    },
    onSuccess: async (_data, { handle }) => {
      toast({
        title: handle
          ? t('comments.blockSuccessName', { name: handle })
          : t('comments.blockSuccess'),
        status: 'info',
        duration: 5000,
        isClosable: true,
      });

      // Update current queries optimistically
      updateCommentReplyData(
        queryClient,
        commentsKeys.replies({
          entityType,
          entityId,
          commentId,
        }),
        null,
        (comment) => {
          if (comment.userHandle === handle) {
            comment.userCommentsBlocked = true;
          }
        }
      );
      updateCommentTopReplyData(
        queryClient,
        commentsKeys.topReplies({
          entityType,
          entityId,
          commentId,
        }),
        null,
        (comment) => {
          if (comment.userHandle === handle) {
            comment.userCommentsBlocked = true;
          }
        }
      );

      // Invalidate all comment-related queries for this entity to ensure consistency
      await queryClient.invalidateQueries({
        queryKey: commentsKeys.all,
        type: 'active',
      });
    },
    onError: () => {
      toast({
        title: t('comments.blockError'),
        status: 'error',
        duration: 5000,
        isClosable: true,
      });
    },
  });

  const replyUnblockMutation = useMutation({
    mutationFn: async ({ handle }: { handle: string }) => {
      const { data, response, error } = await apiClient.POST(
        '/api/comment/unblock-user',
        {
          body: {
            handle,
          },
        }
      );
      if (error) {
        throw formatErrorResponse(error, response);
      }
      return deepCamelKeys(data);
    },
    onSuccess: async (_data, { handle }) => {
      toast({
        title: handle
          ? t('comments.unblockSuccessName', { name: handle })
          : t('comments.unblockSuccess'),
        status: 'info',
        duration: 5000,
        isClosable: true,
      });

      // Update current queries optimistically
      updateCommentReplyData(
        queryClient,
        commentsKeys.replies({
          entityType,
          entityId,
          commentId,
        }),
        null,
        (comment) => {
          if (comment.userHandle === handle) {
            comment.userCommentsBlocked = false;
          }
        }
      );
      updateCommentTopReplyData(
        queryClient,
        commentsKeys.topReplies({
          entityType,
          entityId,
          commentId,
        }),
        null,
        (comment) => {
          if (comment.userHandle === handle) {
            comment.userCommentsBlocked = false;
          }
        }
      );

      // Invalidate all comment-related queries for this entity to ensure consistency
      await queryClient.invalidateQueries({
        queryKey: commentsKeys.all,
        type: 'active',
      });
    },
    onError: () => {
      toast({
        title: t('comments.unblockError'),
        status: 'error',
        duration: 5000,
        isClosable: true,
      });
    },
  });

  return useMemo(
    () => ({
      topComments: topRepliesQuery.data?.replies || [],
      comments: repliesQuery.data?.replies || [],
      numComments: repliesQuery.data?.totalCount,
      updatedTime: new Date(repliesQuery.dataUpdatedAt).toUTCString(),

      query: repliesQuery,
      reactionMutation: replyReactionMutation,
      commentMutation: replyPostMutation,
      deleteMutation: replyDeleteMutation,
      reportMutation: replyReportMutation,
      blockMutation: replyBlockMutation,
      unblockMutation: replyUnblockMutation,
    }),
    [
      topRepliesQuery,
      repliesQuery,
      replyReactionMutation,
      replyPostMutation,
      replyDeleteMutation,
      replyReportMutation,
      replyBlockMutation,
      replyUnblockMutation,
    ]
  );
}

export function useMentionSearch(
  query?: string | null,
  boostedUserHandles?: string[]
) {
  const apiClient = useApiClient();

  const searchUsersQuery = useQuery({
    queryKey: commentsKeys.mentionSearch({
      query,
      boostedUserHandles,
    }),
    queryFn: async () => {
      const response = await apiClient.POST('/api/search/users', {
        body: {
          term: query || '',
          boosted_user_handles: boostedUserHandles,
        },
      });
      return deepCamelKeys(response.data || []);
    },
    enabled: query != null,
  });

  return useMemo(
    () => ({
      suggestedUsers: searchUsersQuery.data || [],
      updatedTime: new Date(searchUsersQuery.dataUpdatedAt).toUTCString(),

      query: searchUsersQuery,
    }),
    [searchUsersQuery]
  );
}

export function useMutualFollowerSearch(
  query?: string | null,
  options?: { enabled?: boolean }
) {
  const apiClient = useApiClient();

  // Fetch mutual followers from the backend with server-side search
  const mutualFollowersQuery = useInfiniteQuery({
    queryKey: mutualFollowersKeys.search(query),
    queryFn: async ({ pageParam = 1 }) => {
      const response = await apiClient.GET('/api/profiles/mutual-followers', {
        params: {
          query: {
            page: pageParam,
            query: query || undefined, // Don't pass empty string, use undefined
          },
        },
      });

      if (!response.data) {
        return { profiles: [], currentPage: pageParam, numTotalProfiles: 0 };
      }

      const data = deepCamelKeys(response.data);
      return {
        profiles: data.profiles || [],
        currentPage: data.currentPage || pageParam,
        numTotalProfiles: data.numTotalProfiles || 0,
      };
    },
    initialPageParam: 1,
    getNextPageParam: (lastPage, allPages) => {
      // Calculate total profiles loaded so far across all pages
      const totalLoadedProfiles = allPages.reduce(
        (sum, page) => sum + page.profiles.length,
        0
      );
      const hasMore = totalLoadedProfiles < lastPage.numTotalProfiles;
      return hasMore ? lastPage.currentPage + 1 : undefined;
    },
    select: (data) => ({
      profiles: data.pages.flatMap((page) => page.profiles),
      currentPage: data.pages[data.pages.length - 1].currentPage,
      numTotalProfiles: data.pages[data.pages.length - 1].numTotalProfiles,
    }),
    staleTime: 5 * 60 * 1000,
    enabled: options?.enabled ?? query != null,
  });

  return useMemo(
    () => ({
      suggestedUsers: mutualFollowersQuery.data?.profiles || [],
      updatedTime: new Date(mutualFollowersQuery.dataUpdatedAt).toUTCString(),
      query: mutualFollowersQuery,
    }),
    [mutualFollowersQuery]
  );
}
