/* eslint-disable @typescript-eslint/no-non-null-assertion */
import type {
  AnyMutationProcedure,
  AnyProcedure,
  AnyQueryProcedure,
  AnyRouter,
  AnySubscriptionProcedure,
  IntersectionError,
  ProcedureArgs,
  ProcedureRouterRecord,
  ProcedureType,
} from '@trpc/server';
import type { Unsubscribable } from '@trpc/server/observable';
import {
  createFlatProxy,
  createRecursiveProxy,
  inferTransformedProcedureOutput,
  inferTransformedSubscriptionOutput,
} from '@trpc/server/shared';
import { TRPCClientError } from './TRPCClientError';
import { TRPCClient } from './createTRPCClient';
import { CreateTRPCClientOptions } from './createTRPCUntypedClient';
import {
  TRPCSubscriptionObserver,
  TRPCUntypedClient,
  UntypedClientProperties,
} from './internals/TRPCUntypedClient';

/** @public */
export type inferRouterProxyClient<TRouter extends AnyRouter> =
  DecoratedProcedureRecord<TRouter['_def']['record'], TRouter>;

type Resolver<TProcedure extends AnyProcedure> = (
  ...args: ProcedureArgs<TProcedure['_def']>
) => Promise<inferTransformedProcedureOutput<TProcedure>>;

type SubscriptionResolver<
  TProcedure extends AnyProcedure,
  TRouter extends AnyRouter,
> = (
  ...args: [
    input: ProcedureArgs<TProcedure['_def']>[0],
    opts: ProcedureArgs<TProcedure['_def']>[1] &
      Partial<
        TRPCSubscriptionObserver<
          inferTransformedSubscriptionOutput<TProcedure>,
          TRPCClientError<TRouter>
        >
      >,
  ]
) => Unsubscribable;

type DecorateProcedure<
  TProcedure extends AnyProcedure,
  TRouter extends AnyRouter,
> = TProcedure extends AnyQueryProcedure
  ? {
      query: Resolver<TProcedure>;
    }
  : TProcedure extends AnyMutationProcedure
  ? {
      mutate: Resolver<TProcedure>;
    }
  : TProcedure extends AnySubscriptionProcedure
  ? {
      subscribe: SubscriptionResolver<TProcedure, TRouter>;
    }
  : never;

/**
 * @internal
 */
type DecoratedProcedureRecord<
  TProcedures extends ProcedureRouterRecord,
  TRouter extends AnyRouter,
> = {
  [TKey in keyof TProcedures]: TProcedures[TKey] extends AnyRouter
    ? DecoratedProcedureRecord<
        TProcedures[TKey]['_def']['record'],
        TProcedures[TKey]
      >
    : TProcedures[TKey] extends AnyProcedure
    ? DecorateProcedure<TProcedures[TKey], TRouter>
    : never;
};

const clientCallTypeMap: Record<
  keyof DecorateProcedure<any, any>,
  ProcedureType
> = {
  query: 'query',
  mutate: 'mutation',
  subscribe: 'subscription',
};

export type CreateTRPCProxyClient<TRouter extends AnyRouter> =
  DecoratedProcedureRecord<
    TRouter['_def']['record'],
    TRouter
  > extends infer TProcedureRecord
    ? UntypedClientProperties & keyof TProcedureRecord extends never
      ? TProcedureRecord
      : IntersectionError<UntypedClientProperties & keyof TProcedureRecord>
    : never;

/**
 * @deprecated use `createTRPCProxyClient` instead
 * @internal
 */
export function createTRPCClientProxy<TRouter extends AnyRouter>(
  client: TRPCClient<TRouter>,
) {
  return createFlatProxy<CreateTRPCProxyClient<TRouter>>((key) => {
    if (client.hasOwnProperty(key)) {
      return (client as any)[key as any];
    }
    return createRecursiveProxy(({ path, args }) => {
      const pathCopy = [key, ...path];
      const clientCallType = pathCopy.pop()! as keyof DecorateProcedure<
        any,
        any
      >;

      const procedureType = clientCallTypeMap[clientCallType];

      const fullPath = pathCopy.join('.');

      return (client as any)[procedureType](fullPath, ...args);
    });
  });
}

export function createTRPCProxyClient<TRouter extends AnyRouter>(
  opts: CreateTRPCClientOptions<TRouter>,
) {
  const client = new TRPCUntypedClient(opts);
  const proxy = createTRPCClientProxy(client as TRPCClient<TRouter>);
  return proxy;
}
