import React from 'react';
import { ApolloClient, NormalizedCacheObject } from '@apollo/client';
import { getDataFromTree } from '@apollo/client/react/ssr';
import { IContext } from 'beta/typings/context';
import initApollo from 'beta/utils/apolloClient';
import { getUserFromToken, initAuth, isStaffToken, isValid, parseToken } from 'beta/utils/auth';
import { NextComponentType, NextPageContext } from 'next';
import { AppProps as NextAppProps } from 'next/app';
import { AppTreeType } from 'next/dist/shared/lib/utils';
import { Router } from 'next/router';

const isBrowser = typeof window !== 'undefined';

type NextComponent = NextComponentType<NextPageContext> & { isWhiteListed: boolean };

type NextAppContext = {
  Component: NextComponent;
  AppTree: AppTreeType;
  ctx: NextPageContext;
  router: Router;
};
interface AppContext extends NextAppContext {
  ctx: IContext;
}

interface AppProps extends NextAppProps {
  apolloState: NormalizedCacheObject;
}

export function withApollo(WrappedComponent: any) {
  return class WithApollo extends React.PureComponent<NextAppProps> {
    apolloClient: ApolloClient<NormalizedCacheObject>;

    static displayName = `WithData(${WrappedComponent.displayName})`;

    static async getInitialProps(context: AppContext) {
      const { Component, router, ctx } = context;
      const isWhiteListed = Component?.isWhiteListed ?? false;

      // check for token
      const { token } = parseToken(ctx.req);

      if (
        (!isValid(token) && !isWhiteListed) ||
        (isValid(token) && ctx.query.isStaff === 'true' && !isStaffToken(token)) ||
        (isWhiteListed && ctx.query.isStaff === 'true' && !isStaffToken(token))
      ) {
        initAuth(ctx, ctx.asPath);
        return {};
      }

      const apollo = initApollo(
        {},
        {
          token: () => parseToken(ctx.req).token,
          appContext: ctx,
          pathname: router.asPath,
          isWhiteListed,
        },
      );

      context.ctx.apolloClient = apollo;

      context.ctx.user = getUserFromToken(parseToken(ctx.req).token);

      let appProps = {};

      if (WrappedComponent.getInitialProps) {
        appProps = await WrappedComponent.getInitialProps(context);
      }

      if (ctx.res && ctx.res.writableEnded) {
        // When redirecting, the response is finished.
        // No point in continuing to render
        return {};
      }

      if (!isBrowser) {
        // Run all graphql queries in the component tree
        // and extract the resulting dataisBrowser
        try {
          // Run all GraphQL queries
          await getDataFromTree(
            <WrappedComponent
              {...appProps}
              Component={Component}
              router={router}
              apolloClient={apollo}
            />,
          );
        } catch (error) {
          // Prevent Apollo Client GraphQL errors from crashing SSR.
          // Handle them in components via the data.error prop:
          // https://www.apollographql.com/docs/react/api/react-apollo.html#graphql-query-data-error
          if (error instanceof Error && !error.message.includes('NextRouter was not mounted.')) {
            console.error('Error while running `getDataFromTree`', error); // eslint-disable-line
          }
        }
      }

      // Extract query data from the Apollo's store
      const apolloState = apollo.cache.extract();

      return {
        ...appProps,
        apolloState,
      };
    }

    constructor(props: AppProps) {
      super(props);
      // `getDataFromTree` renders the component first, the client is passed off as a property.
      // After that rendering is done using Next's normal rendering pipeline

      //IsWhiteListed does extist, just down burried in nested props so i'm not changing it for now.
      this.apolloClient = initApollo(props.apolloState, {
        token: () => parseToken().token,
        isWhiteListed: props.Component.isWhiteListed ?? false,
      });
    }

    render() {
      return <WrappedComponent {...this.props} apolloClient={this.apolloClient} />;
    }
  };
}
