/*
 Licensed to the Apache Software Foundation (ASF) under one or more
 contributor license agreements.  See the NOTICE file distributed with
 this work for additional information regarding copyright ownership.
 The ASF licenses this file to You 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

 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.
*/

solrAdminApp.controller('CloudController',
    function($scope, $location, Zookeeper, Constants, Collections, System, Metrics, MetricsExtractor, ZookeeperStatus) {

        $scope.showDebug = false;

        $scope.$on("cloud-dump", function(event) {
            $scope.showDebug = true;
        });

        $scope.closeDebug = function() {
            $scope.showDebug = false;
        };

        var view = $location.search().view ? $location.search().view : "nodes";
        if (view === "tree") {
            $scope.resetMenu("cloud-tree", Constants.IS_ROOT_PAGE);
            treeSubController($scope, Zookeeper);
        } else if (view === "graph") {
            $scope.resetMenu("cloud-graph", Constants.IS_ROOT_PAGE);
            graphSubController($scope, Zookeeper, false);
        } else if (view === "nodes") {
            $scope.resetMenu("cloud-nodes", Constants.IS_ROOT_PAGE);
            nodesSubController($scope, Collections, System, Metrics, MetricsExtractor);
        } else if (view === "zkstatus") {
            $scope.resetMenu("cloud-zkstatus", Constants.IS_ROOT_PAGE);
            zkStatusSubController($scope, ZookeeperStatus, false);
        }
    }
);

function getOrCreateObj(name, object) {
  if (name in object) {
    entry = object[name];
  } else {
    entry = {};
    object[name] = entry;
  }
  return entry;
}

function getOrCreateList(name, object) {
  if (name in object) {
    entry = object[name];
  } else {
    entry = [];
    object[name] = entry;
  }
  return entry;
}

function ensureInList(string, list) {
  if (list.indexOf(string) === -1) {
    list.push(string);
  }
}

/* Puts a node name into the hosts structure */
function ensureNodeInHosts(node_name, hosts) {
  var hostName = node_name.split(":")[0];
  var host = getOrCreateObj(hostName, hosts);
  var hostNodes = getOrCreateList("nodes", host);
  ensureInList(node_name, hostNodes);
}

// from http://scratch99.com/web-development/javascript/convert-bytes-to-mb-kb/
function bytesToSize(bytes) {
  var sizes = ['b', 'Kb', 'Mb', 'Gb', 'Tb'];
  if (bytes === 0) return '0b';
  var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)));
  if (bytes === 0) return bytes + '' + sizes[i];
  return (bytes / Math.pow(1024, i)).toFixed(1) + '' + sizes[i];
}

function numDocsHuman(docs) {
  var sizes = ['', 'k', 'mn', 'bn', 'tn'];
  if (docs === 0) return '0';
  var i = parseInt(Math.floor(Math.log(docs) / Math.log(1000)));
  if (i === 0) return docs + '' + sizes[i];
  return (docs / Math.pow(1000, i)).toFixed(1) + '' + sizes[i];
}

/* Returns a style class depending on percentage */
var styleForPct = function (pct) {
  if (pct < 60) return "pct-normal";
  if (pct < 80) return "pct-warn";
  return "pct-critical"
};

function isNumeric(n) {
  return !isNaN(parseFloat(n)) && isFinite(n);
}

var nodesSubController = function($scope, Collections, System, Metrics, MetricsExtractor) {
  $scope.pageSize = 10;
  $scope.showNodes = true;
  $scope.showTree = false;
  $scope.showGraph = false;
  $scope.showData = false;
  $scope.showAllDetails = false;
  $scope.showDetails = {};
  $scope.from = 0;
  $scope.to = $scope.pageSize - 1;
  $scope.filterType = "node"; // Pre-initialize dropdown

  $scope.toggleAllDetails = function() {
    $scope.showAllDetails = !$scope.showAllDetails;
    for (var node in $scope.nodes) {
      $scope.showDetails[node] = $scope.showAllDetails;
    }
    for (var host in $scope.hosts) {
      $scope.showDetails[host] = $scope.showAllDetails;
    }
  };

  $scope.toggleDetails = function(key) {
    $scope.showDetails[key] = !$scope.showDetails[key] === true;
  };

  $scope.toggleHostDetails = function(key) {
    $scope.showDetails[key] = !$scope.showDetails[key] === true;
    for (var nodeId in $scope.hosts[key].nodes) {
      var node = $scope.hosts[key].nodes[nodeId];
      $scope.showDetails[node] = $scope.showDetails[key];
    }
  };

  $scope.nextPage = function() {
    $scope.from += parseInt($scope.pageSize);
    $scope.reload();
  };

  $scope.previousPage = function() {
    $scope.from = Math.max(0, $scope.from - parseInt($scope.pageSize));
    $scope.reload();
  };

  // Checks if this node is the first (alphabetically) for a given host. Used to decide rowspan in table
  $scope.isFirstNodeForHost = function(node) {
    var hostName = node.split(":")[0];
    var nodesInHost = $scope.filteredNodes.filter(function (node) {
      return node.split(":")[0] === hostName;
    });
    return nodesInHost[0] === node;
  };

  // Returns the first live node for this host, to make sure we pick host-level metrics from a live node
  $scope.firstLiveNodeForHost = function(key) {
    var hostName = key.split(":")[0];
    var liveNodesInHost = $scope.filteredNodes.filter(function (key) {
      return key.split(":")[0] === hostName;
    }).filter(function (key) {
      return $scope.live_nodes.includes(key);
    });
    return liveNodesInHost.length > 0 ? liveNodesInHost[0] : key;
  };

  // Initializes the cluster state, list of nodes, collections etc
  $scope.initClusterState = function() {
    var nodes = {};
    var hosts = {};
    var live_nodes = [];

    // We build a node-centric view of the cluster state which we can easily consume to render the table
    Collections.status(function (data) {
      // Fetch cluster state from collections API and invert to a nodes structure
      for (var name in data.cluster.collections) {
        var collection = data.cluster.collections[name];
        collection.name = name;
        var shards = collection.shards;
        collection.shards = [];
        for (var shardName in shards) {
          var shard = shards[shardName];
          shard.name = shardName;
          shard.collection = collection.name;
          var replicas = shard.replicas;
          shard.replicas = [];
          for (var replicaName in replicas) {
            var core = replicas[replicaName];
            core.name = replicaName;
            core.replica = core['core'].replace(/.*_(replica_.*)$/, '\$1');
            core.collection = collection.name;
            core.shard = shard.name;
            core.shard_state = shard.state;
            core.label = core['collection'] + "_"
              + (core['shard'] + "_").replace(/shard(\d+)_/, 's\$1')
              + core['replica'].replace(/replica_?[ntp]?(\d+)/, 'r\$1');

            var node_name = core['node_name'];
            var node = getOrCreateObj(node_name, nodes);
            var cores = getOrCreateList("cores", node);
            cores.push(core);
            node['base_url'] = core.base_url;
            node['id'] = core.base_url.replace(/[^\w\d]/g, '');
            node['host'] = node_name.split(":")[0];
            var collections = getOrCreateList("collections", node);
            ensureInList(core.collection, collections);
            ensureNodeInHosts(node_name, hosts);
          }
        }
      }

      live_nodes = data.cluster.live_nodes;
      for (n in data.cluster.live_nodes) {
        node = data.cluster.live_nodes[n];
        if (!(node in nodes)) {
          var hostName = node.split(":")[0];
          nodes[node] = {};
          nodes[node]['host'] = hostName;
        }
        ensureNodeInHosts(node, hosts);
      }

      // Make sure nodes are sorted alphabetically to align with rowspan in table
      for (var host in hosts) {
        hosts[host].nodes.sort();
      }

      $scope.nodes = nodes;
      $scope.hosts = hosts;
      $scope.live_nodes = live_nodes;

      $scope.Math = window.Math;
      $scope.reload();
    });
  };

  $scope.filterInput = function() {
    $scope.from = 0;
    $scope.to = $scope.pageSize - 1;
    $scope.reload();
  };

  /*
    Reload will fetch data for the current page of the table and thus refresh numbers.
    It is also called whenever a filter or paging action is executed
   */
  $scope.reload = function() {
    var nodes = $scope.nodes;
    var node_keys = Object.keys(nodes);
    var hosts = $scope.hosts;
    var live_nodes = $scope.live_nodes;
    var hostNames = Object.keys(hosts);
    hostNames.sort();
    var pageSize = isNumeric($scope.pageSize) ? $scope.pageSize : 10;

    // Calculate what nodes that will show on this page
    var nodesToShow = [];
    var nodesParam;
    var hostsToShow = [];
    var filteredNodes;
    var filteredHosts;
    var isFiltered = false;
    switch ($scope.filterType) {
      case "node":  // Find what nodes match the node filter
        if ($scope.nodeFilter) {
          filteredNodes = node_keys.filter(function (node) {
            return node.indexOf($scope.nodeFilter) !== -1;
          });
        }
        break;

      case "collection": // Find what collections match the collection filter and what nodes that have these collections
        if ($scope.collectionFilter) {
          candidateNodes = {};
          nodesCollections = [];
          for (var i = 0 ; i < node_keys.length ; i++) {
            var node_name = node_keys[i];
            var node = nodes[node_name];
            nodeColl = {};
            nodeColl['node'] = node_name;
            collections = {};
            node.cores.forEach(function(core) {
              collections[core.collection] = true;
            });
            nodeColl['collections'] = Object.keys(collections);
            nodesCollections.push(nodeColl);
          }
          nodesCollections.forEach(function(nc) {
            matchingColls = nc['collections'].filter(function (collection) {
              return collection.indexOf($scope.collectionFilter) !== -1;
            });
            if (matchingColls.length > 0) {
              candidateNodes[nc.node] = true;
            }
          });
          filteredNodes = Object.keys(candidateNodes);
        }
        break;

      case "health":

    }

    if (filteredNodes) {
      // If filtering is active, calculate what hosts contain the nodes that match the filters
      isFiltered = true;
      filteredHosts = filteredNodes.map(function (node) {
        return node.split(":")[0];
      }).filter(function (item, index, self) {
        return self.indexOf(item) === index;
      });
    } else {
      filteredNodes = node_keys;
      filteredHosts = hostNames;
    }
    filteredNodes.sort();
    filteredHosts.sort();

    // Find what hosts & nodes (from the filtered set) that should be displayed on current page
    for (var id = $scope.from ; id < $scope.from + pageSize && filteredHosts[id] ; id++) {
      var hostName = filteredHosts[id];
      hostsToShow.push(hostName);
      if (isFiltered) { // Only show the nodes per host matching active filter
        nodesToShow = nodesToShow.concat(filteredNodes.filter(function (node) {
          return node.split(":")[0] === hostName;
        }));
      } else {
        nodesToShow = nodesToShow.concat(hosts[hostName]['nodes']);
      }
    }
    var liveNodesToShow = nodesToShow.filter(function (node) {
      return live_nodes.includes(node);
    });
    var deadNodes = nodesToShow.filter(function (node) {
      return !live_nodes.includes(node);
    });
    deadNodes.forEach(function (node) {
      nodes[node]['dead'] = true;
    });
    $scope.nextEnabled = $scope.from + pageSize < filteredHosts.length;
    $scope.prevEnabled = $scope.from - pageSize >= 0;
    nodesToShow.sort();
    hostsToShow.sort();

    /*
     Fetch system info for all selected nodes
     Pick the data we want to display and add it to the node-centric data structure
      */
    System.get({"nodes": liveNodesToShow.join(',')}, function (systemResponse) {
      for (var node in systemResponse) {
        if (node in nodes) {
          var s = systemResponse[node];
          nodes[node]['system'] = s;
          var memTotal = s.system.totalPhysicalMemorySize;
          var memFree = s.system.freePhysicalMemorySize;
          var memPercentage = Math.floor((memTotal - memFree) / memTotal * 100);
          nodes[node]['memUsedPct'] = memPercentage;
          nodes[node]['memUsedPctStyle'] = styleForPct(memPercentage);
          nodes[node]['memTotal'] = bytesToSize(memTotal);
          nodes[node]['memFree'] = bytesToSize(memFree);
          nodes[node]['memUsed'] = bytesToSize(memTotal - memFree);

          var heapTotal = s.jvm.memory.raw.total;
          var heapFree = s.jvm.memory.raw.free;
          var heapPercentage = Math.floor((heapTotal - heapFree) / heapTotal * 100);
          nodes[node]['heapUsed'] = bytesToSize(heapTotal - heapFree);
          nodes[node]['heapUsedPct'] = heapPercentage;
          nodes[node]['heapUsedPctStyle'] = styleForPct(heapPercentage);
          nodes[node]['heapTotal'] = bytesToSize(heapTotal);
          nodes[node]['heapFree'] = bytesToSize(heapFree);

          var jvmUptime = s.jvm.jmx.upTimeMS / 1000; // Seconds
          nodes[node]['jvmUptime'] = secondsForHumans(jvmUptime);
          nodes[node]['jvmUptimeSec'] = jvmUptime;

          nodes[node]['uptime'] = (s.system.uptime || "unknown").replace(/.*up (.*?,.*?),.*/, "$1");
          nodes[node]['loadAvg'] = Math.round(s.system.systemLoadAverage * 100) / 100;
          nodes[node]['cpuPct'] = Math.ceil(s.system.processCpuLoad * 100);
          nodes[node]['cpuPctStyle'] = styleForPct(Math.ceil(s.system.processCpuLoad));
          nodes[node]['maxFileDescriptorCount'] = s.system.maxFileDescriptorCount;
          nodes[node]['openFileDescriptorCount'] = s.system.openFileDescriptorCount;
        }
      }
    });

    /*
     Fetch metrics for all selected nodes in parallel. Make one request per node.
     Only pull the metrics that we'll show to save bandwidth.
      */
    var metricsNameParam = "solr_disk_space_megabytes,solr_core_index_size_megabytes,solr_core_indexsearcher_index_num_docs,solr_core_indexsearcher_index_docs,solr_core_indexsearcher_open_time_milliseconds";

    // Create array of promises (one per node)
    var metricsPromises = [];
    liveNodesToShow.forEach(function(node) {
      var promise = Metrics.get({
        node: node,
        name: metricsNameParam
      }).$promise.then(
        function(response) {
          // Success - return the parsed metrics with node identifier
          return {
            node: node,
            metrics: response.metrics,
            success: true
          };
        },
        function(error) {
          // Failure - log and return error marker
          console.error('Failed to fetch metrics from node ' + node + ':', error);
          return {
            node: node,
            success: false,
            error: error
          };
        }
      );
      metricsPromises.push(promise);
    });

    // Wait for all requests to complete (success or failure)
    Promise.all(metricsPromises).then(function(results) {
      // Separate successful and failed results
      var successfulResults = results.filter(function(r) { return r.success; });
      var failedResults = results.filter(function(r) { return !r.success; });

      // Log any failures
      if (failedResults.length > 0) {
        console.warn('Failed to fetch metrics from ' + failedResults.length + ' node(s):',
                     failedResults.map(function(r) { return r.node; }));
      }

      // If all nodes failed, show error state
      if (successfulResults.length === 0) {
        console.error('Failed to fetch metrics from all nodes');
        $scope.metricsError = true;
        return;
      }

      // Merge all successful metrics responses, passing node info along
      var parsedMetrics = mergePrometheusMetrics(successfulResults);

      if (!parsedMetrics) {
        console.error('Failed to merge metrics');
        $scope.metricsError = true;
        return;
      }

      // Now process the merged metrics the same way as before
      for (var i = 0; i < nodesToShow.length; i++) {
          var node = nodesToShow[i];
          if (!nodes[node]) continue;

          nodes[node]['metrics'] = parsedMetrics;

          // Extract disk metrics with node filter
          var diskMetrics = MetricsExtractor.extractDiskMetrics(parsedMetrics, { node: node });
          if (diskMetrics) {
            var diskTotal = diskMetrics.totalSpace || 0;
            var diskFree = diskMetrics.usableSpace || 0;
            var diskPercentage = diskTotal > 0 ? Math.floor((diskTotal - diskFree) / diskTotal * 100) : 0;
            nodes[node]['diskUsedPct'] = diskPercentage;
            nodes[node]['diskUsedPctStyle'] = styleForPct(diskPercentage);
            nodes[node]['diskTotal'] = bytesToSize(diskTotal);
            nodes[node]['diskFree'] = bytesToSize(diskFree);
          }

          // These are the cores we _expect_ to find on this node according to the CLUSTERSTATUS
          var cores = nodes[node]['cores'];
          if (!cores || typeof cores !== 'object') {
            cores = {};
            nodes[node]['cores'] = cores;
          }
          var indexSizeTotal = 0;
          var indexSizeMax = 0;
          var docsTotal = 0;
          var graphData = [];

          for (var coreId in cores) {
            var core = cores[coreId];

            if (core['shard_state'] !== 'active' || core['state'] !== 'active') {
              // If core state is not active, display the real state, or if shard is inactive, display that
              var labelState = (core['state'] !== 'active') ? core['state'] : core['shard_state'];
              core['label'] += "_(" + labelState + ")";
            }

            // Build full core name for label matching
            // Prometheus metrics use format: "collection_shard_replica"
            var fullCoreName = core['collection'] + '_' + core['shard'] + '_' + core['replica'];
            var coreLabels = { core: fullCoreName, node: node };

            // Extract metrics using helpers (with node filter)
            var size = MetricsExtractor.extractCoreIndexSize(parsedMetrics, coreLabels);
            var searcherMetrics = MetricsExtractor.extractSearcherMetrics(parsedMetrics, coreLabels);

            core['sizeInBytes'] = size;
            core['size'] = bytesToSize(size);
            indexSizeTotal = indexSizeTotal + size;
            indexSizeMax = size > indexSizeMax ? size : indexSizeMax;

            var numDocs = searcherMetrics.numDocs || 0;
            core['numDocs'] = numDocs;
            core['numDocsHuman'] = numDocsHuman(numDocs);
            core['avgSizePerDoc'] = bytesToSize(numDocs === 0 ? 0 : size / numDocs);

            var deletedDocs = searcherMetrics.deletedDocs || 0;
            core['deletedDocs'] = deletedDocs;
            core['deletedDocsHuman'] = numDocsHuman(deletedDocs);

            var warmupTime = searcherMetrics.warmupTime || 0;
            core['warmupTime'] = warmupTime;

            docsTotal += core['numDocs'];
          }

          for (var coreId in cores) {
            var core = cores[coreId];
            var graphObj = {};
            graphObj['label'] = core['label'];
            graphObj['size'] = core['sizeInBytes'];
            graphObj['sizeHuman'] = core['size'];
            graphObj['pct'] = indexSizeMax > 0 ? (core['sizeInBytes'] / indexSizeMax) * 100 : 0;
            graphData.push(graphObj);
          }

          // Note: cores is an object (key-value pairs), not an array, so we cannot sort it directly.
          // The sorting is handled separately for graphData which is an array.

          graphData.sort(function (a, b) {
            return b.size - a.size
          });

          nodes[node]['graphData'] = graphData;
          nodes[node]['numDocs'] = numDocsHuman(docsTotal);
          nodes[node]['sizeInBytes'] = indexSizeTotal;
          nodes[node]['size'] = bytesToSize(indexSizeTotal);
          nodes[node]['sizePerDoc'] = docsTotal === 0 ? '0b' : bytesToSize(indexSizeTotal / docsTotal);

          // Build the d3 powered bar chart
          $('#chart' + nodes[node]['id']).empty();
          var chart = d3.select('#chart' + nodes[node]['id']).append('div').attr('class', 'chart');

          // Add one div per bar which will group together both labels and bars
          var g = chart.selectAll('div')
              .data(nodes[node]['graphData']).enter()
              .append('div');

          // Add the bars
          var bars = g.append("div")
              .attr("class", "rect")
              .text(function (d) {
                return d.label + ':\u00A0\u00A0' + d.sizeHuman;
              });

          // Execute the transition to show the bars
          bars.transition()
              .ease('elastic')
              .style('width', function (d) {
                return d.pct + '%';
              });
      }
    });
    $scope.nodes = nodes;
    $scope.hosts = hosts;
    $scope.live_nodes = live_nodes;
    $scope.nodesToShow = nodesToShow;
    $scope.hostsToShow = hostsToShow;
    $scope.filteredNodes = filteredNodes;
    $scope.filteredHosts = filteredHosts;
  };

  /**
   * Merge multiple Prometheus metrics objects into a single object.
   * Each result has {node: nodeName, metrics: {...}}
   * Merging combines all samples from all sources under the same metric names,
   * and adds a 'node' label to each sample to track which node it came from.
   *
   * @param {Array} resultsArray - Array of {node, metrics} objects
   * @returns {Object} Merged metrics object
   */
  function mergePrometheusMetrics(resultsArray) {
    var merged = {};

    resultsArray.forEach(function(result) {
      if (!result || !result.metrics) return;

      var nodeName = result.node;
      var metrics = result.metrics;

      for (var metricName in metrics) {
        if (!metrics.hasOwnProperty(metricName)) continue;

        var metric = metrics[metricName];

        if (!merged[metricName]) {
          // First time seeing this metric - initialize
          merged[metricName] = {
            type: metric.type,
            help: metric.help,
            samples: []
          };
        }

        // Add all samples from this metric, injecting the node label
        if (metric.samples && Array.isArray(metric.samples)) {
          metric.samples.forEach(function(sample) {
            // Create a copy of the sample with the node label added
            var sampleWithNode = {
              metricName: sample.metricName,
              labels: Object.assign({}, sample.labels || {}, {node: nodeName}),
              value: sample.value,
              metricSuffix: sample.metricSuffix
            };
            if (sample.timestamp !== undefined) {
              sampleWithNode.timestamp = sample.timestamp;
            }
            merged[metricName].samples.push(sampleWithNode);
          });
        }
      }
    });

    return merged;
  }

  // Initialize cluster state
  $scope.initClusterState();
};

var zkStatusSubController = function($scope, ZookeeperStatus) {
    $scope.showZkStatus = true;
    $scope.showNodes = false;
    $scope.showTree = false;
    $scope.showGraph = false;
    $scope.tree = {};
    $scope.showData = false;
    $scope.showDetails = false;

    $scope.toggleDetails = function() {
      $scope.showDetails = !$scope.showDetails === true;
    };

    $scope.initZookeeper = function() {
      ZookeeperStatus.monitor({}, function(data) {
        $scope.zkState = data.zkStatus;
        $scope.mainKeys = ["ok", "clientPort", "secureClientPort", "zk_server_state", "zk_version",
          "zk_approximate_data_size", "zk_znode_count", "zk_num_alive_connections"];
        $scope.detailKeys = ["dataDir", "dataLogDir",
          "zk_avg_latency", "zk_max_file_descriptor_count", "zk_watch_count",
          "zk_packets_sent", "zk_packets_received",
          "tickTime", "maxClientCnxns", "minSessionTimeout", "maxSessionTimeout"];
        $scope.ensembleMainKeys = ["serverId", "electionPort", "quorumPort", "role"];
        $scope.ensembleDetailKeys = ["peerType", "electionAlg", "initLimit", "syncLimit",
          "zk_followers", "zk_synced_followers", "zk_pending_syncs"];
        $scope.notEmptyRow = function(key) {
          for (hostId in $scope.zkState.details) {
            if (key in $scope.zkState.details[hostId]) return true;
          }
          return false;
        };
      });
    };

    $scope.initZookeeper();
};

var treeSubController = function($scope, Zookeeper) {
    $scope.showZkStatus = false;
    $scope.showTree = true;
    $scope.showGraph = false;
    $scope.tree = {};
    $scope.showData = false;

    $scope.showTreeLink = function(link) {
        var path = decodeURIComponent(link.replace(/.*[\\?&]path=([^&#]*).*/, "$1"));
        if (path === '/security.json' && !$scope.isPermitted(permissions.SECURITY_READ_PERM)) {
          // TODO: Set proper data here to display a warning in right panel "You lack the required role to see this file"
          $scope.znode = {};
          $scope.showData = false;
        } else {
          Zookeeper.detail({path: path}, function(data) {
              $scope.znode = data.znode;
              if (data.znode.path.endsWith("/managed-schema") || data.znode.path.endsWith(".xml.bak")) {
                $scope.lang = "xml";
              } else {
                var lastPathElement = data.znode.path.split( '/' ).pop();
                var lastDotAt = lastPathElement ? lastPathElement.lastIndexOf('.') : -1;
                $scope.lang = lastDotAt != -1 ? lastPathElement.substring(lastDotAt+1) : "txt";
              }
              $scope.showData = true;
          });
        }
    };

    $scope.hideData = function() {
        $scope.showData = false;
    };

    $scope.initTree = function() {
      Zookeeper.simple(function(data) {
        $scope.tree = data.tree;
      });
    };

    $scope.initTree();
};

/**
 * Translates seconds into human readable format of seconds, minutes, hours, days, and years
 *
 * @param  {number} seconds The number of seconds to be processed
 * @return {string}         The phrase describing the amount of time
 */
function secondsForHumans ( seconds ) {
    var levels = [
        [Math.floor(seconds / 31536000), 'y'],
        [Math.floor((seconds % 31536000) / 86400), 'd'],
        [Math.floor(((seconds % 31536000) % 86400) / 3600), 'h'],
        [Math.floor((((seconds % 31536000) % 86400) % 3600) / 60), 'm']
    ];
    var returntext = '';

    for (var i = 0, max = levels.length; i < max; i++) {
        if ( levels[i][0] === 0 ) continue;
        returntext += ' ' + levels[i][0] + levels[i][1];
    }
    return returntext.trim() === '' ? '0m' : returntext.trim();
}

var graphSubController = function ($scope, Zookeeper) {
    $scope.showZkStatus = false;
    $scope.showTree = false;
    $scope.showGraph = true;

    $scope.filterType = "status";

    $scope.helperData = {
        protocol: [],
        host: [],
        hostname: [],
        port: [],
        pathname: [],
        replicaType: [],
        base_url: [],
        core: [],
        node_name: [],
        state: [],
        core_node: []
    };

    $scope.next = function() {
        $scope.pos += $scope.rows;
        $scope.initGraph();
    };

    $scope.previous = function() {
        $scope.pos = Math.max(0, $scope.pos - $scope.rows);
        $scope.initGraph();
    };

    $scope.resetGraph = function() {
        $scope.pos = 0;
        $scope.initGraph();
    };

    $scope.initGraph = function() {
        Zookeeper.liveNodes(function (data) {
            var live_nodes = {};
            for (var c in data.tree[0].children) {
                live_nodes[data.tree[0].children[c].text] = true;
            }

            var params = {view: "graph"};
            if ($scope.rows) {
                params.start = $scope.pos;
                params.rows = $scope.rows;
            }

            var filter = ($scope.filterType=='status') ? $scope.pagingStatusFilter : $scope.pagingFilter;

            if (filter) {
                params.filterType = $scope.filterType;
                params.filter = filter;
            }

            Zookeeper.clusterState(params, function (data) {
                    var state = $.parseJSON(data.znode.data);

                    var leaf_count = 0;
                    var graph_data = {
                        name: null,
                        children: []
                    };

                    for (var c in state) {
                        var shards = [];
                        for (var s in state[c].shards) {
                            var shard_status = state[c].shards[s].state;
                            shard_status = shard_status == 'inactive' ? 'shard-inactive' : shard_status;
                            var nodes = [];
                            for (var n in state[c].shards[s].replicas) {
                                leaf_count++;
                                var replica = state[c].shards[s].replicas[n]

                                var uri = replica.base_url;
                                var parts = uri.match(/^(\w+:)\/\/(([\w\d\.-]+)(:(\d+))?)(.+)$/);
                                var uri_parts = {
                                    protocol: parts[1],
                                    host: parts[2],
                                    hostname: parts[3],
                                    port: parseInt(parts[5] || 80, 10),
                                    pathname: parts[6],
                                    replicaType: replica.type,
                                    base_url: replica.base_url,
                                    core: replica.core,
                                    node_name: replica.node_name,
                                    state: replica.state,
                                    core_node: n
                                };

                                $scope.helperData.protocol.push(uri_parts.protocol);
                                $scope.helperData.host.push(uri_parts.host);
                                $scope.helperData.hostname.push(uri_parts.hostname);
                                $scope.helperData.port.push(uri_parts.port);
                                $scope.helperData.pathname.push(uri_parts.pathname);
                                $scope.helperData.replicaType.push(uri_parts.replicaType);
                                $scope.helperData.base_url.push(uri_parts.base_url);
                                $scope.helperData.core.push(uri_parts.core);
                                $scope.helperData.node_name.push(uri_parts.node_name);
                                $scope.helperData.state.push(uri_parts.state);
                                $scope.helperData.core_node.push(uri_parts.core_node);

                                var replica_status = replica.state;

                                if (!live_nodes[replica.node_name]) {
                                    replica_status = 'gone';
                                } else if(shard_status=='shard-inactive') {
                                    replica_status += ' ' + shard_status;
                                }

                                var node = {
                                    name: uri,
                                    data: {
                                        type: 'node',
                                        state: replica_status,
                                        leader: 'true' === replica.leader,
                                        uri: uri_parts
                                    }
                                };
                                nodes.push(node);
                            }

                            var shard = {
                                name: shard_status == "shard-inactive" ? s + ' (inactive)' : s,
                                data: {
                                    type: 'shard',
                                    state: shard_status,
                                    range: state[c].shards[s].range

                                },
                                children: nodes
                            };
                            shards.push(shard);
                        }

                        var collection = {
                            name: c,
                            data: {
                                type: 'collection',
                                pullReplicas: state[c].pullReplicas,
                                replicationFactor: state[c].replicationFactor,
                                router: state[c].router.name,
                                nrtReplicas: state[c].nrtReplicas,
                                tlogReplicas: state[c].tlogReplicas,
                                numShards: shards.length
                            },
                            children: shards
                        };
                        graph_data.children.push(collection);
                    }
                    $scope.helperData.protocol = $.unique($scope.helperData.protocol);
                    $scope.helperData.host = $.unique($scope.helperData.host);
                    $scope.helperData.hostname = $.unique($scope.helperData.hostname);
                    $scope.helperData.port = $.unique($scope.helperData.port);
                    $scope.helperData.pathname = $.unique($scope.helperData.pathname);
                    $scope.helperData.replicaType = $.unique($scope.helperData.replicaType);
                    $scope.helperData.base_url = $.unique($scope.helperData.base_url);
                    $scope.helperData.core = $.unique($scope.helperData.core);
                    $scope.helperData.node_name = $.unique($scope.helperData.node_name);
                    $scope.helperData.state = $.unique($scope.helperData.state);
                    $scope.helperData.core_node = $.unique($scope.helperData.core_node);

                    if (data.znode && data.znode.paging) {
                        $scope.showPaging = true;

                        var parr = data.znode.paging.split('|');
                        if (parr.length < 3) {
                            $scope.showPaging = false;
                        } else {
                            $scope.start = Math.max(parseInt(parr[0]), 0);
                            $scope.prevEnabled = ($scope.start > 0);
                            $scope.rows = parseInt(parr[1]);
                            $scope.total = parseInt(parr[2]);

                            if ($scope.rows == -1) {
                                $scope.showPaging = false;
                            } else {
                                var filterType = parr.length > 3 ? parr[3] : '';

                                if (filterType == '' || filterType == 'none') filterType = 'status';

                                +$('#cloudGraphPagingFilterType').val(filterType);

                                var filter = parr.length > 4 ? parr[4] : '';
                                var page = Math.floor($scope.start / $scope.rows) + 1;
                                var pages = Math.ceil($scope.total / $scope.rows);
                                $scope.last = Math.min($scope.start + $scope.rows, $scope.total);
                                $scope.nextEnabled = ($scope.last < $scope.total);
                            }
                        }
                    }
                    else {
                        $scope.showPaging = false;
                    }
                    $scope.graphData = graph_data;
                    $scope.leafCount = leaf_count;
                });
        });
    };

    $scope.initGraph();
    $scope.pos = 0;
};

solrAdminApp.directive('graph', function(Constants) {
    return {
        restrict: 'EA',
        scope: {
            data: "=",
            leafCount: "=",
            helperData: "=",
        },
        link: function (scope, element, attrs) {
            var helper_path_class = function (p) {
                var classes = ['link'];
                classes.push('lvl-' + p.target.depth);

                if (p.target.data && p.target.data.leader) {
                    classes.push('leader');
                }

                if (p.target.data && p.target.data.state) {
                    classes.push(p.target.data.state);
                }

                return classes.join(' ');
            };

            var helper_node_class = function (d) {
                var classes = ['node'];
                classes.push('lvl-' + d.depth);

                if (d.data && d.data.leader) {
                    classes.push('leader');
                }

                if (d.data && d.data.state) {
                    if(!(d.data.type=='shard' && d.data.state=='active')){
                        classes.push(d.data.state);
                    }
                }

                return classes.join(' ');
            };

            var helper_tooltip_text = function (d) {
                if (!d.data) {
                  return tooltip;
                }
                var tooltip;

                if (! d.data.type) {
                  return tooltip;
                }


                if (d.data.type == 'collection') {
                  tooltip = d.name + " {<br/> ";
                  tooltip += "numShards: [" + d.data.numShards + "],<br/>";
                  tooltip += "router: [" + d.data.router + "],<br/>";
                  tooltip += "replicationFactor: [" + d.data.replicationFactor + "],<br/>";
                  tooltip += "nrtReplicas: [" + d.data.nrtReplicas + "],<br/>";
                  tooltip += "pullReplicas: [" + d.data.pullReplicas + "],<br/>";
                  tooltip += "tlogReplicas: [" + d.data.tlogReplicas + "],<br/>";
                  tooltip += "}";
                } else if (d.data.type == 'shard') {
                  tooltip = d.name + " {<br/> ";
                  tooltip += "range: [" + d.data.range + "],<br/>";
                  tooltip += "state: [" + d.data.state + "],<br/>";
                  tooltip += "}";
                } else if (d.data.type == 'node') {
                  tooltip = d.data.uri.core_node + " {<br/>";

                  if (0 !== scope.helperData.core.length) {
                      tooltip += "core: [" + d.data.uri.core + "],<br/>";
                  }

                  if (0 !== scope.helperData.node_name.length) {
                      tooltip += "node_name: [" + d.data.uri.node_name + "],<br/>";
                  }
                  tooltip += "}";
                }

                return tooltip;
            };

            var helper_node_text = function (d) {
                if (!d.data || !d.data.uri) {
                    return d.name;
                }

                var name = d.data.uri.hostname;
                if (1 !== scope.helperData.protocol.length) {
                    name = d.data.uri.protocol + '//' + name;
                }

                if (1 !== scope.helperData.port.length) {
                    name += ':' + d.data.uri.port;
                }

                if (1 !== scope.helperData.pathname.length) {
                    name += d.data.uri.pathname;
                }

                if(0 !== scope.helperData.replicaType.length) {
                    name += ' (' + d.data.uri.replicaType[0] + ')';
                }

                return name;
            };

            var drawGraph = function(newValue, oldValue) {
                if (newValue) {
                  flatGraph(element, scope.data, scope.leafCount);
                }

                $('text').tooltip({
                  content: function() {
                    return $(this).attr('title');
                  }
                });
              };

            scope.$watch("data", drawGraph);

            window.onresize = drawGraph;

            function setNodeNavigationBehavior(node, view){
                node
                .attr('data-href', function (d) {
                    if (d.type == "node"){
                        return getNodeUrl(d, view);
                    }
                    else{
                        return "";
                    }
                })
                .on('click', function(d) {
                    if (d.data.type == "node"){
                        location.href = getNodeUrl(d, view);
                    }
                });
            }

            function getNodeUrl(d, view){
                var url = d.name + Constants.ROOT_URL + "#/~cloud";
                if (view != undefined){
                    url += "?view=" + view;
                }
                return url;
            }

            var flatGraph = function(element, graphData, leafCount) {
                var w = element.width(),
                    h = leafCount * 20;

                // Calculate roughly the width of host name to align the graph
                var hostnameWidth = graphData.children.length > 0 ?
                  graphData.children[0].children[0].children[0].name.length * 5.5 : 400;
                var tree = d3.layout.tree().size([h, Math.max(w - 140 - hostnameWidth, 100)]);

                var diagonal = d3.svg.diagonal().projection(function (d) {
                    return [d.y, d.x];
                });

                d3.select('#canvas', element).html('');
                var vis = d3.select('#canvas', element).append('svg')
                    .attr('width', w)
                    .attr('height', h)
                    .append('g')
                    .attr('transform', 'translate(10, 0)');

                var nodes = tree.nodes(graphData);

                var link = vis.selectAll('path.link')
                    .data(tree.links(nodes))
                    .enter().append('path')
                    .attr('class', helper_path_class)
                    .attr('d', diagonal);

                var node = vis.selectAll('g.node')
                    .data(nodes)
                    .enter().append('g')
                    .attr('class', helper_node_class)
                    .attr('transform', function (d) {
                        return 'translate(' + d.y + ',' + d.x + ')';
                    })

                node.append('circle')
                    .attr('r', 4.5);

                node.append('text')
                    .attr('dx', function (d) {
                        return 0 === d.depth ? -8 : 8;
                    })
                    .attr('dy', function (d) {
                        return 5;
                    })
                    .attr('text-anchor', function (d) {
                        return 0 === d.depth ? 'end' : 'start';
                    })
                    .attr("title", helper_tooltip_text)
                    .text(helper_node_text);

                setNodeNavigationBehavior(node);
            };
        }
    };
});
