'use strict'

//manage https servers
const async = require('async'),
  https = require('https'),
  tls = require('tls'),
  assert = require('assert'),
  crypto = require('crypto'),
  color = require('colorful'),
  certMgr = require('./certMgr'),
  logUtil = require('./log'),
  util = require('./util'),
  wsServerMgr = require('./wsServerMgr'),
  constants = require('constants'),
  asyncTask = require('async-task-mgr');

/**
 * Create an https server
 *
 * @param {object} config
 * @param {number} config.port
 * @param {function} config.handler
 */
function createHttpsSNIServer(port, handler) {
  assert(port && handler, 'invalid param for https SNI server');

  const createSecureContext = tls.createSecureContext || crypto.createSecureContext;
  function SNIPrepareCert(serverName, SNICallback) {
    let keyContent,
      crtContent,
      ctx;
  
    async.series([
      (callback) => {
        certMgr.getCertificate(serverName, (err, key, crt) => {
          if (err) {
            callback(err);
          } else {
            keyContent = key;
            crtContent = crt;
            callback();
          }
        });
      },
      (callback) => {
        try {
          ctx = createSecureContext({
            key: keyContent,
            cert: crtContent
          });
          callback();
        } catch (e) {
          callback(e);
        }
      }
    ], (err) => {
      if (!err) {
        const tipText = 'proxy server for __NAME established'.replace('__NAME', serverName);
        logUtil.printLog(color.yellow(color.bold('[internal https]')) + color.yellow(tipText));
        SNICallback(null, ctx);
      } else {
        logUtil.printLog('err occurred when prepare certs for SNI - ' + err, logUtil.T_ERR);
        logUtil.printLog('err occurred when prepare certs for SNI - ' + err.stack, logUtil.T_ERR);
        SNICallback(err);
      }
    });
  }

  return new Promise((resolve) => {
    const server = https.createServer({
      secureOptions: constants.SSL_OP_NO_SSLv3 || constants.SSL_OP_NO_TLSv1,
      SNICallback: SNIPrepareCert,
    }, handler).listen(port);
    resolve(server);
  });
}

function createHttpsIPServer(ip, port, handler) {
  assert(ip && port && handler, 'invalid param for https IP server');

  return new Promise((resolve, reject) => {
    certMgr.getCertificate(ip, (err, keyContent, crtContent) => {
      if (err) return reject(err);
      const server = https.createServer({
        secureOptions: constants.SSL_OP_NO_SSLv3 || constants.SSL_OP_NO_TLSv1,
        key: keyContent,
        cert: crtContent,
      }, handler).listen(port);

      resolve(server);
    });
  });
}

class httpsServerMgr {
  constructor(config) {
    if (!config || !config.handler) {
      throw new Error('handler is required');
    }
    this.httpsAsyncTask = new asyncTask();
    this.handler = config.handler;
    this.wsHandler = config.wsHandler
    this.asyncSNITaskName = `https_SNI_${Math.random()}`;
    this.activeServers = [];
  }

  getSharedHttpsServer(hostname) {
    const self = this;
    const ifIPHost = hostname && util.isIp(hostname);
    const serverHost = '127.0.0.1';

    function prepareServer(callback) {
      let port;
      Promise.resolve(util.getFreePort())
        .then(freePort => {
          port = freePort;
          if (ifIPHost) {
            return createHttpsIPServer(hostname, port, self.handler);
          } else {
            return createHttpsSNIServer(port, self.handler);
          }
        })
        .then(httpsServer => {
          self.activeServers.push(httpsServer);

          wsServerMgr.getWsServer({
            server: httpsServer,
            connHandler: self.wsHandler
          });

          httpsServer.on('upgrade', (req, cltSocket, head) => {
            logUtil.debug('will let WebSocket server to handle the upgrade event');
          });

          const result = {
            host: serverHost,
            port,
          };
          callback(null, result);
        })
        .catch(e => {
          callback(e);
        });
    }

    // same server for same host
    return new Promise((resolve, reject) => {
      self.httpsAsyncTask.addTask(ifIPHost ? hostname : serverHost, prepareServer, (error, serverInfo) => {
        if (error) {
          reject(error);
        } else {
          resolve(serverInfo);
        }
      });
    });
  }

  close() {
    this.activeServers.forEach(server => {
      server.close();
    });
  }
}

module.exports = httpsServerMgr;
