// LICENSE_CODE ZON
'use strict'; /*zlint node:true*/
require('../../util/config.js');
const zconf = require('../../util/config.js');
const zutil = require('../../util/util.js');
const attrib = require('../../util/attrib.js');
const string = require('../../util/string.js');
const date = require('../../util/date.js');
const etask = require('../../util/etask.js');
const file = require('../../util/file.js');
const zerr = require('../../util/zerr.js');
const zws = require('../../util/ws.js');
const ws_stream = require('../../util/ws_stream.js');
const {Ws_demuxer, Ws_stream} = ws_stream;
const rand_uniform = require('../../util/rand_uniform.js');
const tls = require('tls');
const dns = require('dns');
const net = require('net');
const os = require('os');
const child_process = require('child_process');
const _ = require('underscore');
const https = require('https');
const E = exports;
const qw = string.qw, ms = date.ms, env = process.env;
let started, proxyjs_conn, cid, apkid, appid, confdir, gw_ip, idle = false;
let android_ver, mobile_enable = 0, usage = {}, conf;

const cmd_dns = msg=>etask(function*(){
    let res;
    if (!msg.domain)
        return;
    zerr.notice('cmd_dns '+msg.domain);
    try { res = yield etask.nfn_apply(dns, '.resolve4', [msg.domain]); }
    catch(e){ return {err: e.toString()}; }
    return {res};
});

let tun_vfd = 1;
let streams = new Map();
const cmd_tun = (msg, conn)=>etask(function*(){
    if (!msg.host || !msg.port)
        return;
    let timeline = [];
    let log_timeline = ev=>timeline.push({ev, ts: Date.now()});
    zerr.notice(`cmd_tun ${msg.host}:${msg.port} ${msg.debug||''}`);
    log_timeline('init');
    let host = msg.host;
    if (!net.isIPv4(host))
    {
        let addr;
        try {
            addr = yield etask.nfn_apply(dns, '.resolve4', [host]);
            log_timeline('dns');
            if (!addr || !addr.length)
                return {err: 'not found'};
            addr = rand_uniform.rand_subset(addr, addr.length);
            host = addr[0];
        } catch(e){
            zerr('resolve_err: '+zerr.e2s(e));
            return {err: e.toString()};
        }
    }
    let w = etask.wait();
    let sock = net.createConnection({port: msg.port, host});
    sock.on('error', e=>w.throw(e));
    sock.on('connect', ()=>w.continue());
    try { yield w; }
    catch(e){ return {err: 'timeout'}; }
    log_timeline('connect');
    let vfd = tun_vfd++;
    let stream = new Ws_stream({conn, vfd, demuxer: conn.demuxer});
    stream.on('error', e=>zerr('ws_stream err '+zerr.e2s(e)));
    stream.on('close', ()=>{
        zerr.info(`ws stream close ${vfd}`);
        streams.delete(vfd);
    });
    sock.on('close', ()=>{
        if (stream._closed)
            return;
        try { stream.emit('close'); }
        catch(e){ zerr('ws_stream on sock close err '+zerr.e2s(e)); }
        zws.msg_call(conn, 'tun_destroy', {vfd});
    });
    streams.set(vfd, {stream, sock});
    stream.pipe(sock).pipe(stream);
    return {vfd, timeline};
});

const cmd_tun_close = (msg, conn)=>{
    zerr.notice(`cmd_tun_close ${msg.vfd} ${msg.debug||''}`);
    let vfd = +msg.vfd;
    if (!vfd)
        return;
    let rec = streams.get(vfd);
    if (!rec)
        return {err: 'not found '+vfd};
    let {stream, sock} = rec;
    try { stream.emit('close'); }
    catch(e){ zerr('ws_stream close err '+zerr.e2s(e)); }
    stream._closed = true;
    // XXX vladimir: destroy?
    try { sock.end(); } catch(e){ zerr('cmd_tun sock end '+zerr.e2s(e)); }
    return {};
};

const cid_set = msg=>{
    zerr.notice(`cid_set ${msg.cid}`);
    cid = msg.cid;
};

E.set_apkid = _apkid=>{
    zerr.info(`apkid_set ${_apkid}`);
    apkid = _apkid;
};

E.set_appid = _appid=>{
    zerr.info(`appid_set ${_appid}`);
    appid = _appid;
};

E.set_confdir = _confdir=>{
    zerr.info(`set_confdir ${_confdir}`);
    confdir = _confdir;
};

E.set_idle = _idle=>{
    zerr.info(`set_idle ${idle}->${!!_idle}`);
    idle = !!_idle;
};

E.set_mobile_enable = _mobile_enable=>{
    zerr.info(`set_mobile_enable ${mobile_enable} -> ${_mobile_enable}`);
    mobile_enable = _mobile_enable;
};

E.zid_update = ()=>{
    zerr.info(`zid_update`);
};

E.usage_update = (app_bytes, total_bytes)=>{
    zerr.info(`usage_update ${app_bytes} ${total_bytes}`);
    usage.app_bytes = app_bytes;
    usage.total_bytes = total_bytes;
    // XXX vladimir: move to zjson and date()
    usage.ts = date.to_sql();
};

const status_get = ()=>{
    zerr.notice(`status_get`);
    return {idle, mobile_enable, usage};
};

const exec = cmd=>{
    try {
        let shell = !env.NODE_EMBEDDED && env.SHELL;
        // XXX vladimir: use exec.js instead of execSync
        return child_process.execSync(cmd,
            {shell: shell||'/system/bin/sh', stdio: 'pipe', encoding: 'utf8'});
    } catch(e){ return void zerr('exec err '+zerr.e2s(e)); }
};

const client_connect = msg=>etask(function*(){
    this.on('uncaught', e=>zerr('client_connect err '+zerr.e2s(e)));
    zerr.notice(`client_connect start to ${msg.host}:${msg.port}`);
    if (!msg.port || !msg.host)
        return;
    zerr.notice(`client_connect to ${msg.host}:${msg.port}`);
    let session = msg.session ? new Buffer(msg.session, 'base64') : null;
    let sock = tls.connect({host: msg.host, port: msg.port, session}, ()=>{
        zerr.notice(`client_connect established ${msg.host}:${msg.port}`);
        this.continue();
    });
    sock.on('error', this.throw_fn());
    yield this.wait();
    let cmd_conn = zws.fast_client_init(sock);
    cmd_conn.ev = cmd_conn;
    zws.ipc_init(cmd_conn, {});
    zws.msg_parse(cmd_conn);
    zws.ipc_handler(cmd_conn, 'dns', cmd_dns);
    zws.ipc_handler(cmd_conn, 'tun', cmd_tun);
    zws.ipc_handler(cmd_conn, 'tun_close', cmd_tun_close);
    zws.ipc_handler(cmd_conn, 'tunnel_init', tunnel_init);
    zws.ipc_handler(cmd_conn, 'status_get', status_get);
    cmd_conn.demuxer = new Ws_demuxer(cmd_conn);
    cmd_conn.demuxer.on('error', e=>zerr('ws_stream err: unexpected'+e));
});

const parse_ifconfig = ifconfig=>{
    let lines = ifconfig.split('\n'), ifs = [], cur;
    for (let l of lines)
    {
        if (/^\S/.test(l))
        {
            if (cur)
                ifs.push(cur);
            cur = {name: l.split(/\s/)[0]};
        }
        let m = l.match(/inet addr:(\S*)/);
        if (m)
            cur.addr = m[1];
    }
    if (cur)
        ifs.push(cur);
    return ifs;
};

const parse_netcfg = netcfg=>{
    let ifs = [], lines = netcfg.split('\n');
    for (let l of lines)
    {
        let parts = string.split_ws(l);
        if (parts.length<3)
            continue;
        if (parts[1]!='UP')
            continue;
        ifs.push({name: parts[0], addr: parts[2].split('/')[0]});
    }
    return ifs;
};

const parse_interfaces = ()=>{
    let ifs = os.networkInterfaces();
    let recs = [];
    for (let [conn, addresses] of Object.entries(ifs))
    {
        let rec = {name: conn};
        for (let addr of addresses)
        {
            if (addr.family!='IPv4' || addr.internal)
                continue;
            rec.addr = addr.address;
        }
        if (rec.addr)
            recs.push(rec);
    }
    return recs;
};

const ip_type = (ip, ifs, if_type)=>{
    if (!ip || !ifs || !if_type)
        return;
    let dev = _.findWhere(ifs, {addr: ip});
    if (!dev)
        return;
    return {name: dev.name, type: if_type[dev.name]};
};

const detect_ifs = ()=>{
    let ifs, if_type;
    if (process.platform=='win32')
        ifs = parse_interfaces();
    else if (env.NODE_EMBEDDED && get_android_sdk_ver()<23)
    {
        try {
            // XXX vladimir: os.networkInterfcases() crashes the process
            let netcfg;
            if (!(netcfg = exec('netcfg')))
                return;
            ifs = parse_netcfg(netcfg);
        } catch(e){ return void zerr('netcfg err '+zerr.e2s(e)); }
    }
    else
    {
        try {
            // XXX vladimir: os.networkInterfcases() crashes the process
            let ifconfig;
            if (!(ifconfig = exec('ifconfig')))
                return;
            ifs = parse_ifconfig(ifconfig);
        } catch(e){ return void zerr('ifconfig err '+zerr.e2s(e)); }
    }
    if (!confdir)
        return void zerr('confdir not set');
    try {
        let zid = file.read(confdir+'/db/z.id')||
            file.read(confdir+'/db/hola.id')||'';
        let ids = attrib.to_obj(attrib.from_str(zid));
        let dev_type = string.split_ws(ids.dev_type||'');
        if_type = _.object(dev_type.filter((x, i)=>!(i%2)),
            dev_type.filter((x, i)=>i%2));
    } catch(e){ return void zerr('Conf read fail '+zerr.e2s(e)); }
    E.ifs = ifs;
    E.if_type = if_type;
};

const get_android_sdk_ver = ()=>{
    if (E.android_sdk_ver)
        return E.android_sdk_ver;
    try {
        let android_sdk_ver;
        if (!(android_sdk_ver = exec('getprop ro.build.version.sdk')))
            return E.android_sdk_ver = -1;
        E.android_sdk_ver = +string.chomp(android_sdk_ver);
    } catch(e){ E.android_sdk_ver = -1; }
    return E.android_sdk_ver;
};

const tunnel_init = (msg, conn)=>{
    let local_address = conn._socket&&conn._socket.localAddress;
    detect_ifs();
    let info = ip_type(local_address, E.ifs, E.if_type);
    zerr.notice('js peer init from '+local_address+' '+
        (info ? info.name+' '+(info.type||'unknown') : ''));
    if (!info)
    {
        zerr('connection info unknown %s %O %O', local_address, E.ifs,
            E.if_type);
        return;
    }
    return {version: zconf.ZON_VERSION, idle,
        platform: process.platform, apkid, appid, type: info.type,
        ifname: info.name, mobile_enable, cid, android_ver, usage,
        android_sdk_ver: E.android_sdk_ver};
};

const tunnel_redirect = (msg, conn)=>{
    zerr.notice(`tunnel_redirect ${msg.redirect}`);
    if (net.isIPv4(msg.redirect))
        next_ip = msg.redirect;
    try { proxyjs_conn.close(); }
    catch(e){ zerr('redirect close failed'); }
};

const conf_read = ()=>{
    try {
        let zid = file.read(confdir+'/db/z.id')||
            file.read(confdir+'/db/hola.id')||'';
        let ids = attrib.to_obj(attrib.from_str(zid));
        if (ids.android_ver)
            android_ver = ids.android_ver;
        if (!ids.js_conf)
            return;
        conf = JSON.parse(new Buffer(ids.js_conf, 'base64').toString());
        zerr.notice('js_conf found %O', conf);
    } catch(e){ zerr('failed conf_read '+zerr.e2s(e)); }
};

let fallback_ips = qw`54.243.132.124 23.23.115.110 54.197.238.153
    23.23.176.28`;
const proxy_domain = 'proxyjs.luminatinet.com';
let next_ip;

const connect = ()=>etask(function*(){
    let ips, ip_idx = 0, ips_resolved;
    let agent = new https.Agent({servername: proxy_domain});
    let last_conn, ping_et, retry_sec = 5;
    let url_cb = ()=>etask(function*(){
        if (!ips_resolved)
        {
            try {
                ips = yield etask.nfn_apply(dns, '.resolve4', [proxy_domain]);
            } catch(e){ ips = null; }
            zerr.notice(`resolved ${proxy_domain} to ${ips}`);
            if (!ips || !ips.length)
                ips = fallback_ips;
            else
                ips_resolved = ips;
            ips = rand_uniform.rand_subset(ips, ips.length);
        }
        let ip;
        if (next_ip)
        {
            ip = next_ip;
            next_ip = null;
        }
        else
        {
            ip = ips[ip_idx%ips.length];
            ip_idx = (ip_idx+1)%ips.length;
        }
        return 'wss://'+ip;
    });
    let conn_cb = ()=>last_conn = 0;
    let retry_cb = ()=>{
        if (ping_et)
            ping_et = void ping_et.return();
        retry_sec = last_conn ? rand_uniform.rand_range(5, 60) :
            zutil.clamp(5, retry_sec*2, 60);
        zerr.notice(`waiting ${retry_sec}s to reconnect`);
        return retry_sec*ms.SEC;
    };
    yield zws.connect('wss://'+proxy_domain, conn=>{
        let local_address = conn._socket&&conn._socket.localAddress;
        proxyjs_conn = conn;
        last_conn = Date.now();
        ping_et = etask.interval(10*ms.MIN, function*(){
            if (Date.now()-last_conn<10*ms.MIN)
                return;
            try { yield zws.msg_call(conn, 'ping_ka', {}); }
            catch(e){ zerr('ping failed'); }
        });
        zerr.notice('js peer connect from '+local_address+
            ' waiting for tunnel_init');
        zws.ipc_handler(conn, 'tunnel_init', tunnel_init);
        zws.ipc_handler(conn, 'status_get', status_get);
        zws.msg_handler(conn, 'tunnel_redirect', tunnel_redirect);
        zws.msg_handler(conn, 'connect', client_connect);
        zws.msg_handler(conn, 'cid_set', cid_set);
    }, {ws_opt: {agent}, url_cb, retry_cb, conn_cb});
});

E.connect = _gw_ip=>{
    gw_ip = _gw_ip;
    if (started)
        return;
    started = true;
    if (!env.NODE_EMBEDDED && env.CONFDIR)
        E.set_confdir(env.CONFDIR);
    conf_read();
    if (conf && conf.disable_svc_js)
        return void zerr('disabling - js_conf');
    if (conf && conf.fallback_ips)
        fallback_ips = conf.fallback_ips;
    connect();
};

E.t = {detect_ifs};

if (!module.parent)
    E.connect();
