import {
  UseQueryOptions,
  UseMutationOptions
} from "@tanstack/react-query";

// ---------------------------------------------------------------------------
// React Query shared types
// ---------------------------------------------------------------------------

export type QueryConfig<T> = Omit<
  UseQueryOptions<T, Error, T, readonly unknown[]>,
  "queryKey" | "queryFn"
>;

/**
 * Query keys a mutation should invalidate on success. Receives the mutation
 * input and response so keys can be derived from them. Returns a list of query
 * keys — each is matched as a *prefix* by React Query, so returning a service
 * root (e.g. `["cargoes"]`) invalidates every query nested under it.
 *
 * The keys are surfaced through `mutationOptions().meta.invalidates`; the
 * app-wide `MutationCache` (see `lib/queryClient.ts`) reads them and invalidates
 * automatically, so components never wire `onSuccess` invalidation by hand.
 */
export type InvalidatesFn<TInput, TResponse> = (
  input: TInput,
  data: TResponse,
) => ReadonlyArray<readonly unknown[]>;

/** Shape stored in `mutation.meta.invalidates` and consumed by the MutationCache. */
export type InvalidatesMeta = (
  variables: unknown,
  data: unknown,
) => ReadonlyArray<readonly unknown[]>;

// ---------------------------------------------------------------------------
// Endpoint interfaces
// ---------------------------------------------------------------------------

export interface EndpointWithInput<TInput, TResponse> {
  call(input: TInput): Promise<TResponse>;
  queryKey(input: TInput): readonly unknown[];
  queryOptions(
    config: { input: TInput } & QueryConfig<TResponse>,
  ): UseQueryOptions<TResponse, Error, TResponse, readonly unknown[]>;
}

export interface EndpointWithoutInput<TResponse> {
  call(): Promise<TResponse>;
  queryKey(): readonly unknown[];
  queryOptions(
    config?: QueryConfig<TResponse>,
  ): UseQueryOptions<TResponse, Error, TResponse, readonly unknown[]>;
}

export type Endpoint<TInput, TResponse> = TInput extends void
  ? EndpointWithoutInput<TResponse>
  : EndpointWithInput<TInput, TResponse>;

// ---------------------------------------------------------------------------
// Endpoint builder
// ---------------------------------------------------------------------------

export function endpoint<TInput, TResponse>(
  service: string,
  action: string,
  execute: (input: TInput) => Promise<TResponse>,
  queryKeyBuilder?: (input: TInput) => readonly unknown[],
  invalidates?: InvalidatesFn<TInput, TResponse>,
) {
  const buildKey = (input?: TInput): readonly unknown[] => {
    if (queryKeyBuilder && input !== undefined) {
      return queryKeyBuilder(input as TInput);
    }
    return input === undefined
      ? [service, action]
      : [service, action, input];
  };

  const call = (input: TInput) => execute(input);

  const queryKey = (input?: TInput) => buildKey(input);

  const queryOptions = (
    config?: { input?: TInput } & QueryConfig<TResponse>,
  ): UseQueryOptions<
    TResponse,
    Error,
    TResponse,
    readonly unknown[]
  > => {
    const { input, ...rest } = config ?? {};

    return {
      ...rest,
      queryKey: buildKey(input),
      queryFn: () => execute(input as TInput),
    };
  };

  const mutationOptions = (
    config?: Omit<UseMutationOptions<TResponse, Error, TInput>, "mutationFn">,
  ): UseMutationOptions<TResponse, Error, TInput> => {
    const meta = invalidates
      ? {
          ...config?.meta,
          invalidates: ((variables, data) =>
            invalidates(
              variables as TInput,
              data as TResponse,
            )) satisfies InvalidatesMeta,
        }
      : config?.meta;

    return {
      ...config,
      meta,
      mutationFn: (variables: TInput): Promise<TResponse> => execute(variables),
    };
  };

  return {
    call,
    queryKey,
    queryOptions,
    mutationOptions
  };
}

// ---------------------------------------------------------------------------
// Helper utilities
// ---------------------------------------------------------------------------

export function unwrap<T>(response: { data: T } | T): T {
  if (
    response &&
    typeof response === "object" &&
    "data" in (response as object)
  ) {
    return (response as { data: T }).data;
  }

  return response as T;
}
