package ru.yandex.webmaster3.monitoring.queue.moderation;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.TreeMap;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;

import com.datastax.driver.core.utils.UUIDs;
import com.google.common.base.Stopwatch;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.mutable.MutableInt;
import org.apache.commons.lang3.mutable.MutableLong;
import org.apache.commons.lang3.mutable.MutableObject;
import org.apache.commons.lang3.tuple.Pair;
import org.joda.time.DateTime;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

import ru.yandex.webmaster3.core.data.WebmasterHostId;
import ru.yandex.webmaster3.core.solomon.HandleCommonMetricsService;
import ru.yandex.webmaster3.core.solomon.Indicators;
import ru.yandex.webmaster3.core.solomon.SolomonSensor;
import ru.yandex.webmaster3.monitoring.common.YtQueueUtils;
import ru.yandex.webmaster3.storage.host.AllVerifiedHostsCacheService;
import ru.yandex.webmaster3.storage.host.moderation.camelcase.DisplayNameRequest;
import ru.yandex.webmaster3.storage.host.moderation.camelcase.dao.HostDisplayNameModerationRequestsYDao;
import ru.yandex.webmaster3.storage.host.moderation.camelcase.dao.HostDisplayNameModerationYtRequestsYDao;
import ru.yandex.webmaster3.storage.util.yt.YtPath;
import ru.yandex.webmaster3.storage.util.yt.YtService;

import static ru.yandex.webmaster3.storage.host.moderation.camelcase.HostDisplayNameModerationRequestState.IN_PROGRESS;

/**
 * @author leonidrom
 */
@Slf4j
@Service
@RequiredArgsConstructor(onConstructor_ = {@Autowired})
public class HostDisplayNameModerationMonitoringService {
    private static final Predicate<String> RESULTS_TABLES_FILTER = t -> t.startsWith("req") || t.startsWith("auto");
    private static final String SECTION_LABEL_VALUE = "host_display_name";
    private static final String CASSANDRA_QUEUE_TYPE = "cassandra_queue";
    private static final String YT_QUEUE_TYPE = "yt_queue";
    private static final String HOST_DISPLAY_NAME_QUEUE_TYPE = "host_display_name_queue";
    private static final String YT_RESULTS_QUEUE_TYPE = "yt_results";
    private static final int AVERAGE_SENSORS_SIZE = 500;
    private static DateTime SKIP_REQUESTS_BEFORE_DATE = DateTime.parse("2018-01-25T19:54:52.128+03:00");

    private final HandleCommonMetricsService handleCommonMetricsService;
    private final YtService ytService;

    private final HostDisplayNameModerationRequestsYDao hdnModerationRequestsYDao;
    private final HostDisplayNameModerationYtRequestsYDao hdnYtRequestsYDao;
    private final AllVerifiedHostsCacheService allVerifiedHostsCacheService;


    @Value("${webmaster3.monitoring.solomon.hostregions.enabled}")
    private boolean enabled;
    @Value("${webmaster3.monitoring.solomon.hostregions.refresh-interval}")
    private long refreshIntervalSeconds;
    @Value("${external.yt.service.hahn.root.default}/export/yang/host_display_name")
    private YtPath ytRequestsPath;
    @Value("${external.yt.service.hahn.root.default}/import/yang/host_display_name")
    private YtPath ytResultsPath;

    @Scheduled(cron = "${webmaster3.monitoring.solomon.hostregions.refresh-cron}")
    private void push() throws Exception {
        if (!enabled) {
            log.warn("Host display name moderation metrics service disabled");
            return;
        }
        log.info("Started collecting metrics for host display name moderation");
        List<SolomonSensor> sensors = new ArrayList<>();

        Stopwatch sw = Stopwatch.createStarted();
        Pair<Integer, Long> unprocessedCassandraRequestsStats = getUnprocessedCassandraRequestsStats();
        sensors.add(SolomonSensor.createAligned(refreshIntervalSeconds, unprocessedCassandraRequestsStats.getLeft())
                .withLabel(SolomonSensor.LABEL_SECTION, SECTION_LABEL_VALUE)
                .withLabel(SolomonSensor.LABEL_DATA_TYPE, CASSANDRA_QUEUE_TYPE)
                .withLabel(SolomonSensor.LABEL_INDICATOR, Indicators.QUEUE_SIZE));
        sensors.add(SolomonSensor.createAligned(refreshIntervalSeconds, unprocessedCassandraRequestsStats.getRight() / 1000L)
                .withLabel(SolomonSensor.LABEL_SECTION, SECTION_LABEL_VALUE)
                .withLabel(SolomonSensor.LABEL_DATA_TYPE, CASSANDRA_QUEUE_TYPE)
                .withLabel(SolomonSensor.LABEL_INDICATOR, Indicators.DATA_AGE));
        log.info("Collected stats for cassandra requests in {}s", sw.stop().elapsed(TimeUnit.SECONDS));

        sw.reset().start();
        YtQueueUtils.QueueStats unprocessedYtRequestsStats = YtQueueUtils.getQueueStats(
                ytService, ytRequestsPath, YtQueueUtils.ALL_TABLES_FILTER, YtQueueUtils.JAVA_TIMESTAMP_EXTRACTOR);
        sensors.add(SolomonSensor.createAligned(refreshIntervalSeconds, unprocessedYtRequestsStats.tablesCount)
                .withLabel(SolomonSensor.LABEL_SECTION, SECTION_LABEL_VALUE)
                .withLabel(SolomonSensor.LABEL_DATA_TYPE, YT_QUEUE_TYPE)
                .withLabel(SolomonSensor.LABEL_INDICATOR, Indicators.QUEUE_SIZE));
        sensors.add(SolomonSensor.createAligned(refreshIntervalSeconds, unprocessedYtRequestsStats.maxAgeInMillis / 1000)
                .withLabel(SolomonSensor.LABEL_SECTION, SECTION_LABEL_VALUE)
                .withLabel(SolomonSensor.LABEL_DATA_TYPE, YT_QUEUE_TYPE)
                .withLabel(SolomonSensor.LABEL_INDICATOR, Indicators.DATA_AGE));
        log.info("Collected stats for unprocessed YT requests in {}s", sw.stop().elapsed(TimeUnit.SECONDS));

        sw.reset().start();
        YtQueueUtils.QueueStats unprocessedYtResultsStats = YtQueueUtils.getQueueStats(
                ytService, ytResultsPath, RESULTS_TABLES_FILTER, YtQueueUtils.JAVA_TIMESTAMP_EXTRACTOR);
        sensors.add(SolomonSensor.createAligned(refreshIntervalSeconds, unprocessedYtResultsStats.tablesCount)
                .withLabel(SolomonSensor.LABEL_SECTION, SECTION_LABEL_VALUE)
                .withLabel(SolomonSensor.LABEL_DATA_TYPE, YT_RESULTS_QUEUE_TYPE)
                .withLabel(SolomonSensor.LABEL_INDICATOR, Indicators.QUEUE_SIZE));
        sensors.add(SolomonSensor.createAligned(refreshIntervalSeconds, unprocessedYtResultsStats.maxAgeInMillis / 1000)
                .withLabel(SolomonSensor.LABEL_SECTION, SECTION_LABEL_VALUE)
                .withLabel(SolomonSensor.LABEL_DATA_TYPE, YT_RESULTS_QUEUE_TYPE)
                .withLabel(SolomonSensor.LABEL_INDICATOR, Indicators.DATA_AGE));
        log.info("Collected stats for unprocessed YT results in {}s", sw.stop().elapsed(TimeUnit.SECONDS));

        handleCommonMetricsService.handle(sensors, AVERAGE_SENSORS_SIZE);
        log.info("Done collecting metrics for host display name moderation");
    }

    @Scheduled(cron = "${webmaster3.monitoring.solomon.hostregions.refresh-heavy-cron}")
    private void pushHeavyMetrics() {
        if (!enabled) {
            log.warn("Host display name moderation metrics service disabled");
            return;
        }
        log.info("Started collecting heavy metrics for host display name moderation");

        Stopwatch sw = Stopwatch.createStarted();
        NavigableMap<Long, DisplayNameRequest> unprocessedRequestsMap = getUnprocessedRequestsStats();
        log.info("Collected stats for whole host display name moderation process in {}s, total  {}",
                sw.stop().elapsed(TimeUnit.SECONDS), unprocessedRequestsMap.size());

        List<SolomonSensor> sensors = new ArrayList<>();
        sensors.add(SolomonSensor.createAligned(refreshIntervalSeconds, unprocessedRequestsMap.size())
                .withLabel(SolomonSensor.LABEL_SECTION, SECTION_LABEL_VALUE)
                .withLabel(SolomonSensor.LABEL_DATA_TYPE, HOST_DISPLAY_NAME_QUEUE_TYPE)
                .withLabel(SolomonSensor.LABEL_INDICATOR, Indicators.QUEUE_SIZE));

        Map.Entry<Long, DisplayNameRequest> oldestEntry = unprocessedRequestsMap.firstEntry();
        if (oldestEntry == null) {
            return;
        }

        sensors.add(SolomonSensor.createAligned(refreshIntervalSeconds, oldestEntry.getKey() / 1000L)
                .withLabel(SolomonSensor.LABEL_SECTION, SECTION_LABEL_VALUE)
                .withLabel(SolomonSensor.LABEL_DATA_TYPE, HOST_DISPLAY_NAME_QUEUE_TYPE)
                .withLabel(SolomonSensor.LABEL_INDICATOR, Indicators.DATA_AGE));

        handleCommonMetricsService.handle(sensors, AVERAGE_SENSORS_SIZE);
        log.info("Done collecting heavy metrics for host display name moderation");
    }

    /**
     * Возвращает пару (размер очереди, возраст) для очереди запросов в Кассандре,
     * ожидающих отправку в Yt.
     */
    private Pair<Integer, Long> getUnprocessedCassandraRequestsStats() {
        long now = System.currentTimeMillis();
        MutableInt count = new MutableInt(0);
        MutableLong minTime = new MutableLong(Long.MAX_VALUE);
        hdnYtRequestsYDao.forEachRequestId(
                reqId -> {
                    long reqTime = UUIDs.unixTimestamp(reqId);
                    if (minTime.getValue() > reqTime) {
                        minTime.setValue(reqTime);
                    }

                    count.increment();
                }
        );

        if (minTime.getValue() == Long.MAX_VALUE) {
            minTime.setValue(now);
        }

        return Pair.of(count.getValue(), now - minTime.getValue());
    }

    /**
     * Возвращает мапу (возраст, заявка) дл всех необработанных заявок.
     */
    private NavigableMap<Long, DisplayNameRequest> getUnprocessedRequestsStats() {
        long now = System.currentTimeMillis();
        NavigableMap<Long, DisplayNameRequest> unprocessedRequestsMap = new TreeMap<>(Comparator.reverseOrder());

        MutableObject<DisplayNameRequest> maxReqObj = new MutableObject<>(null);
        MutableObject<WebmasterHostId> curHostIdObj = new MutableObject<>(null);
        hdnModerationRequestsYDao.forEach(req -> {
            var hostId = req.getHostId();
            if (!allVerifiedHostsCacheService.contains(hostId) || SKIP_REQUESTS_BEFORE_DATE.isAfter(req.getCreationDate())) {
                return;
            }

            var curHostId = curHostIdObj.getValue();
            var maxReq = maxReqObj.getValue();
            if (!hostId.equals(curHostId)) {
                if (maxReq != null && maxReq.getState() == IN_PROGRESS) {
                    unprocessedRequestsMap.put(now - maxReq.getCreationDate().getMillis(), maxReq);
                }

                maxReqObj.setValue(req);
                curHostIdObj.setValue(hostId);
            } else {
                if (req.getCreationDate().isAfter(maxReq.getCreationDate())) {
                    maxReqObj.setValue(req);
                }
            }
        });

        var maxReq = maxReqObj.getValue();
        if (maxReq != null && maxReq.getState() == IN_PROGRESS) {
            unprocessedRequestsMap.put(now - maxReq.getCreationDate().getMillis(), maxReq);
        }

        return unprocessedRequestsMap;
    }
}
