package ru.yandex.webmaster3.monitoring.regions;

import com.google.common.collect.Sets;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.tuple.Pair;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import ru.yandex.webmaster3.core.data.W3RegionInfo;
import ru.yandex.webmaster3.core.data.WebmasterHostId;
import ru.yandex.webmaster3.core.regions.data.HostRegion;
import ru.yandex.webmaster3.core.regions.data.HostRegionSourceTypeEnum;
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.core.solomon.metric.SolomonKey;
import ru.yandex.webmaster3.monitoring.common.MonRegionsTreeService;
import ru.yandex.webmaster3.storage.host.AllVerifiedHostsCacheService;
import ru.yandex.webmaster3.storage.host.dao.HostRegionsCHDao;
import ru.yandex.webmaster3.storage.host.moderation.regions.HostModeratedRegions;
import ru.yandex.webmaster3.storage.host.moderation.regions.dao.HostModeratedRegionsYDao;
import ru.yandex.webmaster3.storage.host.service.MirrorService2;

import java.util.*;
import java.util.concurrent.*;
import java.util.stream.Collectors;

import static ru.yandex.webmaster3.core.regions.RegionUtils.NON_HIDDEN_REGION_TYPES_PREDICATE;

/**
 * @author leonidrom
 */
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class ModeratedRegionsMonitoringService {
    private static final Logger log = LoggerFactory.getLogger(ModeratedRegionsMonitoringService.class);

    private static final String LABEL_IS_MAIN_MIRROR = "is_main_mirror";

    private static final int TOTAL_THREADS = 16;
    private static final int AVERAGE_SENSORS_SIZE = 500;

    private final HandleCommonMetricsService handleCommonMetricsService;
    private final HostModeratedRegionsYDao hostModeratedRegionsYDao;
    private final HostRegionsCHDao hostRegionsCHDao;
    private final MonRegionsTreeService monRegionsTreeService;
    private final AllVerifiedHostsCacheService allVerifiedHostsCacheService;
    private final MirrorService2 mirrorService2;

    @Scheduled(cron = "0 0 0/12 * * *")
    private void pushModeratedRegionsStats() throws Exception {
        SolomonKey baseKeyHostsCounter = SolomonKey.create()
                .withLabel(SolomonSensor.LABEL_SECTION, "host_regions")
                .withLabel(SolomonSensor.LABEL_INDICATOR, Indicators.HOSTS_COUNT);

        List<Callable<MirrorsAwareStats>> callables = new ArrayList<>();
        List<HostModeratedRegions> batch = new ArrayList<>();
        hostModeratedRegionsYDao.forEach(regions -> {
            batch.add(regions);
            if (batch.size() >= 20_000) {
                var batchCopy = new ArrayList<>(batch);
                callables.add(() -> processHostModeratedRegionsBatch(batchCopy));
                batch.clear();
            }
        });

        if (!batch.isEmpty()) {
            callables.add(() -> processHostModeratedRegionsBatch(batch));
        }

        ExecutorService executorService = Executors.newFixedThreadPool(TOTAL_THREADS);
        List<Future<MirrorsAwareStats>> futures = executorService.invokeAll(callables);

        MirrorsAwareStats stats = new MirrorsAwareStats();
        for (Future<MirrorsAwareStats> f : futures) {
            stats.merge(f.get());
        }

        long now = (System.currentTimeMillis() / (TimeUnit.SECONDS.toMillis(3600))) * 3600;
        List<SolomonSensor> sensors = new ArrayList<>();
        fillSensors(now, baseKeyHostsCounter, stats, sensors, false);
        fillSensors(now, baseKeyHostsCounter, stats, sensors, true);

        handleCommonMetricsService.handle(sensors, AVERAGE_SENSORS_SIZE);

    }

    private void fillSensors(long now, SolomonKey baseKey, MirrorsAwareStats mirrorsAwareStats, List<SolomonSensor> sensors, boolean mainMirror) {
        HostModeratedRegionsStats stats = mirrorsAwareStats.getStats(mainMirror);
        int totalHosts = stats.unprocessedHosts + stats.processedHosts;
        baseKey = baseKey.withLabel(LABEL_IS_MAIN_MIRROR, String.valueOf(mainMirror));

        sensors.add(new SolomonSensor(baseKey.getLabels(), now, stats.processedHosts)
                .withLabel(SolomonSensor.LABEL_DATA_TYPE, "moderated_regions_processed"));
        sensors.add(new SolomonSensor(baseKey.getLabels(), now, (double) stats.processedHosts / totalHosts)
                .withLabel(SolomonSensor.LABEL_DATA_TYPE, "moderated_regions_processed_ratio"));

        sensors.add(new SolomonSensor(baseKey.getLabels(), now, stats.unprocessedHosts)
                .withLabel(SolomonSensor.LABEL_DATA_TYPE, "moderated_regions_unprocessed"));
        sensors.add(new SolomonSensor(baseKey.getLabels(), now, (double) stats.unprocessedHosts / totalHosts)
                .withLabel(SolomonSensor.LABEL_DATA_TYPE, "moderated_regions_unprocessed_ratio"));

        sensors.add(new SolomonSensor(baseKey.getLabels(), now, totalHosts)
                .withLabel(SolomonSensor.LABEL_DATA_TYPE, "moderated_regions_total"));
        log.info("ModeratedRegionsMonitoringService: mainMirror={}, total={}, processed={}, unprocessed={}, oldest={}",
                mainMirror, totalHosts, stats.processedHosts, stats.unprocessedHosts, stats.oldestUnprocessedHostId);
    }

    private MirrorsAwareStats processHostModeratedRegionsBatch(List<HostModeratedRegions> batch) {
        MirrorsAwareStats mirrorsAwareStats = new MirrorsAwareStats();
        for (HostModeratedRegions hmr : batch) {
            var hostId = hmr.getHostId();
            if (hostId == null) {
                log.error("Null host id");
                continue;
            }

            if (!allVerifiedHostsCacheService.contains(hostId)) {
                continue;
            }

            var createdDate = hmr.getCreated();
            if (createdDate == null) {
                log.error("Null created date for {}", hostId);
                continue;
            }

            boolean isMainMirror = mirrorService2.isMainMirror(hostId);
            HostModeratedRegionsStats stats = mirrorsAwareStats.getStats(isMainMirror);
            if (stats.oldestUnprocessedDate == null || createdDate.isBefore(stats.oldestUnprocessedDate)) {
                stats.oldestUnprocessedDate = createdDate;
                stats.oldestUnprocessedHostId = hostId;
            }

            Pair<Boolean, Boolean> p = hasPendingModeratedRegions(hostId, hmr.getRegions());
            boolean hostFound = p.getRight();
            if (!hostFound) {
                // У нас есть хосты с регионами из каталога, которых не подтверждены в ВМ
                // Их мы считать не хотим
                continue;
            }

            boolean hasPendingRegions = p.getLeft();
            if (!hasPendingRegions) {
                stats.processedHosts++;
            } else {
                stats.unprocessedHosts++;
            }
        }

        return mirrorsAwareStats;
    }

    private static class MirrorsAwareStats {
        private final HostModeratedRegionsStats mainMirrorStats = new HostModeratedRegionsStats();
        private final HostModeratedRegionsStats notMainMirrorStats = new HostModeratedRegionsStats();

        HostModeratedRegionsStats getStats(boolean mainMirror) {
            return mainMirror ? mainMirrorStats : notMainMirrorStats;
        }

        void merge(MirrorsAwareStats that) {
            mainMirrorStats.merge(that.mainMirrorStats);
            notMainMirrorStats.merge(that.notMainMirrorStats);
        }
    }

    private static class HostModeratedRegionsStats {
        int processedHosts = 0;
        int unprocessedHosts = 0;
        DateTime oldestUnprocessedDate = null;
        WebmasterHostId oldestUnprocessedHostId = null;

        void merge(HostModeratedRegionsStats s) {
            this.processedHosts += s.processedHosts;
            this.unprocessedHosts += s.unprocessedHosts;

            if (s.oldestUnprocessedDate != null) {
                if (this.oldestUnprocessedDate == null || this.oldestUnprocessedDate.isAfter(s.oldestUnprocessedDate)) {
                    this.oldestUnprocessedDate = s.oldestUnprocessedDate;
                    this.oldestUnprocessedHostId = s.oldestUnprocessedHostId;
                }
            }
        }
    }

    private Pair<Boolean, Boolean> hasPendingModeratedRegions(WebmasterHostId hostId, Set<Integer> moderatedRegions) {
        Optional<Set<HostRegion>> hostRegionsOpt = hostRegionsCHDao.getHostRegionsOptional(hostId);
        boolean hostFound = false;
        boolean hasPending = true;
        if (hostRegionsOpt.isEmpty()) {
            return Pair.of(hasPending, hostFound);
        }

        hostFound = true;
        Set<Integer> webmasterRegions = filterRegions(hostRegionsOpt.get());
        Set<Integer> diff = Sets.difference(moderatedRegions, webmasterRegions);
        hasPending = !diff.isEmpty();

        return Pair.of(hasPending, hostFound);
    }

    private Set<Integer> filterRegions(Collection<HostRegion> regions) {
        return regions.stream()
                .filter(region -> region.getSourceType() == HostRegionSourceTypeEnum.WEBMASTER)
                .map(region -> {
                    W3RegionInfo visibleRegion = monRegionsTreeService.getVisibleRegionOrParent(region.getRegionId(), NON_HIDDEN_REGION_TYPES_PREDICATE);
                    return visibleRegion == null ? null : visibleRegion.getId();
                })
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
    }
}

