/*
	Licensed Material - Property of IBM

	(C) Copyright 2015, 2016 IBM Corp.

	Unless required by applicable law or agreed to in writing, software
	distributed under the License is distributed on an "AS IS" BASIS,
	WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
	See the License for the specific language governing permissions and
	limitations under the License.
*/


'use strict';

var moduleName = 'putRawBody';

//private modules
var errors = require('mdo-errors');
var messages = require('../externalizedStrings');
var strings = require('ibm-strings');

//3rd party modules
var log = require('npmlog');
var Q = require('q');
var http = require('http');
var Verror   = require('verror');

module.exports = {
  putRawBody: putRawBody
}

/**
  Function to PUT raw JSON in the body of a server request.
**/
function putRawBody(host, port, path, protocol, userName, password, timeout, bodyPayload) {

  var payloadString,
      https,
      d = Q.defer();

  var argsErr = null;
  if (typeof host === 'undefined' || host === null) {
    argsErr = new Error(strings.format(messages.paramNotDefined, 'host'));
  } else if (typeof port === 'undefined' || port === null) {
    argsErr = new Error(strings.format(messages.paramNotDefined, 'port'));
  } else if (typeof path === 'undefined' || path === null) {
    argsErr = new Error(strings.format(messages.paramNotDefined, 'path'));
  } else if (typeof protocol === 'undefined' || protocol === null) {
    argsErr = new Error(strings.format(messages.paramNotDefined, 'protocol'));
  } else if (typeof userName === 'undefined' || userName === null) {
    argsErr = new Error(strings.format(messages.paramNotDefined, 'userName'));
  } else if (typeof password === 'undefined' || password === null) {
    argsErr = new Error(strings.format(messages.paramNotDefined, 'password'));
  } else if (typeof timeout === 'undefined' || timeout === null) {
    argsErr = new Error(strings.format(messages.paramNotDefined, 'timeout'));
  } else if (typeof bodyPayload === 'undefined' || bodyPayload === null) {
    argsErr = new Error(strings.format(messages.paramNotDefined, 'payload'));
  }
  if (argsErr !== null) {
    argsErr.name = 'InvalidArgumentsError';
    d.reject(argsErr);
    return d.promise;
  }
  try {
    payloadString = JSON.stringify(bodyPayload);
  } catch (err) {
    var invalidJSONError = new Error(messages.invalidJSONError);
    invalidJSONError.name = "invalidJSONError"
    d.reject(invalidJSONError);
    return d.promise;
  }

  var headers = {
    'Authorization': 'Basic ' + new Buffer(userName + ':' + password).toString('base64'),
    'Accept': 'application/json',
    'Content-Type': 'application/json',
  };

  // Allow for an override of the CLI->server communication protocol argument
  // This override will allow for CLI->server communication over 'http' while maintaining
  // mobile client->server communication over 'https'.  This is a temporary hack
  // as the CLI does not completely support https.
  if (process.env.MFP_CLI_USE_HTTP && ('true' === process.env.MFP_CLI_USE_HTTP.toLocaleLowerCase())) {
    log.verbose(moduleName, 'Overriding protocol due to env MFP_CLI_USE_HTTP: http');
    protocol = 'http:';

    if (process.env.MFP_CLI_USE_HTTP_PORT) {
      log.verbose(moduleName, 'Overriding port due to env MFP_CLI_USE_HTTP_PORT: '+process.env.MFP_CLI_USE_HTTP_PORT);
      port = process.env.MFP_CLI_USE_HTTP_PORT;
    }
  }

  var http_options = {
    host: host,
    port: port,
    path: path,
    method: 'PUT',
    protocol: protocol,
    headers:headers,
  };

  if (protocol === 'https:') {
    //If our protocol is https we have to load the module
    https = require('https');
    // Default rejectUnauthorized=true
    if (process.env.MFP_HTTPS_SSL_REJECT_UNAUTHORIZED && ('false' === process.env.MFP_HTTPS_SSL_REJECT_UNAUTHORIZED.toLocaleLowerCase())) {
      log.silly(moduleName, 'rejectUnauthorized = false');
      http_options.rejectUnauthorized = false;
    }

    // An array of authorized certificates can be provided.  The following code supports up to
    // 10 such certificates.  Set env vars in the format MFP_HTTPS_SSL_TRUSTSTORE_n, where n = 0-9.
    // with the value of the fully qualified path to a Base64 encoded certificate.
    //
    // Example: Using Java's keytool utility, export the Liberty server's self-generated, self-signed certificate
    // keytool -exportcert -keystore key.jks -storepass worklight -alias default -file keyjks-default.cer -rfc
    // (Windows)
    //     set MFP_HTTPS_SSL_TRUSTSTORE_0 = <full-path>\keyjks-default.cer
    // (server.xml) - confirm 'location', 'password', and 'serverKeyAlias' values
    //     <keyStore id="MyKeyStore" location="key.jks" type="JKS" password="worklight"/>
    //     <ssl id="MySSL" keyStoreRef="MyKeyStore" clientAuthenticationSupported="true" serverKeyAlias="default"/>
    //     <sslDefault sslRef="MySSL"/>
    var caArray = [];
    var fs = require('fs');
    for (var idx = 0; idx < 10; idx++) {
      log.silly(moduleName, 'Checking trust store env '+'MFP_HTTPS_SSL_TRUSTSTORE_'+idx);
      if (process.env['MFP_HTTPS_SSL_TRUSTSTORE_'+idx]) {
        try {
          log.silly(moduleName, 'Reading truststore: '+process.env['MFP_HTTPS_SSL_TRUSTSTORE_'+idx]);
          caArray.push(fs.readFileSync(process.env['MFP_HTTPS_SSL_TRUSTSTORE_'+idx]));
        }
        catch(err) {
          log.verbose(moduleName, 'Unable to read specified truststore: '+process.env['MFP_HTTPS_SSL_TRUSTSTORE_'+idx]+'; error = '+err);
        }
      }
    }
    // If no CA certificates supplied, then make sure the https 'ca' property is undefined; this
    // will cause node to use a "well known" list of CAs to validate the certificate
    http_options.ca = caArray.length > 0 ? caArray : undefined;
  }

  var req;
  if(protocol === 'https:') {
    log.silly(moduleName, 'performing https request');
    req = https.request(http_options, _requestCallback);
  } else {
    log.silly(moduleName, 'performing http request');
    req = http.request(http_options, _requestCallback);
  }

  req.on('error', function (err) {
    log.silly(moduleName, 'Error during the HTTP request: ' + err);
    var httpRequestError = new Error(strings.format(messages.httpRequestError, err));
    httpRequestError.name = 'HTTPRequestError';
    httpRequestError.code = httpRequestError.name;
    d.reject(httpRequestError);
  });

  req.on('socket', function(socket) {
    socket.on('connect', function(socket) {  //jshint ignore:line
      log.silly(moduleName, 'socket connect');
    });
    socket.setTimeout(timeout, function() {
      req.abort();
      log.verbose(moduleName, 'socket timeout!');
      var err = new Error(messages.requestTimeout);
      err.name = 'RequestTimeout';
      d.reject(err);
    });
  });

  req.write(payloadString);
  req.end();

  //nested function to handle the callback for the request
  //sets up listeners on the response object as well as kicks of _processResponse
  function _requestCallback(resp) {
    var body = '';
    if (resp) {
      // For possible later use, create a common HTTP error object
      var httperr = new Error('HTTP '+resp.statusCode+' - '+http.STATUS_CODES[resp.statusCode]);
      httperr.name = http.STATUS_CODES[resp.statusCode].replace(/ /g, '');

      resp.on('data', function(data) {  //readable stream event; fires when there's data to read. cb arg = data chunk Buffer or String
        body += data;
        log.silly(moduleName, 'resp.data: got some data; '+data);
      });
      resp.on('close', function() { //readable stream event; may fire when underlying connection is closed.
        log.silly(moduleName, 'resp.close: resp connection closed.  status: '+resp.statusCode);
      });
      resp.on('end', function() { //readable stream event; fires when no [more] data to read
        log.silly(moduleName, 'resp.end: resp data completely read.  status: '+resp.statusCode);
        //_processResponse(resp, body, httperr)
        //.then(function (pResp) {
          d.resolve({
        	  resp: resp,
        	  body: body,
        	  httperr: httperr
          });
        //}, function (err) {
        //  d.reject(err);
        //});
      });
      resp.on('error', function(err) {  //readable stream event; fires when error receiving data.  cb arg = Error object
        var resperr = new Verror(err, httperr.message);
        resperr.name = httperr.name;
        d.reject(resperr);
      });
      log.silly(moduleName, 'put response: ', resp.statusCode, resp.statusMessage);
    } else {
      var responseUndefinedError = new Error(messages.responseUndefined);
      responseUndefinedError.name = 'responseUndefinedError';
      d.reject(responseUndefinedError);
    }
  }

  return d.promise;
};
