/*
 * Copyright (c) All respective contributors to the Peridot Project. All rights reserved.
 * Copyright (c) 2021-2022 Rocky Enterprise Software Foundation, Inc. All rights reserved.
 * Copyright (c) 2021-2022 Ctrl IQ, Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 * this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 *
 * 3. Neither the name of the copyright holder nor the names of its contributors
 * may be used to endorse or promote products derived from this software without
 * specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

import express from 'express';
import httpProxyMiddleware from 'http-proxy-middleware';
import bodyParser from 'body-parser';
import cookieParser from 'cookie-parser';
import helmet from 'helmet';
import webpack from 'webpack';
import webpackDevMiddleware from 'webpack-dev-middleware';
import webpackHotMiddleware from 'webpack-hot-middleware';
import webpackMildCompile from 'webpack-mild-compile';
import expressOidc from 'express-openid-connect';
import history from 'connect-history-api-fallback';
import hbs from 'hbs';
import evilDns from 'evil-dns';
import fs from 'fs';
import dns from 'dns';

const { createProxyMiddleware } = httpProxyMiddleware;

const { auth } = expressOidc;

export default async function(opts) {
  // Create a new app for health checks.
  const appZ = express();
  appZ.get('/healthz', ((req, res) => {
    res.end();
  }));
  appZ.get('/_/healthz', ((req, res) => {
    res.end();
  }));

  const app = express();
  app.use(function(req, res, next) {
    // Including byc-internal-req: 1 should return the Z page
    if (req.header('byc-internal-req') === 'yes') {
      appZ(req, res, next);
    } else {
      next();
    }
  });
  const prod = process.env.NODE_ENV === 'production';

  const port = prod ? (process.env.PORT || 8086) : opts.port;

  opts.secret = process.env.BYC_SECRET;

  // If we're in prod, then a secret has to be present
  if (prod && (!opts.secret || opts.secret.length < 32)) {
    throw 'secret has to be at least 32 characters';
  }

  // Add authentication if not disabled
  if (!opts.disableAuth) {
    console.log(`Using issuer: ${opts.issuerBaseURL}`);
    console.log(`Using clientID: ${opts.clientID}`);
    console.log(`Using baseURL: ${opts.baseURL}`);

    if ((opts.issuerBaseURL.endsWith('.localhost')
        || opts.issuerBaseURL.endsWith('.localhost/'))
      && process.env['BYC_ENV']) {
      const kong = 'kong-proxy.kong.svc.cluster.local';
      const urlObject = new URL(opts.issuerBaseURL);
      console.warn(`Forcing ${urlObject.hostname} to resolve to ${kong}`);
      const lookup = async () => {
        return new Promise((resolve, reject) => {
          // noinspection HttpUrlsUsage
          dns.lookup(kong, { family: 4 }, (err, address, family) => {
            if (err) {
              reject(err);
            }
            resolve(address);
          });
        });
      };
      const internalServiceResolve = await lookup();
      evilDns.add(urlObject.hostname, internalServiceResolve);
      // Disable TLS verification for development
      process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = 0;
    }

    const config = {
      authRequired: process.env['DISABLE_AUTH_ENFORCE']
        ? process.env['DISABLE_AUTH_ENFORCE'] === 'false'
        : !!!opts.disableAuthEnforce,
      // Disable telemetry
      enableTelemetry: false,
      // Use dev secret is none is present (Prod requires a secret so not a security issue)
      secret: opts.secret || 'dev-secret-123',
      // Add BaseURL for callback purposes. This has to be specified in the initial server call
      // The FRONTEND_URL environment variable can override this value in prod.
      baseURL: opts.baseURL,
      // The specific application should supply a dev client ID while prod IDs should be set as an env variable
      clientID: opts.clientID,
      // The specific application should supply a dev secret while prod secrets should be set as an env variable
      clientSecret: opts.clientSecret,
      issuerBaseURL: opts.issuerBaseURL,
      idpLogout: true,
      authorizationParams: {
        response_type: 'code',
        scope: 'openid profile email offline_access'
      },
      session: {
        rolling: true,
        rollingDuration: 86400,
        absoluteDuration: 86400 * 7
      },
      routes: {
        callback: '/oauth2/callback',
        logout: '/oauth2/logout',
        login: '/oauth2/login'
      }
    };

    // If we have a authentication prefix, only force redirect on paths with that prefix
    // Remember, authentication done here is only for simplicity purposes.
    // The authentication token is then passed on to the API.
    // Bypassing auth here doesn't accomplish anything.
    let middlewares = [];

    // If requireEmailSuffix is present, let's validate post callback
    // that the signed in email ends with a suffix in the allowlist
    // Again, a bypass here doesn't accomplish anything.
    let requireEmailSuffix = opts.authOptions?.requireEmailSuffix;
    if (process.env['AUTH_OPTIONS_REQUIRE_EMAIL_SUFFIX']) {
      requireEmailSuffix = process.env['AUTH_OPTIONS_REQUIRE_EMAIL_SUFFIX'].split(
        ',');
    }
    if (requireEmailSuffix) {
      middlewares.push((req, res, next) => {
        const email = req.oidc?.user?.email;
        if (!email) {
          return next('No email found in the user object');
        }
        const suffixes = requireEmailSuffix;
        let isAllowed = false;
        for (const suffix of suffixes) {
          if (email.endsWith(suffix)) {
            isAllowed = true;
            break;
          }
        }

        if (isAllowed) {
          next();
        } else {
          res.redirect(process.env['AUTH_REJECT_REDIRECT_URL']
            ? process.env['AUTH_REJECT_REDIRECT_URL']
            : (opts.authOptions.authRejectRedirectUrl
              || 'https://rockylinux.org'));
        }
      });
    }

    app.use((req, res, next) => {
      try {
        auth(config)(req, res, next);
      } catch (err) {
        next(err);
      }
    }, [middlewares]);
  }

  // Currently in dev, webpack is handling all file serving
  // This is just a placeholder
  let distDir = process.cwd() + '/dist';
  if (prod) {
    // Enable security hardening in prod
    app.use(helmet({
      contentSecurityPolicy: false
    }));

    // Prod expects a certain container structure for all apps
    // Packaging this application with the web base should do
    // all this for you
    const dirs = fs.readdirSync('/home/app/bundle');
    distDir = `/home/app/bundle/${dirs[0]}`;
  }
  app.set('views', distDir);
  app.use(cookieParser());
  app.set('view engine', 'hbs');
  // Use the handlebar engine
  app.engine('hbs', hbs.__express);

  app.use(express.static(distDir));

  if (opts.apis) {
    Object.keys(opts.apis).forEach(x => {
      app.use(x, async (req, res, next) => {
        let authorization = '';

        // If we have an authenticated user, send the token with the request
        if (req.oidc && req.oidc.accessToken) {
          let { access_token, isExpired, refresh } = req.oidc.accessToken;
          if (isExpired()) {
            try {
              ({ access_token } = await refresh());
            } catch (err) {
              res.oidc.logout({ returnTo: '/' });
              return next('User has to re-authenticate');
            }
          }
          authorization = `Bearer ${access_token}`;

          if (!prod) {
            console.log(`Using id token: ${req.oidc.idToken}`);
            console.log(`Using access token: ${access_token}`);
          }
        }

        const rewrite = {};
        rewrite[`^${x}`] = '';

        // Make it possible to override api url using an env variable.
        // Example: /api can be set with URL_API
        // Example 2: /manage/api can be set with URL_MANAGE_API
        const prodEnvName = `URL_${x.substr(1).replace('/',
          '_').toUpperCase()}`;

        const apiUrl = process.env[prodEnvName]
          ? process.env[prodEnvName]
          : prod
            ? opts.apis[x].prodApiUrl
            : opts.apis[x].devApiUrl;

        createProxyMiddleware({
          target: apiUrl,
          changeOrigin: true,
          headers: {
            host: apiUrl,
            authorization
          },
          pathRewrite: rewrite
        })(req, res);
      });
    });
  }

  // Template parameters for values in initial state
  const templateParams = (req) => {
    // If auth is disabled, then either return an empty list
    // or run the templateFunc WITHOUT a user.
    // It's important that apps do not use `user` without validation
    if (opts.disableAuth || !req.oidc) {
      if (opts.templateFunc) {
        return opts.templateFunc();
      }
      return {};
    }

    const { user } = req.oidc;
    if (!user) {
      return {};
    }

    if (opts.templateFunc) {
      return opts.templateFunc(user);
    }

    // Return default values
    const { email, name, picture } = user;
    return {
      email,
      name,
      picture
    };
  };

  if (prod) {
    app.get('/*', (req, res) => {
      // Prod doesn't do hacky shit with the webpack compiler so just add the params
      // to the locals
      res.locals = templateParams(req);

      res.render('index');
    });
  } else {
    // Here comes the hack train
    if (!opts.webpackConfig && opts.webpackPath) {
      opts.webpackConfig = await import(opts.webpackPath);
    }
    // Create a live-reloading dev instance of the app with the given webpack config
    const compiler = webpack(opts.webpackConfig);
    webpackMildCompile(compiler);

    const wdm = webpackDevMiddleware(compiler, {
      publicPath: opts.webpackConfig.output.publicPath
    });

    app.use(history());
    app.use((req, res, next) => {
      // Here we cache the old send function to re-use after we run the HTML through handlebars
      const oldSend = res.send;

      res.send = data => {
        let newData;
        // Check if the request returned a HTML page
        // For SPAs, the only HTML page is the index page
        if (res.get('content-type').indexOf('text/html') !== -1) {
          // Run through handlebars compiler with our template parameters
          newData = hbs.handlebars.compile(data.toString())(
            templateParams(req));
        } else {
          // No new data, just return old data
          newData = data;
        }
        // Re-replace res.send with the old res.send
        res.send = oldSend;
        // Run the old res.send with the new data
        return res.send(newData);
      };

      next();
    });
    // Enable hot reload
    app.use(wdm);
    app.use(webpackHotMiddleware(compiler));
  }

  // Enable JSON bodies. We're forwarding this to the API
  app.use(bodyParser.json());

  console.log(`view app on ${opts.baseURL}`);
  app.listen(port);
}
