/**
 * Render the page server side
 * @see https://github.com/szomolanyi/MeteorApolloStarter/blob/master/imports/startup/server/ssr.js
 * @see https://github.com/apollographql/GitHunt-React/blob/master/src/server.js
 * @see https://www.apollographql.com/docs/react/features/server-side-rendering.html#renderToStringWithData
 */
import React from 'react';
import ReactDOM from 'react-dom/server';
import { getDataFromTree } from '@apollo/client/react/ssr';

import { runCallbacks } from '../../modules/callbacks';
import { createClient } from './apolloClient';

import Head from './components/Head';
import ApolloState from './components/ApolloState';
import AppGenerator from './components/AppGenerator';
import injectDefaultData from './injectDefaultData';

const makePageRenderer = ({ computeContext }) => {
  // onPageLoad callback
  const renderPage = async sink => {
    const req = sink.request;
    // according to the Apollo doc, client needs to be recreated on every request
    // this avoids caching server side
    const client = await createClient({ req, computeContext });

    // Used by callbacks to handle side effects
    // E.g storing the stylesheet generated by styled-components
    const context = {};

    // TODO: req object does not seem to have been processed by the Express
    // middlewares at this point
    // @see https://github.com/meteor/meteor-feature-requests/issues/174#issuecomment-441047495

    const App = <AppGenerator req={req} apolloClient={client} context={context} />;

    let htmlContent = '';
    try {
      // run user registered callbacks that wraps the React app
      // The wrappers must NOT have any side effect during React tree traversal
      // otherwise SSR may fail
      const WrappedApp = runCallbacks({
        name: 'router.server.wrapper',
        iterator: App,
        properties: { req, context, apolloClient: client },
      });


      // run wrappers that must only me applied during the data collection step
      // eg Material UI theming WITHOUT style generation
      // The wrappers must NOT have any side effect during React tree traversal
      // otherwise SSR may fail
      const DataWrappedApp = runCallbacks({
        name: 'router.server.dataWrapper',
        iterator: WrappedApp,
        properties: { req, context, apolloClient: client },
      });
      // fill apollo store
      // NOTE: we CAN'T use renderToStringWithData on the wrapped app (so with Material UI, styled components etc.), 
      //because react-apollo may trigger style generation while walking the React tree to find query
      await getDataFromTree(DataWrappedApp);

      // run callback related to rendering
      // eg Material UI theming WITH style generation
      // those wrapper can tolerate side effects during React tree traversal (eg className/styles generation)
      const StyledWrappedApp = runCallbacks({
        name: 'router.server.renderWrapper',
        iterator: WrappedApp,
        properties: { req, context, apolloClient: client },
      });
      // equivalent to calling getDataFromTree and then renderToStringWithData
      htmlContent = await ReactDOM.renderToString(StyledWrappedApp);
    } catch (err) {
      // eslint-disable-next-line no-console
      console.error(`Error while server-rendering. date: ${new Date().toString()} url: ${req.url}`); // eslint-disable-line no-console
      // eslint-disable-next-line no-console
      console.error(err);
      // show error in client in dev
      if (Meteor.isDevelopment) {
        htmlContent = `Error while server-rendering: ${err.message}`;
      }
    }

    // TODO: there should be a cleaner way to set this wrapper
    // id must always match the client side start.jsx file
    const wrappedHtmlContent = `<div id="react-app">${htmlContent}</div>`;
    sink.appendToBody(wrappedHtmlContent);
    // TODO: this sounds cleaner but where do we add the <div id="react-app"> ?
    //sink.renderIntoElementById('react-app', content)

    // add headers using helmet
    const head = ReactDOM.renderToString(<Head />);
    sink.appendToHead(head);

    // add complementary data to the HTML (previously done by inject_data)
    const dataToInject = injectDefaultData(req, { responseHeaders: sink.responseHeaders });
    if (dataToInject._injectHtml) {
      sink.appendToHead(dataToInject._injectHtml);
    }


    // add Apollo state, the client will then parse the string
    const initialState = client.extract();
    const serializedApolloState = ReactDOM.renderToString(
      <ApolloState initialState={initialState} />
    );
    sink.appendToBody(serializedApolloState);

    // post render callback
    runCallbacks({
      name: 'router.server.postRender',
      iterator: sink,
      properties: { context },
    });
  };
  return renderPage;
};

export default makePageRenderer;