package ru.yandex.webmaster3.storage.events.service;

import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import javax.annotation.PostConstruct;

import com.datastax.driver.core.utils.UUIDs;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.Value;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.tuple.Pair;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.springframework.beans.factory.annotation.Autowired;

import ru.yandex.webmaster3.core.logbroker.reader.IDataProcessing;
import ru.yandex.webmaster3.core.logbroker.reader.MessageContainer;
import ru.yandex.webmaster3.core.logbroker.writer.LogbrokerClient;
import ru.yandex.webmaster3.core.logbroker.writer.LogbrokerWriter;
import ru.yandex.webmaster3.core.util.json.JsonMapping;
import ru.yandex.webmaster3.storage.events.dao.WMCEventMapper;
import ru.yandex.webmaster3.storage.events.data.WMCEvent;
import ru.yandex.webmaster3.storage.events.data.WMCEventContent;
import ru.yandex.webmaster3.storage.events.data.WMCEventProblemContainer;
import ru.yandex.webmaster3.storage.events.data.WMCEventType;

/**
 * ishalaru
 * 03.12.2020
 **/
@Slf4j
@RequiredArgsConstructor(onConstructor_ = {@Autowired})
public class WMCEventsProblemsObservingProcessingService implements IDataProcessing {
    private static final int MAX_RETRIES_COUNT = 5;
    private static final String observerName = "problemObserver";

    private final LogbrokerClient eventsLogbrokerClient;
    private final LogbrokerWriter problemEventsLogbrokerClient;
    private final WMCEventsObservingService wmcEventsObservingService;

    private Map<Pair<String, WMCEventType>, WMCEventsObserver> wmcEventsObserverMap;

    @Setter
    private int threadPoolSize;
    @Setter
    private int queueCapacity;
    private ArrayBlockingQueue<byte[]> queue;
    private ExecutorService executorService;

    @PostConstruct
    public void init() {
        queue = new ArrayBlockingQueue<>(queueCapacity);
        executorService = Executors.newFixedThreadPool(
                threadPoolSize,
                new ThreadFactoryBuilder()
                        .setDaemon(true)
                        .setNameFormat(observerName + "-lbevent-worker-%d")
                        .build()
        );
        for (int i = 0; i < threadPoolSize; i++) {
            executorService.submit(new Processing());
        }
    }

    @Override
    public void process(MessageContainer messageContainer) {
        messageContainer.commit();
        for (byte[] rawMessage : messageContainer.getRawMessages()) {
            try {
                queue.put(rawMessage);
            } catch (InterruptedException e) {
                log.error(e.getMessage(), e);
            }
        }

    }


    private void writeInLog(WMCEvent event, String observerName) {
        String message = WMCEventMapper.serializeProcessed(event, observerName, DateTime.now());
        try {
            eventsLogbrokerClient.write(message.getBytes(StandardCharsets.UTF_8), Duration.standardSeconds(20));
        } catch (Exception e) {
            log.error("Failed to send event to Logbroker: " + event.getId(), e);
        }
    }

    private void writeInProblemTopic(WMCEventProblemContainer wmcEventProblemContainer) {

        try {
            problemEventsLogbrokerClient.write(JsonMapping.writeValueAsBytes(wmcEventProblemContainer));
        } catch (Exception e) {
            log.error("Failed to send event to Logbroker: " + wmcEventProblemContainer, e);
        }
    }


    @Autowired
    public void setListOfAvailableObservers(Collection<WMCEventsObservingProcessingService> collection) {
        wmcEventsObserverMap = new HashMap<>();
        for (WMCEventsObservingProcessingService eventProcessing : collection) {
            String name = eventProcessing.getObserverName();
            for (Map.Entry<WMCEventType, WMCEventsObserver> item : eventProcessing.getWmcEventsObserverMap().entrySet()) {
                wmcEventsObserverMap.put(Pair.of(name, item.getKey()), item.getValue());
            }
        }
    }

    @Value
    public class Processing implements Runnable {

        @Override
        public void run() {
            while (!Thread.currentThread().isInterrupted()) {
                byte[] rawMessage = null;
                try {
                    rawMessage = queue.poll(1000, TimeUnit.MILLISECONDS);
                } catch (InterruptedException e) {
                    rawMessage = null;
                }
                if (rawMessage == null) {
                    continue;
                }
                try {
                    final WMCEventProblemContainer wmcEventContainer = JsonMapping.readValue(rawMessage, WMCEventProblemContainer.class);
                    if (!wmcEventContainer.getNextProcessedTime().isAfterNow()) {
                        //Мы засыпаем если то что мы собираемся обработать еще рано обрабатывать, что бы не создавать загруженность очереди из пустого места.
                        writeInProblemTopic(wmcEventContainer);
                        continue;
                    }
                    final WMCEventContent deserialize =
                            WMCEventMapper.deserialize(wmcEventContainer.getEventType(), wmcEventContainer.getContent());
                    final WMCEventsObserver wmcEventsObserver = wmcEventsObserverMap.get(Pair.of(wmcEventContainer.getObserverName(), wmcEventContainer.getEventType()));
                    if (wmcEventsObserver == null) {
                        log.error("Observer for type: {}, not found in {}", wmcEventContainer.getEventType(), observerName);
                    } else {
                        final WMCEvent wmcEvent = new WMCEvent(UUIDs.timeBased(), wmcEventContainer.getEventType(), deserialize, 1L);
                        ObserverResult observerResult = wmcEventsObservingService.processEvent(wmcEventContainer.getObserverName(), wmcEvent, wmcEventsObserver);
                        if (observerResult == ObserverResult.FAIL) {
                            if (wmcEventContainer.getRetriesCount() >= MAX_RETRIES_COUNT) {
                                writeInLog(wmcEvent, observerName);
                            } else {
                                writeInProblemTopic(wmcEventContainer.incrementRetriesCount());
                            }
                        } else if (wmcEventsObserver.isProcessedLoggingEnabled()) {
                            writeInLog(wmcEvent, wmcEventContainer.getObserverName());
                        }


                    }
                } catch (Exception exp) {
                    log.error("Can't process message: " + new String(rawMessage), exp);
                }
            }
        }
    }

}
