const CoreClass = require("../core.js");

const socketio = require("socket.io");
const async = require("async");
const config = require("config");

class IOModule extends CoreClass {
    constructor() {
        super("io");
    }

    initialize() {
        return new Promise(async (resolve, reject) => {
            this.setStage(1);

            const app = this.moduleManager.modules["app"],
                cache = this.moduleManager.modules["cache"],
                utils = this.moduleManager.modules["utils"],
                db = this.moduleManager.modules["db"],
                punishments = this.moduleManager.modules["punishments"];

            const actions = require("./actions");

            this.setStage(2);

            const SIDname = config.get("cookie.SIDname");

            // TODO: Check every 30s/60s, for all sockets, if they are still allowed to be in the rooms they are in, and on socket at all (permission changing/banning)
            this._io = socketio(await app.runJob("SERVER", {}));

            this.setStage(3);

            this._io.use(async (socket, next) => {
                if (this.getStatus() !== "READY") {
                    this.log(
                        "INFO",
                        "IO_REJECTED_CONNECTION",
                        `A user tried to connect, but the IO module is currently not ready. IP: ${socket.ip}.${sessionInfo}`
                    );
                    return socket.disconnect(true);
                }

                let SID;

                socket.ip =
                    socket.request.headers["x-forwarded-for"] || "0.0.0.0";

                async.waterfall(
                    [
                        (next) => {
                            utils
                                .runJob("PARSE_COOKIES", {
                                    cookieString: socket.request.headers.cookie,
                                })
                                .then((res) => {
                                    SID = res[SIDname];
                                    next(null);
                                });
                        },

                        (next) => {
                            if (!SID) return next("No SID.");
                            next();
                        },

                        (next) => {
                            cache
                                .runJob("HGET", { table: "sessions", key: SID })
                                .then((session) => {
                                    next(null, session);
                                });
                        },

                        (session, next) => {
                            if (!session) return next("No session found.");

                            session.refreshDate = Date.now();

                            socket.session = session;
                            cache
                                .runJob("HSET", {
                                    table: "sessions",
                                    key: SID,
                                    value: session,
                                })
                                .then((session) => {
                                    next(null, session);
                                });
                        },

                        (res, next) => {
                            // check if a session's user / IP is banned
                            punishments
                                .runJob("GET_PUNISHMENTS", {})
                                .then((punishments) => {
                                    const isLoggedIn = !!(
                                        socket.session &&
                                        socket.session.refreshDate
                                    );
                                    const userId = isLoggedIn
                                        ? socket.session.userId
                                        : null;

                                    let banishment = { banned: false, ban: 0 };

                                    punishments.forEach((punishment) => {
                                        if (
                                            punishment.expiresAt >
                                            banishment.ban
                                        )
                                            banishment.ban = punishment;
                                        if (
                                            punishment.type === "banUserId" &&
                                            isLoggedIn &&
                                            punishment.value === userId
                                        )
                                            banishment.banned = true;
                                        if (
                                            punishment.type === "banUserIp" &&
                                            punishment.value === socket.ip
                                        )
                                            banishment.banned = true;
                                    });

                                    socket.banishment = banishment;

                                    next();
                                })
                                .catch(() => {
                                    next();
                                });
                        },
                    ],
                    () => {
                        if (!socket.session)
                            socket.session = { socketId: socket.id };
                        else socket.session.socketId = socket.id;

                        next();
                    }
                );
            });

            this.setStage(4);

            this._io.on("connection", async (socket) => {
                if (this.getStatus() !== "READY") {
                    this.log(
                        "INFO",
                        "IO_REJECTED_CONNECTION",
                        `A user tried to connect, but the IO module is currently not ready. IP: ${socket.ip}.${sessionInfo}`
                    );
                    return socket.disconnect(true);
                }

                let sessionInfo = "";

                if (socket.session.sessionId)
                    sessionInfo = ` UserID: ${socket.session.userId}.`;

                // if session is banned
                if (socket.banishment && socket.banishment.banned) {
                    this.log(
                        "INFO",
                        "IO_BANNED_CONNECTION",
                        `A user tried to connect, but is currently banned. IP: ${socket.ip}.${sessionInfo}`
                    );
                    socket.emit("keep.event:banned", socket.banishment.ban);
                    socket.disconnect(true);
                } else {
                    this.log(
                        "INFO",
                        "IO_CONNECTION",
                        `User connected. IP: ${socket.ip}.${sessionInfo}`
                    );

                    // catch when the socket has been disconnected
                    socket.on("disconnect", () => {
                        if (socket.session.sessionId)
                            sessionInfo = ` UserID: ${socket.session.userId}.`;
                        this.log(
                            "INFO",
                            "IO_DISCONNECTION",
                            `User disconnected. IP: ${socket.ip}.${sessionInfo}`
                        );
                    });

                    socket.use((data, next) => {
                        if (data.length === 0)
                            return next(
                                new Error("Not enough arguments specified.")
                            );
                        else if (typeof data[0] !== "string")
                            return next(
                                new Error("First argument must be a string.")
                            );
                        else {
                            const namespaceAction = data[0];
                            if (
                                !namespaceAction ||
                                namespaceAction.indexOf(".") === -1 ||
                                namespaceAction.indexOf(".") !==
                                    namespaceAction.lastIndexOf(".")
                            )
                                return next(
                                    new Error("Invalid first argument")
                                );
                            const namespace = data[0].split(".")[0];
                            const action = data[0].split(".")[1];
                            if (!namespace)
                                return next(new Error("Invalid namespace."));
                            else if (!action)
                                return next(new Error("Invalid action."));
                            else if (!actions[namespace])
                                return next(new Error("Namespace not found."));
                            else if (!actions[namespace][action])
                                return next(new Error("Action not found."));
                            else return next();
                        }
                    });

                    // catch errors on the socket (internal to socket.io)
                    socket.on("error", console.error);

                    // have the socket listen for each action
                    Object.keys(actions).forEach((namespace) => {
                        Object.keys(actions[namespace]).forEach((action) => {
                            // the full name of the action
                            let name = `${namespace}.${action}`;

                            // listen for this action to be called
                            socket.on(name, async (...args) => {
                                let cb = args[args.length - 1];
                                if (typeof cb !== "function")
                                    cb = () => {
                                        this.this.log(
                                            "INFO",
                                            "IO_MODULE",
                                            `There was no callback provided for ${name}.`
                                        );
                                    };
                                else args.pop();

                                if (this.getStatus() !== "READY") {
                                    this.log(
                                        "INFO",
                                        "IO_REJECTED_ACTION",
                                        `A user tried to execute an action, but the IO module is currently not ready. Action: ${namespace}.${action}.`
                                    );
                                    return;
                                } else {
                                    this.log(
                                        "INFO",
                                        "IO_ACTION",
                                        `A user executed an action. Action: ${namespace}.${action}.`
                                    );
                                }

                                // load the session from the cache
                                cache
                                    .runJob("HGET", {
                                        table: "sessions",
                                        key: socket.session.sessionId,
                                    })
                                    .then((session) => {
                                        // make sure the sockets sessionId isn't set if there is no session
                                        if (
                                            socket.session.sessionId &&
                                            session === null
                                        )
                                            delete socket.session.sessionId;

                                        try {
                                            // call the action, passing it the session, and the arguments socket.io passed us
                                            actions[namespace][action].apply(
                                                null,
                                                [socket.session]
                                                    .concat(args)
                                                    .concat([
                                                        (result) => {
                                                            this.log(
                                                                "INFO",
                                                                "IO_ACTION",
                                                                `Response to action. Action: ${namespace}.${action}. Response status: ${result.status}`
                                                            );
                                                            // respond to the socket with our message
                                                            if (
                                                                typeof cb ===
                                                                "function"
                                                            )
                                                                return cb(
                                                                    result
                                                                );
                                                        },
                                                    ])
                                            );
                                        } catch (err) {
                                            this.log(
                                                "ERROR",
                                                "IO_ACTION_ERROR",
                                                `Some type of exception occurred in the action ${namespace}.${action}. Error message: ${err.message}`
                                            );
                                            if (typeof cb === "function")
                                                return cb({
                                                    status: "error",
                                                    message:
                                                        "An error occurred while executing the specified action.",
                                                });
                                        }
                                    })
                                    .catch((err) => {
                                        if (typeof cb === "function")
                                            return cb({
                                                status: "error",
                                                message:
                                                    "An error occurred while obtaining your session",
                                            });
                                    });
                            });
                        });
                    });

                    if (socket.session.sessionId) {
                        cache
                            .runJob("HGET", {
                                table: "sessions",
                                key: socket.session.sessionId,
                            })
                            .then((session) => {
                                if (session && session.userId) {
                                    db.runJob("GET_MODEL", {
                                        modelName: "user",
                                    }).then((userModel) => {
                                        userModel.findOne(
                                            { _id: session.userId },
                                            (err, user) => {
                                                if (err || !user)
                                                    return socket.emit(
                                                        "ready",
                                                        false
                                                    );
                                                let role = "";
                                                let username = "";
                                                let userId = "";
                                                if (user) {
                                                    role = user.role;
                                                    username = user.username;
                                                    userId = session.userId;
                                                }
                                                socket.emit(
                                                    "ready",
                                                    true,
                                                    role,
                                                    username,
                                                    userId
                                                );
                                            }
                                        );
                                    });
                                } else socket.emit("ready", false);
                            })
                            .catch((err) => {
                                socket.emit("ready", false);
                            });
                    } else socket.emit("ready", false);
                }
            });

            this.setStage(5);

            resolve();
        });
    }

    IO() {
        return new Promise((resolve, reject) => {
            resolve(this._io);
        });
    }
}

module.exports = new IOModule();