/*
 * ROMAN DIGITAL - Autor: Américo Patetta
 */
package com.teltonikaServer.network;

import com.teltonikaServer.lib.Protocol;
import com.teltonikaServer.lib.Utils;
import com.teltonikaServer.persistence.Asset;
import com.teltonikaServer.persistence.Command;
import com.teltonikaServer.protocol.Codec8Extended_AvlData;
import com.teltonikaServer.protocol.IoElementsSection;
import com.teltonikaServer.protocol.GpsElementSection;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

public class ThreadHandler implements Runnable {

    // Identificación del thread
    private Socket socket = null;
    private String imei = null;
    private Asset asset;
    String sourceAddress;
    InputStream inputStream = null;
    OutputStream outputStream = null;
    long threadId;

    public ThreadHandler(Socket clientSocket, long threadId) {
        this.threadId = threadId;
        this.socket = clientSocket;
        try {
            this.socket.setSoTimeout(10000);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        sourceAddress = this.socket.getInetAddress().getHostAddress();

        TCPServer.serverLog.logEvent(Level.INFO,
                "Running thread #" + this.threadId + ", source Address: " + sourceAddress);

        boolean reading = openStreams();
        while (reading) {
            try {
                System.out.println(">>> New message (" + imei + ")" + threadId + "/" + TCPServer.getThreadCnt());
                reading = doRead(); // Acá se hace todo el trabajo
                Thread.sleep(TCPServer.PAUSE);
            } catch (Exception e) {
                reading = false;
                TCPServer.serverLog.logEvent(Level.SEVERE, "ERROR NW 202. doRead error" + e.getMessage());
                e.printStackTrace();
            }
        }
        TCPServer.serverLog.logEvent(Level.INFO, "Exiting thread #" + this.threadId);
        TCPServer.decThreadCnt();
    }

    private boolean openStreams() {

        boolean allOpen = true;
        try {
            outputStream = this.socket.getOutputStream();
        } catch (IOException socketInstantiationError) {
            allOpen = false;
            TCPServer.serverLog.logEvent(Level.SEVERE,
                    "ERROR NW 200. No es posible abrir outputstream (" + socketInstantiationError.getMessage() + ")");
        }

        try {
            inputStream = socket.getInputStream();
        } catch (IOException socketInstantiationError) {
            inputStream = null;
            allOpen = false;
            TCPServer.serverLog.logEvent(Level.SEVERE,
                    "ERROR NW 201. No es posible abrir inputstream (" + socketInstantiationError.getMessage() + ")");
        }
        return allOpen;
    }

    /*
     * private void displayReading(byte socketReadBuffer[]) {
     * //System.out.println("\nThreadId: " + this.threadId + ", RED (String): " +
     * new String(socketReadBuffer));
     * System.out.println("\nHEX: " + Utils.getHexString(socketReadBuffer));
     * System.out.println("\nIMEI: " + ((this.imei == null) ? "?" : this.imei));
     * System.out.println("Total bytes red: " + socketReadBuffer.length);
     * Utils.debugByteArray(socketReadBuffer);
     * }
     */
    private void getAssetConfig() {

    }

    private boolean processLoginMessage(long logId, byte[] socketReadBuffer) {
        try {
            this.imei = new String(Utils.getSubArray(socketReadBuffer, 2));

            if (this.imei != null && this.imei.matches("-?\\d+(\\.\\d+)?")) {
                System.out.println("IMEI id: " + this.imei);
                this.asset = TCPServer.persistor.getAssetFromImei(this.imei);
                getAssetConfig();
                TCPServer.serverLog.logEvent(Level.INFO, "Nuevo mensaje de login (Imei: " + this.imei + ", asset ID:" + this.asset.getAssetId() + ")");
            } else {
                this.asset.setAssetId(-1);
            }
        } catch (Exception ex) {
            ex.printStackTrace();
            TCPServer.serverLog.logEvent(Level.SEVERE, "Error al interpretar mensaje de login, logId: " + logId);
        }
        if (!(this.asset.getAssetId() == -1)) {
            sendAvlAck(outputStream, new byte[]{0x01});
            sendConfigRequest(outputStream);
            return true;
        } else {
            return false;
        }
    }

    private boolean doRead() {

        boolean reading = false;
        byte socketReadBuffer[] = new byte[TCPServer.BUFFER_SIZE];
        long currentTimestamp = System.currentTimeMillis() / 1000;

        try {
            // System.out.print("-");
            int redBytes = inputStream.read(socketReadBuffer);
            // System.out.println("+++RED BYTES: " + redBytes);
            if (redBytes == -1) {
                // TCPServer.errorLog.logEvent(Level.INFO,"Conexión cerrada Thread/imei: " +
                // this.threadId + "/" + imei);
                // reading = false;
            } else {
                socketReadBuffer = Utils.getSubArray(socketReadBuffer, 0, redBytes);
                // displayReading(socketReadBuffer);
                long logId = TCPServer.persistor.persistReceivedMessage(socketReadBuffer, this.sourceAddress,
                        TCPServer.getOrigin(), TCPServer.SIGNATURE, this.imei);
                if (logId > 0) {
                    if (isLoginMessage(socketReadBuffer)) {
                        processLoginMessage(logId, socketReadBuffer);
                    } else {
                        try {
                            int codecId = getCodecId(socketReadBuffer);
                            switch (codecId) {

                                case 0x8e -> {
                                    try {
                                        int records = parseCodec8Extended(socketReadBuffer, logId);
                                        if (records >= 0) {
                                            sendAvlAck(outputStream, new byte[]{0x00, 0x00, 0x00, (byte) records});
                                        } else {
                                            TCPServer.serverLog.logEvent(Level.SEVERE,
                                                    "001: Error de parsing (CODEC 8 extended), logId: " + logId
                                                    + " Records parseados no coinciden con el valor declarado.");
                                        }
                                    } catch (Exception ex) {
                                        TCPServer.serverLog.logEvent(Level.SEVERE,
                                                "Error de parsing (CODEC 8 extended), logId: " + logId);
                                    }
                                }
                                case 0x0C -> {
                                    // TCPServer.appLog.logEvent(Level.INFO, "Nuevo mensaje Codec 12 (" + this.imei
                                    // + ")");
                                    try {
                                        parseCodec12(socketReadBuffer, logId);
                                    } catch (Exception ex) {
                                        TCPServer.serverLog.logEvent(Level.SEVERE,
                                                "Error de parsing (CODEC 8 extended), logId: " + logId);
                                    }
                                }
                            }
                        } catch (Exception ex) {
                            TCPServer.serverLog.logEvent(Level.SEVERE, "Error de parsing general, logId: " + logId);
                        }
                    }
                    registerLastMessageTs(currentTimestamp);
                    String scr = sendPendingCommands(outputStream, TCPServer.persistor.getPendingCommands(this.imei));

                    TCPServer.serverLog.logEvent(Level.INFO, scr);

                    reading = true;
                } else {
                    TCPServer.serverLog.logEvent(Level.SEVERE, "ERROR DE CONEXIÓN A BASE DE DATOS!" + logId);
                    reading = false;
                }
            }
        } catch (SocketTimeoutException socketTimeOut) {
            System.out.println("Bye! (Socket timeout)");
            reading = false;
        } catch (IOException ex) {
            // El cliente corta la conexión
            // System.out.println("El cliente corta la conexión");

            TCPServer.serverLog.logEvent(Level.INFO,
                    "Client ends connection " + this.threadId + "/" + imei);

            reading = false;
        } catch (Exception exg) {
            exg.printStackTrace();
            TCPServer.serverLog.logEvent(Level.SEVERE, "Unhandled error " + this.threadId + "/" + imei);
            reading = false;
        }
        return reading;
    }

    private void registerLastMessageTs(long currentTs) {
        TCPServer.persistor.persistMessageTs(this.imei, currentTs);
    }

    private void sendAvlAck(OutputStream outputStream, byte ackMessage[]) {
        try {
            // byte ack[] = {0x00, 0x00, 0x00, (this.asset.getAssetId() > 0) ? (byte) 0x01 :
            // (byte) 0x00};
            outputStream.write(ackMessage);
            outputStream.flush();
        } catch (IOException io) {
            TCPServer.serverLog.logEvent(Level.SEVERE, "Error al enviar ACK a login");
        }
    }

    private long setIgnitionCount(long ignCount) {

        long ignOffset;
        if (ignCount > 0) {
            if (this.asset.getIgnitionCounter() > ignCount) {
                ignOffset = asset.fitIgnCounterOffsetTo(ignCount);
            } else {
                ignOffset = asset.getIgnitionCounterOffset();
            }
            asset.setLastIgnitionCounter(ignCount);
        } else {
            ignOffset = -1;
        }
        return ignOffset;

    }

    private int parseCodec8Extended(byte[] aByteArray, long logId) {

        int numberOfData = -1;
        if (preambleOk(aByteArray)) {
            long dataFieldLength = getDataFieldLength(aByteArray);
            if (dataFieldLength > 0) {
                numberOfData = Utils.getUnsignedbyte(aByteArray[9]);
                aByteArray = Utils.getSubArray(aByteArray, 10);
                for (int i = 0; i < numberOfData; i++) {
                    try {
                        Codec8Extended_AvlData avlData;
                        avlData = new Codec8Extended_AvlData(aByteArray, logId);
                        int messageType = avlData.parse(this.asset);
                        GpsElementSection gpsElement = avlData.getGpsElement();
                        IoElementsSection ioElementSection = avlData.getIoElement();
                        long ignOffset = setIgnitionCount(ioElementSection.getIgnitionCounter());
                        long logGpsDataId = TCPServer.persistor.persistGPS_Data(gpsElement, asset, logId,
                                ioElementSection, ignOffset, messageType);
                        ioElementSection.persist(logGpsDataId, gpsElement, asset);
                        this.asset.setGpsElement(avlData.getGpsElement());
                        this.asset.setIoElement(avlData.getIoElement());
                        this.asset.setLastMessageType(messageType);
                        this.asset.persist();
                        asset.updateIgnitionCounterOffset();
                        aByteArray = Utils.getSubArray(aByteArray, avlData.getOffset());

                        TCPServer.serverLog.logEvent(Level.FINE, "Parsed:\n" + avlData.toString());

                    } catch (Exception e) {
                        TCPServer.serverLog.logEvent(Level.SEVERE,
                                "Error al parsear Codec 8 extended AVL/IO, logId: " + logId + e.getMessage());
                    }
                }
            }
        }
        return numberOfData;
    }

    private boolean parseCodec12(byte aByteArray[], long logId) {
        boolean parsed = true;
        if (preambleOk(aByteArray)) {

            try {
                // int commandQuantity = Utils.getUnsignedbyte(aByteArray[9]);
                // int commandType = Utils.getUnsignedbyte(aByteArray[10]);
                long commandSize = getCodecWord(Utils.getSubArray(aByteArray, 11, 4));

                String command = new String(Utils.getSubArray(aByteArray, 15, (int) commandSize));
                System.out.println("Command: >" + command + "<");
                if (command.startsWith("Param ID:")) {
                    command = Protocol.cleanParameterResponse(command);
                    System.out.println("CLEAN COMMAND: >" + command);
                    List<Map.Entry<Integer, Integer>> parsedParams = Protocol.parseParams(command);
                    for (Map.Entry<Integer, Integer> entry : parsedParams) {
                        System.out.println("Parámetro: " + entry.getKey() + ", Valor: " + entry.getValue());
                    }

                }

            } catch (Exception anyException) {
                TCPServer.serverLog.logEvent(Level.SEVERE, "Cocec12 parsing error");
                parsed = false;
            }
        }
        return parsed;
    }

    private int getCodecId(byte aByteArray[]) {
        return Utils.getUnsignedbyte(aByteArray[8]);
    }

    private boolean isLoginMessage(byte aByteArray[]) {
        try {
            int imeiLength = Utils.getUnsignedbyte(aByteArray[0]) * 255 + Utils.getUnsignedbyte(aByteArray[1]);
            return aByteArray.length == imeiLength + 2;
        } catch (ArrayIndexOutOfBoundsException ex) {
            return false;
        }
    }

    private boolean preambleOk(byte aByteArray[]) {
        return (aByteArray[0] == 0x00 && aByteArray[1] == 0x00 && aByteArray[2] == 0x00 && aByteArray[3] == 0x00);
    }

    private long getDataFieldLength(byte aByteArray[]) {
        long dataFieldLength;
        try {
            dataFieldLength = Utils.getLongFromMessageArray(aByteArray, 4, 4);
        } catch (NumberFormatException numberFormatException) {
            dataFieldLength = -1;
        }
        return dataFieldLength;
    }

    private long getCodecWord(byte fourBytes[]) {
        long dataFieldLength;
        try {
            dataFieldLength = Utils.getLongFromMessageArray(fourBytes, 0, 4);
        } catch (NumberFormatException numberFormatException) {
            dataFieldLength = -1;
        }
        return dataFieldLength;
    }

    private String sendPendingCommands(OutputStream outputStream, ArrayList<Command> pendingCommands) {
        int sentCnt = 0;
        for (Command command : pendingCommands) {
            try {
                System.out.print("Sending command (ID: " + command.getCommandId() + "): " + command.getCommandBody());
                outputStream.write(Protocol.getCommand(command.getCommandBody()));
                outputStream.flush();
                if (command.getCommandId() > 0) {
                    try {
                        TCPServer.persistor.markCommandSending(command);
                    } catch (Exception e) {

                    }
                }
                System.out.println("Sent!");
                sentCnt++;
            } catch (IOException ex) {
                Logger.getLogger(ThreadHandler.class.getName()).log(Level.SEVERE, null, ex);
                sentCnt = -1;
                return "Coldn't send " + pendingCommands.size() + " commands. Sending fail";

            }
        }
        return "Sent " + sentCnt + "/" + pendingCommands.size() + " commands!";
    }

    public void sendConfigRequest(OutputStream outputStream) {

        ArrayList<Command> pendingCommands;
        pendingCommands = new ArrayList();

        Command command;

        command = new Command();
        command.setCommandBody("getparam " + Protocol.OVERSPEEDING + "; " + Protocol.SPEED_LIMIT + "; " + Protocol.SPEED_ALERT + ";" + Protocol.INHIBITOR_ENABLED + "; " + Protocol.INHIBITOR_OUTPUT);
        command.setCommandId(0);
        pendingCommands.add(command);

        sendPendingCommands(outputStream, pendingCommands);
    }

}
