const {
    Request,
    Annotation
} = require('zipkin');

import * as zipkin from "zipkin";
import Settings from "../../settings";

export default class HttpOutBound{
    
    private methods:any = [];
    
    public constructor(private tracer:zipkin.Tracer, private serviceName:string = "test-http", private ignoreHostList:string[] = [], private protocol = 'http'){
        let _protocol = require(protocol);
        this.methods.push(_protocol.request);
        this.boundModule(_protocol);
    }
    
    private beforeHook(methodArgs:any, res:any, context:any) {
        this.tracer.scoped(() => {
            //if express context, overwrite the id
            let sessionName = (<any>this).tracer['_ctxImpl']._session.name;
            if(Settings.ZIPKIN_CONTEXT_NAME === sessionName ){
                context = Object.assign(context, {traceId:this.tracer.id});
            }
            let {host, method, path, port, hostname} = methodArgs;
            let prettyHost = host || hostname;
            if(port != undefined){
                prettyHost= `${prettyHost}:${port}`;
            }
            let completeUrl = `${this.protocol}://${prettyHost}${path}`;
            if (!methodArgs.headers) methodArgs.headers = {};


            //don't report yourself
            if(this.ignoreHostList.indexOf(completeUrl) >= 0){
                context = Object.assign(context, {ignored:true});
                return;
            }
            
            /**
            * account for in-flight traces
            */
            let {traceId} = context;
            //pass the current in-flight trace to the tracer
            this.tracer.setId(traceId) ;
            // create a new trace either way(dependent on the parent trace or a new one)
            let _traceId = this.tracer.createChildId();      
            
            let { headers } = Request.addZipkinHeaders(methodArgs, _traceId);
            Object.assign(methodArgs.headers, headers);
            
            this.tracer.setId(_traceId);
            context = Object.assign(context, {traceId:_traceId});
            
            this.tracer.recordServiceName(this.serviceName);
            this.tracer.recordRpc(method);
            this.tracer.recordBinary('http.url', completeUrl);
            this.tracer.recordBinary('http.method', method);
            this.tracer.recordAnnotation(new Annotation.ClientSend());
            
        });
    };
    
    private afterHook(clientReq:any, second:any, context:any) {
        
        let {traceId, ignored} = context ;
        if(ignored) return;
        let {res} = clientReq;
        let self = this;
        this.tracer.scoped(()=>{
            self.tracer.setId(traceId);
            let code = res.statusCode;
            let message = res.statusMessage;
            self.tracer.recordBinary('http.status_code', code.toString());
            if(code != 200){
                self.tracer.recordBinary('error', new Error(message || 'error').toString());
            }
            self.tracer.recordAnnotation(new Annotation.ClientRecv());      
        })
    }
    
    private boundModule(protocol:any) {
        
        let self = this;
        this.methods.forEach(function(existing:any) {
            let newFunc = function(this: any) {
                let context = {traceId:undefined, ignored:false};
                
                //call this to generate a new trace
                self.beforeHook.bind(self).apply(self, <any>[arguments[0], arguments[1], context]);
                
                let secondSelf = this;
                
                //add hooks
                self.attach(secondSelf, arguments, context, undefined, self.afterHook.bind(self));
                
                return existing.apply(this, <any>arguments);
            };
            
            newFunc.prototype = existing.prototype;
            protocol[existing.name] = newFunc;            
        });
    };
    
    private attach(obj:any, args:any, probeData:any, hookBefore:any = undefined, hookAfter:any = undefined){  
        var position = this.findCallbackArg(args);
        if (position == undefined) return;
        
        var orig = args[position];
        
        if (orig == undefined || orig.__zipkin_intercepted__) return;
        orig.__zipkin_intercepted__ = true;
        
        args[position] = function() {
            if (hookBefore) {
                hookBefore(this, arguments, probeData);
            }
            
            var ret = orig.apply(this, arguments);
            
            if (hookAfter) {
                hookAfter(this, arguments, probeData, ret);
            }
            return ret;
        };
    }
    
    private findCallbackArg(args:any) {
        var position;
        for (var i = 0; i < args.length; i++) {
            if ((typeof args[i] === 'function')) {
                position = i;
                break;
            }
        }
        return position;
    };
};
