
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")
    ,httpsRequest = require('./httpsRequest.js').httpsRequest
;

const SLACK_ENDPOINT = "https://slack.com/api/"
    ,SLACK_HOSTNAME = "slack.com"
    ,SLACK_ENDPOINT_PATH = "/api/"
    ,GETAPI = {
        rtmStart: "rtm.start"
        ,oauth: "oauth.access"
        ,identityEmail: "users.identity"
        ,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"
        ,sendAction: "chat.attachmentAction"
        ,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
;

/**
 * @implements {ChatSystem}
**/
function Slack(sess, manager) {
    this.token = sess.slackToken;
    this.sessId = sess.id;
    this.manager = manager;
    this.rtm = null;
    this.rtmId = 1;
    this.data = new SlackData(this);
    this.history = {};
    this.pendingRtm = {};
    this.pendingMessages = [];
    this.pendingPing = false;
    this.connected = false;
    this.closing = false;
}

Slack.prototype.getId = function() {
    return this.data.team ? this.data.team.id : null;
};

Slack.prototype.onRequest = function() {
    if (this.connected === false) {
        this.connect();
    }
};

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

    this.connected = undefined;
    httpsRequest(SLACK_ENDPOINT +GETAPI.rtmStart +"?token=" +this.token, (status, body) => {
        if (!body || !body.ok) {
            _this.error = "Slack API error";
            _this.connected = false;
            console.error("Slack api responded " +status +" with body " +JSON.stringify(body));
            cb && cb(_this);
        } else if (status !== 200) {
            _this.error = body.error;
            _this.connected = false;
            console.error("Slack api responded " +status);
            cb && cb(_this);
        } else {
            _this.data.updateStatic({
                team: body["team"],
                users: body["users"],
                bots: body["bots"],
                self: body["self"]
            }, Date.now());
            _this.connectRtm(body.url);
        }
    });
};

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.remoteId
        +"&text=" +arg);
}

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

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.poll = function(knownVersion, now) {
    if (this.connected) {
        var updatedCtx = this.data.getUpdates(knownVersion)
            ,updatedTyping = this.data.getWhoIsTyping(now)
            ,updatedLive = this.getLiveUpdates(knownVersion);

        if (updatedCtx || updatedLive || updatedTyping) {
            return {
                "static": updatedCtx,
                "live": updatedLive,
                "typing": updatedTyping,
                "v": Math.max(this.data.liveV, this.data.staticV)
            };
        }
    }
};

/** @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.unstackPendingMessages = function() {
    for (var i = this.pendingMessages.length -1; i >= 0; i--) {
        this.onMessage(this.pendingMessages[0], Date.now());
        this.pendingMessages.unshift();
    }
};

Slack.prototype.onMessage = function(msg, t) {
    if (msg["reply_to"] && this.pendingRtm[msg["reply_to"]]) {
        var ts = msg["ts"]
            ,rtmId = msg["reply_to"];
        msg = this.pendingRtm[rtmId];
        msg["ts"] = ts;
        delete this.pendingRtm[rtmId];
    }
    if (msg["type"] === "hello" && msg["start"] && msg["start"]["rtm_start"]) {
        var _this = this;
        _this.getEmojis((emojis) => {
            _this.getSlashCommands((commands) => {
                var msgContent = msg.start.rtm_start;
                msgContent.self = msg.self;
                msgContent.emojis = emojis;
                msgContent.commands = commands;

                _this.data.updateStatic(msgContent, Date.now());
                _this.unstackPendingMessages();
                _this.connected = true;
                _this.unstackPendingMessages();
            });
        });
    } else if (this.connected) {
        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 = this.data.team.id +'|' +(msg["channel"] || msg["channel_id"] || msg["item"]["channel"])
                ,channel = this.data.channels[channelId]
                ,histo = this.history[channelId];
            // FIXME remove typing for user
            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(channel);
            }
        }
    } else {
        this.pendingMessages.push(msg);
    }
};

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

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

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

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

    this.rtmId = 1;
    var protocol = url.substr(0, url.indexOf('://') +3);
    url = url.substr(protocol.length);
    url = protocol +url.substr(0, url.indexOf('/'))+
        "/?flannel=1&token=" +this.token+
        "&start_args="+
        encodeURIComponent("?simple_latest=true&presence_sub=true&mpim_aware=false&canonical_avatars=true")
    this.rtm = new WebSocket(url);
    this.rtm.on("message", function(msg) {
        if (!_this.connected && cb) {
            cb();
        }
        try {
            msg = JSON.parse(msg);
        } catch (ex) {
            console.error("WTF invalid JSON ", msg);
        }
        _this.onMessage(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.rtmPing = function() {
    if (this.connected) {
        if (this.pendingPing && this.pendingRtm[this.pendingPing]) {
            //FIXME timeout
            console.error("Ping timeout");
        } else {
            var rtmId = this.rtmId++;
            this.pendingRtm[rtmId] = { type: 'ping' };
            this.pendingPing = rtmId;
            this.rtm.send('{"id":' +rtmId +',"type":"ping"}');
        }
    }
};

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

Slack.getUserId = function(code, redirectUri, cb) {
    Slack.getOauthToken(code, redirectUri, (token) => {
        if (token) {
            httpsRequest(SLACK_ENDPOINT+GETAPI.identityEmail +"?token="+token,
            (status, resp) => {
                if (status === 200 && resp.ok && resp.user && resp.user.email) {
                    cb(resp.user.id +'_' +resp.team.id);
                } else {
                    cb(null);
                }
            });
        } else {
            cb(null);
        }
    });
};

Slack.getOauthToken = function(code, redirectUri, cb) {
    httpsRequest(SLACK_ENDPOINT+GETAPI.oauth
        +"?client_id=" +config.services.Slack.clientId
        +"&client_secret=" +config.services.Slack.clientSecret
        +"&redirect_uri=" +redirectUri
        +"&code=" +code,
    (status, resp) => {
        if (status === 200 && resp.ok) {
            cb(resp["access_token"]);
        } 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.remoteId
        ,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;
};

function findBoundary() {
    const prefix = '-'.repeat(15)
        ,alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
        ,doYouKnowWhoManyTheyAre = alphabet.length // 26 letters in da alphabet
        ,nbArg = arguments.length;

    for (let i =0; i < doYouKnowWhoManyTheyAre; i++)
        bLoop: for (let j =0; j < doYouKnowWhoManyTheyAre; j++) {
            const boundary = prefix +alphabet[i] +alphabet[j];
            for (let argIndex =0; argIndex < nbArg; argIndex++) {
                if (arguments[argIndex].indexOf(boundary) >= 0)
                    continue bLoop;
            }
            return boundary;
        }
}

function encodeWithBoundary(boundary, data) {
    var resp = "";
    for (var k in data) {
        resp += '--' +boundary +'\r\n';
        resp += 'Content-Disposition: form-data; name="' +k +'"\r\n\r\n'
            +data[k]
            +'\r\n';
    };
    return resp +'--' +boundary +'--\r\n';
}

/**
 * @param {string} serviceId
 * @param {Object} payload
 * @param {function(string|null)=} callback
**/
Slack.prototype.sendAction = function(serviceId, payload, callback) {
    var channel = this.data.channels[payload["channel_id"]]
        ,service = this.data.users[serviceId];

    if (channel && service) {
        payload["channel_id"] = channel.remoteId;
        var payloadString = JSON.stringify(payload)
            ,boundary = findBoundary(service.remoteId, payloadString)
            ,body = encodeWithBoundary(boundary, {
                "service_id": service.remoteId
                ,"payload": payloadString
            });

        var req = https.request({
            hostname: SLACK_HOSTNAME
            ,method: 'POST'
            ,path: SLACK_ENDPOINT_PATH +GETAPI.sendAction +"?token=" +this.token
            ,headers: {
                "Content-Type": "multipart/form-data; boundary=" +boundary,
                "Content-Length": body.length
            }
        }, (res) => {
            if (callback) {
                var resp = [];
                res.on("data", (chunk) => { resp.push(chunk); });
                res.once("end", () => {
                    resp = Buffer.concat(resp).toString();
                    try {
                        resp = JSON.parse(resp);
                    } catch (e) {
                        resp = null;
                    }
                    callback(resp && resp.ok ? resp : false);
                });
            }
        });
        req.end(body);
        return true;
    }
    return false;
};

/**
 * @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.remoteId
        ,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.remoteId
        +"&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.remoteId
        +"&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.remoteId
        +"&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) {
    if (attachments) {
        attachments.forEach((attachmentObj) => {
            if (attachmentObj["ts"])
                attachmentObj["ts"] /= 1000;
        });
        httpsRequest(SLACK_ENDPOINT +GETAPI.postMsg
            +"?token=" +this.token
            +"&channel=" +channel.remoteId
            +"&text=" +text.join("\n")
            + (attachments ? ("&attachments=" +encodeURIComponent(JSON.stringify(attachments))) : "")
            +"&as_user=true");
    } else {
        var decodedText = [];
        text.forEach(function(i) {
            decodedText.push(decodeURIComponent(i));
        });
        var fullDecodedText = decodedText.join("\n");
        this.pendingRtm[this.rtmId] = {
            type: 'message',
            channel: channel.remoteId,
            user: this.data.self.remoteId,
            text: fullDecodedText
        };
        this.rtm.send('{"id":' +this.rtmId++ +',"type":"message","channel":"' +channel.remoteId +'", "text":' +JSON.stringify(fullDecodedText) +'}');
    }
};

/**
 * @param {SlackChan|SlackGroup|SlackIms} channel
 * @param {string} msgId
**/
Slack.prototype.removeMsg = function(channel, msgId) {
    httpsRequest(SLACK_ENDPOINT +GETAPI.removeMsg
        +"?token=" +this.token
        +"&channel=" +channel.remoteId
        +"&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.remoteId
        +"&ts=" +msgId
        +"&text=" +text.join("\n")
        +"&as_user=true");
};

/**
 * @param {SlackChan|SlackGroup|SlackIms} target
**/
Slack.prototype.fetchHistory = function(target) {
    var _this = this
        ,baseUrl = ""
        ,targetId = target.remoteId;

    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[target.id]
                ,now = Date.now();
            if (!history) {
                history = _this.history[target.id] = new SlackHistory(target.remoteId, target.id, this.data.team.id +'|', HISTORY_LENGTH);
                history.isNew = true;
            }
            var now = Date.now()
                ,lastTs = history.pushAll(resp.messages, now);

            if (lastTs)
                this.data.liveV = now;
            target.setLastMsg(lastTs, now);
        }
    });
};

Slack.prototype.getChatContext = function() {
    return this.data;
};

module.exports.Slack = Slack;

