'use strict';

function httpsTunnel(servername, conn) {
  console.error('tunnel server received encrypted connection to', servername);
  conn.end();
}
function handleHttp(servername, conn) {
  console.error('tunnel server received un-encrypted connection to', servername);
  conn.end([
    'HTTP/1.1 404 Not Found'
  , 'Date: ' + (new Date()).toUTCString()
  , 'Connection: close'
  , 'Content-Type: text/html'
  , 'Content-Length: 9'
  , ''
  , 'Not Found'
  ].join('\r\n'));
}
function rejectNonWebsocket(req, res) {
  // status code 426 = Upgrade Required
  res.statusCode = 426;
  res.setHeader('Content-Type', 'application/json');
  res.send({error: { message: 'Only websockets accepted for tunnel server' }});
}

var defaultConfig = {
  servernames: []
, secret: null
};
var tunnelFuncs = {
  // These functions should not be called because connections to the admin domains
  // should already be decrypted, and connections to non-client domains should never
  // be given to us in the first place.
  httpsTunnel:  httpsTunnel
, httpsInvalid: httpsTunnel
  // These function should not be called because ACME challenges should be handled
  // before admin domain connections are given to us, and the only non-encrypted
  // client connections that should be given to us are ACME challenges.
, handleHttp:         handleHttp
, handleInsecureHttp: handleHttp
};

module.exports.create = function (deps, config) {
  var equal = require('deep-equal');
  var enableDestroy = require('server-destroy');
  var currentOpts = Object.assign({}, defaultConfig);

  var httpServer, wsServer, stunneld;
  function start() {
    if (httpServer || wsServer || stunneld) {
      throw new Error('trying to start already started tunnel server');
    }
    httpServer = require('http').createServer(rejectNonWebsocket);
    enableDestroy(httpServer);

    wsServer = new (require('ws').Server)({ server: httpServer });

    var tunnelOpts = Object.assign({}, tunnelFuncs, currentOpts);
    stunneld = require('stunneld').create(tunnelOpts);
    wsServer.on('connection', stunneld.ws);
  }

  function stop() {
    if (!httpServer || !wsServer || !stunneld) {
      throw new Error('trying to stop unstarted tunnel server (or it got into semi-initialized state');
    }
    wsServer.close();
    wsServer = null;
    httpServer.destroy();
    httpServer = null;
    // Nothing to close here, just need to set it to null to allow it to be garbage-collected.
    stunneld = null;
  }

  function updateConf() {
    var newOpts = Object.assign({}, defaultConfig, config.tunnelServer);
    if (!Array.isArray(newOpts.servernames)) {
      newOpts.servernames = [];
    }
    var trimmedOpts = {
      servernames: newOpts.servernames.slice().sort()
    , secret:      newOpts.secret
    };

    if (equal(trimmedOpts, currentOpts)) {
      return;
    }
    currentOpts = trimmedOpts;

    // Stop what's currently running, then if we are still supposed to be running then we
    // can start it again with the updated options. It might be possible to make use of
    // the existing http and ws servers when the config changes, but I'm not sure what
    // state the actions needed to close all existing connections would put them in.
    if (httpServer || wsServer || stunneld) {
      stop();
    }
    if (currentOpts.servernames.length && currentOpts.secret) {
      start();
    }
  }
  process.nextTick(updateConf);

  return {
    isAdminDomain: function (domain) {
      return currentOpts.servernames.indexOf(domain) !== -1;
    }
  , handleAdminConn: function (conn) {
      if (!httpServer) {
        console.error(new Error('handleAdminConn called with no active tunnel server'));
        conn.end();
      } else {
        return httpServer.emit('connection', conn);
      }
    }

  , isClientDomain: function (domain) {
      if (!stunneld) { return false; }
      return stunneld.isClientDomain(domain);
    }
  , handleClientConn: function (conn) {
      if (!stunneld) {
        console.error(new Error('handleClientConn called with no active tunnel server'));
        conn.end();
      } else {
        return stunneld.tcp(conn);
      }
    }

  , updateConf
  };
};