import { flow, fromPairs, get } from 'lodash';
import { map } from 'lodash/fp';
import { fromJS } from 'immutable';
import unsentRequest from './unsentRequest';
import APIError from './APIError';

type Formatter = (res: Response) => Promise<string | Blob | unknown>;

export const filterByPropExtension = (extension: string, propName: string) => <T>(arr: T[]) =>
  arr.filter(el =>
    get(el, propName, '').endsWith(extension.startsWith('.') ? extension : `.${extension}`),
  );

const catchFormatErrors = (format: string, formatter: Formatter) => (res: Response) => {
  try {
    return formatter(res);
  } catch (err) {
    throw new Error(
      `Response cannot be parsed into the expected format (${format}): ${err.message}`,
    );
  }
};

const responseFormatters = fromJS({
  json: async (res: Response) => {
    const contentType = res.headers.get('Content-Type') || '';
    if (!contentType.startsWith('application/json') && !contentType.startsWith('text/json')) {
      throw new Error(`${contentType} is not a valid JSON Content-Type`);
    }
    return res.json();
  },
  text: async (res: Response) => res.text(),
  blob: async (res: Response) => res.blob(),
}).mapEntries(([format, formatter]: [string, Formatter]) => [
  format,
  catchFormatErrors(format, formatter),
]);

export const parseResponse = async (
  res: Response,
  { expectingOk = true, format = 'text', apiName = '' },
) => {
  let body;
  try {
    const formatter = responseFormatters.get(format, false);
    if (!formatter) {
      throw new Error(`${format} is not a supported response format.`);
    }
    body = await formatter(res);
  } catch (err) {
    throw new APIError(err.message, res.status, apiName);
  }
  if (expectingOk && !res.ok) {
    const isJSON = format === 'json';
    const message = isJSON ? body.message || body.msg || body.error?.message : body;
    throw new APIError(isJSON && message ? message : body, res.status, apiName);
  }
  return body;
};

export const responseParser = (options: {
  expectingOk?: boolean;
  format: string;
  apiName: string;
}) => (res: Response) => parseResponse(res, options);

export const parseLinkHeader = flow([
  linksString => linksString.split(','),
  map((str: string) => str.trim().split(';')),
  map(([linkStr, keyStr]) => [
    keyStr.match(/rel="(.*?)"/)[1],
    linkStr
      .trim()
      .match(/<(.*?)>/)[1]
      .replace(/\+/g, '%20'),
  ]),
  fromPairs,
]);

export const getAllResponses = async (
  url: string,
  options: { headers?: {} } = {},
  linkHeaderRelName: string,
  nextUrlProcessor: (url: string) => string,
) => {
  const maxResponses = 30;
  let responseCount = 1;

  let req = unsentRequest.fromFetchArguments(url, options);

  const pageResponses = [];

  while (req && responseCount < maxResponses) {
    const pageResponse = await unsentRequest.performRequest(req);
    const linkHeader = pageResponse.headers.get('Link');
    const nextURL = linkHeader && parseLinkHeader(linkHeader)[linkHeaderRelName];

    const { headers = {} } = options;
    req = nextURL && unsentRequest.fromFetchArguments(nextUrlProcessor(nextURL), { headers });
    pageResponses.push(pageResponse);
    responseCount++;
  }

  return pageResponses;
};

export const getPathDepth = (path: string) => {
  const depth = path.split('/').length;
  return depth;
};