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

//private modules
var errors = require('mdo-errors');
var messages = require('../externalizedStrings');
var strings = require('ibm-strings');
var mdoServerEndpoints = require('./constants/mdo-server-endpoints');
var defaultOptions = require('./constants/defaultOptions');

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

/**
 * Obtains a list of runtimes from the MFP server and returns a JSON object containing those information.
 *
 * The following REST API is documented here in the MFP Knowledge Center.
 * http://www-01.ibm.com/support/knowledgecenter/SSHS8R_7.0.0/com.ibm.worklight.apiref.doc/apiref/r_restapi_runtimes_get.html?lang=en
 *
 * Example JSON output using the following curl command to access the REST API (no details)
 * curl -u admin:admin -k -i -H "Accept: application/json" -X GET http://localhost:10080/mfpadmin/management-apis/2.0/runtimes/?fullInfo=false
 *
 * {
 *   "projects":[
 *     {
 *       "name":"TestHybridAndroid",
 *       "link":"http://localhost:9080/mfpadmin/management-apis/2.0/runtimes/TestHybridAndroid",
 *       "numberOfAdapters":0,
 *       "numberOfApplications":1,
 *       "topology":"STANDALONE",
 *       "synchronizationStatus":"ok",
 *       "numberOfActiveDevices":0,
 *       "numberOfDecommissionedDevices":0,
 *       "running":true,
 *       "auditEnabled":true
 *     }
 *   ],
 *   "productVersion":"7.0.0.00-20150409-2201"
 * }
 *
 * Parameters:
 * serverDetails = {
 *    protocol: <http-protocol,     // Optional String. http || https. defaults to http
 *    host: <host-dns-name>,        // Optional String. defaults to localhost
 *    port: <host-port>,            // Optional Numeric. defaults to 9080
 *    contextRoot: <host-context>,  // Optional String. Server root context.  defaults to 'mfpadmin'
 *    userName: <user-id>,          // Optional String. Server login id. defaults to 'admin'
 *    userPassword: <user-pwd>,     // Optional String. Server login id password.  defaults to 'admin'
 *    timeout: <http-request-timeout>,// Optional Numeric (milliseconds).  http request timeout; defaults to 30000 (30 seconds)
 *                                  // Overrideable by MFP_HTTP_DEFAULT_TIMEOUT env variable
 *                                  // When timeout occurs, the request is aborted and an error is returned.
 *    details: true|false           // Options boolean.  Defaults to false.  When true, expanded output
 *                                     includes additional details, including license info
 *    debuglevel: <npmlog debug level>// Optional String. Trace values "verbose", "silly". If not specified, defaults to "info".
 * }
 *
 * Returns:
 * Q.deferred.promise.
 *   On success, a JSON object (defined by MFP server) representing the specified runtime details is returned
 *   On failure, an Error object is returned.  Includes statusCode (HTTP code), errors (array from server)
 *     and warnings (array from server).
 */

var getRuntimes = function (serverDetails) {
  var d = Q.defer();

  log.silly('getRuntimes', 'Arguments: '+ (serverDetails ? JSON.stringify(serverDetails,null,' ') : serverDetails));

  var protocol     = (serverDetails && serverDetails.protocol ? serverDetails.protocol : defaultOptions.PROTOCOL) + ':';  //jshint ignore:line
  var host         = (serverDetails && serverDetails.host ? serverDetails.host : defaultOptions.HOST);
  var port         = (serverDetails && serverDetails.port ? serverDetails.port : defaultOptions.PORT);
  var instanceID   = (serverDetails && (typeof serverDetails.path !== 'undefined' && serverDetails.path.length > 1) ? serverDetails.path : defaultOptions.PATH);
  var contextRoot  = (serverDetails && serverDetails.contextRoot ? serverDetails.contextRoot : defaultOptions.CONTEXT_ROOT);
  var userName     = (serverDetails && serverDetails.userName ? serverDetails.userName : defaultOptions.CREDENTIALS);
  var userPassword = (serverDetails && serverDetails.userPassword ? serverDetails.userPassword : defaultOptions.CREDENTIALS);
  var timeout      = (serverDetails && serverDetails.timeout ? parseInt(serverDetails.timeout)*1000 : defaultOptions.TIMEOUT_MILLISECONDS);
  var details      = (serverDetails && serverDetails.details ? serverDetails.details : false);
  // Set trace level
  log.level = global.MDO_CLI_LOG_LEVEL ? global.MDO_CLI_LOG_LEVEL : 'info';

  // Log the input parameters
  log.silly('getRuntimes', 'Arguments (with defaults): '+ JSON.stringify(serverDetails,null,' '));
  log.silly('getRuntimes', 'HTTP timeout (milliseconds): '+ timeout);
  var path = strings.format(instanceID+mdoServerEndpoints.GET_RUNTIMES, contextRoot, details);
  var get = require('../rest-client/rest-client.js').get;
  var requestPromise = get(host, port, path, protocol, userName, userPassword, timeout);
  requestPromise.then (function (rspObject) {
    if (rspObject === undefined || rspObject.resp === undefined || rspObject.body === undefined || rspObject.httperr === undefined) {
      var formerr = new Verror(new Error(messages.responseUndefined), messages.responseUndefined);
      formerr.name = 'GetRuntimesError';
      d.reject(formerr);
    } else {
      var pPromise = _processResponse (rspObject.resp, rspObject.body, rspObject.httperr);
      pPromise.then (function (resp) {
        log.silly(moduleName, 'Runtime Response: ' + resp.statusCode);
        d.resolve(resp);
      }, function (err) {
        d.reject(err);
      });
    }
  }, function (err) {
    log.verbose(moduleName, 'form error: ', err);
    var formerr = new Verror(err, messages.missingRuntimeDetails);
    formerr.name = 'GetRuntimesError';
    d.reject(formerr);
  });

  /*
   * 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
   */
  var _processResponse = function(resp, body, httperr) {
    log.silly('getRuntimes', 'processResp: resp data completely read.  status: '+resp.statusCode);

    var pD = Q.defer();

    // HTTP Resp Status    Resp Body
    // ================    ===============================================
    // 200                 Expect JSON parsable body
    //                     If body parse failure -> reject with parse error + HTTP status
    //                     If body parse OK -> return SUCCESS with resp body as JSON
    // !200                If body does not exist -> reject with HTTP err
    //                     If body parse failure -> reject with parse error + HTTP status
    //                     reject with err + body.message (if exists)
    //                                     + body.additionalMessage (if exists)
    //                                     + body.errors (if exists)
    //                                     + body.warnings (if exists)

    // If the body exists, parse it
    var jsonBody = null;
    if (body) {
      log.silly('getRuntimes', '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('getRuntimes', 'processResp: JSON body: '+ JSON.stringify(jsonBody, null,' '));
      }
      catch(e) {  // Response body parsing syntax error
        log.verbose('getRuntimes', 'processResp: parse exception: '+e.stack);
        var parseerr = new Verror(httperr, e.message);
        parseerr.name = e.name;
        var err = new Verror(parseerr, messages.failedToGetRuntimeDetails);
        err.name = 'GetRuntimesError';
        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) {
      log.verbose('getRuntimes', 'processResp: success');
      pD.resolve(jsonBody);  // Success.  Nothing to return
    }
    else { // Bad HTTP response

      if (!jsonBody) {  // No response body; just reject with HTTP resp code
        var err = new Verror(httperr, messages.failedToGetRuntimeDetails);
        err.name = 'GetRuntimesError';
        err.statusCode = resp.statusCode;
        log.verbose('getRuntimes', 'processResp: failure: '+err);
        pD.reject(err);
      }
      else {  // Got body, reject with HTTP + body parts that exist
        var errors = (jsonBody.transaction && jsonBody.transaction.errors) ? jsonBody.transaction.errors : null;
        var errMsgs = _concatErrDetails(errors);
        var warnings = (jsonBody.transaction && jsonBody.transaction.warnings) ? jsonBody.transaction.warnings : null;
        var warnMsgs = _concatWarnDetails(warnings);  // jshint ignore:line

        // Concatentate pertinent response message strings
        var 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 || '');

        var resperr = new Verror(httperr, serverErrMsg);
        resperr.name = 'GetRuntimesError';
        var newErr = new Verror(resperr, messages.failedToGetRuntimeDetails);
        newErr.name = 'GetRuntimesError';
        newErr.statusCode = resp.statusCode;
        newErr.errors = errors;
        newErr.warnings = warnings;
        log.verbose('getRuntimes', 'processResp: failure: '+JSON.stringify(newErr, null, 2));

        pD.reject(newErr);
      }
    }

    return pD.promise;

  };

  return d.promise;
};

/*
 * Parameters:
 * options = {
 *    protocol: <http-protocol>,    // Optional String. http || https. defaults to http
 *    host: <host-dns-name>,        // Optional String. defaults to localhost
 *    port: <host-port>,            // Optional Numeric. defaults to 10080
 *    contextRoot: <host-context>,  // Optional String. Server root context.  defaults to 'worklightadmin'
 *    userName: <user-id>,          // Optional String. Server login id. defaults to 'admin'
 *    userPassword: <user-pwd>,     // Optional String. Server login id password.  defaults to 'admin'
 *    runtime: <project-runtime>,   // Mandatory String. Project name (aka runtime, project context root)
 *    timeout: <http-request-timeout>,// Optional Numeric (milliseconds).  http request timeout; defaults to 30000 (30 seconds)
 *                                  // Overrideable by MFP_HTTP_DEFAULT_TIMEOUT env variable
 *                                  // When timeout occurs, the request is aborted and an error is returned.
 * }
 *
 *
 * Returns:
 * Q.deferred.promise.
 *   On success - a json object containing the runtime config details:
 {
 "runtimeConfiguration": {
 "productVersion": "8.0.0.00-20151207-0630"
 }
 }
 *   On failure - an Error object is returned
 */
var getRuntimeConfiguration = function(serverDetails) {

  var d = Q.defer();
  var path = require('path');


  // These default settings getting associated with the serverDetails Object are really here
  // to cover development environment scenario.  Not sure we should keep this logic?
  log.silly(moduleName, 'serverDetails: ' + (serverDetails ? JSON.stringify(serverDetails, null, ' ') : serverDetails));

  var timeout      = (serverDetails && serverDetails.timeout ? parseInt(serverDetails.timeout)*1000 : defaultOptions.TIMEOUT_MILLISECONDS);
  var protocol     = (serverDetails && serverDetails.protocol ? serverDetails.protocol : defaultOptions.PROTOCOL) + ':';  //jshint ignore:line
  var host         = (serverDetails && serverDetails.host ? serverDetails.host : defaultOptions.HOST);
  var port         = (serverDetails && serverDetails.port ? serverDetails.port : defaultOptions.PORT);
  var instanceID   = (serverDetails && (typeof serverDetails.path !== 'undefined' && serverDetails.path.length > 1) ? serverDetails.path : defaultOptions.PATH);
  var contextRoot  = (serverDetails && serverDetails.contextRoot ? serverDetails.contextRoot : defaultOptions.CONTEXT_ROOT);
  var userName     = (serverDetails && serverDetails.userName ? serverDetails.userName : defaultOptions.CREDENTIALS);
  var userPassword = (serverDetails && serverDetails.userPassword ? serverDetails.userPassword : defaultOptions.CREDENTIALS);
  var runtime      = (serverDetails && serverDetails.runtime ? serverDetails.runtime : 'mfp');
  log.silly(moduleName, 'serverDetails after defaults applied: ' + (serverDetails ? JSON.stringify(serverDetails, null, ' ') : serverDetails));

  // Set trace level
  log.level = global.MDO_CLI_LOG_LEVEL ? global.MDO_CLI_LOG_LEVEL : log.level;

  // runtime and jsonObj arguments are mandatory.  If not specified, fail the request
  if (!runtime) {
    var missingArg = (runtime ? 'file' :  'runtime');
    var err = new Error(strings.format(messages.missingArgument, missingArg));
    err.name = 'InvalidArgumentError';  // Alas, an equivalent error is not defined by JavaScript
    log.verbose(moduleName, err);
    d.reject(err);
    return d.promise;
  }

  var path = strings.format(instanceID+mdoServerEndpoints.PULL_RUNTIME_CONFIGURATION, contextRoot, runtime);
  var get = require('../rest-client/rest-client.js').get;
  var requestPromise = get(host, port, path, protocol, userName, userPassword, timeout);
  requestPromise.then (function (rspObject) {
    if (rspObject === undefined || rspObject.resp === undefined || rspObject.body === undefined || rspObject.httperr === undefined) {
      var formerr = new Verror(new Error(messages.responseUndefined), messages.responseUndefined);
      formerr.name = 'RuntimeConfigPullError';
      d.reject(formerr);
    } else {

      var pPromise = _processResponse (rspObject.resp, rspObject.body, rspObject.httperr);
      pPromise.then (function (resp) {
        log.silly(moduleName, 'Runtime Config Response Details: ' + JSON.stringify(resp));
        d.resolve(resp);
      }, function (err) {
        d.reject(err);
      });
    }

  }, function (err) {
    log.verbose(moduleName, 'form error: ', err);
    var formerr = new Verror(err, messages.failedToGetRuntimeDetails);
    formerr.name = 'RuntimeConfigPullError';
    d.reject(formerr);
  });

  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.appDescriptorPullError);
	        err.name = 'RuntimeConfigPullError';
	        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 && jsonBody.transaction && jsonBody.transaction.status === 'SUCCESS') {
	        log.verbose(moduleName, 'processResp: success');
	        warnings = jsonBody.transaction.warnings ? {'warnings':jsonBody.transaction.warnings} : null;
	        pD.resolve({
	        	state: 'fulfilled',
                value: {runtimeConfig: jsonBody},
                warnings: warnings
	        });  // Success.  Return any warnings
	      }
	      else if (jsonBody && jsonBody.transaction) {
	        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.appDescriptorPullError);
	        err.name = 'RuntimeConfigPullError';
	        err.errors = errors;
	        err.warnings = warnings;
	        log.verbose(moduleName, 'processResp: OK(200) failure: '+JSON.stringify(err, null, 2));
	        pD.reject(err);
	      } else {

	        pD.resolve({
	        	state: 'fulfilled',
                value: {runtimeConfig: jsonBody}
	        });

	      }
	    }
	    else { // Bad HTTP response

	      if (jsonBody === undefined || jsonBody.transaction === undefined) {  // No response body; just reject with HTTP resp code
	        err = new Verror(httperr, messages.appDescriptorPullError);
	        err.name = 'RuntimeConfigPullError';
	        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 : 'ApplicationDescriptorPullError');
	        err = new Verror(deployerr, messages.appDescriptorPullError);
	        err.name = 'RuntimeConfigPullError';
	        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;

}

/*
 * Parameters:
 * options = {
 *    protocol: <http-protocol>,    // Optional String. http || https. defaults to http
 *    host: <host-dns-name>,        // Optional String. defaults to localhost
 *    port: <host-port>,            // Optional Numeric. defaults to 10080
 *    contextRoot: <host-context>,  // Optional String. Server root context.  defaults to 'worklightadmin'
 *    userName: <user-id>,          // Optional String. Server login id. defaults to 'admin'
 *    userPassword: <user-pwd>,     // Optional String. Server login id password.  defaults to 'admin'
 *    runtime: <project-runtime>,   // Mandatory String. Project name (aka runtime, project context root)
 *    timeout: <http-request-timeout>,// Optional Numeric (milliseconds).  http request timeout; defaults to 30000 (30 seconds)
 *                                  // Overrideable by MFP_HTTP_DEFAULT_TIMEOUT env variable
 *                                  // When timeout occurs, the request is aborted and an error is returned.
 * }
 *
 * payload =
 *
 {
 "runtimeConfiguration": {
 "productVersion": "8.0.0.00-20151207-0630"
 }
 }
 *
 *
 * Returns:
 * Q.deferred.promise.
 *   On success -
 *   On failure - an Error object is returned
 */
var setRuntimeConfiguration = function(serverDetails, payload) {

  var d = Q.defer();
  var path = require('path');

  // These default settings getting associated with the serverDetails Object are really here
  // to cover development environment scenario.  Not sure we should keep this logic?
  log.silly(moduleName, 'serverDetails: ' + (serverDetails ? JSON.stringify(serverDetails, null, ' ') : serverDetails));
  var timeout      = (serverDetails && serverDetails.timeout ? parseInt(serverDetails.timeout)*1000 : defaultOptions.TIMEOUT_MILLISECONDS);
  var protocol     = (serverDetails && serverDetails.protocol ? serverDetails.protocol : defaultOptions.PROTOCOL) + ':';  //jshint ignore:line
  var host         = (serverDetails && serverDetails.host ? serverDetails.host : defaultOptions.HOST);
  var port         = (serverDetails && serverDetails.port ? serverDetails.port : defaultOptions.PORT);
  var instanceID   = (serverDetails && (typeof serverDetails.path !== 'undefined' && serverDetails.path.length > 1) ? serverDetails.path : defaultOptions.PATH);
  var contextRoot  = (serverDetails && serverDetails.contextRoot ? serverDetails.contextRoot : defaultOptions.CONTEXT_ROOT);
  var userName     = (serverDetails && serverDetails.userName ? serverDetails.userName : defaultOptions.CREDENTIALS);
  var userPassword = (serverDetails && serverDetails.userPassword ? serverDetails.userPassword : defaultOptions.CREDENTIALS);
  var runtime      = (serverDetails && serverDetails.runtime ? serverDetails.runtime : 'mfp');
  log.silly(moduleName, 'serverDetails after defaults applied: ' + (serverDetails ? JSON.stringify(serverDetails, null, ' ') : serverDetails));

  // Set trace level
  log.level = global.MDO_CLI_LOG_LEVEL ? global.MDO_CLI_LOG_LEVEL : log.level;

  // if the application key has not been defined properly we need to error out appropriately
  if (payload === undefined) {
	var err = errors.getError('NORUNTIMECONFIGEXIST');//new Error(messages.missingRuntimeDetails);
    //err.name = 'InvalidArgumentError';
    log.verbose(moduleName, err);
    d.reject(err);
    return d.promise;
  }

  // runtime and jsonObj arguments are mandatory.  If not specified, fail the request
  if (!runtime) {
    var missingArg = (runtime ? 'file' :  'runtime');
    var err = new Error('Missing argument \''+missingArg+'\'');
    err.name = 'InvalidArgumentError';  // Alas, an equivalent error is not defined by JavaScript
    log.verbose(moduleName, err);
    d.reject(err);
    return d.promise;
  }

  var runtimeConfiguration = payload;
  log.silly(moduleName, 'runtime config payload: ' + (payload ? JSON.stringify(payload, null, ' ') : payload));
  
  var path = strings.format(instanceID+mdoServerEndpoints.PUSH_RUNTIME_CONFIGURATION, contextRoot, runtime);
  var putRawBody = require('../rest-client/rest-client.js').putRawBody;
  var requestPromise = putRawBody(host, port, path, protocol, userName, userPassword, timeout, runtimeConfiguration);
  requestPromise.then (function (rspObject) {

	if (rspObject === undefined || rspObject.resp === undefined || rspObject.body === undefined || rspObject.httperr === undefined) {
      var formerr = new Verror(new Error(messages.undefinedHTTPResponse), messages.undefinedHTTPResponse);
      formerr.name = 'RuntimeConfigPushError';
      d.reject(formerr);
    } else {

      var pPromise = _processResponse (rspObject.resp, rspObject.body, rspObject.httperr);
      pPromise.then (function (resp) {
        log.silly(moduleName, 'Runtime Config Response: ' + resp.statusCode);
        d.resolve(resp);
      }, function (err) {
        d.reject(err);
      });

    }
  }, function (err) {
    log.verbose(moduleName, 'form error: ', err);
    var formerr = new Verror(err, messages.failedToGetRuntimeDetails);
    formerr.name = 'RuntimeConfigPushError';
    d.reject(formerr);
  });

  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.appDescriptorPullError);
	        err.name = 'RuntimeConfigurationPushError';
	        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 && jsonBody.transaction && jsonBody.transaction.status === 'SUCCESS') {
	        log.verbose(moduleName, 'processResp: success');
	        warnings = jsonBody.transaction.warnings ? {'warnings':jsonBody.transaction.warnings} : null;
	        pD.resolve({
	        	state: 'fulfilled',
                value: jsonBody,
                warnings: warnings
	        });  // Success.  Return any warnings
	      }
	      else if (jsonBody && jsonBody.transaction) {
	        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.appDescriptorPullError);
	        err.name = 'RuntimeConfigurationPushError';
	        err.errors = errors;
	        err.warnings = warnings;
	        log.verbose(moduleName, 'processResp: OK(200) failure: '+JSON.stringify(err, null, 2));
	        pD.reject(err);
	      } else {

	        pD.resolve({
	          runtimeConfig: jsonBody
	        });

	      }
	    }
	    else { // Bad HTTP response


	      if (jsonBody === null || jsonBody === undefined || jsonBody.transaction === undefined) {  // No response body; just reject with HTTP resp code
	        err = new Verror(httperr, messages.appDescriptorPullError);
	        err.name = 'RuntimeConfigurationPushError';
	        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 : 'ApplicationDescriptorPullError');
	        err = new Verror(deployerr, messages.appDescriptorPullError);
	        err.name = 'RuntimeConfigurationPushError';
	        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;
}

module.exports = {

  getRuntimes: getRuntimes,
  getRuntimeConfiguration: getRuntimeConfiguration,
  setRuntimeConfiguration: setRuntimeConfiguration

}
