package org.stagemonitor.core.metrics.metrics2;

import com.codahale.metrics.Counter;
import com.codahale.metrics.Gauge;
import com.codahale.metrics.Histogram;
import com.codahale.metrics.Meter;
import com.codahale.metrics.Metric;
import com.codahale.metrics.Timer;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.stagemonitor.core.CorePlugin;
import org.stagemonitor.core.elasticsearch.ElasticsearchClient;
import org.stagemonitor.core.util.HttpClient;
import org.stagemonitor.core.util.JsonUtils;
import org.stagemonitor.core.util.StringUtils;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Map;

import static org.stagemonitor.core.metrics.metrics2.MetricName.name;

public class ElasticsearchReporter extends ScheduledMetrics2Reporter {

	public static final String STAGEMONITOR_METRICS_INDEX_PREFIX = "stagemonitor-metrics-";
	public static final String ES_METRICS_LOGGER = "ElasticsearchMetrics";
	private static final String METRICS_TYPE = "metrics";
	private static final MetricName reportingTimeMetricName = name("reporting_time").tag("reporter", "elasticsearch").build();

	private final Logger logger = LoggerFactory.getLogger(getClass());
	private final Logger elasticsearchMetricsLogger;

	private final Map<String, String> globalTags;
	private final CorePlugin corePlugin;
	private final HttpClient httpClient;
	private final JsonFactory jfactory = new JsonFactory();
	private final Metric2RegistryModule metric2RegistryModule;
	private final ElasticsearchClient elasticsearchClient;

	public static ElasticsearchReporter.Builder forRegistry(Metric2Registry registry, CorePlugin corePlugin) {
		return new Builder(registry, corePlugin);
	}

	private ElasticsearchReporter(Builder builder) {
		super(builder);
		this.elasticsearchMetricsLogger = builder.getElasticsearchMetricsLogger();
		this.globalTags = builder.getGlobalTags();
		this.httpClient = builder.getHttpClient();
		this.jfactory.setCodec(JsonUtils.getMapper());
		this.metric2RegistryModule = new Metric2RegistryModule(builder.getRateUnit(), builder.getDurationUnit());
		this.corePlugin = builder.getCorePlugin();
		this.elasticsearchClient = corePlugin.getElasticsearchClient();
	}

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

		long timestamp = clock.getTime();

		final Timer.Context time = registry.timer(reportingTimeMetricName).time();
		final MetricsOutputStreamHandler metricsOutputStreamHandler = new MetricsOutputStreamHandler(gauges, counters, histograms, meters, timers, timestamp);
		if (!corePlugin.isOnlyLogElasticsearchMetricReports()) {
			if (!elasticsearchClient.isElasticsearchAvailable()) {
				return;
			}

			httpClient.send("POST", corePlugin.getElasticsearchUrl() + "/_bulk", null,
					metricsOutputStreamHandler, new ElasticsearchClient.BulkErrorReportingResponseHandler());
		} else {
			try {
				final ByteArrayOutputStream os = new ByteArrayOutputStream();
				metricsOutputStreamHandler.withHttpURLConnection(os);
				elasticsearchMetricsLogger.info(os.toString("UTF-8"));
			} catch (IOException e) {
				logger.warn(e.getMessage(), e);
			}
		}
		time.stop();
	}

	public void reportMetrics(Map<MetricName, Gauge> gauges, Map<MetricName, Counter> counters,
							  Map<MetricName, Histogram> histograms, final Map<MetricName, Meter> meters,
							  Map<MetricName, Timer> timers, OutputStream os, byte[] bulkActionBytes, long timestamp) throws IOException {

		reportMetric(gauges, timestamp, metric2RegistryModule.getValueWriter(Gauge.class), os, bulkActionBytes);
		reportMetric(counters, timestamp, metric2RegistryModule.getValueWriter(Counter.class), os, bulkActionBytes);
		reportMetric(histograms, timestamp, metric2RegistryModule.getValueWriter(Histogram.class), os, bulkActionBytes);
		reportMetric(meters, timestamp, metric2RegistryModule.getValueWriter(Meter.class), os, bulkActionBytes);
		reportMetric(timers, timestamp, metric2RegistryModule.getValueWriter(Timer.class), os, bulkActionBytes);
	}

	private <T extends Metric> void reportMetric(Map<MetricName, T> metrics, long timestamp, Metric2RegistryModule.ValueWriter<T> valueWriter,
												 OutputStream os, byte[] bulkActionBytes) throws IOException {

		for (Map.Entry<MetricName, T> entry : metrics.entrySet()) {
			os.write(bulkActionBytes);
			final JsonGenerator jg = jfactory.createGenerator(os);
			jg.writeStartObject();
			MetricName metricName = entry.getKey();
			jg.writeNumberField("@timestamp", timestamp);
			jg.writeStringField("name", metricName.getName());
			writeMap(jg, metricName.getTags());
			writeMap(jg, globalTags);
			valueWriter.writeValues(entry.getValue(), jg);
			jg.writeEndObject();
			jg.flush();
			os.write('\n');
		}
	}

	private void writeMap(JsonGenerator jg, Map<String, String> map) throws IOException {
		for (Map.Entry<String, String> entry : map.entrySet()) {
			jg.writeObjectField(entry.getKey(), entry.getValue());
		}
	}

	private class MetricsOutputStreamHandler implements HttpClient.OutputStreamHandler {
		private final Map<MetricName, Gauge> gauges;
		private final Map<MetricName, Counter> counters;
		private final Map<MetricName, Histogram> histograms;
		private final Map<MetricName, Meter> meters;
		private final Map<MetricName, Timer> timers;
		private final long timestamp;

		public MetricsOutputStreamHandler(Map<MetricName, Gauge> gauges, Map<MetricName, Counter> counters, Map<MetricName, Histogram> histograms, Map<MetricName, Meter> meters, Map<MetricName, Timer> timers, long timestamp) {
			this.gauges = gauges;
			this.counters = counters;
			this.histograms = histograms;
			this.meters = meters;
			this.timers = timers;
			this.timestamp = timestamp;
		}

		@Override
		public void withHttpURLConnection(OutputStream os) throws IOException {
			String bulkAction = ElasticsearchClient.getBulkHeader("index", STAGEMONITOR_METRICS_INDEX_PREFIX + StringUtils.getLogstashStyleDate(), METRICS_TYPE);
			byte[] bulkActionBytes = bulkAction.getBytes("UTF-8");
			reportMetrics(gauges, counters, histograms, meters, timers, os, bulkActionBytes, timestamp);
			os.close();
		}
	}

	public static class Builder extends ScheduledMetrics2Reporter.Builder<ElasticsearchReporter, Builder> {
		private HttpClient httpClient = new HttpClient();
		private Logger elasticsearchMetricsLogger = LoggerFactory.getLogger(ES_METRICS_LOGGER);
		private final CorePlugin corePlugin;

		private Builder(Metric2Registry registry, CorePlugin corePlugin) {
			super(registry, "stagemonitor-elasticsearch-reporter");
			this.corePlugin = corePlugin;
		}

		@Override
		public ElasticsearchReporter build() {
			return new ElasticsearchReporter(this);
		}

		public HttpClient getHttpClient() {
			return httpClient;
		}


		public Logger getElasticsearchMetricsLogger() {
			return elasticsearchMetricsLogger;
		}

		public Builder httpClient(HttpClient httpClient) {
			this.httpClient = httpClient;
			return this;
		}

		public Builder elasticsearchMetricsLogger(Logger elasticsearchMetricsLogger) {
			this.elasticsearchMetricsLogger = elasticsearchMetricsLogger;
			return this;
		}

		public CorePlugin getCorePlugin() {
			return corePlugin;
		}

	}
}
