'use client';

import type { Passenger } from '@edr/types';
import {
  useInfiniteQuery,
  useMutation,
  useQuery,
  useQueryClient,
  type InfiniteData,
  type QueryClient,
  type UseInfiniteQueryResult,
} from '@tanstack/react-query';
import { useMemo } from 'react';

import { MESSAGES_PAGE_SIZE, supportApi, type SendMessageInput } from './supportApi';

type ThreadDto = Passenger.PassengerSupportThreadDto;
type MessageDto = Passenger.PassengerSupportMessageDto;
type ConversationDto = Passenger.PassengerSupportConversationDto;

export const SUPPORT_THREAD_KEY = ['support', 'thread'];
export const SUPPORT_UNREAD_KEY = ['support', 'unread'];

/**
 * The device's single support thread, paged backwards from newest.
 *
 * react-query's "next page" is *older* history here, so `pages` runs
 * newest-block-first and has to be reversed to render top-to-bottom in time
 * order. Cursor-based rather than offset so a message arriving mid-scroll can't
 * shift or duplicate the pages already loaded.
 */
// The return type is spelled out rather than inferred: spreading the query
// result produces an anonymous type that names symbols from `query-core`, which
// pnpm's non-flat node_modules makes unnameable from here (TS2742).
export function useThread(enabled = true): UseInfiniteQueryResult<
  InfiniteData<ThreadDto>,
  Error
> & {
  conversation: ConversationDto | null;
  messages: MessageDto[];
  pageCount: number;
} {
  const query = useInfiniteQuery({
    queryKey: SUPPORT_THREAD_KEY,
    queryFn: ({ pageParam }) =>
      supportApi.getThread({ before: pageParam, limit: MESSAGES_PAGE_SIZE }),
    initialPageParam: undefined as string | undefined,
    getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
    enabled,
  });

  // Page 0 is the newest fetch, so its conversation carries the freshest status
  // and unread count even after older pages load.
  const conversation = query.data?.pages[0]?.conversation ?? null;

  const messages = useMemo(
    () => [...(query.data?.pages ?? [])].reverse().flatMap((p) => p.messages),
    [query.data],
  );

  // `pageCount` is exposed so the view can tell a prepended history page from a
  // message appended at the bottom — `messages` changing says nothing about which.
  return {
    ...query,
    conversation,
    messages,
    pageCount: query.data?.pages.length ?? 0,
  };
}

/**
 * Splice a newly-arrived message into the cached thread.
 *
 * Deliberately not `invalidateQueries`: the thread is paginated, so refetching
 * would re-request *every* page the visitor has scrolled back through on every
 * single inbound message — the cost of a message would grow with how far they've
 * read. Page 0 is the newest block and its messages are oldest-first within the
 * block, so a new message belongs on its end.
 *
 * Doesn't seed the cache when the thread isn't loaded: a partial cache here would
 * leave a thread whose "first page" is one message and whose `nextCursor` is
 * missing — which would render as a complete conversation with no way to page
 * back. The caller is told instead, via the outcome below.
 */
/**
 * Why this reports an outcome rather than nothing: the two ways it can decline to
 * append need opposite handling. A duplicate is our own echo and must be ignored —
 * refetching there would undo the whole point. "Uncached" means the thread's first
 * page is still in flight and may have been read on the server *before* this
 * message existed, so dropping it silently would lose it until something else
 * happened to refetch; the caller refetches instead. That's cheap precisely
 * because nothing is loaded yet.
 */
export type AppendOutcome = 'appended' | 'duplicate' | 'uncached';

export function appendMessageToCache(qc: QueryClient, message: MessageDto): AppendOutcome {
  let outcome: AppendOutcome = 'uncached';
  qc.setQueryData<InfiniteData<ThreadDto>>(SUPPORT_THREAD_KEY, (current) => {
    if (!current?.pages.length) return current;
    const [newest, ...rest] = current.pages;
    // Our own message arrives twice — once as the POST response, once as the
    // socket echo. Ignore the duplicate rather than render it twice.
    if (newest.messages.some((m) => m.id === message.id)) {
      outcome = 'duplicate';
      return current;
    }
    outcome = 'appended';
    return {
      ...current,
      pages: [{ ...newest, messages: [...newest.messages, message] }, ...rest],
    };
  });
  return outcome;
}

/**
 * Replace the cached conversation with a freshly-pushed copy.
 *
 * Page 0 is where `useThread` reads the conversation from, so patching it there
 * is enough; `messages` and `nextCursor` are left exactly as they were, which is
 * the point — this must not disturb the paged history. No-op when the thread
 * isn't cached: the in-flight fetch will bring the current conversation anyway.
 */
export function patchConversationInCache(qc: QueryClient, conversation: ConversationDto): void {
  qc.setQueryData<InfiniteData<ThreadDto>>(SUPPORT_THREAD_KEY, (current) => {
    if (!current?.pages.length) return current;
    const [newest, ...rest] = current.pages;
    return { ...current, pages: [{ ...newest, conversation }, ...rest] };
  });
}

export function useUnreadCount(enabled = true) {
  return useQuery({
    queryKey: SUPPORT_UNREAD_KEY,
    queryFn: () => supportApi.unreadCount(),
    enabled,
    refetchInterval: 60_000,
  });
}

export function useSendMessage() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: (input: SendMessageInput) => supportApi.sendMessage(input),
    // The gateway echoes our own message back over the socket, which appends it
    // to the cache — so don't invalidate the thread here, or every send would
    // refetch every page the visitor has scrolled through.
    onSuccess: () => {
      qc.invalidateQueries({ queryKey: SUPPORT_UNREAD_KEY });
    },
  });
}

export function useMarkRead() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: () => supportApi.markRead(),
    // Same reasoning as above: reading a thread changes only the badge, and the
    // messages already on screen are the ones being marked.
    onSuccess: () => {
      qc.invalidateQueries({ queryKey: SUPPORT_UNREAD_KEY });
    },
  });
}
