import React from 'react';
import ReactDOM from 'react-dom';
import axios from 'axios';
import createAuthRefreshInterceptor from 'axios-auth-refresh';
import { ApolloProvider } from '@apollo/react-hooks';
import { ApolloClient } from 'apollo-client';
import { HttpLink } from 'apollo-link-http';
import { onError } from 'apollo-link-error';
import { enableMapSet } from 'immer';
import { ApolloLink, Observable, fromPromise } from 'apollo-link';
import { getAccessToken, getNewToken, setAccessToken } from 'shared/utils/accessToken';
import cache from './App/cache';
import App from './App';

// https://able.bio/AnasT/apollo-graphql-async-access-token-refresh--470t1c8
enableMapSet();

let forward$;
let isRefreshing = false;
let pendingRequests: any = [];

const refreshAuthLogic = (failedRequest: any) =>
  axios.post('/auth/refresh_token', {}, { withCredentials: true }).then(tokenRefreshResponse => {
    setAccessToken(tokenRefreshResponse.data.accessToken);
    failedRequest.response.config.headers.Authorization = `Bearer ${tokenRefreshResponse.data.accessToken}`;
    return Promise.resolve();
  });

createAuthRefreshInterceptor(axios, refreshAuthLogic);

const resolvePendingRequests = () => {
  pendingRequests.map((callback: any) => callback());
  pendingRequests = [];
};

const resolvePromise = (resolve: () => void) => {
  pendingRequests.push(() => resolve());
};

const resetPendingRequests = () => {
  pendingRequests = [];
};

const setRefreshing = (newVal: boolean) => {
  isRefreshing = newVal;
};

const errorLink = onError(({ graphQLErrors, networkError, operation, forward }) => {
  if (graphQLErrors) {
    for (const err of graphQLErrors) {
      if (err.extensions && err.extensions.code) {
        switch (err.extensions.code) {
          case 'UNAUTHENTICATED':
            if (!isRefreshing) {
              setRefreshing(true);
              forward$ = fromPromise(
                getNewToken()
                  .then((response: any) => {
                    setAccessToken(response.accessToken);
                    resolvePendingRequests();
                    return response.accessToken;
                  })
                  .catch(() => {
                    resetPendingRequests();
                    // TODO
                    // Handle token refresh errors e.g clear stored tokens, redirect to login, ...
                    return undefined;
                  })
                  .finally(() => {
                    setRefreshing(false);
                  }),
              ).filter(value => Boolean(value));
            } else {
              forward$ = fromPromise(new Promise(resolvePromise));
            }
            return forward$.flatMap(() => forward(operation));
          default:
          // pass
        }
      }
    }
  }
  if (networkError) {
    console.log(`[Network error]: ${networkError}`); // eslint-disable-line no-console
  }
  return undefined;
});

const requestLink = new ApolloLink(
  (operation, forward) =>
    new Observable((observer: any) => {
      let handle: any;
      Promise.resolve(operation)
        .then((op: any) => {
          const accessToken = getAccessToken();
          if (accessToken) {
            op.setContext({
              headers: {
                Authorization: `Bearer ${accessToken}`,
              },
            });
          }
        })
        .then(() => {
          handle = forward(operation).subscribe({
            next: observer.next.bind(observer),
            error: observer.error.bind(observer),
            complete: observer.complete.bind(observer),
          });
        })
        .catch(observer.error.bind(observer));

      return () => {
        if (handle) {
          handle.unsubscribe();
        }
      };
    }),
);

const client = new ApolloClient({
  link: ApolloLink.from([
    onError(({ graphQLErrors, networkError }) => {
      if (graphQLErrors) {
        graphQLErrors.forEach(
          ({ message, locations, path }) =>
            console.log(`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`), // eslint-disable-line no-console
        );
      }
      if (networkError) {
        console.log(`[Network error]: ${networkError}`); // eslint-disable-line no-console
      }
    }),
    errorLink,
    requestLink,
    new HttpLink({
      uri: '/graphql',
      credentials: 'same-origin',
    }),
  ]),
  cache,
});

ReactDOM.render(
  <ApolloProvider client={client}>
    <App />
  </ApolloProvider>,
  document.getElementById('root'),
);
