'use client' import { useMemo } from 'react' import { useQuery, useQueryClient } from '@tanstack/react-query' import { rankApiContacts } from '@/lib/contacts/contact-match-score' import { useAuthReady } from '@/lib/api/use-auth-ready' import { useIsDemoContacts } from '@/lib/demo/demo-contacts-context' import { DEMO_CONTACTS_QUERY_ROOT } from '@/lib/demo/demo-contacts-bootstrap' import { useDemoContactsStore } from '@/lib/demo/demo-contacts-store' import { apiClient, OfflineError } from '../client' import type { ApiContact, ApiContactSyncResponse } from '../types' export const FALLBACK_CONTACT_BOOK_ID = 'contacts' type ApiContactBook = { id: string; name: string } type ApiContactsListResponse = { contacts: ApiContact[] pagination?: { page: number page_size: number total?: number } } export function normalizeContactBooksResponse(booksRaw: unknown): ApiContactBook[] { if (Array.isArray(booksRaw)) return booksRaw as ApiContactBook[] if (booksRaw && typeof booksRaw === 'object' && 'address_books' in booksRaw) { return (booksRaw as { address_books: ApiContactBook[] }).address_books ?? [] } return [] } export async function fetchContactByPath(path: string): Promise { const apiPath = path.replace(/^\/+/, '') if (useDemoContactsStore.getState().active) { const found = useDemoContactsStore .getState() .contacts.find( (contact) => contact.path?.replace(/^\/+/, '') === apiPath || contact.uid === apiPath || contact.uid === path, ) if (found) return found } return apiClient.get(`/contacts/${apiPath}`) } export async function fetchContactsForBook(bookId: string): Promise { const pageSize = 500 let page = 1 const all: ApiContact[] = [] while (page <= 100) { const res = await apiClient.get( `/contacts/books/${bookId}`, { page: String(page), page_size: String(pageSize) }, ) const batch = Array.isArray(res) ? res : (res.contacts ?? []) all.push(...batch) if (batch.length < pageSize) break const total = !Array.isArray(res) ? res.pagination?.total : undefined if (total != null && all.length >= total) break page += 1 } return all } export function useDefaultContactBookId() { const { data: booksRaw, isLoading, isError } = useContactBooks() return useMemo(() => { if (isLoading || isError) return undefined const books = normalizeContactBooksResponse(booksRaw) return books[0]?.id ?? FALLBACK_CONTACT_BOOK_ID }, [booksRaw, isLoading, isError]) } export function useContacts(bookId?: string) { const { ready, authenticated } = useAuthReady() const isDemoContacts = useIsDemoContacts() const demoVersion = useDemoContactsStore((s) => s.version) const defaultBookId = useDefaultContactBookId() const resolvedBookId = bookId ?? defaultBookId return useQuery({ queryKey: isDemoContacts ? [...DEMO_CONTACTS_QUERY_ROOT, 'list', resolvedBookId, demoVersion] : ['contacts', resolvedBookId], queryFn: () => { if (isDemoContacts && resolvedBookId) { return useDemoContactsStore.getState().listContacts(resolvedBookId) } return fetchContactsForBook(resolvedBookId!) }, enabled: isDemoContacts ? ready && authenticated && !!resolvedBookId : !!resolvedBookId, staleTime: 5 * 60_000, initialData: isDemoContacts && resolvedBookId ? () => useDemoContactsStore.getState().listContacts(resolvedBookId) : undefined, }) } export function useContactBooks() { const { ready, authenticated } = useAuthReady() const isDemoContacts = useIsDemoContacts() const demoVersion = useDemoContactsStore((s) => s.version) return useQuery({ queryKey: isDemoContacts ? [...DEMO_CONTACTS_QUERY_ROOT, 'books', demoVersion] : ['contact-books'], queryFn: async () => { if (isDemoContacts) { return useDemoContactsStore.getState().listBooks() } const res = await apiClient.get( '/contacts/books', ) return normalizeContactBooksResponse(res) }, enabled: isDemoContacts ? ready && authenticated : true, staleTime: 10 * 60_000, initialData: isDemoContacts ? () => useDemoContactsStore.getState().listBooks() : undefined, }) } export function useSyncContacts(bookId?: string, syncToken?: string) { return useQuery({ queryKey: ['contacts-sync', bookId, syncToken], queryFn: () => apiClient.get(`/contacts/books/${bookId}/sync`, { sync_token: syncToken, }), enabled: !!bookId && !!syncToken, }) } export function useSearchContacts(query: string) { const queryClient = useQueryClient() const isDemoContacts = useIsDemoContacts() const demoVersion = useDemoContactsStore((s) => s.version) return useQuery({ queryKey: isDemoContacts ? [...DEMO_CONTACTS_QUERY_ROOT, 'search', query, demoVersion] : ['contacts-search', query], queryFn: async () => { if (isDemoContacts) { return useDemoContactsStore.getState().searchContacts(query) } try { const res = await apiClient.get( '/contacts/search', { q: query }, ) const list = Array.isArray(res) ? res : (res.contacts ?? []) return rankApiContacts(list, query) } catch (err) { if (err instanceof OfflineError) { const cached = queryClient.getQueriesData({ queryKey: ['contacts'], }) const allContacts: ApiContact[] = [] for (const [, data] of cached) { if (data) allContacts.push(...data) } return rankApiContacts(allContacts, query) } throw err } }, enabled: query.length >= 2, staleTime: 30_000, initialData: isDemoContacts ? () => useDemoContactsStore.getState().searchContacts(query) : undefined, }) } export function useContactInteractions(email?: string) { return useQuery({ queryKey: ['contact-interactions', email], queryFn: () => apiClient.get('/contacts/interactions', { email }), enabled: !!email, }) }