import {WebSocketPayload} from "../../WebSocketPayload";
import debug from "debug";
import {WebSocketHandler} from "../../WebSocketHandler";
import {iTender} from "../../iTender";
import {iTenderStatus} from "../../iTenderStatus";
import {WebSocketEvent} from "../../WebSocketEvent";
import Container from "../../database/Container";
import {SensorType} from "../../SensorType";
import {Settings} from "../../Settings";
import Ingredient from "../../database/Ingredient";
import {RequestType} from "../../RequestType";
import {IJob} from "../../database/IJob";
import {SensorHelper} from "../../SensorHelper";
import {Mixer} from "../../Mixer";
import {ArduinoProxy} from "../../ArduinoProxy";
import * as os from "os";
import {promisify} from "util";
import Drink from "../../database/Drink";
import path from "path";
import {Utils} from "../../Utils";
import {ErrorHandler, InternalError} from "../../ErrorHandler";
import {NextFunction, Request} from "express";
import WebsocketContainers from "./WebsocketContainers";
import WebsocketContainerUpdate from "./WebsocketContainerUpdate";

const exec = promisify(require('child_process').exec)

const log = debug("itender:websocket");


export async function handleWebSocket(ws: WebSocket | any, req: Request, next: NextFunction) {
    log("Incoming websocket connection...");

    if (WebSocketHandler.ws) {
        WebSocketHandler.ws.close(1001);
    }
    WebSocketHandler.ws = ws;

    await WebSocketHandler.sendRunningConfig();
    await WebSocketHandler.sendContainers();
    await WebSocketHandler.sendStatus();
    await WebSocketHandler.sendDrinks();


    ws.on('message', async (raw, bool) => {
        try {
            let msg = WebSocketPayload.parseFromBase64Json(raw);
            // If message is null, close the socket because it could not be decompiled
            if (!msg) {
                ws.close(1011);
                return;
            }
            console.log(msg.event, msg.data)


            switch (msg.event) {
                case WebSocketEvent.CONTAINERS: {
                    await WebsocketContainers(msg);
                    break;
                }

                case WebSocketEvent.CONTAINER_UPDATE: {
                    await WebsocketContainerUpdate(msg);
                    break;
                }

                case WebSocketEvent.CONFIG: {
                    // ToDo
                    console.log("New Settings:", msg.data);
                    // Danach setup modus aus
                    for (const [key, value] of Object.entries(msg.data)) {
                        Settings.set(key, value);
                    }

                    Settings.setupDone = true;
                    break;
                }

                case WebSocketEvent.SETUP: {
                    if ((msg.data as boolean)) {
                        iTender.setStatus(iTenderStatus.SETUP);
                    } else {
                        if (Settings.setupDone) {
                            iTender.setStatus(iTenderStatus.READY);
                            await WebSocketHandler.sendRunningConfig();
                        }
                    }
                    await WebSocketHandler.sendContainers();
                    break;
                }

                case WebSocketEvent.CANCEL: {
                    await Mixer.cancelFill();
                    break;
                }

                case WebSocketEvent.REQUEST: {
                    log("Request to " + msg.data["type"]);

                    switch (msg.data["type"] as RequestType) {
                        case RequestType.STATS: {
                            await WebSocketHandler.sendStats();
                            break;
                        }
                        case RequestType.CONTAINERS: {
                            WebSocketHandler.answerRequest(msg.data["type"] as RequestType, (await Container.find().sort({"slot": 1}).populate("content")));
                            break;
                        }
                        case RequestType.INGREDIENTS: {
                            WebSocketHandler.answerRequest(msg.data["type"] as RequestType, (await Ingredient.find().sort({"name": 1})));
                            break;
                        }
                        case RequestType.STARTFILL: {
                            let job: IJob | null = null;
                            try {
                                job = await iTender.onReceiveFill(msg.data.data);
                            } catch (e: any) {
                                console.error(e);
                            }
                            WebSocketHandler.answerRequest(msg.data["type"] as RequestType, {
                                success: (!!job),
                                job: job
                            });
                            break;
                        }
                        case RequestType.JOB: {
                            WebSocketHandler.answerRequest(msg.data["type"] as RequestType, Mixer.currentJob);
                            break;
                        }
                        case RequestType.DOWNLOAD_DRINKS: {
                            await iTender.refreshFromServer();
                            WebSocketHandler.answerRequest(msg.data["type"] as RequestType, "ok");
                            break;
                        }
                        case RequestType.CHECK: {
                            let conf = msg.data.data as {
                                "led_enabled": boolean,
                                "remote_enabled": boolean,
                                "hotspot_enabled": boolean,
                                "arduino_proxy_enabled": boolean,
                                "led_gpio": number,
                                "ambient_color": string
                            }

                            await SensorHelper.clearAllRawMeasurements();

                            let content: { success: boolean, msg: string } = {
                                success: true,
                                msg: "Prüfung erfolgreich."
                            };

                            // Check config
                            /// Check Proxy
                            if (conf["arduino_proxy_enabled"]) {
                                try {
                                    ArduinoProxy.disconnect();
                                } catch (e) {

                                }
                                try {
                                    await ArduinoProxy.connect();
                                } catch (e) {
                                    log("Checkup failed");
                                    content.success = false;
                                    content.msg = "Bei der Kommunikation mit dem Arduino Proxy ist ein Fehler aufgetreten.<br><br><em>Technische Details: " + e + "</em>";
                                    return WebSocketHandler.answerRequest(msg.data["type"] as RequestType, content);
                                }
                            }

                            // Check measurements
                            try {
                                await SensorHelper.measureAllRaw();
                            } catch (e) {
                                content.success = false;
                                content.msg = e + "<br>Überprüfe die Einstellungen der Sensoren-Pins.";
                                return WebSocketHandler.answerRequest(msg.data["type"] as RequestType, content);
                            }

                            for (let c of await Container.find()) {
                                if (c.sensorType != SensorType.NONE && c.rawData == -1) {
                                    content.success = false;
                                    content.msg = "Container " + (c.slot + 1) + " weist Fehler im Sensor auf.<br>Überprüfe die Einstellungen der Sensoren-Pins.";
                                    return WebSocketHandler.answerRequest(msg.data["type"] as RequestType, content);
                                }
                            }

                            WebSocketHandler.answerRequest(msg.data["type"] as RequestType, content);
                            break;
                        }
                        case RequestType.TARE: {
                            let type = msg.data["type"];
                            // Start TARE
                            let success = true;

                            for (let c of await Container.find({})) {
                                if (c.sensorType != SensorType.NONE) {
                                    c.sensorTare = 0;
                                    await c.save();
                                }
                            }

                            let timeouts: Array<NodeJS.Timeout> = [];

                            async function measureAndSafe() {
                                try {
                                    await SensorHelper.measureAllRaw();
                                    for (let c of await Container.find({})) {
                                        if (c.sensorType != SensorType.NONE) {
                                            c.sensorTare += c.rawData;
                                        }
                                    }
                                } catch (e) {
                                    WebSocketHandler.answerRequest(type, {success: false, msg: e});
                                    success = false;
                                    for (let t of timeouts)
                                        clearTimeout(t);
                                }
                            }

                            timeouts.push(setTimeout(measureAndSafe, 500));
                            timeouts.push(setTimeout(measureAndSafe, 1000));
                            timeouts.push(setTimeout(measureAndSafe, 2000));
                            timeouts.push(setTimeout(measureAndSafe, 3000));

                            setTimeout(async () => {
                                if (success) {
                                    for (let c of await Container.find({})) {
                                        if (c.sensorType != SensorType.NONE) {
                                            c.sensorTare = c.sensorTare / 4;
                                            await c.save();
                                        }
                                    }
                                    WebSocketHandler.answerRequest(type, {success: true, msg: "OK"});
                                }
                            }, 4000);
                            break;

                        }

                        case RequestType.UPDATE: {
                            if (!iTender.internetConnection)
                                return WebSocketHandler.answerRequest(msg.data["type"] as RequestType, false);
                            WebSocketHandler.answerRequest(msg.data["type"] as RequestType, true);

                            try {
                                let result = await exec(path.join(global.appRoot, "/../update.sh"));
                                if (result.stderr)
                                    await WebSocketHandler.send(new WebSocketPayload(WebSocketEvent.ERROR, "Der iTender konnte das Update nicht installieren.<br>Möglicherweise ist die Internetverbindung nicht ausreichend oder das Update enthält Fehler.<br><br><em>" + result.stderr + "</em>"));
                                let error = new InternalError("Update request from user-interface failed while executing update script", result.stderr, result.code);
                                await ErrorHandler.sendError(error);
                            } catch (e) {
                                let error = e as { code: number, killed: boolean, cmd: string, stderr: string };

                                let msg = "";
                                if (error.code == 127)
                                    msg = "Beim Ausführen ist ein unbekanntes Problem aufgetreten.";
                                else if (error.code == 1)
                                    msg = "Die Internetverbindung ist nicht ausreichend, um iTender zu aktualisieren.";

                                await WebSocketHandler.send(new WebSocketPayload(WebSocketEvent.ERROR, "Der iTender konnte das Update nicht installieren.<br><br>" + msg));
                                log("Could not execute update.sh");

                                let iE = new InternalError("Update request from user-interface failed while executing update script", error.stderr, error.code + "");
                                await ErrorHandler.sendError(iE);
                            }
                            break;
                        }

                        case RequestType.INFO: {
                            let nets = os.networkInterfaces();
                            let net = nets["wlan0"];
                            if (!net)
                                net = nets["wlp0s20f3"];
                            let ipAddr: string = "";

                            if (net)
                                for (let addr of net) {
                                    if (addr.family == "IPv4" && addr.address && addr.address !== "127.0.0.1")
                                        ipAddr = addr.address;
                                }

                            let packageJson = require('../../../package.json');
                            let wifi, uptime;
                            try {
                                wifi = (await exec("iwgetid")).stdout
                                uptime = (await exec("uptime -p")).stdout;
                            } catch (e) {
                            }

                            wifi = wifi.substring(wifi.indexOf('"') + 1, wifi.length - 2);

                            let data = {
                                "internet": iTender.internetConnection,
                                "ip": ipAddr,
                                "network": wifi ?? "Kein WiFi",
                                "uptime": uptime ?? "Unbekannt",
                                "version": packageJson.version,
                                "author": "Tobias Hopp",
                                "contact": "tobi@gaminggeneration.de"
                            }

                            WebSocketHandler.answerRequest(msg.data["type"] as RequestType, data);
                            break;
                        }

                        case RequestType.CLEAR_DB: {
                            await Drink.deleteMany({});
                            await Ingredient.deleteMany({});
                            for (let c of (await Container.find())) {
                                Utils.deleteImage(c._id);
                                c.content = undefined;
                                c.save();
                            }

                            await iTender.refreshDrinks();
                            WebSocketHandler.answerRequest(msg.data["type"] as RequestType, true);
                            break;
                        }

                        case RequestType.SHUTDOWN: {
                            await exec('sudo shutdown now');
                        }
                    }
                    break;
                }

                default: {
                    log("WebSocketHandler does not know how to handle " + msg.event + " Event");
                }
            }
        } catch (e: any) {
            await ErrorHandler.sendError(new InternalError("UncaughtException", e.toString(), ""));
        }

    });
}
