package com.sf.monitor.druid;

import com.fasterxml.jackson.core.type.TypeReference;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.sf.log.Logger;
import com.sf.monitor.Config;
import com.sf.monitor.Event;
import com.sf.monitor.Resources;
import com.sf.monitor.utils.Utils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;

import java.util.Collections;
import java.util.List;
import java.util.Map;

public class EmitMetricsAnalyzer {
  private static final Logger log = new Logger(EmitMetricsAnalyzer.class);
  public static final String tableName = "druid_metrics";

  public static class MetricInfo {
    public String feed;
    public String timestamp;
    public String service;
    public String host;
    public String metric;
    public double value;
    public String severity;
    public String description;
    public String user1;
    public String user2;
    public String user3;
    public String user4;
    public Object user5;
    public Object user6;
    public Object user7;
    public Object user8;
    public Object user9;
    public Object user10;
  }

  public static List<Event> fetchReceivedInfos(List<MetricInfo> rawInfos) {
    if (rawInfos == null || rawInfos.size() == 0) {
      return Collections.emptyList();
    }
    String timestamp = rawInfos.get(0).timestamp;
    List<Event> points = Lists.newArrayList();
    for (MetricInfo info : rawInfos) {
      for (MetricFetchers fetchers : allMetrics) {
        Event p = fetchers.toPoint(info);
        if (p != null) {
          points.add(p);
        }
      }
    }

    return points;
  }

  private static MetricFetchers systemMetrics = new MetricFetchers(
    new MetricFetcher("sys/mem")
      .withKeys("max", "used"),
    new MetricFetcher("sys/fs")
      .withKeys("max", "used")
      .withUsers(
        "user1", "device",
        "user2", "mount"
      ),
    new MetricFetcher("sys/disk")
      .withKeys("write/count", "write/size", "read/count", "read/size")
      .withUsers(
        "user1", "device",
        "user2", "mount"
      ),
    new MetricFetcher("sys/net")
      .withKeys("write/size", "read/size")
      .withUsers(
        "user1", "device",
        "user2", "ip",
        "user3", "ether"
      ),
    new MetricFetcher("sys/cpu")
      .withUsers(
        "user1", "cpuid",
        "user2", "type"
      ),
    new MetricFetcher("sys/swap")
      .withKeys("max", "free", "pageIn", "pageOut")
  );

  private static MetricFetchers jvmMetrics = new MetricFetchers(
    new MetricFetcher("jvm/mem")
      .withKeys("committed", "init", "max", "used")
      .withUsers("user1", "type"),
    new MetricFetcher("jvm/pool")
      .withKeys("committed", "init", "max", "used")
      .withUsers(
        "user1", "type",
        "user2", "space"
      ),
    new MetricFetcher("jvm/gc")
      .withKeys("time", "count")
      .withUsers("user1", "type"),
    new MetricFetcher("jvm/bufferpool")
      .withKeys("capacity", "used")
      .withUsers("user1", "type")
  );

  private static MetricFetchers cacheMetrics = new MetricFetchers(
    new MetricFetcher("cache/delta").withKeys(
      "numEntries",
      "numEntries",
      "sizeBytes",
      "hits",
      "misses",
      "evictions",
      "averageBytes",
      "timeouts",
      "errors",
      "hitRate"
    )
  );

  private static MetricFetchers segmentMetrics = new MetricFetchers(
    new MetricFetcher("server/segment")
      .withKeys("used", "usedPercent", "count")
      .withUsers(
        "user1", "dataSource",
        "user2", "tier"
      ),
    new MetricFetcher("server/segment")
      .withKeys("max"),
    new MetricFetcher("server/segment")
      .withKeys("totalUsed", "totalCount", "totalUsedPercent")
      .withUsers("user2", "tier")
      .withWarning("server/segment/totalUsedPercent", new Warning(0.85, "running out of segment space"))
  );

  private static MetricFetchers indexMetrics = new MetricFetchers(
    new MetricFetcher("events")
      .withKeys("thrownAway", "unparseable", "processed")
      .withUsers("user2", "dataSource"),
    new MetricFetcher("rows/output")
      .withUsers("user2", "dataSource"),
    new MetricFetcher("persists")
      .withKeys("num", "time", "backPressure")
      .withUsers("user2", "dataSource")
  );

  // Those are the metrics we care about currently.
  private static MetricFetchers[] allMetrics = new MetricFetchers[]{
    systemMetrics,
    jvmMetrics,
    cacheMetrics,
    segmentMetrics,
    indexMetrics
  };

  private static class MetricFetcher {
    final String name;
    List<String> accpetMetrics;
    Map<String, String> userMap = Maps.newHashMap();
    Map<String, Warning> warnings = Maps.newHashMap();

    MetricFetcher(String name) {
      this.name = StringUtils.removeStart(StringUtils.removeEnd(name, "/"), "/");
      this.accpetMetrics = Collections.singletonList(name);
    }

    MetricFetcher withKeys(String... keys) {
      this.accpetMetrics = Lists.transform(
        Lists.newArrayList(keys), new Function<String, String>() {
          @Override
          public String apply(String key) {
            return name + "/" + StringUtils.removeStart(StringUtils.removeEnd(key, "/"), "/");
          }
        }
      );
      return this;
    }

    MetricFetcher withUsers(String... users) {
      for (int i = 0; i + 1 < users.length; i += 2) {
        userMap.put(users[i], users[i + 1]);
      }
      return this;
    }

    MetricFetcher withWarning(String key, Warning warning) {
      warnings.put(key, warning);
      return this;
    }

    Iterable<String> accpetMetrics() {
      return accpetMetrics;
    }

    Event toPoint(MetricInfo info) {
      Event p = new Event();
      p.metricName = Utils.smoothText.apply(info.metric);
      p.metricValue = info.value;
      Warning warning = warnings.get(info.metric);
      p.tags = Maps.newLinkedHashMap(); // We need the order!
      p.tags.put("service", info.service.replace("/", ":")); // Transform into real service modelName.
      p.tags.put("host", info.host);
      for (Map.Entry<String, String> e : userMap.entrySet()) {
        String user = e.getKey();
        String dim = e.getValue();
        if ("user1".equals(user)) {
          p.tags.put(dim, info.user1);
        } else if ("user2".equals(user)) {
          p.tags.put(dim, info.user2);
        } else if ("user3".equals(user)) {
          p.tags.put(dim, info.user3);
        } else if ("user4".equals(user)) {
          p.tags.put(dim, info.user4);
        }
      }

      if (warning != null) {
        warning.checkAlarm(info, p.tags);
      }

      return p;
    }
  }

  private static class Warning {
    static enum Watcher {
      ShouldSmaller,
      ShouldBigger
    }

    double threshold;
    String warning;
    Watcher watcher = Watcher.ShouldSmaller;

    Warning(double threshold, String warning) {
      this.threshold = threshold;
      this.warning = warning;
    }

    Warning withWatcher(Watcher watcher) {
      this.watcher = watcher;
      return this;
    }

    void checkAlarm(MetricInfo info, Map<String, String> tags) {
      boolean shouldWarn = false;
      if (watcher == Watcher.ShouldBigger) {
        shouldWarn = info.value < threshold;
      } else if (watcher == Watcher.ShouldSmaller) {
        shouldWarn = info.value > threshold;
      }
      if (shouldWarn && Config.config.druidInfos.shouldWarn(info.metric)) {
        String warnMsg = getWarning(info, tags);
        Utils.sendNotify("druid", warnMsg);
        log.warn("druid - " + warnMsg);
      }
    }

    String getWarning(MetricInfo info, Map<String, String> tags) {
      String tagStr = Joiner.on(',').join(
        Iterables.transform(
          tags.entrySet(), new Function<Map.Entry<String, String>, String>() {

            @Override
            public String apply(Map.Entry<String, String> e) {
              return e.getKey() + ":[" + e.getValue() + "]";
            }
          }
        )
      );
      return String.format(
        "%s - %s: current[%f], threshold[%f], %s",
        tagStr,
        info.metric,
        info.value,
        threshold,
        warning
      );
    }
  }

  private static class MetricFetchers {
    final Map<String, MetricFetcher> fetchers;

    MetricFetchers(MetricFetcher... mappers) {
      this.fetchers = Maps.newHashMap();
      for (MetricFetcher fetcher : mappers) {
        for (String metric : fetcher.accpetMetrics()) {
          fetchers.put(metric, fetcher);
        }
      }
    }

    Event toPoint(MetricInfo info) {
      if (!"metrics".equals(info.feed)) {
        return null;
      }
      MetricFetcher fetcher = fetchers.get(info.metric);
      if (fetcher == null) {
        return null;
      }
      return fetcher.toPoint(info);
    }
  }

  private static List<MetricInfo> parseMetrics(String rawMetricStr) {
    try {
      return Resources.jsonMapper.readValue(
        rawMetricStr, new TypeReference<List<MetricInfo>>() {
        }
      );
    } catch (Exception e) {
      log.error(e, "failed to parse metric infos");
      return Collections.emptyList();
    }
  }

  public static void main(String[] args) throws Exception {
    Config.init("config");
    Resources.init();

    String msg = IOUtils.toString(ClassLoader.getSystemResourceAsStream("druid_metrics_msg"));

    List<Event> points = EmitMetricsAnalyzer.fetchReceivedInfos(parseMetrics(msg));

    //Resources.influxDB.write("dcmonitor", "", points);

    System.out.println(Resources.jsonMapper.writeValueAsString(points));
  }

}