package Web;

import Other.SomeCodes;
import com.corundumstudio.socketio.Configuration;
import com.corundumstudio.socketio.SocketIOClient;
import com.corundumstudio.socketio.SocketIOServer;
import com.corundumstudio.socketio.listener.ConnectListener;
import com.corundumstudio.socketio.listener.DisconnectListener;
import io.javalin.Javalin;
import io.javalin.http.UploadedFile;
import io.javalin.util.FileUtil;
import io.javalin.util.JavalinException;
import io.javalin.websocket.*;
import lombok.Getter;
import lombok.Setter;
import org.tinylog.Logger;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

import static Other.SomeCodes.*;
import static io.javalin.apibuilder.ApiBuilder.*;


@SuppressWarnings({"unused"})
public class WebServer {
    private @Getter @Setter String webusername;
    private @Getter @Setter String webpassword;
    private final Javalin app;
    private final Set<WsContext> connectedWebsocketClients = ConcurrentHashMap.newKeySet();
    private final SocketIOServer socketServer;
    private final Configuration socketConfig;
    private final HashMap<String, SocketIOClient> socketIOClients = new HashMap<>();

    public WebServer(WebsocketEvent event, String webusername, String webpassword){
        this.webusername = webusername;
        this.webpassword = webpassword;
        app = Javalin.create(config -> {
            config.useVirtualThreads = true;

            config.staticFiles.add("/html");
            config.router.apiBuilder(()-> path("setting", () ->{
                get(ctx -> ctx.json(SettingInfo.getInstance()));
                path("audiofile",()-> post(ctx -> {
                    Logger.info("api /setting/audiofile");
                    String audiofile1 = ctx.formParam("preset1");
                    String audiofile2 = ctx.formParam("preset2");
                    String audiofile3 = ctx.formParam("preset3");
                    String audiofile4 = ctx.formParam("preset4");
                    String audiofile5 = ctx.formParam("preset5");
                    Logger.info("audiofile1: {}", audiofile1);
                    Logger.info("audiofile2: {}", audiofile2);
                    Logger.info("audiofile3: {}", audiofile3);
                    Logger.info("audiofile4: {}", audiofile4);
                    Logger.info("audiofile5: {}", audiofile5);

                    Properties prop = SomeCodes.LoadProperties("config.properties");
                    prop.setProperty("AudioFile01", audiofile1!=null?audiofile1:"");
                    prop.setProperty("AudioFile02", audiofile2!=null?audiofile2:"");
                    prop.setProperty("AudioFile03", audiofile3!=null?audiofile3:"");
                    prop.setProperty("AudioFile04", audiofile4!=null?audiofile4:"");
                    prop.setProperty("AudioFile05", audiofile5!=null?audiofile5:"");
                    if (SaveProperties(prop, "config.properties")){
                        Logger.info("audiofile saved");
                        ctx.status(200);
                    } else {
                        Logger.error("Failed to save audiofile");
                        ctx.status(400);
                        ctx.result("Failed to save audiofile");
                    }
                }));
                path("uploadaudiofile", ()-> post(ctx -> {
                    List<UploadedFile> uploadedFileList = ctx.uploadedFiles();
                    int size = uploadedFileList.size();
                    if (size>0){
                        uploadedFileList.forEach(ff ->{
                            String targetsave = audioPath.resolve(ff.filename()).toString();
                            FileUtil.streamToFile(ff.content(), targetsave);
                            Logger.info("Uploaded file: {}", targetsave);
                        });
                        ctx.status(200);
                        ctx.result("UploadedFiles: "+size);
                    } else {
                        ctx.status(400);
                        ctx.result("No file uploaded");
                    }


                }));
                path("weblogin", ()-> post(ctx -> {
                    String username = ctx.formParam("username");
                    String password = ctx.formParam("password");
                    Logger.info("api /setting/weblogin");
                    Logger.info("username: {}", username);
                    Logger.info("password: {}", password);

                    Properties prop = SomeCodes.LoadProperties("config.properties");
                    prop.setProperty("WebUsername", ValidString(username)?username:"admin");
                    prop.setProperty("WebPassword", ValidString(password)?password:"bandara");
                    if (SaveProperties(prop, "config.properties")){
                        Logger.info("weblogin saved");

                        //ctx.status(200);
                        this.webusername = username;
                        this.webpassword = password;
                        ctx.redirect("/logout");
                    } else {
                        Logger.error("Failed to save weblogin");
                        ctx.status(400);
                        ctx.result("Failed to save weblogin");
                    }
                }));
                path("camera",()-> post(ctx -> {
                    String camera_ip = ctx.formParam("ip");
                    String camera_port = ctx.formParam("port");
                    String camera_username = ctx.formParam("username");
                    String camera_password = ctx.formParam("password");
                    Logger.info("api /setting/camera");
                    Logger.info("camera_ip: {}", camera_ip);
                    Logger.info("camera_port: {}", camera_port);
                    Logger.info("camera_username: {}", camera_username);
                    Logger.info("camera_password: {}", camera_password);

                    Properties prop = SomeCodes.LoadProperties("config.properties");
                    prop.setProperty("Camera_ip", ValidString(camera_ip)?camera_ip:"192.168.0.4");
                    prop.setProperty("Camera_port", ValidString(camera_port)?camera_port:"80");
                    prop.setProperty("Camera_user", ValidString(camera_username)?camera_username:"root");
                    prop.setProperty("Camera_password", ValidString(camera_password)?camera_password:"password");
                    if (SaveProperties(prop, "config.properties")){
                        Logger.info("IP camera setting saved");
                        ctx.status(200);
                        if (event!=null) event.NewCameraConfiguration();
                    } else {
                        Logger.error("Failed to save IP camera setting");
                        ctx.status(400);
                        ctx.result("Failed to save IP camera setting");
                    }
                }));

            }));
        });

        app.get("/", ctx-> {
            if (ctx.sessionAttribute("username")==null) {
                // belum login
                ctx.redirect("/login.html");
            } else if (Objects.equals(ctx.sessionAttribute("username"), this.webusername)){
                // sudah login
                ctx.redirect("/index.html");
            } else {
                // sudah login tapi bukan username yang benar
                ctx.redirect("/login.html");
            }
        });

        app.before("/index.html", ctx ->{
            if (ctx.sessionAttribute("username")==null){
                ctx.redirect("/login.html");
            }
        });

        app.before("/setting.html", ctx ->{
            if (ctx.sessionAttribute("username")==null){
                ctx.redirect("/login.html");
            }
        });

        app.post("/login", ctx ->{
            String username = ctx.formParam("username");
            String password = ctx.formParam("password");
            if (Objects.equals(username, this.webusername) && Objects.equals(password, this.webpassword)){
                ctx.sessionAttribute("username", username);
                ctx.redirect("/index.html");
            } else {
                ctx.status(400);
                ctx.redirect("/login.html");
            }
        });

        app.get("/logout", ctx ->{
            ctx.sessionAttribute("username", null);
            ctx.redirect("/login.html");
        });

        /*
          WebSocket Communication
          This is temporary, later must choose either WebSocket or SocketIO
         */
        app.ws("/ws", ws -> {
            ws.onConnect(connectws);
            ws.onClose(closews);
            ws.onMessage(ctx -> {
                try{
                    //Logger.info("WebSocket message {}", ctx.message());
                    WebsocketCommand command = ctx.messageAsClass(WebsocketCommand.class);
                    if (event!=null) {
                        WebsocketReply reply = event.onWebsocketCommand(command);
                        if (reply!=null) ctx.sendAsClass(reply, WebsocketReply.class);
                        //if (reply!=null) SendtoAll(reply);
                    }
                } catch (Exception e){
                    Logger.error("Failed to parse WebSocketCommand message: {}", e.getMessage());
                    ctx.closeSession();
                    connectedWebsocketClients.remove(ctx);
                }
            });

        });

        /*
          SocketIO Communication
          This is temporary, later must choose either WebSocket or SocketIO
         */
        socketConfig = new Configuration();
        socketConfig.setHostname("0.0.0.0");
        socketConfig.setPort(3001);

        socketServer = new SocketIOServer(socketConfig);
        socketServer.addConnectListener(connectListener);
        socketServer.addDisconnectListener(disconnectListener);

        socketServer.addNamespace("/socketio").addEventListener("message", String.class, (client, data, ackSender) -> {
            //Logger.info("SocketIO message from namespace /socketio from {}: {}", client.getRemoteAddress(), data);
            WebsocketCommand command = gson.fromJson(data, WebsocketCommand.class);
            if (event!=null) {
                WebsocketReply reply = event.onWebsocketCommand(command);
                if (reply!=null) client.sendEvent("message", gson.toJson(reply));
            }
        });


    }

    /**
     * Send Object message to all connected websocket clients
     * @param obj Object to send
     */
    public void SendtoAll(Object obj){

        connectedWebsocketClients
                .stream()
                .filter(ws -> ws.session.isOpen())
                .forEach(ws -> ws.send(obj));
        socketIOClients.forEach((key, client) -> client.sendEvent("message", gson.toJson(obj)));
    }

    /**
     * Start the web server
     * @param localip Local IP address to bind
     * @param port Port to bind
     */
    public void Start(String localip, int port){
        try{
            app.start(localip, port);
            Logger.info("Web server started at {}:{}", localip, port);
            socketServer.start();
            Logger.info("SocketIO server started at {}:{}", socketConfig.getHostname(), socketConfig.getPort());
        } catch (JavalinException e){
            Logger.error("Web server failed to start: {}", e.getMessage());
        }
    }

    /**
     * Stop the web server
     */
    public void Stop(){
        try{
            connectedWebsocketClients.forEach(WsContext::closeSession);
            connectedWebsocketClients.clear();
            app.stop();
            Logger.info("Web server stopped");
            socketIOClients.forEach((key, client) -> client.disconnect());
            socketIOClients.clear();
            socketServer.stop();
            Logger.info("SocketIO server stopped");
        } catch (JavalinException e){
            Logger.error("Web server failed to stop: {}", e.getMessage());
        }
    }

    WsConnectHandler connectws = ws ->{
        Logger.info("WebSocket connected from {}", ws.sessionId());
        //ws.headerMap().forEach((key, value) -> Logger.info("HeaderMap {}: {}", key, value));
        connectedWebsocketClients.add(ws);
    };

    WsCloseHandler closews = ws ->{
        Logger.info("WebSocket closed from {}, code {}, reason {}", ws.sessionId(), ws.status(), ws.reason());
        connectedWebsocketClients.remove(ws);
    };

    ConnectListener connectListener = client -> {
        Logger.info("SocketIO connected, id {} from {}", client.getSessionId(), client.getRemoteAddress());
        socketIOClients.put(client.getSessionId().toString(), client);
    };

    DisconnectListener disconnectListener = client -> {
        Logger.info("SocketIO disconnected, id {} from {}", client.getSessionId(), client.getRemoteAddress());
        socketIOClients.remove(client.getSessionId().toString());
    };




}
