import type { QueryClient } from '@tanstack/react-query'
import type { Status } from 'src/lib/api-types'
import { queryKeys } from 'src/lib/queries/keys'

/**
 * All feed-level query keys that may contain post data. Shared with
 * `postScopeKeys` in `cacheMutation.ts` so snapshot/rollback and patch walk
 * the same set.
 *
 * Resolved per call rather than as a module-level constant: every key now
 * starts with the active accountId (queryKeys factory prepends `acct()`),
 * which throws if read before a session exists. Inlining at call time keeps
 * resolution to runtime, when an account is guaranteed to be active.
 */
export function postFeedKeys() {
  return [
    queryKeys.posts.homeFeed(),
    // Prefix — matches both 'all' and 'followed' network-feed variants.
    queryKeys.posts.networkFeedPrefix(),
    queryKeys.posts.bookmarks(),
    queryKeys.posts.selfLikes(),
    // Prefix — matches every `[acct(), 'thread', anchorId]` cache slice.
    queryKeys.posts.thread('').slice(0, -1),
  ]
}

/**
 * Apply `updater` to a status whose id matches `postId`, descending into the
 * `reblog` field when the row is a boost wrapper. Mastodon's home / network /
 * profile timelines deliver boosts as outer `Status` objects whose own `id`
 * is the wrapper-id with the real content nested under `reblog`. Like /
 * share / bookmark / RSVP all target the inner-status id, so a `s.id ===
 * postId`-only walk silently misses every boost — the visible count stays
 * stale until the next refetch and optimistic UX is invisible on Mastodon's
 * boost-heavy feeds. Returns `[s, changed]` so callers can preserve
 * referential identity when nothing matched (avoids spurious re-renders /
 * FlashList cell churn).
 */
function applyToStatus(
  s: Status,
  postId: string,
  updater: (post: Status) => Status
): [Status, boolean] {
  if (s.id === postId) return [updater(s), true]
  if (s.reblog && s.reblog.id === postId) {
    return [{ ...s, reblog: updater(s.reblog) }, true]
  }
  return [s, false]
}

/**
 * Shared utility for optimistically updating a post across all relevant query caches.
 * Used by useLikeMutation, useBookmarkMutation, and useDeletePostMutation.
 */
export function updatePostInCache(
  queryClient: QueryClient,
  postId: string,
  updater: (post: Status) => Status,
  extraKeys?: readonly (readonly string[])[]
) {
  // Single-post cache. byId stores inner statuses (mutation reconcilers
  // unwrap server wrapper responses, and /post/:id navigation always uses
  // the inner-status id), so a direct id match is enough here.
  queryClient.setQueryData(queryKeys.posts.byId(postId), (old?: Status) =>
    old ? updater(old) : old
  )

  // All paginated feed caches
  const allFeedKeys = [...postFeedKeys(), ...(extraKeys ?? [])]
  for (const key of allFeedKeys) {
    queryClient.setQueriesData({ queryKey: key }, (old: any) => {
      // Thread cache shape: { ancestors: Status[]; descendants: Status[] }
      if (old && Array.isArray(old.ancestors) && Array.isArray(old.descendants)) {
        let threadChanged = false
        const map = (s: Status) => {
          const [next, hit] = applyToStatus(s, postId, updater)
          if (hit) threadChanged = true
          return next
        }
        const next = {
          ...old,
          ancestors: old.ancestors.map(map),
          descendants: old.descendants.map(map),
        }
        return threadChanged ? next : old
      }

      if (!old?.pages) return old

      let changed = false
      const newPages = old.pages.map((page: any) => {
        // Page is an array directly
        if (Array.isArray(page)) {
          let pageChanged = false
          const newData = page.map((post: Status) => {
            const [next, hit] = applyToStatus(post, postId, updater)
            if (hit) pageChanged = true
            return next
          })
          if (pageChanged) {
            changed = true
            return newData
          }
          return page
        }
        // Page has a .data array
        if (Array.isArray(page.data)) {
          let pageChanged = false
          const newData = page.data.map((post: Status) => {
            const [next, hit] = applyToStatus(post, postId, updater)
            if (hit) pageChanged = true
            return next
          })
          if (pageChanged) {
            changed = true
            return { ...page, data: newData }
          }
          return page
        }
        return page
      })
      return changed ? { ...old, pages: newPages } : old
    })
  }
}

/**
 * Snapshot query data for rollback — captures all feed keys + detail + extras.
 */
export function snapshotPostCaches(
  queryClient: QueryClient,
  postId: string,
  extraKeys?: readonly (readonly string[])[]
): Map<string, unknown> {
  const snapshot = new Map<string, unknown>()
  const allKeys = [...postFeedKeys(), queryKeys.posts.byId(postId), ...(extraKeys ?? [])]
  // `getQueriesData` does prefix matching, so `['fetchNetworkFeed']`
  // captures every variant (e.g. 'all', 'followed') in one pass.
  for (const key of allKeys) {
    const pairs = queryClient.getQueriesData({ queryKey: key })
    for (const [matchedKey, data] of pairs) {
      if (data !== undefined) {
        snapshot.set(JSON.stringify(matchedKey), data)
      }
    }
  }
  return snapshot
}

/**
 * Restore query data from a snapshot (used on mutation error).
 */
export function restorePostCaches(
  queryClient: QueryClient,
  snapshot: Map<string, unknown>
) {
  for (const [keyStr, value] of snapshot) {
    queryClient.setQueryData(JSON.parse(keyStr), value)
  }
}
