import { type QueryClient, useMutation, useQueryClient } from '@tanstack/react-query'
import { queryKeys } from 'src/lib/queries/keys'
import { useInflight } from 'src/state/InflightProvider'
import { postFeedKeys } from './updatePostInCache'

// Single, generic wiring for the mutation patterns we already had four copies
// of. Every call site provides: which cache keys to guard, how to transform
// the cache optimistically, and how to reconcile the server response.
// Behaviour is uniform — cancel in-flight → snapshot → apply → on error
// restore → on success reconcile. Callers only describe intent.

type AnyKey = readonly unknown[]
export type CacheSnapshot = Array<[AnyKey, unknown]>

export type CacheMutationArgs<TData, TVariables> = {
  mutationFn: (vars: TVariables) => Promise<TData>
  /**
   * Query keys (prefix matching supported via getQueriesData) whose current
   * data should be captured before the optimistic update so we can roll back.
   */
  snapshotKeys: (vars: TVariables) => readonly AnyKey[]
  /**
   * Keys to cancel in-flight fetches on. Defaults to `snapshotKeys`.
   * Override when the cancel surface is narrower than the snapshot surface.
   */
  cancelKeys?: (vars: TVariables) => readonly AnyKey[]
  /** Apply the optimistic update to the cache. */
  applyOptimistic?: (qc: QueryClient, vars: TVariables) => void
  /**
   * Fold the authoritative server response back into the cache. Preferred
   * over `invalidateQueries` so the UI doesn't flash stale numbers between
   * the optimistic value and a refetch.
   */
  reconcile?: (qc: QueryClient, data: TData, vars: TVariables) => void
  onSuccess?: (data: TData, vars: TVariables, qc: QueryClient) => void
  onError?: (err: unknown, vars: TVariables, qc: QueryClient) => void
  /**
   * Opt-in label that lights the global <InflightBar /> while the mutation is
   * in-flight. Omit for snappy optimistic updates where the bar would be
   * noisier than useful; pass a short verb phrase ("Posting…", "Sending…")
   * for flows where the user is waiting on the network. A function form lets
   * callers vary the label by direction — e.g. follow vs. unfollow.
   */
  label?: string | ((vars: TVariables) => string)
}

export function useCacheMutation<TData, TVariables>(
  args: CacheMutationArgs<TData, TVariables>
) {
  const qc = useQueryClient()
  const result = useMutation<TData, unknown, TVariables, CacheSnapshot>({
    mutationFn: args.mutationFn,
    onMutate: async (vars) => {
      const keysToCancel = args.cancelKeys?.(vars) ?? args.snapshotKeys(vars)
      await Promise.all(
        keysToCancel.map((key) => qc.cancelQueries({ queryKey: key as AnyKey }))
      )
      const snapshot: CacheSnapshot = []
      for (const key of args.snapshotKeys(vars)) {
        for (const [matched, data] of qc.getQueriesData({ queryKey: key as AnyKey })) {
          if (data !== undefined) snapshot.push([matched as AnyKey, data])
        }
      }
      args.applyOptimistic?.(qc, vars)
      return snapshot
    },
    onError: (err, vars, snapshot) => {
      if (snapshot) for (const [key, data] of snapshot) qc.setQueryData(key, data)
      args.onError?.(err, vars, qc)
    },
    onSuccess: (data, vars) => {
      args.reconcile?.(qc, data, vars)
      args.onSuccess?.(data, vars, qc)
    },
  })
  const label =
    typeof args.label === 'function'
      ? result.variables !== undefined
        ? args.label(result.variables)
        : undefined
      : args.label
  useInflight(!!label && result.isPending, label)
  return result
}

// --- Scope key helpers -----------------------------------------------------

/**
 * Every cache family that may hold Status objects for this post: the shared
 * `postFeedKeys()` set (home / network / bookmarks / self-likes / thread
 * prefix) plus the single-post detail cache, plus any caller extras.
 */
export function postScopeKeys(postId: string, extras?: readonly AnyKey[]): AnyKey[] {
  return [
    ...(postFeedKeys() as AnyKey[]),
    queryKeys.posts.byId(postId),
    ...(extras ?? []),
  ]
}

/**
 * Cache families that describe an account relationship + surfaces that render
 * its counters. `includeSelf` pulls in the viewer's own `byId` cache so a
 * follow can bump `following_count` on the header the user is currently on.
 */
export function accountScopeKeys(
  accountId: string,
  opts?: {
    handle?: string
    selfId?: string
    includePrivacyLists?: boolean
    extras?: readonly AnyKey[]
  }
): AnyKey[] {
  return [
    queryKeys.accounts.byId(accountId),
    queryKeys.accounts.relationship(accountId),
    ...(opts?.handle ? [queryKeys.accounts.byHandle(opts.handle)] : []),
    ...(opts?.selfId
      ? [queryKeys.accounts.byId(opts.selfId), queryKeys.accounts.self()]
      : []),
    ...(opts?.includePrivacyLists
      ? [queryKeys.privacy.blocked(), queryKeys.privacy.muted()]
      : []),
    ...(opts?.extras ?? []),
  ]
}

/** Trivial pass-through for callers that just want their own list keys. */
export function listScopeKeys(...keys: AnyKey[]): AnyKey[] {
  return keys
}
