// site-platform.js
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0

'use strict';

const path = require('path');
const fs = require('fs');
const glob = require('glob');

const express = require('express');
const cookieParser = require('cookie-parser');
const session = require('express-session');
const RedisSessionStore = require('connect-redis')(session);
const passport = require('passport');

const mongoose = require('mongoose');
const Redis = require('ioredis');
const ioEmitter = require('socket.io-emitter');

const morgan = require('morgan');
const rfs = require('rotating-file-stream');
const compress = require('compression');
const methodOverride = require('method-override');
const marked = require('marked');

const { SiteAsync } = require(path.join(__dirname, 'site-async'));
const { SiteLog } = require(path.join(__dirname, 'site-log'));

module.connectDatabase = async (/*dtp*/) => {
  try {
    module.log.info('connecting to MongoDB database', {
      pid: process.pid,
      host: process.env.MONGODB_HOST,
      database: process.env.MONGODB_DATABASE,
    });
    const mongoConnectionInfo = {
      host: process.env.MONGODB_HOST,
      db: process.env.MONGODB_DATABASE,
      username: encodeURIComponent(process.env.MONGODB_USERNAME),
      password: encodeURIComponent(process.env.MONGODB_PASSWORD),
      options: process.env.MONGODB_OPTIONS || '',
    };
    let mongoConnectUri = `mongodb://${process.env.MONGODB_HOST}/${process.env.MONGODB_DATABASE}`;
    if (process.env.NODE_ENV === 'production'){
      mongoConnectUri = `mongodb://${mongoConnectionInfo.username}:${mongoConnectionInfo.password}@${mongoConnectionInfo.host}/${mongoConnectionInfo.options}`;
    }
    module.db = await mongoose.connect(mongoConnectUri, {
      socketTimeoutMS: 0,
      keepAlive: true,
      keepAliveInitialDelay: 300000,
      dbName: mongoConnectionInfo.db,
    });
    module.log.info('connected to MongoDB');
  } catch (error) {
    module.log.error('failed to connect to database', { error });
    throw error;
  }
};


module.loadModels = async (dtp) => {
  dtp.models = module.models = [ ];
  const modelScripts = glob.sync(path.join(dtp.config.root, 'app', 'models', '*.js'));
  modelScripts.forEach((modelScript) => {
    const model = require(modelScript);
    if (module.models[model.modelName]) {
      module.log.error('model name collision', { name: model.modelName });
      process.exit(-1);
    }
    module.models.push(model);
  });
};

module.exports.resetIndexes = async (dtp) => {
  await SiteAsync.each(dtp.models, module.resetIndex);
};

module.resetIndex = async (model) => {
  return new Promise(async (resolve, reject) => {
    module.log.info('dropping model indexes', { model: model.modelName });
    model.collection.dropIndexes((err) => {
      if (err) {
        return reject(err);
      }
      module.log.info('creating model indexes', { model: model.modelName });
      model.ensureIndexes((err) => {
        if (err) {
          return reject(err);
        }
        return resolve(model);
      });
    });
  });
};

module.connectRedis = async (dtp) => {
  try {
    const options = {
      host: process.env.REDIS_HOST,
      port: parseInt(process.env.REDIS_PORT || '6379', 10),
      password: process.env.REDIS_PASSWORD,
      keyPrefix: process.env.REDIS_KEY_PREFIX,
      lazyConnect: false,
    };
    module.log.info('connecting to Redis', {
      host: options.host,
      port: options.port,
      prefix: options.keyPrefix,
    });

    module.redis = dtp.redis = new Redis(options);
    module.redis.setMaxListeners(64); // prevents warnings/errors with Bull Queue

    module.ioEmitter = dtp.ioEmitter = ioEmitter(module.redis);

    module.log.info('Redis connected');
  } catch (error) {
    module.log.error('failed to connect to Redis', error);
    throw error;
  }
};

module.getRedisKeys = (pattern) => {
  return new Promise((resolve, reject) => {
    return module.redis.keys(pattern, (err, response) => {
      if (err) {
        return reject(err);
      }
      return resolve(response);
    });
  });
};

module.loadServices = async (dtp) => {
  dtp.services = module.services = { };
  const scripts = glob.sync(path.join(dtp.config.root, 'app', 'services', '*.js'));
  const inits = [ ];
  await SiteAsync.each(scripts, async (script) => {
    const service = await require(script);
    module.services[service.name] = service.create(dtp);
    module.services[service.name].__dtp_service_name = service.name;
    inits.push(module.services[service.name]);
  });
  await SiteAsync.each(inits, async (service) => {
    await service.start();
  });
};

module.loadControllers = async (dtp) => {
  const scripts = glob.sync(path.join(dtp.config.root, 'app', 'controllers', '*.js'));
  const inits = [ ];

  dtp.controllers = { };

  await SiteAsync.each(scripts, async (script) => {
    const controller = await require(script);
    controller.instance = await controller.create(dtp);
    
    dtp.controllers[controller.name] = controller;

    inits.push(controller);
  });

  await SiteAsync.each(inits, async (controller) => {
    if (controller.isHome) {
      return; // must run last
    }
    await controller.instance.start();
  });

  /*
   * Start the Home controller
   */
  await dtp.controllers.home.instance.start();

  /*
   * Default error handler
   */
  module.log.info('registering ExpressJS error handler');
  dtp.app.use((error, req, res, next) => { // jshint ignore:line
    res.locals.errorCode = error.statusCode || error.status || 500;
    module.log.error('ExpressJS error', { url: req.url, error });
    res.status(res.locals.errorCode).render('error', {
      message: error.message,
      error,
      errorCode: res.locals.errorCode,
    });
    // return next(error);
  });
};

module.exports.startPlatform = async (dtp) => {
  try {

    module.log = new SiteLog(module, dtp.config.component);
    dtp.config.jobQueues = require(path.join(dtp.config.root, 'config', 'job-queues'));

    await module.connectDatabase(dtp);
    await module.connectRedis(dtp);

    await module.loadModels(dtp);
    SiteLog.setModel(mongoose.model('Log'));

    await module.loadServices(dtp);

    module.log.info(`Digital Telepresence Platform v${dtp.pkg.version} started for [${dtp.pkg.name}]`);
  } catch (error) {
    module.log.error('platform failed to start', { error });
    return;
  }
};

module.exports.startWebServer = async (dtp) => {
  const IS_PRODUCTION = (process.env.NODE_ENV === 'production');

  dtp.app = module.app = express();

  module.app.set('views', path.join(dtp.config.root, 'app', 'views'));
  module.app.set('view engine', 'pug');
  module.app.set('x-powered-by', false);

  /*
   * Expose useful modules and information
   */
  module.app.locals.DTP_SCRIPT_DEBUG = (process.env.NODE_ENV === 'local');
  module.app.locals.env = process.env;
  module.app.locals.dtp = dtp;
  module.app.locals.pkg = require(path.join(dtp.config.root, 'package.json'));
  module.app.locals.mongoose = require('mongoose');
  module.app.locals.moment = require('moment');
  module.app.locals.numeral = require('numeral');
  module.app.locals.phoneNumberJS = require('libphonenumber-js');
  module.app.locals.anchorme = require('anchorme').default;
  module.app.locals.hljs = require('highlight.js');
  
  /*
   * Set up the protected markdown renderer that will refuse to process links and images
   * for security reasons.
   */
  var markedRenderer = new marked.Renderer();
  markedRenderer.link = (href, title, text) => { return text; };
  markedRenderer.image = (href, title, text) => { return text; };
  marked.setOptions({ renderer: markedRenderer });
  module.app.locals.marked = marked;

  /*
   * HTTP request logging
   */
  if (process.env.DTP_LOG_FILE === 'enabled') {
    const httpLogStream = rfs.createStream(process.env.DTP_LOG_FILE_NAME_HTTP || 'dtp-sites-access.log', {
      interval: '1d',
      path: process.env.DTP_LOG_FILE_PATH || '/tmp',
      compress: 'gzip',
    });
    module.app.use(morgan(process.env.DTP_LOG_HTTP_FORMAT || 'combined', { stream: httpLogStream }));
  }

  function cacheOneDay (req, res, next) {
    res.set('Cache-Control', 's-maxage=86400');
    return next();
  }

  /*
   * Static file services (project)
   */
  module.app.use(express.static(path.join(dtp.config.root, 'client')));
  module.app.use('/dist', cacheOneDay, express.static(path.join(dtp.config.root, 'dist')));
  module.app.use('/img', cacheOneDay, express.static(path.join(dtp.config.root, 'client', 'img')));

  /*
   * Static file services (vendor)
   */

  module.app.use('/uikit/images', cacheOneDay, express.static(path.join(dtp.config.root, 'node_modules', 'uikit', 'src', 'images')));
  module.app.use('/uikit', cacheOneDay, express.static(path.join(dtp.config.root, 'node_modules', 'uikit', 'dist')));
  module.app.use('/chart.js', cacheOneDay, express.static(path.join(dtp.config.root, 'node_modules', 'chart.js', 'dist')));
  module.app.use('/chartjs-adapter-moment', cacheOneDay, express.static(path.join(dtp.config.root, 'node_modules', 'chartjs-adapter-moment', 'dist')));
  module.app.use('/pretty-checkbox', cacheOneDay, express.static(path.join(dtp.config.root, 'node_modules', 'pretty-checkbox', 'dist')));

  module.app.use('/fontawesome', cacheOneDay, express.static(path.join(dtp.config.root, 'node_modules', '@fortawesome', 'fontawesome-free')));
  module.app.use('/moment', cacheOneDay, express.static(path.join(dtp.config.root, 'node_modules', 'moment', 'min')));
  module.app.use('/numeral', cacheOneDay, express.static(path.join(dtp.config.root, 'node_modules', 'numeral', 'min')));

  module.app.use('/cropperjs', cacheOneDay, express.static(path.join(dtp.config.root, 'node_modules', 'cropperjs', 'dist')));
  module.app.use('/tinymce', cacheOneDay, express.static(path.join(dtp.config.root, 'node_modules', 'tinymce')));
  module.app.use('/highlight.js', cacheOneDay, express.static(path.join(dtp.config.root, 'node_modules', 'highlight.js')));

  module.app.use('/mpegts', cacheOneDay, express.static(path.join(dtp.config.root, 'node_modules', 'mpegts.js', 'dist')));

  /*
   * ExpressJS middleware
   */
  module.app.use(express.json({ }));
  module.app.use(express.urlencoded({ extended: true }));
  module.app.use(cookieParser());
  module.app.use(compress());
  module.app.use(methodOverride());

  /*
   * Express sessions
   */

  module.log.info('initializing redis session store');
  var sessionStore = new RedisSessionStore({ client: module.redis });

  const SESSION_DURATION = 1000 * 60 * 60 * 24 * 30; // 30 days
  module.sessionConfig = {
    name: `dtp:${process.env.DTP_SITE_DOMAIN_KEY}.${process.env.NODE_ENV}`,
    secret: process.env.HTTP_SESSION_SECRET,
    resave: true,
    proxy: IS_PRODUCTION || (process.env.HTTP_SESSION_TRUST_PROXY === 'enabled'),
    saveUninitialized: true,
    cookie: {
      domain: process.env.DTP_SITE_DOMAIN_KEY,
      path: '/',
      httpOnly: true,
      secure: true,
      sameSite: process.env.HTTP_COOKIE_SAMESITE || false,
      expires: SESSION_DURATION,
    },
    store: sessionStore,
  };
  module.log.info('configuring session handler', {
    domain: module.sessionConfig.cookie.domain,
    httpOnly: module.sessionConfig.cookie.httpOnly,
    secure: module.sessionConfig.cookie.secure,
    sameSite: module.sessionConfig.cookie.sameSite,
    expires: module.sessionConfig.cookie.expires,
  });
  if (module.sessionConfig.proxy) {
    module.log.info('session will be trusting first proxy');
    module.app.set('trust proxy', true);
    module.sessionConfig.cookie.secure = true;
  }
  module.app.use(session(module.sessionConfig));

  /*
   * PassportJS setup
   */
  module.log.info('initializting PassportJS');
  module.app.use(passport.initialize());
  module.app.use(passport.session());

  module.services.oauth2.registerPassport();
  module.app.use(module.services.session.middleware());
  module.app.use(module.services.userNotification.middleware({ withNotifications: false }));

  /*
   * Application logic middleware
   */
  module.app.use(async (req, res, next) => {
    const { cache: cacheService } = dtp.services;
    try {
      res.locals.request = req;

      const settingsKey = `settings:${dtp.config.site.domainKey}:site`;
      res.locals.site = Object.assign({ }, dtp.config.site);

      const settings = await cacheService.getObject(settingsKey);
      if (settings) {
        res.locals.site = Object.assign(res.locals.site, settings);
      }
      return next();
    } catch (error) {
      module.log.error('failed to populate general request data', { error });
      return next(error);
    }
  });

  /*
   * Call out to application to register their custom middleware at the right
   * point in the processing chain.
   */
  module.log.debug('typeof dtp.config.registerMiddleware', { type: (typeof dtp.config.registerMiddleware) });
  if (dtp.config && (typeof dtp.config.registerMiddleware === 'function')) {
    module.log.info('registering custom application middleware');
    await dtp.config.registerMiddleware(dtp, module.app);
  }

  /*
   * System Init
   */
  try {
    dtp.services.oauth2.attachRoutes(module.app);
    await module.loadControllers(dtp);
  } catch (error) {
    module.log.error('failed to initialize application controller', { error });
    return;
  }

  module.app.use(async (err, req, res, next) => { // jshint ignore:line
    var errorCode = err.status || err.statusCode || err.code || 500;
    module.log.error('HTTP error', { error: err });
    res.status(errorCode).render('error', {
      message: err.message,
      error: err,
      title: 'error'
    });
  });

  if (process.env.HTTP_ENABLE === 'enabled') {
    if (process.env.HTTP_REDIRECT_SSL === 'enabled') {
      await module.createSslRedirectApp(dtp);
      await module.createHttpServer(dtp, module.redirectApp);
    } else {
      await module.createHttpServer(dtp, module.app);
    }
  }
  if (process.env.HTTPS_ENABLE === 'enabled') {
    await module.createHttpsServer(dtp, module.app);
  }

  // prefer to attach Socket.io to the HTTPS server and fall back to HTTP
  await module.createSocketServer(dtp, module.https || module.http);

  if (module.http) {
    await module.startHttpServer(dtp, module.http, dtp.config.http);
  }
  if (module.https) {
    await module.startHttpServer(dtp, module.https, dtp.config.https);
  }

  module.log.info(`${dtp.config.component.name} platform online`, {
    http: dtp.config.http.port,
    https: dtp.config.https.port,
  });
};

module.createHttpServer = async (dtp, app) => {
  module.log.info('creating HTTP server');
  module.http = require('http').createServer(app);
};

module.createHttpsServer = async (dtp, app) => {
  const httpsOptions = {
    cert: await fs.promises.readFile(
      process.env.HTTPS_SSL_CRT || path.join(dtp.config.root, 'ssl', 'dtp-webapp.crt')
    ),
    key: await fs.promises.readFile(
      process.env.HTTPS_SSL_KEY || path.join(dtp.config.root, 'ssl', 'dtp-webapp.key')
    ),
  };

  module.log.info('creating HTTPS server');
  module.https = require('https').createServer(httpsOptions, app);

  return module.https;
};

module.createSslRedirectApp = async (/* dtp */) => {
  module.log.info('creating HTTP SSL redirect app');

  module.redirectApp = express();

  module.redirectApp.use((req, res) => {
    module.log.info('redirecting to SSL', { host: req.host, url: req.url });
    res.redirect(`https://${process.env.DTP_SITE_DOMAIN}${req.url}`);
  });

  return module.redirectApp;
};

module.startHttpServer = async (dtp, server, config) => {
  return new Promise((resolve, reject) => {
    module.log.info('starting HTTP server', { port: config.port, bind: config.address });
    server.listen(config.port, config.address, (err) => {
      if (err) {
        return reject(err);
      }
      return resolve();
    });
  });
};

module.createSocketServer = async (dtp, http) => {
  module.log.info('creating Socket.io server');
  const { SiteIoServer } = require(path.join(__dirname, 'site-ioserver'));

  module.io = new SiteIoServer(dtp);
  await module.io.start(http);

  return module.io;
};

module.exports.shutdown = async ( ) => {
  module.log.info('platform shutting down');
};
