import { QueryObserver } from '@tanstack/query-core'
import {
  Injector,
  assertInInjectionContext,
  inject,
  runInInjectionContext,
} from '@angular/core'
import { createBaseQuery } from './create-base-query'
import type { DefaultError, QueryKey } from '@tanstack/query-core'
import type {
  CreateQueryOptions,
  CreateQueryResult,
  DefinedCreateQueryResult,
} from './types'
import type {
  DefinedInitialDataOptions,
  UndefinedInitialDataOptions,
} from './query-options'

/**
 * Options for `injectQuery`, passed after the function that returns the query options.
 */
export interface InjectQueryOptions {
  /**
   * The `Injector` in which to create the query.
   *
   * If this is not provided, the current injection context will be used instead (via `inject`).
   */
  injector?: Injector
}

/**
 * This overload is selected when `initialData` is set on the options returned by `injectQueryFn`, so the
 * resulting `data` signal is never `undefined` (unless a `select` changes `TData` to include `undefined`).
 * @param injectQueryFn - A function returning the {@link DefinedInitialDataOptions} to use — everything you
 * can pass to `injectQuery`, with `initialData` set. Similar to `computed` from Angular, this function runs
 * in the reactive context, so signals read inside it (in `queryKey`, `enabled`, etc.) drive the query.
 * @param options - Additional configuration
 * @returns The query result, typed so that `data` is never `undefined` (unless a `select` changes `TData` to
 * include `undefined`).
 * @see https://tanstack.com/query/latest/docs/framework/angular/guides/queries
 * @see {@link queryOptions} to share these options between `injectQuery` and imperative APIs like
 * `queryClient.fetchQuery`.
 * @example
 * ```angular-ts
 * @Component({
 *   selector: 'posts',
 *   template: `
 *     <!-- `postsQuery.data()` is `Post[]`, never `undefined`, thanks to `initialData` — even if a
 *     refetch fails, so the list stays visible alongside the error. -->
 *     @if (postsQuery.isError()) {
 *       <span>Error: {{ postsQuery.error()?.message }}</span>
 *     }
 *     <ul>
 *       @for (post of postsQuery.data(); track post.id) {
 *         <li>{{ post.title }}</li>
 *       }
 *     </ul>
 *   `,
 * })
 * export class Posts {
 *   readonly postsQuery = injectQuery(() => ({
 *     queryKey: ['posts'],
 *     queryFn: fetchPosts,
 *     initialData: [],
 *   }))
 * }
 * ```
 */
export function injectQuery<
  TQueryFnData = unknown,
  TError = DefaultError,
  TData = TQueryFnData,
  TQueryKey extends QueryKey = QueryKey,
>(
  injectQueryFn: () => DefinedInitialDataOptions<
    TQueryFnData,
    TError,
    TData,
    TQueryKey
  >,
  options?: InjectQueryOptions,
): DefinedCreateQueryResult<TData, TError>

/**
 * Injects a query: a declarative dependency on an asynchronous source of data that is tied to a unique key.
 * @param injectQueryFn - A function returning the {@link UndefinedInitialDataOptions} to use — everything
 * you can pass to `injectQuery`. Similar to `computed` from Angular, this function runs in the reactive
 * context, so signals read inside it (in `queryKey`, `enabled`, etc.) drive the query.
 * @param options - Additional configuration
 * @returns The query result. `status()` is `'pending'` if there is no cached data to display, `'error'` if
 * the last fetch attempt failed, or `'success'` if the query has data to display. `isPending`/`isSuccess`/
 * `isError` are type-guard methods for convenience.
 * @see https://tanstack.com/query/latest/docs/framework/angular/guides/queries
 * @see {@link queryOptions} to share these options between `injectQuery` and imperative APIs like
 * `queryClient.fetchQuery`.
 * @example
 * ```angular-ts
 * @Component({
 *   selector: 'posts',
 *   template: `
 *     @if (postsQuery.isPending()) {
 *       Loading...
 *     } @else if (postsQuery.isError()) {
 *       <span>Error: {{ postsQuery.error()?.message }}</span>
 *     } @else {
 *       <ul>
 *         @for (post of postsQuery.data(); track post.id) {
 *           <li>{{ post.title }}</li>
 *         }
 *       </ul>
 *     }
 *   `,
 * })
 * export class Posts {
 *   readonly postsQuery = injectQuery(() => ({
 *     queryKey: ['posts'],
 *     queryFn: fetchPosts,
 *   }))
 * }
 * ```
 * @example
 * Similar to `computed` from Angular, the function passed to `injectQuery` runs in the reactive context. In
 * the example below, the query is automatically enabled and executed when the filter signal changes to a
 * truthy value. When the filter signal changes back to a falsy value, the query is disabled.
 * ```angular-ts
 * @Component({
 *   selector: 'posts',
 *   template: `
 *     <input [ngModel]="filter()" (ngModelChange)="filter.set($event)" />
 *     @if (postsQuery.isPending()) {
 *       Loading...
 *     } @else if (postsQuery.isError()) {
 *       <span>Error: {{ postsQuery.error()?.message }}</span>
 *     } @else {
 *       <ul>
 *         @for (post of postsQuery.data(); track post.id) {
 *           <li>{{ post.title }}</li>
 *         }
 *       </ul>
 *     }
 *   `,
 * })
 * export class Posts {
 *   readonly filter = signal('')
 *
 *   readonly postsQuery = injectQuery(() => ({
 *     queryKey: ['posts', this.filter()],
 *     queryFn: () => fetchPosts(this.filter()),
 *     // Signals can be combined with expressions
 *     enabled: !!this.filter(),
 *   }))
 * }
 * ```
 */
export function injectQuery<
  TQueryFnData = unknown,
  TError = DefaultError,
  TData = TQueryFnData,
  TQueryKey extends QueryKey = QueryKey,
>(
  injectQueryFn: () => UndefinedInitialDataOptions<
    TQueryFnData,
    TError,
    TData,
    TQueryKey
  >,
  options?: InjectQueryOptions,
): CreateQueryResult<TData, TError>

/**
 * This overload accepts the general {@link CreateQueryOptions} shape rather than the `initialData`-aware
 * overloads above, so whether `data` is defined can't be inferred from the call site — useful when wrapping
 * `injectQuery` in your own helper function that forwards caller-provided options.
 * @param injectQueryFn - A function that returns query options. Similar to `computed` from Angular, this
 * function runs in the reactive context, so signals read inside it (in `queryKey`, `enabled`, etc.) drive
 * the query.
 * @param options - Additional configuration
 * @returns The query result.
 * @see https://tanstack.com/query/latest/docs/framework/angular/guides/queries
 */
export function injectQuery<
  TQueryFnData = unknown,
  TError = DefaultError,
  TData = TQueryFnData,
  TQueryKey extends QueryKey = QueryKey,
>(
  injectQueryFn: () => CreateQueryOptions<
    TQueryFnData,
    TError,
    TData,
    TQueryKey
  >,
  options?: InjectQueryOptions,
): CreateQueryResult<TData, TError>

export function injectQuery(
  injectQueryFn: () => CreateQueryOptions,
  options?: InjectQueryOptions,
) {
  !options?.injector && assertInInjectionContext(injectQuery)
  return runInInjectionContext(options?.injector ?? inject(Injector), () =>
    createBaseQuery(injectQueryFn, QueryObserver),
  ) as unknown as CreateQueryResult
}