import { AttachmentParserResult, CreateHeaderOptions, ErrorMapperResult } from './types';
import _ from 'lodash';
import { stringify } from 'query-string';
import { GetListParams, GetManyParams } from 'ra-core';

function createHeadersFromOptions(options: CreateHeaderOptions): Headers | any {
  const requestHeaders =
    options.headers ||
    new Headers({
      Accept: 'application/json'
    });
  if (
    !requestHeaders.has('Content-Type') &&
    !(options && (!options.method || options.method === 'GET')) &&
    !(options && options.body && options.body instanceof FormData)
  ) {
    requestHeaders.set('Content-Type', 'application/json');
  }
  if (options.user && options.user && options.user.token) {
    requestHeaders.set('Authorization', options.user.token);
  }
  return requestHeaders;
}

function createFormData(body: any): string {
  return Object.keys(body)
    .map((key) => encodeURIComponent(key) + '=' + encodeURIComponent(body[key]))
    .join('&');
}

function fetchJson(url: string, options: any = {}, HttpErrorClass: any = Error): Promise<any> {
  return fetch(url, { ...options })
    .then((response) =>
      response.text().then((text) => ({
        status: response.status,
        statusText: response.statusText,
        headers: response.headers,
        body: text
      }))
    )
    .then(({ status, headers, body }) => {
      if (status === 401) {
        return Promise.reject(new HttpErrorClass('iam.error.unauthorized', status, {}));
      } else if (status === 403) {
        return Promise.reject(new HttpErrorClass('iam.error.forbidden', status, {}));
      } else if (status === 404) {
        return Promise.reject(new HttpErrorClass('error.not_found', 404, {}));
      }
      let json;
      try {
        json = JSON.parse(body);
      } catch (e) {
        return Promise.reject(new HttpErrorClass('error.invalid_json_response', status, {}));
      }
      if (json?.responseCode === 'error.validation' || json?.responseCode === 'error.generic') {
        const body = {
          errors: createErrorMapper(json?.result)
        };
        return Promise.reject(new HttpErrorClass(json?.responseCode, status, body));
      } else if (json?.responseCode !== undefined && json?.responseCode !== 'ok') {
        return Promise.reject(new HttpErrorClass(json?.responseCode, status, body));
      }
      return Promise.resolve({ status, headers, body, json });
    });
}

function isValidObject(value: any): boolean {
  if (!value) {
    return false;
  }

  const isArray = Array.isArray(value);
  // eslint-disable-next-line no-undef
  const isBuffer = typeof Buffer !== 'undefined' && Buffer.isBuffer(value);
  const isObject = Object.prototype.toString.call(value) === '[object Object]';
  const hasKeys = !!Object.keys(value).length;

  return !isArray && !isBuffer && isObject && hasKeys;
}

function flattenObject(value: any, path: string[] = []): any {
  if (isValidObject(value)) {
    return Object.assign({}, ...Object.keys(value).map((key: string) => flattenObject(value[key], path.concat([key]))));
  } else {
    return path.length ? { [path.join('.')]: value } : value;
  }
}

const queryParameters = stringify;

function createGetQuery(params: GetListParams | GetManyParams | any) {
  const filter = params?.filter || {};
  const page = params?.pagination?.page || 1;
  const perPage = params?.pagination?.perPage || 10;
  const field = params?.sort?.field || 'id';
  const order = params?.sort?.order || 'DESC';
  return {
    filters: Object.keys(filter).reduce((acc: any[], key) => {
      let property = key;
      if (property === 'keyword') {
        return acc;
      }
      let value = filter[key];

      let type = 'eq';
      if (property.includes('__')) {
        const args = property.split('__');
        property = args[0];
        type = args[1];
      } else if (typeof value === 'object') {
        type = Object.keys(value)[0];
        value = value[type];
      }

      return acc.concat([
        {
          property,
          value,
          type
        }
      ]);
    }, []),
    page: page,
    rowsPerPage: perPage,
    keyword: filter?.keyword,
    sorts: [
      {
        property: field,
        descending: 'DESC' === order.toUpperCase()
      }
    ]
  };
}
type ConvertFileToBase64Result = null | { name: string; data: string | ArrayBuffer | null };
function convertFileToBase64(file: any): Promise<ConvertFileToBase64Result> {
  if (!file.rawFile) {
    return Promise.resolve(file);
  }
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.readAsDataURL(file.rawFile);
    reader.onload = () => {
      resolve({
        name: file.rawFile.name,
        data: reader.result
      });
    };
    reader.onerror = reject;
  });
}
async function convertFile(file: any) {
  return file.rawFile
    ? convertFileToBase64(file).then((convertedFile) => ({
        data: convertedFile,
        name: file.rawFile.name,
        size: file.rawFile.size,
        type: file.rawFile.type
      }))
    : Promise.resolve(file);
}

function uuid(): string {
  let d = new Date().getTime();
  if (window.performance && typeof window.performance.now === 'function') {
    d += performance.now(); //use high-precision timer if available
  }
  const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
    const r = (d + Math.random() * 16) % 16 | 0;
    d = Math.floor(d / 16);
    return (c == 'x' ? r : (r & 0x3) | 0x8).toString(16);
  });
  return uuid;
}

function convertFileToPart(item: any, parts: any[]): string {
  let data = item;
  const file = item?.rawFile || item;
  if (file instanceof File) {
    const id = uuid();
    const { name, size, type } = file;
    parts.push({ id, file });
    data = {
      id,
      name,
      size,
      contentType: type,
      action: 'Add'
    };
  }
  return data;
}

function springErrorMapper(errors: any[]): any[] {
  const validationErrors = errors.reduce(
    (errorMap, error) => ({
      ...errorMap,
      [error.property]: error.message
    }),
    {}
  );
  return validationErrors;
}

function createErrorMapper(result: ErrorMapperResult): any | any[] | boolean {
  const errors = result?.errors || [];
  if (errors) {
    const mappedErrors = springErrorMapper(errors);
    return mappedErrors;
  } else {
    return false;
  }
}

function flattenKeys(obj: any, prefix: string = '.'): any {
  const keys = [];

  if (typeof obj === 'object' && obj !== null) {
    for (const key in obj) {
      let newPrefix = prefix + key + '.';
      if (newPrefix.startsWith('.')) {
        newPrefix = newPrefix.substring(1);
      }
      keys.push(...flattenKeys(obj[key], newPrefix));
    }
  } else {
    if (prefix.endsWith('.') && prefix.length > 1) {
      prefix = prefix.substring(0, prefix.length - 1);
    }
    keys.push(prefix);
  }

  return keys;
}

/**
 * Configure a parser capable of working with attachments managed within forms.
 *
 * @param {Array<String>} fields
 * @returns {Function} Returns a parser function that can be used to parse attachments.
 * @example
 * import { createAttachmentsParser } from '@applica-software-guru/crud-client';
 * const parser = createAttachmentsParser();
 */
function createAttachmentsParser() {
  return async (data: any): Promise<AttachmentParserResult> => {
    const parts: any[] = [];
    const regex = new RegExp(/__attachment_*|__file_*|__image_*/);
    const fields = flattenKeys(data).filter((f: string) => regex.test(f));
    for (let i = 0; i < fields.length; i++) {
      const attachment = fields[i];
      const type = regex.exec(attachment);
      if (type && type[0]) {
        const theKey = attachment.replace(type[0], '');
        const value = _.get(data, theKey);
        const isAttachment = type[0] === '__attachment__';

        if (!value) {
          if (!isAttachment) {
            data = _.set(data, `_${theKey}`, null);
          }
        } else {
          const _value = _.get(data, theKey);
          if (Array.isArray(_value)) {
            if (!isAttachment) {
              throw new Error('Array of file/image is not supported, please use Attachment instead.');
            }
            for (let j = 0; j < _value.length; j++) {
              const item = _value[j];
              const file = convertFileToPart(item, parts);
              _value[j] = file;
            }
            _.set(data, theKey, _value);
          } else {
            const file = (
              isAttachment ? await convertFileToPart(_value, parts) : await convertFileToBase64(_value)
            ) as any;
            const alreadyStoredBase64Value = _.get(data, `_${theKey}`);
            _.set(
              data,
              isAttachment ? theKey : `_${theKey}`,
              isAttachment ? file : file.data || alreadyStoredBase64Value
            );
            if (!isAttachment && file?.name) {
              _.set(data, theKey, file.name);
            }
          }
        }
      }
      data = _.omit(data, attachment);
    }

    return { data, parts };
  };
}

export {
  convertFile,
  convertFileToBase64,
  convertFileToPart,
  createAttachmentsParser,
  createErrorMapper,
  createFormData,
  createGetQuery,
  createHeadersFromOptions,
  fetchJson,
  flattenKeys,
  flattenObject,
  queryParameters,
  springErrorMapper,
  uuid
};
