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

package org.apache.ambari.view.slider.rest.client;

import org.apache.commons.io.IOUtils;
import org.apache.log4j.Logger;
import org.codehaus.jackson.map.DeserializationConfig;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.ObjectReader;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class SliderAppJmxHelper {

  private static final Logger logger = Logger
      .getLogger(SliderAppJmxHelper.class);
  private static final String NAME_KEY = "name";
  private static final String PORT_KEY = "tag.port";
  private static final String FORPORT_KEY = "ForPort";
  private static final String JSON_METRIC_START = "$['";
  private static final String JSON_METRIC_END = "']";

  public static JMXTypes jmxTypeExpected(Map<String, Metric> metrics) {
    JMXTypes retVal = null;
    for (Metric metric : metrics.values()) {
      if (retVal == null) {
        retVal = getMetricType(metric.getMetric());
        continue;
      } else {
        if (retVal != getMetricType(metric.getMetric())) {
          retVal = null;
          break;
        }
      }
    }
    return retVal;
  }

  public static void extractMetricsFromJmxBean(InputStream jmxStream, String jmxUrl,
                                               Map<String, String> jmxProperties,
                                               Map<String, Metric> metrics) {
    ObjectMapper jmxObjectMapper = new ObjectMapper();
    jmxObjectMapper.configure(DeserializationConfig.Feature.USE_ANNOTATIONS, false);
    ObjectReader jmxObjectReader = jmxObjectMapper.reader(JMXMetricHolder.class);
    JMXMetricHolder metricHolder = null;
    try {
      metricHolder = jmxObjectReader.readValue(jmxStream);
    } catch (IOException e) {
      logger.error(String.format("Malformed jmx data from %s. Error %s", jmxUrl, e.getMessage()));
    }

    Map<String, Map<String, Object>> categories = new HashMap<String, Map<String, Object>>();

    for (Map<String, Object> bean : metricHolder.getBeans()) {
      String category = getCategory(bean);
      if (category != null) {
        categories.put(category, bean);
      }
    }

    addJmxPropertiesFromBeans(jmxProperties, categories, metrics);
  }

  public static void extractMetricsFromJmxJson(InputStream jmxStream, String jmxUrl,
                                               Map<String, String> jmxProperties,
                                               Map<String, Metric> metrics)
      throws IOException, ParseException {
    JSONParser parser = new JSONParser();
    Object obj = parser.parse(IOUtils.toString(jmxStream));
    JSONObject jsonObject = (JSONObject) obj;
    for (String key : metrics.keySet()) {
      Metric metric = metrics.get(key);
      String jsonKey = extractJsonKeySingleLevel(metric.getMetric());
      Object value = jsonObject.get(jsonKey);
      if (value != null) {
        jmxProperties.put(key, value.toString());
      }
    }
  }

  private static String extractJsonKeySingleLevel(String metricKey) {
    String jsonKey = metricKey;
    if (metricKey != null) {
      if (metricKey.startsWith(JSON_METRIC_START) && metricKey.endsWith(JSON_METRIC_END)) {
        jsonKey = metricKey.substring(JSON_METRIC_START.length(), metricKey.length() - JSON_METRIC_END.length());
      }
    }

    return jsonKey;
  }

  public static void extractMetricsFromJmxXML(InputStream jmxStream, String jmxUrl,
                                              Map<String, String> jmxProperties,
                                              Map<String, Metric> metrics)
      throws ParserConfigurationException, IOException, SAXException, XPathExpressionException {
    DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
    DocumentBuilder builder = factory.newDocumentBuilder();
    Document doc = builder.parse(jmxStream);
    for (String key : metrics.keySet()) {
      Metric metric = metrics.get(key);
      XPathExpression xPathExpression = metric.getxPathExpression();
      if (xPathExpression != null) {
        String value = xPathExpression.evaluate(doc);
        if (value != null) {
          jmxProperties.put(key, value.toString().trim());
        }
      }
    }
  }

  private static String getCategory(Map<String, Object> bean) {
    if (bean.containsKey(NAME_KEY)) {
      String name = (String) bean.get(NAME_KEY);

      if (bean.containsKey(PORT_KEY)) {
        String port = (String) bean.get(PORT_KEY);
        name = name.replace(FORPORT_KEY + port, "");
      }
      return name;
    }
    return null;
  }

  protected static void addJmxPropertiesFromBeans(Map<String, String> jmxProperties,
                                                  Map<String, Map<String, Object>> categories,
                                                  Map<String, Metric> relevantMetrics) {
    for (String metricName : relevantMetrics.keySet()) {
      Metric metric = relevantMetrics.get(metricName);
      String beanName = metric.getJmxBeanKeyName();
      Object value = categories.get(beanName);
      if (value instanceof Map) {
        Map<?, ?> map = (Map<?, ?>) value;
        for (List<String> matcher : metric.getMatchers()) {
          boolean foundMetrics = false;
          for (int matchIndex = 0; matchIndex < matcher.size(); matchIndex++) {
            String matchKey = matcher.get(matchIndex);
            value = map.get(matchKey);
            if (value instanceof Map) {
              map = (Map<?, ?>) value;
              continue;
            } else {
              if (value != null && matchIndex == matcher.size() - 1) {
                jmxProperties.put(metricName, value.toString());
                foundMetrics = true;
              } else {
                break;
              }
            }
          }
          if (foundMetrics) {
            break;
          }
        }
      }
    }
  }

  private static JMXTypes getMetricType(String metricKey) {
    assert metricKey != null;
    if (metricKey.startsWith("/")) {
      return JMXTypes.XML;
    } else if (metricKey.startsWith("$")) {
      return JMXTypes.JSON;
    } else {
      return JMXTypes.JMX_BEAN;
    }
  }

  public enum JMXTypes {
    JMX_BEAN,
    JSON,
    XML
  }
}
