/*
 * @copyright
 * Copyright © Microsoft Open Technologies, Inc.
 *
 * All Rights Reserved
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http: *www.apache.org/licenses/LICENSE-2.0
 *
 * THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS
 * OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION
 * ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A
 * PARTICULAR PURPOSE, MERCHANTABILITY OR NON-INFRINGEMENT.
 *
 * See the Apache License, Version 2.0 for the specific language
 * governing permissions and limitations under the License.
 */
'use strict';

var querystring = require('querystring');
var axios = require('axios');
var _ = require('underscore');
var url = require('url');

var constants = require('./constants');
var Logger = require('./log').Logger;
var util = require('./util');

var AccountType = constants.UserRealm.AccountType;
var FederationProtocolType = constants.UserRealm.FederationProtocolType;

var USER_REALM_PATH_TEMPLATE = 'common/UserRealm/<user>';

/**
 * Create a new UserRealm object
 * @private
 * @constructor
 * @param {object} callContext Contains any context information that applies to the request.
 * @param {string} userPrinciple   The username for which a realm needs to be discovered.
 * @param {string} authority       The string url of the authority that owns the userPrinciple.
 */
function UserRealm(callContext, userPrinciple, authority) {
  this._log = new Logger('UserRealm', callContext._logContext);
  this._callContext = callContext;
  this._apiVersion = '1.0';
  this._federationProtocol = null;
  this._accountType = null;
  this._federationMetadataUrl = null;
  this._federationActiveAuthUrl = null;
  this._userPrinciple = userPrinciple;
  this._authority = authority;
}

/**
* The API version requested by UserRealm.
* @type {string}
* @instance
* @memberOf UserRealm
* @name apiVersion
*/
Object.defineProperty(UserRealm.prototype, 'apiVersion', {
  get : function() {
    return this._apiVersion;
  }
});

/**
* The federation protocol used by the users realm.
* @type {string}
* @instance
* @memberOf UserRealm
* @name federationProtocol
*/
Object.defineProperty(UserRealm.prototype, 'federationProtocol', {
  get : function() {
    return this._federationProtocol;
  }
});

/**
* The Type of account.  Either managed or federated.
* @type {string}
* @instance
* @memberOf UserRealm
* @name accountType
*/
Object.defineProperty(UserRealm.prototype, 'accountType', {
  get : function() {
    return this._accountType;
  }
});

/**
* If this is a federated account then this property will contain the mex url.
* @type {string}
* @instance
* @memberOf UserRealm
* @name federationsMetadataUrl
*/
Object.defineProperty(UserRealm.prototype, 'federationMetadataUrl', {
  get : function() {
    return this._federationMetadataUrl;
  }
});

/**
* If the account is federated this will contain the authentication endpoint.
* @type {string}
* @instance
* @memberOf UserRealm
* @name federationActiveAuthUrl
*/
Object.defineProperty(UserRealm.prototype, 'federationActiveAuthUrl', {
  get : function() {
    return this._federationActiveAuthUrl;
  }
});

/**
* Given the authority url this method constructs a full user realm discovery url.
* @private
* @returns A full user realm discovery url including path and query string.
*/
UserRealm.prototype._getUserRealmUrl = function() {
  var userRealmUrl = util.copyUrl(this._authority);
  var urlEncodedUser = encodeURIComponent(this._userPrinciple);
  userRealmUrl.pathname = USER_REALM_PATH_TEMPLATE.replace('<user>', urlEncodedUser);

  var userRealmQuery = {
    'api-version' : this._apiVersion
  };

  userRealmUrl.search = querystring.stringify(userRealmQuery);

  userRealmUrl = util.copyUrl(userRealmUrl);

  return userRealmUrl;
};

/**
* Given a constants object and a value, validates that the value is a key in the constants object.
* @private
* @param {object} constants   An object containing constant key value pairs.
* @param {string}  value       A value to check against the constants
* @param {bool}   caseSensitive  set to true if comparisons should be made as case sensitive.  Defaults to false.
* @returns {bool|string}  If value passed in matches one of the constants then the return value is the matched constant.
*                         If a non case sensitive match was done, then the value returned may be different than the value
*                         passed in.  If there is no match then the method returns false.
*/
UserRealm.prototype._validateConstantValue = function(constants, value, caseSensitive) {
  if (!value) {
    return false;
  }
  if (!caseSensitive) {
    value = value.toLowerCase();
  }
  return _.contains(_.values(constants), value) ? value : false;
};

/**
* Checks whether an account type string is valid.
* @private
* @param {string} type  An account type string.
* @returns {bool}
*/
UserRealm.prototype._validateAccountType = function(type) {
  return this._validateConstantValue(AccountType, type);
};

/**
* Checks whether a federation protocol string is valid.
* @private
* @param {string} protocol  A federation protocol string.
* @returns {bool}
*/
UserRealm.prototype._validateFederationProtocol = function(protocol) {
  return this._validateConstantValue(FederationProtocolType, protocol);
};

/**
* Logs the values parsed as part of user realm discovery.
* @private
*/
UserRealm.prototype._logParsedResponse = function() {
  this._log.verbose('UserRealm response:');
  this._log.verbose(' AccountType:             ' + this.accountType);
  this._log.verbose(' FederationProtocol:      ' + this.federationProtocol);
  this._log.verbose(' FederationMetatdataUrl:  ' + this.federationMetadataUrl, true);
  this._log.verbose(' FederationActiveAuthUrl: ' + this.federationActiveAuthUrl, true);
};

/**
* Parses the response from a user realm discovery request.
* @private
* @param {string} body    The body returned as part of the http user realm discovery request.
* @param {UserRealm.DiscoverCallback} callback  Called when parsing is complete.
*/
UserRealm.prototype._parseDiscoveryResponse = function(body, callback) {
  this._log.verbose('Discovery response:\n' + body, true);

  var response;
  try {
    response = body;
  } catch (err) {
    callback(this._log.createError('Parsing realm discovery respone JSON failed: ' + body, true));
    return;
  }

  var accountType = this._validateAccountType(response['account_type']);
  if (!accountType) {
    callback(this._log.createError('Cannot parse account_type: ' + accountType));
    return;
  }

  this._accountType = accountType;

  if (this._accountType === AccountType.Federated) {
    var protocol = this._validateFederationProtocol(response['federation_protocol']);

    if (!protocol) {
      callback(this._log.createError('Cannot parse federation protocol: ' + protocol));
      return;
    }

    this._federationProtocol = protocol;
    this._federationMetadataUrl = response['federation_metadata_url'];
    this._federationActiveAuthUrl = response['federation_active_auth_url'];
  }

  this._logParsedResponse();
  callback();
};

/**
* @callback DiscoverCallback
* @memberOf UserRealm
* @param {Error} error   If an error occurs during discovery then this parameter will be used to return the error.
*/

/**
* Performs user realm discovery and fills in the properties on this object.
* @private
* @param {UserRealm.DiscoverCallback} callback  Called when discovery is complete.
*/
UserRealm.prototype.discover = function(callback) {
  var self = this;
  var options = util.createRequestOptions(
    this,
    {
      headers : {
        Accept : 'application/json'
      }
    }
  );

  var userRealmUrl = this._getUserRealmUrl();
  this._log.verbose('Performing user realm discovery at: ' + url.format(userRealmUrl), true);

  axios.get(userRealmUrl, options).then((response) => {
    util.logReturnCorrelationId(this._log, 'User Realm Discovery', response);
    // status >= 300 && < 400
    if (!util.isHttpSuccess(response.status)) {
      var returnErrorString = 'User Realm Discovery' + ' request returned http error: ' + response.status + ' and server response: ' + response.status;
      callback(this._log.createError(returnErrorString, true), response.data);
    }
    // Success case: status >= 200 && < 300
    self._parseDiscoveryResponse(response.data, callback);
  }).catch((error) => {
    // status >= 400: error case
    if (error.response) {
      util.logReturnCorrelationId(this._log, 'User Realm Discovery', error.response);
      this._log.error('User Realm Discovery' + ' request failed with', error.response.status, true);
      var returnErrorString = 'User Realm Discovery' + ' request returned http error: ' + error.response.status + ' and server response: ' + JSON.stringify(error.response.data);
      callback(this._log.createError(returnErrorString, true), error.response.data);
    }
    // if there is no response from the server
    else if (error.request) {
      this._log.error('User Realm Discovery' + ' request was made but no response was received', error.request, true);
      callback(self._log.createError('No response from the server'));
    }
    // request was never made
    else {
      this._log.error('User Realm Discovery' + ' request was never made, please check', error.message, true);
      callback(error.message);
    }
  });
};

module.exports = UserRealm;