import {
  HttpRequestSchema,
  HttpMethod,
  HttpSchema,
  HttpSchemaPath,
  HttpSchemaMethod,
  HttpMethodSchema,
  HttpResponseSchemaStatusCode,
  HttpStatusCode,
  HttpResponse,
  HttpRequest,
  HttpSearchParams,
  HttpHeaders,
  AllowAnyStringInPathParams,
  LiteralHttpSchemaPathFromNonLiteral,
  JSONValue,
  HttpResponseBodySchema,
  HttpResponseHeadersSchema,
  HttpRequestHeadersSchema,
  HttpHeadersSchema,
  HttpSearchParamsSchema,
} from '@zimic/http';
import { Default, DefaultNoExclude, IfNever, ReplaceBy } from '@zimic/utils/types';

import FetchResponseError, { AnyFetchRequestError } from '../errors/FetchResponseError';
import { JSONStringified } from './json';
import { FetchInput } from './public';

type FetchRequestInitHeaders<RequestSchema extends HttpRequestSchema> =
  | RequestSchema['headers']
  | HttpHeaders<Default<RequestSchema['headers']>>;

type FetchRequestInitWithHeaders<RequestSchema extends HttpRequestSchema> = 'headers' extends keyof RequestSchema
  ? [RequestSchema['headers']] extends [never]
    ? { headers?: undefined }
    : undefined extends RequestSchema['headers']
      ? { headers?: FetchRequestInitHeaders<RequestSchema> }
      : { headers: FetchRequestInitHeaders<RequestSchema> }
  : { headers?: undefined };

type FetchRequestInitSearchParams<RequestSchema extends HttpRequestSchema> =
  | RequestSchema['searchParams']
  | HttpSearchParams<Default<RequestSchema['searchParams']>>;

type FetchRequestInitWithSearchParams<RequestSchema extends HttpRequestSchema> =
  'searchParams' extends keyof RequestSchema
    ? [RequestSchema['searchParams']] extends [never]
      ? { searchParams?: undefined }
      : undefined extends RequestSchema['searchParams']
        ? { searchParams?: FetchRequestInitSearchParams<RequestSchema> }
        : { searchParams: FetchRequestInitSearchParams<RequestSchema> }
    : { searchParams?: undefined };

type FetchRequestInitWithBody<RequestSchema extends HttpRequestSchema> = 'body' extends keyof RequestSchema
  ? [RequestSchema['body']] extends [never]
    ? { body?: null }
    : RequestSchema['body'] extends string
      ? undefined extends RequestSchema['body']
        ? { body?: ReplaceBy<RequestSchema['body'], undefined, null> }
        : { body: RequestSchema['body'] }
      : RequestSchema['body'] extends JSONValue
        ? undefined extends RequestSchema['body']
          ? { body?: JSONStringified<ReplaceBy<RequestSchema['body'], undefined, null>> }
          : { body: JSONStringified<RequestSchema['body']> }
        : undefined extends RequestSchema['body']
          ? { body?: ReplaceBy<RequestSchema['body'], undefined, null> }
          : { body: RequestSchema['body'] }
  : { body?: null };

type FetchRequestInitPerPath<RequestSchema extends HttpRequestSchema> = FetchRequestInitWithHeaders<RequestSchema> &
  FetchRequestInitWithSearchParams<RequestSchema> &
  FetchRequestInitWithBody<RequestSchema>;

/**
 * The options to create a {@link FetchRequest} instance, compatible with
 * {@link https://developer.mozilla.org/docs/Web/API/RequestInit `RequestInit`}.
 *
 * @see {@link https://github.com/zimicjs/zimic/wiki/api‐zimic‐fetch#fetch `fetch` API reference}
 * @see {@link https://developer.mozilla.org/docs/Web/API/RequestInit `RequestInit`}
 */
export type FetchRequestInit<
  Schema extends HttpSchema,
  Method extends HttpSchemaMethod<Schema>,
  Path extends HttpSchemaPath<Schema, Method>,
  Redirect extends RequestRedirect = 'follow',
> = Omit<RequestInit, 'method' | 'headers' | 'body'> & {
  /** The HTTP method of the request. */
  method: Method;
  /** The base URL to prefix the path of the request. */
  baseURL?: string;
  redirect?: Redirect;
} & (Path extends Path ? FetchRequestInitPerPath<Default<Default<Schema[Path][Method]>['request']>> : never);

export namespace FetchRequestInit {
  /** The default options for each request sent by a fetch instance. */
  export interface Defaults extends Omit<RequestInit, 'headers'> {
    baseURL: string;
    /** The HTTP method of the request. */
    method?: HttpMethod;
    /** The headers of the request. */
    headers?: HttpHeadersSchema;
    /** The search parameters of the request. */
    searchParams?: HttpSearchParamsSchema;
  }

  /** A loosely typed version of {@link FetchRequestInit `FetchRequestInit`}. */
  export type Loose = Partial<Defaults>;
}

type AllFetchResponseStatusCode<MethodSchema extends HttpMethodSchema> = HttpResponseSchemaStatusCode<
  Default<MethodSchema['response']>
>;

type FilterFetchResponseStatusCodeByError<
  StatusCode extends HttpStatusCode,
  ErrorOnly extends boolean,
> = ErrorOnly extends true ? Extract<StatusCode, HttpStatusCode.ClientError | HttpStatusCode.ServerError> : StatusCode;

type FilterFetchResponseStatusCodeByRedirect<
  StatusCode extends HttpStatusCode,
  Redirect extends RequestRedirect,
> = Redirect extends 'error'
  ? FilterFetchResponseStatusCodeByRedirect<StatusCode, 'follow'>
  : Redirect extends 'follow'
    ? Exclude<StatusCode, Exclude<HttpStatusCode.Redirection, 304>>
    : StatusCode;

type FetchResponseStatusCode<
  MethodSchema extends HttpMethodSchema,
  ErrorOnly extends boolean,
  Redirect extends RequestRedirect,
> = FilterFetchResponseStatusCodeByRedirect<
  FilterFetchResponseStatusCodeByError<AllFetchResponseStatusCode<MethodSchema>, ErrorOnly>,
  Redirect
>;

type HttpRequestBodySchema<MethodSchema extends HttpMethodSchema> = ReplaceBy<
  ReplaceBy<IfNever<DefaultNoExclude<Default<MethodSchema['request']>['body']>, null>, undefined, null>,
  ArrayBuffer,
  Blob
>;

/**
 * A request instance typed with an HTTP schema, closely compatible with the
 * {@link https://developer.mozilla.org/docs/Web/API/Request native Request class}.
 *
 * On top of the properties available in native {@link https://developer.mozilla.org/docs/Web/API/Request `Request`}
 * instances, fetch requests have their URL automatically prefixed with the base URL of their fetch instance. Default
 * options are also applied, if present in the fetch instance.
 *
 * The path of the request is extracted from the URL, excluding the base URL, and is available in the `path` property.
 *
 * @example
 *   import { type HttpSchema } from '@zimic/http';
 *   import { createFetch } from '@zimic/fetch';
 *
 *   interface User {
 *     id: string;
 *     username: string;
 *   }
 *
 *   type Schema = HttpSchema<{
 *     '/users': {
 *       POST: {
 *         request: {
 *           headers: { 'content-type': 'application/json' };
 *           body: { username: string };
 *         };
 *         response: {
 *           201: { body: User };
 *         };
 *       };
 *     };
 *   }>;
 *
 *   const fetch = createFetch<Schema>({
 *     baseURL: 'http://localhost:3000',
 *   });
 *
 *   const request = new fetch.Request('/users', {
 *     method: 'POST',
 *     headers: { 'content-type': 'application/json' },
 *     body: JSON.stringify({ username: 'me' }),
 *   });
 *
 *   console.log(request); // FetchRequest<Schema, 'POST', '/users'>
 *   console.log(request.path); // '/users'
 *
 * @see {@link https://github.com/zimicjs/zimic/wiki/api‐zimic‐fetch#fetchrequest `FetchRequest` API reference}
 * @see {@link https://developer.mozilla.org/docs/Web/API/Request}
 */
export interface FetchRequest<
  Schema extends HttpSchema,
  Method extends HttpSchemaMethod<Schema>,
  Path extends HttpSchemaPath.Literal<Schema, Method>,
> extends HttpRequest<
    HttpRequestBodySchema<Default<Schema[Path][Method]>>,
    HttpRequestHeadersSchema<Default<Schema[Path][Method]>>
  > {
  /** The path of the request, excluding the base URL. */
  path: AllowAnyStringInPathParams<Path>;
  /** The HTTP method of the request. */
  method: Method;
}

export namespace FetchRequest {
  /** A loosely typed version of a {@link FetchRequest `FetchRequest`}. */
  export interface Loose extends Request {
    /** The path of the request, excluding the base URL. */
    path: string;
    /** The HTTP method of the request. */
    method: HttpMethod;
    /** Clones the request instance, returning a new instance with the same properties. */
    clone: () => Loose;
  }
}

/**
 * A plain object representation of a {@link FetchRequest `FetchRequest`}, compatible with JSON.
 *
 * If the body is included in the object, it is represented as a string or null if empty.
 */
export type FetchRequestObject = Pick<
  FetchRequest.Loose,
  | 'url'
  | 'path'
  | 'method'
  | 'cache'
  | 'destination'
  | 'credentials'
  | 'integrity'
  | 'keepalive'
  | 'mode'
  | 'redirect'
  | 'referrer'
  | 'referrerPolicy'
> & {
  /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response/headers) */
  headers: HttpHeadersSchema;
  /**
   * The body of the response, represented as a string or null if empty.
   *
   * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/body)
   */
  body?: string | null;
};

/**
 * A {@link FetchResponse `FetchResponse`} instance with a specific status code.
 *
 * @see {@link https://github.com/zimicjs/zimic/wiki/api‐zimic‐fetch#fetchresponse `FetchResponse` API reference}
 * @see {@link https://developer.mozilla.org/docs/Web/API/Response}
 */
export interface FetchResponsePerStatusCode<
  Schema extends HttpSchema,
  Method extends HttpSchemaMethod<Schema>,
  Path extends HttpSchemaPath.Literal<Schema, Method>,
  StatusCode extends HttpStatusCode = HttpStatusCode,
> extends HttpResponse<
    HttpResponseBodySchema<Default<Schema[Path][Method]>, StatusCode>,
    StatusCode,
    HttpResponseHeadersSchema<Default<Schema[Path][Method]>, StatusCode>
  > {
  /** The request that originated the response. */
  request: FetchRequest<Schema, Method, Path>;

  /**
   * An error representing a response with a failure status code (4XX or 5XX). It can be thrown to handle the error
   * upper in the call stack.
   *
   * If the response has a success status code (1XX, 2XX or 3XX), this property will be null.
   */
  error: StatusCode extends HttpStatusCode.ClientError | HttpStatusCode.ServerError
    ? FetchResponseError<Schema, Method, Path>
    : null;
}

/**
 * A response instance typed with an HTTP schema, closely compatible with the
 * {@link https://developer.mozilla.org/docs/Web/API/Response native Response class}.
 *
 * On top of the properties available in native Response instances, fetch responses have a reference to the request that
 * originated them, available in the `request` property.
 *
 * If the response has a failure status code (4XX or 5XX), an error is available in the `error` property.
 *
 * @example
 *   import { type HttpSchema } from '@zimic/http';
 *   import { createFetch } from '@zimic/fetch';
 *
 *   interface User {
 *     id: string;
 *     username: string;
 *   }
 *
 *   type Schema = HttpSchema<{
 *     '/users/:userId': {
 *       GET: {
 *         response: {
 *           200: { body: User };
 *           404: { body: { message: string } };
 *         };
 *       };
 *     };
 *   }>;
 *
 *   const fetch = createFetch<Schema>({
 *     baseURL: 'http://localhost:3000',
 *   });
 *
 *   const response = await fetch(`/users/${userId}`, {
 *     method: 'GET',
 *   });
 *
 *   console.log(response); // FetchResponse<Schema, 'GET', '/users'>
 *
 *   if (response.status === 404) {
 *     const errorBody = await response.json(); // { message: string }
 *     console.error(errorBody.message);
 *     return null;
 *   } else {
 *     const user = await response.json(); // User
 *     return user;
 *   }
 *
 * @see {@link https://github.com/zimicjs/zimic/wiki/api‐zimic‐fetch#fetchresponse `FetchResponse` API reference}
 * @see {@link https://developer.mozilla.org/docs/Web/API/Response}
 */
export type FetchResponse<
  Schema extends HttpSchema,
  Method extends HttpSchemaMethod<Schema>,
  Path extends HttpSchemaPath.Literal<Schema, Method>,
  ErrorOnly extends boolean = false,
  Redirect extends RequestRedirect = 'follow',
  StatusCode extends FetchResponseStatusCode<
    Default<Schema[Path][Method]>,
    ErrorOnly,
    Redirect
  > = FetchResponseStatusCode<Default<Schema[Path][Method]>, ErrorOnly, Redirect>,
> = StatusCode extends StatusCode ? FetchResponsePerStatusCode<Schema, Method, Path, StatusCode> : never;

export namespace FetchResponse {
  /** A loosely typed version of a {@link FetchResponse}. */
  export interface Loose extends Response {
    /** The request that originated the response. */
    request: FetchRequest.Loose;

    /**
     * An error representing a response with a failure status code (4XX or 5XX). It can be thrown to handle the error
     * upper in the call stack.
     *
     * If the response has a success status code (1XX, 2XX or 3XX), this property will be null.
     */
    error: AnyFetchRequestError | null;

    /** Clones the request instance, returning a new instance with the same properties. */
    clone: () => Loose;
  }
}

/**
 * A plain object representation of a {@link FetchResponse `FetchResponse`}, compatible with JSON.
 *
 * If the body is included in the object, it is represented as a string or null if empty.
 */
export type FetchResponseObject = Pick<
  FetchResponse.Loose,
  'url' | 'type' | 'status' | 'statusText' | 'ok' | 'redirected'
> & {
  /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response/headers) */
  headers: HttpHeadersSchema;
  /**
   * The body of the response, represented as a string or null if empty.
   *
   * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/body)
   */
  body?: string | null;
};

/**
 * A constructor for {@link FetchRequest} instances, typed with an HTTP schema and compatible with the
 * {@link https://developer.mozilla.org/docs/Web/API/Request Request class constructor}.
 *
 * @example
 *   import { type HttpSchema } from '@zimic/http';
 *   import { createFetch } from '@zimic/fetch';
 *
 *   type Schema = HttpSchema<{
 *     // ...
 *   }>;
 *
 *   const fetch = createFetch<Schema>({
 *     baseURL: 'http://localhost:3000',
 *   });
 *
 *   const request = new fetch.Request('POST', '/users', {
 *     body: JSON.stringify({ username: 'me' }),
 *   });
 *   console.log(request); // FetchRequest<Schema, 'POST', '/users'>
 *
 * @param input The resource to fetch, either a path, a URL, or a {@link FetchRequest request}. If a path is provided, it
 *   is automatically prefixed with the base URL of the fetch instance when the request is sent. If a URL or a request
 *   is provided, it is used as is.
 * @param init The request options. If a path or a URL is provided as the first argument, this argument is required and
 *   should contain at least the method of the request. If the first argument is a {@link FetchRequest request}, this
 *   argument is optional.
 * @returns A promise that resolves to the response to the request.
 * @see {@link https://github.com/zimicjs/zimic/wiki/api‐zimic‐fetch#fetchresponse `FetchResponse` API reference}
 * @see {@link https://developer.mozilla.org/docs/Web/API/Request}
 */
export type FetchRequestConstructor<Schema extends HttpSchema> = new <
  Method extends HttpSchemaMethod<Schema>,
  Path extends HttpSchemaPath.NonLiteral<Schema, Method>,
>(
  input: FetchInput<Schema, Method, Path>,
  init: FetchRequestInit<Schema, Method, LiteralHttpSchemaPathFromNonLiteral<Schema, Method, Path>>,
) => FetchRequest<Schema, Method, LiteralHttpSchemaPathFromNonLiteral<Schema, Method, Path>>;
