package Web;

import Gson.DateTimeHelper;
import Gson.GsonFormatter;
import com.corundumstudio.socketio.Configuration;
import com.corundumstudio.socketio.SocketIOServer;
import com.corundumstudio.socketio.protocol.JacksonJsonSupport;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.google.gson.JsonObject;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;



public class SocketIO {
    private final Configuration config;
    private SocketIOServer server;

    /**
     * Create Socket.io Server
     * @param host listening host, default to localhost
     * @param port listening port, default to 9092
     */
    @SuppressWarnings("unused")
    public SocketIO(String host, int port){
        if (port<1 || port>65535) port = 9092;
        config = new Configuration();
        config.setHostname(host);
        config.setPort(port);

        ObjectMapper objectMapper = new ObjectMapper();
        JavaTimeModule javaTimeModule = new JavaTimeModule();

        // Register LocalDateTime serializer and deserializer
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(formatter));
        javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(formatter));

        objectMapper.registerModule(javaTimeModule);
        config.setJsonSupport(new JacksonJsonSupport(javaTimeModule));
    }

    /**
     * Start the server
     */
    @SuppressWarnings("unused")
    public void Start(final SocketIOEvents event){
        server = new SocketIOServer(config);
        server.start();
        raise_log(String.format("SocketIO server started on host %s port %d", config.getHostname(), config.getPort()));

        server.addConnectListener(client -> {
            String remoteAddress = client.getRemoteAddress().toString();
            String sessionId = client.getSessionId().toString();
            if (event!=null) event.Connected(remoteAddress, sessionId);
        });

        server.addDisconnectListener(client -> {
            String remoteAddress = client.getRemoteAddress().toString();
            String sessionId = client.getSessionId().toString();
            if (event!=null) event.Disconnected(remoteAddress, sessionId);
        });

        server.addEventListener("login",LoginStructure.class, (client, data, ackSender)->{
            String remoteAddress = client.getRemoteAddress().toString();
            String sessionId = client.getSessionId().toString();
            raise_log(String.format("Client %s with id=%s request to login", remoteAddress, sessionId));

            ReplyLoginStructure reply = new ReplyLoginStructure();
            reply.command = "login";
            reply.value = data;
            reply.result = "invalid data";      // default reply sebelum process
            if (data instanceof LoginStructure login){
                if (login.username!=null && login.password!=null){
                    if (!login.username.isEmpty() && !login.password.isEmpty()){
                        if (event!=null){
                            reply.result = event.Login(remoteAddress, sessionId, login.username, login.password);
                        } else reply.result = "no event handler";
                    }
                }
            }
            ackSender.sendAckData(reply);
        });

        /*
            Get table names event
            data: null
            <p/>
            return: {
                command: "gettablenames",
                result: "success" or "no data",
                data: JsonArray of table names
                }
         */
        server.addEventListener("gettablenames", String.class, (client, data, ackSender) -> {
            String remoteAddress = client.getRemoteAddress().toString();
            String sessionId = client.getSessionId().toString();
            raise_log(String.format("Client %s with id=%s request to get tables", remoteAddress, sessionId));
            ReplyArrayStringStructure reply = new ReplyArrayStringStructure();
            reply.command = "gettablenames";

            if (event!=null){
                reply.data = event.GetTables(remoteAddress, sessionId);
                reply.result = reply.data!=null?"success":"no data";
            } else reply.result = "no event handler";

            ackSender.sendAckData(reply);
        });

        /*
          Create table event
          data: "table name"
          <p/>
          return: {
            command: "createtable",
            value: "table name",
            result: "success" or "failed"
            }
         */
        server.addEventListener("createtable",String.class, (client, data, ackSender) -> {
            String remoteAddress = client.getRemoteAddress().toString();
            String sessionId = client.getSessionId().toString();
            // data is tablename in format yyyy-MM-dd
            // but SQL table name cannot contain -
            // so tablename will be converted to yyyyMMdd
            ReplyStringStructure reply = new ReplyStringStructure();
            reply.command = "createtable";
            reply.value = data;
            reply.result = "invalid data";      // default reply sebelum process
            if (data instanceof String tt){
                if (!tt.isEmpty()){
                    if (DateTimeHelper.isValidDate_yyyyMMdd(tt)){
                        String tablename = DateTimeHelper.ConvertToTableName(tt);
                        raise_log(String.format("Client %s with id=%s request to create table %s", remoteAddress, sessionId, tablename));
                        if (event!=null){
                            reply.result = event.CreateTable(remoteAddress, sessionId, tablename);
                        } else reply.result = "no event handler";
                    }
                }
            }

            ackSender.sendAckData(reply);
        });

        /*
          Drop table event
          data: "table name"
          return: {
           command: "droptable",
           value: "table name",
           result: "success" or "failed"
           }
         */
        server.addEventListener("droptable",String.class, (client, data, ackSender) -> {
            String remoteAddress = client.getRemoteAddress().toString();
            String sessionId = client.getSessionId().toString();
            ReplyStringStructure reply = new ReplyStringStructure();
            reply.command = "droptable";
            reply.value = data;
            reply.result = "invalid data";      // default reply sebelum process
            if (data instanceof String tt){
                if (!tt.isEmpty()){
                    if (DateTimeHelper.isValidDate_yyyyMMdd(tt)){
                        String tablename = DateTimeHelper.ConvertToTableName(tt);
                        // data is tablename in format yyyy-MM-dd
                        // but SQL table name cannot contain -
                        // so tablename will be converted to yyyyMMdd
                        raise_log(String.format("Client %s with id=%s request to drop table %s", remoteAddress, sessionId, tablename));
                        if (event!=null){
                            reply.result = event.DropTable(remoteAddress, sessionId, tablename);
                        } else reply.result = "no event handler";
                    }
                }
            }
            ackSender.sendAckData(reply);
        });

        /*
          Insert row event
          data: {
             tablename: "table name",
             fisdata: FisData in JsonObject, or JsonArray of FisData in JsonObject
             }
          <p/>
          return: {
            command: "insertrow",
            value: FisData in JsonObject,
            result: "success" or "failed"
            }
         */
        server.addEventListener("insertrow", InsertDataStructure.class, (client, data, ackSender) -> {
            String remoteAddress = client.getRemoteAddress().toString();
            String sessionId = client.getSessionId().toString();
            raise_log(String.format("Client %s with id=%s request to insert table %s", remoteAddress, sessionId, data));
            ReplyInsertDataStructure reply = new ReplyInsertDataStructure();
            reply.command = "insertrow";
            reply.value = data;
            reply.result = "invalid data";      // default reply sebelum process

            if (!data.tablename.isEmpty()){
                if (DateTimeHelper.isValidDate_yyyyMMdd(data.tablename)){
                    var tablename = DateTimeHelper.ConvertToTableName(data.tablename);
                    if (event!=null){
                        reply.result = event.InsertFisDataToTable(remoteAddress, sessionId, tablename, data.fisdata);
                        System.out.println("SocketIO insertrow result: "+reply.result);
                    } else reply.result = "no event handler";
                }
            }
            ackSender.sendAckData(reply);
        });

        /*
          Delete Row event
          data: {
            tablename: "table name",
            rowid: id of the row to be deleted, in integer
            }
          <p/>
            return: {
            command: "deleterow",
            value: {
            tablename: "table name",
            rowid: id of the row to be deleted, in integer
            },
            result: "success" or "failed"
         */
        server.addEventListener("deleterow", DeleteRowStructure.class, (client, data, ackSender) -> {
            String remoteAddress = client.getRemoteAddress().toString();
            String sessionId = client.getSessionId().toString();
            raise_log(String.format("Client %s with id=%s request to delete row %s", remoteAddress, sessionId, data));

            ReplyDeleteRowStructure reply = new ReplyDeleteRowStructure();
            reply.command = "deleterow";
            reply.value = data;
            reply.result = "invalid data";      // default reply sebelum process

            if (!data.tablename.isEmpty()){
                if (DateTimeHelper.isValidDate_yyyyMMdd(data.tablename)){
                    var tablename = DateTimeHelper.ConvertToTableName(data.tablename);
                    if (event!=null){
                        reply.result = event.DeleteFisDataFromTable(remoteAddress, sessionId, tablename, data.rowid);
                    } else reply.result = "no event handler";
                }
            }
            ackSender.sendAckData(reply);
        });

        /*
          Edit Row event
          data: {
           tablename: "table name",
           rowid: id of the row to be edited, in integer
           newdata: FisData in JsonObject
           }
          <p/>
           return: {
           command: "editrow",
           value: {
           tablename: "table name",
           rowid: id of the row to be edited, in integer
           newdata: FisData in JsonObject
           },
           result: "success" or "failed"
         */
        server.addEventListener("editrow", EditRowStructure.class, (client, data, ackSender) -> {
            String remoteAddress = client.getRemoteAddress().toString();
            String sessionId = client.getSessionId().toString();
            raise_log(String.format("Client %s with id=%s request to edit row %s", remoteAddress, sessionId, data));

            ReplyEditRowStructure reply = new ReplyEditRowStructure();
            reply.command = "editrow";
            reply.value = data;
            reply.result = "invalid data";      // default reply sebelum process

            if (!data.tablename.isEmpty()){
                if (DateTimeHelper.isValidDate_yyyyMMdd(data.tablename)){
                    var tablename = DateTimeHelper.ConvertToTableName(data.tablename);
                    if (event!=null){
                        reply.result = event.UpdateFisDataInTable(remoteAddress, sessionId, tablename, data.rowid, data.newdata);
                    } else reply.result = "no event handler";
                }
            }
            ackSender.sendAckData(reply);
        });

        /*
          Get Data event
          data: {
            tablename: "table name",
            filter: {   // optional, can be null
              airlinecode: "filter by airline code",  // optional, can be null
              flightnumber: "filter by flight number", // optional, can be null
              DA: "filter by DA",   // optional, can be null
              DI: "filter by DI"    // optional, can be null
            }
          }
          <p/>
          return: {
            command: "getdata",
            value: {
              tablename: "table name",
                filter: {
                  airlinecode: "filter by airline code",
                 flightnumber: "filter by flight number",
                 DA: "filter by DA",
                 DI: "filter by DI"
               }
            },
            result: "success" or "failed" or "no data",
            data: FisData in JsonArray
            }
         */
        server.addEventListener("getdata", GetDataStructure.class, (client, data, ackSender) -> {
            String remoteAddress = client.getRemoteAddress().toString();
            String sessionId = client.getSessionId().toString();
            raise_log(String.format("Client %s with id=%s request to get data %s", remoteAddress, sessionId, GsonFormatter.toJson(data)));

            ReplyGetDataStructure reply = new ReplyGetDataStructure();
            reply.command = "getdata";
            reply.value = data;
            reply.result = "invalid data";      // default reply sebelum process
            if (!data.tablename.isEmpty()){
                String tablename = DateTimeHelper.ConvertToTableName(data.tablename);
                try{
                    if (event!=null){
                        reply.data = event.GetFisDataFromTable(remoteAddress, sessionId, tablename,
                                data.filter!=null ? data.filter.airlinecode:null,
                                data.filter !=null ? data.filter.flightnumber:null,
                                data.filter !=null ?data.filter.DA:null,
                                data.filter !=null ? data.filter.DI:null);


                        if (reply.data!=null && reply.data.length>0){
                            reply.result = "success";

                        } else reply.result = "no data";
                    } else reply.result = "no event handler";
                } catch (Exception e){
                    System.out.println("SocketIO getdata error: "+e.getMessage());
                }
            }

            //System.out.println("SocketIO getdata reply: "+reply);
            ackSender.sendAckData(reply);
        });


        /*
            Export to XLSX event
            data: {
                tablename: "table name"
            }
            <p/>
            return: {
                command: "exportxlsx",
                tablename: "table name",
                result: "success" or "failed" or "no data",
                filename: "filename",
                size: file size in bytes
                }
         */
        server.addEventListener("exportxlsx", ExportXLSXStructure.class, ((socketIOClient, exportXL, ackRequest) -> {
            String remoteAddress = socketIOClient.getRemoteAddress().toString();
            String sessionId = socketIOClient.getSessionId().toString();
            raise_log(String.format("Client %s with id=%s request to export xlsx %s", remoteAddress, sessionId, GsonFormatter.toJson(exportXL)));

            ReplyExportXLSXStructure reply = new ReplyExportXLSXStructure();
            reply.tablename = exportXL.tablename;
            reply.result = "invalid data";      // default reply sebelum process

            if (exportXL.tablename!=null &&  !exportXL.tablename.isEmpty()){
                if (DateTimeHelper.isValidDate_yyyyMMdd(exportXL.tablename)){
                    var tablename = DateTimeHelper.ConvertToTableName(exportXL.tablename);
                    if (event!=null){
                        Path path = event.ExportToXLSX(remoteAddress, sessionId, tablename);
                        if (path!=null){
                            if (path.toFile().isFile()){
                                reply.size = (int) path.toFile().length();
                                reply.filename = "/export/"+path.getFileName().toString();
                                reply.result = "success";
                            } else reply.result = "file not found";
                        } else reply.result = "no file or unauthorized";
                    } else reply.result = "no event handler";
                }
            }

            //System.out.printf("SocketIO exportxlsx reply: %s, length: %d\n", reply.result, reply.size);
            ackRequest.sendAckData(reply);

        }));
    }

    /**
     * Convert Socket.io data object to JsonObject
     *
     * @param data must be instance of JsonObject
     * @param keys must contains these keys
     * @return JsonObject if data is instance of JsonObject and contains the keys, otherwise return null
     */
    private JsonObject ConvertDataToJsonObject(Object data, String... keys){
        if (data != null){
            if (data instanceof JsonObject obj){
                if (keys!=null && keys.length>0){
                    for(String key : keys){
                        if (!obj.has(key)) return null;
                        Object val = obj.get(key);
                        if (val==null) return null;
                    }
                    return obj;
                }
                return obj;
            }
        }
        return null;
    }

    /**
     * Stop the server
     */
    @SuppressWarnings("unused")
    public void Stop(){

        if (server!=null) server.stop();
        raise_log("SocketIO server stopped");
    }

    private void raise_log(String msg){
        System.out.println(msg);
    }
}
