/*
 * Copyright 2012-2017 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.boot.actuate.metrics.repository.redis;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.springframework.boot.actuate.metrics.Metric;
import org.springframework.boot.actuate.metrics.repository.MultiMetricRepository;
import org.springframework.boot.actuate.metrics.writer.Delta;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.BoundZSetOperations;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.util.Assert;

/**
 * {@link MultiMetricRepository} implementation backed by a redis store. Metric values are
 * stored as zset values and the timestamps as regular values, both against a key composed
 * of the group name prefixed with a constant prefix (default "spring.groups."). The group
 * names are stored as a zset under "keys." + {@code [prefix]}.
 *
 * @author Dave Syer
 */
public class RedisMultiMetricRepository implements MultiMetricRepository {

	private static final String DEFAULT_METRICS_PREFIX = "spring.groups.";

	private final String prefix;

	private final String keys;

	private final BoundZSetOperations<String, String> zSetOperations;

	private final RedisOperations<String, String> redisOperations;

	public RedisMultiMetricRepository(RedisConnectionFactory redisConnectionFactory) {
		this(redisConnectionFactory, DEFAULT_METRICS_PREFIX);
	}

	public RedisMultiMetricRepository(RedisConnectionFactory redisConnectionFactory,
			String prefix) {
		Assert.notNull(redisConnectionFactory, "RedisConnectionFactory must not be null");
		this.redisOperations = RedisUtils.stringTemplate(redisConnectionFactory);
		if (!prefix.endsWith(".")) {
			prefix = prefix + ".";
		}
		this.prefix = prefix;
		this.keys = "keys." + this.prefix.substring(0, prefix.length() - 1);
		this.zSetOperations = this.redisOperations.boundZSetOps(this.keys);
	}

	@Override
	public Iterable<Metric<?>> findAll(String group) {

		BoundZSetOperations<String, String> zSetOperations = this.redisOperations
				.boundZSetOps(keyFor(group));

		Set<String> keys = zSetOperations.range(0, -1);
		Iterator<String> keysIt = keys.iterator();

		List<Metric<?>> result = new ArrayList<>(keys.size());
		List<String> values = this.redisOperations.opsForValue().multiGet(keys);
		for (String v : values) {
			String key = keysIt.next();
			result.add(deserialize(group, key, v, zSetOperations.score(key)));
		}
		return result;

	}

	@Override
	public void set(String group, Collection<Metric<?>> values) {
		String groupKey = keyFor(group);
		trackMembership(groupKey);
		BoundZSetOperations<String, String> zSetOperations = this.redisOperations
				.boundZSetOps(groupKey);
		for (Metric<?> metric : values) {
			String raw = serialize(metric);
			String key = keyFor(metric.getName());
			zSetOperations.add(key, metric.getValue().doubleValue());
			this.redisOperations.opsForValue().set(key, raw);
		}
	}

	@Override
	public void increment(String group, Delta<?> delta) {
		String groupKey = keyFor(group);
		trackMembership(groupKey);
		BoundZSetOperations<String, String> zSetOperations = this.redisOperations
				.boundZSetOps(groupKey);
		String key = keyFor(delta.getName());
		double value = zSetOperations.incrementScore(key, delta.getValue().doubleValue());
		String raw = serialize(
				new Metric<>(delta.getName(), value, delta.getTimestamp()));
		this.redisOperations.opsForValue().set(key, raw);
	}

	@Override
	public Iterable<String> groups() {
		Set<String> range = this.zSetOperations.range(0, -1);
		Collection<String> result = new ArrayList<>();
		for (String key : range) {
			result.add(key.substring(this.prefix.length()));
		}
		return result;
	}

	@Override
	public long countGroups() {
		return this.zSetOperations.size();
	}

	@Override
	public void reset(String group) {
		String groupKey = keyFor(group);
		if (this.redisOperations.hasKey(groupKey)) {
			BoundZSetOperations<String, String> zSetOperations = this.redisOperations
					.boundZSetOps(groupKey);
			Set<String> keys = zSetOperations.range(0, -1);
			for (String key : keys) {
				this.redisOperations.delete(key);
			}
			this.redisOperations.delete(groupKey);
		}
		this.zSetOperations.remove(groupKey);
	}

	private Metric<?> deserialize(String group, String redisKey, String v, Double value) {
		Date timestamp = new Date(Long.valueOf(v));
		return new Metric<>(nameFor(redisKey), value, timestamp);
	}

	private String serialize(Metric<?> entity) {
		return String.valueOf(entity.getTimestamp().getTime());
	}

	private String keyFor(String name) {
		return this.prefix + name;
	}

	private String nameFor(String redisKey) {
		Assert.state(redisKey != null && redisKey.startsWith(this.prefix),
				"Invalid key does not start with prefix: " + redisKey);
		return redisKey.substring(this.prefix.length());
	}

	private void trackMembership(String redisKey) {
		this.zSetOperations.incrementScore(redisKey, 0.0D);
	}

}
