/*
 * Licensed to The OpenNMS Group, Inc (TOG) under one or more
 * contributor license agreements.  See the LICENSE.md file
 * distributed with this work for additional information
 * regarding copyright ownership.
 *
 * TOG licenses this file to You under the GNU Affero General
 * Public License Version 3 (the "License") or (at your option)
 * any later version.  You may not use this file except in
 * compliance with the License.  You may obtain a copy of the
 * License at:
 *
 *      https://www.gnu.org/licenses/agpl-3.0.txt
 *
 * 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.
 */
/**
* @author Alejandro Galue <agalue@opennms.org>
* @copyright 2014-2025 The OpenNMS Group, Inc.
*/

const RequisitionsData = require('../model/RequisitionsData');
const Requisition      = require('../model/Requisition');
const RequisitionNode  = require('../model/RequisitionNode');

(function() {

  'use strict';

  angular.module('onms-requisitions')

  /**
  * @ngdoc service
  * @name RequisitionsService
  * @module onms-requisitions
  *
  * @requires $q Angular promise/deferred implementation
  * @requires $cacheFactory Angular cache management
  * @requires $window Document window
  * @requires $http Angular service that facilitates communication with the remote HTTP servers
  * @requires $timeout Angular service that facilitates timeout functions
  * @requires $log Angular log facility
  *
  * @description The RequisitionsService provides all the required methods to access ReST API for the OpenNMS requisitions.
  *
  * It uses Angular's Cache service to store locally all the requisitions after retrieving them from the server the first time.
  * This helps in terms of performance and responsiveness of the UI. Only changes are pushed back to the server.
  *
  * Conflicts may occur if someone else is changing the requisitions at the same time.
  *
  * If the cache is not going to be used, the controllers are responsible for maintaining the state of the data.
  */
  .factory('RequisitionsService', ['$q', '$cacheFactory', '$window', '$http', '$timeout', '$log', function($q, $cacheFactory, $window, $http, $timeout, $log) {

    $log.debug('Initializing RequisitionsService');

    const requisitionsService = {};
    requisitionsService.internal = {};

    // Cache Configuration

    requisitionsService.internal.cacheEnabled = true;
    requisitionsService.internal.cache = $cacheFactory('RequisitionsService');

    // URLs

    requisitionsService.internal.requisitionsUrl = 'rest/requisitions';
    requisitionsService.internal.requisitionNamesUrl = 'rest/requisitionNames';
    requisitionsService.internal.foreignSourcesUrl = 'rest/foreignSources';
    requisitionsService.internal.foreignSourcesConfigUrl = 'rest/foreignSourcesConfig';
    requisitionsService.internal.monitoringLocationsUrl = 'rest/monitoringLocations';
    requisitionsService.internal.snmpConfigUrl = 'api/v2/snmp-config';
    requisitionsService.internal.errorHelp = ' Check the OpenNMS logs for more details, or try again later.';

    requisitionsService.internal.excludedRequisitionNames = ['selfmonitor', 'minions'];

    // Timeouts

    requisitionsService.internal.defaultTimeout = 3; // Time to wait in seconds
    requisitionsService.internal.timingStatus = { isRunning: false };

    /**
    * @description (Internal) Gets the data from the internal cache
    *
    * @private
    * @name RequisitionsService:internal.getCatchedConfigData
    * @ngdoc method
    * @methodOf RequisitionsService
    * @param {string} configName The name of the config object
    * @returns {object} the internal cache content
    */
    requisitionsService.internal.getCatchedConfigData = function(configName) {
      return requisitionsService.internal.cache.get(configName);
    };

    /**
    * @description (Internal) Saves the data into internal cache
    *
    * @private
    * @name RequisitionsService:internal.setCatchedConfigData
    * @ngdoc method
    * @methodOf RequisitionsService
    * @param {string} configName The name of the config object
    * @param {object} configObject The config object
    */
    requisitionsService.internal.setCatchedConfigData = function(configName, configObject) {
      if (requisitionsService.internal.cacheEnabled) {
        requisitionsService.internal.cache.put(configName, configObject);
      }
    };

    /**
    * @description (Internal) Gets the requisitions from the internal cache
    *
    * @private
    * @name RequisitionsService:internal.getCatchedConfigData
    * @ngdoc method
    * @methodOf RequisitionsService
    * @returns {object} the internal cache content
    */
    requisitionsService.internal.getCachedRequisitionsData = function() {
      return requisitionsService.internal.getCatchedConfigData('requisitionsData');
    };

    /**
    * @description (Internal) Saves the requisitions data into internal cache
    *
    * @private
    * @name RequisitionsService:internal.setCachedRequisitionsData
    * @ngdoc method
    * @methodOf RequisitionsService
    * @param {object} requisitionsData The requisitions data
    */
    requisitionsService.internal.setCachedRequisitionsData = function(requisitionsData) {
      return requisitionsService.internal.setCatchedConfigData('requisitionsData', requisitionsData);
    };

    /**
    * @description (Internal) Gets a specific requisition object from the cache.
    *
    * @private
    * @name RequisitionsService:internal.getCachedRequisition
    * @ngdoc method
    * @methodOf RequisitionsService
    * @param {string} foreignSource The requisition's name (a.k.a. foreign source)
    * @returns {object} the requisition object.
    */
    requisitionsService.internal.getCachedRequisition = function(foreignSource) {
      const requisitionsData = requisitionsService.internal.getCachedRequisitionsData();
      if (!requisitionsData) {
        return null;
      }
      return requisitionsData.getRequisition(foreignSource);
    };

    /**
    * @description (Internal) Gets a specific node object from the cache.
    *
    * @private
    * @name RequisitionsService:internal.getCachedNode
    * @ngdoc method
    * @methodOf RequisitionsService
    * @param {string} foreignSource The requisition's name (a.k.a. foreign source)
    * @param {string} foreignId The foreign Id
    * @returns {object} the node object.
    */
    requisitionsService.internal.getCachedNode = function(foreignSource, foreignId) {
      const requisition = requisitionsService.internal.getCachedRequisition(foreignSource);
      if (!requisition) {
        return null;
      }
      return requisition.getNode(foreignId);
    };

    /**
    * @description (Internal) Quick-Add a new node to an existing requisition
    *
    * @private
    * @name RequisitionsService:addQuickNode
    * @ngdoc method
    * @methodOf RequisitionsService
    * @param {object} node the QuickNode object
    */
    requisitionsService.internal.addQuickNode = function(quickNode) {
      const node = quickNode.createRequisitionedNode();

      return requisitionsService.saveNode(node).then(
        function() { // saveNode:success
          $log.debug('addQuickNode: the node ' + node.nodeLabel + ' has been saved.');
          return requisitionsService.synchronizeRequisition(node.foreignSource, 'false').then(
            function() { // synchronizeRequisition:success
              $log.debug('addQuickNode: the requisition ' + node.foreignSource + ' has been synchronized.');
              return node;
            },
            function() { // synchronizeRequisition:failure
              return $q.reject('Cannot synchronize requisition ' + node.foreignSource);
            }
          );
        },
        function() { // saveNode:failure
          return $q.reject('Cannot quick-add node to requisition ' + node.foreignSource);
        }
      );
    };

    /**
    * @description (Internal) Updates a Requisition object based on a Deployed Statistics Object.
    *
    * @private
    * @name RequisitionsService:updateRequisition
    * @ngdoc method
    * @methodOf RequisitionsService
    * @param {object} existingReq the existing requisition object
    * @param {object} deployedReq the deployed statistics object
    */
    requisitionsService.internal.updateRequisition = function(existingReq, deployedReq) {
      $log.debug('updateRequisition: updating deployed statistics for requisition ' + deployedReq.name + '.');
      const foreignIds = deployedReq['foreign-id'];
      existingReq.nodesInDatabase = foreignIds.length;
      existingReq.deployed = foreignIds.length > 0;
      existingReq.lastImport = deployedReq['last-imported'];
      for (let idx = 0; idx < foreignIds.length; idx++) {
        const existingNode = existingReq.getNode(foreignIds[idx]);
        if (existingNode) {
          existingNode.deployed = true;
        }
      }
      return existingReq;
    };

    /**
    * @description Clears all the internal cache.
    *
    * This forces the service to retrieve the data from the server on next request.
    *
    * @name RequisitionsService:internal.clearCache
    * @ngdoc method
    * @methodOf RequisitionsService
    */
    requisitionsService.clearCache = function() {
      $log.debug('clearCache: removing everything from the internal cache');
      requisitionsService.internal.cache.removeAll();
      return true;
    };

    /**
    * @description Removes a specific requisition from the internal cache
    *
    * This forces the service to retrieve the data from the server on next request.
    *
    * @name RequisitionsService:internal.removeRequisitionFromCache
    * @ngdoc method
    * @methodOf RequisitionsService
    * @param {string} foreignSource The requisition's name (a.k.a. foreignSource)
    */
    requisitionsService.removeRequisitionFromCache = function(foreignSource) {
      const requisitionsData = requisitionsService.internal.getCachedRequisitionsData();
      if (requisitionsData) {
        const reqIdx = requisitionsData.indexOf(foreignSource);
        if (reqIdx >= 0) {
          $log.debug('clearRequisitionCache: removing requisition ' + foreignSource + ' from the internal cache');
          requisitionsData.requisitions.splice(reqIdx, 1);
          return true;
        }
      }
      return false;
    };

    /**
    * @description Gets the timing status object
    * The reason for using this is because of NMS-7872.
    *
    * @name RequisitionsService:startTiming
    * @ngdoc method
    * @methodOf RequisitionsService
    * @param {integer} ts The timeout in seconds (optional)
    * @returns {object} the timing status object
    */
    requisitionsService.startTiming = function(ts) {
      const seconds = ts? ts : requisitionsService.internal.defaultTimeout;
      $log.debug('startTiming: starting timeout of ' + seconds + ' seconds.');
      requisitionsService.internal.timingStatus.isRunning = true;
      $timeout(function() {
        requisitionsService.internal.timingStatus.isRunning = false;
      }, seconds * 1000);
    };

    /**
    * @description Gets the timing status object
    *
    * @name RequisitionsService:getTiming
    * @ngdoc method
    * @methodOf RequisitionsService
    * @returns {object} the timing status object
    */
    requisitionsService.getTiming = function() {
      return requisitionsService.internal.timingStatus;
    };

    /**
    * @description Requests all the requisitions (pending and deployed) from OpenNMS.
    *
    * If the data exists on the cache, that will be used instead of retrieving the data
    * from the OpenNMS server.
    *
    * After retrieving the requisitions, the deployed statistics will be retrieved, and the
    * statistics of the requisitions will be updated. Then, the data will be saved on the
    * internal cache.
    *
    * @name RequisitionsService:getRequisitions
    * @ngdoc method
    * @methodOf RequisitionsService
    * @returns {object} a promise. On success, it provides a RequisitionsData object.
    */
    requisitionsService.getRequisitions = function() {
      const rd = requisitionsService.internal.getCachedRequisitionsData();
      if (rd) {
        $log.debug('getRequisitions: returning a cached copy of the requisitions data');
        return $q.when(rd);
      }

      const deferred = $q.defer();
      const url = requisitionsService.internal.requisitionsUrl;
      $log.debug('getRequisitions: retrieving requisitions.');
      $http.get(url)
      .then(function getRequisitionsSuccess(response) {
        const data = response.data;
        const requisitionsData = new RequisitionsData();
        angular.forEach(data['model-import'], function(onmsRequisition) {
          const requisition = new Requisition(onmsRequisition, false);
          $log.debug('getRequisitions: adding requisition ' + requisition.foreignSource + '.');
          requisitionsData.requisitions.push(requisition);
        });
        requisitionsService.updateDeployedStats(requisitionsData).then(
          function() { // success;
            requisitionsService.internal.setCachedRequisitionsData(requisitionsData);
            deferred.resolve(requisitionsData);
          },
          function(error) { // error
            deferred.reject(error);
          }
        );
      }, function getRequisitionsError(response) {
        const error = response.data;
        const status = response.status;
        $log.error('getRequisitions: GET ' + url + ' failed:', error, status);
        deferred.reject('Cannot retrieve the requisitions.' + requisitionsService.internal.errorHelp);
      });

      return deferred.promise;
    };

    /**
    * @description Gets the requisition names.
    *
    * If the data exists on the cache, that will be used instead of retrieving the data
    * from the OpenNMS server.
    *
    * After retrieving the requisitions, the data will be saved on the internal cache.
    *
    * @name RequisitionsService:getRequisitionNames
    * @ngdoc method
    * @methodOf RequisitionsService
    * @returns {object} a promise. On success, it provides a list of requisition names.
    */
    requisitionsService.getRequisitionNames = function() {
      const config = requisitionsService.internal.getCatchedConfigData('requisitionNames');
      if (config) {
        $log.debug('getRequisitionNames: returning a cached copy of requisition names');
        return $q.when(config);
      }

      const deferred = $q.defer();
      const url = requisitionsService.internal.requisitionNamesUrl;
      $log.debug('getRequisitionNames: getting requisition names');
      $http.get(url)
      .then(function getRequisitionNamesSuccess(response) {
        const data = response.data;
        $log.debug('getRequisitionNames: got requisition names');
        requisitionsService.internal.setCatchedConfigData('requisitionNames', data['foreign-source']);
        deferred.resolve(data['foreign-source']);
      }, function getRequisitionNamesError(response) {
        const error = response.data;
        const status = response.status;
        $log.error('getRequisitionNames: GET ' + url + ' failed:', error, status);
        deferred.reject('Cannot retrieve requisition names.' + requisitionsService.internal.errorHelp);
      });

      return deferred.promise;
    };

    /**
    * @description Updates the requisitions data object with the deployed statistics.
    *
    * After retrieving the data, the provided object will be updated.
    *
    * @name RequisitionsService:updateDeployedStats
    * @ngdoc method
    * @param {object} requisitionsData The requisitions data object
    * @methodOf RequisitionsService
    * @returns {object} a promise. On success, it provides the updated RequisitionsData object.
    */
    requisitionsService.updateDeployedStats = function(requisitionsData) {
      const deferred = $q.defer();
      const url = requisitionsService.internal.requisitionsUrl + '/deployed/stats';
      $log.debug('updateDeployedStats: retrieving deployed statistics.');
      $http.get(url)
      .then(function updateDeployedStatsSuccess(response) {
        const data = response.data;
        angular.forEach(requisitionsData.requisitions, function(existingReq) {
          let deployedReq = null;
          angular.forEach(data['foreign-source'], function(r) {
            if (r.name === existingReq.foreignSource) {
              deployedReq = r;
            }
          });
          if (!deployedReq) {
            existingReq.setDeployed(false);
          } else {
            requisitionsService.internal.updateRequisition(existingReq, deployedReq);
          }
        });
        deferred.resolve(requisitionsData);
      }, function updateDeployedStatsError(response) {
        const error = response.data;
        const status = response.status;
        $log.error('updateDeployedStats: GET ' + url + ' failed:', error, status);
        deferred.reject('Cannot retrieve the deployed statistics.' + requisitionsService.internal.errorHelp);
      });
      return deferred.promise;
    };

    /**
    * @description Updates the requisition object with the deployed statistics.
    *
    * After retrieving the data successfully, the provided object will be updated.
    * Otherwise, the provided object will be returned unmodified, and this includes HTTP errors.
    *
    * @name RequisitionsService:updateDeployedStatsForRequisition
    * @ngdoc method
    * @param {object} requisition The requisition object
    * @methodOf RequisitionsService
    * @returns {object} a promise. On success, it provides a Requisition object.
    */
    requisitionsService.updateDeployedStatsForRequisition = function(existingReq) {
      const deferred = $q.defer();
      const url = requisitionsService.internal.requisitionsUrl + '/deployed/stats/' + encodeURIComponent(existingReq.foreignSource);
      $log.debug('updateDeployedStatsForRequisition: retrieving deployed statistics for requisition ' + existingReq.foreignSource);
      $http.get(url)
      .then(function updateDeployedStatsForRequisitionSuccess(response) {
        const deployedReq = response.data;
        requisitionsService.internal.updateRequisition(existingReq, deployedReq);
        deferred.resolve(existingReq);
      }, function updateDeployedStatsForRequisitionError(response) {
        const error = response.data;
        const status = response.status;
        $log.error('updateDeployedStatsForRequisition: GET ' + url + ' failed:', error, status);
        deferred.resolve(existingReq);
      });
      return deferred.promise;
    };

    /**
    * @description Request a sepcific requisition from OpenNMS.
    *
    * If the data exists on the cache, that will be used instead of retrieving the data
    * from the OpenNMS server.
    *
    * After retrieving the requisitions, the data will be saved on the internal cache.
    *
    * @name RequisitionsService:getRequisition
    * @ngdoc method
    * @param {string} foreignSource The requisition's name (a.k.a. foreignSource)
    * @methodOf RequisitionsService
    * @returns {object} a promise. On success, it provides a Requisition object.
    */
    requisitionsService.getRequisition = function(foreignSource) {
      const requisition = requisitionsService.internal.getCachedRequisition(foreignSource);
      if (requisition) {
        $log.debug('getRequisition: returning a cached copy of ' + foreignSource);
        return $q.when(requisition);
      }

      const deferred = $q.defer();
      const url = requisitionsService.internal.requisitionsUrl + '/' + encodeURIComponent(foreignSource);
      $log.debug('getRequisition: getting requisition ' + foreignSource);
      $http.get(url)
      .then(function getRequisitionSuccess(response) {
        const req = new Requisition(response.data);
        $log.debug('getRequisition: got requisition ' + foreignSource);
        requisitionsService.updateDeployedStatsForRequisition(req).then(
          function(updatedReq) { // success;
            const requisitionsData = requisitionsService.internal.getCachedRequisitionsData();
            if (requisitionsData) {
              $log.debug('getRequisition: updating cache for requisition ' + foreignSource);
              requisitionsData.setRequisition(updatedReq);
            }
            deferred.resolve(updatedReq);
          }
        );
      }, function getRequisitionError(response) {
        const error = response.data;
        const status = response.status;
        $log.error('getRequisition: GET ' + url + ' failed:', error, status);
        deferred.reject('Cannot retrieve the requisition ' + foreignSource + '.' + requisitionsService.internal.errorHelp);
      });
      return deferred.promise;
    };

    /**
    * @description Request the synchronization/import of a requisition on the OpenNMS server.
    *
    * If the data exists on the cache, and the provided foreign source doesn't exist, the
    * request will be rejected.
    *
    * After retrieving the requisitions, the data on the internal cache will be updated.
    *
    * @name RequisitionsService:synchronizeRequisition
    * @ngdoc method
    * @methodOf RequisitionsService
    * @param {string} foreignSource The requisition's name (a.k.a. foreign source)
    * @param {string} rescanExisting [true, false, dbonly]
    * @returns {object} a promise.
    */
    requisitionsService.synchronizeRequisition = function(foreignSource, rescanExisting) {
      const requisitionsData = requisitionsService.internal.getCachedRequisitionsData();
      if (requisitionsData) {
        const reqIdx = requisitionsData.indexOf(foreignSource);
        if (reqIdx < 0) {
          return $q.reject('The foreignSource ' + foreignSource + ' does not exist.');
        }
      }

      const deferred = $q.defer();
      const url = requisitionsService.internal.requisitionsUrl + '/' + encodeURIComponent(foreignSource) + '/import';
      $log.debug('synchronizeRequisition: synchronizing requisition ' + foreignSource + ' with rescanExisting=' + rescanExisting);
      $http({ method: 'PUT', url: url, params: { rescanExisting: rescanExisting }})
      .then(function(response) {
        $log.debug('synchronizeRequisition: synchronized requisition ' + foreignSource);
        const r = requisitionsService.internal.getCachedRequisition(foreignSource);
        if (r) {
          $log.debug('synchronizeRequisition: updating deployed status of requisition ' + foreignSource);
          r.setDeployed(true);
        }
        deferred.resolve(response.data);
      }, function(response) {
        const error = response.data;
        const status = response.status;
        $log.error('synchronizeRequisition: PUT ' + url + ' failed:', error, status);
        deferred.reject('Cannot synchronize the requisition ' + foreignSource + '.' + requisitionsService.internal.errorHelp);
      });
      return deferred.promise;
    };

    /**
    * @description Request the creation of a new requisition on the OpenNMS server.
    *
    * If the data exists on the cache, and the provided foreign source exist, the
    * request will be rejected, because a foreign source must be unique.
    *
    * After retrieving the requisitions, the data on the internal cache will be updated.
    *
    * @name RequisitionsService:addRequisition
    * @ngdoc method
    * @methodOf RequisitionsService
    * @param {string} foreignSource The requisition's name (a.k.a. foreign source)
    * @returns {object} a promise. On success, it provides a Requisition object.
    */
    requisitionsService.addRequisition = function(foreignSource) {
      const req = requisitionsService.internal.getCachedRequisition(foreignSource);
      if (req) {
        return $q.reject('Invalid foreignSource ' + foreignSource + ', it already exist.');
      }

      const deferred = $q.defer();
      const emptyReq = { 'foreign-source': foreignSource, node: [] };
      const url = requisitionsService.internal.requisitionsUrl;
      $log.debug('addRequisition: adding requisition ' + foreignSource);
      $http.post(url, emptyReq)
      .then(function addRequisitionSuccess() {
        const requisition = new Requisition(emptyReq, false);
        $log.debug('addRequisition: added requisition ' + requisition.foreignSource);
        const data = requisitionsService.internal.getCachedRequisitionsData();
        if (data) {
          $log.debug('addRequisition: pushing requisition ' + foreignSource + ' into the internal cache');
          data.requisitions.push(requisition);
        }
        deferred.resolve(requisition);
      }, function addRequisitionError(response) {
        const error = response.data;
        const status = response.status;
        $log.error('addRequisition: POST ' + url + ' failed:', error, status);
        deferred.reject('Cannot add the requisition ' + foreignSource + '.' + requisitionsService.internal.errorHelp);
      });
      return deferred.promise;
    };

    /**
    * @description Request the deletion of a new requisition on the OpenNMS server.
    *
    * If the data exists on the cache, and the provided foreign source doesn't exist, the
    * request will be rejected. Also, if the requisition exist and it contains nodes (i.e.
    * it is not empty), the request will be rejected.
    *
    * After retrieving the requisitions, the data on the internal cache will be updated.
    *
    * @name RequisitionsService:deleteRequisition
    * @ngdoc method
    * @methodOf RequisitionsService
    * @param {string} foreignSource The requisition's name (a.k.a. foreign source)
    * @returns {object} a promise.
    */
    requisitionsService.deleteRequisition = function(foreignSource) {
      const requisitionsData = requisitionsService.internal.getCachedRequisitionsData();
      if (requisitionsData) {
        const reqIdx = requisitionsData.indexOf(foreignSource);
        if (reqIdx < 0) {
          return $q.reject('The foreignSource ' + foreignSource + ' does not exist.');
        }
        const req = requisitionsData.requisitions[reqIdx];
        if (req.nodesInDatabase > 0) {
          return $q.reject('The foreignSource ' + foreignSource + ' contains ' + req.nodesInDatabase + ' nodes on the database, it cannot be deleted.');
        }
      }

      const deferred = $q.defer();

      $log.debug('deleteRequisition: deleting requisition ' + foreignSource);
      const deferredReqPending  = $http.delete(requisitionsService.internal.requisitionsUrl + '/' + encodeURIComponent(foreignSource));
      const deferredReqDeployed = $http.delete(requisitionsService.internal.requisitionsUrl + '/deployed/' + encodeURIComponent(foreignSource));
      const deferredFSPending  = $http.delete(requisitionsService.internal.foreignSourcesUrl + '/' + encodeURIComponent(foreignSource));
      const deferredFSDeployed = $http.delete(requisitionsService.internal.foreignSourcesUrl + '/deployed/' + encodeURIComponent(foreignSource));

      $q.all([ deferredReqPending, deferredReqDeployed, deferredFSPending, deferredFSDeployed ])
      .then(function deleteRequisitionSuccess(response) {
        $log.debug('deleteRequisition: deleted requisition ' + foreignSource);
        requisitionsService.removeRequisitionFromCache(foreignSource);
        deferred.resolve(response.data);
      }, function deleteRequisitionError(response) {
        const error = response.data;
        const status = response.status;
        $log.error('deleteRequisition: DELETE operation failed:', error, status);
        deferred.reject('Cannot delete the requisition ' + foreignSource + '.' + requisitionsService.internal.errorHelp);
      });

      return deferred.promise;
    };

    /**
    * @description Request the removal of all from an existing requisition on the OpenNMS server.
    *
    * If the data exists on the cache, and the provided foreign source doesn't exist, the
    * request will be rejected.
    *
    * After retrieving the requisitions, the data on the internal cache will be updated.
    * After updating the requisition, a synchronization with rescanExisting=false will be performed.
    *
    * @name RequisitionsService:removeAllNodesFromRequisition
    * @ngdoc method
    * @methodOf RequisitionsService
    * @param {string} foreignSource The requisition's name (a.k.a. foreign source)
    * @returns {object} a promise.
    */
    requisitionsService.removeAllNodesFromRequisition = function(foreignSource) {
      const requisitionsData = requisitionsService.internal.getCachedRequisitionsData();
      if (requisitionsData) {
        if (!requisitionsData.getRequisition(foreignSource)) {
          return $q.reject('The foreignSource ' + foreignSource + ' does not exist.');
        }
      }

      const deferred = $q.defer();
      const requisition = {'foreign-source': foreignSource, node: []};
      $log.debug('removeAllNodesFromRequisition: removing nodes from requisition ' + foreignSource);
      const url = requisitionsService.internal.requisitionsUrl;
      $http.post(url, requisition)
      .then(function removeAllNodesFromRequisitionSuccess(response) {
        const data = response.data;
        $log.debug('removeAllNodesFromRequisition: removed nodes from requisition ' + foreignSource);
        requisitionsService.synchronizeRequisition(foreignSource, 'false').then(
          function() { // synchronizeRequisition:success
            $log.debug('removeAllNodesFromRequisition: rhe requisition ' + foreignSource + ' has been synchronized.');
            const req = requisitionsService.internal.getCachedRequisition(foreignSource);
            if (req) {
              $log.debug('removeAllNodesFromRequisition: updating requisition ' + foreignSource + ' on the internal cache');
              req.reset();
            }
            deferred.resolve(data);
          },
          function() { // synchronizeRequisition:failure
            deferred.reject('Cannot synchronize requisition ' + foreignSource);
          }
        );
      }, function removeAllNodesFromRequisitionError(response) {
        const error = response.data;
        const status = response.status;
        $log.error('removeAllNodesFromRequisition: POST ' + url + ' failed:', error, status);
        deferred.reject('Cannot remove all nodes from requisition ' + foreignSource + '.' + requisitionsService.internal.errorHelp);
      });

      return deferred.promise;
    };

    /**
    * @description Request a sepcific node from a requisition from OpenNMS.
    *
    * If the data exists on the cache, that will be used instead of retrieving the data
    * from the OpenNMS server.
    *
    * After retrieving the node, the data will be saved on the internal cache.
    *
    * @name RequisitionsService:getNode
    * @ngdoc method
    * @param {string} foreignSource The requisition's name (a.k.a. foreign source)
    * @param {string} foreignId The foreignId of the node
    * @methodOf RequisitionsService
    * @returns {object} a promise. On success, it provides a RequisitionNode object.
    */
    requisitionsService.getNode = function(foreignSource, foreignId) {
      const node = requisitionsService.internal.getCachedNode(foreignSource, foreignId);
      if (node) {
        $log.debug('getNode: returning a cached copy of ' + foreignId + '@' + foreignSource);
        return $q.when(node);
      }

      const deferred = $q.defer();
      const url  = requisitionsService.internal.requisitionsUrl + '/' + encodeURIComponent(foreignSource) + '/nodes/' + encodeURIComponent(foreignId);
      $log.debug('getNode: getting node ' + foreignId + '@' + foreignSource);
      $http.get(url)
      .then(function getNodeSuccess(response) {
        const node = new RequisitionNode(foreignSource, response.data);
        $log.debug('getNode: got node ' + foreignId + '@' + foreignSource);
        const requisition = requisitionsService.internal.getCachedRequisition(foreignSource);
        if (requisition) {
          $log.debug('getNode: updating cache for requisition ' + foreignSource);
          requisition.setNode(node);
        }
        deferred.resolve(node);
      }, function getNodeError(response) {
        const error = response.data;
        const status = response.status;
        $log.error('getNode: GET ' + url + ' failed:', error, status);
        deferred.reject('Cannot retrieve node ' + foreignId + ' from requisition ' + foreignSource + '.' + requisitionsService.internal.errorHelp);
      });

      return deferred.promise;
    };

    /**
    * @description Updates a node on an existing requisition on the OpenNMS server.
    *
    * The internal cache will be updated after the request is completed successfully if exist,
    * depending if the save operation is related with the update of an existing node, or if it
    * is related with the creation of a new node.
    *
    * @name RequisitionsService:removeAllNodesFromRequisition
    * @ngdoc method
    * @methodOf RequisitionsService
    * @param {object} node The RequisitionNode Object
    * @returns {object} a promise. 
    */
    // TODO If the parent properties are updated, verify they are valid through ReST prior, adding the node.
    requisitionsService.saveNode = function(node) {
      const deferred = $q.defer();
      const requisitionNode = node.getOnmsRequisitionNode();

      const url = requisitionsService.internal.requisitionsUrl + '/' + encodeURIComponent(node.foreignSource) + '/nodes';
      $log.debug('saveNode: saving node ' + node.nodeLabel + ' on requisition ' + node.foreignSource);
      $http.post(url, requisitionNode)
      .then(function saveNodeSuccess(response) {
        const data = response.data;
        $log.debug('saveNode: saved node ' + node.nodeLabel + ' on requisition ' + node.foreignSource);
        node.modified = true;
        const requisition = requisitionsService.internal.getCachedRequisition(node.foreignSource);
        if (requisition) {
          requisition.setNode(node);
        }
        // Updating categories cache
        const categories = requisitionsService.internal.getCatchedConfigData('categoriesConfig');
        if (categories) {
          let categoriesChanged = false;
          angular.forEach(node.categories, function(cat) {
            if (categories.indexOf(cat.name) === -1) {
              categoriesChanged = true;
              categories.push(cat.name);
            }
          });
          if (categoriesChanged) {
            requisitionsService.internal.setCatchedConfigData('categoriesConfig', categories);
          }
        }
        // Updating services cache
        const services = requisitionsService.internal.getCatchedConfigData('servicesConfig');
        if (services) {
          let servicesChanged = false;
          angular.forEach(node.interfaces, function(intf) {
            angular.forEach(intf.services, function(svc) {
              if (services.indexOf(svc.name) === -1) {
                servicesChanged = true;
                services.push(svc.name);
              }
            });
          });
          if (servicesChanged) {
            requisitionsService.internal.setCatchedConfigData('servicesConfig', services);
          }
        }
        deferred.resolve(data);
      }, function saveNodeError(response) {
        const error = response.data;
        const status = response.status;
        $log.error('saveNode: POST ' + url + ' failed:', error, status);
        deferred.reject('Cannot save node ' + node.foreignId + ' on requisition ' + node.foreignSource + '.' + requisitionsService.internal.errorHelp);
      });

      return deferred.promise;
    };

    /**
    * @description Request the removal of a node from an existing requisition on the OpenNMS server.
    *
    * The internal cache will be updated after the request is completed successfully if exist.
    *
    * @name RequisitionsService:deleteNode
    * @ngdoc method
    * @methodOf RequisitionsService
    * @param {object} node The RequisitionNode Object
    * @returns {object} a promise.
    */
    requisitionsService.deleteNode = function(node) {
      const deferred = $q.defer();

      const url = requisitionsService.internal.requisitionsUrl + '/' + encodeURIComponent(node.foreignSource) + '/nodes/' + encodeURIComponent(node.foreignId);
      $log.debug('deleteNode: deleting node ' + node.nodeLabel + ' from requisition ' + node.foreignSource);
      $http.delete(url)
      .then(function deleteNodeSuccess(response) {
        const data = response.data;
        $log.debug('deleteNode: deleted node ' + node.nodeLabel + ' on requisition ' + node.foreignSource);
        const r = requisitionsService.internal.getCachedRequisition(node.foreignSource);
        if (r) {
          const idx = r.indexOf(node.foreignId);
          if (idx > -1) {
            $log.debug('deleteNode: removing node ' + node.foreignId + '@' + node.foreignSource + ' from the internal cache');
            r.nodes.splice(idx, 1);
            r.nodesDefined--;
            r.modified = true;
            r.dateStamp = Date.now();
          }
        }
        deferred.resolve(data);
      }, function deleteNodeError(response) {
        const error = response.data;
        const status = response.status;
        $log.error('deleteNode: DELETE ' + url + ' failed:', error, status);
        deferred.reject('Cannot delete node ' + node.foreignId + ' from requisition ' + node.foreignSource + '.' + requisitionsService.internal.errorHelp);
      });

      return deferred.promise;
    };

    /**
    * @description Request a foreign source definition from OpenNMS for a given requisition.
    *
    * The foreign source definition contains the set of policies and detectors, as well as the scan frequency.
    * The information is not stored on cache. Each request will perform a server call.
    *
    * @name RequisitionsService:getForeignSourceDefinition
    * @ngdoc method
    * @param {string} foreignSource The requisition's name (a.k.a. foreign source), use 'default' for the default foreign source.
    * @methodOf RequisitionsService
    * @returns {object} a promise. On success, it provides a foreign source object.
    */
    requisitionsService.getForeignSourceDefinition = function(foreignSource) {
      const deferred = $q.defer();

      const url = requisitionsService.internal.foreignSourcesUrl + '/' + encodeURIComponent(foreignSource);
      $log.debug('getForeignSourceDefinition: getting definition for requisition ' + foreignSource);
      $http.get(url)
      .then(function getForeignSourceDefinitionSuccess(response) {
        $log.debug('getForeignSourceDefinition: got definition for requisition ' + foreignSource);
        deferred.resolve(response.data);
      }, function getForeignSourceDefinitionError(response) {
        const error = response.data;
        const status = response.status;
        $log.error('getForeignSourceDefinition: GET ' + url + ' failed:', error, status);
        deferred.reject('Cannot retrieve foreign source definition (detectors and policies) for requisition ' + foreignSource + '.' + requisitionsService.internal.errorHelp);
      });

      return deferred.promise;
    };

    /**
    * @description Updates the foreign source definition on the OpenNMS server for a given requisition.
    *
    * The foreign source definition contains the set of policies and detectors, as well as the scan frequency.
    *
    * @name RequisitionsService:saveForeignSourceDefinition
    * @ngdoc method
    * @methodOf RequisitionsService
    * @param {object} foreignSourceDef The requisition foreign source Object
    * @returns {object} a promise.
    */
    requisitionsService.saveForeignSourceDefinition = function(foreignSourceDef) {
      const deferred = $q.defer();
      const foreignSource = foreignSourceDef.name;

      const url = requisitionsService.internal.foreignSourcesUrl;
      $log.debug('saveForeignSourceDefinition: saving definition for requisition ' + foreignSource);
      $http.post(url, foreignSourceDef)
      .then(function saveForeignSourceDefinitionSuccess(response) {
        $log.debug('saveForeignSourceDefinition: saved definition for requisition ' + foreignSource);
        deferred.resolve(response.data);
      }, function saveForeignSourceDefinitionError(response) {
        const error = response.data;
        const status = response.status;
        $log.error('saveForeignSourceDefinition: POST ' + url + ' failed:', error, status);
        deferred.reject('Cannot save foreign source definition (detectors and policies) for requisition ' + foreignSource + '.' + requisitionsService.internal.errorHelp);
      });

      return deferred.promise;
    };

    /**
    * @description Clones an existing foreign source definition to another.
    *
    * The foreign source definition contains the set of policies and detectors, as well as the scan frequency.
    * If the source or the target requisition doesn't appear on the existing requisitions reported by the
    * OpenNMS server, the operation will be rejected.
    *
    * @name RequisitionsService:cloneForeignSourceDefinition
    * @ngdoc method
    * @methodOf RequisitionsService
    * @param {string} sourceRequisition The name of the source requisition
    * @param {string} targetRequisition The name of the target requisition
    * @returns {object} a promise.
    */
    requisitionsService.cloneForeignSourceDefinition = function(sourceRequisition, targetRequisition) {
      const deferred = $q.defer();

      requisitionsService.getRequisitionNames().then(
        function(requisitions) { // success
          if (requisitions.indexOf(sourceRequisition) < 0) {
            deferred.reject('The source requisition ' + sourceRequisition + ' does not exist.');
            return;
          }
          if (requisitions.indexOf(targetRequisition) < 0) {
            deferred.reject('The target requisition ' + targetRequisition + ' does not exist.');
            return;
          }
          requisitionsService.getForeignSourceDefinition(sourceRequisition).then(
            function(fsDef) { // success
              fsDef.name = targetRequisition;
              requisitionsService.saveForeignSourceDefinition(fsDef).then(
                function() { // success
                  deferred.resolve(fsDef);
                },
                function() { // error
                  deferred.reject('Cannot save foreign source definition for requisition ' + targetRequisition);
                }
              );
            },
            function() { // error
              deferred.reject('Cannot get foreign source definition for requisition ' + sourceRequisition);
            }
          );
        },
        function() { // error
          deferred.reject('Cannot validate the existance of the source and target requisitions.');
        }
      );

      return deferred.promise;
    };

    /**
    * @description Request the removal of a foreign source definition on the OpenNMS server.
    *
    * @name RequisitionsService:deleteForeignSourceDefinition
    * @ngdoc method
    * @methodOf RequisitionsService
    * @param {string} foreignSource The requisition's name (a.k.a. foreign source), use 'default' for the default foreign source.
    * @returns {object} a promise.
    */
    requisitionsService.deleteForeignSourceDefinition = function(foreignSource) {
      const deferred = $q.defer();

      $log.debug('deleteForeignSourceDefinition: deleting foreign source definition ' + foreignSource);
      const deferredFSPending  = $http.delete(requisitionsService.internal.foreignSourcesUrl + '/' + encodeURIComponent(foreignSource));
      const deferredFSDeployed = $http.delete(requisitionsService.internal.foreignSourcesUrl + '/deployed/' + encodeURIComponent(foreignSource));

      $q.all([ deferredFSPending, deferredFSDeployed ])
      .then(function deleteForeignSourceDefinitionSuccess(response) {
        $log.debug('deleteForeignSourceDefinition: deleted foreign source definition ' + foreignSource);
        deferred.resolve(response.data);
      }, function deleteForeignSourceDefinitionError(response) {
        const error = response.data;
        const status = response.status;
        $log.error('deleteForeignSourceDefinition: DELETE operation failed:', error, status);
        deferred.reject('Cannot delete the foreign source definition ' + foreignSource + '.' + requisitionsService.internal.errorHelp);
      });

      return deferred.promise;
    };

    /**
    * @description Gets the available detectors.
    *
    * If the data exists on the cache, that will be used instead of retrieving the data
    * from the OpenNMS server.
    *
    * After retrieving the node, the data will be saved on the internal cache.
    *
    * The data return by the promise should be an array of objects.
    * Each object contains the name of the detector and the full class name.
    *
    * @name RequisitionsService:getAvailableDetectors
    * @ngdoc method
    * @methodOf RequisitionsService
    * @returns {object} a promise. On success, it provides a list of available detector objects.
    */
    requisitionsService.getAvailableDetectors = function() {
      const config = requisitionsService.internal.getCatchedConfigData('detectorsConfig');
      if (config) {
        $log.debug('getAvailableDetectors: returning a cached copy of detectors configuration');
        return $q.when(config);
      }

      const deferred = $q.defer();
      const url = requisitionsService.internal.foreignSourcesConfigUrl + '/detectors';
      $log.debug('getAvailableDetectors: getting available detectors');
      $http.get(url)
      .then(function getAvailableDetectorsSuccess(response) {
        const data = response.data;
        $log.debug('getAvailableDetectors: got available detectors');
        requisitionsService.internal.setCatchedConfigData('detectorsConfig', data.plugins);
        deferred.resolve(data.plugins);
      }, function getAvailableDetectorsError(response) {
        const error = response.data;
        const status = response.status;
        $log.error('getAvailableDetectors: GET ' + url + ' failed:', error, status);
        deferred.reject('Cannot retrieve available detectors.' + requisitionsService.internal.errorHelp);
      });

      return deferred.promise;
    };

    /**
    * @description Gets the available policies.
    *
    * If the data exists on the cache, that will be used instead of retrieving the data
    * from the OpenNMS server.
    *
    * After retrieving the node, the data will be saved on the internal cache.
    *
    * The data return by the promise should be an array of objects.
    * Each object contains the name of the policy and the full class name.
    *
    * @name RequisitionsService:getAvailablePolicies
    * @ngdoc method
    * @methodOf RequisitionsService
    * @returns {object} a promise. On success, it provides a list of available polict objects.
    */
    requisitionsService.getAvailablePolicies = function() {
      const config = requisitionsService.internal.getCatchedConfigData('policiesConfig');
      if (config) {
        $log.debug('getAvailablePolicies: returning a cached copy of policies configuration');
        return $q.when(config);
      }

      const deferred = $q.defer();
      const url = requisitionsService.internal.foreignSourcesConfigUrl + '/policies';
      $log.debug('getAvailablePolicies: getting available policies');
      $http.get(url)
      .then(function getAvailablePoliciesSuccess(response) {
        const data = response.data;
        $log.debug('getAvailablePolicies: got available policies');
        requisitionsService.internal.setCatchedConfigData('policiesConfig', data.plugins);
        deferred.resolve(data.plugins);
      }, function getAvailablePoliciesError(response) {
        const error = response.data;
        const status = response.status;
        $log.error('getAvailablePolicies: GET ' + url + ' failed:', error, status);
        deferred.reject('Cannot retrieve available policies.' + requisitionsService.internal.errorHelp);
      });

      return deferred.promise;
    };

    /**
    * @description Gets the available services.
    *
    * If the data exists on the cache, that will be used instead of retrieving the data
    * from the OpenNMS server.
    *
    * After retrieving the node, the data will be saved on the internal cache.
    *
    * The data return by the promise should be an array of strings.
    * Each strings contains the name of the service.
    *
    * @example [ 'ICMP', 'SNMP' ]
    *
    * @name RequisitionsService:getAvailableServices
    * @ngdoc method
    * @methodOf RequisitionsService
    * @param {string} foreignSource The requisition's name (a.k.a. foreign source), use 'default' for the default foreign source.
    * @returns {object} a promise. On success, it provides a list of available services.
    */
    requisitionsService.getAvailableServices = function(foreignSource) {
      const config = requisitionsService.internal.getCatchedConfigData('servicesConfig');
      if (config) {
        $log.debug('getAvailableServices: returning a cached copy of services configuration');
        return $q.when(config);
      }

      const deferred = $q.defer();
      const url = requisitionsService.internal.foreignSourcesConfigUrl + '/services/' + encodeURIComponent(foreignSource);
      $log.debug('getAvailableServices: getting available services');
      $http.get(url)
      .then(function getAvailableServicesSuccess(response) {
        const data = response.data;
        $log.debug('getAvailableServices: got available services');
        requisitionsService.internal.setCatchedConfigData('servicesConfig', data.element);
        deferred.resolve(data.element);
      }, function getAvailableServicesError(response) {
        const error = response.data;
        const status = response.status;
        $log.error('getAvailableServices: GET ' + url + ' failed:', error, status);
        deferred.reject('Cannot retrieve available services.' + requisitionsService.internal.errorHelp);
      });

      return deferred.promise;
    };

    /**
    * @description Gets the available assets.
    *
    * If the data exists on the cache, that will be used instead of retrieving the data
    * from the OpenNMS server.
    *
    * After retrieving the node, the data will be saved on the internal cache.
    *
    * The data return by the promise should be an array of strings.
    * Each string is a valid asset field.
    *
    * @example [ 'address1, 'city', 'zip' ]
    *
    * @name RequisitionsService:getAvailableAssets
    * @ngdoc method
    * @methodOf RequisitionsService
    * @returns {object} a promise. On success, it provides a list of available assets.
    */
    requisitionsService.getAvailableAssets = function() {
      const config = requisitionsService.internal.getCatchedConfigData('assetsConfig');
      if (config) {
        $log.debug('getAvailableAssets: returning a cached copy of assets configuration');
        return $q.when(config);
      }

      const deferred = $q.defer();
      const url = requisitionsService.internal.foreignSourcesConfigUrl + '/assets';
      $log.debug('getAvailableAssets: getting available assets');
      $http.get(url)
      .then(function getAvailableAssetsSuccess(response) {
        const data = response.data;
        $log.debug('getAvailableAssets: got available assets');
        requisitionsService.internal.setCatchedConfigData('assetsConfig', data.element);
        deferred.resolve(data.element);
      }, function getAvailableAssetsError(response) {
        const error = response.data;
        const status = response.status;
        $log.error('getAvailableAssets: GET ' + url + ' failed:', error, status);
        deferred.reject('Cannot retrieve available assets.' + requisitionsService.internal.errorHelp);
      });

      return deferred.promise;
    };

    /**
    * @description Gets the available categories.
    *
    * If the data exists on the cache, that will be used instead of retrieving the data
    * from the OpenNMS server.
    *
    * After retrieving the node, the data will be saved on the internal cache.
    *
    * The data return by the promise should be an array of strings.
    * Each string is a valid category name.
    *
    * @example [ 'Production, 'Development', 'Testing' ]
    *
    * @name RequisitionsService:getAvailableCategories
    * @ngdoc method
    * @methodOf RequisitionsService
    * @returns {object} a promise. On success, it provides a list of available categories.
    */
    requisitionsService.getAvailableCategories = function() {
      const config = requisitionsService.internal.getCatchedConfigData('categoriesConfig');
      if (config) {
        $log.debug('getAvailableCategories: returning a cached copy of categories configuration');
        return $q.when(config);
      }

      const deferred = $q.defer();
      const url = requisitionsService.internal.foreignSourcesConfigUrl + '/categories';
      $log.debug('getAvailableCategories: getting available categories');
      $http.get(url)
      .then(function getAvailableCategoriesSuccess(response) {
        const data = response.data;
        $log.debug('getAvailableCategories: got available categories');
        requisitionsService.internal.setCatchedConfigData('categoriesConfig', data.element);
        deferred.resolve(data.element);
      }, function getAvailableCategoriesError(response) {
        const error = response.data;
        const status = response.status;
        $log.error('getAvailableCategories: GET ' + url + ' failed:', error, status);
        deferred.reject('Cannot retrieve available categories.' + requisitionsService.internal.errorHelp);
      });

      return deferred.promise;
    };

    /**
    * @description Gets the available locations.
    *
    * The 'Default' location is not considered as a valid option.
    * The location field should be either null or a valid location with Minions.
    *
    * @name RequisitionsService:getAvailableLocations
    * @ngdoc method
    * @methodOf RequisitionsService
    * @returns {object} a promise. On success, it provides a list of available locations.
    */
    requisitionsService.getAvailableLocations = function() {
      const deferred = $q.defer();
      const url = requisitionsService.internal.monitoringLocationsUrl;
      $log.debug('getAvailableLocations: getting available locations');
      $http.get(url)
      .then(function getAvailableLocationsSuccess(response) {
        const data = response.data;
        $log.debug('getAvailableLocations: got available locations');
        const locations = data.location.map((loc) => {
          return loc['location-name'];
        }).filter((name) => {
          return name && name.length > 0 && name !== 'Default';
        });
        $log.debug('Locations =' + JSON.stringify(locations));
        deferred.resolve(locations);
      }, function getAvailableLocationsError(response) {
        const error = response.data;
        const status = response.status;
        $log.error('getAvailableLocations: GET ' + url + ' failed:', error, status);
        deferred.reject('Cannot retrieve available locations.' + requisitionsService.internal.errorHelp);
      });
      return deferred.promise;
    };

    /**
    * @description Checks if a given foreignId exists on a specific requisition
    *
    * @name RequisitionsService:isForeignIdOnRequisition
    * @ngdoc method
    * @methodOf RequisitionsService
    * @param {string} foreignSource The requisition's name (a.k.a. foreign source)
    * @param {string} foreignId The foreign Id
    * @returns {object} a promise. On success, return true if the foreignId exists on the requisition.
    */
    requisitionsService.isForeignIdOnRequisition = function(foreignSource, foreignId) {
      const deferred = $q.defer();

      requisitionsService.getRequisition(foreignSource)
      .then(function(req) {
        let found = false;
        angular.forEach(req.nodes, function(n) {
          if (n.foreignId === foreignId) {
            found = true;
          }
        });
        deferred.resolve(found);
      }, function(err) {
        const message = 'cannot verify foreignId ' + foreignId + '@' + foreignSource + '. ';
        $log.error('isForeignIdOnRequisition: ' + message, err);
        deferred.reject(message + requisitionsService.internal.errorHelp);
      });

      return deferred.promise;
    };

    /**
    * @description Checks if a given nodeLabel exists on a specific requisition
    *
    * @name RequisitionsService:isForeignIdOnRequisition
    * @ngdoc method
    * @methodOf RequisitionsService
    * @param {string} foreignSource The requisition's name (a.k.a. foreign source)
    * @param {string} nodeLabel The node label
    * @returns {object} a promise. On success, return true if the nodeLabel exists on the requisition.
    */
    requisitionsService.isNodeLabelOnRequisition = function(foreignSource, nodeLabel) {
      const deferred = $q.defer();

      requisitionsService.getRequisition(foreignSource)
      .then(function(req) {
        let found = false;
        angular.forEach(req.nodes, function(n) {
          if (n.nodeLabel === nodeLabel) {
            found = true;
          }
        });
        deferred.resolve(found);
      }, function(err) {
        const message = 'cannot verify nodeLabel ' + nodeLabel + '@' + foreignSource + '. ';
        $log.error('isForeignIdOnRequisition: ' + message, err);
        deferred.reject(message + requisitionsService.internal.errorHelp);
      });

      return deferred.promise;
    };

    /**
    * @description Checks if a given IP Address exists on a specific node
    *
    * @name RequisitionsService:isIpAddressOnNode
    * @ngdoc method
    * @methodOf RequisitionsService
    * @param {string} foreignSource The requisition's name (a.k.a. foreign source)
    * @param {string} foreignId The foreign Id of the node
    * @param {string} ipAddress The IP address to check
    * @returns {object} a promise. On success, return  true if the IP Address exists on the node.
    */
    requisitionsService.isIpAddressOnNode = function(foreignSource, foreignId, ipAddress) {
      const deferred = $q.defer();

      requisitionsService.getNode(foreignSource, foreignId)
      .then(function(node) {
        const found = node.interfaces.filter((intf) => {
          return intf.ipAddress === ipAddress;
        }).length > 0;
        deferred.resolve(found);
      }, function(err) {
        const message = 'cannot verify ipAddress on node ' + foreignId + '@' + foreignSource + '. ';
        $log.error('isIpAddressOnNode: ' + message, err);
        deferred.reject(message + requisitionsService.internal.errorHelp);
      });

      return deferred.promise;
    };

    /**
    * @description Checks if a given category exists on a specific node
    *
    * @name RequisitionsService:isCategoryOnNode
    * @ngdoc method
    * @methodOf RequisitionsService
    * @param {string} foreignSource The requisition's name (a.k.a. foreign source)
    * @param {string} foreignId The foreign Id of the node
    * @param {string} category The category to check
    * @returns {object} a promise. On success, return true if the category exists on the node.
    */
    requisitionsService.isCategoryOnNode = function(foreignSource, foreignId, category) {
      const deferred = $q.defer();

      requisitionsService.getNode(foreignSource, foreignId)
      .then(function(node) {
        const found = node.categories.filter((cat) => {
          return cat.name === category;
        }).length > 0;
        deferred.resolve(found);
      }, function(err) {
        const message = 'cannot verify category on node ' + foreignId + '@' + foreignSource + '. ';
        $log.error('isCategoryOnNode: ' + message, err);
        deferred.reject(message + requisitionsService.internal.errorHelp);
      });

      return deferred.promise;
    };

    /**
    * @description Checks if a given category exists on a specific node
    *
    * @name RequisitionsService:isCategoryOnNode
    * @ngdoc method
    * @methodOf RequisitionsService
    * @param {string} foreignSource The requisition's name (a.k.a. foreign source)
    * @param {string} foreignId The foreign Id of the node
    * @param {string} ipAddress The IP address to check
    * @param {string} service The service to check
    * @returns {object} a promise. On success, return true if the service exists on the Node/IP.
    */
    requisitionsService.isServiceOnNode = function(foreignSource, foreignId, ipAddress, service) {
      const deferred = $q.defer();

      requisitionsService.getNode(foreignSource, foreignId)
      .then(function(node) {
        const found = node.interfaces.filter((intf) => {
          if (intf.ipAddress !== ipAddress) {
            return false;
          }
          return intf.services.filter((svc) => {
            return svc.name === service;
          }).length > 0;
        }).length > 0;
        deferred.resolve(found);
      }, function(err) {
        const message = 'cannot verify category on node ' + foreignId + '@' + foreignSource + '. ';
        $log.error('isIpAddressOnNode: ' + message, err);
        deferred.reject(message + requisitionsService.internal.errorHelp);
      });

      return deferred.promise;
    };

    /**
    * @description Update the SNMP credentials for a given IP Address.
    *
    * @name RequisitionsService:addRequisition
    * @ngdoc method
    * @methodOf RequisitionsService
    * @param {string} ipAddress The IP Address
    * @param {string} snmpCommunity The SNMP Community String
    * @param {string} snmpVersion The SNMP Version
    * @returns {object} a promise.
    */
    requisitionsService.updateSnmpCommunity = function(ipAddress, snmpCommunity, snmpVersion) {
      const deferred = $q.defer();

      const url = requisitionsService.internal.snmpConfigUrl + '/definition';

      $log.debug('updateSnmpCommunity: updating snmp community for ' + ipAddress);
      $http.put(url, {'readCommunity': snmpCommunity, 'version': snmpVersion, 'specific': [ipAddress] })
      .then(function updateSnmpCommunitySuccess() {
        $log.debug('updateSnmpCommunity: updated snmp community for ' + ipAddress);
        deferred.resolve(ipAddress);
      }, function updateSnmpCommunityError(response) {
        const error = response.data;
        const status = response.status;
        $log.error('updateSnmpCommunity: PUT ' + url + ' failed:', error, status);
        deferred.reject('Cannot update snmp community for ' + ipAddress + '.' + requisitionsService.internal.errorHelp);
      });

      return deferred.promise;
    };

    /**
    * @description Quick add a node to a requisition.
    *
    * @name RequisitionsService:quickAddNode
    * @ngdoc method
    * @methodOf RequisitionsService
    * @param {object} quickNode The QuickNode object
    * @returns {object} a promise.
    */
    requisitionsService.quickAddNode = function(quickNode) {
      if (quickNode.noSnmp === false && quickNode.snmpCommunity && quickNode.snmpCommunity.trim() !== '') {
        const deferred = $q.defer();
        requisitionsService.updateSnmpCommunity(quickNode.ipAddress, quickNode.snmpCommunity, quickNode.snmpVersion).then(
          function() { // updateSnmpCommunity:success
            requisitionsService.internal.addQuickNode(quickNode).then(
              function(node) { // addQuickNode:success
                deferred.resolve(node);
              },
              function(msg) { // addQuickNode:failure
                deferred.reject(msg);
              }
            );
          },
          function() { // updateSnmpCommunity:failure
            deferred.reject('Cannot update SNMP credentials for ' + quickNode.ipAddress);
          }
        );
        return deferred.promise;
      }
      return requisitionsService.internal.addQuickNode(quickNode);
    };

    /**
    * @description Checks whether a requisition name should not be used to add a node to.
    * We do not want users adding a node via "Quick Add Node" to these specific requisitions.
    *
    * @name RequisitionsService:isExcludedRequisitionName
    * @ngdoc method
    * @methodOf RequisitionsService
    * @param {string} reqName The requisition name to check.
    * @returns {boolean}
    */
    requisitionsService.isExcludedRequisitionName = function(reqName) {
      const lowerName = (reqName || '').toLowerCase();

      return requisitionsService.internal.excludedRequisitionNames.includes(lowerName);
    };

    return requisitionsService;
  }]);
}());
