'use client'

import { useEffect, useLayoutEffect, useRef, useSyncExternalStore } from 'react'
import {
  UnhashableQueryIRError,
  createLiveQueryObserver,
  createLiveQuerySourceIdClaims,
  deepEquals,
  getLiveQueryKey,
  getSourceObjectToken,
  getStableValueHash,
  identifyLiveQuery,
  isCollection,
  prepareLiveQueryValue,
  rejectLiveQueryInput,
  releaseDeferredOnReclaim,
  resolveLiveQueryValue,
  resumeDeferredLiveQueryCollections,
} from '@tanstack/db'
import { useOptionalDbClient } from './DbProvider'
import { shouldWarnInDevelopment } from './development'
import {
  getPreparedSources,
  getUnboundDescriptorReporter,
} from './source-id-bindings'
import type {
  Collection,
  CollectionImpl,
  CollectionStatus,
  Context,
  DbClient,
  DeferredLiveQueryCollections,
  GetResult,
  InferResultType,
  InitialQueryBuilder,
  LiveQueryCollectionConfig,
  LiveQueryIdentityScope,
  LiveQueryKey,
  LiveQueryObserver,
  LiveQuerySourceIdClaims,
  LiveQueryStatusFlags,
  NonSingleResult,
  PooledLiveQueries,
  QueryBuilder,
  SingleResult,
} from '@tanstack/db'

// Use the client layout commit to schedule window moves. Server rendering has
// no commit; React18 diagnoses a layout effect there even when it is empty.
const useWindowCommitEffect =
  typeof window === `undefined` ? useEffect : useLayoutEffect

const DEFAULT_GC_TIME_MS = 1 // Live queries created by useLiveQuery are cleaned up immediately (0 disables GC)
// Suspense renders can be abandoned before React subscribes. Keep their
// collection briefly so a nearby retry can commit without starting over.
const DEFAULT_SUSPENSE_GC_TIME_MS = 5000
const DERIVED_IDENTITY_SINGLE_RENDER_WARN_MS = 16
const DERIVED_IDENTITY_RENDER_COUNT_WARN_THRESHOLD = 10
const DERIVED_IDENTITY_TOTAL_WARN_MS = 50
const warnedDerivedIdentityCallsites = new Set<string>()
const unpreparedQueryValue = Symbol(`unpreparedQueryValue`)

type SuspenseCollection = Collection<object, string | number, {}>
type SuspenseCollectionEntry = {
  collection: SuspenseCollection
  removeStatusListeners: Array<() => void>
}

const unscopedSuspenseScope = {}
const suspenseCollectionsByClient = new WeakMap<
  object,
  Map<string, SuspenseCollectionEntry>
>()
function getSourceQualifiedSuspenseKey(
  preparedValue: unknown,
  queryHash: string,
  client: DbClient | undefined,
): string {
  const sourceIds = getPreparedSources(
    preparedValue,
    client,
    `useLiveSuspenseQuery`,
  ).map(getSourceObjectToken)
  const {
    query: _query,
    pool: _pool,
    ...options
  } = (preparedValue as { query?: unknown } | null | undefined)?.query
    ? (preparedValue as Record<string, unknown>)
    : {}
  // Local output options qualify sharing; query identity still binds mounted
  // options. Object/function declarations compare by exact runtime reference.
  options.autoIndex ??= `off`
  for (const key in options) {
    const value = options[key]
    if (value === undefined) delete options[key]
    else if (
      value &&
      (typeof value === `object` || typeof value === `function`)
    )
      options[key] = getSourceObjectToken(value)
  }
  return getStableValueHash([sourceIds, options, queryHash])
}

function getSuspenseCollections(
  client: DbClient | undefined,
): Map<string, SuspenseCollectionEntry> {
  const scope = client ?? unscopedSuspenseScope
  let collections = suspenseCollectionsByClient.get(scope)
  if (!collections) {
    collections = new Map()
    suspenseCollectionsByClient.set(scope, collections)
  }
  return collections
}

export type DerivedIdentityProfiler = {
  renderCount: number
  totalMs: number
  maxMs: number
  warned: boolean
}

/** Identity timing and warning state have no work to do in production. */
export function createDerivedIdentityProfiler():
  DerivedIdentityProfiler | undefined {
  try {
    if (process.env.NODE_ENV !== `production`) {
      return { renderCount: 0, totalMs: 0, maxMs: 0, warned: false }
    }
  } catch {
    // Unbundled browsers may omit process; the warning policy handles that.
  }
  return undefined
}

export type UseLiveQueryStatus = CollectionStatus | `disabled`
export type { LiveQueryKey }
type UseLiveQueryConfigOptions<TContext extends Context> = Omit<
  LiveQueryCollectionConfig<TContext>,
  `query`
> & {
  /**
   * Explicit identity for queries that contain opaque functional variants or
   * are hot enough that deriving identity from structured IR is too expensive.
   * Structured queries should omit this so DB can derive identity directly.
   */
  queryKey?: LiveQueryKey
  /** Override the nearest DbProvider for this query. */
  client?: DbClient
  /**
   * Pass `pooledLiveQueries` to share one partition among equality-filtered
   * queries of one shape. Suspense and DbClient queries never pool.
   */
  pool?: PooledLiveQueries
}

type ConfiguredQueryBuilder<TContext extends Context> = Extract<
  LiveQueryCollectionConfig<TContext>[`query`],
  QueryBuilder<TContext>
>

export type UseLiveQueryConfig<TContext extends Context> =
  UseLiveQueryConfigOptions<TContext> &
    Pick<LiveQueryCollectionConfig<TContext>, `query`>

export type ConditionalUseLiveQueryConfig<TContext extends Context> =
  UseLiveQueryConfigOptions<TContext> & {
    query:
      | ConfiguredQueryBuilder<TContext>
      | ((
          q: InitialQueryBuilder,
        ) => ConfiguredQueryBuilder<TContext> | undefined | null)
  }

function getCurrentTime(): number {
  return typeof performance !== `undefined` &&
    typeof performance.now === `function`
    ? performance.now()
    : Date.now()
}

function getWarningCallsite(stackIndex: number): string {
  const stack = new Error().stack ?? `unknown`
  const lines = stack.split(`\n`)
  const userFrame = lines
    .slice(1)
    .find(
      (line) =>
        !line.includes(`useLiveQuery.ts`) &&
        !line.includes(`useLiveSuspenseQuery.ts`) &&
        !line.includes(`useLiveInfiniteQuery.ts`),
    )
  return userFrame?.trim() ?? lines[stackIndex]?.trim() ?? stack
}

function warnDerivedIdentityHotPath(
  profiler: DerivedIdentityProfiler,
  durationMs: number,
): void {
  if (
    profiler.warned ||
    !(
      shouldWarnInDevelopment(`TANSTACK_DB_DISABLE_QUERY_IDENTITY_WARNINGS`) &&
      process.env.NODE_ENV !== `production`
    )
  ) {
    return
  }

  const isSlowSingleRender =
    durationMs >= DERIVED_IDENTITY_SINGLE_RENDER_WARN_MS
  const isHotRenderPath =
    profiler.renderCount >= DERIVED_IDENTITY_RENDER_COUNT_WARN_THRESHOLD &&
    profiler.totalMs >= DERIVED_IDENTITY_TOTAL_WARN_MS

  if (!isSlowSingleRender && !isHotRenderPath) {
    return
  }

  const callsite = getWarningCallsite(5)
  if (warnedDerivedIdentityCallsites.has(callsite)) {
    profiler.warned = true
    return
  }

  warnedDerivedIdentityCallsites.add(callsite)
  profiler.warned = true

  const reason = isSlowSingleRender
    ? `one render took ${durationMs.toFixed(1)}ms`
    : `${profiler.renderCount} renders took ${profiler.totalMs.toFixed(1)}ms`

  console.warn(
    `[useLiveQuery] Deriving live query identity from structured query IR is running on a hot render path (${reason}, max ${profiler.maxMs.toFixed(1)}ms). ` +
      `Provide an explicit queryKey to skip rebuilding and hashing the IR on every render: useLiveQuery({ queryKey: [...], query }).`,
  )
}

export { prepareLiveQueryValue as prepareQueryValue }

type DerivedQueryPreparation = {
  value: unknown
  identityDeps: Array<unknown>
}

/** How a hook checks a prepared query whose identity it compares. */
export function getIdentityScope(
  claims: LiveQuerySourceIdClaims,
  dbClient: DbClient | undefined,
  hookName: string,
): LiveQueryIdentityScope {
  return {
    claims,
    client: dbClient,
    bindingName: hookName,
    onUnboundDescriptor: getUnboundDescriptorReporter(dbClient, hookName),
  }
}

/**
 * Prepare a query without `queryKey`, derive its identity, and claim its
 * source Collection IDs. A query whose IR has no stable identity throws
 * `UnhashableQueryIRError`. The hook's render boundary releases the sync
 * starts a rejected render deferred.
 */
export function prepareDerivedQuery(
  value: unknown,
  profiler: DerivedIdentityProfiler | undefined,
  scope: LiveQueryIdentityScope,
  deferredCollections: Set<CollectionImpl<any, string | number, any, any, any>>,
): DerivedQueryPreparation {
  const shouldProfile =
    profiler !== undefined &&
    /* @__PURE__ */ shouldWarnInDevelopment(
      `TANSTACK_DB_DISABLE_QUERY_IDENTITY_WARNINGS`,
    )
  // Keep literal build guards at both timing sites so production bundlers can
  // erase the timer and warning machinery, as well as the profiler initializer.
  const start =
    shouldProfile && process.env.NODE_ENV !== `production`
      ? getCurrentTime()
      : 0
  const preparedValue = prepareLiveQueryValue(
    value,
    scope.client,
    deferredCollections,
  )

  try {
    return {
      value: preparedValue,
      identityDeps: [`derived`, identifyLiveQuery(preparedValue, scope)],
    }
  } finally {
    if (shouldProfile && process.env.NODE_ENV !== `production`) {
      const durationMs = getCurrentTime() - start
      profiler.renderCount += 1
      profiler.totalMs += durationMs
      profiler.maxMs = Math.max(profiler.maxMs, durationMs)
      warnDerivedIdentityHotPath(profiler, durationMs)
    }
  }
}

/**
 * Create a live query using a query function.
 * @param queryFn - Query function that defines what data to fetch
 * @returns Object with reactive data, state, and status information
 * @example
 * // Prefer config object syntax
 * const { data, isLoading } = useLiveQuery({
 *   query: (q) =>
 *     q.from({ todos: todosCollection })
 *      .where(({ todos }) => eq(todos.completed, false))
 *      .select(({ todos }) => ({ id: todos.id, text: todos.text }))
 * })
 *
 *  @example
 * // Single result query
 * const { data } = useLiveQuery({
 *   query: (q) => q.from({ todos: todosCollection })
 *          .where(({ todos }) => eq(todos.id, 1))
 *          .findOne()
 * })
 *
 * @example
 * // Structured captured values are included in derived query identity
 * const { data, state } = useLiveQuery({
 *   query: (q) => q.from({ todos: todosCollection })
 *          .where(({ todos }) => gt(todos.priority, minPriority)),
 * })
 *
 * @example
 * // Return undefined or null to disable a query
 * const { data, isEnabled } = useLiveQuery({
 *   query: (q) => {
 *     if (!userId) return undefined
 *     return q.from({ todos: todosCollection })
 *             .where(({ todos }) => eq(todos.userId, userId))
 *   },
 * })
 *
 * @example
 * // Join pattern
 * const { data } = useLiveQuery({
 *   query: (q) =>
 *     q.from({ issues: issueCollection })
 *      .join({ persons: personCollection }, ({ issues, persons }) =>
 *        eq(issues.userId, persons.id)
 *      )
 *      .select(({ issues, persons }) => ({
 *        id: issues.id,
 *        title: issues.title,
 *        userName: persons.name
 *      }))
 * })
 *
 * @example
 * // Handle loading and error states
 * const { data, isLoading, isLoadingError, status } = useLiveQuery({
 *   query: (q) => q.from({ todos: todoCollection })
 * })
 *
 * if (isLoading) return <div>Loading...</div>
 * if (isLoadingError) return <div>Error: {status}</div>
 *
 * return (
 *   <ul>
 *     {data.map(todo => <li key={todo.id}>{todo.text}</li>)}
 *   </ul>
 * )
 */
// Overload 1: Accept query function that always returns QueryBuilder
export function useLiveQuery<TContext extends Context>(
  queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,
): {
  state: Map<string | number, GetResult<TContext>>
  data: InferResultType<TContext>
  collection: Collection<GetResult<TContext>, string | number, {}>
  status: CollectionStatus // Can't be disabled if always returns QueryBuilder
  isEnabled: true // Always true if always returns QueryBuilder
} & LiveQueryStatusFlags

// Overload 2: Accept query function that can return undefined/null
export function useLiveQuery<TContext extends Context>(
  queryFn: (
    q: InitialQueryBuilder,
  ) => QueryBuilder<TContext> | undefined | null,
): {
  state: Map<string | number, GetResult<TContext>> | undefined
  data: InferResultType<TContext> | undefined
  collection: Collection<GetResult<TContext>, string | number, {}> | undefined
  status: UseLiveQueryStatus
  isEnabled: boolean
} & LiveQueryStatusFlags

// Overload 3: Accept query function that can return LiveQueryCollectionConfig
export function useLiveQuery<TContext extends Context>(
  queryFn: (
    q: InitialQueryBuilder,
  ) => LiveQueryCollectionConfig<TContext> | undefined | null,
): {
  state: Map<string | number, GetResult<TContext>> | undefined
  data: InferResultType<TContext> | undefined
  collection: Collection<GetResult<TContext>, string | number, {}> | undefined
  status: UseLiveQueryStatus
  isEnabled: boolean
} & LiveQueryStatusFlags

// Overload 4: Accept query function that can return Collection
export function useLiveQuery<
  TResult extends object,
  TKey extends string | number,
  TUtils extends Record<string, any>,
>(
  queryFn: (
    q: InitialQueryBuilder,
  ) => Collection<TResult, TKey, TUtils> | undefined | null,
): {
  state: Map<TKey, TResult> | undefined
  data: Array<TResult> | undefined
  collection: Collection<TResult, TKey, TUtils> | undefined
  status: UseLiveQueryStatus
  isEnabled: boolean
} & LiveQueryStatusFlags

// Overload 5: Accept query function that can return all types
export function useLiveQuery<
  TContext extends Context,
  TResult extends object,
  TKey extends string | number,
  TUtils extends Record<string, any>,
>(
  queryFn: (
    q: InitialQueryBuilder,
  ) =>
    | QueryBuilder<TContext>
    | LiveQueryCollectionConfig<TContext>
    | Collection<TResult, TKey, TUtils>
    | undefined
    | null,
): {
  state:
    Map<string | number, GetResult<TContext>> | Map<TKey, TResult> | undefined
  data: InferResultType<TContext> | Array<TResult> | undefined
  collection:
    | Collection<GetResult<TContext>, string | number, {}>
    | Collection<TResult, TKey, TUtils>
    | undefined
  status: UseLiveQueryStatus
  isEnabled: boolean
} & LiveQueryStatusFlags

/**
 * Create a live query using configuration object
 * @param config - Configuration object with query and options
 * @returns Object with reactive data, state, and status information
 * @example
 * // Basic config object usage
 * const { data, status } = useLiveQuery({
 *   query: (q) => q.from({ todos: todosCollection }),
 *   gcTime: 60000
 * })
 *
 * @example
 * // With query builder and options
 * const queryBuilder = new Query()
 *   .from({ persons: collection })
 *   .where(({ persons }) => gt(persons.age, 30))
 *   .select(({ persons }) => ({ id: persons.id, name: persons.name }))
 *
 * const { data, isReady } = useLiveQuery({
 *   query: queryBuilder,
 * })
 *
 * @example
 * // Handle all states uniformly
 * const { data, isLoading, isReady, isLoadingError } = useLiveQuery({
 *   query: (q) => q.from({ items: itemCollection })
 * })
 *
 * if (isLoading) return <div>Loading...</div>
 * if (isLoadingError) return <div>Something went wrong</div>
 * if (!isReady) return <div>Preparing...</div>
 *
 * return <div>{data.length} items loaded</div>
 */
// Overload 6: Accept config object
export function useLiveQuery<TContext extends Context>(
  config: UseLiveQueryConfig<TContext>,
): {
  state: Map<string | number, GetResult<TContext>>
  data: InferResultType<TContext>
  collection: Collection<GetResult<TContext>, string | number, {}>
  status: CollectionStatus // Can't be disabled when query always returns a builder
  isEnabled: true // Always true when query always returns a builder
} & LiveQueryStatusFlags

// Overload 7: Accept config object with a query that can return undefined/null
export function useLiveQuery<TContext extends Context>(
  config: ConditionalUseLiveQueryConfig<TContext>,
): {
  state: Map<string | number, GetResult<TContext>> | undefined
  data: InferResultType<TContext> | undefined
  collection: Collection<GetResult<TContext>, string | number, {}> | undefined
  status: UseLiveQueryStatus
  isEnabled: boolean
} & LiveQueryStatusFlags

/**
 * Subscribe to an existing live query collection
 * @param liveQueryCollection - Pre-created live query collection to subscribe to
 * @returns Object with reactive data, state, and status information
 * @example
 * // Using pre-created live query collection
 * const myLiveQuery = createLiveQueryCollection((q) =>
 *   q.from({ todos: todosCollection }).where(({ todos }) => eq(todos.active, true))
 * )
 * const { data, collection } = useLiveQuery(myLiveQuery)
 *
 * @example
 * // Access collection methods directly
 * const { data, collection, isReady } = useLiveQuery(existingCollection)
 *
 * // Use collection for mutations
 * const handleToggle = (id) => {
 *   collection.update(id, draft => { draft.completed = !draft.completed })
 * }
 *
 * @example
 * // Handle states consistently
 * const { data, isLoading, isLoadingError } = useLiveQuery(sharedCollection)
 *
 * if (isLoading) return <div>Loading...</div>
 * if (isLoadingError) return <div>Error loading data</div>
 *
 * return <div>{data.map(item => <Item key={item.id} {...item} />)}</div>
 */
// Overload 10: Accept pre-created live query collection
export function useLiveQuery<
  TResult extends object,
  TKey extends string | number,
  TUtils extends Record<string, any>,
>(
  liveQueryCollection: Collection<TResult, TKey, TUtils> & NonSingleResult,
): {
  state: Map<TKey, TResult>
  data: Array<TResult>
  collection: Collection<TResult, TKey, TUtils>
  status: CollectionStatus // Can't be disabled for pre-created live query collections
  isEnabled: true // Always true for pre-created live query collections
} & LiveQueryStatusFlags

// Overload 10: Accept pre-created live query collection with singleResult: true
export function useLiveQuery<
  TResult extends object,
  TKey extends string | number,
  TUtils extends Record<string, any>,
>(
  liveQueryCollection: Collection<TResult, TKey, TUtils> & SingleResult,
): {
  state: Map<TKey, TResult>
  data: TResult | undefined
  collection: Collection<TResult, TKey, TUtils> & SingleResult
  status: CollectionStatus // Can't be disabled for pre-created live query collections
  isEnabled: true // Always true for pre-created live query collections
} & LiveQueryStatusFlags

// Implementation - use function overloads to infer the actual collection type
export function useLiveQuery(configOrQueryOrCollection: any) {
  return useLiveQueryImpl(configOrQueryOrCollection) as any
}

/** @internal Shared implementation for the Suspense wrapper. */
export function useLiveQueryForSuspense(configOrQueryOrCollection: unknown) {
  return useLiveQueryImpl(configOrQueryOrCollection, true)
}

// What one hook instance keeps across renders. Fields produced by resolution
// are installed with their first value, rather than initialized and overwritten
// in the same render.
type HookInstance = {
  collection: Collection<object, string | number, {}> | null
  identityDeps: Array<unknown> | null
  deferredCollections: Set<CollectionImpl<any, string | number, any, any, any>>
  sourceIds: LiveQuerySourceIdClaims
  config?: unknown
  client?: DbClient
  derivedIdentityProfiler?: DerivedIdentityProfiler
  cancelReclaimRelease?: () => void
  observer?: LiveQueryObserver<object, string | number>
  queryHash?: string
  identityError?: UnhashableQueryIRError
  subscribe?: (onStoreChange: () => void) => () => void
}

type LiveQueryResult = ReturnType<
  LiveQueryObserver<object, string | number>[`getSnapshot`]
>

function useLiveQueryImpl(configOrQueryOrCollection: unknown): LiveQueryResult
function useLiveQueryImpl(
  configOrQueryOrCollection: unknown,
  forSuspense: true,
): {
  result: LiveQueryResult
  client: DbClient | undefined
  queryHash: string | undefined
  identityError: UnhashableQueryIRError | undefined
  observer: LiveQueryObserver<object, string | number>
  /** Sync starts this render deferred, released if the render throws. */
  deferredCollections: DeferredLiveQueryCollections
}
function useLiveQueryImpl(configOrQueryOrCollection: any, forSuspense = false) {
  const contextDbClient = useOptionalDbClient()
  // Check if it's already a collection
  const inputIsCollection = isCollection(configOrQueryOrCollection)
  const dbClient: DbClient | undefined =
    (inputIsCollection || typeof configOrQueryOrCollection !== `object`
      ? undefined
      : configOrQueryOrCollection?.client) ?? contextDbClient

  const instanceRef = useRef<HookInstance | null>(null)
  const instance: HookInstance = (instanceRef.current ??= {
    collection: null,
    identityDeps: null,
    derivedIdentityProfiler: createDerivedIdentityProfiler(),
    deferredCollections: new Set(),
    sourceIds: createLiveQuerySourceIdClaims(),
  })
  // A commit releases the starts itself, so it drops the reclaim listener.
  const resumeDeferredCollections = () => {
    instance.cancelReclaimRelease?.()
    instance.cancelReclaimRelease = undefined
    resumeDeferredLiveQueryCollections(instance.deferredCollections)
  }

  // The render boundary: a render that throws anywhere after preparation
  // never commits, so it releases the sync starts it deferred here.
  try {
    const queryKey = !inputIsCollection
      ? getLiveQueryKey(configOrQueryOrCollection)
      : undefined
    let preparedQueryValue: unknown | typeof unpreparedQueryValue =
      unpreparedQueryValue
    let identityDeps: ReadonlyArray<unknown>
    let streamIdentity: ReadonlyArray<unknown>
    let identityError: UnhashableQueryIRError | undefined

    if (queryKey) {
      identityDeps = queryKey
      streamIdentity = [`queryKey`, queryKey]
    } else if (inputIsCollection) {
      identityDeps = []
      streamIdentity = [`collection`, configOrQueryOrCollection.id]
    } else {
      const preparation = prepareDerivedQuery(
        configOrQueryOrCollection,
        instance.derivedIdentityProfiler,
        getIdentityScope(instance.sourceIds, dbClient, `useLiveQuery`),
        instance.deferredCollections,
      )
      preparedQueryValue = preparation.value
      identityDeps = preparation.identityDeps
      streamIdentity = preparation.identityDeps
    }

    let queryHash: string | undefined
    try {
      queryHash = getStableValueHash(streamIdentity, `queryKey`)
    } catch (error) {
      if (!(error instanceof UnhashableQueryIRError) || queryKey !== undefined)
        throw error
      identityError = error
    }

    // Check if we need to create/recreate the collection
    let needsNewCollection =
      !instance.collection ||
      (inputIsCollection
        ? instance.config !== configOrQueryOrCollection
        : instance.client !== dbClient ||
          !deepEquals(instance.identityDeps, identityDeps))

    if (
      needsNewCollection &&
      !inputIsCollection &&
      preparedQueryValue === unpreparedQueryValue
    ) {
      preparedQueryValue = prepareLiveQueryValue(
        configOrQueryOrCollection,
        dbClient,
        instance.deferredCollections,
      )
    }

    let commitWindow: (() => void) | undefined
    if (needsNewCollection) {
      if (inputIsCollection) {
        // Warn when passing a collection directly with on-demand sync mode
        // In on-demand mode, data is only loaded when queries with predicates request it
        // Passing the collection directly doesn't provide any predicates, so no data loads
        if (
          /* @__PURE__ */ shouldWarnInDevelopment(
            `TANSTACK_DB_DISABLE_QUERY_IDENTITY_WARNINGS`,
          ) &&
          process.env.NODE_ENV !== `production` &&
          (configOrQueryOrCollection as { config?: { syncMode?: string } })
            .config?.syncMode === `on-demand`
        ) {
          console.warn(
            `[useLiveQuery] Warning: Passing a collection with syncMode "on-demand" directly to useLiveQuery ` +
              `will not load any data. In on-demand mode, data is only loaded when queries with predicates request it.\n\n` +
              `Instead, use a query builder function:\n` +
              `  const { data } = useLiveQuery({ query: (q) => q.from({ c: myCollection }).select(({ c }) => c) })\n\n` +
              `Or switch to syncMode "eager" if you want all data to sync automatically.`,
          )
        }
        // It's already a collection, ensure sync is started for React hooks
        configOrQueryOrCollection.startSyncImmediate()
        instance.collection = configOrQueryOrCollection
      } else {
        // Only a new binding consults the shared cache. Mounted bindings keep
        // their Collection and its first options until query identity changes.
        // The creation path above has already prepared keyed inputs too.
        const suspenseKey =
          forSuspense && queryHash
            ? getSourceQualifiedSuspenseKey(
                preparedQueryValue,
                queryHash,
                dbClient,
              )
            : undefined
        const suspenseCollections = suspenseKey
          ? getSuspenseCollections(dbClient)
          : undefined
        const suspenseCollection = suspenseCollections?.get(
          suspenseKey!,
        )?.collection
        if (suspenseCollection) {
          instance.collection = suspenseCollection
        } else {
          // A keyed query claims nothing, so it reports an unbound descriptor
          // here; the other forms reported it when they claimed.
          if (!dbClient && queryKey !== undefined) {
            getPreparedSources(preparedQueryValue, dbClient, `useLiveQuery`)
          }
          instance.collection = resolveLiveQueryValue(preparedQueryValue, {
            gcTime: forSuspense
              ? DEFAULT_SUSPENSE_GC_TIME_MS
              : DEFAULT_GC_TIME_MS,
            // Hydration and Suspense key the live-query Collection by identity,
            // so they never pool, even when the config passes a pool.
            pool: forSuspense || dbClient ? false : undefined,
            previousCollection:
              forSuspense || dbClient ? undefined : instance.collection,
            windowOwner: instance,
            onWindowChange: (move) => {
              commitWindow = move
            },
            // Without a DbClient, Suspense starts it through a preload below.
            startSync: !forSuspense || dbClient !== undefined,
          }) as SuspenseCollection | null
          // A render that never commits releases its deferred sync starts
          // when garbage collection reclaims the live query it built. A
          // Collection the callback returned is not this render's to reclaim.
          if (instance.collection && !isCollection(preparedQueryValue)) {
            instance.cancelReclaimRelease?.()
            instance.cancelReclaimRelease = releaseDeferredOnReclaim(
              instance.collection,
              instance.deferredCollections,
            )
          }
          if (suspenseCollections && instance.collection) {
            const collection = instance.collection
            const releaseCollection = () => {
              const entry = suspenseCollections.get(suspenseKey!)
              if (entry?.collection !== collection) return
              suspenseCollections.delete(suspenseKey!)
              entry.removeStatusListeners.forEach((remove) => remove())
            }
            // Keep a failed or crashed collection through React's immediate
            // retry so the hook can throw its actual error to an ErrorBoundary.
            // Retire it afterward so a later boundary reset starts fresh.
            const retireAfterRetry = () => setTimeout(releaseCollection, 0)
            suspenseCollections.set(suspenseKey!, {
              collection,
              removeStatusListeners: [
                collection.on(`status:cleaned-up`, releaseCollection),
                collection.on(`status:error`, retireAfterRetry),
                collection.on(`status:crashed`, retireAfterRetry),
              ],
            })
          }
        }
        // An abandoned render must leave invalidation pending. Its retry or
        // urgent replacement still prepares and commits the requested move.
        if (!commitWindow) instance.identityDeps = [...identityDeps]
      }
      instance.config = configOrQueryOrCollection
      instance.client = dbClient
      instance.queryHash = queryHash
      instance.identityError = identityError
    }

    if (commitWindow) needsNewCollection = false
    useWindowCommitEffect(() => {
      if (commitWindow) {
        instance.identityDeps = [...identityDeps]
        commitWindow()
      }
    })

    if (needsNewCollection) {
      // Recreate the observer when the underlying collection changes. The observer
      // is not disposed explicitly here or on unmount: `useSyncExternalStore`
      // unsubscribes it when the subscribe changes or the component unmounts, which
      // detaches the collection subscription; the observer is then GC'd. (An unmount
      // effect that disposed it would misfire under StrictMode/offscreen effect
      // replay, leaving a disposed observer in the ref.)
      // Defer the initial notify: useSyncExternalStore must not be notified
      // synchronously during subscribe.
      // Wholesale mode: React re-reads getSnapshot() on notify, keeps the
      // hook's pre-observer loading policy, and — because wholesale delivers
      // nothing synchronously during subscribe — never notifies
      // useSyncExternalStore inside its own subscribe call.
      const observer = (instance.observer = createLiveQueryObserver(
        instance.collection,
        {
          mode: `wholesale`,
          client: dbClient,
          queryHash: instance.queryHash,
          onPreload: resumeDeferredCollections,
          suspendUntilReady: forSuspense,
        },
      ))
      // The subscribe function stays bound to the observer that created it.
      instance.subscribe = (onStoreChange: () => void) => {
        const unsubscribe = observer.subscribe(onStoreChange)
        resumeDeferredCollections()
        return unsubscribe
      }
    }
    const observer = instance.observer!
    // A Suspense render asks for its data. Preloading starts the sync run, so a
    // source that already holds the rows makes it ready before the snapshot is
    // read instead of suspending once. useLiveSuspenseQuery surfaces failures.
    // A DbClient defers source starts past render and owns its streamed query.
    if (
      forSuspense &&
      !dbClient &&
      (instance.collection?.status === `idle` ||
        instance.collection?.status === `cleaned-up`)
    ) {
      observer.preload().catch(() => {})
    }

    const returned = useSyncExternalStore(
      instance.subscribe!,
      () => observer.getSnapshot(),
      () => observer.getServerSnapshot(forSuspense),
    )
    // Pass render-local metadata directly to the Suspense wrapper. The public
    // hook returns the observer's stable snapshot without a wrapper allocation.
    if (forSuspense) {
      return {
        result: returned,
        client: dbClient,
        queryHash: instance.queryHash,
        identityError: instance.identityError,
        observer,
        deferredCollections: instance.deferredCollections,
      }
    }
    return returned
  } catch (error) {
    return rejectLiveQueryInput(instance.deferredCollections, error)
  }
}
