import { call, select, put } from 'redux-saga/effects';

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

import client from 'store/rootApollo';

import get from 'lodash/get';
import moment from 'moment';

import gql from 'graphql-tag';

import pick from 'lodash/pick';

import { ApolloClient } from '@apollo/client';
import cloneDeep from 'lodash/cloneDeep';
import { refreshIdToken } from './firebase';
import authActions from '../auth/actions';
import appSelectors from '../app/selectors';
import authSelectors from '../auth/selectors';
import contentSelectors from '../content/selectors';
import { AsyncResponseStructure } from './actions';
import { StoreState } from '../rootReducer';
import app from '../app';
import { handleApolloError, getErrorMessage, getErrorTitle, extractErrorCode } from './errors';
import { deepOmit } from './helper';
import { refreshToken } from '../auth/sagas';

const { AllTypesProps, Zeus, $ } = zeus;

export interface ApiOptions<Payload, Request> {
  callbackSuccess?: () => void;
  resolve?: () => void;
  actionSuccess: AsyncResponseStructure<Payload, Request>;
  callbackError?: () => void;
  reject: () => void;
  actionFailure: AsyncResponseStructure<Payload, Request>;
  actionRes: AsyncResponseStructure<Payload, Request>;
  treatError: (errors: any[]) => object[];
  defaultLanguage?: string;
  noDisconnect?: boolean;
  noFeedback?: boolean;
  noLoading?: boolean;
}

export type GenericMutationInterface<Payload, Request> = ApiOptions<Payload, Request> & {
  objectReturn: string;
  name: string | string[];
  actions: any;
  actionRes?: (...args: any[]) => any;
  model: any;
  treatmentOut?: (...args: any[]) => any;
  inputs?: any;
  Z?: any;
  client?: ApolloClient;
  filterLanguage?: boolean;
};

export type GenericQueryInterface<Payload, Request> = ApiOptions<Payload, Request> & {
  objectReturn: string | string[];
  name: string | string[];
  actions?: any;
  actionRes?: (...args: any[]) => any;
  model: any | object[];
  search?: any;
  Z?: any;
  client?: ApolloClient;
  log?: boolean;
  nullable?: boolean;
  filterLanguage?: boolean;
};

export interface ApolloHelper<Payload, Request> {
  genericMutation: (options: GenericMutationInterface) => void;
  genericQuery: (options: GenericQueryInterface<Payload, Request>) => void;
  apolloCall: (options: ApiOptions<Payload, Request>) => void;
}

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;
}

function* treatmentError(response, options, requestData) {
  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));
  }

  // Construct error data
  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);
  }

  // Handle auto disconnection if the first error return an error code understood as a disconnect error
  if (!options.noDisconnect) {
    // Authentication error
    const disconnectedErrors = [
      ...config.disconnectedErrors,
      // FIREBASE GENERAL ERRORS
      'auth/argument-error',
      // FIREBASE AUTH ERRORS
      // see https://firebase.google.com/docs/reference/js/firebase.auth.Auth#signout
      'auth/invalid-user-token',
      'auth/user-token-expired',
    ];

    const firstErrorCode = extractErrorCode(errorData[0]);

    console.log('first error code', firstErrorCode);

    // Stop if disconnected error
    if (disconnectedErrors.indexOf(firstErrorCode) >= 0) {
      yield put(
        app.actions.dialog.error.action({
          data: config.authenticationError || content.modal?.authenticationError,
        }),
      );

      yield put(authActions.logout.result.action());
      return 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 graphQLCall = (type, graphCall, context, options) => {
  let apolloClient;
  if (options.client) apolloClient = options.client;
  else apolloClient = client.getInstance();

  const baseOptions = {
    variables: options.variables || options.data,
    context,
    fetchPolicy: 'no-cache',
    errorPolicy: 'all',
  };

  const promise =
    type === 'query'
      ? apolloClient.query({
          ...baseOptions,
          query: graphCall,
        })
      : apolloClient.mutate({
          ...baseOptions,
          mutation: graphCall,
        });

  return promise.then(res => res).catch(handleApolloError);
};

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 (
      options.forceRefresh ||
      (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: any = {};
  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?.getHeader && !options.noAppHeaders) {
    const headers = yield select(appSelectors.getHeader);

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

  // global config headers
  if (config.headers) {
    context.headers = { ...config.headers, ...context.headers };
  }

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

  const requestData = cloneDeep(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(graphQLCall, 'query', options.query, context, options);
  }

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

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

  // Error handling
  if (!response?.data) {
    return yield call(treatmentError, response, options, requestData);
  }

  const data = response.data;
  // Success handling
  if (data) {
    let result;
    if (options.nullable) {
      if (get(data, options.objectReturn) === null) result = null;
      else result = get(data, options.objectReturn) || data;
    } else {
      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);
 *
 */
/**
 * @deprecated use genericQuery & genericMutation instead
 * @param objectReturn name of the query / mutation
 * @param actionRes response action (both success and failure with status)
 */
export const genericApolloSaga = (objectReturn: string, actionRes: (...args: any[]) => any) =>
  function*(action) {
    const payload = action.payload || {};

    return yield call(apolloCall, {
      ...payload,
      objectReturn,
      actionRes,
    });
  };

const filterCurrentLanguage = (inputModel, lg) => {
  const outputModel = cloneDeep(inputModel);

  const filterLanguage = model => {
    const keys = Object.keys(model);
    for (let i = 0; i < keys.length; i += 1) {
      const key = keys[i];
      const elem = model[key];

      if (elem[lg]) {
        // we have an translated object
        model[key] = {
          [lg]: true,
        };
        if (lg !== config.defaultLanguage) {
          model[key][config.defaultLanguage] = true;
        }
      } else if (typeof elem === 'object') {
        filterLanguage(elem, lg);
      }
    }
  };

  filterLanguage(outputModel, lg);

  return outputModel;
};

const handleTreatmentModel = (model, payload, filterLanguage, lg) => {
  let queryModel = model;

  if (filterLanguage) queryModel = filterCurrentLanguage(queryModel, lg);

  // Allow to select only a part of the given model
  if (payload?.selection && queryModel) {
    queryModel = pick(queryModel, payload.selection);
  }

  // Allow to omit a part of the given model
  if (payload?.omit && queryModel) {
    queryModel = deepOmit(queryModel, payload.omit);
  }

  return queryModel;
};

/**
 * Example usage
 *
 * yield call(
 *    genericQuery({
 *      name: 'listGeOne',
 *      actions,
 *      search: { id: action?.payload?._id },
 *      model: models.list,
 *    }),
 *   action,
 *  );
 *
 */
export const genericQuery = ({
  // @deprecated : use name instead
  objectReturn,
  name = objectReturn,
  actions,
  actionRes,
  model,
  search = {},
  Z = Zeus,
  client: apolloClient = null,
  log = false,
  filterLanguage = false,
  ...rest
}: GenericQueryInterface<any, any>) =>
  function*(action: any = {}) {
    if ((!actionRes && !actions) || !name) {
      console.warn('Please provide name and either actions or actionRes');
      return null;
    }
    action.payload = action.payload || {};
    const payload = action.payload;

    // Add search to data so that it is accessible in requestData (clone to keep immutability)
    payload.data = cloneDeep(search);

    const lg = yield select((state: StoreState) => state.content.lg);

    const queryObj = {};
    try {
      if (Array.isArray(name)) {
        for (let i = 0; i < name.length; i += 1) {
          const query = name[i];

          let queryModel = Array.isArray(model) ? model[i] : model;
          queryModel = handleTreatmentModel(queryModel, payload, filterLanguage, lg);

          const querySearch = Array.isArray(search) ? search[i] || {} : search || {};
          queryObj[query] = queryModel ? [querySearch, queryModel] : [querySearch];
        }
      } else {
        const queryModel = handleTreatmentModel(model, payload, filterLanguage, lg);

        queryObj[name] = queryModel ? [search, queryModel] : [search];
      }
      if (log) console.log('query obj', queryObj);

      // With zeus v4
      // const zeusQuery = Z('query', zeusObj);
      const zeusQuery = Z.query(queryObj);

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

      const actionResult = actionRes?.result?.action || actionRes || actions[name]?.result.action;

      // override default apollo client
      if (apolloClient) payload.client = apolloClient;

      return yield call(apolloCall, {
        ...rest,
        ...payload,
        objectReturn: name,
        actionRes: actionResult,
      });
    } catch (e) {
      console.log('ERROR query', name, e);
      return null;
    }
  };

/**
 * Example usage
 *
 * yield call(
 *    genericMutation({
 *      name: 'listAddOne',
 *      actions,
 *      inputs: { id: action?.payload?._id },
 *      model: models.detail,
 *    }),
 *   action,
 *  );
 *
 */
export const genericMutation = ({
  // @deprecated : use name instead
  objectReturn,
  name = objectReturn,
  actions,
  actionRes,
  model,
  treatmentOut = data => deepOmit(data, ['__typename', 'get.*']),
  inputs = {},
  Z = Zeus,
  client: apolloClient = null,
  filterLanguage = false,
  log = false,
  ...rest
}: GenericMutationInterface<any, any>) =>
  function*(action: any = {}) {
    if ((!actionRes && !actions) || !name) {
      console.warn('Please provide  name and either actions or actionRes');
      return null;
    }

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

    const lg = yield select((state: StoreState) => state.content.lg);

    try {
      const inputData = {
        ...inputs,
      };
      if (payload.data) {
        inputData.input = $`input`;
      }

      const queryModel = handleTreatmentModel(model, payload, filterLanguage, lg);

      const mutationObj = {
        [name]: queryModel ? [inputData, queryModel] : [inputData],
      };
      if (log) console.log('mutation obj', mutationObj);

      // With zeus v4
      // const zeusMutation = Z('mutation', zeusObj);
      const zeusMutation = Z.mutation(mutationObj);

      if (!zeusMutation) return null;

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

      // Add inputs to data so that it is accessible in requestData
      if (payload.data) {
        payload.data = {
          ...inputs,
          input: treatmentOut(payload.data),
        };
      } else {
        payload.data = inputs;
      }

      const actionResult = actionRes?.result?.action || actionRes || actions[name].result.action;

      // override default apollo client
      if (apolloClient) payload.client = apolloClient;

      return yield call(apolloCall, {
        ...rest,
        ...payload,
        objectReturn: name,
        actionRes: actionResult,
      });
    } catch (e) {
      console.warn('ERROR mutation ', name, e);
      return null;
    }
  };

// https://github.com/jaydenseric/apollo-upload-client/issues/88
export const customFetch = (url, opts: any = {}) => {
  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: any) =>
      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);
  });
};
