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

const redis = require("redis");
const config = require("config");
const mongoose = require("mongoose");

// Lightweight / convenience wrapper around redis module for our needs

const pubs = {},
    subs = {};

class CacheModule extends CoreClass {
    constructor() {
        super("cache");
    }

    initialize() {
        return new Promise((resolve, reject) => {
            this.schemas = {
                session: require("./schemas/session"),
                station: require("./schemas/station"),
                playlist: require("./schemas/playlist"),
                officialPlaylist: require("./schemas/officialPlaylist"),
                song: require("./schemas/song"),
                punishment: require("./schemas/punishment"),
            };

            this.url = config.get("redis").url;
            this.password = config.get("redis").password;

            this.log("INFO", "Connecting...");

            this.client = redis.createClient({
                url: this.url,
                password: this.password,
                retry_strategy: (options) => {
                    if (this.getStatus() === "LOCKDOWN") return;
                    if (this.getStatus() !== "RECONNECTING")
                        this.setStatus("RECONNECTING");

                    this.log("INFO", `Attempting to reconnect.`);

                    if (options.attempt >= 10) {
                        this.log("ERROR", `Stopped trying to reconnect.`);

                        this.setStatus("FAILED");

                        // this.failed = true;
                        // this._lockdown();

                        return undefined;
                    }

                    return 3000;
                },
            });

            this.client.on("error", (err) => {
                if (this.getStatus() === "INITIALIZING") reject(err);
                if (this.getStatus() === "LOCKDOWN") return;

                this.log("ERROR", `Error ${err.message}.`);
            });

            this.client.on("connect", () => {
                this.log("INFO", "Connected succesfully.");

                if (this.getStatus() === "INITIALIZING") resolve();
                else if (
                    this.getStatus() === "FAILED" ||
                    this.getStatus() === "RECONNECTING"
                )
                    this.setStatus("READY");
            });
        });
    }

    /**
     * Gracefully closes all the Redis client connections
     */
    QUIT(payload) {
        return new Promise((resolve, reject) => {
            if (this.client.connected) {
                this.client.quit();
                Object.keys(pubs).forEach((channel) => pubs[channel].quit());
                Object.keys(subs).forEach((channel) =>
                    subs[channel].client.quit()
                );
            }
            resolve();
        });
    }

    /**
     * Sets a single value in a table
     *
     * @param {String} table - name of the table we want to set a key of (table === redis hash)
     * @param {String} key -  name of the key to set
     * @param {*} value - the value we want to set
     * @param {Function} cb - gets called when the value has been set in Redis
     * @param {Boolean} [stringifyJson=true] - stringify 'value' if it's an Object or Array
     */
    HSET(payload) {
        //table, key, value, cb, stringifyJson = true
        return new Promise((resolve, reject) => {
            let key = payload.key;
            let value = payload.value;

            if (mongoose.Types.ObjectId.isValid(key)) key = key.toString();
            // automatically stringify objects and arrays into JSON
            if (["object", "array"].includes(typeof value))
                value = JSON.stringify(value);

            this.client.hset(payload.table, key, value, (err) => {
                if (err) return reject(new Error(err));
                else resolve(JSON.parse(value));
            });
        });
    }

    /**
     * Gets a single value from a table
     *
     * @param {String} table - name of the table to get the value from (table === redis hash)
     * @param {String} key - name of the key to fetch
     * @param {Function} cb - gets called when the value is returned from Redis
     * @param {Boolean} [parseJson=true] - attempt to parse returned data as JSON
     */
    HGET(payload) {
        //table, key, cb, parseJson = true
        return new Promise((resolve, reject) => {
            // if (!key || !table)
            // return typeof cb === "function" ? cb(null, null) : null;
            let key = payload.key;

            if (mongoose.Types.ObjectId.isValid(key)) key = key.toString();

            this.client.hget(payload.table, key, (err, value) => {
                if (err) return reject(new Error(err));
                try {
                    value = JSON.parse(value);
                } catch (e) {}
                resolve(value);
            });
        });
    }

    /**
     * Deletes a single value from a table
     *
     * @param {String} table - name of the table to delete the value from (table === redis hash)
     * @param {String} key - name of the key to delete
     * @param {Function} cb - gets called when the value has been deleted from Redis or when it returned an error
     */
    HDEL(payload) {
        //table, key, cb
        return new Promise((resolve, reject) => {
            // if (!payload.key || !table || typeof key !== "string")
            // return cb(null, null);

            let key = payload.key;

            if (mongoose.Types.ObjectId.isValid(key)) key = key.toString();

            this.client.hdel(payload.table, key, (err) => {
                if (err) return reject(new Error(err));
                else return resolve();
            });
        });
    }

    /**
     * Returns all the keys for a table
     *
     * @param {String} table - name of the table to get the values from (table === redis hash)
     * @param {Function} cb - gets called when the values are returned from Redis
     * @param {Boolean} [parseJson=true] - attempts to parse all values as JSON by default
     */
    HGETALL(payload) {
        //table, cb, parseJson = true
        return new Promise((resolve, reject) => {
            this.client.hgetall(payload.table, (err, obj) => {
                if (err) return reject(new Error(err));
                if (obj)
                    Object.keys(obj).forEach((key) => {
                        try {
                            obj[key] = JSON.parse(obj[key]);
                        } catch (e) {}
                    });
                else if (!obj) obj = [];
                resolve(obj);
            });
        });
    }

    /**
     * Publish a message to a channel, caches the redis client connection
     *
     * @param {String} channel - the name of the channel we want to publish a message to
     * @param {*} value - the value we want to send
     * @param {Boolean} [stringifyJson=true] - stringify 'value' if it's an Object or Array
     */
    PUB(payload) {
        //channel, value, stringifyJson = true
        return new Promise((resolve, reject) => {
            /*if (pubs[channel] === undefined) {
            pubs[channel] = redis.createClient({ url: this.url });
            pubs[channel].on('error', (err) => console.error);
            }*/

            let value = payload.value;

            if (["object", "array"].includes(typeof value))
                value = JSON.stringify(value);

            //pubs[channel].publish(channel, value);
            this.client.publish(payload.channel, value);

            resolve();
        });
    }

    /**
     * Subscribe to a channel, caches the redis client connection
     *
     * @param {String} channel - name of the channel to subscribe to
     * @param {Function} cb - gets called when a message is received
     * @param {Boolean} [parseJson=true] - parse the message as JSON
     */
    SUB(payload) {
        //channel, cb, parseJson = true
        return new Promise((resolve, reject) => {
            if (subs[payload.channel] === undefined) {
                subs[payload.channel] = {
                    client: redis.createClient({
                        url: this.url,
                        password: this.password,
                    }),
                    cbs: [],
                };
                subs[payload.channel].client.on(
                    "message",
                    (channel, message) => {
                        try {
                            message = JSON.parse(message);
                        } catch (e) {}
                        subs[channel].cbs.forEach((cb) => cb(message));
                    }
                );
                subs[payload.channel].client.subscribe(payload.channel);
            }

            subs[payload.channel].cbs.push(payload.cb);

            resolve();
        });
    }

    GET_SCHEMA(payload) {
        return new Promise((resolve, reject) => {
            resolve(this.schemas[payload.schemaName]);
        });
    }
}

module.exports = new CacheModule();