"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
// this must be imported to allow async-functions within an AWS lambda environment
// see: https://github.com/babel/babel/issues/5085
require("@babel/polyfill");
const server_1 = __importDefault(require("react-dom/server"));
const express_1 = __importDefault(require("express"));
const serverless_http_1 = __importDefault(require("serverless-http"));
const connect_sequence_1 = __importDefault(require("connect-sequence"));
// make the styled-components work with server-side rendering
const styled_components_1 = require("styled-components"); // <-- importing ServerStyleSheet
const react_router_1 = require("react-router");
const react_helmet_1 = __importDefault(require("react-helmet"));
const routed_app_1 = require("./routed-app");
const iso_libs_1 = require("../libs/iso-libs");
const attach_data_layer_1 = require("./attach-data-layer");
const attach_storage_1 = require("../components/attach-storage");
const storage_libs_1 = require("../storage/storage-libs");
const libs_1 = require("../libs");
const storage_component_1 = require("../storage/storage-component");
const attach_service_1 = require("../components/attach-service");
const service_libs_1 = require("./service-libs");
const types_1 = __importDefault(require("../types"));
const loader_1 = require("../libs/loader");
const datalayer_integration_1 = require("./datalayer-integration");
const graphql_1 = require("graphql");
const server_libs_1 = require("../libs/server-libs");
//import {loadIsoConfigFromComponent, applyCustomComponents} from "../isolib";
//import { applyAppClientModules } from '../types/client-app-config';
const createServer = (assetsDir, resolvedAssetsPath, isomorphicId, isOffline) => {
    // express is the web-framework that lets us configure the endpoints
    const app = express_1.default();
    // in production, API-Gateway proxy redirects the request to S3
    // serve static files - the async components of the server - only used of localhost
    app.use('/' + assetsDir, express_1.default.static(resolvedAssetsPath));
    // load the IsomorphicComponent
    // we must load it directly from the module here, to enable the aliad of the config_file_path
    const isoConfig = loader_1.loadConfigurationFromModule(require('__CONFIG_FILE_PATH__'), loader_1.INFRASTRUCTURE_MODES.RUNTIME);
    // let's extract it from the root configuration
    const isoApp = loader_1.extractObject(isoConfig, types_1.default.INFRASTRUCTURE_TYPE_CONFIGURATION, isomorphicId);
    // connect the middlewares
    isoApp.middlewares.map(mw => app.use(mw.callback));
    // let's extract it from the root configuration
    const dataLayer = loader_1.extractObject(isoConfig, types_1.default.INFRASTRUCTURE_TYPE_COMPONENT, __DATALAYER_ID__);
    if (dataLayer) {
        //console.log ("Datalayer Active: ", dataLayer.id)
        if (isOffline) {
            //console.log("setOffline!")
            dataLayer.setOffline(true);
        }
        else {
            //console.log("NOT offline!")
            const cors = require('cors');
            const corsOptions = {
                origin(origin, callback) {
                    callback(null, true);
                },
                credentials: true
            };
            app.use(cors(corsOptions));
            // TODO only allow the domains of the app (S3, APIs)
            var allowCrossDomain = function (req, res, next) {
                res.header('Access-Control-Allow-Origin', '*');
                res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS');
                //res.header('Access-Control-Allow-Headers', 'Content-Type,token');
                next();
            };
            app.use(allowCrossDomain);
        }
        app.use('/query', (req, res, next) => __awaiter(void 0, void 0, void 0, function* () {
            //console.log("query-endpoint / offline: ", isOffline);
            const parsedBody = JSON.parse(req.body);
            //console.log(parsedBody)
            yield graphql_1.graphql(dataLayer.getSchema(false), parsedBody.query).then(result_type => {
                const entryQueryName = Object.keys(result_type.data)[0];
                // when the query resolves, we get back 
                //console.log("pre-resolve | found entry: ", entryQueryName)
                new connect_sequence_1.default(req, res, next)
                    .append(...dataLayer.entries.filter(entry => entry.providesQuery(entryQueryName)).map(entry => entry.middleware.callback))
                    .append((req, res, next) => __awaiter(void 0, void 0, void 0, function* () {
                    //console.log("DL-mw: req: ");
                    //const parsedBody = JSON.parse(req.body);
                    //console.log("parsedBody: ", parsedBody);
                    // now we let the schema resolve with data
                    yield graphql_1.graphql(dataLayer.getSchema(true), parsedBody.query).then(result => res.status(200).set({
                        "Access-Control-Allow-Origin": "*",
                        "Access-Control-Allow-Credentials": true // Required for cookies, authorization headers with HTTPS
                    }).send(JSON.stringify(result)), err => res.set({
                        "Access-Control-Allow-Origin": "*",
                        "Access-Control-Allow-Credentials": true // Required for cookies, authorization headers with HTTPS
                    }).status(500).send(err));
                }))
                    .run();
            }, err => res.set({
                "Access-Control-Allow-Origin": "*",
                "Access-Control-Allow-Credentials": true // Required for cookies, authorization headers with HTTPS
            }).status(500).send(err));
        }));
    }
    ;
    // let's extract it from the root configuration
    const storages = libs_1.findComponentRecursively(isoConfig, c => storage_component_1.isStorage(c));
    const reqListFiles = (storageId, prefix, listMode, data, onComplete, onError) => {
        //console.log("listFiles function: ", storageId);
        //console.log("found storages: ", storages);
        const storage = storages.find(storage => storage.id == storageId);
        if (storage) {
            storage_libs_1.listFiles(storageId, prefix, listMode, data, onComplete, onError, storage, isOffline);
        }
        else {
            onError("could not find storage with id " + storageId);
        }
    };
    const reqCallService = (id, args, onResult, onError, config, isOffline = false) => service_libs_1.callService(id, args, onResult, onError, isoConfig, isOffline);
    // flattens the callbacks
    const unpackMiddlewares = (middlewares) => {
        // always returns the list of callbacks
        const cbList = (mw) => Array.isArray(mw.callback) ? mw.callback : [mw.callback];
        return middlewares.reduce((res, mw) => res.concat(...cbList(mw)), 
        // attach the callService-function
        [attach_service_1.serviceAttachService(reqCallService)].concat(
        // when we have a dataLayer, let's attach it to the request
        dataLayer ? [attach_data_layer_1.serviceAttachDataLayer(dataLayer)] : [], 
        //when we have a storage, attach the listFiles-function
        storages ? [attach_storage_1.serviceAttachStorage(reqListFiles)] : []));
    };
    // split the clientApps here and define a function for each of the clientApps, with the right middleware
    isoApp.services.map(service => {
        //console.log("found service: ", service);
        if (service.method.toUpperCase() == "GET") {
            app.get(service.path, ...unpackMiddlewares(service.middlewares));
        }
        else if (service.method.toUpperCase() == "POST") {
            app.post(service.path, ...unpackMiddlewares(service.middlewares));
        }
        else if (service.method.toUpperCase() == "PUT") {
            app.put(service.path, ...unpackMiddlewares(service.middlewares));
        }
        else if (service.method.toUpperCase() == "DELETE") {
            app.delete(service.path, ...unpackMiddlewares(service.middlewares));
        }
        return service;
    });
    //console.log("webApps: ",isoApp.webApps.length, " -> ", isoApp.webApps);
    // split the clientApps here and define a function for each of the clientApps, with the right middleware
    isoApp.webApps
        //.filter(clientApp => clientApp.middlewares !== undefined)
        .map(clientApp => {
        const serveMiddleware = (req, res, next) => serve(req, res, next, clientApp, assetsDir, isoConfig, isOffline);
        const routes = clientApp.routes.filter(route => route.middlewares !== undefined && route.middlewares.length > 0);
        if (clientApp.method.toUpperCase() == "GET") {
            app.get(clientApp.path, ...unpackMiddlewares(clientApp.middlewares));
            routes.forEach(route => app.get(route.path, ...unpackMiddlewares(route.middlewares)));
            app.get(clientApp.path, serveMiddleware);
        }
        else if (clientApp.method.toUpperCase() == "POST") {
            app.post(clientApp.path, ...unpackMiddlewares(clientApp.middlewares));
            routes.forEach(route => app.post(route.path, ...unpackMiddlewares(route.middlewares)));
            app.post(clientApp.path, serveMiddleware);
        }
        else if (clientApp.method.toUpperCase() == "PUT") {
            app.put(clientApp.path, ...unpackMiddlewares(clientApp.middlewares));
            routes.forEach(route => app.put(route.path, ...unpackMiddlewares(route.middlewares)));
            app.put(clientApp.path, serveMiddleware);
        }
        else if (clientApp.method.toUpperCase() == "DELETE") {
            app.delete(clientApp.path, ...unpackMiddlewares(clientApp.middlewares));
            routes.forEach(route => app.delete(route.path, ...unpackMiddlewares(route.middlewares)));
            app.delete(clientApp.path, serveMiddleware);
        }
        return clientApp;
    });
    return app;
};
function serve(req, res, next, clientApp, assetsDir, isoConfig, isOffline) {
    return __awaiter(this, void 0, void 0, function* () {
        //console.log("serve - isOffline: ", isOffline)
        //TODO use try catch depending on the environment
        //try {
        //context is used by react router, empty by default
        let context = {};
        const basename = iso_libs_1.getBasename();
        // creating the stylesheet
        const sheet = new styled_components_1.ServerStyleSheet();
        const parsedUrl = req.url.indexOf("?") >= 0 ? req.url.substring(0, req.url.indexOf("?")) : req.url;
        //console.log("parsedUrl: ", parsedUrl);
        ////////// TODO refactor
        var foundPath = undefined;
        // match request url to our React Router paths and grab the path-params
        let matchResult = clientApp.routes.find(({ path, exact }) => {
            //console.log("match path: ", new RegExp(path))
            foundPath = react_router_1.matchPath(parsedUrl, {
                path: new RegExp(path),
                exact: exact,
                strict: false
            });
            //console.log("foundPath: ", foundPath)
            return foundPath;
        }) || {};
        let { path } = matchResult;
        //console.log("found: ", foundPath);
        //console.log("server: path params: ", foundPath ? foundPath.params : "---");
        //console.log("url: ", req.url);
        const routePath = foundPath ? ((typeof foundPath.path === 'string' || foundPath.path instanceof String) ? (foundPath.path.indexOf("/:") > 0 ?
            foundPath.path.substring(0, foundPath.path.indexOf("/:")) :
            foundPath.path) : req.url) : "";
        //console.log("routePath: ", routePath);
        ////////// END OF REFACTORING required
        //console.log("app data layer id: ", clientApp.dataLayerId);
        const serverState = {};
        const setServerValue = (id, value, isInitial = false) => {
            if (!serverState[id] || !isInitial) {
                serverState[id] = value;
                //console.log("set >>", id, "<< -> ", value);
            }
        };
        const getIsomorphicState = () => {
            //console.log("serverState: ", serverState);
            return `window.__ISOMORPHICSTATE__ = ${JSON.stringify(serverState)}`;
        };
        const fConnectWithDataLayer = clientApp.dataLayerId !== undefined ?
            datalayer_integration_1.connectWithDataLayer(clientApp.dataLayerId, req, isOffline) :
            function (app) {
                return __awaiter(this, void 0, void 0, function* () {
                    //console.log("default dummy data layer")
                    return { connectedApp: app, getState: () => "" };
                });
            };
        /*const serverApp = await new Promise((resolve, reject) => {
    
        });*/
        var renderList = [];
        const addToRenderList = (fRenderSsr, hashValue) => {
            //console.log("add to Render: ", hashValue);
            renderList = renderList.concat([{ fRenderSsr: fRenderSsr, hashValue: hashValue }]);
        };
        const completeSSR = (htmlData, getState, renderListResults) => {
            // getting all the tags from the sheet
            const styles = sheet.getStyleTags();
            //render helmet data aka meta data in <head></head>
            const helmetData = react_helmet_1.default.renderStatic();
            const fRender = clientApp.renderHtmlPage ? clientApp.renderHtmlPage : renderHtmlPage;
            // render a page with the state and return it in the response
            res.status(200).send(fRender(htmlData, styles, getState(), getIsomorphicState(), `window.__RENDERLISTSTATE__ = ${JSON.stringify(renderListResults)}`, helmetData, basename, req.url, clientApp, assetsDir)).end();
        };
        function renderApp(apolloState, oldServerState, oldStorageState, htmlData, getApolloState, newStorageState) {
            return __awaiter(this, void 0, void 0, function* () {
                const tempApolloState = getApolloState();
                const tempServerState = Object.assign({}, serverState);
                /*console.log("APOLLO STATE BEFORE", apolloState, "\nAPOLLO STATE AFTER ", tempApolloState);
                console.log("SERVER STATE BEFORE", oldServerState, "\nSERVER STATE AFTER ", serverState);
                console.log("STORAGE STATE BEFORE", oldStorageState, "\nSTORAGE STATE AFTER ", newStorageState);
                */
                if (JSON.stringify(apolloState) !== JSON.stringify(tempApolloState) ||
                    JSON.stringify(oldServerState) !== JSON.stringify(serverState) ||
                    (oldStorageState && oldStorageState.length !== newStorageState.length)) {
                    //console.log("--------- need to render --------");
                    // create the app and connect it with the DataAbstractionLayer
                    yield fConnectWithDataLayer(routed_app_1.createServerApp(clientApp.routes, clientApp.redirects, basename, req.url, context, req, res, require('infrastructure-components').getAuthCallback(isoConfig, clientApp.authenticationId), setServerValue, addToRenderList, isoConfig, isOffline, newStorageState, serverState)).then(({ connectedApp, getState }) => __awaiter(this, void 0, void 0, function* () {
                        //console.log("renderList before: ", renderList)
                        // collect the styles from the connected appsheet.collectStyles(
                        const newHtmlData = server_1.default.renderToString(connectedApp);
                        const addToStorage = yield Promise.all(renderList.filter((item, index, arr) => {
                            const c = arr.map(item => item.hashValue);
                            return index === c.indexOf(item.hashValue);
                        }).map(({ fRenderSsr, hashValue }) => new Promise((resolve, reject) => {
                            return fRenderSsr(({ data, files }) => {
                                //console.log("resolved: ", files);
                                resolve({
                                    hashValue: hashValue,
                                    data: data,
                                    files: files,
                                });
                            }, err => {
                                //console.log("rejected: ", err);
                                reject(err);
                            });
                        })));
                        //console.log("renderList after: ", renderList)
                        renderList.length = 0;
                        //console.log("addToStorage: ", addToStorage);
                        //completeSSR(newHtmlData, getState, storageState);
                        yield renderApp(tempApolloState, tempServerState, newStorageState, newHtmlData, getState, newStorageState.concat(addToStorage));
                    }));
                }
                else {
                    //console.log("----- no difference, return ------");
                    completeSSR(htmlData, getApolloState, newStorageState);
                }
                ;
            });
        }
        ;
        yield renderApp({}, serverState, undefined, "", () => "", []);
        /*
        // create the app and connect it with the DataAbstractionLayer
        await fConnectWithDataLayer(
            createServerApp(
                clientApp.routes,
                clientApp.redirects,
                basename,
                req.url,
                context,
                req,
                require('infrastructure-components').getAuthCallback(isoConfig, clientApp.authenticationId),
                setServerValue,
                addToRenderList,
                isoConfig,
                isOffline,
                undefined,
                undefined
            )
        ).then(async ({connectedApp, getState}) => {
    
            //console.log("resolved... renderList: ", renderList);
    
            // collect the styles from the connected appsheet.collectStyles(
            const htmlData = ReactDOMServer.renderToString(connectedApp);
            //console.log(htmlData);
    
            const renderListResults = await Promise.all(
                renderList.filter((item, index, arr)=> {
                    const c = arr.map(item=> item.hashValue);
                    return  index === c.indexOf(item.hashValue)
                }).map(({fRenderSsr, hashValue}) => new Promise((resolve, reject) => {
                    return fRenderSsr(
                        (data, files)=>{
                            //console.log("resolved: ", files);
                            resolve({
                                hashValue: hashValue,
                                data: data,
                                files: files,
                            })
                        }, err=>{
                            //console.log("rejected: ", err);
                            reject(err);
                        }
                    )
                }))
            );
    
    
            if (renderListResults && renderListResults.length > 0) {
                //console.log("need to rerender! got renderListResults: ", renderListResults);
    
                await renderApp ({}, serverState, renderListResults, htmlData, getState);
    
            } else {
                completeSSR(htmlData, getState, renderListResults);
            }
    
        });*/
    });
}
/**
 *
 * This functions puts together the whole Html
 *
 * - style as collected from the styled-components
 * - head-meta data: from helmet
 * - data state: from the DAL
 * - basename: to let the client know
 *
 * The html loads the script from the path where we find the assets. This is part of the config, thus load it
 * using `require('../config').pathToAssets(process.env.STAGE_PATH)`
 *
 * The script loading the app.bundle.js uses the window location in order to find out whether there is a slash
 *
 * //TODO the app.bundle.js depends on the name "app". Paramterize this!
 *
 * //TODO: apply the same base style as the client does
 *
 * when we are in a sub-route, e.g. when we have path parameters, we need to add ../ to the path to the assets
 *
 * Routing to the Assets
 *
 * entered url | basename==/ |  basename==/dev
 * ---------------------------------------------
 * (none)           /               /dev
 * /                /               /dev
 * (dev)/           /               /
 * (dev)/route      /               /
 * (dev)/route/     ../             ../
 * (dev)/route/:var ../             ../
 * TODO what happens with more than one path parameter?
 *
 *
 * @param host the host of the request
 * @param html
 * @param styles
 * @param preloadedState the state in form of a script
 * @param helmet
 */
function renderHtmlPage(html, styles, preloadedState, isomorphicState, renderListResults, helmet, basename, routePath, clientApp, assetsDir) {
    //<link rel="icon" href="/assets/favicon.ico" type="image/ico" />
    //console.log(preloadedState);
    const path = require('path');
    const calcBase = () => {
        return path.join(basename, routePath);
    };
    //For each"/" in the entered path after the basename, we need to add "../" to the assets-path
    //when there is a basename, it must be added
    //console.log("calcBase: ", calcBase());
    return `<!doctype html>
    <html>
      <head>
        <meta charset="utf-8" />
        ${helmet.title.toString()}
        ${helmet.meta.toString()}
        ${helmet.link.toString()}
        <meta name="viewport" content="width=device-width, initial-scale=1.0"> 
        ${styles}
        <style>
            html, body, #root {
                display: block;
                margin: 0px;
            }
         </style>
      </head>
      <body>
        <div id="root">${html.trim()}</div>
        <script>
          ${preloadedState}
          ${isomorphicState}
          ${renderListResults}
          window.__BASENAME__ = "${basename}";
        </script>
        <script>
            var loadscript = document.createElement('script');
            function getPath() {
                const basePath = ${basename !== "/" ? "window.location.pathname.startsWith(\"" + basename + "\") ? \"\": \"/\" " : "\"\""};
                const routePath= "${routePath !== "/" ? routePath : ""}";
                const pre = window.location.pathname.startsWith(basePath+routePath+"/") ? ".." : "";
                return pre+"${path.join(basename, assetsDir, server_libs_1.getClientFilename(clientApp.id))}";
                
            }
            
            loadscript.setAttribute('src',getPath());
            document.body.appendChild(loadscript);            
        </script>
        
      </body>
    </html>`;
}
// we're exporting the handler-function as default, must match the sls-config!
//export default (assetsDir, resolvedAssetsPath) => serverless(createServer(assetsDir, resolvedAssetsPath));
/*
const serverIndexPath = path.join(serverPath, "index.js");
fs.writeFileSync(serverIndexPath, `const lib = require ('./server');
const server = lib.default('${ssrConfig.assetsPath}', '${resolveAssetsPath(ssrConfig)}');
exports.default = server;*/
// these variables are replaced during compilation
exports.default = serverless_http_1.default(createServer(__ASSETS_PATH__, __RESOLVED_ASSETS_PATH__, __ISOMORPHIC_ID__, __ISOFFLINE__));
//# sourceMappingURL=server.js.map