import type { inferObservableValue, Observable } from '../observable';
import { getTRPCErrorFromUnknown, TRPCError } from './error/TRPCError';
import type {
  AnyMiddlewareFunction,
  MiddlewareBuilder,
  MiddlewareFunction,
  MiddlewareResult,
} from './middleware';
import {
  createInputMiddleware,
  createOutputMiddleware,
  middlewareMarker,
} from './middleware';
import type { inferParser, Parser } from './parser';
import { getParseFn } from './parser';
import type {
  AnyMutationProcedure,
  AnyProcedure,
  AnyQueryProcedure,
  LegacyObservableSubscriptionProcedure,
  MutationProcedure,
  ProcedureType,
  QueryProcedure,
  SubscriptionProcedure,
} from './procedure';
import type { inferTrackedOutput } from './stream/tracked';
import type {
  GetRawInputFn,
  MaybePromise,
  Overwrite,
  Simplify,
  TypeError,
} from './types';
import type { UnsetMarker } from './utils';
import { mergeWithoutOverrides } from './utils';

type IntersectIfDefined<TType, TWith> = TType extends UnsetMarker
  ? TWith
  : TWith extends UnsetMarker
    ? TType
    : Simplify<TType & TWith>;

type DefaultValue<TValue, TFallback> = TValue extends UnsetMarker
  ? TFallback
  : TValue;

type inferAsyncIterable<TOutput> =
  TOutput extends AsyncIterable<infer $Yield, infer $Return, infer $Next>
    ? {
        yield: $Yield;
        return: $Return;
        next: $Next;
      }
    : never;
type inferSubscriptionOutput<TOutput> =
  TOutput extends AsyncIterable<any>
    ? AsyncIterable<
        inferTrackedOutput<inferAsyncIterable<TOutput>['yield']>,
        inferAsyncIterable<TOutput>['return'],
        inferAsyncIterable<TOutput>['next']
      >
    : TypeError<'Subscription output could not be inferred'>;

export type CallerOverride<TContext> = (opts: {
  args: unknown[];
  invoke: (opts: ProcedureCallOptions<TContext>) => Promise<unknown>;
  _def: AnyProcedure['_def'];
}) => Promise<unknown>;
type ProcedureBuilderDef<TMeta> = {
  procedure: true;
  inputs: Parser[];
  output?: Parser;
  meta?: TMeta;
  resolver?: ProcedureBuilderResolver;
  middlewares: AnyMiddlewareFunction[];
  /**
   * @deprecated use `type` instead
   */
  mutation?: boolean;
  /**
   * @deprecated use `type` instead
   */
  query?: boolean;
  /**
   * @deprecated use `type` instead
   */
  subscription?: boolean;
  type?: ProcedureType;
  caller?: CallerOverride<unknown>;
};

type AnyProcedureBuilderDef = ProcedureBuilderDef<any>;

/**
 * Procedure resolver options (what the `.query()`, `.mutation()`, and `.subscription()` functions receive)
 * @internal
 */
export interface ProcedureResolverOptions<
  TContext,
  _TMeta,
  TContextOverridesIn,
  TInputOut,
> {
  ctx: Simplify<Overwrite<TContext, TContextOverridesIn>>;
  input: TInputOut extends UnsetMarker ? undefined : TInputOut;
  /**
   * The AbortSignal of the request
   */
  signal: AbortSignal | undefined;
  /**
   * The path of the procedure
   */
  path: string;
}

/**
 * A procedure resolver
 */
type ProcedureResolver<
  TContext,
  TMeta,
  TContextOverrides,
  TInputOut,
  TOutputParserIn,
  $Output,
> = (
  opts: ProcedureResolverOptions<TContext, TMeta, TContextOverrides, TInputOut>,
) => MaybePromise<
  // If an output parser is defined, we need to return what the parser expects, otherwise we return the inferred type
  DefaultValue<TOutputParserIn, $Output>
>;

type AnyResolver = ProcedureResolver<any, any, any, any, any, any>;
export type AnyProcedureBuilder = ProcedureBuilder<
  any,
  any,
  any,
  any,
  any,
  any,
  any,
  any
>;

/**
 * Infer the context type from a procedure builder
 * Useful to create common helper functions for different procedures
 */
export type inferProcedureBuilderResolverOptions<
  TProcedureBuilder extends AnyProcedureBuilder,
> =
  TProcedureBuilder extends ProcedureBuilder<
    infer TContext,
    infer TMeta,
    infer TContextOverrides,
    infer _TInputIn,
    infer TInputOut,
    infer _TOutputIn,
    infer _TOutputOut,
    infer _TCaller
  >
    ? ProcedureResolverOptions<
        TContext,
        TMeta,
        TContextOverrides,
        TInputOut extends UnsetMarker
          ? // if input is not set, we don't want to infer it as `undefined` since a procedure further down the chain might have set an input
            unknown
          : TInputOut extends object
            ? Simplify<
                TInputOut & {
                  /**
                   * Extra input params might have been added by a `.input()` further down the chain
                   */
                  [keyAddedByInputCallFurtherDown: string]: unknown;
                }
              >
            : TInputOut
      >
    : never;

export interface ProcedureBuilder<
  TContext,
  TMeta,
  TContextOverrides,
  TInputIn,
  TInputOut,
  TOutputIn,
  TOutputOut,
  TCaller extends boolean,
> {
  /**
   * Add an input parser to the procedure.
   * @see https://trpc.io/docs/v11/server/validators
   */
  input<$Parser extends Parser>(
    schema: TInputOut extends UnsetMarker
      ? $Parser
      : inferParser<$Parser>['out'] extends Record<string, unknown> | undefined
        ? TInputOut extends Record<string, unknown> | undefined
          ? undefined extends inferParser<$Parser>['out'] // if current is optional the previous must be too
            ? undefined extends TInputOut
              ? $Parser
              : TypeError<'Cannot chain an optional parser to a required parser'>
            : $Parser
          : TypeError<'All input parsers did not resolve to an object'>
        : TypeError<'All input parsers did not resolve to an object'>,
  ): ProcedureBuilder<
    TContext,
    TMeta,
    TContextOverrides,
    IntersectIfDefined<TInputIn, inferParser<$Parser>['in']>,
    IntersectIfDefined<TInputOut, inferParser<$Parser>['out']>,
    TOutputIn,
    TOutputOut,
    TCaller
  >;
  /**
   * Add an output parser to the procedure.
   * @see https://trpc.io/docs/v11/server/validators
   */
  output<$Parser extends Parser>(
    schema: $Parser,
  ): ProcedureBuilder<
    TContext,
    TMeta,
    TContextOverrides,
    TInputIn,
    TInputOut,
    IntersectIfDefined<TOutputIn, inferParser<$Parser>['in']>,
    IntersectIfDefined<TOutputOut, inferParser<$Parser>['out']>,
    TCaller
  >;
  /**
   * Add a meta data to the procedure.
   * @see https://trpc.io/docs/v11/server/metadata
   */
  meta(
    meta: TMeta,
  ): ProcedureBuilder<
    TContext,
    TMeta,
    TContextOverrides,
    TInputIn,
    TInputOut,
    TOutputIn,
    TOutputOut,
    TCaller
  >;
  /**
   * Add a middleware to the procedure.
   * @see https://trpc.io/docs/v11/server/middlewares
   */
  use<$ContextOverridesOut>(
    fn:
      | MiddlewareBuilder<
          Overwrite<TContext, TContextOverrides>,
          TMeta,
          $ContextOverridesOut,
          TInputOut
        >
      | MiddlewareFunction<
          TContext,
          TMeta,
          TContextOverrides,
          $ContextOverridesOut,
          TInputOut
        >,
  ): ProcedureBuilder<
    TContext,
    TMeta,
    Overwrite<TContextOverrides, $ContextOverridesOut>,
    TInputIn,
    TInputOut,
    TOutputIn,
    TOutputOut,
    TCaller
  >;

  /**
   * @deprecated use {@link concat} instead
   */
  unstable_concat<
    $Context,
    $Meta,
    $ContextOverrides,
    $InputIn,
    $InputOut,
    $OutputIn,
    $OutputOut,
  >(
    builder: Overwrite<TContext, TContextOverrides> extends $Context
      ? TMeta extends $Meta
        ? ProcedureBuilder<
            $Context,
            $Meta,
            $ContextOverrides,
            $InputIn,
            $InputOut,
            $OutputIn,
            $OutputOut,
            TCaller
          >
        : TypeError<'Meta mismatch'>
      : TypeError<'Context mismatch'>,
  ): ProcedureBuilder<
    TContext,
    TMeta,
    Overwrite<TContextOverrides, $ContextOverrides>,
    IntersectIfDefined<TInputIn, $InputIn>,
    IntersectIfDefined<TInputOut, $InputOut>,
    IntersectIfDefined<TOutputIn, $OutputIn>,
    IntersectIfDefined<TOutputOut, $OutputOut>,
    TCaller
  >;

  /**
   * Combine two procedure builders
   */
  concat<
    $Context,
    $Meta,
    $ContextOverrides,
    $InputIn,
    $InputOut,
    $OutputIn,
    $OutputOut,
  >(
    builder: Overwrite<TContext, TContextOverrides> extends $Context
      ? TMeta extends $Meta
        ? ProcedureBuilder<
            $Context,
            $Meta,
            $ContextOverrides,
            $InputIn,
            $InputOut,
            $OutputIn,
            $OutputOut,
            TCaller
          >
        : TypeError<'Meta mismatch'>
      : TypeError<'Context mismatch'>,
  ): ProcedureBuilder<
    TContext,
    TMeta,
    Overwrite<TContextOverrides, $ContextOverrides>,
    IntersectIfDefined<TInputIn, $InputIn>,
    IntersectIfDefined<TInputOut, $InputOut>,
    IntersectIfDefined<TOutputIn, $OutputIn>,
    IntersectIfDefined<TOutputOut, $OutputOut>,
    TCaller
  >;
  /**
   * Query procedure
   * @see https://trpc.io/docs/v11/concepts#vocabulary
   */
  query<$Output>(
    resolver: ProcedureResolver<
      TContext,
      TMeta,
      TContextOverrides,
      TInputOut,
      TOutputIn,
      $Output
    >,
  ): TCaller extends true
    ? (
        input: DefaultValue<TInputIn, void>,
      ) => Promise<DefaultValue<TOutputOut, $Output>>
    : QueryProcedure<{
        input: DefaultValue<TInputIn, void>;
        output: DefaultValue<TOutputOut, $Output>;
        meta: TMeta;
      }>;

  /**
   * Mutation procedure
   * @see https://trpc.io/docs/v11/concepts#vocabulary
   */
  mutation<$Output>(
    resolver: ProcedureResolver<
      TContext,
      TMeta,
      TContextOverrides,
      TInputOut,
      TOutputIn,
      $Output
    >,
  ): TCaller extends true
    ? (
        input: DefaultValue<TInputIn, void>,
      ) => Promise<DefaultValue<TOutputOut, $Output>>
    : MutationProcedure<{
        input: DefaultValue<TInputIn, void>;
        output: DefaultValue<TOutputOut, $Output>;
        meta: TMeta;
      }>;

  /**
   * Subscription procedure
   * @see https://trpc.io/docs/v11/server/subscriptions
   */
  subscription<$Output extends AsyncIterable<any, void, any>>(
    resolver: ProcedureResolver<
      TContext,
      TMeta,
      TContextOverrides,
      TInputOut,
      TOutputIn,
      $Output
    >,
  ): TCaller extends true
    ? TypeError<'Not implemented'>
    : SubscriptionProcedure<{
        input: DefaultValue<TInputIn, void>;
        output: inferSubscriptionOutput<DefaultValue<TOutputOut, $Output>>;
        meta: TMeta;
      }>;
  /**
   * @deprecated Using subscriptions with an observable is deprecated. Use an async generator instead.
   * This feature will be removed in v12 of tRPC.
   * @see https://trpc.io/docs/v11/server/subscriptions
   */
  subscription<$Output extends Observable<any, any>>(
    resolver: ProcedureResolver<
      TContext,
      TMeta,
      TContextOverrides,
      TInputOut,
      TOutputIn,
      $Output
    >,
  ): TCaller extends true
    ? TypeError<'Not implemented'>
    : LegacyObservableSubscriptionProcedure<{
        input: DefaultValue<TInputIn, void>;
        output: inferObservableValue<DefaultValue<TOutputOut, $Output>>;
        meta: TMeta;
      }>;
  /**
   * Overrides the way a procedure is invoked
   * Do not use this unless you know what you're doing - this is an experimental API
   */
  experimental_caller(
    caller: CallerOverride<TContext>,
  ): ProcedureBuilder<
    TContext,
    TMeta,
    TContextOverrides,
    TInputIn,
    TInputOut,
    TOutputIn,
    TOutputOut,
    true
  >;
  /**
   * @internal
   */
  _def: ProcedureBuilderDef<TMeta>;
}

type ProcedureBuilderResolver = (
  opts: ProcedureResolverOptions<any, any, any, any>,
) => Promise<unknown>;

function createNewBuilder(
  def1: AnyProcedureBuilderDef,
  def2: Partial<AnyProcedureBuilderDef>,
): AnyProcedureBuilder {
  const { middlewares = [], inputs, meta, ...rest } = def2;

  // TODO: maybe have a fn here to warn about calls
  return createBuilder({
    ...mergeWithoutOverrides(def1, rest),
    inputs: [...def1.inputs, ...(inputs ?? [])],
    middlewares: [...def1.middlewares, ...middlewares],
    meta: def1.meta && meta ? { ...def1.meta, ...meta } : (meta ?? def1.meta),
  });
}

export function createBuilder<TContext, TMeta>(
  initDef: Partial<AnyProcedureBuilderDef> = {},
): ProcedureBuilder<
  TContext,
  TMeta,
  object,
  UnsetMarker,
  UnsetMarker,
  UnsetMarker,
  UnsetMarker,
  false
> {
  const _def: AnyProcedureBuilderDef = {
    procedure: true,
    inputs: [],
    middlewares: [],
    ...initDef,
  };

  const builder: AnyProcedureBuilder = {
    _def,
    input(input) {
      const parser = getParseFn(input as Parser);
      return createNewBuilder(_def, {
        inputs: [input as Parser],
        middlewares: [createInputMiddleware(parser)],
      });
    },
    output(output: Parser) {
      const parser = getParseFn(output);
      return createNewBuilder(_def, {
        output,
        middlewares: [createOutputMiddleware(parser)],
      });
    },
    meta(meta) {
      return createNewBuilder(_def, {
        meta,
      });
    },
    use(middlewareBuilderOrFn) {
      // Distinguish between a middleware builder and a middleware function
      const middlewares =
        '_middlewares' in middlewareBuilderOrFn
          ? middlewareBuilderOrFn._middlewares
          : [middlewareBuilderOrFn];

      return createNewBuilder(_def, {
        middlewares: middlewares,
      });
    },
    unstable_concat(builder) {
      return createNewBuilder(_def, (builder as AnyProcedureBuilder)._def);
    },
    concat(builder) {
      return createNewBuilder(_def, (builder as AnyProcedureBuilder)._def);
    },
    query(resolver) {
      return createResolver(
        { ..._def, type: 'query' },
        resolver,
      ) as AnyQueryProcedure;
    },
    mutation(resolver) {
      return createResolver(
        { ..._def, type: 'mutation' },
        resolver,
      ) as AnyMutationProcedure;
    },
    subscription(resolver: ProcedureResolver<any, any, any, any, any, any>) {
      return createResolver({ ..._def, type: 'subscription' }, resolver) as any;
    },
    experimental_caller(caller) {
      return createNewBuilder(_def, {
        caller,
      }) as any;
    },
  };

  return builder;
}

function createResolver(
  _defIn: AnyProcedureBuilderDef & { type: ProcedureType },
  resolver: AnyResolver,
) {
  const finalBuilder = createNewBuilder(_defIn, {
    resolver,
    middlewares: [
      async function resolveMiddleware(opts) {
        const data = await resolver(opts);
        return {
          marker: middlewareMarker,
          ok: true,
          data,
          ctx: opts.ctx,
        } as const;
      },
    ],
  });
  const _def: AnyProcedure['_def'] = {
    ...finalBuilder._def,
    type: _defIn.type,
    experimental_caller: Boolean(finalBuilder._def.caller),
    meta: finalBuilder._def.meta,
    $types: null as any,
  };

  const invoke = createProcedureCaller(finalBuilder._def);
  const callerOverride = finalBuilder._def.caller;
  if (!callerOverride) {
    return invoke;
  }
  const callerWrapper = async (...args: unknown[]) => {
    return await callerOverride({
      args,
      invoke,
      _def: _def,
    });
  };

  callerWrapper._def = _def;

  return callerWrapper;
}

/**
 * @internal
 */
export interface ProcedureCallOptions<TContext> {
  ctx: TContext;
  getRawInput: GetRawInputFn;
  input?: unknown;
  path: string;
  type: ProcedureType;
  signal: AbortSignal | undefined;
}

const codeblock = `
This is a client-only function.
If you want to call this function on the server, see https://trpc.io/docs/v11/server/server-side-calls
`.trim();

// run the middlewares recursively with the resolver as the last one
async function callRecursive(
  index: number,
  _def: AnyProcedureBuilderDef,
  opts: ProcedureCallOptions<any>,
): Promise<MiddlewareResult<any>> {
  try {
    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
    const middleware = _def.middlewares[index]!;
    const result = await middleware({
      ...opts,
      meta: _def.meta,
      input: opts.input,
      next(_nextOpts?: any) {
        const nextOpts = _nextOpts as
          | {
              ctx?: Record<string, unknown>;
              input?: unknown;
              getRawInput?: GetRawInputFn;
            }
          | undefined;

        return callRecursive(index + 1, _def, {
          ...opts,
          ctx: nextOpts?.ctx ? { ...opts.ctx, ...nextOpts.ctx } : opts.ctx,
          input: nextOpts && 'input' in nextOpts ? nextOpts.input : opts.input,
          getRawInput: nextOpts?.getRawInput ?? opts.getRawInput,
        });
      },
    });

    return result;
  } catch (cause) {
    return {
      ok: false,
      error: getTRPCErrorFromUnknown(cause),
      marker: middlewareMarker,
    };
  }
}

function createProcedureCaller(_def: AnyProcedureBuilderDef): AnyProcedure {
  async function procedure(opts: ProcedureCallOptions<unknown>) {
    // is direct server-side call
    if (!opts || !('getRawInput' in opts)) {
      throw new Error(codeblock);
    }

    // there's always at least one "next" since we wrap this.resolver in a middleware
    const result = await callRecursive(0, _def, opts);

    if (!result) {
      throw new TRPCError({
        code: 'INTERNAL_SERVER_ERROR',
        message:
          'No result from middlewares - did you forget to `return next()`?',
      });
    }
    if (!result.ok) {
      // re-throw original error
      throw result.error;
    }
    return result.data;
  }

  procedure._def = _def;
  procedure.procedure = true;
  procedure.meta = _def.meta;

  // FIXME typecast shouldn't be needed - fixittt
  return procedure as unknown as AnyProcedure;
}
