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

//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');

/*
 * 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.
 * }
 *
 * appDescFilePath =  This parameters is optional and is the path to place the application descriptor zip file into
 *                    the default location for this file is the mobile first directory
 *
 * applicationKey =
 *
 - iOS
 {
 "applicationKey": {
 "bundleId": "com.dummy",
 "version": "1.0",
 "clientPlatform": "ios"
 }
 }

 - Android
 {
 "applicationKey": {
 "packageName": "com.dummy",
 "version": "1.0",
 "clientPlatform": "android"
 }
 }

 - Windows (universal)
 {
 "applicationKey": {
 "assemblyName": "com.sampleone.bankApp",
 "version": "1.0",
 "clientPlatform":"windows"
 }
 }

 -Windows Phone 8:

 for older "windowsphone8" platform:

 {
 "applicationKey": {
 "assemblyName": "com.sampleone.bankApp",
 "version": "1.0",
 "clientPlatform":"windowsphone8"
 }
 }

 *
 * Returns:
 * Q.deferred.promise.
 *   On success - a zip file containing the contents of app descriptor
 *   On failure - an Error object is returned
 */
var getApplicationDescriptor = function(serverDetails, applicationKey, appDescFilePath) {

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

  // 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 (applicationKey === undefined) {
    var err = new Error(strings.format(messages.missingAppDetails, 'id'));
    err.name = 'InvalidArgumentError';
    log.verbose(moduleName, err);
    d.reject(err);
    return d.promise;
  }

  // These default settings getting associated with the applicationKey Object are really here
  // to cover development environment scenario.  Not sure we should keep this logic?
  var environmentId = (applicationKey.clientPlatform ? applicationKey.clientPlatform : null);
  var applicationVersion = (applicationKey.version ? applicationKey.version : null);
  var applicationId = null;

  // TODO: replace literals like 'packageName', 'bundleId', etc... with constants
  var idTypes = ['packageName', 'bundleId', 'applicationId', 'assemblyName'];
  idTypes.forEach(function (appId) {

    if (applicationKey[appId] !== undefined) {
      applicationId = applicationKey[appId];
      return;
    }

  });

  // if the application id has not been defined properly we need to error out appropriately
  if (applicationId === null || applicationVersion === null || environmentId === null) {
      if (applicationVersion === null && environmentId === 'web') {
          // There is no version for web/browser apps. Set version to 'none' instead of erroring.
          applicationVersion = 'none'
      }
      else {
          var err = new Error(strings.format(messages.missingAppDetails, 'id') + ' ' + JSON.stringify({
              'environmentId': environmentId,
              'applicationId': applicationId,
              'applicationVersion': applicationVersion}));
          err.name = 'InvalidArgumentError';
          log.verbose(moduleName, err);
          d.reject(err);
          return d.promise;
      }
  }

  // if the app desc file path has not been define then we will try to build it up here
  // assumption is that most of the time it will not be defined
  if (appDescFilePath === undefined) {

    // if the mobulefirst directory does not exist then create it
    var mfpDir = path.resolve(process.cwd(), 'mobilefirst');
    if (!fs.existsSync(mfpDir)){
      fs.mkdirSync(mfpDir);
    }

    appDescFilePath = path.resolve(
      process.cwd(),
      'mobilefirst',
      applicationId + '-' + environmentId + '-' + applicationVersion +'-artifacts.zip');

    log.verbose(moduleName, 'Application Descriptor File Path: ' + appDescFilePath);

  }

  // if the application desc path has still not been defined properly we need to error out appropriately
  if (appDescFilePath === undefined) {

    var err = new Error(messages.missingAppDescriptorPath);
    err.name = 'InvalidArgumentError';
    log.verbose(moduleName, err);
    d.reject(err);
    return d.promise;

  }

  // 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;
    }
  }

  // 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.missingAppDetails, 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_APPLICATION_DESCRIPTOR, contextRoot, runtime, applicationId, environmentId, applicationVersion);
  var getFile = require('../rest-client/rest-client.js').getFile;
  var requestPromise = getFile(host, port, path, protocol, userName, userPassword, timeout, appDescFilePath);
  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 = 'ApplicationDescriptorPullError';
      d.reject(formerr);
    } else {
      var pPromise = _processResponse (rspObject.resp, rspObject.body, rspObject.httperr);
      pPromise.then (function (resp) {
        log.silly(moduleName, 'Response Details: ' + JSON.stringify(resp));
        d.resolve(resp);
      }, function (err) {
        // delete what there is of the appDescFilePath file it is either corrupt or empty
        if (fs.existsSync(appDescFilePath)) {
          try {
            fs.unlinkSync(appDescFilePath);
          }
          catch (err) {
          }
        }
        log.verbose(moduleName, 'form error: ', err);
        var formerr = new Verror(err, messages.appDescriptorPullError);
        formerr.name = 'ApplicationDescriptorPullError';
        formerr.appDetails = JSON.stringify(applicationKey);
        d.reject(formerr);
      });
    }
  }, function (err) {
    // delete what there is of the appDescFilePath file it is either corrupt or empty
    if (fs.existsSync(appDescFilePath)) {
      try {
        fs.unlinkSync(appDescFilePath);
      }
      catch (err) {
      }
    }
    log.verbose(moduleName, 'form error: ', err);
    var formerr = new Verror(err, messages.appDescriptorPullError);
    formerr.name = 'ApplicationDescriptorPullError';
    formerr.appDetails = JSON.stringify(applicationKey);
    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 = 'ApplicationDescriptorPullError';
        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({
          warnings: warnings,
          appInfo: applicationKey
        });  // 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 = 'ApplicationDescriptorPullError';
        err.errors = errors;
        err.warnings = warnings;
        log.verbose(moduleName, 'processResp: OK(200) failure: '+JSON.stringify(err, null, 2));
        pD.reject(err);
      } else {

        // we recieved a 200 with no response data ie "body" is empty
        pD.resolve({
          appInfo: applicationKey
        });

      }
    }
    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 = 'ApplicationDescriptorPullError';
        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 = 'ApplicationDescriptorPullError';
        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.
 * }
 *
 * appDescFilePath =  This parameters is REQUIRED and is the path to the application descriptor zip file
 *
 *
 * applicationKey =
 *
 - iOS

 "applicationKey": {
 "bundleId": "com.dummy",
 "version": "1.0",
 "clientPlatform": "ios"
 }


 - Android

 "applicationKey": {
 "packageName": "com.dummy",
 "version": "1.0",
 "clientPlatform": "android"
 }


 - Windows (universal)

 "applicationKey": {
 "assemblyName": "com.sampleone.bankApp",
 "version": "1.0",
 "clientPlatform":"windows"
 }


 -Windows Phone 8:

 for older "windowsphone8" platform:


 "applicationKey": {
 "assemblyName": "com.sampleone.bankApp",
 "version": "1.0",
 "clientPlatform":"windowsphone8"
 }

 *
 * Returns:
 * Q.deferred.promise.
 *   On success -
 *   On failure - an Error object is returned
 */
var setApplicationDescriptor = function(serverDetails, appDescFilePath, applicationKey) {

  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 (applicationKey === undefined) {
    var err = new Error(strings.format(messages.missingAppDetails, 'id'));
    err.name = 'InvalidArgumentError';
    log.verbose(moduleName, err);
    d.reject(err);
    return d.promise;
  }

  // These default settings getting associated with the applicationKey Object are really here
  // to cover development environment scenario.  Not sure we should keep this logic?
  var environmentId = (applicationKey.clientPlatform ? applicationKey.clientPlatform : null);
  var applicationVersion = (applicationKey.version ? applicationKey.version : null);
  var applicationId = null;

  // TODO: replace literals like 'packageName', 'bundleId', etc... with constants
  var idTypes = ['packageName', 'bundleId', 'applicationId', 'assemblyName'];
  idTypes.forEach(function (appId) {

    if (applicationKey[appId] !== undefined) {
      applicationId = applicationKey[appId];
      return;
    }

  });

  // if the application id has not been defined properly we need to error out appropriately
  if (applicationId === null || applicationVersion === null || environmentId === null) {
      if (applicationVersion === null && environmentId === 'web') {
          // There is no version for web/browser apps. Set version to 'none' instead of erroring.
          applicationVersion = 'none'
      }
      else {
          var err = new Error(strings.format(messages.missingAppDetails, 'id') + JSON.stringify({
              'environmentId': environmentId,
              'applicationId': applicationId,
              'applicationVersion': applicationVersion}));
          err.name = 'InvalidArgumentError';
          log.verbose(moduleName, err);
          d.reject(err);
          return d.promise;
      }
  }

  // if the app desc file path has not been define then we will try to build it up here
  // assumption is that most of the time it will not be defined
  if (appDescFilePath === undefined) {

    appDescFilePath = path.resolve(
      process.cwd(),
      'mobilefirst',
      applicationId + '-' + environmentId + '-' + applicationVersion +'-artifacts.zip');

    log.verbose(moduleName, 'Application Descriptor File Path: ' + appDescFilePath);

  }

  // if the application desc path has still not been defined properly we need to error out appropriately
  if (appDescFilePath === undefined) {

    var err = new Error(messages.missingAppDescriptorPath);
    err.name = 'InvalidArgumentError';
    log.verbose(moduleName, err);
    d.reject(err);
    return d.promise;

  }

  // 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;
    }
  }

  // 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.missingAppDetails, 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.PUSH_APPLICATION_DESCRIPTOR, contextRoot, runtime);
  var postFile = require('../rest-client/rest-client.js').postFile;

  var requestPromise = postFile(host, port, path, protocol, userName, userPassword, timeout, appDescFilePath);
  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 = 'ApplicationDescriptorPushError';
      d.reject(formerr);
    } else {
      var pPromise = _processResponse (rspObject.resp, rspObject.body, rspObject.httperr);
      pPromise.then (function (resp) {
        log.silly(moduleName, '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.appDescriptorPushError);
    formerr.name = 'ApplicationDescriptorPushError';
    formerr.appDetails = JSON.stringify(applicationKey);
    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;
    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.appDescriptorPushError);
        err.name = 'ApplicationDescriptorPushError';
        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({
          warnings: warnings,
          appInfo: applicationKey
        });  // 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.appDescriptorPushError);
        err.name = 'ApplicationDescriptorPushError';
        err.errors = errors;
        err.warnings = warnings;
        log.verbose(moduleName, 'processResp: OK(200) failure: '+JSON.stringify(err, null, 2));
        pD.reject(err);
      } else {

        // we recieved a 200 with no response data ie "body" is empty
        pD.resolve({
          appInfo: applicationKey
        });

      }
    }
    else { // Bad HTTP response

      if (jsonBody === undefined || jsonBody.transaction === undefined) {  // No response body; just reject with HTTP resp code
        err = new Verror(httperr, messages.appDescriptorPushError);
        err.name = 'ApplicationDescriptorPullError';
        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.appDescriptorPushError);
        err.name = 'ApplicationDescriptorPullError';
        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++ ) {
            for (var key in serverErrArr[i].detailedErrorMessages) {
                retStr += (i!==0 ? ' ' : '') + (serverErrArr[i].detailedErrorMessages[key] || '');
            }
        }
    }
    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 = {

  getApplicationDescriptor: getApplicationDescriptor,
  setApplicationDescriptor: setApplicationDescriptor

}
