// HTTP types

type HttpMethod = "get" | "put" | "post" | "delete" | "options" | "head" | "patch" | "trace";
/** 2XX statuses */
type OkStatus = 200 | 201 | 202 | 203 | 204 | 206 | 207 | "2XX";
/** 4XX and 5XX statuses */
// biome-ignore format: keep on one line
type ErrorStatus = 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 510 | 511 | '5XX' | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 444 | 450 | 451 | 497 | 498 | 499 | '4XX' | "default";

// OpenAPI type helpers

/** Given an OpenAPI **Paths Object**, find all paths that have the given method */
type PathsWithMethod<Paths extends {}, PathnameMethod extends HttpMethod> = {
  [Pathname in keyof Paths]: Paths[Pathname] extends {
    [K in PathnameMethod]: any;
  }
    ? Pathname
    : never;
}[keyof Paths];

/** Return `responses` for an Operation Object */
type ResponseObjectMap<T> = T extends { responses: any } ? T["responses"] : unknown;

/** Return `content` for a Response Object */
type ResponseContent<T> = T extends { content: any } ? T["content"] : unknown;

/** Return type of `requestBody` for an Operation Object */
type OperationRequestBody<T> = "requestBody" extends keyof T ? T["requestBody"] : never;

/** Internal helper to get object type with only the `requestBody` property */
type PickRequestBody<T> = "requestBody" extends keyof T ? Pick<T, "requestBody"> : never;

/** Resolve to `true` if request body is optional, else `false` */
type IsOperationRequestBodyOptional<T> = RequiredKeysOf<PickRequestBody<T>> extends never ? true : false;

/** Internal helper used in OperationRequestBodyContent */
type OperationRequestBodyMediaContent<T> = IsOperationRequestBodyOptional<T> extends true
  ? ResponseContent<NonNullable<OperationRequestBody<T>>> | undefined
  : ResponseContent<OperationRequestBody<T>>;

/** Return first `content` from a Request Object Mapping, allowing any media type */
type OperationRequestBodyContent<T> = FilterKeys<OperationRequestBodyMediaContent<T>, MediaType> extends never
  ? FilterKeys<NonNullable<OperationRequestBodyMediaContent<T>>, MediaType> | undefined
  : FilterKeys<OperationRequestBodyMediaContent<T>, MediaType>;

/** Return all 2XX responses from a Response Object Map */
type SuccessResponse<
  T extends Record<string | number, any>,
  Media extends MediaType = MediaType,
> = GetResponseContent<T, Media, OkStatus>;

type GetResponseContent<
  T extends Record<string | number, any>,
  Media extends MediaType = MediaType,
  ResponseCode extends keyof T = keyof T,
> = ResponseCode extends keyof T
  ? {
      [K in ResponseCode]: T[K]["content"] extends Record<string, any>
        ? FilterKeys<T[K]["content"], Media> extends never
          ? T[K]["content"]
          : FilterKeys<T[K]["content"], Media>
        : K extends keyof T
          ? T[K]["content"]
          : never;
    }[ResponseCode]
  : never;

/**
 * Return all 5XX and 4XX responses (in that order) from a Response Object Map
 */
type ErrorResponse<
  T extends Record<string | number, any>,
  Media extends MediaType = MediaType,
> = GetResponseContent<T, Media, ErrorStatus>;

// Generic TS utils

/** Find first match of multiple keys */
type FilterKeys<Obj, Matchers> = Obj[keyof Obj & Matchers];

/** Return any `[string]/[string]` media type (important because openapi-fetch allows any content response, not just JSON-like) */
type MediaType = `${string}/${string}`;

/** Helper to get the required keys of an object. If no keys are required, will be `undefined` with strictNullChecks enabled, else `never` */
type RequiredKeysOfHelper<T> = {
  // biome-ignore lint/complexity/noBannedTypes: `{}` is necessary here
  [K in keyof T]: {} extends Pick<T, K> ? never : K;
}[keyof T];
/** Get the required keys of an object, or `never` if no keys are required */
type RequiredKeysOf<T> = RequiredKeysOfHelper<T> extends undefined ? never : RequiredKeysOfHelper<T>;

/** Options for each client instance */
interface ClientOptions extends Omit<RequestInit, "headers"> {
  /** set the common root URL for all API requests */
  baseUrl?: string;
  /** custom fetch (defaults to globalThis.fetch) */
  fetch?: (input: Request) => Promise<Response>;
  /** custom Request (defaults to globalThis.Request) */
  Request?: typeof Request;
  /** global querySerializer */
  querySerializer?: QuerySerializer<unknown> | QuerySerializerOptions;
  /** global bodySerializer */
  bodySerializer?: BodySerializer<unknown>;
  headers?: HeadersOptions;
  /** RequestInit extension object to pass as 2nd argument to fetch when supported (defaults to undefined) */
  requestInitExt?: Record<string, unknown>;
}

type HeadersOptions =
  | Required<RequestInit>["headers"]
  | Record<string, string | number | boolean | (string | number | boolean)[] | null | undefined>;

type QuerySerializer<T> = (
  query: T extends { parameters: any } ? NonNullable<T["parameters"]["query"]> : Record<string, unknown>,
) => string;

/** @see https://swagger.io/docs/specification/serialization/#query */
type QuerySerializerOptions = {
  /** Set serialization for arrays. @see https://swagger.io/docs/specification/serialization/#query */
  array?: {
    /** default: "form" */
    style: "form" | "spaceDelimited" | "pipeDelimited";
    /** default: true */
    explode: boolean;
  };
  /** Set serialization for objects. @see https://swagger.io/docs/specification/serialization/#query */
  object?: {
    /** default: "deepObject" */
    style: "form" | "deepObject";
    /** default: true */
    explode: boolean;
  };
  /**
   * The `allowReserved` keyword specifies whether the reserved characters
   * `:/?#[]@!$&'()*+,;=` in parameter values are allowed to be sent as they
   * are, or should be percent-encoded. By default, allowReserved is `false`,
   * and reserved characters are percent-encoded.
   * @see https://swagger.io/docs/specification/serialization/#query
   */
  allowReserved?: boolean;
};

type BodySerializer<T> = (body: OperationRequestBodyContent<T>) => any;

type BodyType<T = unknown> = {
  json: T;
  text: Awaited<ReturnType<Response["text"]>>;
  blob: Awaited<ReturnType<Response["blob"]>>;
  arrayBuffer: Awaited<ReturnType<Response["arrayBuffer"]>>;
  stream: Response["body"];
};
type ParseAs = keyof BodyType;
type ParseAsResponse<T, Options> = Options extends {
  parseAs: ParseAs;
}
  ? BodyType<T>[Options["parseAs"]]
  : T;

interface DefaultParamsOption {
  params?: {
    query?: Record<string, unknown>;
  };
}

type ParamsOption<T> = T extends {
  parameters: any;
}
  ? RequiredKeysOf<T["parameters"]> extends never
    ? { params?: T["parameters"] }
    : { params: T["parameters"] }
  : DefaultParamsOption;

type RequestBodyOption<T> = OperationRequestBodyContent<T> extends never
  ? { body?: never }
  : IsOperationRequestBodyOptional<T> extends true
    ? { body?: OperationRequestBodyContent<T> }
    : { body: OperationRequestBodyContent<T> };

type FetchOptions<T> = RequestOptions<T> & Omit<RequestInit, "body" | "headers">;

type FetchResponse<T extends Record<string | number, any>, Options, Media extends MediaType> =
  | {
      data: ParseAsResponse<SuccessResponse<ResponseObjectMap<T>, Media>, Options>;
      error?: never;
      response: Response;
    }
  | {
      data?: never;
      error: ErrorResponse<ResponseObjectMap<T>, Media>;
      response: Response;
    };

type RequestOptions<T> = ParamsOption<T> &
  RequestBodyOption<T> & {
    baseUrl?: string;
    querySerializer?: QuerySerializer<T> | QuerySerializerOptions;
    bodySerializer?: BodySerializer<T>;
    parseAs?: ParseAs;
    fetch?: ClientOptions["fetch"];
    headers?: HeadersOptions;
  };

type MergedOptions<T = unknown> = {
  baseUrl: string;
  parseAs: ParseAs;
  querySerializer: QuerySerializer<T>;
  bodySerializer: BodySerializer<T>;
  fetch: typeof globalThis.fetch;
};

interface MiddlewareCallbackParams {
  /** Current Request object */
  request: Request;
  /** The original OpenAPI schema path (including curly braces) */
  readonly schemaPath: string;
  /** OpenAPI parameters as provided from openapi-fetch */
  readonly params: {
    query?: Record<string, unknown>;
    header?: Record<string, unknown>;
    path?: Record<string, unknown>;
    cookie?: Record<string, unknown>;
  };
  /** Unique ID for this request */
  readonly id: string;
  /** createClient options (read-only) */
  readonly options: MergedOptions;
}

type MiddlewareOnRequest = (
  options: MiddlewareCallbackParams,
) => void | Request | Response | undefined | Promise<Request | Response | undefined | void>;
type MiddlewareOnResponse = (
  options: MiddlewareCallbackParams & { response: Response },
) => void | Response | undefined | Promise<Response | undefined | void>;
type MiddlewareOnError = (
  options: MiddlewareCallbackParams & { error: unknown },
) => void | Response | Error | Promise<void | Response | Error>;

type Middleware =
  | {
      onRequest: MiddlewareOnRequest;
      onResponse?: MiddlewareOnResponse;
      onError?: MiddlewareOnError;
    }
  | {
      onRequest?: MiddlewareOnRequest;
      onResponse: MiddlewareOnResponse;
      onError?: MiddlewareOnError;
    }
  | {
      onRequest?: MiddlewareOnRequest;
      onResponse?: MiddlewareOnResponse;
      onError: MiddlewareOnError;
    };

/** This type helper makes the 2nd function param required if params/requestBody are required; otherwise, optional */
type MaybeOptionalInit<Params, Location extends keyof Params> = RequiredKeysOf<
  FetchOptions<FilterKeys<Params, Location>>
> extends never
  ? FetchOptions<FilterKeys<Params, Location>> | undefined
  : FetchOptions<FilterKeys<Params, Location>>;

// The final init param to accept.
// - Determines if the param is optional or not.
// - Performs arbitrary [key: string] addition.
// Note: the addition MUST happen after all the inference happens (otherwise TS can’t infer if init is required or not).
type InitParam<Init> = RequiredKeysOf<Init> extends never
  ? [(Init & { [key: string]: unknown })?]
  : [Init & { [key: string]: unknown }];

type ClientMethod<
  Paths extends Record<string, Record<HttpMethod, {}>>,
  Method extends HttpMethod,
  Media extends MediaType,
> = <Path extends PathsWithMethod<Paths, Method>, Init extends MaybeOptionalInit<Paths[Path], Method>>(
  url: Path,
  ...init: InitParam<Init>
) => Promise<FetchResponse<Paths[Path][Method], Init, Media>>;

type ClientRequestMethod<Paths extends Record<string, Record<HttpMethod, {}>>, Media extends MediaType> = <
  Method extends HttpMethod,
  Path extends PathsWithMethod<Paths, Method>,
  Init extends MaybeOptionalInit<Paths[Path], Method>,
>(
  method: Method,
  url: Path,
  ...init: InitParam<Init>
) => Promise<FetchResponse<Paths[Path][Method], Init, Media>>;

interface Client<Paths extends {}, Media extends MediaType = MediaType> {
  request: ClientRequestMethod<Paths, Media>;
  /** Call a GET endpoint */
  GET: ClientMethod<Paths, "get", Media>;
  /** Call a PUT endpoint */
  PUT: ClientMethod<Paths, "put", Media>;
  /** Call a POST endpoint */
  POST: ClientMethod<Paths, "post", Media>;
  /** Call a DELETE endpoint */
  DELETE: ClientMethod<Paths, "delete", Media>;
  /** Call a OPTIONS endpoint */
  OPTIONS: ClientMethod<Paths, "options", Media>;
  /** Call a HEAD endpoint */
  HEAD: ClientMethod<Paths, "head", Media>;
  /** Call a PATCH endpoint */
  PATCH: ClientMethod<Paths, "patch", Media>;
  /** Call a TRACE endpoint */
  TRACE: ClientMethod<Paths, "trace", Media>;
  /** Register middleware */
  use(...middleware: Middleware[]): void;
  /** Unregister middleware */
  eject(...middleware: Middleware[]): void;
}

declare function createClient$2<Paths extends {}, Media extends MediaType = MediaType>(
  clientOptions?: ClientOptions,
): Client<Paths, Media>;

/**
 * Creates an instance of the OpenAPI client.
 * @returns {ReturnType<typeof OpenApiCreateClient>} - The configured OpenAPI client.
 * @example
 * import type { paths } from "./openapi.d.ts" // Types generated by `buildSchema` function from `@backan/builder`
 *
 * // create client
 * const client = createClient<paths>( {
 *   baseUrl : 'http://localhost:1312/',
 * } )
 *
 * // example of call
 * const response = await client.GET( '/random/child', {
 *   params : { query : { value : 'myValue', }, },
 * } )
 *
 * console.log( response )
 */
declare const createClient$1: typeof createClient$2;

interface paths {
    "/size": {
        parameters: {
            query?: never;
            header?: never;
            path?: never;
            cookie?: never;
        };
        /** Get Package size data */
        get: {
            parameters: {
                query: {
                    /** @description Set the input */
                    input: string;
                };
                header?: never;
                path?: never;
                cookie?: never;
            };
            requestBody?: never;
            responses: {
                /** @description Successfully fetched data */
                200: {
                    headers: {
                        [name: string]: unknown;
                    };
                    content: {
                        "application/json": Record<string, never>;
                    };
                };
                /** @description Bad request */
                400: {
                    headers: {
                        [name: string]: unknown;
                    };
                    content: {
                        "application/json": {
                            /** @enum {number} */
                            status: 400;
                            id: string;
                            message: string;
                            error: Record<string, never>;
                            help: string;
                        };
                    };
                };
                /** @description Internal Server error */
                500: {
                    headers: {
                        [name: string]: unknown;
                    };
                    content: {
                        "application/json": {
                            /** @enum {number} */
                            status: 500;
                            id: string;
                            message: string;
                            error: Record<string, never>;
                            help: string;
                        };
                    };
                };
            };
        };
        put?: never;
        post?: never;
        delete?: never;
        options?: never;
        head?: never;
        patch?: never;
        trace?: never;
    };
}

type ClientOpts = NonNullable<Parameters<typeof createClient$1>[0]>;
type ClientRes = ReturnType<typeof createClient$1<paths>>;
declare const createClient: (opts?: ClientOpts) => ClientRes;

export { createClient };
export type { ClientOpts, ClientRes, paths };
