'use client';

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

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

type MessageDto = Passenger.PassengerSupportMessageDto;
type MessageListResult = Passenger.PassengerSupportMessageListResult;

export const SUPPORT_CONVERSATIONS_KEY = ['support', 'conversations'];
export const SUPPORT_UNREAD_KEY = ['support', 'unread'];
export const supportMessagesKey = (id: string) => ['support', 'messages', id];

/** Threads per page in the inbox. */
const CONVERSATIONS_PAGE_SIZE = 20;
/** Messages per page in a thread. */
const MESSAGES_PAGE_SIZE = 30;

/**
 * Shared inbox: every thread, filterable by status + subject/passenger search.
 *
 * Pages on scroll. This previously fetched one unbounded page and rendered
 * whatever came back, which silently truncated the inbox at the server's own
 * default of 100 with no way to reach the rest.
 */
export function useConversations(params: ListParams = {}) {
  const query = useInfiniteQuery({
    queryKey: [...SUPPORT_CONVERSATIONS_KEY, params],
    queryFn: ({ pageParam }) =>
      supportApi.listConversations({
        ...params,
        page: pageParam,
        limit: CONVERSATIONS_PAGE_SIZE,
      }),
    initialPageParam: 1,
    getNextPageParam: (lastPage, allPages) => {
      const loaded = allPages.reduce((n, page) => n + page.items.length, 0);
      return loaded < lastPage.count ? allPages.length + 1 : undefined;
    },
  });

  /**
   * Flatten for rendering, keeping the page-level fields (count/unreadCount)
   * from the first fetch so badges don't go stale as more pages load.
   *
   * De-duplicated by id because this list pages by OFFSET over a sort key that
   * moves: a thread jumps to rank 1 the moment it gets a message, shifting
   * everything down, so a row already shown on page 1 can be served again on
   * page 2 — and React would then see two children with the same key. The
   * conversations invalidate that rides along with every such event heals the
   * ordering a beat later; this just stops the intervening render from breaking.
   *
   * Keyset wouldn't help here, unlike the message list: the sort key itself
   * mutates, so no cursor over it is stable either.
   */
  const items = useMemo(() => {
    const seen = new Set<string>();
    const flat: Passenger.PassengerSupportConversationDto[] = [];
    for (const page of query.data?.pages ?? []) {
      for (const conversation of page.items) {
        if (seen.has(conversation.id)) continue;
        seen.add(conversation.id);
        flat.push(conversation);
      }
    }
    return flat;
  }, [query.data]);

  return {
    ...query,
    items,
    count: query.data?.pages[0]?.count ?? 0,
    unreadCount: query.data?.pages[0]?.unreadCount ?? 0,
  };
}

/**
 * A 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(conversationId: string | null) {
  const query = useInfiniteQuery({
    queryKey: supportMessagesKey(conversationId ?? ''),
    queryFn: ({ pageParam }) =>
      supportApi.listMessages(conversationId as string, {
        before: pageParam,
        limit: MESSAGES_PAGE_SIZE,
      }),
    initialPageParam: undefined as string | undefined,
    getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
    enabled: !!conversationId,
  });

  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 a cached thread.
 *
 * Deliberately not `invalidateQueries`: that refetches *every* page the agent
 * has scrolled back through, so the cost of each inbound message would 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.
 *
 * No-ops when the thread isn't cached — nothing is rendering it, and seeding a
 * partial cache here would leave a thread whose "first page" is one message and
 * whose `nextCursor` is missing.
 */
/**
 * 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: MessageDto): AppendOutcome {
  let outcome: AppendOutcome = 'uncached';
  qc.setQueryData<InfiniteData<MessageListResult>>(
    supportMessagesKey(message.conversationId),
    (current) => {
      if (!current?.pages.length) return current;
      const [newest, ...rest] = current.pages;
      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 useUnreadCount(enabled = true) {
  return useQuery({
    queryKey: SUPPORT_UNREAD_KEY,
    queryFn: () => supportApi.unreadCount(),
    enabled,
    refetchInterval: 60_000,
  });
}

export function useSendMessage(conversationId: string) {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: (input: SendMessageInput) => supportApi.sendMessage(conversationId, 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 agent has scrolled through. The conversation list
    // still needs a refresh for its last-message preview and ordering.
    onSuccess: () => {
      qc.invalidateQueries({ queryKey: SUPPORT_CONVERSATIONS_KEY });
    },
  });
}

export function useSetStatus() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: ({ id, status }: { id: string; status: string }) =>
      supportApi.setStatus(id, status),
    onSuccess: () => qc.invalidateQueries({ queryKey: SUPPORT_CONVERSATIONS_KEY }),
  });
}

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