/*
 * Copyright 2013-2015 the original author or authors.
 *
 * Licensed 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.springframework.cloud.netflix.hystrix.stream;

import java.io.IOException;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.concurrent.LinkedBlockingQueue;

import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
import com.netflix.hystrix.HystrixCircuitBreaker;
import com.netflix.hystrix.HystrixCommandKey;
import com.netflix.hystrix.HystrixCommandMetrics;
import com.netflix.hystrix.HystrixCommandProperties;
import com.netflix.hystrix.HystrixThreadPoolKey;
import com.netflix.hystrix.HystrixThreadPoolMetrics;
import com.netflix.hystrix.util.HystrixRollingNumberEvent;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.beans.BeansException;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.scheduling.annotation.Scheduled;

/**
 * @author Spencer Gibb
 *
 * @see com.netflix.hystrix.contrib.metrics.eventstream.HystrixMetricsPoller (nested
 * private class MetricsPoller)
 */
public class HystrixStreamTask implements ApplicationContextAware {
	
	private static Log log = LogFactory.getLog(HystrixStreamTask.class);

	private MessageChannel outboundChannel;

	private DiscoveryClient discoveryClient;

	private HystrixStreamProperties properties;

	private ApplicationContext context;

	// Visible for testing
	final LinkedBlockingQueue<String> jsonMetrics;

	private final JsonFactory jsonFactory = new JsonFactory();

	public HystrixStreamTask(MessageChannel outboundChannel, DiscoveryClient discoveryClient, HystrixStreamProperties properties) {
		this.outboundChannel = outboundChannel;
		this.discoveryClient = discoveryClient;
		this.properties = properties;
		this.jsonMetrics = new LinkedBlockingQueue<>(properties.getSize());
	}

	@Override
	public void setApplicationContext(ApplicationContext applicationContext)
			throws BeansException {
		this.context = applicationContext;
	}

	// TODO: use integration to split this up?
	@Scheduled(fixedRateString = "${hystrix.stream.queue.sendRate:500}")
	public void sendMetrics() {
		ArrayList<String> metrics = new ArrayList<>();
		this.jsonMetrics.drainTo(metrics);

		if (!metrics.isEmpty()) {
			if (log.isTraceEnabled()) {
				log.trace("sending stream metrics size: " + metrics.size());
			}
			for (String json : metrics) {
				// TODO: batch all metrics to one message
				try {
					// TODO: remove the explicit content type when s-c-stream can handle that for us
					this.outboundChannel.send(MessageBuilder.withPayload(json)
							.setHeader(MessageHeaders.CONTENT_TYPE,
									this.properties.getContentType())
							.build());
				}
				catch (Exception ex) {
					if (log.isTraceEnabled()) {
						log.trace("failed sending stream metrics: " + ex.getMessage());
					}
				}
			}
		}
	}

	@Scheduled(fixedRateString = "${hystrix.stream.queue.gatherRate:500}")
	public void gatherMetrics() {
		try {
			// command metrics
			Collection<HystrixCommandMetrics> instances = HystrixCommandMetrics
					.getInstances();
			if (!instances.isEmpty()) {
				log.trace("gathering metrics size: " + instances.size());
			}

			ServiceInstance localService = this.discoveryClient.getLocalServiceInstance();

			for (HystrixCommandMetrics commandMetrics : instances) {
				HystrixCommandKey key = commandMetrics.getCommandKey();
				HystrixCircuitBreaker circuitBreaker = HystrixCircuitBreaker.Factory
						.getInstance(key);

				StringWriter jsonString = new StringWriter();
				JsonGenerator json = this.jsonFactory.createGenerator(jsonString);

				json.writeStartObject();

				addServiceData(json, localService);
				json.writeObjectFieldStart("data");
				json.writeStringField("type", "HystrixCommand");
				String name = key.name();

				if (this.properties.isPrefixMetricName()) {
					name = localService.getServiceId() + "." + name;
				}

				json.writeStringField("name", name);
				json.writeStringField("group", commandMetrics.getCommandGroup().name());
				json.writeNumberField("currentTime", System.currentTimeMillis());

				// circuit breaker
				if (circuitBreaker == null) {
					// circuit breaker is disabled and thus never open
					json.writeBooleanField("isCircuitBreakerOpen", false);
				}
				else {
					json.writeBooleanField("isCircuitBreakerOpen",
							circuitBreaker.isOpen());
				}
				HystrixCommandMetrics.HealthCounts healthCounts = commandMetrics
						.getHealthCounts();
				json.writeNumberField("errorPercentage",
						healthCounts.getErrorPercentage());
				json.writeNumberField("errorCount", healthCounts.getErrorCount());
				json.writeNumberField("requestCount", healthCounts.getTotalRequests());

				// rolling counters
				json.writeNumberField("rollingCountCollapsedRequests", commandMetrics
						.getRollingCount(HystrixRollingNumberEvent.COLLAPSED));
				json.writeNumberField("rollingCountExceptionsThrown", commandMetrics
						.getRollingCount(HystrixRollingNumberEvent.EXCEPTION_THROWN));
				json.writeNumberField("rollingCountFailure", commandMetrics
						.getRollingCount(HystrixRollingNumberEvent.FAILURE));
				json.writeNumberField("rollingCountFallbackFailure", commandMetrics
						.getRollingCount(HystrixRollingNumberEvent.FALLBACK_FAILURE));
				json.writeNumberField("rollingCountFallbackRejection", commandMetrics
						.getRollingCount(HystrixRollingNumberEvent.FALLBACK_REJECTION));
				json.writeNumberField("rollingCountFallbackSuccess", commandMetrics
						.getRollingCount(HystrixRollingNumberEvent.FALLBACK_SUCCESS));
				json.writeNumberField("rollingCountResponsesFromCache", commandMetrics
						.getRollingCount(HystrixRollingNumberEvent.RESPONSE_FROM_CACHE));
				json.writeNumberField("rollingCountSemaphoreRejected", commandMetrics
						.getRollingCount(HystrixRollingNumberEvent.SEMAPHORE_REJECTED));
				json.writeNumberField("rollingCountShortCircuited", commandMetrics
						.getRollingCount(HystrixRollingNumberEvent.SHORT_CIRCUITED));
				json.writeNumberField("rollingCountSuccess", commandMetrics
						.getRollingCount(HystrixRollingNumberEvent.SUCCESS));
				json.writeNumberField("rollingCountThreadPoolRejected", commandMetrics
						.getRollingCount(HystrixRollingNumberEvent.THREAD_POOL_REJECTED));
				json.writeNumberField("rollingCountTimeout", commandMetrics
						.getRollingCount(HystrixRollingNumberEvent.TIMEOUT));

				json.writeNumberField("currentConcurrentExecutionCount",
						commandMetrics.getCurrentConcurrentExecutionCount());

				// latency percentiles
				json.writeNumberField("latencyExecute_mean",
						commandMetrics.getExecutionTimeMean());
				json.writeObjectFieldStart("latencyExecute");
				json.writeNumberField("0", commandMetrics.getExecutionTimePercentile(0));
				json.writeNumberField("25",
						commandMetrics.getExecutionTimePercentile(25));
				json.writeNumberField("50",
						commandMetrics.getExecutionTimePercentile(50));
				json.writeNumberField("75",
						commandMetrics.getExecutionTimePercentile(75));
				json.writeNumberField("90",
						commandMetrics.getExecutionTimePercentile(90));
				json.writeNumberField("95",
						commandMetrics.getExecutionTimePercentile(95));
				json.writeNumberField("99",
						commandMetrics.getExecutionTimePercentile(99));
				json.writeNumberField("99.5",
						commandMetrics.getExecutionTimePercentile(99.5));
				json.writeNumberField("100",
						commandMetrics.getExecutionTimePercentile(100));
				json.writeEndObject();
				//
				json.writeNumberField("latencyTotal_mean",
						commandMetrics.getTotalTimeMean());
				json.writeObjectFieldStart("latencyTotal");
				json.writeNumberField("0", commandMetrics.getTotalTimePercentile(0));
				json.writeNumberField("25", commandMetrics.getTotalTimePercentile(25));
				json.writeNumberField("50", commandMetrics.getTotalTimePercentile(50));
				json.writeNumberField("75", commandMetrics.getTotalTimePercentile(75));
				json.writeNumberField("90", commandMetrics.getTotalTimePercentile(90));
				json.writeNumberField("95", commandMetrics.getTotalTimePercentile(95));
				json.writeNumberField("99", commandMetrics.getTotalTimePercentile(99));
				json.writeNumberField("99.5",
						commandMetrics.getTotalTimePercentile(99.5));
				json.writeNumberField("100", commandMetrics.getTotalTimePercentile(100));
				json.writeEndObject();

				// property values for reporting what is actually seen by the command
				// rather than what was set somewhere
				HystrixCommandProperties commandProperties = commandMetrics
						.getProperties();

				json.writeNumberField(
						"propertyValue_circuitBreakerRequestVolumeThreshold",
						commandProperties.circuitBreakerRequestVolumeThreshold().get());
				json.writeNumberField(
						"propertyValue_circuitBreakerSleepWindowInMilliseconds",
						commandProperties.circuitBreakerSleepWindowInMilliseconds()
								.get());
				json.writeNumberField(
						"propertyValue_circuitBreakerErrorThresholdPercentage",
						commandProperties.circuitBreakerErrorThresholdPercentage().get());
				json.writeBooleanField("propertyValue_circuitBreakerForceOpen",
						commandProperties.circuitBreakerForceOpen().get());
				json.writeBooleanField("propertyValue_circuitBreakerForceClosed",
						commandProperties.circuitBreakerForceClosed().get());
				json.writeBooleanField("propertyValue_circuitBreakerEnabled",
						commandProperties.circuitBreakerEnabled().get());

				json.writeStringField("propertyValue_executionIsolationStrategy",
						commandProperties.executionIsolationStrategy().get().name());
				json.writeNumberField(
						"propertyValue_executionIsolationThreadTimeoutInMilliseconds",
						commandProperties.executionIsolationThreadTimeoutInMilliseconds()
								.get());
				json.writeBooleanField(
						"propertyValue_executionIsolationThreadInterruptOnTimeout",
						commandProperties.executionIsolationThreadInterruptOnTimeout()
								.get());
				json.writeStringField(
						"propertyValue_executionIsolationThreadPoolKeyOverride",
						commandProperties.executionIsolationThreadPoolKeyOverride()
								.get());
				json.writeNumberField(
						"propertyValue_executionIsolationSemaphoreMaxConcurrentRequests",
						commandProperties
								.executionIsolationSemaphoreMaxConcurrentRequests()
								.get());
				json.writeNumberField(
						"propertyValue_fallbackIsolationSemaphoreMaxConcurrentRequests",
						commandProperties
								.fallbackIsolationSemaphoreMaxConcurrentRequests().get());

				// TODO
				/*
				 * The following are commented out as these rarely change and are verbose
				 * for streaming for something people don't change. We could perhaps allow
				 * a property or request argument to include these.
				 */

				// json.put("propertyValue_metricsRollingPercentileEnabled",
				// commandProperties.metricsRollingPercentileEnabled().get());
				// json.put("propertyValue_metricsRollingPercentileBucketSize",
				// commandProperties.metricsRollingPercentileBucketSize().get());
				// json.put("propertyValue_metricsRollingPercentileWindow",
				// commandProperties.metricsRollingPercentileWindowInMilliseconds().get());
				// json.put("propertyValue_metricsRollingPercentileWindowBuckets",
				// commandProperties.metricsRollingPercentileWindowBuckets().get());
				// json.put("propertyValue_metricsRollingStatisticalWindowBuckets",
				// commandProperties.metricsRollingStatisticalWindowBuckets().get());
				json.writeNumberField(
						"propertyValue_metricsRollingStatisticalWindowInMilliseconds",
						commandProperties.metricsRollingStatisticalWindowInMilliseconds()
								.get());

				json.writeBooleanField("propertyValue_requestCacheEnabled",
						commandProperties.requestCacheEnabled().get());
				json.writeBooleanField("propertyValue_requestLogEnabled",
						commandProperties.requestLogEnabled().get());

				json.writeNumberField("reportingHosts", 1); // this will get summed across
															// all instances in a cluster

				json.writeEndObject(); // end data attribute
				json.writeEndObject();
				json.close();

				// output
				this.jsonMetrics.add(jsonString.getBuffer().toString());
			}

			// thread pool metrics
			for (HystrixThreadPoolMetrics threadPoolMetrics : HystrixThreadPoolMetrics
					.getInstances()) {
				HystrixThreadPoolKey key = threadPoolMetrics.getThreadPoolKey();

				StringWriter jsonString = new StringWriter();
				JsonGenerator json = this.jsonFactory.createGenerator(jsonString);
				json.writeStartObject();

				addServiceData(json, localService);
				json.writeObjectFieldStart("data");

				json.writeStringField("type", "HystrixThreadPool");
				json.writeStringField("name", key.name());
				json.writeNumberField("currentTime", System.currentTimeMillis());

				json.writeNumberField("currentActiveCount",
						threadPoolMetrics.getCurrentActiveCount().intValue());
				json.writeNumberField("currentCompletedTaskCount",
						threadPoolMetrics.getCurrentCompletedTaskCount().longValue());
				json.writeNumberField("currentCorePoolSize",
						threadPoolMetrics.getCurrentCorePoolSize().intValue());
				json.writeNumberField("currentLargestPoolSize",
						threadPoolMetrics.getCurrentLargestPoolSize().intValue());
				json.writeNumberField("currentMaximumPoolSize",
						threadPoolMetrics.getCurrentMaximumPoolSize().intValue());
				json.writeNumberField("currentPoolSize",
						threadPoolMetrics.getCurrentPoolSize().intValue());
				json.writeNumberField("currentQueueSize",
						threadPoolMetrics.getCurrentQueueSize().intValue());
				json.writeNumberField("currentTaskCount",
						threadPoolMetrics.getCurrentTaskCount().longValue());
				json.writeNumberField("rollingCountThreadsExecuted",
						threadPoolMetrics.getRollingCountThreadsExecuted());
				json.writeNumberField("rollingMaxActiveThreads",
						threadPoolMetrics.getRollingMaxActiveThreads());

				json.writeNumberField("propertyValue_queueSizeRejectionThreshold",
						threadPoolMetrics.getProperties().queueSizeRejectionThreshold()
								.get());
				json.writeNumberField(
						"propertyValue_metricsRollingStatisticalWindowInMilliseconds",
						threadPoolMetrics.getProperties()
								.metricsRollingStatisticalWindowInMilliseconds().get());

				json.writeNumberField("reportingHosts", 1); // this will get summed across
															// all instances in a cluster

				json.writeEndObject(); // end of data object
				json.writeEndObject();
				json.close();
				// output to stream
				this.jsonMetrics.add(jsonString.getBuffer().toString());
			}
		}
		catch (Exception ex) {
			log.error("Error adding metrics to queue", ex);
		}
	}

	private void addServiceData(JsonGenerator json, ServiceInstance localService)
			throws IOException {
		json.writeObjectFieldStart("origin");
		json.writeStringField("host", localService.getHost());
		json.writeNumberField("port", localService.getPort());
		json.writeStringField("serviceId", localService.getServiceId());
		if (this.properties.isSendId()) {
			json.writeStringField("id", this.context.getId());
		}
		json.writeEndObject();
	}

}
