package ru.yandex.webmaster3.core.solomon;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

import lombok.RequiredArgsConstructor;
import org.joda.time.Duration;
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.solomon.metric.SolomonCommonLabels;
import ru.yandex.webmaster3.core.solomon.metric.SolomonKey;
import ru.yandex.webmaster3.core.solomon.metric.SolomonMetricRegistry;
import ru.yandex.webmaster3.core.util.RetryUtils;
import ru.yandex.webmaster3.core.util.environment.YandexEnvironmentProvider;
import ru.yandex.webmaster3.core.util.environment.YandexEnvironmentType;

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

    private static final int QUEUE_SIZE = 5000;
    private static final int BATCH_SIZE = 2;

    public static final int RESOLUTION_SECONDS = 15;

    private final SolomonPushAPIService solomonPushAPIService;
    private final SolomonMetricRegistry solomonMetricRegistry;
    private final String project;
    private final String cluster;
    private final String host;
    private final boolean enabled;
    private final String service;

    private BlockingQueue<List<SolomonSensor>> sensorsQueue = new ArrayBlockingQueue<>(QUEUE_SIZE);
    private Thread senderThread;

    public void init() {
        if (!enabled) {
            log.warn("Solomon push metrics service disabled");
            return;
        }

        senderThread = new Thread(new SensorSender());
        senderThread.setDaemon(true);
        senderThread.setName("solomon-pusher");
        senderThread.start();
    }

    public void destroy() {
        if (enabled) {
            senderThread.interrupt();
        }
    }

    @Scheduled(cron = "0/15 * * * * *")
    private void push() {
        if (!enabled) {
            return;
        }
        long now = (System.currentTimeMillis() / (RESOLUTION_SECONDS * 1000L)) * RESOLUTION_SECONDS;
        var indicators = solomonMetricRegistry.getIndicatorsSnapshot();
        if (indicators.isEmpty()) {
            return;
        }
        List<SolomonSensor> sensors = new ArrayList<>();
        for (Map.Entry<SolomonKey, Number> entry : indicators.entrySet()) {
            sensors.add(new SolomonSensor(entry.getKey().getLabels(), now, entry.getValue()));
        }
        if (!sensorsQueue.offer(sensors)) {
            log.error("Sensors queue is full, will try to send synchronously");
            try {
                if (!solomonPushAPIService.pushMetrics(sensors, new SolomonCommonLabels(project, service, cluster, host))) {
                    log.error("Synchronous metrics push failed unrecoverably");
                }
            } catch (Exception e) {
                log.error("Synchronous metrics push failed", e);
            }
        }
    }

    private class SensorSender implements Runnable {
        @Override
        public void run() {
            try {
                while (!Thread.interrupted()) {
                    List<SolomonSensor> firstBatch = sensorsQueue.take();

                    List<SolomonSensor> toSend;
                    int queueSize = Math.min(sensorsQueue.size(), BATCH_SIZE);
                    if (queueSize == 0) {
                        toSend = firstBatch;
                    } else {
                        toSend = new ArrayList<>(firstBatch);
                        for (int i = 0; i < queueSize; i++) {
                            List<SolomonSensor> nullableSensorsList = sensorsQueue.poll();
                            if (nullableSensorsList != null) {
                                toSend.addAll(nullableSensorsList);
                            } else {
                                break; // queue is empty
                            }
                        }
                    }
                    try {
                        boolean sent = RetryUtils.query(
                                RetryUtils.linearBackoff(5, Duration.standardMinutes(5)),
                                () -> solomonPushAPIService.pushMetrics(toSend, new SolomonCommonLabels(project, service, cluster, host))
                        );
                        if (!sent) {
                            YandexEnvironmentType environmentType = YandexEnvironmentProvider.getEnvironmentType();
                            if (environmentType == YandexEnvironmentType.PRODUCTION) {
                                log.error("Solomon write failed unrecoverably - will drop " + queueSize + " items.");
                            }
                        }
                    } catch (Exception e) {
                        log.error("Solomon write failed - will drop " + queueSize + " items.", e);
                    }
                }
            } catch (InterruptedException e) {
                log.error("Sensors pusher was interrupted", e);
            }
        }
    }
}
