import { infiniteQueryOptions, queryOptions } from '@tanstack/react-query'
import {
  fetchHomeFeed,
  fetchMastodonListTimeline,
  fetchNetworkFeed,
  getAccountStatusesById,
  getSelfBookmarks,
  getSelfLikes,
  getStatusById,
  getStatusContextById,
  getStatusHistory,
  getStatusLikes,
  getStatusQuotes,
  getStatusReblogs,
} from 'src/lib/api'
import type { Account, PaginatedStatus, Status } from 'src/lib/api-types'
import type { LinkPaginatedResponse } from 'src/lib/pagination'
import { isMediaPost, linkPageParam, nextCursorFromLowestId } from 'src/lib/pagination'
import { fetchRemoteOutboxPage } from 'src/lib/remote-outbox'
import { AccountStore } from 'src/state/accountStore'
import { getReplyCount } from 'src/utils'
import { queryClient } from './client'
import { queryKeys } from './keys'

// Bump the cached anchor's reply count to at least `walkedSize` — the
// minimum truthful subtree size implied by a successful `/context` walk
// (descendants.length, after recursive-thread-context). The server's
// stamped `reply_count` may lag (locally-known direct-replies count
// only); we know more after walking. `setQueryData` is cheap and
// flicker-free; `invalidateQueries` would re-fetch the same stamped
// value. Never lowers an existing count — server may have a higher
// truthful count we haven't walked. Sits in the queries module rather
// than `src/utils.ts` so the pure-function tests for utils don't
// transitively pull in the QueryClient / AccountStore / MMKV chain.
export function patchAnchorReplyCount(anchorId: string, walkedSize: number): void {
  queryClient.setQueryData<Status | undefined>(queryKeys.posts.byId(anchorId), (prev) => {
    if (!prev) return prev
    const current = getReplyCount(prev)
    if (walkedSize <= current) return prev
    return { ...prev, reply_count: walkedSize, replies_count: walkedSize }
  })
}

// Plant a server-stamped reply into every cached thread whose
// ancestors+descendants contains the parent (or whose anchor *is* the
// parent), and bump the parent's `reply_count` on each. Also bumps the
// `posts.byId(parentId)` slice so peer surfaces (Home, Profile, Search)
// reflect the new count without a refetch. Sibling of `patchAnchorReplyCount`
// — same singleton-`queryClient` import, same "imperative cache write from
// a non-hook caller" shape, same flicker-free semantic. Called from
// `useComposer.handlePost`'s reply-success branch.
//
// The prefix-match drops the id suffix from `[acct(), 'thread', id]`; same
// trick the multi-account sign-out path uses on `[accountId]` and what
// `patchAnchorReplyCount` would do if it walked instead of single-shot.
type ThreadCtx = { ancestors: Status[]; descendants: Status[] } | undefined
export function appendReplyToCachedThread(parentId: string, reply: Status): void {
  const threadPrefix = queryKeys.posts.thread('').slice(0, -1)
  const threadQueries = queryClient.getQueriesData<ThreadCtx>({
    queryKey: threadPrefix as unknown as readonly unknown[],
  })
  for (const [key, ctx] of threadQueries) {
    if (!ctx) continue
    const inAncestors = ctx.ancestors.some((s) => s.id === parentId)
    const inDescendants = ctx.descendants.some((s) => s.id === parentId)
    const isAnchor = (key as readonly unknown[]).at(-1) === parentId
    if (!inAncestors && !inDescendants && !isAnchor) continue
    const bump = (s: Status) =>
      s.id === parentId ? { ...s, reply_count: getReplyCount(s) + 1 } : s
    queryClient.setQueryData(key, {
      ancestors: ctx.ancestors.map(bump),
      descendants: [...ctx.descendants.map(bump), reply],
    })
  }
  queryClient.setQueryData<Status | undefined>(queryKeys.posts.byId(parentId), (prev) =>
    prev
      ? {
          ...prev,
          reply_count: getReplyCount(prev) + 1,
          replies_count: getReplyCount(prev) + 1,
        }
      : prev
  )
}

export const postQueries = {
  detail: (id: string) =>
    queryOptions({
      queryKey: queryKeys.posts.byId(id),
      queryFn: () => getStatusById(id),
      staleTime: 2 * 60 * 1000,
      gcTime: 30 * 60 * 1000,
    }),

  feed: (opts?: { listId?: string | null }) =>
    infiniteQueryOptions({
      queryKey: opts?.listId
        ? queryKeys.posts.feedByList(opts.listId)
        : queryKeys.posts.homeFeed(),
      queryFn: ({ pageParam }) =>
        opts?.listId
          ? fetchMastodonListTimeline({ listId: opts.listId, pageParam })
          : fetchHomeFeed({ pageParam }),
      initialPageParam: false as string | false,
      refetchOnWindowFocus: false,
      staleTime: 2 * 60 * 1000,
      gcTime: 30 * 60 * 1000,
      getNextPageParam: (lastPage: PaginatedStatus) => lastPage.nextPage ?? undefined,
      getPreviousPageParam: (lastPage: PaginatedStatus) => lastPage.prevPage ?? undefined,
    }),

  networkFeed: (remoteFilter: 'all' | 'followed' = 'all') =>
    infiniteQueryOptions({
      queryKey: queryKeys.posts.networkFeed(remoteFilter),
      queryFn: ({ pageParam }) => fetchNetworkFeed({ pageParam, remoteFilter }),
      initialPageParam: false as string | false,
      refetchOnWindowFocus: false,
      staleTime: 2 * 60 * 1000,
      gcTime: 30 * 60 * 1000,
      getNextPageParam: (lastPage: PaginatedStatus) => lastPage.nextPage ?? undefined,
      getPreviousPageParam: (lastPage: PaginatedStatus) => lastPage.prevPage ?? undefined,
    }),

  // Phase 2.3 — variant decides which cache slice to read and which server-
  // side filter to send. Pixelfed users see two independent feeds on their
  // own profile (the media grid under "Posts" and the everything-feed under
  // "Microblog") that previously shared one cache filtered to media-only,
  // which left users with mostly text posts staring at two empty tabs. The
  // variant suffix on the query key keeps each cache warm independently.
  // Mastodon has a single chronological stream, so 'all' is the only legal
  // variant there — the screen-side helper picks the right one per
  // (accountType, activeTab).
  profileStatuses: (userId: string, variant: 'grid' | 'microblog' | 'all' = 'all') =>
    infiniteQueryOptions({
      queryKey: [...queryKeys.posts.byUser(userId), variant] as const,
      queryFn: async ({ pageParam }): Promise<Status[]> => {
        const accountType = AccountStore.activeAccountOrThrow().type
        const params: {
          max_id?: string
          only_media?: true
        } = { max_id: pageParam }
        // Server-side `only_media` cuts the grid to media-only at the source
        // (Pixelfed honors it; Mastodon ignores it gracefully). The client-
        // side `isMediaPost` filter below is defense-in-depth for Pixelfed
        // posts whose `pf_type` falls outside the media set even when the
        // server returned them.
        if (accountType === 'pixelfed' && variant === 'grid') {
          params.only_media = true
        }
        const data = await getAccountStatusesById(userId, params)
        // Mastodon's `pf_type`-shaped media filter is a no-op (every
        // Mastodon Status has `pf_type === undefined`), so applying it would
        // empty the feed. Pixelfed grid still filters; Pixelfed microblog
        // returns everything (text + media + replies + boosts).
        if (accountType === 'mastodon') return data
        return variant === 'grid' ? data.filter(isMediaPost) : data
      },
      maxPages: 80,
      initialPageParam: undefined as string | undefined,
      staleTime: 5 * 60 * 1000,
      gcTime: 60 * 60 * 1000,
      getNextPageParam: (lastPage: Status[]) => nextCursorFromLowestId(lastPage),
    }),

  // D1a — direct AP outbox fallback for remote profiles whose home instance
  // returns no statuses. The query is paginated by the AP `next` URL emitted
  // on each `OrderedCollectionPage`; pageParam carries that URL forward.
  // `persister: undefined` keeps synthetic entries off the MMKV-persisted
  // query cache — they should evaporate when the user leaves the screen since
  // the home server hasn't actually federated them. Caller (the profile
  // screen) gates `enabled` on (remote profile && empty home result).
  profileStatusesRemoteOutbox: (userId: string, account: Account) =>
    infiniteQueryOptions({
      queryKey: queryKeys.posts.byUserOutbox(userId),
      queryFn: async ({ pageParam }) => {
        return await fetchRemoteOutboxPage(pageParam, account)
      },
      initialPageParam: null as string | null,
      maxPages: 5,
      staleTime: 0,
      gcTime: 5 * 60 * 1000,
      retry: false,
      persister: undefined,
      getNextPageParam: (lastPage) => lastPage.nextPageUrl ?? undefined,
    }),

  profileEvents: (userId: string) =>
    infiniteQueryOptions({
      queryKey: queryKeys.posts.eventsByUser(userId),
      queryFn: async ({ pageParam }): Promise<Status[]> => {
        const data = await getAccountStatusesById(userId, { max_id: pageParam })
        return data.filter((s) => s.pf_type === 'event' || s.reblog?.pf_type === 'event')
      },
      maxPages: 80,
      initialPageParam: undefined as string | undefined,
      staleTime: 5 * 60 * 1000,
      gcTime: 60 * 60 * 1000,
      getNextPageParam: (lastPage: Status[]) => nextCursorFromLowestId(lastPage),
    }),

  // Phase 4.1a — single round-trip against `/context` returning both
  // ancestors and descendants. Replaces the prior `replies` infinite-list
  // shape (which discarded ancestors and chunked descendants by Link
  // header) — `/context` is bodily one-shot on both servers, so neither
  // page-state nor max_id apply. On Halflight-patched Pixelfed (gated
  // client-side via `useRecursiveThreadContext()`) the server walks the
  // full ancestor chain (up to ANCESTORS_LIMIT=40) and the full
  // descendants subtree in DFS preorder; on upstream Pixelfed it walks
  // one level on each side. The "Show full conversation" pivot covers
  // the gap, and `useAsyncRefresh` polls when the response carries the
  // `Mastodon-Async-Refresh` header (federated reply walk in flight).
  thread: (id: string) =>
    queryOptions({
      queryKey: queryKeys.posts.thread(id),
      queryFn: async () => {
        const ctx = await getStatusContextById(id)
        // Opportunistic anchor patch: the walker bound is the minimum
        // truthful subtree size we know. Server's stamped `reply_count`
        // may lag federated replies; the recursive walker has the fuller
        // view. `setQueryData` is cheap and flicker-free.
        patchAnchorReplyCount(id, ctx.descendants?.length ?? 0)
        return ctx
      },
      staleTime: 60 * 1000,
      gcTime: 30 * 60 * 1000,
    }),

  likes: (id: string) =>
    infiniteQueryOptions({
      queryKey: queryKeys.posts.likes(id),
      queryFn: ({ pageParam }) => getStatusLikes(id, pageParam as string),
      initialPageParam: null as string | null,
      staleTime: 30 * 1000,
      gcTime: 10 * 60 * 1000,
      getNextPageParam: (lastPage: LinkPaginatedResponse<unknown>) =>
        linkPageParam(lastPage),
      getPreviousPageParam: (lastPage: LinkPaginatedResponse<unknown>) =>
        lastPage.prevPage ?? undefined,
    }),

  shares: (id: string) =>
    infiniteQueryOptions({
      queryKey: queryKeys.posts.reblogs(id),
      queryFn: ({ pageParam }) => getStatusReblogs(id, pageParam as string),
      initialPageParam: null as string | null,
      staleTime: 30 * 1000,
      gcTime: 10 * 60 * 1000,
      getNextPageParam: (lastPage: LinkPaginatedResponse<unknown>) =>
        linkPageParam(lastPage),
      getPreviousPageParam: (lastPage: LinkPaginatedResponse<unknown>) =>
        lastPage.prevPage ?? undefined,
    }),

  quotes: (id: string) =>
    infiniteQueryOptions({
      queryKey: queryKeys.posts.quotes(id),
      queryFn: ({ pageParam }) => getStatusQuotes(id, pageParam as string | null),
      initialPageParam: null as string | null,
      staleTime: 30 * 1000,
      gcTime: 10 * 60 * 1000,
      getNextPageParam: (lastPage: LinkPaginatedResponse<unknown>) =>
        linkPageParam(lastPage),
      getPreviousPageParam: (lastPage: LinkPaginatedResponse<unknown>) =>
        lastPage.prevPage ?? undefined,
    }),

  history: (id: string) =>
    queryOptions({
      queryKey: queryKeys.posts.history(id),
      queryFn: () => getStatusHistory(id),
      staleTime: 5 * 60 * 1000,
      gcTime: 60 * 60 * 1000,
    }),

  selfBookmarks: () =>
    infiniteQueryOptions({
      queryKey: queryKeys.posts.bookmarks(),
      queryFn: ({ pageParam }) => getSelfBookmarks({ pageParam }),
      initialPageParam: false as string | false,
      staleTime: 2 * 60 * 1000,
      gcTime: 30 * 60 * 1000,
      getNextPageParam: (lastPage: PaginatedStatus) => lastPage.nextPage ?? undefined,
      getPreviousPageParam: (lastPage: PaginatedStatus) => lastPage.prevPage ?? undefined,
    }),

  selfLikes: () =>
    infiniteQueryOptions({
      queryKey: queryKeys.posts.selfLikes(),
      queryFn: ({ pageParam }) => getSelfLikes({ pageParam }),
      initialPageParam: false as string | false,
      staleTime: 2 * 60 * 1000,
      gcTime: 30 * 60 * 1000,
      getNextPageParam: (lastPage: PaginatedStatus) => lastPage.nextPage ?? undefined,
      getPreviousPageParam: (lastPage: PaginatedStatus) => lastPage.prevPage ?? undefined,
    }),
}
