import { defer, EMPTY, from, of } from 'rxjs';
import { catchError, finalize } from 'rxjs/operators';

import { USER_UPDATED_HEADER } from '../user/constants';
import { NO_PERMISSION_MESSAGE, TOKEN_EXPIRED_MESSAGE } from '@/modules/@common/envs/constants';
import { TYPE_ERROR } from '@/modules/@common/notify/constants';
import { GRAPHQL_URI } from '@/modules/@common/request/constants';
import { TOKEN_COOKIE_NAME, TOKEN_COOKIE_VALUE } from '@/modules/@common/token/constants';

import { getUserUpdatedTime } from '../user/redux/selectors';
import { getUserDataAction } from '../user/redux/slices';
import { featureFlags } from '@/modules/@common/helpers/featureFlags';
import { requestEventAction, responseEventAction } from '@/modules/@common/request/redux/slices';
import { getToken, getTokenCookieName, getTokenCookieValue } from '@/modules/@common/token/helpers';
import { logOutEventAction } from '@/modules/@common/token/redux/slices';
import { showNotificationAction } from '@/modules/layout/redux/slices';

import store from '@/modules/@common/store';

export const parseErrorMessage = (errorObject, mapError) => {
  if (errorObject?.response) {
    const dataErrorMessage = errorObject.response.data?.errors?.[0]?.message;

    if (dataErrorMessage) {
      return [dataErrorMessage];
    }

    const errors = Array.isArray(errorObject.response.details)
      ? errorObject.response.details.map((detail) => {
          const mainMessagePart = errorObject.response.message ?? errorObject.message;
          const source = detail.source ? `${detail.source}: ` : '';

          return `${mainMessagePart} ${source}${detail.message}`;
        })
      : [
          errorObject.response.message ||
            errorObject.response.error?.message ||
            errorObject.response.error ||
            errorObject.message,
        ];

    return mapError ? mapError(errors) : errors;
  }

  return errorObject?.message ?? 'Connection error';
};

const FAILED_TO_FETCH_MESSAGE = 'Failed to fetch';

const tokenExpiredReg = new RegExp(`${TOKEN_EXPIRED_MESSAGE}`, 'i');
const noPermissionsReg = new RegExp(`${NO_PERMISSION_MESSAGE}`, 'i');
const failedToFetchReg = new RegExp(`${FAILED_TO_FETCH_MESSAGE}`, 'i');

const checkUserDataUpdates = (userLastUpdateHeader) => {
  const state = store.current.getState();

  const userUpdatedDate = getUserUpdatedTime(state);

  if (userUpdatedDate !== userLastUpdateHeader) {
    store.current.dispatch(getUserDataAction());
  }
};

export const processResponse = (cSettings, data = {}) => {
  const settings = {
    showNotificationMessage: true,
    mapError: (error) => error,
    ...cSettings,
  };

  store.current.dispatch(responseEventAction());

  const errors = data.errors ?? [];

  if (errors.length) {
    const tokenWasExpired = errors.some(({ message }) => tokenExpiredReg.test(message));
    const noPermission = errors.some(({ message }) => noPermissionsReg.test(message));
    const failedToFetch = errors.some(({ message }) => failedToFetchReg.test(message));

    if (tokenWasExpired || failedToFetch || errors[0]?.statusCode === 401 || errors[0]?.response?.status === 401) {
      store.current.dispatch(
        showNotificationAction({
          key: TOKEN_EXPIRED_MESSAGE,
          type: TYPE_ERROR,
          message: TOKEN_EXPIRED_MESSAGE,
        }),
      );

      store.current.dispatch(logOutEventAction());
    } else if (settings.showNotificationMessage) {
      if (noPermission) {
        store.current.dispatch(
          showNotificationAction({
            key: NO_PERMISSION_MESSAGE,
            type: TYPE_ERROR,
            message: NO_PERMISSION_MESSAGE,
          }),
        );
      } else {
        const errorMessage = parseErrorMessage(errors[0], settings.mapError);

        store.current.dispatch(
          showNotificationAction({
            key: errorMessage,
            type: TYPE_ERROR,
            message: errorMessage,
          }),
        );
      }
    }
  }

  return {
    data: data?.data,
    ...data,
    errors,
  };
};

export const uploadS3 = (uploadUrl, file, cSettings) => {
  let controller = null;

  return defer(() => {
    if (controller) {
      controller.abort();
    }

    controller = new AbortController();

    return fetch(uploadUrl, {
      method: 'PUT',
      body: file,
      signal: controller.signal,
    })
      .then(async (response) => {
        if (!response.ok) {
          const errorData = await response.json().catch(() => ({
            errors: [`HTTP error ${response.status}: ${response.statusText}`],
          }));
          return processResponse(cSettings, errorData);
        }

        const data = await response.json().catch(() => ({})); // Handle empty response
        return processResponse(cSettings, data);
      })
      .catch((error) => {
        const errorData = { errors: [error.message || error] };
        return processResponse(cSettings, errorData);
      });
  });
};

export const gqlRequest = (body, settings) => {
  let controller = null;

  const bodyRequest = {
    query: body.query.trim(),
    variables: body.variables ?? {},
  };

  return defer(() => {
    if (controller) {
      controller.abort();
    }

    controller = new AbortController();

    store.current.dispatch(requestEventAction());

    let headers = {
      Accept: 'application/json',
      Authorization: getToken() || '',
      'Content-Type': 'application/json',
    };

    const tokenCookieName = getTokenCookieName();
    const tokenCookieValue = getTokenCookieValue();

    if (tokenCookieName && tokenCookieValue) {
      headers = {
        ...headers,
        [TOKEN_COOKIE_NAME]: tokenCookieName,
        [TOKEN_COOKIE_VALUE]: tokenCookieValue,
      };
    }

    if (featureFlags.logs) {
      headers.aclog = true;
      // eslint-disable-next-line no-console
      console.log('Request log: ', bodyRequest);
    }

    const promise = fetch(GRAPHQL_URI, {
      method: 'POST',
      mode: 'cors',
      headers,
      credentials: 'include',
      body: JSON.stringify(bodyRequest),
      signal: controller.signal,
      redirect: 'error',
    }).then(async (response) => {
      const data = (await response.json()) || {};

      const userLastUpdate = response.headers.get(USER_UPDATED_HEADER);

      if (userLastUpdate) {
        checkUserDataUpdates(userLastUpdate);
      }

      return processResponse(settings, data);
    });

    // Convert to observable; cancel on unsubscribe; hide AbortError
    return from(promise).pipe(
      catchError((error) => {
        if (error?.name === 'AbortError') return EMPTY; // silent cancel
        return of(processResponse(settings, { errors: [error] }));
      }),
      finalize(() => {
        // ensure inflight request is aborted on switchMap unsubscribe
        if (controller && !controller.signal.aborted) controller.abort('switchMap unsubscribe');
      }),
    );
  });
};
