/**
 * Centralized query key factory — single source of truth for all query keys.
 * Used by both query factories and mutation cache invalidation.
 *
 * Every key starts with `acct()` (the active account id) so per-account caches
 * are naturally namespaced; switching accounts re-keys every consuming hook.
 *
 * When there is no active account (the brief render window between sign-out
 * clearing the active id and the navigator landing on `/(public)/login`),
 * `acct()` returns a stable `__no_active_account__` sentinel rather than
 * throwing. Every consuming hook gates its query on a real-account predicate
 * (`enabled: !!activeAccountId` / `enabled: userId.length > 0` / etc.), so
 * sentinel-prefixed keys are never actually fetched — they exist just long
 * enough to keep `useQuery` from crashing the render. Without the sentinel,
 * a screen still mounted past the active-id flip (Settings, Settings →
 * Accounts, profile, …) throws on every render and the post-sign-out
 * `router.replace('/(public)/login')` never gets a chance to apply.
 *
 * Keys must still only be constructed at runtime (inside hooks/mutations/
 * queryFns), never at module load — a sentinel-keyspace key built at module
 * load and persisted into the cache would survive the user's next sign-in
 * and re-key incorrectly. Module-level helpers that need feed-key lists must
 * wrap them in a function so resolution is deferred until a session exists.
 */
import { AccountStore } from '@state/accountStore'

const NO_ACTIVE_ACCOUNT_SENTINEL = '__no_active_account__'

const acct = () => AccountStore.activeId() ?? NO_ACTIVE_ACCOUNT_SENTINEL

export const queryKeys = {
  posts: {
    homeFeed: () => [acct(), 'homeFeed'] as const,
    // Mastodon-only "list timeline" feed (Phase B §3.3). Account-scoped
    // because lists are per-account; the feed cache rotates with the active
    // account. The literal segment differs from `homeFeed` so optimistic
    // mutations targeting one don't accidentally match the other.
    feedByList: (listId: string) => [acct(), 'homeFeed', 'list', listId] as const,
    networkFeed: (remoteFilter: 'all' | 'followed' = 'all') =>
      [acct(), 'fetchNetworkFeed', remoteFilter] as const,
    // Prefix for cache-wide operations that want to match every network feed
    // variant (e.g. `setQueriesData` optimistic updates).
    networkFeedPrefix: () => [acct(), 'fetchNetworkFeed'] as const,
    byId: (id: string) => [acct(), 'getStatusById', id] as const,
    byUser: (userId: string) => [acct(), 'statusesById', userId] as const,
    // Single most-recent status — the List Organizer quick-look's "latest
    // post" peek. Separate literal from `byUser` so the 1-item fetch never
    // collides with the full paginated profile-statuses cache.
    latestByUser: (userId: string) => [acct(), 'latestStatusByUser', userId] as const,
    // D1a — separate slice for synthetic posts assembled directly from a
    // remote actor's AP outbox when the home server returns no statuses for
    // them. Kept off the persisted/MMKV cache (the consuming infinite query
    // sets `persister: undefined`); the key just keeps it from colliding with
    // `byUser`'s home-instance slice.
    byUserOutbox: (userId: string) =>
      [acct(), 'statusesById', userId, 'remoteOutbox'] as const,
    eventsByUser: (userId: string) => [acct(), 'eventsByUser', userId] as const,
    thread: (id: string) => [acct(), 'thread', id] as const,
    // Side-channel key for the `Mastodon-Async-Refresh` polling state of a
    // `/context` view. Distinct from `posts.thread(id)` so polling re-renders
    // (or future polling-state subscribers) don't collide with thread data
    // subscribers.
    threadAsyncRefresh: (id: string) => [acct(), 'thread', id, 'asyncRefresh'] as const,
    likes: (id: string) => [acct(), 'getStatusLikes', id] as const,
    reblogs: (id: string) => [acct(), 'getStatusReblogs', id] as const,
    quotes: (id: string) => [acct(), 'getStatusQuotes', id] as const,
    history: (id: string) => [acct(), 'getStatusHistory', id] as const,
    bookmarks: () => [acct(), 'getSelfBookmarks'] as const,
    selfLikes: () => [acct(), 'getSelfLikes'] as const,
  },
  lists: {
    // Mastodon-only — Phase B §3.3. Pixelfed has no equivalent; the queryFn
    // is `mastodonActiveContext()`-asserted, so a Pixelfed-active read fails
    // loudly.
    lists: () => [acct(), 'getMastodonLists'] as const,
    // Paginated `/api/v1/lists/:id/accounts`.
    accounts: (listId: string) => [acct(), 'getMastodonListAccounts', listId] as const,
    // Inverse lookup `/api/v1/accounts/:id/lists`.
    forAccount: (accountId: string) =>
      [acct(), 'getMastodonListsForAccount', accountId] as const,
    // Prefix that matches every `forAccount(*)` cache, used by `setQueriesData`
    // when a deleted list needs to be scrubbed from every account's inverse
    // lookup at once.
    forAccountPrefix: () => [acct(), 'getMastodonListsForAccount'] as const,
    // List Organizer's derived `accountId → listIds[]` map (the inverse of the
    // catalogue), keyed on a stable join of the sorted list-id set. Persisted
    // like every other query so the organizer's per-list fan-out only runs on
    // the first cold open and stays warm after.
    membershipMap: (listKey: string) =>
      [acct(), 'getListMembershipMap', listKey] as const,
    // Prefix that matches every `membershipMap(*)` slice — lets the drop site
    // apply an optimistic `setQueriesData` without reconstructing the exact
    // list-set key.
    membershipMapPrefix: () => [acct(), 'getListMembershipMap'] as const,
  },
  accounts: {
    byId: (id: string) => [acct(), 'getAccountById', id] as const,
    // Handle-keyed account cache. The handle is the normalised `user` or
    // `user@host` form produced by `parseHandle().acct` so the cache slice
    // for a federated mention is the same regardless of where the user
    // tapped it. Resolution goes through `activeProtocol().lookupAccount`,
    // so the per-protocol resolution chain (local-lookup vs. search +
    // webfinger fallback) is opaque to consumers — the cache hits once.
    byHandle: (handle: string) => [acct(), 'getAccountByHandle', handle] as const,
    relationship: (id: string) => [acct(), 'getAccountRelationship', id] as const,
    // Batch relationship lookup (List Organizer). Keyed on a compact stable
    // hash of the sorted account-id set rather than the raw list so the key
    // stays small for a 600-follow batch; the consuming factory in
    // `accounts.ts` computes the hash.
    relationshipsBatch: (idsHash: string) =>
      [acct(), 'getRelationshipsBatch', idsHash] as const,
    followers: (id: string) => [acct(), 'getAccountFollowers', id] as const,
    following: (id: string) => [acct(), 'getAccountFollowing', id] as const,
    self: () => [acct(), 'getSelfAccount'] as const,
    selfProfile: (id: string) => [acct(), 'profileById', id] as const,
    mutuals: (id: string) => [acct(), 'getMutualFollowing', id] as const,
    followRequests: () => [acct(), 'getFollowRequests'] as const,
  },
  notifications: {
    byTab: (tab: string) => [acct(), 'notifications', tab] as const,
    grouped: (tab: string) => [acct(), 'notifications', 'v2', tab] as const,
    v2Probe: () => [acct(), 'notifications', 'v2Probe'] as const,
    policy: () => [acct(), 'notifications', 'policy'] as const,
    requests: () => [acct(), 'notifications', 'requests'] as const,
    unreadCount: () => [acct(), 'notifications', 'unreadCount'] as const,
    pushState: () => [acct(), 'pushState'] as const,
    pushCheck: () => [acct(), 'pushNotificationCheck'] as const,
    // Prefix that matches every notification family above. Used by mutations
    // (e.g. policy edit) that want to drop every cached notification slice.
    allPrefix: () => [acct(), 'notifications'] as const,
  },
  chats: {
    conversations: () => [acct(), 'getConversations'] as const,
    thread: (id: string) => [acct(), 'fetchChat', id] as const,
    composeMutuals: () => [acct(), 'directComposeMutuals'] as const,
  },
  discover: {
    trendingHashtags: () => [acct(), 'getTrendingHashtags'] as const,
    localTimeline: () => [acct(), 'getLocalTimelinePosts'] as const,
  },
  search: {
    query: (q: string) => [acct(), 'search', q] as const,
  },
  hashtags: {
    byName: (id: string) => [acct(), 'getHashtagByName', id] as const,
    feed: (id: string) => [acct(), 'getHashtagByNameFeed', id] as const,
    related: (id: string) => [acct(), 'getHashtagRelated', id] as const,
    followed: () => [acct(), 'getFollowedTags'] as const,
  },
  stories: {
    carousel: () => [acct(), 'getStoryCarousel'] as const,
    viewers: (id: string) => [acct(), 'storyViewers', id] as const,
  },
  collections: {
    self: () => [acct(), 'getSelfCollections'] as const,
  },
  settings: {
    config: () => [acct(), 'getConfig'] as const,
    instanceV1: () => [acct(), 'getInstanceV1'] as const,
    appSettings: () => [acct(), 'getAppSettings'] as const,
    compose: () => [acct(), 'composeSettings'] as const,
    cameraInstance: () => [acct(), 'cameraInstance'] as const,
  },
  privacy: {
    blocked: () => [acct(), 'blockedAccounts'] as const,
    blockedDomains: () => [acct(), 'blockedDomains'] as const,
    muted: () => [acct(), 'mutedAccounts'] as const,
  },
  admin: {
    stats: () => [acct(), 'adminStats'] as const,
    config: () => [acct(), 'getAdminConfig'] as const,
    user: (id: string) => [acct(), 'getAdminUser', id] as const,
    users: () => [acct(), 'users'] as const,
    instances: () => [acct(), 'instances'] as const,
    modReports: () => [acct(), 'getModReports'] as const,
    autospam: () => [acct(), 'getAutospamReports'] as const,
  },
  // Login-screen server discovery hits a public pixelfed.org endpoint with
  // no per-account dimension and is reachable BEFORE any account exists, so
  // these keys are intentionally device-global (no `acct()` prefix). The
  // Phase 2 commit's "every key prepends acct()" rule explicitly does not
  // apply to pre-auth queries — calling `acct()` here would throw on the
  // login screen.
  auth: {
    openServers: () => ['global', 'openServers'] as const,
    registerServers: () => ['global', 'getRegisterServers'] as const,
  },
  // Tapestry caches (Halflight feature) are per-instance, device-global —
  // not per-account. Two accounts on the same instance see the same letters
  // because the bytes are the instance's collective gesture surface, not a
  // per-user feed. Keys live under the same `'global'` carve-out as
  // `auth.*` (Phase 2 query-key rule); `acct()` would be wrong here.
  tapestry: {
    pages: (instance: string) => ['global', 'tapestry', instance, 'pages'] as const,
  },
  halflight: {
    mobilizonEvent: (host: string, uuid: string) =>
      [acct(), 'halflight', 'mobilizonEvent', host, uuid] as const,
    nodeinfo: (host: string) => [acct(), 'halflight', 'nodeinfo', host] as const,
    urlPaste: (href: string) => [acct(), 'halflight', 'urlPaste', href] as const,
  },
  newsroom: {
    timeline: () => [acct(), 'newsroom', 'timeline'] as const,
  },
} as const
