/*
 * Jooby https://jooby.io
 * Apache License Version 2.0 https://jooby.io/LICENSE.txt
 * Copyright 2014 Edgar Espina
 */
package io.jooby.metrics;

import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.concurrent.TimeUnit;

import com.codahale.metrics.Counter;
import com.codahale.metrics.Gauge;
import com.codahale.metrics.Histogram;
import com.codahale.metrics.Meter;
import com.codahale.metrics.Metered;
import com.codahale.metrics.Metric;
import com.codahale.metrics.MetricFilter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.Sampling;
import com.codahale.metrics.Snapshot;
import com.codahale.metrics.Timer;
import edu.umd.cs.findbugs.annotations.NonNull;
import io.jooby.Context;
import io.jooby.Route;
import io.jooby.StatusCode;

public class MetricHandler implements Route.Handler {

  @NonNull @Override
  public Object apply(@NonNull Context ctx) {
    MetricRegistry registry = ctx.require(MetricRegistry.class);
    Map<String, Metric> allMetrics = registry.getMetrics();

    if (allMetrics.isEmpty()) {
      ctx.setResponseCode(StatusCode.NOT_IMPLEMENTED);
      ctx.setResponseHeader(MetricsModule.CACHE_HEADER_NAME, MetricsModule.CACHE_HEADER_VALUE);
      return allMetrics;
    } else {
      // params & filters
      String type = ctx.query("type").value("*");
      MetricFilter filter =
          ctx.query("name")
              .toOptional()
              .<MetricFilter>map(name -> (n, m) -> n.startsWith(name))
              .orElse(MetricFilter.ALL);

      TimeUnit unit = TimeUnit.valueOf(ctx.query("unit").value("seconds").toUpperCase());
      String rateUnitLabel = calculateRateUnit(unit, "ops");
      double rateFactor = unit.toSeconds(1);

      String durationUnitLabel = unit.toString().toLowerCase(Locale.US);
      double durationFactor = 1.0 / unit.toNanos(1);

      boolean showSamples = ctx.query("showSamples").booleanValue(false);

      // dump metrics
      Map<String, Object> metrics = new TreeMap<>();
      Map<String, Object> counters = counters(registry.getCounters(filter));
      if (counters.size() > 0) {
        metrics.put("counters", counters);
      }
      Map<String, Object> gauges = gauges(registry.getGauges(filter));
      if (gauges.size() > 0) {
        metrics.put("gauges", gauges);
      }
      Map<String, Object> histograms = histograms(registry.getHistograms(filter), showSamples);
      if (histograms.size() > 0) {
        metrics.put("histograms", histograms);
      }
      Map<String, Object> meters =
          meters(registry.getMeters(filter), rateUnitLabel, rateFactor, durationUnitLabel);
      if (meters.size() > 0) {
        metrics.put("meters", meters);
      }
      Map<String, Object> timers =
          timers(
              registry.getTimers(filter),
              rateUnitLabel,
              rateFactor,
              durationUnitLabel,
              durationFactor,
              showSamples);
      if (timers.size() > 0) {
        metrics.put("timers", timers);
      }

      // send
      ctx.setResponseHeader(MetricsModule.CACHE_HEADER_NAME, MetricsModule.CACHE_HEADER_VALUE);

      //noinspection CollectionAddedToSelf
      return metrics.getOrDefault(type, metrics);
    }
  }

  private static Map<String, Object> timers(
      final SortedMap<String, Timer> timers,
      final String rateUnit,
      final double rateFactor,
      final String durationUnit,
      final double durationFactor,
      final boolean showSamples) {
    Map<String, Object> result = new TreeMap<>();
    timers.forEach(
        (name, timer) ->
            result.put(
                name,
                timer(timer, rateUnit, rateFactor, durationUnit, durationFactor, showSamples)));
    return result;
  }

  private static Map<String, Object> timer(
      final Timer timer,
      final String rateUnit,
      final double rateFactor,
      final String durationUnit,
      final double durationFactor,
      final boolean showSamples) {
    Map<String, Object> result = meter(timer, rateUnit, rateFactor, durationUnit);

    result.putAll(snapshot(timer, durationFactor, showSamples));

    return result;
  }

  private static Map<String, Object> snapshot(
      final Sampling sampling, final double durationFactor, final boolean showSamples) {
    Map<String, Object> result = new TreeMap<>();
    final Snapshot snapshot = sampling.getSnapshot();
    result.put("max", snapshot.getMax() * durationFactor);
    result.put("mean", snapshot.getMean() * durationFactor);
    result.put("min", snapshot.getMin() * durationFactor);

    result.put("p50", snapshot.getMedian() * durationFactor);
    result.put("p75", snapshot.get75thPercentile() * durationFactor);
    result.put("p95", snapshot.get95thPercentile() * durationFactor);
    result.put("p98", snapshot.get98thPercentile() * durationFactor);
    result.put("p99", snapshot.get99thPercentile() * durationFactor);
    result.put("p999", snapshot.get999thPercentile() * durationFactor);

    if (showSamples) {
      final long[] values = snapshot.getValues();
      List<Double> scaledValues = new ArrayList<>(values.length);
      for (long value : values) {
        scaledValues.add(value * durationFactor);
      }
      result.put("values", scaledValues);
    }
    return result;
  }

  @SuppressWarnings("rawtypes")
  private static Map<String, Object> gauges(final SortedMap<String, Gauge> gauges) {
    Map<String, Object> result = new TreeMap<>();
    gauges.forEach(
        (name, gauge) -> {
          try {
            result.put(name, gauge.getValue());
          } catch (Exception ex) {
            result.put(name, ex.toString());
          }
        });
    return result;
  }

  private static Map<String, Object> counters(final SortedMap<String, Counter> counters) {
    Map<String, Object> result = new TreeMap<>();
    counters.forEach((name, c) -> result.put(name, c.getCount()));
    return result;
  }

  private static Map<String, Object> histograms(
      final SortedMap<String, Histogram> histograms, final boolean showSamples) {
    Map<String, Object> result = new TreeMap<>();
    histograms.forEach((name, timer) -> result.put(name, snapshot(timer, 1, showSamples)));
    return result;
  }

  private static Map<String, Object> meters(
      final SortedMap<String, Meter> timers,
      final String rateUnit,
      final double rateFactor,
      final String durationUnit) {
    Map<String, Object> result = new TreeMap<>();
    timers.forEach(
        (name, timer) -> result.put(name, meter(timer, rateUnit, rateFactor, durationUnit)));
    return result;
  }

  private static Map<String, Object> meter(
      final Metered meter,
      final String rateUnit,
      final double rateFactor,
      final String durationUnit) {
    Map<String, Object> result = new TreeMap<>();
    result.put("count", meter.getCount());
    result.put("m15_rate", meter.getFifteenMinuteRate() * rateFactor);
    result.put("m1_rate", meter.getOneMinuteRate() * rateFactor);
    result.put("m5_rate", meter.getFiveMinuteRate() * rateFactor);
    result.put("mean_rate", meter.getMeanRate() * rateFactor);
    result.put("duration_units", durationUnit);
    result.put("rate_units", rateUnit);

    return result;
  }

  private static String calculateRateUnit(final TimeUnit unit, final String name) {
    final String s = unit.toString().toLowerCase(Locale.US);
    return name + '/' + s.substring(0, s.length() - 1);
  }
}
