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

//private modules
var errors = require('mdo-errors');
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 Verror = require('verror');


/**
 * Obtains the specified runtime's confidential client details from the MFP server and returns a JSON
 * object containing those details.
 *
 *
 * Example JSON ouput
 *
 * {
 *    "clients": [
 *      {
 *        "id": "sampleId",
 *        "displayName": "sampleName",
 *        "secret": "sample",
 *        "allowedScope": "client.read",
 *        "customconfclient": "false"
 *      }
 *    ]
 * }
 *
 * 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'
 *    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 (defined by MFP server) representing the confidential client details is returned
 *   On failure, an Error object is returned.  Includes statusCode (HTTP code), errors (array from server)
 *     and warnings (array from server).
 */

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

  log.silly(moduleName, '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 runtime      = (serverDetails && serverDetails.runtime ? serverDetails.runtime : null);
  var timeout      = (serverDetails && serverDetails.timeout ? parseInt(serverDetails.timeout)*1000 : defaultOptions.TIMEOUT_MILLISECONDS);

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

  // Log the input parameters
  log.silly(moduleName, 'Arguments (with defaults): '+ JSON.stringify(serverDetails,null,' '));
  log.silly(moduleName, 'HTTP timeout (milliseconds): '+ timeout);

  // runtime argument is mandatory.  If not specified, fail the request
  if (!runtime) {
    var err = new Error('Missing argument \'runtime\'');
    err.name = 'InvalidArgumentError';  // Alas, an equivalent error is not defined by JavaScript
    log.verbose('getConfidentialClients', err);
    d.reject(err);
    return d.promise;
  }

  var path = strings.format(instanceID+mdoServerEndpoints.CONFIDENTIAL_CLIENTS, 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('server response is undefined'), 'server response error');
      formerr.name = 'GetConfidentialClientsError';
      d.reject(formerr);
    } else {
      var pPromise = _processResponse (rspObject.resp, rspObject.body, rspObject.httperr);
      pPromise.then (function (resp) {
        log.silly(moduleName, 'Response: ' + resp.statusCode);
        d.resolve(resp);
      }, function (err) {
        d.reject(err);
      });
    }
  }, function (err) {
    log.verbose(moduleName, 'form error: ', err);
    var formerr = new Verror(err, 'failed to get confidential clients');
    formerr.name = 'GetConfidentialClientsError';
    d.reject(formerr);
  });

  return d.promise;
};

/*
  * 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(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 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)

  var pD = Q.defer();

  // 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;
      var err = new Verror(parseerr, 'failed to process the response of confidential clients');
      err.name = 'ConfidentialClientsError';
      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(moduleName, '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, 'failed to process the response of confidential clients');
      err.name = 'ConfidentialClientsError';
      log.verbose(moduleName, '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 = 'ConfidentialClientsError';
      var newEerr = new Verror(resperr, 'failed to process the response of confidential clients information');
      newEerr.name = 'ConfidentialClientsError';
      newEerr.statusCode = resp.statusCode;
      newEerr.errors = errors;
      newEerr.warnings = warnings;
      log.verbose(moduleName, 'processResp: failure: '+JSON.stringify(newEerr, null, 2));
      pD.reject(newEerr);
    }
  }

  return pD.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;
}

/*
* 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.
* }
*
* {
*    "clients": [
*      {
*        "id": "sampleId",
*        "displayName": "sampleName",
*        "secret": "sample",
*        "allowedScope": "client.read"
*      }
*    ]
* }
*
* Returns:
* Q.deferred.promise.
*   On success - a string containing details about application register outcome
*   On failure - an Error object is returned
*/
var putConfidentialClients = function(serverDetails, payload) {
  var d = Q.defer();

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

  log.silly(moduleName, 'register payload: ' + (payload ? JSON.stringify(payload, null, ' ') : payload));

  // convert the serverDetails into s json Object
  //var payloadObject = (serverDetails && serverDetails.json ? serverDetails.json : null);
  var payloadObject = (payload ? payload : null);

  // Set trace level
  log.level = global.MDO_CLI_LOG_LEVEL ? global.MDO_CLI_LOG_LEVEL : log.level;
  var path = strings.format(instanceID+mdoServerEndpoints.CONFIDENTIAL_CLIENTS, contextRoot, runtime);
  var putRawBody = require('../rest-client/rest-client.js').putRawBody;
  var requestPromise = putRawBody(host, port, path, protocol, userName, userPassword, timeout, payloadObject);
  requestPromise.then (function (rspObject) {
    if (rspObject === undefined || rspObject.resp === undefined || rspObject.body === undefined || rspObject.httperr === undefined) {
      var formerr = new Verror(new Error('server response is undefined'), 'server response error');
      formerr.name = 'PutConfidentialClientsError';
      d.reject(formerr);
    } else {
      var pPromise = _processResponse (rspObject.resp, rspObject.body, rspObject.httperr);
      pPromise.then (function (resp) {
        log.silly(moduleName, 'Response: ' + resp.statusCode);
        d.resolve(resp);
      }, function (err) {
        d.reject(err);
      });
    }
  }, function (err) {
    log.verbose(moduleName, 'request error:' + err);
    var formerr = new Verror(err, 'failed to put confidential clients');
    formerr.name = 'PutConfidentialClientsError';
    d.reject(formerr);
  });

  return d.promise;
}

module.exports = {

  getConfidentialClients: getConfidentialClients,
  putConfidentialClients: putConfidentialClients

}
