import { call, select, put, takeLatest, takeEvery } from 'redux-saga/effects';
import get from 'lodash/get';
import moment from 'moment';

import { content } from 'config/content';
import { config, zeus } from 'config/general';

import client from 'store/rootApollo';
import app from 'store/app';

import { refreshToken } from 'store/auth/sagas';

import contentSelectors from 'store/content/selectors';
import authSelectors from 'store/auth/selectors';
import appSelectors from 'store/app/selectors';

import { refreshIdToken } from 'store/utils/firebase';

import gql from 'graphql-tag';

import { StoreAction } from 'store/utils/actions';

import { handleApolloError, getErrorMessage, getErrorTitle } from './errors';
import { deepOmit, getContent } from './helper';

const { AllTypesProps, Zeus, $ } = zeus;

function apolloDataTreatment(model, data) {
  const simpleType = ['String', 'enum', 'DateTime', 'Float', 'Boolean', 'Int', 'ID'];

  const result = {};
  const keys = Object.keys(data);

  for (let index = 0; index < keys.length; index++) {
    const key = keys[index];
    const value = data[key];
    const modelType = model[key];

    if (!value && (!modelType || (modelType && !modelType.required))) {
      continue;
    } else if (!value && modelType.required) {
      if (modelType.type === 'Boolean') {
        result[key] = value;
      } else {
        console.warn(key, 'is required in model', model);
        continue;
      }
    }

    if (modelType) {
      if (simpleType.includes(modelType.type)) {
        result[key] = value;
      } else if (simpleType.includes(AllTypesProps[modelType.type])) {
        result[key] = value;
      } else if (modelType.array) {
        result[key] = [];
        value?.forEach(el => {
          const elem = apolloDataTreatment(AllTypesProps[modelType.type], el);
          result[key].push(elem);
        });
      } else {
        result[key] = apolloDataTreatment(AllTypesProps[modelType.type], value);
      }
    }
  }

  return result;
}

export function* apolloCall(options) {
  if (!options.noLoading) {
    yield put(app.actions.loader.show.action(options.loaderInfo || null));
  }

  let session = yield select(authSelectors.getSession);

  if (!options.noToken && session) {
    const now = moment();
    // Frontend Firebase
    if (!session.refreshToken && session?.expirationTime && session?.issuedAtTime) {
      const diff = moment(session.expirationTime).diff(moment(session.issuedAtTime), 'seconds');
      if (
        diff &&
        moment(session.creationDate)
          .add(diff, 'seconds')
          .isBefore(now)
      ) {
        yield call(refreshIdToken);
        session = yield select(authSelectors.getSession);
      }
    }
    // BE refresh token
    else if (
      session.expiresIn &&
      session.creationDate &&
      session.refreshToken &&
      moment(session.creationDate)
        .add(session.expiresIn, 'seconds')
        .isBefore(now)
    ) {
      yield call(refreshToken);
      session = yield select(authSelectors.getSession);
    }
  }

  const context = {};
  context.headers = options.headers || {};

  if (session && !options.noToken) {
    options.token = session.token || session.idToken;

    const tokenType = session.token_type || session.type;

    context.headers.Authorization = tokenType
      ? `${tokenType} ${options.token}`
      : `${options.token}`;
  }

  if (options.progress || options.onProgress) {
    context.fetchOptions = context.fetchOptions || {};
    context.fetchOptions.onProgress = options.progress || options.onProgress;
  }

  if (appSelectors && appSelectors.getHeader && !options.noAppHeaders) {
    const headers = yield select(appSelectors.getHeader);

    if (headers) context.headers = { ...context.headers, ...headers };
  }

  function graphQLQuery(query) {
    let apolloClient;
    if (options.client) apolloClient = options.client;
    else apolloClient = client.getInstance();

    return apolloClient
      .query({
        query,
        variables: options.variables || options.data,
        context,
        fetchPolicy: 'no-cache',
        errorPolicy: 'all',
      })
      .then(res => res)
      .catch(handleApolloError);
  }

  if (options && options.mutate && options.inputModel) {
    options.data[options.inputName || 'input'] = apolloDataTreatment(
      AllTypesProps[options.inputModel],
      options.data[options.inputName || 'input'],
    );
  }

  function graphQLMutate(mutate) {
    let apolloClient;
    if (options.client) apolloClient = options.client;
    else apolloClient = client.getInstance();
    // return Promise.resolve(
    return apolloClient
      .mutate({
        mutation: mutate,
        variables: options.variables || options.data,
        context,
        fetchPolicy: 'no-cache',
        errorPolicy: 'all',
      })
      .then(res => res)
      .catch(handleApolloError);
  }

  const requestData = options.variables || options.data || {};
  requestData.objectReturn = options?.name || options?.objectReturn || null;
  let response;

  if (options.query) {
    // Do not remove, it helps for debug
    console.log('graph call', 'QUERY', options.name || options.objectReturn);
    response = yield call(graphQLQuery, options.query);
  }

  if (options.mutate || options.mutation) {
    // Do not remove, it helps for debug
    console.log('graph call', 'MUTATE', options.name || options.objectReturn);
    response = yield call(graphQLMutate, options.mutate || options.mutation);
  }

  if (!options.noLoading) {
    yield put(app.actions.loader.hide.action());
  }

  // Error handling
  if (!response.data) {
    if (typeof options.callbackError === 'function') {
      yield call(options.callbackError, response);
    }
    if (typeof options.reject === 'function') {
      yield call(options.reject, response);
    }
    if (options.actionFailure) {
      yield put(options.actionFailure(response, 'rejected', requestData));
    }
    if (options.actionRes) {
      yield put(options.actionRes(response, 'rejected', requestData));
    }

    let errorData = [];
    if (response.errors && response.errors.length > 0) {
      for (let index = 0; index < response.errors.length; index++) {
        const element = response.errors[index];
        if (element.data && element.data.errorInfo) {
          errorData.push(element.data.errorInfo);
        } else {
          errorData.push(element);
        }
      }
    }

    if (options.treatError && typeof options.treatError === 'function') {
      errorData = options.treatError(errorData);
    }

    const language = yield select(contentSelectors.getLanguage);
    const appContent = yield select(contentSelectors.getContent);

    // title
    const errorTitle = getErrorTitle(errorData[0], language);
    // message
    const errorMessage = getErrorMessage(
      errorData[0],
      appContent,
      language,
      content.general.messages.defaultError,
    );

    console.log('errorMessage', errorMessage);
    if (!options.noFeedback && __BROWSER__) {
      yield put(
        app.actions.dialog.error.action({
          title: errorTitle,
          data: errorMessage || errorData,
        }),
      );
    }
    return errorData;
  }

  const data = response.data;
  // Success handling
  if (data) {
    let result = (data && options.objectReturn && get(data, options.objectReturn)) || data;

    if (options.actionSuccess) {
      result = options.transform ? options.transform(result) : result;
      yield put(options.actionSuccess(result, 'fulfilled', requestData));
    }
    if (options.actionRes) {
      result = options.transform ? options.transform(result) : result;
      yield put(options.actionRes(result, 'fulfilled', requestData));
    }

    if (typeof options.callback === 'function') {
      yield call(options.callback, result);
    }
    if (typeof options.resolve === 'function') {
      yield call(options.resolve, result);
    }

    if (!options.noSuccess && !options.noFeedback && ((data && data.message) || options.success)) {
      if (typeof options.success !== 'string') {
        yield put(
          app.actions.dialog.success.action({
            title: options.success?.title,
            data: options.success?.message || (data && data.message),
          }),
        );
      } else {
        yield put(
          app.actions.dialog.success.action({
            data: options.success || (data && data.message),
          }),
        );
      }
    }

    return result;
  }

  return response;
}

/**
 * Example usage
 *
 * action.payload = action.payload || {};
 * const payload = action.payload;
 *
 * const zeusQuery = Zeus.query({
 *    listSearchMany: models.listModel]
 * });
 *
 * payload.query = gql`
 *  ${zeusQuery}
 * `;
 *
 * yield call(genericApolloSaga('', actions.list.result.action), action);
 *
 */
export const genericApolloSaga = (objectReturn: string, actionRes: Function) =>
  function*(action) {
    const payload = action.payload || {};

    return yield call(apolloCall, {
      ...payload,
      objectReturn,
      query: payload.query,
      mutate: payload.mutation,
      data: payload.data,
      noLoading: payload.noLoading,
      noFeedback: payload.noFeedback,
      success: payload.success,
      callback: payload.callback,
      callbackError: payload.callbackError,
      actionRes,
    });
  };

/**
 * Example usage
 *
 * yield call(
 *    genericQuery({
 *      objectReturn: 'listGeOne',
 *      actions,
 *      search: { id: action?.payload?._id },
 *      model: models.list,
 *    }),
 *   action,
 *  );
 *
 */
export const genericQuery = ({
  objectReturn,
  actions,
  actionRes,
  model,
  search = {},
  Z = Zeus,
}: {
  objectReturn: string;
  actions?: object;
  actionRes?: Function;
  model: object;
  search?: object;
  Z?: object;
}) =>
  function*(action: StoreAction = {}) {
    if ((!actionRes && !actions) || !model || !objectReturn) {
      console.warn('Please provide model, objectReturn and either actions or actionRes');
      return null;
    }
    action.payload = action.payload || {};
    const payload = action.payload;

    try {
      const zeusQuery = Z.query({
        [objectReturn]: [search, model],
      });

      payload.query = gql`
        ${zeusQuery}
      `;

      return yield call(
        genericApolloSaga(objectReturn, actionRes || actions[objectReturn].result.action),
        action,
      );
    } catch (e) {
      console.log('ERROR query', objectReturn, e);
      return null;
    }
  };

/**
 * Example usage
 *
 * yield call(
 *    genericMutation({
 *      objectReturn: 'listAddOne',
 *      actions,
 *      inputs: { id: action?.payload?._id },
 *      model: models.detail,
 *    }),
 *   action,
 *  );
 *
 */
export const genericMutation = ({
  objectReturn,
  actions,
  actionRes,
  model,
  treatmentOut = data => deepOmit(data, ['__typename', 'get.*']),
  inputs = {},
  Z = Zeus,
}: {
  objectReturn: string;
  actions: object;
  actionRes?: Function;
  model: object;
  treatmentOut?: Function;
  inputs?: object;
  Z?: object;
}) =>
  function*(action: StoreAction = {}) {
    if ((!actionRes && !actions) || !model || !objectReturn) {
      console.warn('Please provide model, objectReturn and either actions or actionRes');
      return null;
    }

    action.payload = action.payload || {};
    const payload = action.payload;

    try {
      const inputData = {
        ...inputs,
      };
      if (payload.data) {
        inputData.input = $`input`;
      }
      const zeusMutation = Z.mutation({
        [objectReturn]: [inputData, model],
      });

      if (!zeusMutation) return null;

      payload.mutation = gql`
        ${zeusMutation}
      `;

      if (payload.data) {
        payload.data = {
          input: treatmentOut(payload.data),
        };
      }

      return yield call(
        genericApolloSaga(objectReturn, actionRes || actions[objectReturn].result.action),
        action,
      );
    } catch (e) {
      console.warn('ERROR mutation ', objectReturn, e);
      return null;
    }
  };

// https://github.com/jaydenseric/apollo-upload-client/issues/88
export const customFetch = (url, opts = {}) => {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();

    xhr.open(opts.method || 'get', url);

    const keys = Object.keys(opts.headers);
    for (let i = 0; i < keys.length; i += 1) {
      const key = keys[i];
      const elem = opts.headers[key];
      xhr.setRequestHeader(key, elem);
    }

    xhr.onload = e =>
      resolve({
        ok: true,
        text: () => Promise.resolve(e.target.responseText),
        json: () => Promise.resolve(JSON.parse(e.target.responseText)),
      });

    xhr.onerror = reject;

    if (xhr.upload) xhr.upload.onprogress = opts.progress || opts.onProgress;

    xhr.send(opts.body);
  });
};
