// LICENSE_CODE ZON
'use strict'; /*jslint node:true, es6:false*/
require('../../util/config.js');
var outgoing_request = require('./outgoing.js');
var unblocker_lib = require('../pub/unblocker_lib.js');
var date = require('../../util/date.js');
var etask = require('../../util/etask.js');
var http_hdr = require('../../util/http_hdr.js');
var zurl = require('../../util/url.js');
var zerr = require('../../util/zerr.js');
var zutil = require('../../util/util.js');
var E = module.exports = incoming_request;

var HARD_TIMEOUT = 5*date.ms.MIN;
var SOFT_TIMEOUT_DEFAULT = 500;
var SOFT_TIMEOUT_FACTOR = 2;

function guess_type(accept){
    if (/^text\/html,/.test(accept))
        return 'main_frame';
    if (/^text\/css,/.test(accept))
        return 'stylesheet';
    if (/^image\//.test(accept))
        return 'image';
}

function incoming_request(smart_proxy, id, req, res){
    this.smart_proxy = smart_proxy;
    this.id = id;
    this.req = req;
    this.res = res;
    this.rdr_info = req.socket.zsvc_rdr_info;
    this.is_proxy_req = false;
    var url = req.url, m;
    if (this.rdr_info)
    {
        if (url.startsWith('/'))
            url = 'http://'+(req.headers.host||this.rdr_info.ip)+url;
    }
    else
    {
        m = url.match(/^\/(\w+)\/(.+)$/);
        if (m)
            url = m[2];
        else if (url.startsWith('/'))
        {
            // probably a request for /favicon.ico
            var e = new Error('Not found');
            e.code = 404;
            throw e;
        }
        else
            this.is_proxy_req = true;
    }
    this.url = zurl.parse(url, true);
    this.outgoing = {};
    this.serving = null;
    this.strategy = unblocker_lib.handle_request(this.url, {
        top_url: req.headers.referer,
        method: req.method,
        country: smart_proxy.country,
        type: guess_type(req.headers.accept),
        force: m && m[1],
    });
    this.sp = null;
}

E.prototype.run = function(){
    var _this = this;
    return this.sp = etask('incoming_request.run', [function(){
        _this.res.on('close', function(){
            _this.zerr(zerr.L.WARN, 'closed by client');
            _this.finalize(true);
        });
        _this.zerr(zerr.L.INFO, '> ('+_this.strategy.desc+') '
            +_this.smart_proxy.country+'/'
            +(_this.rdr_info ? 'app' : _this.is_proxy_req ? 'proxy' : 'media')
            +' '+_this.req.method+' '+_this.url.orig);
    }, function loop(){
        var cmd = _this.strategy(_this.outgoing.direct, _this.outgoing.proxy)
            || {};
        if (cmd.log)
            _this.zerr(zerr.L.INFO, cmd.log);
        ['direct', 'proxy'].forEach(function(route){
            var subcmd = cmd[route];
            if (!subcmd)
                return;
            var outgoing = _this.outgoing[route];
            if (subcmd.abort && outgoing)
            {
                outgoing.abort();
                outgoing = _this.outgoing[route] = null;
            }
            if (subcmd.start)
            {
                if (outgoing)
                    outgoing.abort();
                outgoing = _this.forward(route, subcmd);
            }
            if (subcmd.serve && !_this.serving)
                _this.serve(outgoing);
        });
        return _this.is_done() ? this.return() : this.wait();
    }, function(){
        return this.goto('loop');
    }, function catch$(err){
        _this.finalize(true);
        _this.zerr(zerr.L.ERR, err);
        if (!_this.res.headersSent)
        {
            _this.res.writeHead(err.code || 502);
            _this.res.write(err.toString());
        }
        _this.res.end();
    }, function finally$(){
        _this.finalize(false);
    }]);
};

E.prototype.get_headers = function(){
    var headers = {};
    for (var h in this.req.headers)
    {
        if (h.startsWith('x-hola-'))
            continue;
        if (this.is_proxy_req && h.startsWith('proxy-'))
            continue;
        headers[h] = this.req.headers[h];
    }
    return headers;
};

E.prototype.forward = function(route, opt){
    opt = zutil.clone(opt);
    opt.hard_timeout = HARD_TIMEOUT;
    if (opt.timeout)
    {
        var average = this.smart_proxy.get_avg_proxy_time();
        opt.soft_timeout = average ?
            Math.ceil(average*SOFT_TIMEOUT_FACTOR) : SOFT_TIMEOUT_DEFAULT;
        if (opt.soft_timeout>=opt.hard_timeout)
            opt.soft_timeout = null;
    }
    var outgoing = this.outgoing[route] =
        new outgoing_request(this, route, opt);
    outgoing.run();
    return outgoing;
};

E.prototype.serve = function(outgoing){
    if (outgoing.error)
        throw outgoing.error;
    this.serving = outgoing.route;
    if (this.is_proxy_req && outgoing.route=='proxy' &&
        /^(audio|video)\//.test(outgoing.type))
    {
        // workaround: WebView downloads media using a centralized service
        // which isn't affected by our fake proxy settings
        var target = 'http://127.0.0.1:'+this.smart_proxy.port
            +(outgoing.peer ? '/peer/' : '/proxy/')+this.url.orig;
        this.zerr(zerr.L.INFO, '< redirect 302 '+target);
        this.res.statusCode = 302;
        this.res.setHeader('location', target);
        this.res.end();
        outgoing.abort();
        return;
    }
    var msg = '< serving '+outgoing.route+' '+outgoing.code;
    if (outgoing.type)
        msg += ' '+outgoing.type;
    if (outgoing.len)
        msg += ' len '+outgoing.len;
    if (outgoing.location)
        msg += ' -> '+outgoing.res.headers.location;
    this.zerr(zerr.L.INFO, msg);
    this.res.statusCode = outgoing.code;
    var headers = http_hdr.restore_case(outgoing.get_headers(),
        outgoing.res.rawHeaders);
    for (var h in headers)
        this.res.setHeader(h, headers[h]);
    outgoing.res.pipe(this.res);
    outgoing.complete();
};

E.prototype.is_done = function(){
    var _this = this;
    if (!_this.serving)
        return false;
    var res = true;
    ['direct', 'proxy'].forEach(function(route){
        var outgoing = _this.outgoing[route];
        if (!outgoing || outgoing.slow || outgoing.code || outgoing.error)
            return;
        if (outgoing.timeout)
            res = false;
    });
};

E.prototype.finalize = function(abort){
    var _this = this;
    ['direct', 'proxy'].forEach(function(route){
        var outgoing = _this.outgoing[route];
        if (!outgoing)
            return;
        if (abort || route!=_this.serving)
        {
            outgoing.abort();
            _this.outgoing[route] = null;
        }
    });
};

E.prototype.react = function(outgoing){
    if (outgoing.route=='proxy' && outgoing.code==200 && !outgoing.hdrs_only)
        this.smart_proxy.report_proxy_time(outgoing.end_ts-outgoing.start_ts);
    this.sp.continue();
};

E.prototype.zerr = function(level, msg){
    if (msg instanceof TypeError)
        msg = msg.stack;
    if (!/^[<>]/.test(msg))
        msg = ' '+msg;
    zerr._zerr(level, [this.id+'bp'+msg]);
};
