package com.priusis.client.service;

import com.google.common.collect.Lists;
import com.priusis.client.service.conf.PcConnectionConfiguration;
import com.priusis.client.service.conf.PcPersistenceConfiguration;
import com.priusis.client.service.core.MqttService;
import com.priusis.client.service.core.MqttServiceImpl;
import com.priusis.monitor.mqtt.MqttClient;
import com.priusis.monitor.mqtt.MqttConnectResult;
import com.priusis.monitor.mqtt.MqttHandler;
import io.netty.buffer.Unpooled;
import io.netty.handler.codec.mqtt.MqttQoS;
import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.util.Iterator;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.*;

@Slf4j
public class MqttMessageSender implements Runnable {

    private MqttClient tbClient;

    private PersistentFileService persistentFileService;

    private PcPersistenceConfiguration persistence;
    private MqttHandler mqttService;

    private final PcConnectionConfiguration connection;

    private BlockingQueue<MessageFuturePair> incomingQueue;
    private Queue<Future<Void>> outgoingQueue;

    private static final String DEVICE_RPC_RES_TOPIC = "devices/rpc/req";
    private static final String DEVICE_RPC_TOPIC = DEVICE_RPC_RES_TOPIC + "/+";
    private static final String DEVICE_GET_ATTRIBUTES_RESPONSE_PLUS_TOPIC = "devices/attrs/res/+";
    private static final String DEVICE_GET_ATTRIBUTES_RESPONSE_TOPIC = "devices/attrs/res/1";

    public MqttMessageSender(PcPersistenceConfiguration persistence,
                             PcConnectionConfiguration connection,
                             MqttClient tbClient,
                             MqttHandler mqttService,
                             PersistentFileService persistentFileService,
                             BlockingQueue<MessageFuturePair> incomingQueue) {
        this.persistence = persistence;
        this.connection = connection;
        this.tbClient = tbClient;
        this.mqttService = mqttService;
        this.persistentFileService = persistentFileService;
        this.incomingQueue = incomingQueue;
        outgoingQueue = new ConcurrentLinkedQueue();
    }

    @Override
    public void run() {
        while (!Thread.interrupted()) {
            try {
                checkClientConnected();
                List<MqttPersistentMessage> storedMessages = getMessages();
                if (!storedMessages.isEmpty()) {
                    Iterator<MqttPersistentMessage> iter = storedMessages.iterator();
                    while (iter.hasNext()) {
                        if (!checkClientConnected()) {
                            persistentFileService.saveForResend(Lists.newArrayList(iter));
                            break;
                        }
                        MqttPersistentMessage message = iter.next();
                        log.debug("Sending message [{}]", message);
                        publishMqttMessage(message);
                    }
                } else {
                    Thread.sleep(persistence.getPollingInterval());
                }
            } catch (InterruptedException e) {
                log.trace(e.getMessage());
                Thread.currentThread().interrupt();
            } catch (Throwable e) {
                log.error(e.getMessage(), e);
            }
        }

    }

    private Future<Void> publishMqttMessage(MqttPersistentMessage message) {
        return tbClient.publish(message.getTopic(), Unpooled.wrappedBuffer(message.getPayload()), MqttQoS.AT_MOST_ONCE).addListener(
                future -> incomingQueue.put(new MessageFuturePair(future, message))
        );
    }

    private boolean checkClientConnected() {
        if (!tbClient.isConnected()) {
            try {
                clearOutgoingQueue();
                log.info("Priusisiot MQTT connection failed. Reconnecting in [{}] milliseconds", connection.getRetryInterval());
                Thread.sleep(connection.getRetryInterval());
                log.info("Attempting to reconnect to Priusisiot.");
                MqttConnectResult result = tbClient.reconnect().get(connection.getConnectionTimeout(), TimeUnit.MILLISECONDS);
                if (result.isSuccess()) {
                    tbClient.on(DEVICE_GET_ATTRIBUTES_RESPONSE_PLUS_TOPIC, mqttService).await(connection.getConnectionTimeout(), TimeUnit.MILLISECONDS);
                    tbClient.on(DEVICE_GET_ATTRIBUTES_RESPONSE_TOPIC, mqttService).await(connection.getConnectionTimeout(), TimeUnit.MILLISECONDS);
                    tbClient.on(DEVICE_RPC_TOPIC, mqttService).await(connection.getConnectionTimeout(), TimeUnit.MILLISECONDS);

                    log.info("Successfully reconnected to Priusisiot.");
                }
            } catch (TimeoutException e) {
                log.trace(e.getMessage(), e);
            } catch (Exception e) {
                log.error(e.getMessage(), e);
            }
            return false;
        }
        return true;
    }

    private void clearOutgoingQueue() {
        outgoingQueue.forEach(future -> {
            try {
                future.cancel(true);
            } catch (CancellationException e) {
                log.warn("Failed to cancel outgoing message on disconnected client. Reason: " + e.getMessage(), e);
            }
        });
        outgoingQueue.clear();
    }

    private List<MqttPersistentMessage> getMessages() {
        try {
            List<MqttPersistentMessage> resendMessages = persistentFileService.getResendMessages();
            if (!resendMessages.isEmpty()) {
                return resendMessages;
            } else {
                return persistentFileService.getPersistentMessages();
            }
        } catch (IOException e) {
            log.error(e.getMessage(), e);
            throw new RuntimeException(e);
        }
    }
}
