package org.dhatim.dropwizard.prometheus;

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.Snapshot;
import com.codahale.metrics.Timer;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

class DropwizardMetricsExporter {

    private static final Logger LOG = LoggerFactory.getLogger(DropwizardMetricsExporter.class);

    private final PrometheusTextWriter writer;

    public DropwizardMetricsExporter(PrometheusTextWriter writer) {
        this.writer = writer;
    }

    public void writeGauge(String name, Gauge<?> gauge) throws IOException {
        final String sanitizedName = sanitizeMetricName(name);
        writer.writeHelp(sanitizedName, getHelpMessage(name, gauge));
        writer.writeType(sanitizedName, MetricType.GAUGE);

        Object obj = gauge.getValue();
        double value;
        if (obj instanceof Number) {
            value = ((Number) obj).doubleValue();
        } else if (obj instanceof Boolean) {
            value = ((Boolean) obj) ? 1 : 0;
        } else {
            LOG.warn("Invalid type for Gauge {}: {}", name, obj.getClass().getName());
            return;
        }

        writer.writeSample(sanitizedName, emptyMap(), value);
    }

    /**
     * Export counter as Prometheus <a href="https://prometheus.io/docs/concepts/metric_types/#gauge">Gauge</a>.
     */
    public void writeCounter(String dropwizardName, Counter counter) throws IOException {
        String name = sanitizeMetricName(dropwizardName);
        writer.writeHelp(name, getHelpMessage(dropwizardName, counter));
        writer.writeType(name, MetricType.GAUGE);
        writer.writeSample(name, emptyMap(), counter.getCount());
    }


    /**
     * Export a histogram snapshot as a prometheus SUMMARY.
     *
     * @param dropwizardName metric name.
     * @param snapshot the histogram snapshot.
     * @param count the total sample count for this snapshot.
     * @param factor a factor to apply to histogram values.
     * @throws IOException
     *
     */
    public void writeHistogram(String dropwizardName, Histogram histogram) throws IOException {
        writeSnapshotAndCount(dropwizardName, histogram.getSnapshot(), histogram.getCount(), 1.0, MetricType.SUMMARY, getHelpMessage(dropwizardName, histogram));
    }


    private void writeSnapshotAndCount(String dropwizardName, Snapshot snapshot, long count, double factor, MetricType type, String helpMessage) throws IOException {
        String name = sanitizeMetricName(dropwizardName);
        writer.writeHelp(name, helpMessage);
        writer.writeType(name, type);
        writer.writeSample(name, mapOf("quantile", "0.5"), snapshot.getMedian() * factor);
        writer.writeSample(name, mapOf("quantile", "0.75"), snapshot.get75thPercentile() * factor);
        writer.writeSample(name, mapOf("quantile", "0.95"), snapshot.get95thPercentile() * factor);
        writer.writeSample(name, mapOf("quantile", "0.98"), snapshot.get98thPercentile() * factor);
        writer.writeSample(name, mapOf("quantile", "0.99"), snapshot.get99thPercentile() * factor);
        writer.writeSample(name, mapOf("quantile", "0.999"), snapshot.get999thPercentile() * factor);
        writer.writeSample(name + "_min", emptyMap(), snapshot.getMin());
        writer.writeSample(name + "_max", emptyMap(), snapshot.getMax());
        writer.writeSample(name + "_median", emptyMap(), snapshot.getMedian());
        writer.writeSample(name + "_mean", emptyMap(), snapshot.getMean());
        writer.writeSample(name + "_stddev", emptyMap(), snapshot.getStdDev());
        writer.writeSample(name + "_count", emptyMap(), count);
    }

    private void writeMetered(String dropwizardName, Metered metered) throws IOException {
        String name = sanitizeMetricName(dropwizardName);
        writer.writeSample(name, mapOf("rate", "m1"), metered.getOneMinuteRate());
        writer.writeSample(name, mapOf("rate", "m5"), metered.getFiveMinuteRate());
        writer.writeSample(name, mapOf("rate", "m15"), metered.getFifteenMinuteRate());
        writer.writeSample(name, mapOf("rate", "mean"), metered.getMeanRate());
    }

    private Map<String, String> mapOf(String key, String value) {
        HashMap<String, String> result = new HashMap<>();
        result.put(key, value);
        return result;
    }

    private Map<String, String> emptyMap() {
        return Collections.emptyMap();
    }

    public void writeTimer(String dropwizardName, Timer timer) throws IOException {
        writeSnapshotAndCount(dropwizardName, timer.getSnapshot(), timer.getCount(), 1.0D / TimeUnit.SECONDS.toNanos(1L), MetricType.SUMMARY, getHelpMessage(dropwizardName, timer));
        writeMetered(dropwizardName, timer);
    }

    public void writeMeter(String dropwizardName, Meter meter) throws IOException {
        String name = sanitizeMetricName(dropwizardName) + "_total";

        writer.writeHelp(name, getHelpMessage(dropwizardName, meter));
        writer.writeType(name, MetricType.COUNTER);
        writer.writeSample(name, emptyMap(), meter.getCount());
        
        writeMetered(dropwizardName, meter);
    }

    private static String getHelpMessage(String metricName, Metric metric) {
        return String.format("Generated from Dropwizard metric import (metric=%s, type=%s)", metricName, metric.getClass().getName());
    }

    static String sanitizeMetricName(String dropwizardName) {
        return dropwizardName.replaceAll("[^a-zA-Z0-9:_]", "_");
    }

}