package com.signalfx.codahale.reporter;

import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.concurrent.TimeUnit;

import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.signalfx.endpoint.SignalFxEndpoint;
import com.signalfx.endpoint.SignalFxReceiverEndpoint;
import com.signalfx.metrics.SourceNameHelper;
import com.signalfx.metrics.auth.AuthToken;
import com.signalfx.metrics.auth.StaticAuthToken;
import com.signalfx.metrics.connection.DataPointReceiverFactory;
import com.signalfx.metrics.connection.HttpDataPointProtobufReceiverFactory;
import com.signalfx.metrics.endpoint.DataPointReceiverEndpoint;
import com.signalfx.metrics.errorhandler.OnSendErrorHandler;
import com.signalfx.metrics.flush.AggregateMetricSender;
import com.signalfx.metrics.protobuf.SignalFxProtocolBuffers;
import com.yammer.metrics.core.Counter;
import com.yammer.metrics.core.Gauge;
import com.yammer.metrics.core.Histogram;
import com.yammer.metrics.core.Meter;
import com.yammer.metrics.core.MetricName;
import com.yammer.metrics.core.MetricPredicate;
import com.yammer.metrics.core.MetricsRegistry;
import com.yammer.metrics.core.Timer;

/**
 * Reporter object for codahale metrics that reports values to com.signalfx.signalfx at some
 * interval.
 */
public class SignalFxReporter extends CustomScheduledReporter {
    private final AggregateMetricSender aggregateMetricSender;
    private final Set<MetricDetails> detailsToAdd;
    private final MetricMetadata metricMetadata;
    private final boolean useLocalTime;
    private final boolean sendExtraMetricDimensions;
    private final ImmutableMap<String, String> defaultDimensions;

    protected SignalFxReporter(MetricsRegistry registry, String name, MetricPredicate filter,
                                 TimeUnit rateUnit, TimeUnit durationUnit,
                                 AggregateMetricSender aggregateMetricSender,
                                 Set<MetricDetails> detailsToAdd,
                                 MetricMetadata metricMetadata) {
        this(registry, name, filter, rateUnit, durationUnit, aggregateMetricSender, detailsToAdd,
                metricMetadata, false, false, Collections.<String, String> emptyMap());
    }

    public SignalFxReporter(MetricsRegistry registry, String name, MetricPredicate filter,
                              TimeUnit rateUnit, TimeUnit durationUnit,
                              AggregateMetricSender aggregateMetricSender,
                              Set<MetricDetails> detailsToAdd, MetricMetadata metricMetadata,
                              boolean useLocalTime,
                              boolean sendExtraMetricDimensions,
                              Map<String, String> defaultDimensions) {
        super(registry, name, filter, rateUnit, durationUnit);
        this.aggregateMetricSender = aggregateMetricSender;
        this.useLocalTime = useLocalTime;
        this.detailsToAdd = detailsToAdd;
        this.metricMetadata = metricMetadata;
        this.sendExtraMetricDimensions = sendExtraMetricDimensions;
        this.defaultDimensions = ImmutableMap.copyOf(defaultDimensions);
    }

    /**
     *
     * Reports all given metrics here
     *
     */

    @Override
    public void report(SortedMap<MetricName, Gauge> gauges, SortedMap<MetricName, Counter> counters,
                       SortedMap<MetricName, Histogram> histograms,
                       SortedMap<MetricName, Meter> meters, SortedMap<MetricName, Timer> timers) {

        AggregateMetricSenderSessionWrapper session = new AggregateMetricSenderSessionWrapper(
                aggregateMetricSender.createSession(), Collections.unmodifiableSet(detailsToAdd), metricMetadata,
                aggregateMetricSender.getDefaultSourceName(), "sf_source", useLocalTime, sendExtraMetricDimensions,
                defaultDimensions);
        try {
            for (Map.Entry<MetricName, Gauge> entry : gauges.entrySet()) {
                session.addMetric(entry.getValue(), entry.getKey(),
                        SignalFxProtocolBuffers.MetricType.GAUGE, entry.getValue().value());
            }
            for (Map.Entry<MetricName, Counter> entry : counters.entrySet()) {
                session.addMetric(entry.getValue(), entry.getKey(),
                        SignalFxProtocolBuffers.MetricType.CUMULATIVE_COUNTER,
                        entry.getValue().count());
            }
            for (Map.Entry<MetricName, Histogram> entry : histograms.entrySet()) {
                session.addHistogram(entry.getKey(), entry.getValue());
            }
            for (Map.Entry<MetricName, Meter> entry : meters.entrySet()) {
                session.addMetered(entry.getKey(), entry.getValue());
            }
            for (Map.Entry<MetricName, Timer> entry : timers.entrySet()) {
                session.addTimer(entry.getKey(), entry.getValue());
            }
        } finally {
            try {
                session.close();
            } catch (Exception e) {
                // Unable to register... these exceptions handled by AggregateMetricSender
            }
        }
    }

    public MetricMetadata getMetricMetadata() {
        return metricMetadata;
    }

    public enum MetricDetails {
        // For {@link com.codahale.metrics.Sampling}
        MEDIAN("median"),
        PERCENT_75("75th"),
        PERCENT_95("95th"),
        PERCENT_98("98th"),
        PERCENT_99("99th"),
        PERCENT_999("999th"),
        MAX("max"),
        MIN("min"),
        STD_DEV("stddev"),
        MEAN("mean"),

        // For {@link com.codahale.metrics.Counting}
        COUNT("count"),

        // For {@link com.codahale.metrics.Metered}
        RATE_MEAN("rate.mean"),
        RATE_1_MIN("rate.1min"),
        RATE_5_MIN("rate.5min"),
        RATE_15_MIN("rate.15min");
        public static final Set<MetricDetails> ALL = Collections.unmodifiableSet(EnumSet.allOf(MetricDetails.class));
        public static final Set<MetricDetails> DEFAULTS = ImmutableSet.of(COUNT, MIN, MEAN, MAX);

        private final String description;

        MetricDetails(String description) {
            this.description = description;
        }

        public String getDescription() {
            return description;
        }
    }

    public static final class Builder {
        private final MetricsRegistry registry;
        private String defaultSourceName;
        private AuthToken authToken;
        private SignalFxReceiverEndpoint endpoint = new SignalFxEndpoint();
        private String name = "signalfx-reporter";
        private int timeoutMs = HttpDataPointProtobufReceiverFactory.DEFAULT_TIMEOUT_MS;
        private DataPointReceiverFactory dataPointReceiverFactory = new
                HttpDataPointProtobufReceiverFactory(endpoint);
        private MetricPredicate filter = MetricPredicate.ALL;
        private TimeUnit rateUnit = TimeUnit.SECONDS;
        private TimeUnit durationUnit = TimeUnit.MILLISECONDS; // Maybe nano eventually?
        private Set<MetricDetails> detailsToAdd = MetricDetails.DEFAULTS;
        private Collection<OnSendErrorHandler> onSendErrorHandlerCollection = Collections.emptyList();
        private MetricMetadata metricMetadata = new MetricMetadataImpl();
        private int version = HttpDataPointProtobufReceiverFactory.DEFAULT_VERSION;
        private boolean useLocalTime = false;
        private boolean sendExtraMetricDimensions = false;
        private final ImmutableMap.Builder<String, String> defaultDimensions = new ImmutableMap.Builder<String, String>();

        public Builder(MetricsRegistry registry, String authToken) {
            this(registry, new StaticAuthToken(authToken));
        }

        public Builder(MetricsRegistry registry, AuthToken authToken) {
            this(registry, authToken, SourceNameHelper.getDefaultSourceName());
        }

        public Builder(MetricsRegistry registry, AuthToken authToken, String defaultSourceName) {
            this.registry = registry;
            this.authToken = authToken;
            this.defaultSourceName = defaultSourceName;
        }

        public Builder setDefaultSourceName(String defaultSourceName) {
            this.defaultSourceName = defaultSourceName;
            return this;
        }

        public Builder setAuthToken(AuthToken authToken) {
            this.authToken = authToken;
            return this;
        }

        public Builder setEndpoint(SignalFxReceiverEndpoint endpoint) {
            this.endpoint = endpoint;
            this.dataPointReceiverFactory =
                    new HttpDataPointProtobufReceiverFactory(endpoint)
                            .setTimeoutMs(this.timeoutMs)
                            .setVersion(this.version);
            return this;
        }

        @Deprecated
        public Builder setDataPointEndpoint(DataPointReceiverEndpoint dataPointEndpoint) {
            return setEndpoint(dataPointEndpoint);
        }

        public Builder setName(String name) {
            this.name = name;
            return this;
        }

        public Builder setTimeoutMs(int timeoutMs) {
            this.timeoutMs = timeoutMs;
            this.dataPointReceiverFactory =
                    new HttpDataPointProtobufReceiverFactory(endpoint)
                            .setVersion(this.version)
                            .setTimeoutMs(this.timeoutMs);
            return this;
        }

        public Builder setVersion(int version) {
            this.version = version;
            this.dataPointReceiverFactory =
                    new HttpDataPointProtobufReceiverFactory(endpoint)
                            .setVersion(this.version)
                            .setTimeoutMs(this.timeoutMs);
            return this;
        }

        public Builder setDataPointReceiverFactory(
                DataPointReceiverFactory dataPointReceiverFactory) {
            this.dataPointReceiverFactory = dataPointReceiverFactory;
            return this;
        }

        public Builder setFilter(MetricPredicate filter) {
            this.filter = filter;
            return this;
        }

        public Builder setRateUnit(TimeUnit rateUnit) {
            this.rateUnit = rateUnit;
            return this;
        }

        public Builder setDurationUnit(TimeUnit durationUnit) {
            this.durationUnit = durationUnit;
            return this;
        }

        public Builder setDetailsToAdd(Set<MetricDetails> detailsToAdd) {
            this.detailsToAdd = detailsToAdd;
            return this;
        }

        public Builder setOnSendErrorHandlerCollection(
                Collection<OnSendErrorHandler> onSendErrorHandlerCollection) {
            this.onSendErrorHandlerCollection = onSendErrorHandlerCollection;
            return this;
        }

        public Builder setMetricMetadata(MetricMetadata metricMetadata) {
            this.metricMetadata = metricMetadata;
            return this;
        }

        /**
         * Will use the local system time, rather than zero, on sent datapoints.
         * @param useLocalTime    If true, use local system time
         * @return this
         */
        public Builder useLocalTime(boolean useLocalTime) {
            this.useLocalTime = useLocalTime;
            return this;
        }

        /**
         * Adds all dimensions to the default dimensions to be sent with every datapoint from this
         * reporter.
         *
         * @param dimensions
         *            non-null map of string value pairs
         * @return this
         */
        public Builder addDimensions(Map<String, String> dimensions) {
            // loop here to get "null value" protection of addDimension
            for (Map.Entry<String, String> entry: dimensions.entrySet()) {
                this.addDimension(entry.getKey(), entry.getValue());
            }
            return this;
        }

        /**
         * Adds a dimension to the default dimensions to be sent with every datapoint from this
         * reporter.
         *
         * @param name
         *            Name of the dimension
         * @param value
         *            Value of the dimension. If null then the dimension is not added.
         * @return this
         */
        public Builder addDimension(String name, String value) {
            if (value != null) {
                this.defaultDimensions.put(name, value);
            }
            return this;
        }

        /**
         * If set to true, will send the metric group name as a dimension
         * called "metric_group" for all metrics which have a group set.
         *
         * @return this
         */
        public Builder sendExtraMetricDimensions(boolean sendExtraMetricDimensions) {
            this.sendExtraMetricDimensions = sendExtraMetricDimensions;
            return this;
        }

        public SignalFxReporter build() {
            AggregateMetricSender aggregateMetricSender = new AggregateMetricSender(
                    defaultSourceName, dataPointReceiverFactory, authToken, onSendErrorHandlerCollection);
            return new SignalFxReporter(registry, name, filter, rateUnit, durationUnit,
                    aggregateMetricSender, detailsToAdd, metricMetadata, useLocalTime,
                    sendExtraMetricDimensions, defaultDimensions.build());
        }
    }
}