package store;

import core.manager.ConstantManager;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import profile.IProfile;
import store.cache.CompositeKeyCache;
import store.entity.olap.AshAggrMinData;
import store.entity.olap.AshAggrMinData15Sec;
import store.entity.olap.AshAggrMinData1Min;
import store.service.OlapDAO;

import javax.inject.Inject;
import javax.inject.Singleton;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@Slf4j
@Singleton
public class OlapCacheManager {
    private OlapDAO olapDAO;
    @Getter @Setter private IProfile iProfile;

    // cache
    public enum AggregationTime {OneSecond, FifteenSecond, OneMinute}

    private long cache1SecLongId = 0;
    private long cache15SecLongId = 0;
    private long cache1MinLongId = 0;

    private Map<CompositeKeyCache, Map<Integer,Integer>> hashmap1SecCache = new HashMap<>();
    private Map<CompositeKeyCache, Map<Integer,Integer>> hashmap15SecCache = new HashMap<>();
    private Map<CompositeKeyCache, Map<Integer,Integer>> hashmap1MinCache = new HashMap<>();

    @Inject
    public OlapCacheManager (OlapDAO olapDAO){
        this.olapDAO = olapDAO;
    }

    /**
     *<pre>
     *{@code Load RAW data from ASH online to aggregate set of tables.
     *
     * @param dt dateId of row
     * @param parameter of aggregations by sqlId, sessionId+serial#, etc
     * @param additional parameters (OpName, Program, UserId etc.)
     * @param waitEvent wait class or event
     * @param waitClass group by type of wait class or event
     *                     CPU used = 0,
     *                     System I/O = 1, etc
     *}
     * </pre>
     */
    public void loadAshRawData(LocalDateTime dt, String parameter,
                               String[] additionalParams, String waitEvent, byte waitClass){
        // Check directory
        int paramIdSec = olapDAO.getCheckOrLoadParameter(parameter, additionalParams); // 1 sec/min

        int waitEventI = olapDAO.getCheckOrLoadWaitEvent(waitEvent, waitClass);

        LocalDateTime beginDtS = LocalDateTime.of(
                dt.getYear(),
                dt.getMonth(),
                dt.getDayOfMonth(),
                dt.getHour(),
                dt.getMinute(),
                dt.getSecond());
        loadAshRawData(beginDtS, paramIdSec, waitEventI, cache1SecLongId, hashmap1SecCache,
                String.valueOf(AggregationTime.OneSecond));

        LocalDateTime beginDtS15 = LocalDateTime.of(
                dt.getYear(),
                dt.getMonth(),
                dt.getDayOfMonth(),
                dt.getHour(),
                dt.getMinute(),
                getLocalDateTime15Sec(dt.getSecond()));
        loadAshRowData15(beginDtS15, paramIdSec, waitEventI, cache15SecLongId, hashmap15SecCache,
                String.valueOf(AggregationTime.FifteenSecond));

        LocalDateTime beginDtM = LocalDateTime.of(
                dt.getYear(),
                dt.getMonth(),
                dt.getDayOfMonth(),
                dt.getHour(),
                dt.getMinute());
        loadAshRawData(beginDtM, paramIdSec, waitEventI, cache1MinLongId, hashmap1MinCache,
                String.valueOf(AggregationTime.OneMinute));
    }

    private void loadAshRawData(LocalDateTime dt, int paramId, int waitEventId,
                                long cacheId, Map<CompositeKeyCache, Map<Integer, Integer>> hashmapCache,
                                String aggregationTime){
        long dateIdSec = dt.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
        CompositeKeyCache compositeKeyCacheSec = new CompositeKeyCache(dateIdSec, paramId);

        if (cacheId == dateIdSec) { //update cache data with new values
            this.updateCacheData(hashmapCache, compositeKeyCacheSec, waitEventId);
        } else {
            cacheId = dateIdSec;

            this.loadDataToLocalDb(hashmapCache, aggregationTime);//load data to bdb
            hashmapCache.clear();

            this.updateCacheData(hashmapCache, compositeKeyCacheSec, waitEventId);
        }
    }

    private void loadAshRowData15(LocalDateTime dt, int paramId, int waitEventId,
                                long cacheId, Map<CompositeKeyCache, Map<Integer, Integer>> hashmapCache,
                                String aggregationTime){
        long dateIdSec = dt.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
        CompositeKeyCache compositeKeyCacheSec = new CompositeKeyCache(dateIdSec, paramId);

        this.updateCacheData(hashmapCache, compositeKeyCacheSec, waitEventId);

        cache15SecLongId = dateIdSec;
    }

    public void unloadCacheToDB() {
        if (hashmap1SecCache.entrySet().stream().findAny().isPresent()) {
            cache1SecLongId = hashmap1SecCache.entrySet().stream().findAny().get().getKey().getDateId();
            this.loadDataToLocalDb(hashmap1SecCache, String.valueOf(AggregationTime.OneSecond));//load data to bdb
            hashmap1SecCache.clear();
        }
    }

    public void unloadCacheToDB15(long currentDbSysdate){
            if ((long) hashmap15SecCache.entrySet().size() > 1) {
                if (hashmap15SecCache.entrySet().stream().max(Map.Entry.comparingByKey()).isPresent()) {
                    Map<CompositeKeyCache, Map<Integer, Integer>> hashMapTmp = new HashMap<>();
                    long maxV = hashmap15SecCache.entrySet().stream().max(Map.Entry.comparingByKey()).get().getKey().getDateId();

                    hashmap15SecCache.entrySet().stream()
                            .filter(s -> s.getKey().getDateId() == maxV)
                            .forEach(e -> hashMapTmp.putIfAbsent(e.getKey(), e.getValue()));

                    hashmap15SecCache.keySet().removeIf(val -> val.getDateId() == maxV);

                    this.loadDataToLocalDb(hashmap15SecCache, String.valueOf(AggregationTime.FifteenSecond));//load data to bdb
                    hashmap15SecCache.clear();

                    hashmap15SecCache = hashMapTmp;
                }
            } else if ((long) hashmap15SecCache.entrySet().size() == 1) {
                if (hashmap15SecCache.entrySet().stream().max(Map.Entry.comparingByKey()).isPresent()) {
                    long maxV = hashmap15SecCache.entrySet().stream().max(Map.Entry.comparingByKey()).get().getKey().getDateId();

                    if ((currentDbSysdate - maxV) > 15000) {
                        this.loadDataToLocalDb(hashmap15SecCache, String.valueOf(AggregationTime.FifteenSecond));//load data to bdb
                        hashmap15SecCache.clear();
                    }
                }
            }
    }

    private void updateCacheData(Map<CompositeKeyCache, Map<Integer,Integer>> hashmapCache,
                                        CompositeKeyCache compositeKeyCache, int waitEventI){
        if (hashmapCache.containsKey(compositeKeyCache)){
            if (hashmapCache.get(compositeKeyCache).containsKey(waitEventI)){
                int tmp = hashmapCache.get(compositeKeyCache).get(waitEventI);
                hashmapCache.get(compositeKeyCache).put(waitEventI, tmp + 1);
            } else {
                hashmapCache.get(compositeKeyCache).put(waitEventI, 1);
            }
        } else {
            hashmapCache.put(compositeKeyCache, new HashMap<>());
            hashmapCache.get(compositeKeyCache).put(waitEventI, 1);
        }
    }

    private void loadDataToLocalDb(Map<CompositeKeyCache, Map<Integer,Integer>> hashmapCache, String aggregationTime){

        hashmapCache.entrySet().forEach(e -> {

            int[] waitId = new int[e.getValue().size()];
            int[] waitClass = new int[e.getValue().size()];
            int[] sum = new int[e.getValue().size()];

            int index = 0;
            for (Map.Entry<Integer, Integer> mapEntry : e.getValue().entrySet()) {
                waitId[index] = mapEntry.getKey();
                waitClass[index] = this.olapDAO.getClassIdForWaitEventId(mapEntry.getKey());
                sum[index] = mapEntry.getValue();
                index++;
            }

            boolean isExist = putDataOverwrite(aggregationTime,
                    e.getKey().getDateId(), e.getKey().getParamId(), waitId, waitClass, sum);

            if (!isExist){

                Map<Integer, Integer> allWaitId0 =
                        ConstantManager.zipToMap(ConstantManager.toList(waitId), ConstantManager.toList(sum));
                Map<Integer, Integer> allWaitId1 =
                        ConstantManager.zipToMap(
                                ConstantManager.toList(
                                        getMatrixValues(0, aggregationTime, e.getKey())), //mtrx[0]::waitId;
                                ConstantManager.toList(
                                        getMatrixValues(2, aggregationTime, e.getKey()))); //mtrx[2]::sum;

                Map<Integer, Integer> mx = Stream.of(allWaitId0, allWaitId1)
                        .map(Map::entrySet)
                        .flatMap(Collection::stream)
                        .collect(
                                Collectors.toMap(
                                        Map.Entry::getKey,
                                        Map.Entry::getValue,
                                        Integer::sum
                                )
                        );

                int[] waitId0 = new int[mx.size()];
                int[] waitClass0 = new int[mx.size()];
                int[] sum0 = new int[mx.size()];

                int index0 = 0;
                for (Map.Entry<Integer, Integer> mapEntry0 : mx.entrySet()) {
                    waitId0[index0] = mapEntry0.getKey();
                    waitClass0[index0] = this.olapDAO.getClassIdForWaitEventId(mapEntry0.getKey());
                    sum0[index0] = mapEntry0.getValue();
                    index0++;
                }

                putDataOverwrite(aggregationTime, e.getKey().getDateId(), e.getKey().getParamId(), waitId0, waitClass0, sum0);
            }

        });
    }

    private int[] getMatrixValues(int id, String aggregationTime, CompositeKeyCache compositeKeyCache){
            switch (aggregationTime) {
                case "OneSecond":
                    AshAggrMinData aggrMinData1Sec =
                            this.olapDAO.getAshAggrMinDataDAO().getAshAggrMinDataRange(
                                    compositeKeyCache.getDateId(), compositeKeyCache.getParamId());
                    return aggrMinData1Sec.getMatrixValues()[id];
                case "FifteenSecond":
                    AshAggrMinData15Sec aggrMinData15Sec =
                            this.olapDAO.getAshAggrMinData15SecDAO().getAshAggrMinDataRange(
                                    compositeKeyCache.getDateId(), compositeKeyCache.getParamId());
                    return aggrMinData15Sec.getMatrixValues()[id];
                case "OneMinute":
                    AshAggrMinData1Min aggrMinData1Min =
                            this.olapDAO.getAshAggrMinData1MinDAO().getAshAggrMinDataRange(
                                    compositeKeyCache.getDateId(), compositeKeyCache.getParamId());
                    return aggrMinData1Min.getMatrixValues()[id];
                default:
                    throw new IllegalArgumentException("Invalid aggregation time");
            }
    }

    private boolean putDataOverwrite(String aggregationTime, long dateId, int paramId, int[] waitId, int[] waitClass, int[] sum){
        switch (aggregationTime) {
            case "OneSecond":
                AshAggrMinData obj1sec = new AshAggrMinData(dateId, paramId, waitId, waitClass, sum);
                return olapDAO.getAshAggrMinDataDAO().putDataNoOverwrite(obj1sec);
            case "FifteenSecond":
                AshAggrMinData15Sec obj15sec = new AshAggrMinData15Sec(dateId, paramId, waitId, waitClass, sum);
                return olapDAO.getAshAggrMinData15SecDAO().putDataNoOverwrite(obj15sec);
            case "OneMinute":
                AshAggrMinData1Min obj1min = new AshAggrMinData1Min(dateId, paramId, waitId, waitClass, sum);
                return olapDAO.getAshAggrMinData1MinDAO().putDataNoOverwrite(obj1min);
            default:
                throw new IllegalArgumentException("Invalid aggregation time");
        }
    }

    private int getLocalDateTime15Sec(int seconds){
        if (seconds >= 0 & seconds < 15){
            return 0;
        } else if (seconds >= 15 & seconds < 30){
            return 15;
        } else if (seconds >= 30 & seconds < 45) {
            return 30;
        } else if (seconds >= 45 & seconds <= 59) {
            return 45;
        } else {
            return 0;
        }
    }

}
