// Copyright PCGroup 2014
//
// XSP - Extend server script/page/proxy support R-System of pcgroups
//
// http://ip:port/s-100/hello.xsp?...&trans_enc=u2g
// http://ip:port/p-100/bbs.pcauto.com.cn/intf/_topics.jsp?...&xspt=topics.html
// http://ip:port/x-100-g2u/bbs.pcauto.com.cn/intf/topics.jsp?...
//               /1-2..-3../    1: type, 2: max-age, 3 trans_enc (2,3 optional)
//              
// Author: chen_xiao_hu@qq.com
//

var r     = require('./r.js');
var fs    = require('fs');
var http  = require('http');
var path  = require('path');
var iconv = require('iconv-lite');
var debug = require('debug')('xsp');
var hbs   = require('handlebars');
var domain = require('domain');
var Emitter  = require('events').EventEmitter;
var ResponseHolder = require('./util.js').ResponseHolder;
var removeHeaderAndTail = require('./util.js').removeHeaderAndTail;
var config = require('../conf/config.js').config();

var defaultEncoding = config.xsp.defaultEncoding || 'utf-8';

var DEBUG = process.env.DEBUG && (process.env.DEBUG != "undefined");

// init r module
r.setEnv(config.r.env);
if (config.r.routeOverwrite)            { r.setRouteOverwrite(config.r.routeOverwrite); }
if (config.r.defaultRequestTimeout)     { r.setDefaultRequestTimeout(config.r.defaultRequestTimeout); }
if (config.r.defaultResponseTimeout)    { r.setDefaultResponseTimeout(config.r.defaultResponseTimeout); }
if (config.r.failErrorCount)            { r.setFailErrorCount(config.r.failErrorCount); }
if (config.r.failDisableSeconds)        { r.setFailDisableSeconds(config.r.failDisableSeconds); }
r.init();

// load helper
if (config.xsp.helpers) {
    config.xsp.helpers.forEach(function(e) {
        require('../' + e);
    });
}

// for unique GET the same url in same time
var unique = {};
var uniqNo = 0;

function uniqReq(req) {
    // Only for cache able get url !!!
    //if (req.method === 'GET') {
    //    req.uniq = req.url;
    //    return;
    //}
    if (++uniqNo > 0xFFFFFFFF) {
        uniqNo = 0;
    }
    req.uniq = uniqNo;
}

// for security script path
var scriptPath = fs.realpathSync('.') + path.sep + 'scripts';

///////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////

// parse uri to object with attributes: type, maxAge, transEnc and targetUrl
// type      : x - porxy, p - page with template, s - udf script, ? - unknow
// maxAge    : max-age http header
// transEnc  : transfer char encoding, u2g|g2u, u is UTF-8, g is GBK
// targetUrl : targetUrl to request, when x|p targetUrl is the backend url,
//             when s, targetUrl is uri
//
function parseUri(uri) {
    // min uri /s/x.xsp length is 8
    if (uri.length < 8) {
        return {type: '?'};
    }
    // fetch and check type
    var type = uri.charAt(1);
    if (type != 'x' && type != 's' && type != 'p') {
        return {type: '?'};
    }

    // if type only
    if (uri.charAt(2) === '/') {
        if (type === 's') {
            return {type: type, targetUrl: uri.substring(3)};
        } else {
            return {type: type, targetUrl: 'http://' + uri.substring(3)};
        }
    }

    // more than one items
    if (uri.charAt(2) === '-') {
        var pos = uri.indexOf('/', 2);
        if (pos < 4) {
            console.log('Invalid uri: ' + uri);
            return {type: type};
        }
        var pos2 = uri.indexOf('-', 3);
        var maxAge;
        var transEnc;
        var targetUrl;
        if (pos2 < 3 || pos2 > pos) {
            // 2 items
            var ch = uri.charAt(3);
            if (ch === 'g' || ch === 'u') {
                transEnc = uri.substring(3, pos);
            } else {
                maxAge = parseInt(uri.substring(3, pos));
            }
        } else {
            // 3 items 
            maxAge = parseInt(uri.substring(3, pos2));
            transEnc = uri.substring(pos2 + 1, pos);
        }
        if (type === 's') {
            targetUrl = uri.substring(pos + 1);
        } else {
            targetUrl = 'http://' + uri.substring(pos + 1);
        }
        return {type: type, maxAge: maxAge,
            transEnc: transEnc, targetUrl: targetUrl};
    } else {
        console.log('Invalid uri: ' + uri);
        return {type: type};
    }

}

// parse uri to object with attributes: script and queryString
// script is the filename replace ext name .xsp to .js, and then add the dir
//          ./scripts/ before the filename
// queryString is query string with uri
//
function parseScript(uri) {
    var pos = uri.indexOf('.xsp');
    var script = 'scripts/' + uri.substring(0, pos) + '.js';
    var realScript = fs.realpathSync(script);
    if (realScript.indexOf(scriptPath) !== 0) {
        throw 'Not allow access other path!';
    }
    var queryString = uri.substring(pos + 5);
    return {script: realScript, queryString: queryString};
}

// parse uri to get template name
//
function getTName(uri) {
    var pos = uri.indexOf('xspt=');
    if (pos < 1) {
        throw 'template name must give with parameter xspt= !!!';
    }
    return uri.substring(pos + 5);
}

// get cached template by template name, cache 1 minutes.
//
var templateMap = {};
function getTemplate(name) {
    var holder = templateMap[name];
    if (!holder) {
        holder = ['', 0];
        templateMap[name] = holder;
    }
    var now = new Date().getTime();
    if (now - holder[1] < 60000) {
        if (DEBUG) debug('cache hit for template: ', name);
        return holder[0];
    } else {
        if (DEBUG) debug('cache missed for template: ', name);
    }

    try {
        holder[0] = hbs.compile(fs.readFileSync('./templates/' +
        name).toString());
    } catch (e) {
        if (DEBUG) debug(e);
        return null;
    }
    holder[1] = now;
    return holder[0];
}

// transfer Content-Type header to assigned charset, add or replace
//
function transferCharset(contentType, encoding) {
    var pos = contentType.indexOf('charset=');
    if (pos > -1) {
        return contentType.substring(0, pos) + 'charset=' + encoding;
    } else {
        return contentType + '; charset=' + encoding;
    }
}

// Get content type from template ext name and encoding
//
function getContentType(template, enc) {
    var l = template.length;
    if (template.indexOf('.json') == l - 5) {
        return 'Content-Type: application/json; charset=' + enc;
    }
    return 'text/html; charset=' + enc;
}

// Write response for plain text messate
//
function writeResponse(res, sc, msg) {
    res.setHeader('Content-Type', 'text/plain');
    var body = msg;
    if (! Buffer.isBuffer(body)) {
        body = iconv.encode(msg, defaultEncoding);
    }
    res.setHeader('Content-Length', body.length);
    res.statusCode = sc;
    res.end(msg);
}


// Process the PROXY requst
//
function doProxy(req, res, info) {

    var onOk = function(err, resp) {
        if (err) {
            writeResponse(res, 500, '');
            console.error(err);
            return;
        }
        res.statusCode = resp.statusCode;
        if (info.maxAge) {
            res.setHeader('Cache-Control', 'max-age=' + info.maxAge);
        }

        if (! info.transEnc) {
            res.setHeader('Content-Type',resp.headers['content-type']);
            if (resp.body && resp.body.length) {
                res.setHeader('Content-Length', resp.body.length);
            }
            res.end(resp.body);
        } else {
            var fromEnc, toEnc;
            if (info.transEnc === 'u2g') {
                fromEnc = 'UTF-8';
                toEnc   = 'GBK';
            } else if (info.transEnc === 'g2u') {
                fromEnc = 'GBK';
                toEnc   = 'UTF-8';
            }
            res.setHeader('Content-Type', transferCharset
            (resp.headers['content-type'], toEnc));
            var bb = iconv.encode(iconv.decode(resp.body, fromEnc), toEnc);
            if (bb && bb.length) {
                res.setHeader('Content-Length', bb.length);
            }
            res.end(bb);
        }
    };


    var emitter = unique[req.uniq];
    if (! emitter) {
        emitter = new Emitter();
        emitter.setMaxListeners(0);
        unique[req.uniq] = emitter;

        emitter.once('ok', onOk);

        // http proxy to backend
        if (req.method === 'POST') {
            r.post(info.targetUrl,

                // NOTE: maybe need transfer more headers like cookie etc.
                {headers : r.transHeaders(['Cookie', 'Referer', 'Content-Type',
                    'Content-Length'], req.headers)},
                req.body,
                function(err, resp) {
                    emitter.emit('ok', err, resp);
                    delete unique[req.uniq];
                });
        } else {
            r.get(info.targetUrl,

                // NOTE: maybe need transfer more headers like cookie etc.
                {headers : r.transHeaders(['Cookie', 'Referer'], req.headers)},

                function(err, resp) {
                    emitter.emit('ok', err, resp);
                    delete unique[req.uniq];
                });
        }
    } else {
        emitter.once('ok', onOk);
    }

}

// Process the PAGE request
//
function doPage(req, res, info) {

    var tName = getTName(info.targetUrl);
    var template = getTemplate(tName);
    if (!template) {
        writeResponse(res, 404, 'Template not found! [' + tName + ']');
        return;
    }

    var onOk = function(err, resp) {
        if (err) {
            writeResponse(res, 500, '');
            console.error(err);
            return;
        }

        res.statusCode = resp.statusCode;
        if (info.maxAge) {
            res.setHeader('Cache-Control', 'max-age=' + info.maxAge);
        }

        var fromEnc, toEnc;
        if (info.transEnc === 'u2g') {
            fromEnc = 'UTF-8';
            toEnc   = 'GBK';
        } else if (info.transEnc === 'g2u') {
            fromEnc = 'GBK';
            toEnc   = 'UTF-8';
        } else if (info.transEnc === 'g2g') {
            fromEnc = 'GBK';
            toEnc   = 'GBK';
        } else if (!info.transEnc || info.transEnc === 'u2u') {
            fromEnc = 'UTF-8';
            toEnc   = 'UTF-8';
        }
        try {
            var json = JSON.parse(iconv.decode(resp.body, fromEnc));
            var result = template(json);
            var bb = iconv.encode(result, toEnc);
            res.setHeader('Content-Type', getContentType(tName, toEnc));
            res.setHeader('Content-Length', bb.length);
            res.end(bb);
        } catch (e) {
            writeResponse(res, 500, 'Server error!');
            if (DEBUG) console.log(e.stack);
        }
    };

    var emitter = unique[req.uniq];
    if (! emitter) {
        emitter = new Emitter();
        emitter.setMaxListeners(0);
        unique[req.uniq] = emitter;

        emitter.once('ok', onOk);

        // backend result with template in dir ./templates
        r.get(info.targetUrl,

            // NOTE: maybe need transfer more headers like cookie etc.
            {headers : r.transHeaders(['Cookie', 'Referer'], req.headers)},

            function(err, resp) {
                emitter.emit('ok', err, resp);
                delete unique[req.uniq];
            });
    } else {
        emitter.once('ok', onOk);
    }

}

// Process User Define Script request
// user define script in ./scripts
//
function doScript(req, res, info) {
    try {
        var scriptInfo = parseScript(info.targetUrl);
        var script = require(scriptInfo.script);

        var onOk = function(err, responseHolder) {
            if (err) {
                writeResponse(res, 500, err.message);
                console.log(err.stack);
                return;
            }
            try {

                var fromEnc, toEnc;
                if (info.transEnc === 'u2g') {
                    fromEnc = 'UTF-8';
                    toEnc   = 'GBK';
                } else if (info.transEnc === 'g2u') {
                    fromEnc = 'GBK';
                    toEnc   = 'UTF-8';
                } else if (info.transEnc === 'g2g') {
                    fromEnc = 'GBK';
                    toEnc   = 'GBK';
                } else if (!info.transEnc || info.transEnc === 'u2u') {
                    fromEnc = 'UTF-8';
                    toEnc   = 'UTF-8';
                }
                // 去掉微信页的页头页尾
                if(req.headers && req.headers['user-agent'].indexOf('MicroMessenger') >0){
                  responseHolder.body=removeHeaderAndTail(responseHolder.body);  
                }
                responseHolder.body=iconv.encode(responseHolder.body, fromEnc);

                responseHolder.writeTo(res);
            } catch (err2) {
                writeResponse(res, 500, err2.message);
                console.log(err.stack);
                return;
            }
        };

        var emitter = unique[req.uniq];
        if (! emitter) {
            emitter = new Emitter();
            emitter.setMaxListeners(0);
            unique[req.uniq] = emitter;

            emitter.once('ok', onOk);  // must set before handle for sync call
            script.handle(req, new ResponseHolder(defaultEncoding),
                info.maxAge, info.transEnc, scriptInfo.queryString,
                function(err, rh) {
                    emitter.emit('ok', err, rh);
                    delete unique[req.uniq];
                });
        } else {
            emitter.once('ok', onOk);
        }


    } catch (err) {
        if (err.message &&
            err.message.indexOf('Cannot find module') === 0) {
            writeResponse(res, 404, 'File not found!');
            if (DEBUG) debug(err.message);
        } else {
            if (DEBUG) console.log(err.stack);
            writeResponse(res, 500, err.message);
        }
    }
}

exports = module.exports = function core() {

    return function xspCore(req, res) {
        var d = domain.create();
        uniqReq(req);
        d.add(req);
        d.add(res);
        d.on('error', function(er) {
            console.log('error url:'+ req.url);
            console.log(er.stack);
            var emitter = unique[req.uniq];
            if (emitter) {
                emitter.emit('ok', er);
                delete unique[req.uniq];
            }
        });


        d.run(
            function () {
                var info = parseUri(req.url);
                if (!info.targetUrl) {
                    writeResponse(res, 404, '');
                    return;
                }

                if (req.method === 'POST') {
                    var chunks = [];
                    var length = 0;
                    req.on('data', function (chunk) {
                        length += chunk.length;
                        chunks.push(chunk);
                    }).on('end', function () {
                        var data = new Buffer(length);
                        var pos = 0;
                        var l = chunks.length;
                        for (var i = 0; i < l; i++) {
                            chunks[i].copy(data, pos);
                            pos += chunks[i].length;
                        }
                        req.body = data;
                        if (info.type === 'x') {
                            doProxy(req, res, info);
                        } else if (info.type === 'p') {
                            doPage(req, res, info);
                        } else if (info.type === 's') {
                            doScript(req, res, info);
                        }
                    });
                } else {
                    if (info.type === 'x') {
                        doProxy(req, res, info);
                    } else if (info.type === 'p') {
                        doPage(req, res, info);
                    } else if (info.type === 's') {
                        doScript(req, res, info);
                    }
                }
            }
        );
    };
};


