import Database.FisData;
import Database.MySQLDatabase;
import GPIO.LedController;
import GPIO.LedState;
import GPIO.LedType;
import Gson.DateTimeHelper;
import Web.*;
import XLSX.ExcelFile;
import lombok.val;

import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;

public class MiniFIS {
        public static ConfigStructure config;
        private static final DateTimeFormatter dtfexport = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss");
        public static LedController ledController;
        private static Map<String, SocketIOConnections> ConnectionMap ;

    /**
     * Get System Enviroment and change config if needed
     */
    private static void GetEnviromentVariables(){
        String databasehost = System.getenv("DATABASE_HOST");
        String databaseport = System.getenv("DATABASE_PORT");
        String databaseuser = System.getenv("DATABASE_USER");
        String databasepassword = System.getenv("DATABASE_PASSWORD");
        String databasename = System.getenv("DATABASE_NAME");
        boolean changed = false;
        if (databasename!=null && !databasename.isEmpty()){
            if (!databasename.equals(config.MySQLDatabase)){
                config.MySQLDatabase = databasename;
                System.out.println("Database name changed to: "+databasename);
                changed = true;
            }
        }
        if (databasehost!=null && !databasehost.isEmpty()){
            if (!databasehost.equals(config.MySQLHost)){
                config.MySQLHost = databasehost;
                System.out.println("Database host changed to: "+databasehost);
                changed = true;
            }
        }
        if (databaseport!=null && !databaseport.isEmpty()){
            try{
                int port = Integer.parseInt(databaseport);
                if (port!=config.MySQLPort){
                    config.MySQLPort = port;
                    System.out.println("Database port changed to: "+port);
                    changed = true;
                }
            } catch (Exception e){
                System.out.println("Error parsing DATABASE_PORT: "+e.getMessage());
            }
        }
        if (databaseuser!=null && !databaseuser.isEmpty()){
            if (!databaseuser.equals(config.MySQLAdminUser)){
                config.MySQLAdminUser = databaseuser;
                System.out.println("Database user changed to: "+databaseuser);
                changed = true;
            }
        }
        if (databasepassword!=null && !databasepassword.isEmpty()){
            if (!databasepassword.equals(config.MySQLAdminPassword)){
                config.MySQLAdminPassword = databasepassword;
                System.out.println("Database password changed to: "+databasepassword);
                changed = true;
            }
        }
        if (changed) {
            Path path = Path.of(System.getProperty("user.dir"), "config", "config.json");
            if (config.SaveToFile(path)) {
                System.out.println("Config file updated");
            } else {
                System.out.println("Failed to update config file");
            }
        }

    }

    private static void LoadConfig(){
        String userdir = System.getProperty("user.dir");
        Path configpath = Path.of(userdir,"config","config.json");
        if (Files.exists(configpath)) {
            System.out.println("Config file exists");
            config = ConfigStructure.LoadFromFile(configpath);
        } else {
            System.out.println("Config file does not exist");
            config = new ConfigStructure();
            if (config.SaveToFile(configpath)) {
                System.out.println("Config file created");
            } else {
                System.out.println("Failed to create config file");
            }
        }
    }

    public static void main(String[] args) {
        System.out.println("MiniFIS version 1.0.9");

        LoadConfig();

        GetEnviromentVariables();

        String ledhost = System.getenv("LED_CONTROLLER_HOST");
        if (ledhost==null) ledhost = "localhost";
        if (ledhost.isEmpty()) ledhost = "localhost";

        // mesti di final karena dipakai di dalam thread
        final String ledcontrollerhost = ledhost;
        ledController = new LedController(ledcontrollerhost, 4000);

        AtomicBoolean continueblinking = new AtomicBoolean(false);
        new Thread(()->{
            // blinking controller
            continueblinking.set(true);
            System.out.println("Blinking Controller started, targeting "+ledcontrollerhost);
            while (continueblinking.get()){
                ledController.SetLed(LedType.PILOT, LedState.BLINK);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException ignored) {
                    continueblinking.set(false);
                }
            }
            System.out.println("Blinking Controller stopped");
        }).start();

        MySQLDatabase db = new MySQLDatabase(config.MySQLHost, config.MySQLPort, config.MySQLAdminUser, config.MySQLAdminPassword, config.MySQLDatabase);
        WebServer web = new WebServer((tablename, filename) -> {
            System.out.println("WebServerEvent: onImport: "+tablename+", "+filename);
            if (DateTimeHelper.isValidDate_yyyyMMdd(tablename)){
                tablename = DateTimeHelper.ConvertToTableName(tablename);
                System.out.println("Converted tablename: "+tablename);
                if (Arrays.asList(db.GetTableNames()).contains(tablename)) {
                    System.out.printf("ImportFromXLSX: Table %s exists, dropping...\n", tablename);
                    db.DeleteTable(tablename);
                }
                if ("success".equals(db.CreateTable(tablename))){
                    System.out.printf("ImportFromXLSX: Table %s created\n", tablename);
                    FisData[] data = ExcelFile.LoadFromXLSX(filename.toString(),tablename);
                    if (data.length>0){
                        String insertresult = db.InsertData(tablename, data);
                        System.out.println("MiniFIS insert result: " + insertresult);
                        ledController.SetLed(LedType.ACTION, LedState.BLINK);
                        return insertresult;
                    } else return "no data from xlsx";
                } else return "failed to create table";
            } else return "invalid tablename";

        });
        web.start(config.WebHost, config.WebPort);

        ConnectionMap = new HashMap<>();

        SocketIO socket = new SocketIO(config.WebHost,config.SocketIOPort);
        socket.Start(new SocketIOEvents() {
            @Override
            public void Connected(String remoteAddress, String sessionId) {
                if (!ConnectionMap.containsKey(sessionId)){
                    System.out.println("Socket.io Client Connected: " + remoteAddress + " , id: " + sessionId);
                    val conn = new SocketIOConnections();
                    conn.id = sessionId;
                    conn.remoteAddress = remoteAddress;
                    conn.role = "";
                    ConnectionMap.put(sessionId, conn);
                }
                ledController.SetLed(LedType.NETWORK, LedState.BLINK);
            }

            @Override
            public void Disconnected(String remoteAddress, String sessionId) {
                if (ConnectionMap.containsKey(sessionId)){
                    ConnectionMap.remove(sessionId);
                    System.out.println("Socket.io Client Disconnected: " + remoteAddress + " , id: " + sessionId);
                }
            }

            @Override
            public String[] GetTables(String remoteAddress, String sessionId) {
                ledController.SetLed(LedType.NETWORK, LedState.BLINK);
                val conn = ConnectionMap.get(sessionId);
                if (conn.role!=null && !conn.role.isEmpty()){   // semua role bisa lihat tabel
                    ledController.SetLed(LedType.ACTION, LedState.BLINK);
                    return db.GetTableNames();
                } else return new String[0];
            }

            @Override
            public String CreateTable(String remoteAddress, String sessionId, String tableName) {
                ledController.SetLed(LedType.NETWORK, LedState.BLINK);
                val conn = ConnectionMap.get(sessionId);
                if (conn!=null && config.MySQLAdminUser.equals(conn.role)){   // hanya admin yang bisa create
                    String result = db.CreateTable(tableName);
                    ledController.SetLed(LedType.ACTION, LedState.BLINK);
                    return result;
                }
                else return "unauthorized";
            }

            @Override
            public String DropTable(String remoteAddress, String sessionId, String tableName) {
                ledController.SetLed(LedType.NETWORK, LedState.BLINK);
                val conn = ConnectionMap.get(sessionId);
                if (conn!=null && config.MySQLAdminUser.equals(conn.role)){    // hanya admin yang bisa drop
                    String result =  db.DeleteTable(tableName);
                    ledController.SetLed(LedType.ACTION, LedState.BLINK);
                    return result;
                }
                else return "unauthorized";
            }

            @Override
            public String InsertFisDataToTable(String remoteAddress, String sessionId, String tableName, FisData... data) {
                ledController.SetLed(LedType.NETWORK, LedState.BLINK);
                val conn = ConnectionMap.get(sessionId);
                if (conn!=null && config.MySQLAdminUser.equals(conn.role)){     // hanya admin yang bisa insert
                    String result =  db.InsertData(tableName, data);
                    ledController.SetLed(LedType.ACTION, LedState.BLINK);
                    return result;
                }
                else return "unauthorized";
            }

            @Override
            public String DeleteFisDataFromTable(String remoteAddress, String sessionId, String tableName, int index) {
                ledController.SetLed(LedType.NETWORK, LedState.BLINK);
                val conn = ConnectionMap.get(sessionId);
                if (conn!=null && config.MySQLAdminUser.equals(conn.role)){   // hanya admin yang bisa delete
                    String deleteresult = db.DeleteData(tableName, index);
                    db.ReorderingIndex(tableName);
                    ledController.SetLed(LedType.ACTION, LedState.BLINK);
                    return deleteresult;
                }
                else return "unauthorized";


            }

            @Override
            public String UpdateFisDataInTable(String remoteAddress, String sessionId, String tableName, int index, FisData data) {
                ledController.SetLed(LedType.NETWORK, LedState.BLINK);
                val conn = ConnectionMap.get(sessionId);
                if (conn!=null && !conn.role.isEmpty()){    // semua role bisa update
                    String updateresult = db.UpdateData(tableName, index, data);
                    ledController.SetLed(LedType.ACTION, LedState.BLINK);
                    return updateresult;
                }
                else return "unauthorized";
            }

            @Override
            public FisData[] GetFisDataFromTable(String remoteAddress, String sessionId, String tableName, String filter_airlinecode, String filter_flightnumber, String filter_DA, String filter_DI) {
                ledController.SetLed(LedType.NETWORK, LedState.BLINK);
                val conn = ConnectionMap.get(sessionId);
                if (conn!=null && !conn.role.isEmpty()){    // semua role bisa baca
                    FisData[] result = db.GetData(tableName, filter_airlinecode, filter_flightnumber, filter_DA, filter_DI);
                    ledController.SetLed(LedType.ACTION, LedState.BLINK);
                    return result;
                }
                else return new FisData[0];
            }

            @Override
            public Path ExportToXLSX(String remoteAddress, String sessionId, String tablename) {
                FisData[] data = db.GetData(tablename, null, null, null, null);
                System.out.printf("ExportToXLSX: Got %d rows from %s\n", data.length, tablename);
                ledController.SetLed(LedType.NETWORK, LedState.BLINK);
                String exportfilename = "export_"+ dtfexport.format(LocalDateTime.now())+".xlsx";
                Path exportpath = web.getExportpath().resolve(exportfilename);
                if (ExcelFile.SaveToXLSX(exportpath.toString(), tablename, data)){
                    System.out.printf("ExportToXLSX: Saved to %s\n", exportpath);
                    ledController.SetLed(LedType.ACTION, LedState.BLINK);
                    return exportpath;
                } else System.out.println("ExportToXLSX: Failed to save to xlsx");
                return null;
            }

            @Override
            public String Login(String remoteAddress, String sessionId, String username, String password) {
                ledController.SetLed(LedType.NETWORK, LedState.BLINK);
                if (username.equals(config.MySQLAdminUser) && password.equals(config.MySQLAdminPassword)){
                    ledController.SetLed(LedType.ACTION, LedState.BLINK);
                    val conn = ConnectionMap.get(sessionId);
                    if (conn!=null) conn.role = "adminfis";
                    return "welcome adminfis";
                } else {
                    if (config.users!=null && config.users.length>0){
                        for (Users user : config.users){
                            if (user.username.equals(username) && user.password.equals(password)){
                                ledController.SetLed(LedType.ACTION, LedState.BLINK);
                                val conn = ConnectionMap.get(sessionId);
                                if (conn!=null) conn.role = user.role;
                                return "welcome "+username;
                            }
                        }

                    }
                }
                return "unauthorized";
            }
        });

        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.out.println("Shutting down MiniFIS...");
            continueblinking.set(false);
            web.stop();
            socket.Stop();
            ledController.Close();
        }));
    }
}
