import { infiniteQueryOptions, queryOptions } from '@tanstack/react-query'
import {
  getAccountById,
  getAccountFollowers,
  getAccountFollowing,
  getAccountRelationship,
  getFollowRequests,
  getMutualFollowing,
  getSelfAccount,
} from 'src/lib/api'
import { getRelationshipsBatch } from 'src/lib/api/mastodon'
import type { Account, Relationship } from 'src/lib/api-types'
import type { LinkPaginatedResponse } from 'src/lib/pagination'
import { activeProtocol, parseHandle } from 'src/lib/protocol'
import { AccountStore } from 'src/state/accountStore'
import { queryKeys } from './keys'

// Compact, stable hash of an id set for the relationships-batch query key —
// keeps a 600-element key from ballooning the cache key. cyrb53; collision
// risk is irrelevant here (a collision would just share a cache slice between
// two distinct-but-equal-hash sets, and the queryFn re-fetches the real set).
function hashIds(sortedIds: string[]): string {
  const str = sortedIds.join(',')
  let h1 = 0xdeadbeef
  let h2 = 0x41c6ce57
  for (let i = 0; i < str.length; i++) {
    const ch = str.charCodeAt(i)
    h1 = Math.imul(h1 ^ ch, 2654435761)
    h2 = Math.imul(h2 ^ ch, 1597334677)
  }
  h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909)
  h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909)
  const hash = 4294967296 * (2097151 & h2) + (h1 >>> 0)
  return `${sortedIds.length}:${hash.toString(36)}`
}

export const accountQueries = {
  byId: (id: string) =>
    queryOptions({
      queryKey: queryKeys.accounts.byId(id),
      queryFn: () => getAccountById(id),
      staleTime: 5 * 60 * 1000,
      gcTime: 60 * 60 * 1000,
    }),

  // Replaces the previous `byUsername` factory. The queryFn routes through
  // `activeProtocol().lookupAccount(...)` so the resolution chain is
  // protocol-correct: Pixelfed's `/api/v1.1/accounts/username/{u}` for local,
  // Mastodon's `/api/v1/accounts/lookup` for local + cached remote, and the
  // shared `/api/v2/search?resolve=true` for uncached remote. Throws on
  // not-found / rejected so React Query surfaces the recovery affordance
  // (the "Search for X" button on the profile screen).
  byHandle: (handle: string) =>
    queryOptions<Account>({
      queryKey: queryKeys.accounts.byHandle(handle),
      queryFn: async () => {
        const instance = AccountStore.activeAccountOrThrow().instance
        const parsed = parseHandle(handle, instance)
        const res = await activeProtocol().lookupAccount(parsed)
        if (res.kind === 'found') return res.account
        if (res.kind === 'not-found') {
          throw new Error(`Account "${handle}" not found`)
        }
        throw new Error(res.reason || `Could not look up "${handle}"`)
      },
      staleTime: 5 * 60 * 1000,
      gcTime: 60 * 60 * 1000,
    }),

  relationship: (userId: string) =>
    queryOptions<Relationship>({
      queryKey: queryKeys.accounts.relationship(userId),
      queryFn: () => getAccountRelationship(userId),
      staleTime: 30 * 1000,
      gcTime: 10 * 60 * 1000,
    }),

  // Batch relationships (List Organizer). The id set is sorted so two callers
  // with the same accounts (in any order) share a cache slice, then hashed so
  // the key stays compact for large follow sets. Mastodon-only — the queryFn
  // asserts a Mastodon context.
  relationshipsBatch: (accountIds: string[]) => {
    const sorted = [...accountIds].sort()
    return queryOptions<Relationship[]>({
      queryKey: queryKeys.accounts.relationshipsBatch(hashIds(sorted)),
      queryFn: () => getRelationshipsBatch(sorted),
      staleTime: 5 * 60 * 1000,
      gcTime: 30 * 60 * 1000,
    })
  },

  followers: (id: string) =>
    infiniteQueryOptions({
      queryKey: queryKeys.accounts.followers(id),
      queryFn: ({ pageParam }) => getAccountFollowers(id, pageParam as string),
      initialPageParam: null as string | null,
      staleTime: 5 * 60 * 1000,
      gcTime: 60 * 60 * 1000,
      getNextPageParam: (lastPage: LinkPaginatedResponse<unknown>) =>
        lastPage.nextPage ?? undefined,
      getPreviousPageParam: (lastPage: LinkPaginatedResponse<unknown>) =>
        lastPage.prevPage ?? undefined,
    }),

  following: (id: string) =>
    infiniteQueryOptions({
      queryKey: queryKeys.accounts.following(id),
      queryFn: ({ pageParam }) => getAccountFollowing(id, pageParam as string),
      initialPageParam: null as string | null,
      staleTime: 5 * 60 * 1000,
      gcTime: 60 * 60 * 1000,
      getNextPageParam: (lastPage: LinkPaginatedResponse<unknown>) =>
        lastPage.nextPage ?? undefined,
      getPreviousPageParam: (lastPage: LinkPaginatedResponse<unknown>) =>
        lastPage.prevPage ?? undefined,
    }),

  self: () =>
    queryOptions({
      queryKey: queryKeys.accounts.self(),
      queryFn: getSelfAccount,
      staleTime: 5 * 60 * 1000,
      gcTime: 60 * 60 * 1000,
    }),

  mutualFollowing: (userId: string) =>
    queryOptions({
      queryKey: queryKeys.accounts.mutuals(userId),
      queryFn: () => getMutualFollowing(userId),
      staleTime: 5 * 60 * 1000,
      gcTime: 60 * 60 * 1000,
    }),

  selfProfile: (id: string) =>
    queryOptions<Account>({
      queryKey: queryKeys.accounts.selfProfile(id),
      queryFn: () => getAccountById(id),
      staleTime: 5 * 60 * 1000,
      gcTime: 60 * 60 * 1000,
    }),

  followRequests: () =>
    queryOptions({
      queryKey: queryKeys.accounts.followRequests(),
      queryFn: getFollowRequests,
      staleTime: 60 * 1000,
      gcTime: 30 * 60 * 1000,
    }),
}
