/* This file was originally generated by `openapi-typescript-codegen`, but we've customized it and
 * it is no longer regenerated. The key changes:
 *
 *   * Support pre-constructed `FormData` instances, and make `FormData` isomorphic.
 *   * Add request/response logging for all API requests in `request()`.
 */

/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import axios from "axios";
import type { AxiosRequestConfig, AxiosResponse, AxiosInstance } from "axios";
import { AxiosError } from "axios";
import pRetry from "@fullstax/p-retry";

// Manual edit to use our isomorphic `FormData`.
import { FormData } from "lib/isomorphic";

import { ApiError } from "./ApiError";
import type { ApiRequestOptions } from "./ApiRequestOptions";
import type { ApiResult } from "./ApiResult";
import { CancelablePromise } from "./CancelablePromise";
import type { OnCancel } from "./CancelablePromise";
import type { OpenAPIConfig } from "./OpenAPI";

export const isDefined = <T>(
  value: T | null | undefined,
): value is Exclude<T, null | undefined> => {
  return value !== undefined && value !== null;
};

export const isString = (value: any): value is string => {
  return typeof value === "string";
};

export const isStringWithValue = (value: any): value is string => {
  return isString(value) && value !== "";
};

export const isBlob = (value: any): value is Blob => {
  return (
    typeof value === "object" &&
    typeof value.type === "string" &&
    typeof value.stream === "function" &&
    typeof value.arrayBuffer === "function" &&
    typeof value.constructor === "function" &&
    typeof value.constructor.name === "string" &&
    /^(Blob|File)$/.test(value.constructor.name) &&
    /^(Blob|File)$/.test(value[Symbol.toStringTag])
  );
};

export const isFormData = (value: any): value is FormData => {
  return value instanceof FormData;
};

export const isSuccess = (status: number): boolean => {
  return status >= 200 && status < 300;
};

export const base64 = (str: string): string => {
  try {
    return btoa(str);
  } catch (err) {
    // @ts-ignore
    return Buffer.from(str).toString("base64");
  }
};

export const getQueryString = (params: Record<string, any>): string => {
  const qs: string[] = [];

  const append = (key: string, value: any) => {
    qs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
  };

  const process = (key: string, value: any) => {
    if (isDefined(value)) {
      if (Array.isArray(value)) {
        value.forEach((v) => {
          process(key, v);
        });
      } else if (typeof value === "object") {
        Object.entries(value).forEach(([k, v]) => {
          process(`${key}[${k}]`, v);
        });
      } else {
        append(key, value);
      }
    }
  };

  Object.entries(params).forEach(([key, value]) => {
    process(key, value);
  });

  if (qs.length > 0) {
    return `?${qs.join("&")}`;
  }

  return "";
};

const getUrl = (config: OpenAPIConfig, options: ApiRequestOptions): string => {
  const encoder = config.ENCODE_PATH || encodeURI;

  const path = options.url
    .replace("{api-version}", config.VERSION)
    .replace(/{(.*?)}/g, (substring: string, group: string) => {
      if (options.path?.hasOwnProperty(group)) {
        return encoder(String(options.path[group]));
      }
      return substring;
    });

  const url = `${config.BASE}${path}`;
  if (options.query) {
    return `${url}${getQueryString(options.query)}`;
  }
  return url;
};

export const getFormData = (
  options: ApiRequestOptions,
): FormData | undefined => {
  if (options.formData) {
    // This is a manual edit to allow `FormData` to be passed in directly.
    if (options.formData instanceof FormData) {
      return options.formData;
    }

    const formData = new FormData();

    const process = (key: string, value: any) => {
      if (isString(value) || isBlob(value)) {
        formData.append(key, value);
      } else {
        formData.append(key, JSON.stringify(value));
      }
    };

    Object.entries(options.formData)
      .filter(([_, value]) => isDefined(value))
      .forEach(([key, value]) => {
        if (Array.isArray(value)) {
          value.forEach((v) => process(key, v));
        } else {
          process(key, value);
        }
      });

    return formData;
  }
  return undefined;
};

type Resolver<T> = (options: ApiRequestOptions) => Promise<T>;

export const resolve = async <T>(
  options: ApiRequestOptions,
  resolver?: T | Resolver<T>,
): Promise<T | undefined> => {
  if (typeof resolver === "function") {
    return (resolver as Resolver<T>)(options);
  }
  return resolver;
};

export const getHeaders = async (
  config: OpenAPIConfig,
  options: ApiRequestOptions,
  formData?: FormData,
): Promise<Record<string, string>> => {
  const token = await resolve(options, config.TOKEN);
  const username = await resolve(options, config.USERNAME);
  const password = await resolve(options, config.PASSWORD);
  const additionalHeaders = await resolve(options, config.HEADERS);
  // Manual edit to support `FormData` implementations that don't include `getHeaders()`.
  const formHeaders =
    (formData &&
      "getHeaders" in formData &&
      typeof formData?.getHeaders === "function" &&
      formData?.getHeaders()) ||
    {};

  const headers = Object.entries({
    Accept: "application/json",
    ...additionalHeaders,
    ...options.headers,
    ...formHeaders,
  })
    .filter(([_, value]) => isDefined(value))
    .reduce(
      (headers, [key, value]) => ({
        ...headers,
        [key]: String(value),
      }),
      {} as Record<string, string>,
    );

  if (isStringWithValue(token)) {
    headers["Authorization"] = `Bearer ${token}`;
  }

  if (isStringWithValue(username) && isStringWithValue(password)) {
    const credentials = base64(`${username}:${password}`);
    headers["Authorization"] = `Basic ${credentials}`;
  }

  if (options.body) {
    if (options.mediaType) {
      headers["Content-Type"] = options.mediaType;
    } else if (isBlob(options.body)) {
      headers["Content-Type"] = options.body.type || "application/octet-stream";
    } else if (isString(options.body)) {
      headers["Content-Type"] = "text/plain";
    } else if (!isFormData(options.body)) {
      headers["Content-Type"] = "application/json";
    }
  }

  return headers;
};

export const getRequestBody = (options: ApiRequestOptions): any => {
  if (options.body) {
    return options.body;
  }
  return undefined;
};

const shouldRetry = (error: Error): boolean =>
  !!(
    axios.isAxiosError(error) &&
    ((error.response?.status &&
      [502, 503, 504].includes(error.response.status)) ||
      (error.code &&
        [
          // Some of these codes aren't on `AxiosError` for some reason, see:
          //   * https://github.com/axios/axios/issues/4894#issuecomment-2062064639
          "ECONNREFUSED",
          "ECONNRESET",
          "ENOTFOUND",
          "ENETUNREACH",
          AxiosError.ECONNABORTED,
          AxiosError.ERR_NETWORK,
        ].includes(error.code)))
  );

export const sendRequest = async <T>(
  config: OpenAPIConfig,
  options: ApiRequestOptions,
  url: string,
  body: any,
  formData: FormData | undefined,
  headers: Record<string, string>,
  onCancel: OnCancel,
  axiosClient: AxiosInstance,
): Promise<AxiosResponse<T>> => {
  const source = axios.CancelToken.source();

  const requestConfig: AxiosRequestConfig = {
    url,
    headers,
    data: body ?? formData,
    method: options.method,
    withCredentials: config.WITH_CREDENTIALS,
    cancelToken: source.token,
    responseType: options.responseType,
  };

  onCancel(() => source.cancel("The user aborted a request."));

  try {
    if (!config.sindri) {
      return await axiosClient.request(requestConfig);
    }
    return await pRetry(() => axiosClient.request(requestConfig), {
      ...config.sindri.retryOptions,
      onFailedAttempt: (error) => {
        // Don't log anything if we're not going to retry the request, the error will get logged
        // further up the stack in that case.
        if (!shouldRetry(error)) return;

        config.sindri!.logger.debug(
          {
            attemptNumber: error.attemptNumber,
            error: error.message,
            retriesLeft: error.retriesLeft,
          },
          `${options.method} ${url} - Request failed, ` +
            `${error.retriesLeft > 0 ? "retrying" : "aborting..."}...`,
        );
      },
      shouldRetry,
    });
  } catch (error) {
    const axiosError = error as AxiosError<T>;
    if (axiosError.response) {
      return axiosError.response;
    }
    throw error;
  }
};

export const getResponseHeader = (
  response: AxiosResponse<any>,
  responseHeader?: string,
): string | undefined => {
  if (responseHeader) {
    const content = response.headers[responseHeader];
    if (isString(content)) {
      return content;
    }
  }
  return undefined;
};

export const getResponseBody = (response: AxiosResponse<any>): any => {
  if (response.status !== 204) {
    return response.data;
  }
  return undefined;
};

export const catchErrorCodes = (
  options: ApiRequestOptions,
  result: ApiResult,
): void => {
  const errors: Record<number, string> = {
    400: "Bad Request",
    401: "Unauthorized",
    403: "Forbidden",
    404: "Not Found",
    500: "Internal Server Error",
    502: "Bad Gateway",
    503: "Service Unavailable",
    ...options.errors,
  };

  const error = errors[result.status];
  if (error) {
    throw new ApiError(options, result, error);
  }

  if (!result.ok) {
    const errorStatus = result.status ?? "unknown";
    const errorStatusText = result.statusText ?? "unknown";
    const errorBody = (() => {
      try {
        return JSON.stringify(result.body, null, 2);
      } catch (e) {
        return undefined;
      }
    })();

    throw new ApiError(
      options,
      result,
      `Generic Error: status: ${errorStatus}; status text: ${errorStatusText}; body: ${errorBody}`,
    );
  }
};

/**
 * Request method
 * @param config The OpenAPI configuration object
 * @param options The request options from the service
 * @param axiosClient The axios client instance to use
 * @returns CancelablePromise<T>
 * @throws ApiError
 */
export const request = <T>(
  config: OpenAPIConfig,
  options: ApiRequestOptions,
  axiosClient: AxiosInstance = axios,
): CancelablePromise<T> => {
  return new CancelablePromise(async (resolve, reject, onCancel) => {
    // Get a nicely formatted timedelta to display after requests.
    const startTime = Date.now();
    const getElapsedTime = (): string => {
      const ellapsedMilliseconds = Date.now() - startTime;
      if (ellapsedMilliseconds < 1000) {
        return `${ellapsedMilliseconds} ms`;
      }
      const ellapsedSeconds = ellapsedMilliseconds / 1000;
      if (ellapsedSeconds < 60) {
        return `${ellapsedSeconds.toFixed(2)} s`;
      }
      const ellapsedMinutes = ellapsedSeconds / 60;
      if (ellapsedMinutes < 60) {
        return `${ellapsedMinutes.toFixed(2)} m`;
      }
      const ellapsedHours = ellapsedMinutes / 60;
      return `${ellapsedHours.toFixed(2)} h`;
    };

    const url = getUrl(config, options);
    const logPrefix = `${options.method} ${url}`;
    try {
      const formData = getFormData(options);
      const body = getRequestBody(options);
      const headers = await getHeaders(config, options, formData);

      if (!onCancel.isCancelled) {
        config.sindri?.logger.debug(`${logPrefix} requested`);
        const response = await sendRequest<T>(
          config,
          options,
          url,
          body,
          formData,
          headers,
          onCancel,
          axiosClient,
        );
        const responseBody = getResponseBody(response);
        const responseHeader = getResponseHeader(
          response,
          options.responseHeader,
        );

        const result: ApiResult = {
          url,
          ok: isSuccess(response.status),
          status: response.status,
          statusText: response.statusText,
          body: responseHeader ?? responseBody,
        };
        const responseMessage = `${logPrefix} ${response.status} ${
          response.statusText
        } (${getElapsedTime()})`;
        if (!result.body || typeof result.body === "string") {
          config.sindri?.logger.debug(
            `${responseMessage} - ${result.body || "<empty-body>"}`,
          );
        } else if (options.responseType === "stream") {
          config.sindri?.logger.debug(
            `${responseMessage} - <streaming-response>`,
          );
        } else if (options.responseType === "blob") {
          config.sindri?.logger.debug(`${responseMessage} - <blob-response>`);
        } else {
          config.sindri?.logger.debug(result.body, responseMessage);
        }

        catchErrorCodes(options, result);

        resolve(result.body);
      }
    } catch (error) {
      const errorMessage =
        error instanceof Error ? error.message : "Unknown error";
      config.sindri?.logger.debug(
        `${logPrefix} ERROR (${getElapsedTime()}) - ${errorMessage}`,
      );
      reject(error);
    }
  });
};
