/*
 * 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.gobblin.metrics.influxdb;

import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.SortedMap;
import java.util.concurrent.TimeUnit;

import org.influxdb.dto.Point;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.codahale.metrics.Counter;
import com.codahale.metrics.Counting;
import com.codahale.metrics.Gauge;
import com.codahale.metrics.Histogram;
import com.codahale.metrics.Meter;
import com.codahale.metrics.Metered;
import com.codahale.metrics.MetricFilter;
import com.codahale.metrics.Snapshot;
import com.codahale.metrics.Timer;
import com.google.common.base.Optional;
import com.google.common.collect.Lists;
import com.typesafe.config.Config;

import org.apache.gobblin.configuration.ConfigurationKeys;
import org.apache.gobblin.metrics.Measurements;
import org.apache.gobblin.metrics.reporter.ConfiguredScheduledReporter;
import org.apache.gobblin.util.ConfigUtils;
import static org.apache.gobblin.metrics.Measurements.*;


/**
 * InfluxDB reporter for metrics
 *
 * @author Lorand Bendig
 *
 */
public class InfluxDBReporter extends ConfiguredScheduledReporter {

  private final InfluxDBPusher influxDBPusher;

  private static final Logger LOGGER = LoggerFactory.getLogger(InfluxDBReporter.class);

  public InfluxDBReporter(Builder<?> builder, Config config) {
    super(builder, config);
    if (builder.influxDBPusher.isPresent()) {
      this.influxDBPusher = builder.influxDBPusher.get();
    } else {
      this.influxDBPusher =
          new InfluxDBPusher.Builder(builder.url, builder.username, builder.password, builder.database,
              builder.connectionType).build();
    }
  }

  /**
   * A static factory class for obtaining new {@link org.apache.gobblin.metrics.influxdb.InfluxDBReporter.Builder}s
   *
   * @see org.apache.gobblin.metrics.influxdb.InfluxDBReporter.Builder
   */
  public static class Factory {

    public static BuilderImpl newBuilder() {
      return new BuilderImpl();
    }
  }

  public static class BuilderImpl extends Builder<BuilderImpl> {

    @Override
    protected BuilderImpl self() {
      return this;
    }
  }

  /**
   * Builder for {@link InfluxDBReporter}. Defaults to no filter, reporting rates in seconds and times in
   * milliseconds using TCP sending type
   */
  public static abstract class Builder<T extends ConfiguredScheduledReporter.Builder<T>> extends
      ConfiguredScheduledReporter.Builder<T> {

    protected MetricFilter filter;
    protected String url;
    protected String username;
    protected String password;
    protected String database;
    protected InfluxDBConnectionType connectionType;
    protected Optional<InfluxDBPusher> influxDBPusher;

    protected Builder() {
      super();
      this.name = "InfluxDBReporter";
      this.influxDBPusher = Optional.absent();
      this.filter = MetricFilter.ALL;
      this.connectionType = InfluxDBConnectionType.TCP;
    }

    /**
     * Set {@link org.apache.gobblin.metrics.influxdb.InfluxDBPusher} to use.
     */
    public T withInfluxDBPusher(InfluxDBPusher pusher) {
      this.influxDBPusher = Optional.of(pusher);
      return self();
    }

    /**
     * Set connection parameters for the {@link org.apache.gobblin.metrics.influxdb.InfluxDBPusher} creation
     */
    public T withConnection(String url, String username, String password, String database) {
      this.url = url;
      this.username = username;
      this.password = password;
      this.database = database;
      return self();
    }

    /**
     * Set {@link org.apache.gobblin.metrics.influxdb.InfluxDBConnectionType} to use.
     */
    public T withConnectionType(InfluxDBConnectionType connectionType) {
      this.connectionType = connectionType;
      return self();
    }

    /**
     * Only report metrics which match the given filter.
     *
     * @param filter a {@link MetricFilter}
     * @return {@code this}
     */
    public T filter(MetricFilter filter) {
      this.filter = filter;
      return self();
    }

    /**
     * Builds and returns {@link InfluxDBReporter}.
     *
     * @return InfluxDBReporter
     */
    public InfluxDBReporter build(Properties props) throws IOException {
      return new InfluxDBReporter(this, ConfigUtils.propertiesToConfig(props,
          Optional.of(ConfigurationKeys.METRICS_CONFIGURATIONS_PREFIX)));
    }
  }

  @Override
  protected void report(SortedMap<String, Gauge> gauges, SortedMap<String, Counter> counters,
      SortedMap<String, Histogram> histograms, SortedMap<String, Meter> meters, SortedMap<String, Timer> timers,
      Map<String, Object> tags) {

    String prefix = getMetricNamePrefix(tags);
    long timestamp = System.currentTimeMillis();
    List<Point> points = Lists.newArrayList();

    try {
      for (Map.Entry<String, Gauge> gauge : gauges.entrySet()) {
        reportGauge(points, prefix, gauge.getKey(), gauge.getValue(), timestamp);
      }

      for (Map.Entry<String, Counter> counter : counters.entrySet()) {
        reportCounter(points, prefix, counter.getKey(), counter.getValue(), timestamp);
      }

      for (Map.Entry<String, Histogram> histogram : histograms.entrySet()) {
        reportHistogram(points, prefix, histogram.getKey(), histogram.getValue(), timestamp);
      }

      for (Map.Entry<String, Meter> meter : meters.entrySet()) {
        reportMetered(points, prefix, meter.getKey(), meter.getValue(), timestamp);
      }

      for (Map.Entry<String, Timer> timer : timers.entrySet()) {
        reportTimer(points, prefix, timer.getKey(), timer.getValue(), timestamp);
      }

      influxDBPusher.push(points);

    } catch (IOException ioe) {
      LOGGER.error("Error sending metrics to InfluxDB", ioe);
    }
  }

  private void reportGauge(List<Point> points, String prefix, String name, Gauge gauge, long timestamp)
      throws IOException {
    String metricName = getKey(prefix, name);
    points.add(buildMetricAsPoint(metricName, gauge.getValue(), timestamp));
  }

  private void reportCounter(List<Point> points, String prefix, String name, Counting counter, long timestamp)
      throws IOException {
    String metricName = getKey(prefix, name, COUNT.getName());
    points.add(buildMetricAsPoint(metricName, counter.getCount(), false, timestamp));
  }

  private void reportHistogram(List<Point> points, String prefix, String name, Histogram histogram, long timestamp)
      throws IOException {
    reportCounter(points, prefix, name, histogram, timestamp);
    reportSnapshot(points, prefix, name, histogram.getSnapshot(), timestamp, false);
  }

  private void reportTimer(List<Point> points, String prefix, String name, Timer timer, long timestamp)
      throws IOException {
    reportSnapshot(points, prefix, name, timer.getSnapshot(), timestamp, true);
    reportMetered(points, prefix, name, timer, timestamp);
  }

  private void reportSnapshot(List<Point> points, String prefix, String name, Snapshot snapshot, long timestamp,
      boolean convertDuration) throws IOException {
    String baseMetricName = getKey(prefix, name);
    points.add(buildMetricAsPoint(getKey(baseMetricName, MIN), snapshot.getMin(), convertDuration, timestamp));
    points.add(buildMetricAsPoint(getKey(baseMetricName, MAX), snapshot.getMax(), convertDuration, timestamp));
    points.add(buildMetricAsPoint(getKey(baseMetricName, MEAN), snapshot.getMean(), convertDuration, timestamp));
    points.add(buildMetricAsPoint(getKey(baseMetricName, STDDEV), snapshot.getStdDev(), convertDuration, timestamp));
    points.add(buildMetricAsPoint(getKey(baseMetricName, MEDIAN), snapshot.getMedian(), convertDuration, timestamp));
    points.add(buildMetricAsPoint(getKey(baseMetricName, PERCENTILE_75TH), snapshot.get75thPercentile(), convertDuration, timestamp));
    points.add(buildMetricAsPoint(getKey(baseMetricName, PERCENTILE_95TH), snapshot.get95thPercentile(), convertDuration, timestamp));
    points.add(buildMetricAsPoint(getKey(baseMetricName, PERCENTILE_98TH), snapshot.get98thPercentile(), convertDuration, timestamp));
    points.add(buildMetricAsPoint(getKey(baseMetricName, PERCENTILE_99TH), snapshot.get99thPercentile(), convertDuration, timestamp));
    points.add(buildMetricAsPoint(getKey(baseMetricName, PERCENTILE_999TH), snapshot.get999thPercentile(), convertDuration, timestamp));
  }

  private void reportMetered(List<Point> points, String prefix, String name, Metered metered, long timestamp)
      throws IOException {
    reportCounter(points,prefix, name, metered, timestamp);
    String baseMetricName = getKey(prefix, name);
    points.add(buildRateAsPoint(getKey(baseMetricName, RATE_1MIN), metered.getOneMinuteRate(), timestamp));
    points.add(buildRateAsPoint(getKey(baseMetricName, RATE_5MIN), metered.getFiveMinuteRate(), timestamp));
    points.add(buildRateAsPoint(getKey(baseMetricName, RATE_15MIN), metered.getFifteenMinuteRate(), timestamp));
    points.add(buildRateAsPoint(getKey(baseMetricName, MEAN_RATE), metered.getMeanRate(), timestamp));
  }

  private Point buildMetricAsPoint(String metricName, Number value, boolean toDuration, long timestamp)
      throws IOException {
    Number metricValue = toDuration ? convertDuration(value.doubleValue()) : value;
    return buildMetricAsPoint(metricName, metricValue, timestamp);
  }

  private Point buildRateAsPoint(String metricName, double value, long timestamp)
      throws IOException {
    return buildMetricAsPoint(metricName, convertRate(value), timestamp);
  }

  private Point buildMetricAsPoint(String name, Object value, long timestamp) throws IOException {
    return Point.measurement(name).field("value", value).time(timestamp, TimeUnit.MILLISECONDS).build();
  }

  private String getKey(String baseName, Measurements measurements) {
    return getKey(baseName, measurements.getName());
  }

  private String getKey(String... keys) {
    return JOINER.join(keys);
  }

}
