package jnet.server;

import io.netty.channel.Channel;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.util.concurrent.GlobalEventExecutor;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.net.InetSocketAddress;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.function.Consumer;
import jnet.lib.LogFile;
import jnet.lib.Message;
import jnet.lib.PasswordHashing;
import jnet.lib.object.Event;
import jnet.lib.object.Map;
import jnet.lib.object.MapObject;
import jnet.lib.object.ObjectType;
import jnet.lib.object.OnlineClients;
import jnet.lib.object.ServerConfig;
import jnet.lib.object.SnmpProfile;
import jnet.lib.object.User;
import jnet.lib.snmp.DeviceInfo;
import jnet.lib.snmp.Interface;
import jnet.lib.snmp.SNMPDeviceInfo;
import jnet.lib.snmp.SNMPInterfaceScanner;

public class ServerMessageParser {

    private final Channel currentClient;
    private static final ChannelGroup channels = new DefaultChannelGroup(
            "containers", GlobalEventExecutor.INSTANCE);

    private static final java.util.Map<Integer, Consumer<Message>> handlers = new HashMap<>();

    public ServerMessageParser(Channel currentClient) {
        this.currentClient = currentClient;

        // Inicializace mapy typu zprávy na odpovídající metody
        handlers.put(Message.CLIENT_VERSION, this::handleClientVersion);
        handlers.put(Message.AUTH, this::handleAuth);
        handlers.put(Message.RESTART, this::handleRestart);
        handlers.put(Message.UPTIME, this::handleUptime);
        handlers.put(Message.NEW_OBJECT_TYPE, this::handleNewObjectType);
        handlers.put(Message.SET_MAP_LOCK, this::handleSetMapLock);
        handlers.put(Message.ADD_OBJECT, this::handleAddObject);
        handlers.put(Message.REMOVE_OBJECT, this::handleRemoveObject);
        handlers.put(Message.MAP_REMOVE, this::handleMapRemove);
        handlers.put(Message.MAP_ADD, this::handleMapAdd);
        handlers.put(Message.OBJECT_MOVE, this::handleObjectMove);
        handlers.put(Message.UPDATE_OBJECT, this::handleUpdateObject);
        handlers.put(Message.DELETE_LOG, this::handleDeleteLog);
        handlers.put(Message.USER, this::handleUser);
        handlers.put(Message.USER_EDIT, this::handleUserEdit);
        handlers.put(Message.SERVER_CONFIG, this::handleServerConfig);
        handlers.put(Message.SNMP_OBJECT_INFO, this::handleSnmpObjectInfo);
        handlers.put(Message.SNMP_OBJECT_INTERFACE, this::handleSnmpObjectInterface);
    }

    public void parse(Message msg) {
        LogFile.printDebug("-> Message type: " + msg.getType());

        Consumer<Message> handler = handlers.get(msg.getType());
        if (handler != null) {
            handler.accept(msg);
        } else {
            LogFile.printErr("Neznamy typ zpravy");
        }
    }

    private void handleClientVersion(Message msg) {
        int client_version = (int) msg.getMsg();
        if (client_version < Server.MINIMAL_CLIENT_VERSION) {
            LogFile.printInfo("Outdated client. Sent update request (" + ((InetSocketAddress) currentClient.remoteAddress()).getHostName() + ")");
            send(new Message(Message.UPDATE, null));
        } else {
            LogFile.printInfo("Authorization request send (" + ((InetSocketAddress) currentClient.remoteAddress()).getHostName() + ")");
            // oodeslat pozadavek na autorizacni udaje
            send(new Message(Message.AUTH_REQUEST, null));
        }
    }

    private void handleAuth(Message msg) {
        String[] userData = (String[]) msg.getMsg();
        String username = userData[0];
        String password = userData[1];
        int platform = Integer.parseInt(userData[2]);

        User u = Database.getUserInfo(username);

        if (PasswordHashing.verify(password, u.getPassword())) {
            LogFile.printInfo("User: " + username + " logged in (" + ((InetSocketAddress) currentClient.remoteAddress()).getAddress().getHostName() + ")");
            // odeslat informaci uzivateli o jeho uspesnem prihlaseni
            send(new Message(Message.AUTH_SUCEFULL, null));
            // pridani uzivatele mezi pripojene kanaly 
            channels.add(currentClient);
            // vložit uživatele na seznam online uživatelů
            InetSocketAddress socketAddress = (InetSocketAddress) currentClient.remoteAddress();
            String clientIp = socketAddress.getAddress().getHostAddress();
            String clientPort = String.valueOf(socketAddress.getPort());
            Server.onlineClients.add(new OnlineClients(
                    String.valueOf(u.getId()),
                    u.getUsername(),
                    clientIp,
                    clientPort,
                    platform));
            // odeslat informace o prihlasenem uzivateli, opravneni atd 
            send(new Message(Message.USER_INFO, u));
            // odeslat vsem nový seznam pripojenych klientu
            sendAll(new Message(Message.ONLINE_CLIENTS, Server.onlineClients));
            // odeslat nastaveni serveru 
            ServerConfig sc = new ServerConfig(
                    Server.config.getInt("ping_repeat"),
                    Server.config.getInt("ping_attempt"),
                    Server.config.getInt("ping_timeout"),
                    Server.config.getInt("instability_attempt"),
                    Server.config.getInt("instability_limit"));
            send(new Message(Message.SERVER_CONFIG, sc));
            // odeslat typy objektu
            for (ObjectType ot : Server.objectType) {
                send(new Message(Message.OBJECT_TYPE, ot));
            }
            // odesila seznam uzivatelu
            for (User us : Server.users) {
                send(new Message(Message.USER, us));
            }
            // odeslat seznam map
            for (Map i : Server.maps) {
                send(new Message(Message.MAP, i));
            }
            // odeslat seznam udalosti
            send(new Message(Message.EVENT_LIST, Server.events));
            // odeslat seznam snmp profilů
            for (SnmpProfile i : Server.snmpProfile) {
                send(new Message(Message.SNMP_PROFILE, i));
            }

        } else {
            LogFile.printInfo("Authorization failed (ser: " + username + " )");
        }
    }

    private void handleRestart(Message msg) {
        LogFile.printInfo("Prijat pozadavek na restart serveru");
        Server.restartServer();
    }

    private void handleUptime(Message msg) {
        long uptime = ManagementFactory.getRuntimeMXBean().getUptime();
        send(Message.UPTIME, uptime);
    }

    private void handleNewObjectType(Message msg) {
        ObjectType not = (ObjectType) msg.getMsg();
        int id = Database.addObjectType(not);
        not.setId(id);
        Server.objectType.add(not);
        sendAll(new Message(Message.OBJECT_TYPE, not));
    }

    private void handleSetMapLock(Message msg) {
        int[] data_lock = (int[]) msg.getMsg();
        for (Map map : Server.maps) {
            if (map.getId() == data_lock[0]) {
                map.setLock((data_lock[1] == 1));
                break;
            }
        }
        // upravit informaci  v databazi
        Database.mapLock(data_lock[1], data_lock[0]);
        // odeslani informace ostatnim 
        sendAll(new Message(Message.SET_MAP_LOCK, data_lock));
    }

    private void handleAddObject(Message msg) {
        MapObject mo = Database.addMapObject((MapObject) msg.getMsg());
        // pridat do seznamu
        for (Map map : Server.maps) {
            if (map.getId() == mo.getMap()) {
                map.getObjects().add(mo);
            }
        }
        // odesle všem novy objekt
        sendAll(new Message(Message.ADD_OBJECT, mo));
        // prida event a odesle ho
        Event event = Database.addEvent(mo.getId(), "Přidáno", Event.TYPE_INFO);
        sendAll(new Message(Message.EVENT_NEW, event));
        // pokud je object vypnuty prida log 
        if (!mo.isActive()) {
            Event event_disable = Database.addEvent(mo.getId(), "Ping vypnut", Event.TYPE_PING_OFF);
            sendAll(new Message(Message.EVENT_NEW, event_disable));
        }
    }

    private void handleRemoveObject(Message msg) {
        for (Map map : Server.maps) {
            for (MapObject object : map.getObjects()) {
                if (object.getId() == (int) msg.getMsg()) {
                    // odebere z databaze
                    Database.removeObject(object.getId());
                    // odesle informaci o odebrani objektu klientum
                    sendAll(new Message(Message.REMOVE_OBJECT, object.getId()));
                    // odebere ze seznamu
                    map.getObjects().remove(object);
                    break;
                }
            }
        }
    }

    private void handleMapRemove(Message msg) {
        for (Map map : Server.maps) {
            if (map.getId() == (int) msg.getMsg()) {
                // odebere z databaze
                Database.removeMap(map.getId());
                // odesle informaci o odebrani objektu klientum
                sendAll(new Message(Message.MAP_REMOVE, map.getId()));
                // odebere ze seznamu
                Server.maps.remove(map);
                break;
            }
        }
    }

    private void handleMapAdd(Message msg) {
        Map map = new Map((String) msg.getMsg());
        // přidání mapy do databaze
        map.setId(Database.addMap(map.getName()));
        //odeslání mapy klientum
        send(new Message(Message.MAP, map));
        // pridani mapy do seznamu
        Server.maps.add(map);
    }

    private void handleObjectMove(Message msg) {
        int[] data = (int[]) msg.getMsg();
        // 0 = obj id
        // 2 = x
        // 2 = y
        // zmenit v databázi
        Database.objectMove(data[0], data[1], data[2]);
        // zmenit v seznamu
        for (Map mapa : Server.maps) {
            for (MapObject object : mapa.getObjects()) {
                if (object.getId() == data[0]) {
                    // zmenit pozici 
                    object.setX(data[1]);
                    object.setY(data[2]);
                    // zapsat zmenu do databaze
                    Database.objectMove(data[0], data[1], data[2]);
                    // preposlat zmenu klientum 
                    sendAll(new Message(Message.UPDATE_OBJECT, object));
                    break;
                }
            }
        }
    }

    private void handleUpdateObject(Message msg) {
        MapObject mo_u = (MapObject) msg.getMsg();
        // zmenit v databazi
        Database.updateObject(mo_u);
        // zmenit v seznamu
        for (Map m : Server.maps) {
            for (int i = 0; i < m.getObjects().size(); i++) {
                if (m.getObjects().get(i).getId() == mo_u.getId()) {
                    // pokud je object vypnuty a dřív nebyl prida log 
                    if (!mo_u.isActive() && m.getObjects().get(i).isActive()) {
                        Event event_disable = Database.addEvent(mo_u.getId(), "Ping vypnut", Event.TYPE_PING_OFF);
                        sendAll(new Message(Message.EVENT_NEW, event_disable));
                    }
                    // pokud je object zapnutý a dřív nebyl prida log 
                    if (mo_u.isActive() && !m.getObjects().get(i).isActive()) {
                        Event event_disable = Database.addEvent(mo_u.getId(), "Ping zapnut", Event.TYPE_PING_OFF);
                        sendAll(new Message(Message.EVENT_NEW, event_disable));
                    }
                    // zmeni objekt v seznamu
                    m.getObjects().set(i, mo_u);
                    break;
                }

            }
        }

        // preposlat zmenu klientum 
        sendAll(new Message(Message.UPDATE_OBJECT, mo_u));
    }

    private void handleDeleteLog(Message msg) {
        int objId = (int) msg.getMsg();
        // vymaže logy v databazi
        Database.deleteObjectLog(objId);
        // vymaze logy v listu
        ListIterator<Event> iterator = Server.events.listIterator();
        while (iterator.hasNext()) {
            if (iterator.next().getObject() == objId) {
                iterator.remove();
                break;
            }
        }
        sendAll(new Message(Message.DELETE_LOG, objId));
    }

    private void handleUser(Message msg) {
        User user = (User) msg.getMsg();
        user.setPassword(PasswordHashing.hash(user.getPassword()));
        //pridáni do databáze a nastaveni id
        user.setId(Database.addUser(user));
        // přidání do seznamu
        Server.users.add(user);
        // odeslat klientum nového uživatele
        send(new Message(Message.USER, user));
    }

    private void handleUserEdit(Message msg) {
        User ue = (User) msg.getMsg();
        // upraveni v databázi
        Database.editUser(ue);
        // zmenit v seznamu
        ListIterator<User> iteratorUser = Server.users.listIterator();
        while (iteratorUser.hasNext()) {
            User next = iteratorUser.next();
            if (next.getId() == ue.getId()) {
                iteratorUser.set(ue);
                break;
            }

        }
        // preposlat zmenu klientum 
        sendAll(new Message(Message.USER_EDIT, ue));
    }

    private void handleServerConfig(Message msg) {
        ServerConfig sc = (ServerConfig) msg.getMsg();
        // ulozeni
        Server.config.setInt("ping_repeat", sc.getPingRepeat());
        Server.config.setInt("ping_attempt", sc.getPingAttempt());
        Server.config.setInt("ping_timeout", sc.getPingTimeout());
        Server.config.setInt("instability_attempt", sc.getInstabilityAttempt());
        Server.config.setInt("instability_limit", sc.getInstabilityLimit());
        Server.config.save();
        //odeslani vsem
        sendAll(Message.SERVER_CONFIG, sc);
    }

    private void handleSnmpObjectInfo(Message msg) {
        Object[] msg_object = (Object[]) msg.getMsg();
        String frameName = (String) msg_object[0];
        MapObject obj = (MapObject) msg_object[1];
        Thread t1 = new Thread() {
            @Override
            public void run() {
                try {
                    for (SnmpProfile snmpProfile : Server.snmpProfile) {
                        if (snmpProfile.getId() == obj.getSnmpProfile()) {
                            SNMPDeviceInfo snmp = new SNMPDeviceInfo(obj.getIp(), snmpProfile.getPort(), snmpProfile.getCommunityRead(), snmpProfile.getVersion());
                            DeviceInfo di = snmp.getDeviceInfo();
                            Object[] o = {frameName, di};
                            send(Message.SNMP_OBJECT_INFO, o);
                            break;
                        }
                    }

                } catch (IOException e) {
                    LogFile.printErr("SNMP error: " + e.getMessage());
                }
            }
        };
        t1.start();
    }

    private void handleSnmpObjectInterface(Message msg) {
        Object[] msg_object = (Object[]) msg.getMsg();
        String frameName = (String) msg_object[0];
        MapObject obj = (MapObject) msg_object[1];
        Thread t1 = new Thread() {
            @Override
            public void run() {
                try {
                    for (SnmpProfile snmpProfile : Server.snmpProfile) {
                        if (snmpProfile.getId() == obj.getSnmpProfile()) {
                            SNMPInterfaceScanner snmp = new SNMPInterfaceScanner(obj.getIp(), snmpProfile.getPort(), snmpProfile.getCommunityRead(), snmpProfile.getVersion());
                            List<Interface> list = snmp.scanInterfaces();
                            Object[] o = {frameName, list};
                            send(Message.SNMP_OBJECT_INTERFACE, o);
                            break;
                        }
                    }
                } catch (Exception ex) {
                    LogFile.printErr("SNMP error: " + ex.getMessage());
                }
            }
        };
        t1.start();
    }

    /**
     *
     *
     * @param message
     */
    public void send(Message message) {
        currentClient.writeAndFlush(message);
        LogFile.printDebug("<- Message type: " + message.getType());
    }

    public void send(int type, Object obj) {
        currentClient.writeAndFlush(new Message(type, obj));
        LogFile.printDebug("<- Message type: " + type);
    }

    public static void sendAll(Message message) {
        if (channels.isEmpty()) {
            return;
        }
        channels.writeAndFlush(message);
        LogFile.printDebug("<- For all. Message type: " + message.getType());
    }

    public static void sendAll(int type, Object obj) {
        if (channels.isEmpty()) {
            return;
        }
        channels.writeAndFlush(new Message(type, obj));
        LogFile.printDebug("<- For all. Message type: " + type);
    }

    public static final void removeClient(Channel ch) {
        // odebrani z připojenych kanalu
        channels.remove(ch);
        // odebrani ze seznamu klientu
        InetSocketAddress socketAddress = (InetSocketAddress) ch.remoteAddress();
        String clientIp = socketAddress.getAddress().getHostAddress();
        String clientPort = String.valueOf(socketAddress.getPort());
        for (OnlineClients oc : Server.onlineClients) {
            if (oc.getIp().equals(clientIp) && oc.getPort().equals(clientPort)) {
                Server.onlineClients.remove(oc);
                System.err.println("Client removed");
                break;
            }
        }
        // odeslat vsem novy seznam klientu
        sendAll(new Message(Message.ONLINE_CLIENTS, Server.onlineClients));

    }

}
