import type {
  SupportConversationDto,
  SupportMessageDto,
  SupportMessageListResult,
} from "@edr/types";
import {
  useInfiniteQuery,
  useMutation,
  useQuery,
  useQueryClient,
  type InfiniteData,
  type QueryClient,
} from "@tanstack/react-query";
import { useMemo } from "react";

import { supportApi, type SendMessageInput } from "./supportApi";

export const SUPPORT_KEY = ["support"] as const;
export const SUPPORT_CONVERSATION_KEY = ["support", "conversation"] as const;
export const SUPPORT_MESSAGES_KEY = ["support", "messages"] as const;
export const SUPPORT_UNREAD_KEY = ["support", "unread"] as const;

/** Messages per page in the thread. */
const MESSAGES_PAGE_SIZE = 30;

/** My company's support thread — null until the first message is sent. */
export function useConversation(enabled = true) {
  return useQuery({
    queryKey: SUPPORT_CONVERSATION_KEY,
    queryFn: () => supportApi.getConversation(),
    enabled,
  });
}

/**
 * The thread's messages, 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
 * doesn't shift the pages already loaded.
 */
export function useMessages(enabled = true) {
  const query = useInfiniteQuery({
    queryKey: SUPPORT_MESSAGES_KEY,
    queryFn: ({ pageParam }) =>
      supportApi.listMessages({ before: pageParam, limit: MESSAGES_PAGE_SIZE }),
    initialPageParam: undefined as string | undefined,
    getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
    enabled,
  });

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

  // 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, messages, pageCount: query.data?.pages.length ?? 0 };
}

/**
 * Splice a newly-arrived message into the cached thread.
 *
 * Deliberately not `invalidateQueries`: the thread is paginated, so that would
 * refetch *every* page the customer has scrolled back through — making the cost
 * of each inbound message grow with how far they've read. Page 0 is the newest
 * block and its items are oldest-first within the block, so the new message
 * belongs on its end.
 *
 * The socket only ever pushes this company's own thread, so there's no id to
 * match on. Nothing is written when nothing is cached — the panel isn't
 * rendering it, and a seeded one-message page would carry no `nextCursor` and
 * strand the history.
 */
/**
 * Why this reports an outcome rather than a boolean: the two ways it can decline
 * to append need opposite handling. A duplicate is the sender's 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: SupportMessageDto,
): AppendOutcome {
  let outcome: AppendOutcome = "uncached";
  qc.setQueryData<InfiniteData<SupportMessageListResult>>(
    SUPPORT_MESSAGES_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.items.some((m) => m.id === message.id)) {
        outcome = "duplicate";
        return current;
      }
      outcome = "appended";
      return {
        ...current,
        pages: [{ ...newest, items: [...newest.items, message] }, ...rest],
      };
    },
  );
  return outcome;
}

export function useSupportUnreadCount(enabled = true) {
  return useQuery({
    queryKey: SUPPORT_UNREAD_KEY,
    queryFn: () => supportApi.unreadCount(),
    enabled,
    // WebSocket keeps this fresh; poll as a fallback if the socket drops.
    refetchInterval: 60_000,
  });
}

/** Send as the customer. Opens the thread server-side if this is the first one. */
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 customer has scrolled through. The conversation
    // itself still needs a refresh: the first send is what creates it.
    onSuccess: () => {
      qc.invalidateQueries({ queryKey: SUPPORT_CONVERSATION_KEY });
    },
  });
}

export function useMarkConversationRead() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: () => supportApi.markRead(),
    onSuccess: () => {
      qc.invalidateQueries({ queryKey: SUPPORT_CONVERSATION_KEY });
      qc.invalidateQueries({ queryKey: SUPPORT_UNREAD_KEY });
    },
  });
}

export type { SupportConversationDto };
