
const
    WebSocket = require('ws'),
    https = require('https'),
    sleep = require("sleep").sleep

    ,SlackData = require("./slackData.js").SlackData
    ,SlackHistory = require("./slackHistory.js").SlackHistory
    ,config = require("../config.js")
;

const SLACK_ENDPOINT = "https://slack.com/api/"
    ,SLACK_HOSTNAME = "slack.com"
    ,SLACK_ENDPOINT_PATH = "/api/"
    ,GETAPI = {
        rtmStart: "rtm.start"
        ,oauth: "oauth.access"
        ,channelHistory: "channels.history"
        ,directHistory: "im.history"
        ,groupHistory: "groups.history"
        ,postMsg: "chat.postMessage"
        ,postMeMsg: "chat.meMessage"
        ,editMsg: "chat.update"
        ,removeMsg: "chat.delete"
        ,postFile: "files.upload"
        ,setActive: "users.setActive"
        ,emojiList: "emoji.list"
        ,slashList: "commands.list"
        ,slashExec: "chat.command"
        ,addReaction: "reactions.add"
        ,removeReaction: "reactions.remove"
        ,read: {
            group: "groups.mark"
            ,im: "im.mark"
            ,group: "mpim.mark"
            ,channel: "channels.mark"
        }
    }
    ,HISTORY_LENGTH = 35

    ,UPDATE_LIVE = [
        "message"
        ,"pin_added"
        ,"pin_removed"
        ,"reaction_added"
        ,"reaction_removed"
        ,"star_added"
        ,"star_removed"
    ] // Message type that affect live history
;

function Slack(sess, manager) {
    this.token = sess.slackToken;
    this.sessId = sess.id;
    this.manager = manager;
    this.rtm = null;
    this.data = new SlackData(this);
    this.history = {};
    this.connected = false;
    this.closing = false;
}

Slack.prototype.onRequest = function(knownVersion, cb) {
    this.lastCb = cb;
    if (this.connected === false) {
        this.connect(knownVersion);
    } else {
        this.waitForEvent(knownVersion);
    }
};

function httpsRequest(url, cb) {
    https.get(url, (res) => {
        if (res.statusCode !== 200) {
            cb(res.statusCode, null);
            return;
        }
        var body = null;
        res.on('data', (chunk) => {
            body = body ? Buffer.concat([body, chunk], body.length +chunk.length) : Buffer.from(chunk);
        });
        res.on('end', () => {
            try {
                body = JSON.parse(body.toString("utf8"));
            } catch (e) {}
            cb && cb(res.statusCode, body);
        });
    });
}

Slack.prototype.connect = function(knownVersion) {
    var _this = this;

    this.connected = undefined;
    httpsRequest(SLACK_ENDPOINT +GETAPI.rtmStart +"?token=" +this.token, (status, body) => {
        if (status !== 200) {
            _this.error = body.error;
            _this.connected = false;
            console.error("Slack api responded " +status);
            _this.lastCb(_this);
            return;
        }
        if (!body) {
            _this.error = "Slack API error";
            _this.connected = false;
            _this.lastCb(_this);
            return;
        }
        if (!body.ok) {
            _this.error = body.error;
            _this.connected = false;
            console.error("Slack api responded !ok with ", body);
            _this.lastCb(_this);
            return;
        }
        _this.getEmojis((emojis) => {
            _this.getSlashCommands((commands) => {
                body.emojis = emojis;
                body.commands = commands;
                _this.data.updateStatic(body, Date.now());
                _this.connectRtm(body.url);
                _this.waitForEvent(knownVersion);
            });
        });
    });
};

Slack.prototype.sendCommand = function(room, cmd, arg) {
    httpsRequest(
        SLACK_ENDPOINT
        +GETAPI.slashExec
        +"?token=" +this.token
        +"&command=" +encodeURIComponent(cmd.name)
        +"&disp=" +encodeURIComponent(cmd.name)
        +"&channel=" +room.id
        +"&text=" +arg);
}

Slack.prototype.sendTyping = function(room) {
    this.rtm.send('{"id":' +this.rtmId++ +',"type":"typing","channel":"' +room.id +'"}');
}

Slack.prototype.getSlashCommands = function(cb) {
    httpsRequest(SLACK_ENDPOINT +GETAPI.slashList +"?token=" +this.token, (status, body) => {
        if (!status || !body || !body.ok)
            cb(null);
        else
            cb(body.commands || {});
    });
};

Slack.prototype.getEmojis = function(cb) {
    httpsRequest(SLACK_ENDPOINT +GETAPI.emojiList +"?token=" +this.token, (status, body) => {
        if (!status || !body || !body.ok)
            cb(null);
        else
            cb(body.emoji || {});
    });
};

Slack.prototype.waitForEvent = function(knownVersion) {
    var tick = 0
        ,_this = this
        ,interval;

    interval = setInterval(() => {
        tick++;
        if (!_this.lastCb) {
            clearInterval(interval);
            return;
        }
        if (_this.connected) {
            var updatedCtx = _this.data.getUpdates(knownVersion, Date.now())
                ,updatedLive = _this.getLiveUpdates(knownVersion)
                ,updated;

            if (updatedCtx || updatedLive) {
                updated = {};
                updated["static"] = updatedCtx;
                updated["live"] = updatedLive;
                updated["v"] = Math.max(_this.data.liveV, _this.data.staticV);
            }
            if (updated) {
                clearInterval(interval);
                _this.lastCb(_this, updated);
                return;
            }
        }
        if (tick >= 55) { // < 1 minute timeout
            clearInterval(interval);
            _this.lastCb(_this, { v: knownVersion });
            return;
        }
    }, 1000);
};

/** @return {Object|undefined} */
Slack.prototype.getLiveUpdates = function(knownVersion) {
    var result = {};

    for (var roomId in this.history) {
        var history = this.history[roomId];
        if (history.isNew) {
            result[roomId] = history.toStatic(0);
            history.isNew = false;
        }
        else {
            var roomData = history.toStatic(knownVersion);
            if (roomData.length)
                result[roomId] = roomData;
        }
    }
    for (var roomId in result) {
        return result;
    }
    return undefined;
};

Slack.prototype.onMessage = function(msg, t) {
    this.data.onMessage(msg, t);
    if ((msg["channel"] || msg["channel_id"] || (msg["item"] && msg["item"]["channel"])) && msg["type"] && UPDATE_LIVE.indexOf(msg["type"]) !== -1) {
        var channelId = msg["channel"] || msg["channel_id"] || msg["item"]["channel"]
            ,channel = this.data.getChannel(msg["channel"])
            ,histo = this.history[channelId];
        if (histo) {
            var lastMsg = histo.push(msg, t);
            if (lastMsg)
                this.data.liveV = t;
            histo.resort();
            if (channel)
                channel.setLastMsg(lastMsg, t);
        } else if (channel) {
            this.fetchHistory(msg["channel"]);
        }
    }
};

/**
 * @param {SlackChan|SlackGroup|SlackIms} chan
 * @param {number} ts
**/
Slack.prototype.markRead = function(chan, ts) {
    var apiURI;

    if (chan.id[0] === 'C')
        apiURI = SLACK_ENDPOINT+GETAPI.read.channel;
    else if (chan.id[0] === 'G')
        apiURI = SLACK_ENDPOINT+GETAPI.read.group;
    else if (chan.id[0] === 'D')
        apiURI = SLACK_ENDPOINT+GETAPI.read.im;

    httpsRequest(apiURI
        +"?token=" +this.token
        +"&channel="+chan.id
        +"&ts="+ts);
};

Slack.prototype.connectRtm = function(url, cb) {
    var _this = this;

    this.rtmId = 0;
    this.rtm = new WebSocket(url);
    this.rtm.on("message", function(msg) {
        if (!_this.connected && cb) {
            cb();
        }
        if (!_this.connected) {
            _this.connected = true;
        }
        _this.onMessage(JSON.parse(msg), Date.now());
    });
    this.rtm.once("error", function(e) {
        _this.connected = false;
        console.error(e);
        _this.close();
    });
    this.rtm.once("end", function() {
        console.error("RTM hang up");
        _this.onClose();
    });
};

Slack.prototype.onClose = function() {
    this.manager.suicide(this);
};

Slack.prototype.ping = function() {
    httpsRequest(SLACK_ENDPOINT+GETAPI.setActive
        +"?token=" +this.token);
};

Slack.prototype.close = function() {
    if (!this.closing) {
        this.closing = true;
        this.rtm.close();
        this.onClose();
    }
};

Slack.getOauthToken = function(code, redirectUri, cb) {
    httpsRequest(SLACK_ENDPOINT+GETAPI.oauth
        +"?client_id=" +config.clientId
        +"&client_secret=" +config.clientSecret
        +"&redirect_uri=" +redirectUri
        +"&code=" +code,
    (status, resp) => {
        if (status === 200 && resp.ok) {
            cb(resp);
        } else {
            cb(null);
        }
    });
};

/**
 * @param {SlackChan|SlackGroup|SlackIms} channel
 * @param {string} contentType
 * @param {function(string|null)} callback
**/
Slack.prototype.openUploadFileStream = function(channel, contentType, callback) {
    var req = https.request({
        hostname: SLACK_HOSTNAME
        ,method: 'POST'
        ,path: SLACK_ENDPOINT_PATH +GETAPI.postFile
            +"?token=" +this.token
            +"&channels=" +channel.id
        ,headers: {
            "Content-Type": contentType
        }
    }, (res) => {
        var errorJson;
        res.on("data", (chunk) => {
            errorJson = errorJson ? Buffer.concat([errorJson, chunk], errorJson.length +chunk.length) : Buffer.from(chunk);
        });
        res.once("end", () => {
            if (res.statusCode === 200) {
                callback(null);
            } else {
                try {
                    errorJson = JSON.parse(errorJson.toString());
                } catch(e) {
                    callback("error");
                    return;
                }
                callback(errorJson["error"] || "error");
            }
        });
    });
    return req;
};

/**
 * @param {SlackChan|SlackGroup|SlackIms} channel
 * @param {string} msgId
 * @param {string} reaction
**/
Slack.prototype.addReaction = function(channel, msgId, reaction) {
    httpsRequest(SLACK_ENDPOINT +GETAPI.addReaction
        +"?token=" +this.token
        +"&name=" +reaction
        +"&channel="+channel.id
        +"&timestamp="+msgId);
}

/**
 * @param {SlackChan|SlackGroup|SlackIms} channel
 * @param {string} msgId
 * @param {string} reaction
**/
Slack.prototype.removeReaction = function(channel, msgId, reaction) {
    httpsRequest(SLACK_ENDPOINT +GETAPI.removeReaction
        +"?token=" +this.token
        +"&name=" +reaction
        +"&channel="+channel.id
        +"&timestamp="+msgId);
}

/**
 * @param {SlackChan|SlackGroup|SlackIms} channel
 * @param {Array.<string>} text
**/
Slack.prototype.sendMeMsg = function(channel, text) {
    httpsRequest(SLACK_ENDPOINT +GETAPI.postMeMsg
        +"?token=" +this.token
        +"&channel=" +channel.id
        +"&text=" +text.join("\n")
        +"&as_user=true");
};

/**
 * @param {SlackChan|SlackGroup|SlackIms} channel
 * @param {Array.<string>} text
 * @param {Array.<Object>=} attachments
**/
Slack.prototype.sendMsg = function(channel, text, attachments) {
    httpsRequest(SLACK_ENDPOINT +GETAPI.postMsg
        +"?token=" +this.token
        +"&channel=" +channel.id
        +"&text=" +text.join("\n")
        + (attachments ? ("&attachments=" +encodeURIComponent(JSON.stringify(attachments))) : "")
        +"&as_user=true");
};

/**
 * @param {SlackChan|SlackGroup|SlackIms} channel
 * @param {string} msgId
**/
Slack.prototype.removeMsg = function(channel, msgId) {
    httpsRequest(SLACK_ENDPOINT +GETAPI.removeMsg
        +"?token=" +this.token
        +"&channel=" +channel.id
        +"&ts=" +msgId
        +"&as_user=true");
};

/**
 * @param {SlackChan|SlackGroup|SlackIms} channel
 * @param {string} msgId
 * @param {string} text
**/
Slack.prototype.editMsg = function(channel, msgId, text) {
    httpsRequest(SLACK_ENDPOINT +GETAPI.editMsg
        +"?token=" +this.token
        +"&channel=" +channel.id
        +"&ts=" +msgId
        +"&text=" +text.join("\n")
        +"&as_user=true");
};

Slack.prototype.fetchHistory = function(targetId) {
    var _this = this
        ,baseUrl = "";

    if (targetId[0] === 'D') {
        baseUrl = SLACK_ENDPOINT +GETAPI.directHistory;
    } else if (targetId[0] === 'C') {
        baseUrl = SLACK_ENDPOINT +GETAPI.channelHistory;
    } else if (targetId[0] === 'G') {
        baseUrl = SLACK_ENDPOINT +GETAPI.groupHistory;
    }
    httpsRequest(baseUrl
        +"?token="+this.token
        +"&channel=" +targetId
        +"&count=" +HISTORY_LENGTH,
    (status, resp) => {
        if (status === 200 && resp && resp.ok) {
            var history = _this.history[targetId]
                ,now = Date.now();
            if (!history) {
                history = _this.history[targetId] = new SlackHistory(targetId, HISTORY_LENGTH);
                history.isNew = true;
            }
            var now = Date.now();
            if (history.pushAll(resp.messages, now))
                this.data.liveV = now;
        }
    });
};

module.exports.Slack = Slack;

