/*
 * Copyright 1999-2018 Alibaba Group Holding Ltd.
 *
 * 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 com.alibaba.csp.sentinel.dashboard.repository.metric;

import com.alibaba.csp.sentinel.dashboard.datasource.entity.MetricEntity;
import com.alibaba.csp.sentinel.dashboard.util.pool.PoolUtils;
import com.alibaba.csp.sentinel.util.StringUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.stream.Collectors;

/**
 * Caches metrics data in a period of time in memory.
 *
 * @author Carpenter Lee
 * @author Eric Zhao
 */
@Slf4j
@Component
public class InMemoryMetricsRepository implements MetricsRepository<MetricEntity> {

	private static final Class<MetricEntity> clazz = MetricEntity.class;

	@Autowired
	MongoTemplate mongoTemplate;

	private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();


	@Override
	public void save(MetricEntity entity) {
		if (entity == null || StringUtil.isBlank(entity.getApp())) {
			return;
		}
		readWriteLock.writeLock().lock();
		try {
			PoolUtils.SENTINEL.getInstance().execute(()->mongoTemplate.save(entity));
		}
		finally {
			readWriteLock.writeLock().unlock();

			PoolUtils.SENTINEL.getInstance().execute(this::remove);
		}

	}

	@Override
	public void saveAll(Iterable<MetricEntity> metrics) {
		if (metrics == null) {
			return;
		}
		readWriteLock.writeLock().lock();
		try {
			PoolUtils.SENTINEL.getInstance().execute(()->{
				List<MetricEntity> list = new ArrayList<>();
				metrics.forEach(list::add);
				mongoTemplate.insertAll(list);
			});
		}
		finally {
			readWriteLock.writeLock().unlock();

			PoolUtils.SENTINEL.getInstance().execute(this::remove);
		}
	}

	@Override
	public List<MetricEntity> queryByAppAndResourceBetween(String app, String resource, long startTime, long endTime) {
		List<MetricEntity> results = new ArrayList<>();
		if (StringUtil.isBlank(app)) {
			return results;
		}

		readWriteLock.readLock().lock();
		try {
			List<MetricEntity> metricList = mongoTemplate.find(new Query(
					Criteria.where("app").is(app)
							.andOperator(
									List.of(Criteria.where("timestamp").gte(startTime),
											Criteria.where("timestamp").lte(endTime))
							)
			), clazz);
			if (CollectionUtils.isEmpty(metricList)){
				return results;
			}
			results.addAll(metricList);
			return results;
		}
		finally {
			readWriteLock.readLock().unlock();
		}
	}

	@Override
	public List<String> listResourcesOfApp(String app) {
		List<String> results = new ArrayList<>();
		if (StringUtil.isBlank(app)) {
			return results;
		}
		readWriteLock.readLock().lock();
		try {
			final long minTimeMs = System.currentTimeMillis() - 1000 * 60 * 60;
			Map<String, MetricEntity> resourceCount = new ConcurrentHashMap<>(32);
			List<MetricEntity> entityList = mongoTemplate.find(new Query(
					Criteria.where("app").is(app)
							.and("timestamp").gte(minTimeMs)), clazz);
			if (CollectionUtils.isEmpty(entityList)){
				return results;
			}
			for (MetricEntity newEntity : entityList) {
				String resource = newEntity.getResource();
				if (resourceCount.containsKey(resource)) {
					MetricEntity oldEntity = resourceCount.get(resource);
					oldEntity.addPassQps(newEntity.getPassQps());
					oldEntity.addRtAndSuccessQps(newEntity.getRt(), newEntity.getSuccessQps());
					oldEntity.addBlockQps(newEntity.getBlockQps());
					oldEntity.addExceptionQps(newEntity.getExceptionQps());
					oldEntity.addCount(1);
				} else {
					resourceCount.put(resource, MetricEntity.copyOf(newEntity));
				}
			}
			// Order by last minute b_qps DESC.
			return resourceCount.entrySet()
					.stream()
					.sorted((o1, o2) -> {
						MetricEntity e1 = o1.getValue();
						MetricEntity e2 = o2.getValue();
						int t = e2.getBlockQps().compareTo(e1.getBlockQps());
						if (t != 0) {
							return t;
						}
						return e2.getPassQps().compareTo(e1.getPassQps());
					}).map(Entry::getKey).collect(Collectors.toList());
		}
		finally {
			readWriteLock.readLock().unlock();
		}
	}

	public void remove() {
		long count = mongoTemplate.count(new Query(), MetricEntity.class);
		// 每30万数据进行一次清库
		if (count > 300000){
			long currentTimeMillis = System.currentTimeMillis();
			log.info("清除 mongo sentinel 数据 {}, 30秒之前的所有数据", currentTimeMillis);
			mongoTemplate.remove(new Query()
					.addCriteria(Criteria.where("timestamp").lt(currentTimeMillis - 30000)), MetricEntity.class);
		}
	}

}
