// LICENSE_CODE ZON
'use strict'; /*jslint node:true, es6:false*/
require('../../util/config.js');
var agent_proxy = require('./agent_proxy.js');
var unblocker_lib = require('../pub/unblocker_lib.js');
var incoming_request = require('./incoming.js');
var etask = require('../../util/etask.js');
var date = require('../../util/date.js');
var ztls = require('../../util/tls.js');
var stats = require('../../util/stats.js');
var zurl = require('../../util/url.js');
var zerr = require('../../util/zerr.js');
var http = require('http');
var net = require('net');
var E = exports;

var smart_proxies = {};
var last_id = 0;

function smart_proxy(country){
    this.country = country;
    this.server = http.createServer();
    // HttpServer.prototype.listen is asynchronous, but only because it tries
    // DNS resolution on its address argument. We want to keep this sequence
    // synchronous, so using this undocumented Node internal here.
    this.server._listen2('127.0.0.1', undefined, -1, -1);
    var addr = {};
    this.server._handle.getsockname(addr);
    this.port = addr.port;
    zerr.notice('Opened smart proxy port '+this.port+' for '+this.country);
    this.proxy_time_series = new stats.ewma_series(date.ms.MIN);
    this.agent_proxy = new agent_proxy(this.country);
    this.agent_proxy.use();
    this.peer_proxy = new agent_proxy(this.country+'.peer');
    this.peer_proxy.use();
    this.server.on('request', this.onrequest.bind(this));
    this.server.on('connect', this.onconnect.bind(this));
}

E.get = function(country){
    country = country.toUpperCase();
    var proxy = smart_proxies[country];
    if (!proxy)
        smart_proxies[country] = proxy = new smart_proxy(country);
    return proxy;
};

E.purge_stats = function(){
    for (var country in smart_proxies)
        smart_proxies[country].purge_stats();
};

smart_proxy.prototype.onrequest = function(req, res){
    var id = ++last_id;
    try {
        var incoming = new incoming_request(this, id, req, res);
        incoming.run();
    } catch(e){
        zerr.err(id+'bp> '+(e instanceof TypeError ? e.stack : e));
        res.statusCode = e.code || 502;
        res.end(e.toString());
    }
};

smart_proxy.prototype.onconnect = function(req, socket){
    var _this = this, __this;
    var client, client_socket;
    var id = ++last_id;
    var rdr_info = socket.zsvc_rdr_info;
    var url = zurl.parse('https://'
        +(rdr_info ? rdr_info.ip+':'+rdr_info.port : req.url), true);
    var strategy = unblocker_lib.handle_request(url, {method: 'CONNECT',
        country: this.country});
    var proxy, proxy_subreq, remote_role, tls_handshake;
    return etask('smart_proxy.onconnect', [function(){
        __this = this;
        if (!req)
        {
            socket.once('data', this.continue_fn());
            return this.wait();
        }
    }, function handshake(packet){
        if (packet)
        {
            socket.pause();
            if (!tls_handshake)
                tls_handshake = new ztls.Handshake();
            var res = tls_handshake.extract_sni(packet);
            switch (res.status)
            {
            case 'truncated':
                socket.once('data', this.goto_fn('handshake'));
                return this.wait();
            case 'ok':
                url = zurl.parse('https://'+res.hostname+':'+rdr_info.port,
                    true);
                break;
            case 'err':
                zerr.warn(id+'bp> TLS handshake parse error: '+res.message);
                break;
            }
        }
        zerr.info(id+'bp> ('+strategy.desc+') '+_this.country+'/'
            +(rdr_info ? 'app' : 'proxy')+' CONNECT '+url.host);
    }, function loop(){
        var cmd = strategy(null, proxy_subreq)||{};
        proxy = cmd.proxy && cmd.proxy.start &&
            (cmd.proxy.peer ? _this.peer_proxy : _this.agent_proxy);
        remote_role = proxy ? 'a' : 'w';
        if (proxy)
            return proxy.use();
    }, function(){
        if (proxy || rdr_info && rdr_info.port==80)
        {
            zerr.info(id+'p'+remote_role+'> '
                +(proxy ? 'via '+proxy.server+':'+proxy.port : 'direct'));
            client = http.request({
                host: proxy ? proxy.ip : rdr_info.ip,
                port: proxy ? proxy.port : rdr_info.port,
                method: 'CONNECT',
                path: url.host,
                headers: proxy ? {
                    'x-hola-version': agent_proxy.version_header,
                    'proxy-authorization': proxy.basic,
                } : {},
            });
            client.on('connect', function(res, _client_socket){
                client_socket = _client_socket;
                __this.continue(res);
            });
            client.on('response', this.continue_fn());
            client.on('error', this.throw_fn());
            client.end();
        }
        else
        {
            zerr.info(id+'p'+remote_role+'> direct');
            client_socket = net.connect(url.port,
                rdr_info ? rdr_info.ip : url.hostname,
                this.continue_fn());
            client_socket.on('error', this.throw_fn());
        }
        (req||socket).on('error', this.throw_fn());
        return this.wait();
    }, function(res){
        var policy = proxy && res && res.headers['x-hola-policy'];
        if (policy)
        {
            zerr.info(id+'p'+remote_role+'< '+res.statusCode+' '+policy);
            proxy_subreq = {code: res.code, policy: policy};
            return this.goto('loop');
        }
        if (!client_socket)
            throw new Error(res ? ''+res.code : 'Connection failed');
        zerr.info(id+'p'+remote_role+'< connected');
        if (req)
        {
            socket.write('HTTP/'+req.httpVersion
                +' 200 Connection Established\r\n\r\n');
        }
        if (tls_handshake)
        {
            client_socket.write(tls_handshake.client.buf);
            tls_handshake = null;
        }
        socket.pipe(client_socket);
        client_socket.pipe(socket);
        socket.on('error', this.throw_fn());
        client_socket.on('end', this.continue_fn());
        client_socket.on('error', this.throw_fn());
        return this.wait();
    }, function(){
        zerr.info(id+'p'+remote_role+' done');
    }, function catch$(err){
        zerr.err(id+'p'+remote_role+' '
            +(err instanceof TypeError ? err.stack : err));
        if (client)
            client.abort();
        if (client_socket)
            client_socket.destroy();
        socket.destroy();
    }]);
};

smart_proxy.prototype.intercept = function(socket){
    if (socket.zsvc_rdr_info.port==80)
        this.server.emit('connection', socket);
    else
        this.onconnect(null, socket);
};

smart_proxy.prototype.report_proxy_time = function(time){
    this.proxy_time_series.append(time, Date.now());
    zerr.info(this.country+' proxy response time '+time+'ms, smoothed average '
        +this.get_avg_proxy_time()+'ms');
};

smart_proxy.prototype.get_avg_proxy_time = function(){
    return this.proxy_time_series.get(); };

smart_proxy.prototype.purge_stats = function(){
    this.proxy_time_series.reset(); };
