/*
 * 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.
 */
const bootbox = require('bootbox');
const escape = require('lodash.escape');

require('../services/Requisitions');

const RequisitionNode = require('../model/RequisitionNode');
const RequisitionMetaDataEntry = require('../model/RequisitionMetaDataEntry');

/**
* @author Alejandro Galue <agalue@opennms.org>
* @copyright 2014-2022 The OpenNMS Group, Inc.
*/

(function() {

  'use strict';

  const assetView = require('../../views/asset.html');
  const metaDataView = require('../../views/metadata.html');
  const interfaceView = require('../../views/interface.html');

  const nodeBasicView = require('../../views/node-basic.html');
  const nodePathoutagesView = require('../../views/node-pathoutages.html');
  const nodeInterfacesView = require('../../views/node-interfaces.html');
  const nodeAssetsView = require('../../views/node-assets.html');
  const nodeMetaDataView = require('../../views/node-metadata.html');
  const nodeCategoriesView = require('../../views/node-categories.html');

  angular.module('onms-requisitions')

  .config(['$locationProvider', function($locationProvider) {
    $locationProvider.hashPrefix('');
  }])

  /**
  * @ngdoc controller
  * @name NodeController
  * @module onms-requisitions
  *
  * @requires $scope Angular local scope
  * @requires $routeParams Angular route params
  * @requires $cookies Angular cookies
  * @requires $window Document window
  * @requires $uibModal Angular UI modal
  * @required Configuration The configuration object
  * @requires RequisitionsService The requisitions service
  * @requires growl The growl plugin for instant notifications
  *
  * @description The controller for manage requisitioned nodes (add/edit the nodes on a specific requisition)
  */
  .controller('NodeController', ['$scope', '$routeParams', '$cookies', '$window', '$uibModal', 'Configuration', 'RequisitionsService', 'growl', function($scope, $routeParams, $cookies, $window, $uibModal, Configuration, RequisitionsService, growl) {
    $scope.nodeBasicView = nodeBasicView;
    $scope.nodePathoutagesView = nodePathoutagesView;
    $scope.nodeInterfacesView = nodeInterfacesView;
    $scope.nodeAssetsView = nodeAssetsView;
    $scope.nodeMetaDataView = nodeMetaDataView;
    $scope.nodeCategoriesView = nodeCategoriesView;

    /**
    * @description The timing status.
    *
    * @ngdoc property
    * @name NodeController#timingStatus
    * @propertyOf NodeController
    * @returns {object} The timing status object
    */
    $scope.timingStatus = RequisitionsService.getTiming();

    /**
    * @description The foreign source (a.k.a the name of the requisition).
    * The default value is obtained from the $routeParams.
    *
    * @ngdoc property
    * @name NodeController#foreignSource
    * @propertyOf NodeController
    * @returns {string} The foreign source
    */
    $scope.foreignSource = $routeParams.foreignSource;

    /**
    * @description The foreign ID
    * The default value is obtained from the $routeParams.
    * For new nodes, the content must be '__new__'.
    *
    * @ngdoc property
    * @name NodeController#foreignId
    * @propertyOf NodeController
    * @returns {string} The foreign ID
    */
    $scope.foreignId = $routeParams.foreignId;

    /**
    * @description The isNew flag
    *
    * @ngdoc property
    * @name NodeController#isNew
    * @propertyOf NodeController
    * @returns {boolean} true, if the foreign ID is equal to '__new__'
    */
    $scope.isNew = $scope.foreignId === '__new__';

    /**
    * @description The node object
    *
    * @ngdoc property
    * @name NodeController#node
    * @propertyOf NodeController
    * @returns {object} The node object
    */
    $scope.node = {};

    /**
    * @description The available configured categories
    *
    * @ngdoc property
    * @name NodeController#availableCategories
    * @propertyOf NodeController
    * @returns {array} The categories
    */
    $scope.availableCategories = [];

    /**
    * @description The available configured locations
    *
    * @ngdoc property
    * @name NodeController#availableLocations
    * @propertyOf NodeController
    * @returns {array} The locations
    */
    $scope.availableLocations = [];

    /**
    * @description The list of black-listed foreign IDs.
    * The foreignId must be unique within the requisition.
    * For an existing node, the foreignId should not be changed.
    * For new nodes, the foreignId must be validated.
    *
    * @ngdoc property
    * @name NodeController#foreignIdBlackList
    * @propertyOf NodeController
    * @returns {array} The list of black-listed foreign IDs.
    */
    $scope.foreignIdBlackList = [];

    /**
    * @description Goes to specific URL warning about changes if exist.
    *
    * @name NodeController:goTo
    * @ngdoc method
    * @methodOf NodeController
    * @param {string} url The URL to go
    */
    $scope.goTo = function(url) {
      const doGoTo = function() {
        $window.location.href = url;
      };
      if (this.nodeForm.$dirty) {
        bootbox.dialog({
          message: 'There are changes on the current node. Are you sure you want to cancel ?',
          title: 'Cancel Changes',
          buttons: {
            success: {
              label: 'Yes',
              className: 'btn-primary',
              callback: doGoTo
            },
            main: {
              label: 'No',
              className: 'btn-secondary'
            }
          }
        });
      } else {
        doGoTo();
      }
    };

    /**
    * @description Goes back to requisitions list (navigation)
    *
    * @name NodeController:goTop
    * @ngdoc method
    * @methodOf NodeController
    */
    $scope.goTop = function() {
      $scope.goTo(Configuration.baseHref + '#/requisitions');
    };

    /**
    * @description Goes back to requisition editor (navigation)
    *
    * @name NodeController:goBack
    * @ngdoc method
    * @methodOf NodeController
    */
    $scope.goBack = function() {
      $scope.goTo(Configuration.baseHref + '#/requisitions/' + encodeURIComponent($scope.foreignSource));
    };

    /**
    * @description Goes to the vertical layout page (navigation)
    *
    * @name NodeController:goVerticalLayout
    * @ngdoc method
    * @methodOf NodeController
    */
    $scope.goVerticalLayout = function() {
      $cookies.put('use_requisitions_node_vertical_layout', 'true');
      $scope.goTo(Configuration.baseHref + '#/requisitions/' + encodeURIComponent($scope.foreignSource) + '/nodes/' + encodeURIComponent($scope.foreignId) + '/vertical');
    };

    /**
    * @description Goes to the horizontal layout page (navigation)
    *
    * @name NodeController:goHorizontalLayout
    * @ngdoc method
    * @methodOf NodeController
    */
    $scope.goHorizontalLayout = function() {
      $cookies.put('use_requisitions_node_vertical_layout', 'false');
      $scope.goTo(Configuration.baseHref + '#/requisitions/' + encodeURIComponent($scope.foreignSource) + '/nodes/' + encodeURIComponent($scope.foreignId));
    };

    /**
    * @description Shows an error to the user
    *
    * @name NodeController:errorHandler
    * @ngdoc method
    * @methodOf NodeController
    * @param {string} message The error message
    */
    $scope.errorHandler = function(message) {
      growl.error(message, {ttl: 10000});
    };

    /**
    * @description Generates a foreign Id
    *
    * @name NodeController:generateForeignId
    * @ngdoc method
    * @methodOf NodeController
    * @param {object} the form object associated with the foreignId
    */
    $scope.generateForeignId = function(formObj) {
      $scope.node.foreignId = String(new Date().getTime());
      formObj.$invalid = false;
    };

    /**
    * @description Shows the dialog for add/edit an asset field
    *
    * @name NodeController:editAsset
    * @ngdoc method
    * @methodOf NodeController
    * @param {integer} index The index of the asset to be edited
    * @param {boolean} isNew true, if the asset is new
    */
    $scope.editAsset = function(index, isNew) {
      const form = this.nodeForm;
      const assetToEdit = $scope.node.assets[index];
      const assetsBlackList = [];
      angular.forEach($scope.node.assets, function(asset) {
        assetsBlackList.push(asset.name);
      });

      const modalInstance = $uibModal.open({
        backdrop: 'static',
        keyboard: false,
        controller: 'AssetController',
        templateUrl: assetView,
        resolve: {
          asset: function() { return angular.copy(assetToEdit); },
          assetsBlackList: function() { return assetsBlackList; }
        }
      });

      modalInstance.result.then(function(result) {
        angular.copy(result, assetToEdit);
        form.$dirty = true;
      }, function() {
        if (isNew) {
          $scope.node.assets.pop();
        }
      });
    };

    /**
    * @description Removes an asset from the local node
    *
    * @name NodeController:removeAsset
    * @ngdoc method
    * @methodOf NodeController
    * @param {integer} index The index of the asset to be removed
    */
    $scope.removeAsset = function(index) {
      $scope.node.assets.splice(index, 1);
      this.nodeForm.$dirty = true;
    };

    /**
    * @description Adds a new asset to the local node
    *
    * @name NodeController:addAsset
    * @ngdoc method
    * @methodOf NodeController
    */
    $scope.addAsset = function() {
      $scope.editAsset($scope.node.addNewAsset(), true);
    };

    /**
     * @description Should be called when the meta-data tab is selected
     *
     * @name NodeController:onMetadataTabSelect
     * @ngdoc method
     * @methodOf NodeController
     */
    $scope.onMetadataTabSelect = function() {
      // Before switching over to the tab, let's delete any entries that reference entities which no longer exist
      // i.e. in the case that meta-data was associated with an interface, and that interface is now deleted
      $scope.node.metaData.removeEntriesForMissingScopedEntities();
    };

    $scope.deleteNode = function(node) {
      bootbox.confirm('Are you sure you want to delete the current node?', function(ok) {
      if (ok) {
        RequisitionsService.startTiming();
        RequisitionsService.deleteNode(node)
            .then(function() {
              $scope.nodeForm.$setPristine(); // Ignore dirty state
              $scope.goBack();
              // If node was just created, it has no label yet
              if (node.nodeLabel) {
                growl.success('The node ' + escape(node.nodeLabel) + ' has been deleted.');
              } else {
                growl.success('The node has been deleted.');
              }
            },
            $scope.errorHandler
        );
      }
      });
    };

    /**
     * @description Shows the dialog for add/edit an metaData entry
     *
     * @name NodeController:editMetaData
     * @ngdoc method
     * @methodOf NodeController
     * @param {object} entry The metaData entry to be edited
     * @param {boolean} isNew true, if the metaData entry is new
     */
    $scope.editMetaData = function(entry, isNew) {
        const form = this.nodeForm;

      const modalInstance = $uibModal.open({
            backdrop: 'static',
            keyboard: false,
            controller: 'MetaDataController',
            templateUrl: metaDataView,
            resolve: {
                node: function() { return angular.copy($scope.node); },
                entry: function() { return angular.copy(entry); }
            }
        });

        modalInstance.result.then(function(result) {
            angular.copy(result, entry);
            form.$dirty = true;
            if (isNew) {
              $scope.node.metaData.addEntry(entry);
            }
        });
    };

    /**
     * @description Removes an metaData entry from the local node
     *
     * @name NodeController:removeMetaData
     * @ngdoc method
     * @methodOf NodeController
     * @param {object} entry The index of the metaData entry to be removed
     */
    $scope.removeMetaData = function(entry) {
      $scope.node.metaData.removeEntry(entry);
      this.nodeForm.$dirty = true;
    };

    /**
     * @description Adds a new metaData entry to the local node
     *
     * @name NodeController:addMetaData
     * @ngdoc method
     * @methodOf NodeController
     */
    $scope.addMetaData = function() {
        $scope.editMetaData(new RequisitionMetaDataEntry(), true);
    };

    /**
    * @description Shows a modal dialog for add/edit an interface
    *
    * @name NodeController:editInterface
    * @ngdoc method
    * @methodOf NodeController
    * @param {integer} index The index of the interface to be edited
    * @param {boolean} isNew true, if the interface is new
    */
    $scope.editInterface = function(index, isNew) {
      const form = this.nodeForm;
      const intfToEdit = $scope.node.interfaces[index];
      const foreignSource = $scope.foreignSource;
      const foreignId = $scope.foreignId;
      const ipBlackList = [];
      angular.forEach($scope.node.interfaces, function(intf) {
        ipBlackList.push(intf.ipAddress);
      });

      const modalInstance = $uibModal.open({
        backdrop: 'static',
        keyboard: false,
        controller: 'InterfaceController',
        templateUrl: interfaceView,
        resolve: {
          foreignId: function() { return foreignId; },
          foreignSource: function() { return foreignSource; },
          requisitionInterface: function() { return angular.copy(intfToEdit); },
          ipBlackList: function() { return ipBlackList; },
          primaryInterface : function() { return $scope.getPrimaryAddress();}
        }
      });

      modalInstance.result.then(function(result) {
        angular.copy(result, intfToEdit);
        form.$dirty = true;
      }, function() {
        if (isNew) {
          $scope.node.interfaces.pop();
        }
      });
    };

    /**
    * @description Removes an interface from the local node
    *
    * @name NodeController:removeInterface
    * @ngdoc method
    * @methodOf NodeController
    * @param {integer} index The index of the interface to be removed
    */
    $scope.removeInterface = function(index) {
      $scope.node.interfaces.splice(index, 1);
      this.nodeForm.$dirty = true;
    };

    /**
    * @description Adds a new interface to the local node
    *
    * @name NodeController:addInterface
    * @ngdoc method
    * @methodOf NodeController
    */
    $scope.addInterface = function() {
      $scope.editInterface($scope.node.addNewInterface(), true);
    };

    /**
    * @description Removes a category from the local node
    *
    * @name NodeController:removeCategory
    * @ngdoc method
    * @methodOf NodeController
    * @param {integer} index The index of the category to be removed
    */
    $scope.removeCategory = function(index) {
      $scope.node.categories.splice(index, 1);
      this.nodeForm.$dirty = true;
    };

    /**
    * @description Adds a new category to the local node
    *
    * @name NodeController:addCategory
    * @ngdoc method
    * @methodOf NodeController
    */
    $scope.addCategory = function() {
      $scope.node.addNewCategory();
      this.nodeForm.$dirty = true;
    };

    /**
    * @description Saves the local node on the server
    *
    * @name NodeController:save
    * @ngdoc method
    * @methodOf NodeController
    */
    $scope.save = function() {
      const form = this.nodeForm;
      RequisitionsService.startTiming();
      RequisitionsService.saveNode($scope.node).then(
        function() { // success
          growl.success('The node ' + escape($scope.node.nodeLabel) + ' has been saved.');
          $scope.foreignId = $scope.node.foreignId;
          form.$dirty = false;
        },
        $scope.errorHandler
      );
    };

    /**
    * @description Refresh the local node from the server
    *
    * @name NodeController:refresh
    * @ngdoc method
    * @methodOf NodeController
    */
    $scope.refresh = function() {
      growl.success('Retrieving node ' + escape($scope.foreignId) + ' from requisition ' + escape($scope.foreignSource) + '...');
      RequisitionsService.getNode($scope.foreignSource, $scope.foreignId).then(
        function(node) { // success
          $scope.node = node;
        },
        $scope.errorHandler
      );
    };

    /**
    * @description Get the unused available categories
    *
    * @name NodeController:getAvailableCategories
    * @ngdoc method
    * @methodOf NodeController
    * @returns {array} the unused available categories
    */
    $scope.getAvailableCategories = function() {
      const categories = [];
      angular.forEach($scope.availableCategories, function(category) {
        let found = false;
        angular.forEach($scope.node.categories, function(c) {
          if (c.name === category) {
            found = true;
          }
        });
        if (!found) {
          categories.push(category);
        }
      });
      return categories;
    };

    /**
    * @description Gets the primary IP address
    *
    * @name NodeController:getPrimaryAddress
    * @ngdoc method
    * @methodOf NodeController
    * @returns {string} the primary IP address or 'N/A' if it doesn't exist.
    */
    $scope.getPrimaryAddress = function() {
      const ip = $scope.node.getPrimaryIpAddress();
      return ip ? ip : null;
    };

    // Initialization of the node's page for either adding a new node or editing an existing node

    if ($scope.isNew) {
      $scope.node = new RequisitionNode($scope.foreignSource, {});
    } else {
      $scope.refresh();
    }

    // Initialize categories
    RequisitionsService.getAvailableCategories().then(
      function(categories) { // success
        $scope.availableCategories = categories;
      },
      $scope.errorHandler
    );

    // Initialize locations
    RequisitionsService.getAvailableLocations().then(
      function(locations) { // success
        $scope.availableLocations = locations;
      },
      $scope.errorHandler
    );

    // Initialize foreign-id black list (thanks to the cache, this call is not expensive)
    // TODO: What if the cache is disabled ?
    RequisitionsService.getRequisition($scope.foreignSource).then(
      function(requisition) {
        angular.forEach(requisition.nodes, function(node) {
          $scope.foreignIdBlackList.push(node.foreignId);
        });
      },
      $scope.errorHandler
    );

  }]);

}());
