package me.egg82.antivpn.messaging;

import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import me.egg82.antivpn.services.MessagingHandler;
import me.egg82.antivpn.utils.ValidationUtil;
import ninja.egg82.analytics.utils.JSONUtil;
import org.json.simple.JSONObject;
import org.json.simple.parser.ParseException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.JedisPubSub;
import redis.clients.jedis.exceptions.JedisException;

public class Redis extends JedisPubSub implements Messaging {
    private final Logger logger = LoggerFactory.getLogger(getClass());

    private ExecutorService workPool = Executors.newFixedThreadPool(1, new ThreadFactoryBuilder().setNameFormat("AntiVPN-Redis-%d").build());

    private JedisPool pool;

    private String serverID;
    private UUID uuidServerID;
    private MessagingHandler handler;

    private Redis() { }

    private volatile boolean closed = false;

    public void close() {
        closed = true;
        workPool.shutdown();
        try {
            if (!workPool.awaitTermination(4L, TimeUnit.SECONDS)) {
                workPool.shutdownNow();
            }
        } catch (InterruptedException ignored) {
            Thread.currentThread().interrupt();
        }
        pool.close();
    }

    public boolean isClosed() { return closed || pool.isClosed(); }

    public static Redis.Builder builder(UUID serverID, MessagingHandler handler) { return new Redis.Builder(serverID, handler); }

    public static class Builder {
        private final Logger logger = LoggerFactory.getLogger(getClass());

        private final Redis result = new Redis();
        private final JedisPoolConfig config = new JedisPoolConfig();

        private String address = "127.0.0.1";
        private int port = 6379;
        private int timeout = 5000;
        private String pass = "";

        private Builder(UUID serverID, MessagingHandler handler) {
            if (serverID == null) {
                throw new IllegalArgumentException("serverID cannot be null.");
            }
            if (handler == null) {
                throw new IllegalArgumentException("handler cannot be null.");
            }

            result.uuidServerID = serverID;
            result.serverID = serverID.toString();
            result.handler = handler;
        }

        public Redis.Builder url(String address, int port) {
            this.address = address;
            this.port = port;
            return this;
        }

        public Redis.Builder credentials(String pass) {
            this.pass = pass;
            return this;
        }

        public Redis.Builder poolSize(int min, int max) {
            config.setMinIdle(min);
            config.setMaxTotal(max);
            return this;
        }

        public Redis.Builder life(long lifetime, int timeout) {
            config.setMinEvictableIdleTimeMillis(lifetime);
            config.setMaxWaitMillis(timeout);
            this.timeout = timeout;
            return this;
        }

        public Redis build() throws MessagingException {
            result.pool = new JedisPool(config, address, port, timeout, pass == null || pass.isEmpty() ? null : pass);
            // Warm up pool
            // https://partners-intl.aliyun.com/help/doc-detail/98726.htm
            warmup(result.pool);
            // Indefinite subscription
            subscribe();
            return result;
        }

        private void subscribe() {
            result.workPool.execute(() -> {
                while (!result.isClosed()) {
                    try (Jedis redis = result.pool.getResource()) {
                        redis.subscribe(result,
                                "antivpn-ip",
                                "antivpn-player",
                                "antivpn-post-vpn",
                                "antivpn-post-mcleaks"
                        );
                    } catch (JedisException ex) {
                        if (!result.isClosed()) {
                            logger.warn("Redis pub/sub disconnected. Reconnecting..");
                        }
                    }
                }
            });
        }

        private void warmup(JedisPool pool) throws MessagingException {
            Jedis[] warmpupArr = new Jedis[config.getMinIdle()];

            for (int i = 0; i < config.getMinIdle(); i++) {
                Jedis jedis;
                try {
                    jedis = pool.getResource();
                    warmpupArr[i] = jedis;
                    jedis.ping();
                } catch (JedisException ex) {
                    throw new MessagingException(false, "Could not warm up Redis connection.", ex);
                }
            }
            // Two loops because we need to ensure we don't pull a freshly-created resource from the pool
            for (int i = 0; i < config.getMinIdle(); i++) {
                Jedis jedis;
                try {
                    jedis = warmpupArr[i];
                    jedis.close();
                } catch (JedisException ex) {
                    throw new MessagingException(false, "Could not close warmed Redis connection.", ex);
                }
            }
        }
    }

    public void sendIP(UUID messageID, long longIPID, String ip) throws MessagingException {
        if (messageID == null) {
            throw new IllegalArgumentException("messageID cannot be null.");
        }
        if (ip == null) {
            throw new IllegalArgumentException("ip cannot be null.");
        }
        if (!ValidationUtil.isValidIp(ip)) {
            throw new IllegalArgumentException("ip is invalid.");
        }

        try (Jedis redis = pool.getResource()) {
            JSONObject obj = createJSON(messageID);
            obj.put("longID", longIPID);
            obj.put("ip", ip);
            redis.publish("antivpn-ip", obj.toJSONString());
        } catch (JedisException ex) {
            throw new MessagingException(isAutomaticallyRecoverable(ex), ex);
        }
    }

    public void sendPlayer(UUID messageID, long longPlayerID, UUID playerID) throws MessagingException {
        if (messageID == null) {
            throw new IllegalArgumentException("messageID cannot be null.");
        }
        if (playerID == null) {
            throw new IllegalArgumentException("playerID cannot be null.");
        }

        try (Jedis redis = pool.getResource()) {
            JSONObject obj = createJSON(messageID);
            obj.put("longID", longPlayerID);
            obj.put("id", playerID.toString());
            redis.publish("antivpn-player", obj.toJSONString());
        } catch (JedisException ex) {
            throw new MessagingException(isAutomaticallyRecoverable(ex), ex);
        }
    }

    public void sendPostVPN(UUID messageID, long id, long longIPID, String ip, Optional<Boolean> cascade, Optional<Double> consensus, long created) throws MessagingException {
        if (messageID == null) {
            throw new IllegalArgumentException("messageID cannot be null.");
        }
        if (ip == null) {
            throw new IllegalArgumentException("ip cannot be null.");
        }
        if (!ValidationUtil.isValidIp(ip)) {
            throw new IllegalArgumentException("ip is invalid.");
        }
        if (cascade == null) {
            throw new IllegalArgumentException("cascade cannot be null.");
        }
        if (consensus == null) {
            throw new IllegalArgumentException("consensus cannot be null.");
        }

        try (Jedis redis = pool.getResource()) {
            JSONObject obj = createJSON(messageID);
            obj.put("id", id);
            obj.put("longIPID", longIPID);
            obj.put("ip", ip);
            obj.put("cascade", cascade.orElse(null));
            obj.put("consensus", consensus.orElse(null));
            obj.put("created", created);
            redis.publish("antivpn-post-vpn", obj.toJSONString());
        } catch (JedisException ex) {
            throw new MessagingException(isAutomaticallyRecoverable(ex), ex);
        }
    }

    public void sendPostMCLeaks(UUID messageID, long id, long longPlayerID, UUID playerID, boolean value, long created) throws MessagingException {
        if (messageID == null) {
            throw new IllegalArgumentException("messageID cannot be null.");
        }
        if (playerID == null) {
            throw new IllegalArgumentException("playerID cannot be null.");
        }

        try (Jedis redis = pool.getResource()) {
            JSONObject obj = createJSON(messageID);
            obj.put("id", id);
            obj.put("longPlayerID", longPlayerID);
            obj.put("playerID", playerID.toString());
            obj.put("value", value);
            obj.put("created", created);
            redis.publish("antivpn-post-vpn", obj.toJSONString());
        } catch (JedisException ex) {
            throw new MessagingException(isAutomaticallyRecoverable(ex), ex);
        }
    }

    private JSONObject createJSON(UUID messageID) {
        JSONObject retVal = new JSONObject();
        retVal.put("sender", serverID);
        retVal.put("messageID", messageID.toString());
        return retVal;
    }

    private boolean isAutomaticallyRecoverable(JedisException ex) {
        if (
                ex.getMessage().startsWith("Failed connecting")
                || ex.getMessage().contains("broken connection")
        ) {
            return true;
        }
        return false;
    }

    public void onMessage(String channel, String message) {
        try {
            switch (channel) {
                case "antivpn-ip":
                    receiveIP(message);
                    break;
                case "antivpn-player":
                    receivePlayer(message);
                    break;
                case "antivpn-post-vpn":
                    receivePostVPN(message);
                    break;
                case "antivpn-post-mcleaks":
                    receivePostMCLeaks(message);
                    break;
                default:
                    logger.warn("Got data from channel that should not exist.");
                    break;
            }
        } catch (ParseException | ClassCastException ex) {
            logger.warn("Could not parse incoming data.", ex);
        }
    }

    private void receiveIP(String json) throws ParseException, ClassCastException {
        JSONObject obj = JSONUtil.parseObject(json);
        String sender = (String) obj.get("sender");
        if (!ValidationUtil.isValidUuid(sender)) {
            logger.warn("Non-valid sender received in IP: \"" + sender + "\".");
            return;
        }
        if (serverID.equals(sender)) {
            return;
        }

        String messageID = (String) obj.get("messageID");
        if (!ValidationUtil.isValidUuid(messageID)) {
            logger.warn("Non-valid message ID received in IP: \"" + messageID + "\".");
            return;
        }

        String ip = (String) obj.get("ip");
        if (!ValidationUtil.isValidIp(ip)) {
            logger.warn("Non-valid IP received in IP: \"" + ip + "\".");
            return;
        }

        handler.ipCallback(
                UUID.fromString(messageID),
                ip,
                ((Number) obj.get("longID")).longValue(),
                this
        );
    }

    private void receivePlayer(String json) throws ParseException, ClassCastException {
        JSONObject obj = JSONUtil.parseObject(json);
        String sender = (String) obj.get("sender");
        if (!ValidationUtil.isValidUuid(sender)) {
            logger.warn("Non-valid sender received in player: \"" + sender + "\".");
            return;
        }
        if (serverID.equals(sender)) {
            return;
        }

        String messageID = (String) obj.get("messageID");
        if (!ValidationUtil.isValidUuid(messageID)) {
            logger.warn("Non-valid message ID received in player: \"" + messageID + "\".");
            return;
        }

        String id = (String) obj.get("id");
        if (!ValidationUtil.isValidUuid(id)) {
            logger.warn("Non-valid UUID received in player: \"" + id + "\".");
            return;
        }

        handler.playerCallback(
                UUID.fromString(messageID),
                UUID.fromString(id),
                ((Number) obj.get("longID")).longValue(),
                this
        );
    }

    private void receivePostVPN(String json) throws ParseException, ClassCastException {
        JSONObject obj = JSONUtil.parseObject(json);
        String sender = (String) obj.get("sender");
        if (!ValidationUtil.isValidUuid(sender)) {
            logger.warn("Non-valid sender received in post VPN: \"" + sender + "\".");
            return;
        }
        if (serverID.equals(sender)) {
            return;
        }

        String messageID = (String) obj.get("messageID");
        if (!ValidationUtil.isValidUuid(messageID)) {
            logger.warn("Non-valid message ID received in post VPN: \"" + messageID + "\".");
            return;
        }

        String ip = (String) obj.get("ip");
        if (!ValidationUtil.isValidIp(ip)) {
            logger.warn("Non-valid IP received in post VPN: \"" + ip + "\".");
            return;
        }

        handler.postVPNCallback(
                UUID.fromString(messageID),
                ((Number) obj.get("id")).longValue(),
                ((Number) obj.get("longIPID")).longValue(),
                ip,
                obj.get("cascade") == null ? Optional.empty() : Optional.of((Boolean) obj.get("cascade")),
                obj.get("consensus") == null ? Optional.empty() : Optional.of(((Number) obj.get("consensus")).doubleValue()),
                ((Number) obj.get("created")).longValue(),
                this
        );
    }

    private void receivePostMCLeaks(String json) throws ParseException, ClassCastException {
        JSONObject obj = JSONUtil.parseObject(json);
        String sender = (String) obj.get("sender");
        if (!ValidationUtil.isValidUuid(sender)) {
            logger.warn("Non-valid sender received in post MCLeaks: \"" + sender + "\".");
            return;
        }
        if (serverID.equals(sender)) {
            return;
        }

        String messageID = (String) obj.get("messageID");
        if (!ValidationUtil.isValidUuid(messageID)) {
            logger.warn("Non-valid message ID received in post MCLeaks: \"" + messageID + "\".");
            return;
        }

        String playerID = (String) obj.get("playerID");
        if (!ValidationUtil.isValidUuid(playerID)) {
            logger.warn("Non-valid UUID received in post MCLeaks: \"" + playerID + "\".");
            return;
        }

        handler.postMCLeaksCallback(
                UUID.fromString(messageID),
                ((Number) obj.get("id")).longValue(),
                ((Number) obj.get("longPlayerID")).longValue(),
                UUID.fromString(playerID),
                (Boolean) obj.get("value"),
                ((Number) obj.get("created")).longValue(),
                this
        );
    }
}