import type {
  Denormalize,
  EndpointExtraOptions,
  EndpointInstanceInterface,
  Schema,
  FetchFunction,
  ResolveType,
} from '@data-client/endpoint';

import type { ExtractCollection } from './extractCollection.js';
import {
  OptionsToBodyArgument,
  OptionsToFunction,
} from './OptionsToFunction.js';
import { PathArgs, SoftPathArgs } from './pathTypes.js';
import { EndpointUpdateFunction } from './RestEndpointTypeHelp.js';

export type ContentType = 'json' | 'blob' | 'text' | 'arrayBuffer' | 'stream';

interface ContentTypeMap {
  blob: Blob;
  text: string;
  arrayBuffer: ArrayBuffer;
  stream: ReadableStream<Uint8Array>;
  json: any;
}

type ContentReturnType<C extends ContentType> = ContentTypeMap[C];

type ContentSchemaGuard<O> =
  O extends { content: 'blob' | 'text' | 'arrayBuffer' | 'stream' } ?
    { schema?: undefined }
  : {};

export interface RestInstanceBase<
  F extends FetchFunction = FetchFunction,
  S extends Schema | undefined = any,
  M extends boolean | undefined = boolean | undefined,
  O extends {
    path: string;
    body?: any;
    searchParams?: any;
    method?: string;
  } = { path: string },
> extends EndpointInstanceInterface<F, S, M> {
  /** @see https://dataclient.io/rest/api/RestEndpoint#body */
  readonly body?: 'body' extends keyof O ? O['body'] : any;
  /** @see https://dataclient.io/rest/api/RestEndpoint#searchParams */
  readonly searchParams?: 'searchParams' extends keyof O ? O['searchParams']
  : // unknown is identity with '&' type operator
    unknown;

  /** Pattern to construct url based on Url Parameters
   * @see https://dataclient.io/rest/api/RestEndpoint#path
   */
  readonly path: O['path'];
  /** Prepended to all urls
   * @see https://dataclient.io/rest/api/RestEndpoint#urlPrefix
   */
  readonly urlPrefix: string;
  readonly requestInit: RequestInit;
  /** HTTP request method
   * @see https://dataclient.io/rest/api/RestEndpoint#method
   */
  readonly method: (O & { method: string })['method'];
  readonly signal: AbortSignal | undefined;
  /** @see https://dataclient.io/rest/api/RestEndpoint#paginationField */
  readonly paginationField?: string;
  /** @see https://dataclient.io/rest/api/RestEndpoint#content */
  readonly content?: ContentType;

  /* fetch lifecycles */
  /* before-fetch */
  /** Builds the URL to fetch
   * @see https://dataclient.io/rest/api/RestEndpoint#url
   */
  url(...args: Parameters<F>): string;
  /** Encode the searchParams component of the url
   * @see https://dataclient.io/rest/api/RestEndpoint#searchToString
   */
  searchToString(searchParams: Record<string, any>): string;
  /** Prepares RequestInit used in fetch. This is sent to fetchResponse()
   * @see https://dataclient.io/rest/api/RestEndpoint#getRequestInit
   */
  getRequestInit(
    this: any,
    body?: RequestInit['body'] | Record<string, unknown>,
  ): Promise<RequestInit> | RequestInit;
  /** Called by getRequestInit to determine HTTP Headers
   * @see https://dataclient.io/rest/api/RestEndpoint#getHeaders
   */
  getHeaders(headers: HeadersInit): Promise<HeadersInit> | HeadersInit;
  /* after-fetch */
  /** Performs the fetch call
   * @see https://dataclient.io/rest/api/RestEndpoint#fetchResponse
   */
  fetchResponse(input: RequestInfo, init: RequestInit): Promise<Response>;
  /** Takes the Response and parses via .text() or .json()
   * @see https://dataclient.io/rest/api/RestEndpoint#parseResponse
   */
  parseResponse(response: Response): Promise<any>;
  /** Perform any transforms with the parsed result.
   * @see https://dataclient.io/rest/api/RestEndpoint#process
   */
  process(value: any, ...args: Parameters<F>): ResolveType<F>;

  /* utilities */
  /** Returns true if the provided (fetch) key matches this endpoint.
   * @see https://dataclient.io/rest/api/RestEndpoint#testKey
   */
  testKey(key: string): boolean;

  /* extenders */
  // TODO: figure out better way than wrapping whole options in Readonly<> + making O extend from {}
  //       this is just a hack to handle when no members of PartialRestGenerics are present
  //       Note: Using overloading (like paginated did) struggles because typescript does not have a clear way of distinguishing one
  //       should be used from the other (due to same problem with every member being partial)
  /** Creates a child endpoint that inherits from this while overriding provided `options`.
   * @see https://dataclient.io/rest/api/RestEndpoint#extend
   */
  extend<
    E extends RestInstanceBase,
    ExtendOptions extends PartialRestGenerics | {},
  >(
    this: E,
    options: Readonly<
      RestEndpointExtendOptions<ExtendOptions, E, F> & ExtendOptions
    > &
      ContentSchemaGuard<ExtendOptions>,
  ): RestExtendedEndpoint<ExtendOptions, E>;
}

export interface RestInstance<
  F extends FetchFunction = FetchFunction,
  S extends Schema | undefined = any,
  M extends boolean | undefined = boolean | undefined,
  O extends {
    path: string;
    body?: any;
    searchParams?: any;
    method?: string;
    paginationField?: string;
  } = { path: string },
> extends RestInstanceBase<F, S, M, O> {
  /** Creates an Endpoint to append the next page extending a list for pagination
   * @see https://dataclient.io/rest/api/RestEndpoint#paginated
   */
  paginated<
    E extends RestInstanceBase<FetchFunction, any, undefined>,
    A extends any[],
  >(
    this: E,
    removeCursor: (...args: A) => readonly [...Parameters<E>],
  ): PaginationEndpoint<E, A>;
  paginated<
    E extends RestInstanceBase<FetchFunction, any, undefined>,
    C extends string,
  >(
    this: E,
    cursorField: C,
  ): PaginationFieldEndpoint<E, C>;
  /** Concatinate the next page of results (GET)
   * @see https://dataclient.io/rest/api/RestEndpoint#getPage
   */
  getPage: 'paginationField' extends keyof O ?
    O['paginationField'] extends string ?
      PaginationFieldEndpoint<
        F & { schema: S; sideEffect: M } & O,
        O['paginationField']
      >
    : undefined
  : undefined;
  /** Create a new item (POST) and `push` to the end
   * @see https://dataclient.io/rest/api/RestEndpoint#push
   */
  push: AddEndpoint<
    F,
    ExtractCollection<S>['push'],
    Omit<O, 'body' | 'method'> & {
      body:
        | OptionsToAdderBodyArgument<O, ExtractCollection<S>['push']>
        | OptionsToAdderBodyArgument<O, ExtractCollection<S>['push']>[]
        | FormData;
    }
  >;
  /** Create a new item (POST) and `unshift` to the beginning
   * @see https://dataclient.io/rest/api/RestEndpoint#unshift
   */
  unshift: AddEndpoint<
    F,
    ExtractCollection<S>['unshift'],
    Omit<O, 'body' | 'method'> & {
      body:
        | OptionsToAdderBodyArgument<O, ExtractCollection<S>['unshift']>
        | OptionsToAdderBodyArgument<O, ExtractCollection<S>['unshift']>[]
        | FormData;
    }
  >;
  /** Create new item(s) (POST) and `Object.assign` merge
   * @see https://dataclient.io/rest/api/RestEndpoint#assign
   */
  assign: AddEndpoint<
    F,
    ExtractCollection<S>,
    Omit<O, 'body' | 'method'> & {
      body:
        | Record<
            string,
            OptionsToAdderBodyArgument<O, ExtractCollection<S>['assign']>
          >
        | FormData;
    }
  >;
  /** Remove item(s) (PATCH) from collection
   * @see https://dataclient.io/rest/api/RestEndpoint#remove
   */
  remove: RemoveEndpoint<
    F,
    ExtractCollection<S>['remove'],
    Omit<O, 'body' | 'method'> & {
      body:
        | Partial<OptionsToAdderBodyArgument<O, ExtractCollection<S>['remove']>>
        | Partial<
            OptionsToAdderBodyArgument<O, ExtractCollection<S>['remove']>
          >[]
        | FormData;
    }
  >;
  /** Move item between collections (PATCH) - removes from old, adds to new
   * @see https://dataclient.io/rest/api/RestEndpoint#move
   */
  move: MoveEndpoint<
    F,
    ExtractCollection<S>['move'],
    {
      path: 'movePath' extends keyof O ? O['movePath'] & string : O['path'];
      body:
        | Partial<OptionsToAdderBodyArgument<O, ExtractCollection<S>['move']>>
        | FormData;
    }
  >;
}

export type RestEndpointExtendOptions<
  O extends PartialRestGenerics,
  E extends { body?: any; path?: string; schema?: Schema; method?: string },
  F extends FetchFunction,
> = RestEndpointOptions<
  OptionsToFunction<O, E, F>,
  'schema' extends keyof O ? Extract<O['schema'], Schema | undefined>
  : E['schema']
> &
  Partial<
    Omit<
      E,
      KeyofRestEndpoint | keyof PartialRestGenerics | keyof RestEndpointOptions
    >
  >;

type OptionsToRestEndpoint<
  O extends PartialRestGenerics,
  E extends RestInstanceBase & { body?: any; paginationField?: string },
  F extends FetchFunction,
> =
  'path' extends keyof O ?
    RestType<
      'searchParams' extends keyof O ?
        [O['searchParams']] extends [undefined] ?
          PathArgs<Exclude<O['path'], undefined>>
        : O['searchParams'] & PathArgs<Exclude<O['path'], undefined>>
      : PathArgs<Exclude<O['path'], undefined>>,
      OptionsToBodyArgument<
        'body' extends keyof O ? O : E,
        'method' extends keyof O ? O['method'] : E['method']
      >,
      'schema' extends keyof O ? O['schema'] : E['schema'],
      'sideEffect' extends keyof O ?
        Extract<O['sideEffect'], boolean | undefined>
      : 'method' extends keyof O ? MethodToSide<O['method']>
      : E['sideEffect'],
      O['process'] extends {} ? ReturnType<O['process']>
      : 'content' extends keyof O ?
        ContentReturnType<O['content'] & ContentType>
      : ResolveType<F>,
      {
        path: Exclude<O['path'], undefined>;
        body: 'body' extends keyof O ? O['body'] : E['body'];
        searchParams: 'searchParams' extends keyof O ? O['searchParams']
        : E['searchParams'];
        method: 'method' extends keyof O ? O['method'] : E['method'];
        paginationField: 'paginationField' extends keyof O ?
          O['paginationField']
        : E['paginationField'];
      }
    >
  : 'body' extends keyof O ?
    RestType<
      'searchParams' extends keyof O ?
        [O['searchParams']] extends [undefined] ?
          PathArgs<Exclude<O['path'], undefined>>
        : O['searchParams'] & PathArgs<Exclude<E['path'], undefined>>
      : PathArgs<Exclude<E['path'], undefined>>,
      OptionsToBodyArgument<
        O,
        'method' extends keyof O ? O['method'] : E['method']
      >,
      'schema' extends keyof O ? O['schema'] : E['schema'],
      'sideEffect' extends keyof O ?
        Extract<O['sideEffect'], boolean | undefined>
      : 'method' extends keyof O ? MethodToSide<O['method']>
      : E['sideEffect'],
      O['process'] extends {} ? ReturnType<O['process']>
      : 'content' extends keyof O ?
        ContentReturnType<O['content'] & ContentType>
      : ResolveType<F>,
      {
        path: E['path'];
        body: O['body'];
        searchParams: 'searchParams' extends keyof O ? O['searchParams']
        : E['searchParams'];
        method: 'method' extends keyof O ? O['method'] : E['method'];
        paginationField: 'paginationField' extends keyof O ?
          O['paginationField']
        : Extract<E['paginationField'], string>;
      }
    >
  : 'searchParams' extends keyof O ?
    RestType<
      [O['searchParams']] extends [undefined] ?
        PathArgs<Exclude<O['path'], undefined>>
      : O['searchParams'] & PathArgs<Exclude<E['path'], undefined>>,
      OptionsToBodyArgument<
        E,
        'method' extends keyof O ? O['method'] : E['method']
      >,
      'schema' extends keyof O ? O['schema'] : E['schema'],
      'sideEffect' extends keyof O ?
        Extract<O['sideEffect'], boolean | undefined>
      : 'method' extends keyof O ? MethodToSide<O['method']>
      : E['sideEffect'],
      O['process'] extends {} ? ReturnType<O['process']>
      : 'content' extends keyof O ?
        ContentReturnType<O['content'] & ContentType>
      : ResolveType<F>,
      {
        path: E['path'];
        body: E['body'];
        searchParams: O['searchParams'];
        method: 'method' extends keyof O ? O['method'] : E['method'];
        paginationField: 'paginationField' extends keyof O ?
          O['paginationField']
        : Extract<E['paginationField'], string>;
      }
    >
  : RestInstance<
      F,
      'schema' extends keyof O ? O['schema'] : E['schema'],
      'sideEffect' extends keyof O ?
        Extract<O['sideEffect'], boolean | undefined>
      : 'method' extends keyof O ? MethodToSide<O['method']>
      : E['sideEffect'],
      {
        path: 'path' extends keyof O ? Exclude<O['path'], undefined>
        : E['path'];
        body: 'body' extends keyof O ? O['body'] : E['body'];
        searchParams: 'searchParams' extends keyof O ? O['searchParams']
        : E['searchParams'];
        method: 'method' extends keyof O ? O['method'] : E['method'];
        paginationField: 'paginationField' extends keyof O ?
          O['paginationField']
        : E['paginationField'];
      }
    >;

export type RestExtendedEndpoint<
  O extends PartialRestGenerics,
  E extends RestInstanceBase & { getPage?: unknown },
> = OptionsToRestEndpoint<
  O,
  E &
    (E extends { getPage: { paginationField: string } } ?
      { paginationField: E['getPage']['paginationField'] }
    : unknown),
  RestInstance<
    (...args: Parameters<E>) => O['process'] extends {} ?
      Promise<ReturnType<O['process']>>
    : 'content' extends keyof O ?
      Promise<ContentReturnType<O['content'] & ContentType>>
    : ReturnType<E>,
    'schema' extends keyof O ? O['schema'] : E['schema'],
    'sideEffect' extends keyof O ? Extract<O['sideEffect'], boolean | undefined>
    : 'method' extends keyof O ? MethodToSide<O['method']>
    : E['sideEffect']
  >
> &
  Omit<O, KeyofRestEndpoint> &
  Omit<E, KeyofRestEndpoint | keyof O>;

export interface PartialRestGenerics {
  /** @see https://dataclient.io/rest/api/RestEndpoint#path */
  readonly path?: string;
  /** @see https://dataclient.io/rest/api/RestEndpoint#schema */
  readonly schema?: Schema | undefined;
  /** @see https://dataclient.io/rest/api/RestEndpoint#method */
  readonly method?: string;
  /** Only used for types */
  /** @see https://dataclient.io/rest/api/RestEndpoint#body */
  body?: any;
  /** Only used for types */
  /** @see https://dataclient.io/rest/api/RestEndpoint#searchParams */
  searchParams?: any;
  /** @see https://dataclient.io/rest/api/RestEndpoint#paginationfield */
  readonly paginationField?: string;
  /** @see https://dataclient.io/rest/api/RestEndpoint#process */
  process?(value: any, ...args: any): any;
  /** @see https://dataclient.io/rest/api/RestEndpoint#content */
  readonly content?: ContentType;
}
/** Generic types when constructing a RestEndpoint
 *
 * @see https://dataclient.io/rest/api/RestEndpoint#inheritance
 */
export interface RestGenerics extends PartialRestGenerics {
  readonly path: string;
}

export type PaginationEndpoint<
  E extends FetchFunction & RestGenerics & { sideEffect?: boolean | undefined },
  A extends any[],
> = RestInstanceBase<
  ParamFetchNoBody<A[0], ResolveType<E>>,
  E['schema'],
  E['sideEffect'],
  Pick<E, 'path' | 'searchParams' | 'body'> & {
    searchParams: Omit<A[0], keyof PathArgs<E['path']>>;
  }
>;
/** Merge pagination field C into body, making it required */
type PaginationIntoBody<Body, C extends string> = Body & {
  [K in C]: string | number | boolean;
};

/** Paginated searchParams type */
type PaginatedSearchParams<
  E extends { searchParams?: any; path?: string },
  C extends string,
> = {
  [K in C]: string | number | boolean;
} & E['searchParams'] &
  PathArgs<Exclude<E['path'], undefined>>;

/** searchParams version: pagination in searchParams, optional body support */
type PaginationFieldInSearchParams<
  E extends FetchFunction & RestGenerics & { sideEffect?: boolean | undefined },
  C extends string,
> = RestInstanceBase<
  // Union allows calling with just searchParams or with searchParams + body
  | ParamFetchNoBody<PaginatedSearchParams<E, C>, ResolveType<E>>
  | ParamFetchWithBody<
      PaginatedSearchParams<E, C>,
      NonNullable<E['body']>,
      ResolveType<E>
    >,
  E['schema'],
  E['sideEffect'],
  Pick<E, 'path' | 'searchParams' | 'body'> & {
    searchParams: {
      [K in C]: string | number | boolean;
    } & E['searchParams'];
  }
> & { paginationField: C };

/** body version: pagination field is in body (body required) */
type PaginationFieldInBody<
  E extends FetchFunction & RestGenerics & { sideEffect?: boolean | undefined },
  C extends string,
> = RestInstanceBase<
  ParamFetchWithBody<
    E['searchParams'] & PathArgs<Exclude<E['path'], undefined>>,
    PaginationIntoBody<E['body'], C>,
    ResolveType<E>
  >,
  E['schema'],
  E['sideEffect'],
  Pick<E, 'path' | 'searchParams'> & {
    body: PaginationIntoBody<E['body'], C>;
  }
> & { paginationField: C };

/** Retrieves the next page of results by pagination field */
export type PaginationFieldEndpoint<
  E extends FetchFunction & RestGenerics & { sideEffect?: boolean | undefined },
  C extends string,
> =
  // If body can be undefined or pagination field not in body, use searchParams
  undefined extends E['body'] ? PaginationFieldInSearchParams<E, C>
  : // If pagination field C is a key of body, merge into body
  C extends keyof E['body'] ? PaginationFieldInBody<E, C>
  : // Otherwise use searchParams
    PaginationFieldInSearchParams<E, C>;

export type AddEndpoint<
  F extends FetchFunction = FetchFunction,
  S extends Schema | undefined = any,
  O extends {
    path: string;
    body: any;
    searchParams?: any;
  } = { path: string; body: any },
> = RestInstanceBase<
  RestFetch<
    'searchParams' extends keyof O ?
      [O['searchParams']] extends [undefined] ?
        PathArgs<Exclude<O['path'], undefined>>
      : O['searchParams'] & PathArgs<Exclude<O['path'], undefined>>
    : PathArgs<Exclude<O['path'], undefined>>,
    O['body'],
    ResolveType<F>
  >,
  S,
  true,
  Omit<O, 'method'> & { method: 'POST' }
>;
export type RemoveEndpoint<
  F extends FetchFunction = FetchFunction,
  S extends Schema | undefined = any,
  O extends {
    path: string;
    body: any;
    searchParams?: any;
  } = { path: string; body: any },
> = RestInstanceBase<
  RestFetch<
    'searchParams' extends keyof O ?
      [O['searchParams']] extends [undefined] ?
        PathArgs<Exclude<O['path'], undefined>>
      : O['searchParams'] & PathArgs<Exclude<O['path'], undefined>>
    : PathArgs<Exclude<O['path'], undefined>>,
    O['body'],
    ResolveType<F>
  >,
  S,
  true,
  Omit<O, 'method'> & { method: 'PATCH' }
>;
export type MoveEndpoint<
  F extends FetchFunction = FetchFunction,
  S extends Schema | undefined = any,
  O extends {
    path: string;
    body: any;
  } = { path: string; body: any },
> = RestInstanceBase<
  RestFetch<PathArgs<Exclude<O['path'], undefined>>, O['body'], ResolveType<F>>,
  S,
  true,
  Omit<O, 'method' | 'searchParams'> & { method: 'PATCH' }
>;

type OptionsBodyDefault<O extends RestGenerics> =
  'body' extends keyof O ? O
  : O['method'] extends 'POST' | 'PUT' | 'PATCH' ? O & { body: any }
  : O & { body: undefined };

/** When `method` is omitted from `O`, infer it (must stay aligned with `OptionsToBodyArgument`). */
type InferRestMethodWhenOmitted<O extends RestGenerics> =
  O extends { sideEffect: true } ? 'POST'
  : 'body' extends keyof O ?
    [O['body']] extends [undefined] ?
      'GET'
    : 'POST'
  : 'GET';

type MethodArgForBodyInference<O extends RestGenerics> =
  'method' extends keyof O ? O['method'] : InferRestMethodWhenOmitted<O>;

type OptionsToAdderBodyArgument<O extends { body?: any }, EntitySchema = any> =
  'body' extends keyof O ? O['body'] : Partial<Denormalize<EntitySchema>>;

export interface RestEndpointOptions<
  F extends FetchFunction = FetchFunction,
  S extends Schema | undefined = undefined,
> extends EndpointExtraOptions<F> {
  /** Prepended to all urls
   * @see https://dataclient.io/rest/api/RestEndpoint#urlPrefix
   */
  urlPrefix?: string;
  requestInit?: RequestInit;
  /** Called by getRequestInit to determine HTTP Headers
   * @see https://dataclient.io/rest/api/RestEndpoint#getHeaders
   */
  getHeaders?(headers: HeadersInit): Promise<HeadersInit> | HeadersInit;
  /** Prepares RequestInit used in fetch. This is sent to fetchResponse()
   * @see https://dataclient.io/rest/api/RestEndpoint#getRequestInit
   */
  getRequestInit?(body: any): Promise<RequestInit> | RequestInit;
  /** Performs the fetch call
   * @see https://dataclient.io/rest/api/RestEndpoint#fetchResponse
   */
  fetchResponse?(input: RequestInfo, init: RequestInit): Promise<any>;
  /** Takes the Response and parses via .text() or .json()
   * @see https://dataclient.io/rest/api/RestEndpoint#parseResponse
   */
  parseResponse?(response: Response): Promise<any>;
  /** @see https://dataclient.io/rest/api/RestEndpoint#content */
  content?: ContentType;

  sideEffect?: boolean | undefined;
  name?: string;
  signal?: AbortSignal;
  fetch?: F;
  key?(...args: Parameters<F>): string;
  url?(...args: Parameters<F>): string;
  update?: EndpointUpdateFunction<F, S>;
}

// When subclassing RestEndpoint with `O extends RestGenerics = any`, O defaults
// to `any`. The `unknown extends O ? any` guard catches O=any before it reaches
// PathArgs (see #3782). SoftPathArgs collapses PathArgs<string> to `unknown`
// when a concrete body is present, preventing union overloads that break
// getOptimisticResponse callbacks. Method inference treats explicit body as POST.
export type RestEndpointConstructorOptions<O extends RestGenerics = any> =
  RestEndpointOptions<
    RestFetch<
      unknown extends O ? any
      : 'searchParams' extends keyof O ?
        [O['searchParams']] extends [undefined] ?
          SoftPathArgs<O['path']>
        : O['searchParams'] & SoftPathArgs<O['path']>
      : SoftPathArgs<O['path']>,
      OptionsToBodyArgument<O, MethodArgForBodyInference<O>>,
      O['process'] extends {} ? ReturnType<O['process']>
      : 'content' extends keyof O ?
        ContentReturnType<O['content'] & ContentType>
      : any /*Denormalize<O['schema']>*/
    >,
    O['schema']
  >;

/** Simplifies endpoint definitions that follow REST patterns
 *
 * @see https://dataclient.io/rest/api/RestEndpoint
 */
export interface RestEndpoint<
  O extends RestGenerics = any,
> extends RestInstance<
  RestFetch<
    unknown extends O ? any
    : 'searchParams' extends keyof O ?
      [O['searchParams']] extends [undefined] ?
        PathArgs<O['path']>
      : O['searchParams'] & PathArgs<O['path']>
    : PathArgs<O['path']>,
    OptionsToBodyArgument<O, MethodArgForBodyInference<O>>,
    O['process'] extends {} ? ReturnType<O['process']>
    : 'content' extends keyof O ? ContentReturnType<O['content'] & ContentType>
    : any /*Denormalize<O['schema']>*/
  >,
  'schema' extends keyof O ? O['schema'] : undefined,
  'sideEffect' extends keyof O ? Extract<O['sideEffect'], boolean | undefined>
  : MethodToSide<MethodArgForBodyInference<O>>,
  'method' extends keyof O ? O
  : O & {
      method: InferRestMethodWhenOmitted<O>;
    }
> {}

export interface RestEndpointConstructor {
  /** Simplifies endpoint definitions that follow REST patterns
   *
   * @see https://dataclient.io/rest/api/RestEndpoint
   */
  new <O extends RestGenerics = any>({
    method,
    sideEffect,
    name,
    ...options
  }: RestEndpointConstructorOptions<O> &
    Readonly<O> &
    ContentSchemaGuard<O>): RestEndpoint<O>;
  readonly prototype: RestInstanceBase;
}

export type MethodToSide<M> =
  M extends string ?
    M extends 'GET' ?
      undefined
    : true
  : undefined;

/** RestEndpoint types simplified */
export type RestType<
  UrlParams = any,
  Body = any,
  S extends Schema | undefined = Schema | undefined,
  M extends boolean | undefined = boolean | undefined,
  R = any,
  O extends {
    path: string;
    body?: any;
    searchParams?: any;
    paginationField?: string;
  } = { path: string; paginationField: string },
> = IfTypeScriptLooseNull<
  RestInstance<RestFetch<UrlParams, Body, R>, S, M, O>,
  Body extends {} ? RestTypeWithBody<UrlParams, S, M, Body, R, O>
  : RestTypeNoBody<UrlParams, S, M, R, O>
>;

export type RestTypeWithBody<
  UrlParams = any,
  S extends Schema | undefined = Schema | undefined,
  M extends boolean | undefined = boolean | undefined,
  Body = any,
  R = any /*Denormalize<S>*/,
  O extends {
    path: string;
    body?: any;
    searchParams?: any;
  } = { path: string; body: any },
> = RestInstance<ParamFetchWithBody<UrlParams, Body, R>, S, M, O>;

export type RestTypeNoBody<
  UrlParams = any,
  S extends Schema | undefined = Schema | undefined,
  M extends boolean | undefined = boolean | undefined,
  R = any /*Denormalize<S>*/,
  O extends {
    path: string;
    body?: undefined;
    searchParams?: any;
  } = { path: string; body: undefined },
> = RestInstance<ParamFetchNoBody<UrlParams, R>, S, M, O>;

/** Simple parameters, and body fetch functions */
export type RestFetch<
  UrlParams,
  Body = {},
  Resolve = any,
> = IfTypeScriptLooseNull<
  | ParamFetchNoBody<UrlParams, Resolve>
  | ParamFetchWithBody<UrlParams, Body, Resolve>,
  Body extends {} ? ParamFetchWithBody<UrlParams, Body, Resolve>
  : ParamFetchNoBody<UrlParams, Resolve>
>;

export type ParamFetchWithBody<P, B = {}, R = any> =
  // we must always allow undefined in a union and give it a type without params
  P extends undefined ? (this: EndpointInstanceInterface, body: B) => Promise<R>
  : // even with loose null, this will only be true when all members are optional
  {} extends P ?
    // this safely handles PathArgs with no members that results in a simple `unknown` type
    keyof P extends never ?
      (this: EndpointInstanceInterface, body: B) => Promise<R>
    : | ((this: EndpointInstanceInterface, params: P, body: B) => Promise<R>)
      | ((this: EndpointInstanceInterface, body: B) => Promise<R>)
  : (this: EndpointInstanceInterface, params: P, body: B) => Promise<R>;

export type ParamFetchNoBody<P, R = any> =
  // we must always allow undefined in a union and give it a type without params
  P extends undefined ? (this: EndpointInstanceInterface) => Promise<R>
  : // even with loose null, this will only be true when all members are optional
  {} extends P ?
    // this safely handles PathArgs with no members that results in a simple `unknown` type
    keyof P extends never ?
      (this: EndpointInstanceInterface) => Promise<R>
    : | ((this: EndpointInstanceInterface, params: P) => Promise<R>)
      | ((this: EndpointInstanceInterface) => Promise<R>)
  : (this: EndpointInstanceInterface, params: P) => Promise<R>;

// same algorithm, but for Args (aka readonly any[])
export type ParamToArgs<P> =
  P extends undefined ? []
  : {} extends P ?
    keyof P extends never ?
      []
    : [] | [P]
  : [P];

type IfTypeScriptLooseNull<Y, N> = 1 | undefined extends 1 ? Y : N;

export type KeyofRestEndpoint = keyof RestInstance;

export type FromFallBack<K extends keyof E, O, E> =
  K extends keyof O ? O[K] : E[K];

export type FetchMutate<
  A extends readonly any[] = [any, {}] | [{}],
  R = any,
> = (this: RestInstance, ...args: A) => Promise<R>;

export type FetchGet<A extends readonly any[] = [any], R = any> = (
  this: RestInstance,
  ...args: A
) => Promise<R>;

export type Defaults<O, D> = {
  [K in keyof O | keyof D]: K extends keyof O ? Exclude<O[K], undefined>
  : D[Extract<K, keyof D>];
};

export type GetEndpoint<
  O extends {
    readonly path: string;
    readonly schema: Schema;
    /** Only used for types */
    readonly searchParams?: any;
    readonly paginationField?: string;
  } = {
    path: string;
    schema: Schema;
  },
> = RestTypeNoBody<
  'searchParams' extends keyof O ?
    [O['searchParams']] extends [undefined] ?
      PathArgs<O['path']>
    : O['searchParams'] & PathArgs<O['path']>
  : PathArgs<O['path']>,
  O['schema'],
  undefined,
  any,
  O & { method: 'GET' }
>;

export type MutateEndpoint<
  O extends {
    readonly path: string;
    readonly schema: Schema;
    /** Only used for types */
    readonly searchParams?: any;
    /** Only used for types */
    readonly body?: any;
  } = {
    path: string;
    body: any;
    schema: Schema;
  },
> = RestTypeWithBody<
  'searchParams' extends keyof O ?
    [O['searchParams']] extends [undefined] ?
      PathArgs<O['path']>
    : O['searchParams'] & PathArgs<O['path']>
  : PathArgs<O['path']>,
  O['schema'],
  true,
  O['body'],
  any,
  O & { body: any; method: 'POST' | 'PUT' | 'PATCH' | 'DELETE' }
>;
