import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { syncAction } from 'store/utils/actions';
import app from 'store/app';
import moment from 'moment';
import { gql } from '@apollo/client';
import auth from 'store/auth';
import { config, zeus } from 'config/general';
import { extractErrorCode, getErrorTitle, getErrorMessage } from 'store/utils/errors';
import localeContent from 'config/content';
import { StoreState } from 'store/rootReducer';
import cloneDeep from 'lodash/cloneDeep';
import pick from 'lodash/pick';
import get from 'lodash/get';
import { deepOmit } from 'store/utils/helper';
import client from 'store/rootApollo';

const { AllTypesProps, Zeus, $ } = zeus;

interface ApolloCallOptions {
  noToken?: boolean;
  noHeaders?: boolean;
  noDisconnect: boolean;
  noFeedback?: boolean;
  noLoading?: boolean;

  callback?: Function;
  callbackError?: Function;
  callbackSuccess?: Function;
}
interface GenericQueryInterface {
  objectReturn: string | string[];
  name: string | string[];
  actions?: object;
  actionRes?: Function;
  model: object | object[];
  search?: object;
  Z?: object;
  client?: ApolloClient;
  log?: boolean;
  nullable?: boolean;
  filterLanguage?: boolean;
}

const useApolloCall = () => {
  const lg = useSelector((state: StoreState) => state.content.lg);
  const content = useSelector((state: StoreState) => state.content.raw);
  const session = useSelector((state: StoreState) => state.session.data);
  const appHeaders = useSelector((state: StoreState) => state.app.headers);

  const dispatch = useDispatch();

  const dialogSuccess = syncAction(dispatch, app.actions.dialog.success);
  const dialogError = syncAction(dispatch, app.actions.dialog.error);

  const loaderShow = syncAction(dispatch, app.actions.loader.show);
  const loaderHide = syncAction(dispatch, app.actions.loader.hide);

  const logout = syncAction(dispatch, auth.actions.logout.result.action);

  const apolloDataTreatment = useCallback((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;
  }, []);

  const handleError = useCallback(
    (response, options: ApolloCallOptions, requestData) => {
      if (typeof options.callbackError === 'function') {
        options.callbackError(response);
      }

      if (options.actionFailure) {
        options.actionFailure(response, 'rejected', requestData);
      }
      if (options.actionRes) {
        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 (typeof 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) {
          dialogError({
            data: config.authenticationError || localeContent.modal?.authenticationError,
          });

          logout();
          return errorData;
        }
      }

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

      if (!options.noFeedback && __BROWSER__) {
        dialogError({
          title: errorTitle,
          data: errorMessage || errorData,
        });
      }

      return errorData;
    },
    [dialogError],
  );

  const graphQLQuery = async (query, context, options) => {
    let apolloClient;
    if (options.client) apolloClient = options.client;
    else apolloClient = client.getInstance();

    try {
      const res = await apolloClient.query({
        query,
        variables: options.variables || options.data,
        context,
        fetchPolicy: options.fetchPolicy || 'no-cache',
        errorPolicy: options.errorPolicy || 'all',
      });

      return res;
    } catch (e) {
      return handleError(e, options);
    }
  };

  const graphQLMutation = async (mutation, context, options) => {
    let apolloClient;
    if (options.client) apolloClient = options.client;
    else apolloClient = client.getInstance();

    try {
      const res = await apolloClient.mutate({
        mutation,
        variables: options.variables || options.data,
        context,
        fetchPolicy: options.fetchPolicy || 'no-cache',
        errorPolicy: options.errorPolicy || 'all',
      });

      return res;
    } catch (e) {
      return handleError(e, options);
    }
  };

  const apolloCall = useCallback(async (options: ApolloCallOptions) => {
    if (!options.noLoading) {
      loaderShow(options.loaderInfo || null);
    }
    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)
        ) {
          // TODO : firebase
        }
      }
      // BE refresh token
      else if (
        session.expiresIn &&
        session.creationDate &&
        session.refreshToken &&
        moment(session.creationDate)
          .add(session.expiresIn, 'seconds')
          .isBefore(now)
      ) {
        // TODO : refresh token
      }
    }

    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 (!options.noAppHeaders) {
      if (appHeaders) context.headers = { ...appHeaders, ...context.headers };
    }

    if (options && 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);
      response = await graphQLQuery(options.query, context, options);
    }

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

    if (!options.noLoading) {
      loaderHide();
    }

    if (!response?.data) {
      return response;
    }

    const data = response.data;
    // Success handling
    if (data) {
      let result;
      if (options.nullable) {
        if (get(data, options.name) === null) result = null;
        else result = get(data, options.name) || data;
      } else {
        result = (data && options.name && get(data, options.name)) || 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;
        options.actionRes(result, 'fulfilled', requestData);
      }

      if (typeof options.callback === 'function') {
        options.callback(result);
      }

      if (!options.noSuccess && !options.noFeedback && ((data && data.message) || options.success)) {
        if (typeof options.success !== 'string') {
          dialogSuccess({
            title: options.success?.title,
            data: options.success?.message || (data && data.message),
          });
        } else {
          dialogSuccess({
            data: options.success || (data && data.message),
          });
        }
      }

      return result;
    }

    return response;
  }, []);

  const filterCurrentLanguage = useCallback(inputModel => {
    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 genericQuery = useCallback(async options => {
    const { name, actions, action, search = {}, model, Z = Zeus, log = false, filterLanguage = false, selection, omit } = options;

    if (!name) {
      console.warn('Please provide name and either actions or actionRes');
      return null;
    }

    const actionRequest = syncAction(dispatch, action?.request?.action || actions?.[name]?.request?.action);

    actionRequest();

    const payload = {};

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

    let finalModel = model;
    // Allow to select only a part of the given model
    if (selection && model && !Array.isArray(model)) {
      finalModel = pick(model, selection);
    }

    // Allow to omit a part of the given model
    if (omit && model && !Array.isArray(model)) {
      finalModel = deepOmit(model, omit);
    }

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

          let queryModel = Array.isArray(finalModel) ? finalModel[i] : finalModel;
          if (filterLanguage) queryModel = filterCurrentLanguage(queryModel);

          const querySearch = Array.isArray(search) ? search[i] : search;
          queryObj[query] = queryModel ? [querySearch, queryModel] : [querySearch];
        }
      } else {
        const queryModel = filterLanguage ? filterCurrentLanguage(finalModel) : finalModel;
        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 = syncAction(dispatch, action?.result?.action || actions[name]?.result.action);

      payload.actionRes = actionResult;

      return await apolloCall({
        ...options,
        ...payload,
      });
    } catch (e) {
      console.log('ERROR zeus query', name, e);
      return null;
    }
  }, []);

  const genericMutation = useCallback(async options => {
    const {
      name,
      actions,
      action,
      model,
      treatmentOut = data => deepOmit(data, ['__typename', 'get.*']),
      inputs = {},
      Z = Zeus,
      filterLanguage = false,
      selection,
      omit,
      log = false,
    } = options;

    if (!name) {
      console.warn('Please provide  name and either actions or actionRes');
      return null;
    }

    const actionRequest = syncAction(dispatch, action?.request?.action || actions?.[name]?.request?.action);

    actionRequest();

    const payload = {};

    let finalModel = model;
    // Allow to select only a part of the model
    if (selection && model && !Array.isArray(model)) {
      finalModel = pick(model, selection);
    }

    // Allow to omit a part of the given model
    if (omit && model && !Array.isArray(model)) {
      finalModel = deepOmit(model, omit);
    }

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

      const queryModel = filterLanguage ? filterCurrentLanguage(finalModel, lg) : finalModel;

      const zeusObj = {
        [name]: queryModel ? [inputData, queryModel] : [inputData],
      };

      if (log) console.log('mutation obj', zeusObj);

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

      if (!zeusMutation) return null;

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

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

      const actionResult = syncAction(dispatch, action?.result?.action || actions[name]?.result.action);
      payload.actionRes = actionResult;

      return await apolloCall({
        ...options,
        ...payload,
      });
    } catch (e) {
      console.warn('ERROR mutation ', name, e);
      return null;
    }
  });

  return { handleError, genericQuery, genericMutation };
};

export default useApolloCall;
