/*
	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 = 'postRawBody';

//private modules
var errors = require('mdo-errors');
var messages = require('../externalizedStrings');
var strings = require('ibm-strings');
var getCertificate = require('../rest-client-api/get-certificate.js').getCertificate;

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

module.exports = {
  postRawBody: postRawBody
}

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

  var payloadString,
      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',
  };

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

  getCertificate(http_options, moduleName);

  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 () {
          d.resolve();
        }, 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, 'post response: ', resp.statusCode, resp.statusMessage);
    } else {
      var responseUndefinedError = new Error(messages.responseUndefined);
      responseUndefinedError.name = 'responseUndefinedError';
      d.reject(responseUndefinedError);
    }
  }

  /*
   * Function that processes the completed (i.e. entire body has been read)
   * HTTP response received from the server.
   *
   * Parameters:
   *   resp    - Response object
   *   body    - Response body string
   *   httperr - Error object representing HTTP status
   *   d       - deferred promise to be resolved/rejected
   */
  function _processResponse(resp, body, httperr) {

    var pD = Q.defer();
    var errors = null;
    var errMsgs = '';
    var warnings = null;
    var deployerr = null;
    var err = null;
    var serverErrMsg = '';
    log.silly(moduleName, 'processResp: resp data completely read.  status: '+resp.statusCode);

    // HTTP Resp Status    Resp Body
    // ================    ===============================================
    // 200                 Expect JSON parsable body
    //                     If body parse failure -> reject with parse error + HTTP status
    //                     If body.transaction.status == 'SUCCESS' -> resolve OK
    //                     If body.transaction.status != 'SUCCESS' -> reject with err
    //                                     + HTTP status
    //                                     + body.transaction.status (if exists)
    //                                     + body.transaction.errors (if exists)
    //                                     + body.transaction.warnings (if exists)
    //                                     + body.message (if exists)
    //                                     + body.additionalMessage (if exists)
    //                                     + body.transaction.errors[].details (if exists, for each error)
    //                                     + body.transaction.warnings[].details (if exists, for each error)
    // !200                If body does not exist -> reject with HTTP err
    //                     If body parse failure -> reject with parse error + HTTP status
    //                     reject with err + body.transaction.status (if exists) +
    //                                     + body.transaction.errors (if exists)
    //                                     + body.transaction.warnings (if exists)
    //                                     + body.message (if exists)
    //                                     + body.additionalMessage (if exists)
    //                                     + body.transaction.errors[].details (if exists, for each error)
    //                                     + body.transaction.warnings[].details (if exists, for each error)

    // If the body exists, parse it
    var jsonBody = null;
    if (body) {
      log.silly(moduleName, 'processResp: resp data completely read.  body: '+body);
      try {  // Need try/catch around JSON.parse as can throw an exception
        jsonBody = JSON.parse(body); // Throws exception on failure
        log.silly(moduleName, 'processResp: JSON body: '+ JSON.stringify(jsonBody, null,' '));
      }
      catch(e) {  // Response body parsing syntax error
        log.verbose(moduleName, 'processResp: parse exception: '+e.stack);
        var parseerr = new Verror(httperr, e.message);
        parseerr.name = e.name;
        err = new Verror(parseerr, messages.parseResponseFailed);
        err.name = 'DeployAppError';
        pD.reject(err);
      }
    }

    // Did we receive an 'OK' response?
    // If so, the body must contain a JSON object with a status of 'SUCCESS'
    if (resp.statusCode === 200) {
      if (jsonBody.transaction && jsonBody.transaction.status === 'SUCCESS') {
        log.verbose(moduleName, 'processResp: success');
        warnings = jsonBody.transaction.warnings ? {'warnings':jsonBody.transaction.warnings} : null;
        pD.resolve({
          warnings: warnings,
        });  // Success.  Return any warnings
      }
      else {
        errors = (jsonBody.transaction && jsonBody.transaction.errors) ? jsonBody.transaction.errors : null;
        errMsgs = _concatErrDetails(errors);
        warnings = (jsonBody.transaction && jsonBody.transaction.warnings) ? jsonBody.transaction.warnings : null;
        var warnMsgs = _concatWarnDetails(warnings);  //jshint ignore:line

        // Concatentate pertinent response message strings
        serverErrMsg = jsonBody.message || '';
        serverErrMsg += ((serverErrMsg && jsonBody.additionalMessage) ? ' : ' : '') + (jsonBody.additionalMessage || '');
        serverErrMsg += ((serverErrMsg && errMsgs) ? ' : ' : '') + (errMsgs || '');
        // Since this is a failure; focus just on error messsages
        //serverErrMsg += ((serverErrMsg && warnMsgs) ? ' : ' : '') + (warnMsgs || '');

        deployerr = new Verror(httperr, serverErrMsg);
        deployerr.name = ((jsonBody.transaction && jsonBody.transaction.status) ? jsonBody.transaction.status : 'DeployAppRespError');
        err = new Verror(deployerr, messages.invalidResponseCode);
        err.name = 'DeployAppError';
        err.errors = errors;
        err.warnings = warnings;
        log.verbose(moduleName, 'processResp: OK(200) failure: '+JSON.stringify(err, null, 2));
        pD.reject(err);
      }
    }
    else { // Bad HTTP response
      if (!jsonBody) {  // No response body; just reject with HTTP resp code
        err = new Verror(httperr, messages.noResponseBody);
        err.name = 'DeployAppError';
        err.statusCode = resp.statusCode;
        log.verbose(moduleName, 'processResp: failure: '+err);
        pD.reject(err);
      }
      else {  // Got body, reject with HTTP + body parts that exist
        errors = (jsonBody.transaction && jsonBody.transaction.errors) ? jsonBody.transaction.errors : null;
        errMsgs = _concatErrDetails(errors);
        warnings = (jsonBody.transaction && jsonBody.transaction.warnings) ? jsonBody.transaction.warnings : null;
        //var warnMsgs = _concatWarnDetails(warnings);

        // Concatentate pertinent response message strings
        serverErrMsg = jsonBody.message || '';
        serverErrMsg += ((serverErrMsg && jsonBody.additionalMessage) ? ' : ' : '') + (jsonBody.additionalMessage || '');
        serverErrMsg += ((serverErrMsg && errMsgs) ? ' : ' : '') + (errMsgs || '');
        // Since this is a failure; focus just on error messsages
        //serverErrMsg += ((serverErrMsg && warnMsgs) ? ' : ' : '') + (warnMsgs || '');

        deployerr = new Verror(httperr, serverErrMsg);
        deployerr.name = ((jsonBody.transaction && jsonBody.transaction.status) ? jsonBody.transaction.status : 'DeployAppRespError');
        err = new Verror(deployerr, messages.invalidHTTPResponse);
        err.name = 'DeployAppError';
        err.statusCode = resp.statusCode;
        err.errors = errors;
        err.warnings = warnings;
        log.verbose(moduleName, 'processResp: !OK failure: '+ JSON.stringify(err, null, 2));
        pD.reject(err);
      }
    }

    return pD.promise;
  };

  return d.promise;
};

function _concatErrDetails(serverErrArr) {
    var retStr = '';
    if (serverErrArr) {
        for (var i = 0; i < serverErrArr.length; i++ ) {
            retStr += (i!==0 ? ' ' : '') + (serverErrArr[i].details || '');
        }
    }
    return retStr;
}

function _concatWarnDetails(serverWarnArr) {
    var retStr = '';
    if (serverWarnArr) {
        for (var i = 0; i < serverWarnArr.length; i++ ) {
            retStr += (i!==0 ? ' ' : '') + (serverWarnArr[i].details || '');
        }
    }
    return retStr;
}
