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

import java.nio.charset.StandardCharsets;
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.Getter;
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.RetryUtils;
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;
import ru.yandex.webmaster3.storage.events.data.WmcEventContainer;

/**
 * ishalaru
 * 03.12.2020
 **/
@Slf4j
@RequiredArgsConstructor(onConstructor_ = {@Autowired})
public class WMCEventsObservingProcessingService implements IDataProcessing {
    @Getter
    private final Map<WMCEventType, WMCEventsObserver> wmcEventsObserverMap;
    private final LogbrokerClient eventsLogbrokerClient;
    private final LogbrokerWriter problemEventsLogbrokerClient;
    private final WMCEventsObservingService wmcEventsObservingService;
    @Setter
    @Getter
    private String observerName;
    @Setter
    private int threadPoolSize;
    @Setter
    private int queueCapacity;
    private ArrayBlockingQueue<Pair<WMCEventType, String>> 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) {
        log.info("{} Message start processed.", observerName);
        messageContainer.commit();
        log.info("{} Message commited.", observerName);
        for (byte[] rawMessage : messageContainer.getRawMessages()) {
            try {
                final WmcEventContainer wmcEventContainer = JsonMapping.readValue(rawMessage, WmcEventContainer.class);
                for (String data : wmcEventContainer.getContent()) {
                    queue.put(Pair.of(wmcEventContainer.getEventType(), data));
                }
            } catch (InterruptedException e) {
                log.error(e.getMessage(), e);
            } catch (Exception exp) {
                log.error(exp.getMessage(), exp);
            }
        }
        log.info("{} Message processed.", observerName);

    }


    private void sendProblemEventToLogbroker(WMCEvent event, String content) {
        final WMCEventProblemContainer wmcEventProblemContainer =
                new WMCEventProblemContainer(event.getType(), content, observerName);
        try {
            RetryUtils.execute(RetryUtils.linearBackoff(3, Duration.standardSeconds(10)), () -> {
                problemEventsLogbrokerClient.write(JsonMapping.writeValueAsBytes(wmcEventProblemContainer));
            });
        } catch (Exception e) {
            log.error("Failed to send event to Logbroker: " + event.getId(), e);
        }
    }

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

    @Value
    public class Processing implements Runnable {

        @Override
        public void run() {
            while (!Thread.currentThread().isInterrupted()) {
                Pair<WMCEventType, String> rawMessage = null;
                try {
                    rawMessage = queue.poll(1000, TimeUnit.MILLISECONDS);
                } catch (InterruptedException e) {
                    rawMessage = null;
                }
                if (rawMessage == null) {
                    continue;
                }
                try {
                    final WMCEventContent deserialize =
                            WMCEventMapper.deserialize(rawMessage.getKey(), rawMessage.getValue());
                    final WMCEventsObserver wmcEventsObserver = wmcEventsObserverMap.get(rawMessage.getKey());
                    if (wmcEventsObserver == null) {
                        log.error("Observer for type: {}, not found in {}", rawMessage.getKey(), observerName);
                    } else {
                        final WMCEvent wmcEvent = new WMCEvent(UUIDs.timeBased(), rawMessage.getKey(), deserialize, 1L);
                        ObserverResult eventProcessed = wmcEventsObservingService.processEvent(observerName, wmcEvent, wmcEventsObserver);
                        if (eventProcessed != ObserverResult.FAIL && wmcEventsObserver.isProcessedLoggingEnabled()) {
                            sendProcessedEventToLogbroker(wmcEvent, observerName);
                        }
                        if (eventProcessed == ObserverResult.FAIL) {
                            sendProblemEventToLogbroker(wmcEvent, rawMessage.getRight());
                        }
                    }
                }catch (Exception exp){
                    log.error("Can't process message: {}:{}",rawMessage.getKey(),rawMessage.getValue());
                    log.error(exp.getMessage(),exp);
                }

            }
        }
    }

}
