package cz.senslog.analytics.module.api;

import cz.senslog.analytics.domain.*;
import cz.senslog.analytics.repository.ConfigurationRepository;
import cz.senslog.analytics.repository.AnalyticsDataRepository;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.time.Instant;
import java.time.OffsetDateTime;
import java.util.*;

import static cz.senslog.analytics.utils.TimeUtils.minToMillis;
import static cz.senslog.analytics.utils.TimeUtils.minToSec;

public abstract class CollectorModule extends SimpleModule {

    private static final Logger logger = LogManager.getLogger(CollectorModule.class);

    private static final int MAX_WAITING_TO_HARVEST_MIN = 10;

    private final CollectorType type;

    protected final ConfigurationRepository configRepo;

    private final AnalyticsDataRepository statisticsRepo;

    private Map<Long, List<CollectedStatistics>> collectedStats;
    private Map<Long, Group> groupMap;
    private List<Threshold> thresholds;

    private Instant lastHarvesting;

    protected CollectorModule(CollectorType type, ConfigurationRepository configRepo, AnalyticsDataRepository statisticsRepo) {
        this.type = type;
        this.configRepo = configRepo;
        this.statisticsRepo = statisticsRepo;

        this.groupMap = Collections.emptyMap();
        this.thresholds = Collections.emptyList();
        this.collectedStats = Collections.emptyMap();
    }

    protected abstract void handle(Collection<DoubleStatistics> aggregations);

    public final CollectorType type() {
        return type;
    }

    protected void postConfigure() {}

    @Override
    protected final void configure() {
        lastHarvesting = Instant.now();
        configRepo.loadGroupsByCollectorType(type).onSuccess(groups -> {
            collectedStats = new HashMap<>(groups.size());
            groupMap = groups;

            configRepo.loadGroupThresholds(type).onSuccess(data -> {
                thresholds = data;
                postConfigure();
            }).onFailure(logger::catching);
        }).onFailure(logger::catching);
        long period = minToMillis(MAX_WAITING_TO_HARVEST_MIN);
        vertx.setPeriodic(period, period, l -> {
           if (overtakeHarvestTime()) {
               harvestStatistics();
           }
        });
    }

    private void harvestStatistics() {
        OffsetDateTime currentHarvest = OffsetDateTime.now();
        for (List<CollectedStatistics> statistics : collectedStats.values()) {
            Iterator<CollectedStatistics> stIterator = statistics.iterator();
            while (stIterator.hasNext()) {
                CollectedStatistics st = stIterator.next();
                if (currentHarvest.isAfter(st.endTime())) {
                    handle(st.statistics());
                    stIterator.remove();
                }
            }
        }
        lastHarvesting = currentHarvest.toInstant();
    }

    private boolean overtakeHarvestTime() {
        return lastHarvesting.plusSeconds(minToSec(MAX_WAITING_TO_HARVEST_MIN)).isBefore(Instant.now());
    }

    protected List<Threshold> thresholds() {
        return thresholds;
    }

    protected Map<Long, Group> groups() {
        return groupMap;
    }

    private void collect(Observation value) {
        Group group = groupMap.get(value.source().groupId());

        List<CollectedStatistics> groupStatistics = collectedStats.computeIfAbsent(group.id(), g -> new ArrayList<>());

        OffsetDateTime timestamp = value.timestamp();
        boolean newDataAccepted = false;
        Iterator<CollectedStatistics> statisticsIterator = groupStatistics.iterator();
        while(statisticsIterator.hasNext()) {
            CollectedStatistics st = statisticsIterator.next();
            if (timestamp.isEqual(st.startTime()) ||    // startInterval <= timestamp < endInterval
                    (timestamp.isAfter(st.startTime()) && timestamp.isBefore(st.endTime()))
            ) {
                st.accept(value.source(), value.value());
                newDataAccepted = true;
            } else if (timestamp.isAfter(st.endTime())) {
                handle(st.statistics());
                statisticsIterator.remove();
            }
        }
        lastHarvesting = Instant.now();

        if (!newDataAccepted) {
            CollectedStatistics newSt = CollectedStatistics.init(group, value.timestamp());
            newSt.accept(value.source(), value.value());
            groupStatistics.add(newSt);
        }
    }

    @Override
    public void run() {
        eventBus().<Observation>consumer(id(), v -> collect(v.body()));
    }

    protected final void persist(StatisticRecord record) {
        statisticsRepo.saveStatisticRecord(record).onFailure(logger::catching);
    }
}