import Utils from "./utils";
import Global from "./global";

import * as Express from "express";
import HttpOutBound from "./zipkin/outbound/http";
import HttpsOutBound from "./zipkin/outbound/https";

let fs = require('fs');

import * as Index from "./index.d";
import * as jsYaml from "js-yaml";
import Settings from "./settings";
let morgan = require('morgan');
let bodyParser = require('body-parser');
let swaggerJSDoc = require('swagger-jsdoc');
let rfs = require('rotating-file-stream');
let promBundle = require("express-prom-bundle");

const rest = require('rest');

const {Tracer, BatchRecorder, ConsoleRecorder, jsonEncoder: {JSON_V2}} = require('zipkin');
const { HttpLogger } = require('zipkin-transport-http');
const CLSContext = require('zipkin-context-cls');
const {restInterceptor} = require('zipkin-instrumentation-cujojs-rest');

/**
 * get the inbound connections zipkin middleware
 */
const zipkinMiddleware = require('./zipkin/inbound/express');

export default class WebServer {
    public app: Express.Express;
    public express = require("express");
    private swaggerOptions: Index.SwaggerOptions;
    private morganOptions: Index.MorganOptions;
    private zipkinOptions: Index.ZipkinOptions;
    private routeMetrics: Index.RouteMetricsOptions;
    protected logStream: any;
    private logFileGenerator: (time: Date, index: number) => void;
    private allowedDomains: RegExp = new RegExp("^$");

    public zipkineExternalCaller:any;
    
    constructor(token: string = "global") {
        this.swaggerOptions = Settings.Instance.getSwagger(token);
        this.morganOptions = Settings.Instance.getMorgan(token);
        this.zipkinOptions = Settings.Instance.getZipkin(token);
        this.routeMetrics = Settings.Instance.getRouteMetrics(token);
        this.app = Express();
        this.logFileGenerator = this.customGenerator.bind(this);
    }

    public initialize() {
        this.createLogStream();

        this.includeMorgan();

        //Need to be included before any routes to include all routes
        this.includeMetrics();        

        this.autorizeForSwagger();

        if (this.swaggerOptions != undefined && this.swaggerOptions.isActive) {
            this.includeSwagger();
        }

        this.includeHealthChecker();

        //http://expressjs.com/en/api.html
        this.app.use(bodyParser.json({ type: '*/*', limit: '50mb'}));
        this.app.use(function (error: any, req: any, res: any, next: () => void) {
            if (error instanceof SyntaxError) {
                res.status(400).send(JSON.stringify({ err: 'json parsing failed, check input params' }));
            } else {
                next();
            }
        });
        this.app.use(bodyParser.urlencoded({limit: '50mb', extended: true}));

        if(this.zipkinOptions != undefined && this.zipkinOptions.isActive) {
            this.includeZipkin();
        }
        return this.app;
    }

    public getExpress(){
        return this.express;
    }

    public setLogStream(logStream: any) {
        this.logStream = logStream;
    }

    public setLogFileGenerator(logFileGenerator: (time: Date, index: number) => void) {
        this.logFileGenerator = logFileGenerator;
    }

    public activateMorgan(active: boolean){
        this.morganOptions.isActive = active;
    }

    private includeZipkin() {
        /**
         * from https://zipkin.io/pages/quickstart ->
         * 
         * docker run -d -p 9411:9411 openzipkin/zipkin
         * 
         */
        let self = this;
        const ctxImpl = new CLSContext();
        // const recorder = new ConsoleRecorder();
        const endpoint = self.zipkinOptions.serveradress + '/api/v2/spans';
        const recorder = new BatchRecorder({
            logger: new HttpLogger({
                endpoint: endpoint,
                jsonEncoder : JSON_V2,
                httpInterval: 6000, // How often to sync spans. Optional (defaults to 1000)
                //headers: {'Authorization': 'secret'}, // Custom HTTP headers. Optional
                timeout: 5000, // Timeout for HTTP Post. Optional (defaults to 0)
                //maxPayloadSize: 0, // Max payload size for zipkin span. Optional (defaults to 0)
                //agent: new Agent({keepAlive: true}), // Agent used for network related options. Optional (defaults to null)
            })
        });

        let localServiceName = self.zipkinOptions.localServicename;
        let tracer = new Tracer({ctxImpl, recorder, localServiceName});

        // instrument the client
        this.zipkineExternalCaller = rest.wrap(restInterceptor, { tracer });

        // Allow cross-origin, traced requests. See http://enable-cors.org/server_expressjs.html
        this.app.use(function(req, res, next) {
            /**
             * add zipkin specific headers to the allow headers list
             */
            res.setHeader('Access-Control-Allow-Origin', '*');
            res.setHeader('Access-Control-Allow-Headers', [
            'Origin', 'Accept', 'X-Requested-With', 'X-B3-TraceId',
            'X-B3-ParentSpanId', 'X-B3-SpanId', 'X-B3-Sampled'
            ].join(', '));

            next();
        });

         /**
         *  Add the Zipkin middleware
         */ 
        this.app.use(zipkinMiddleware({tracer}));

        new HttpOutBound(tracer, localServiceName, [endpoint]);
        new HttpsOutBound(tracer, localServiceName, [endpoint]);
    }

    private includeSwagger() {
        let self = this;
        if(self.swaggerOptions.apis != null && self.swaggerOptions.apis.length > 0){
            let apisPath: string[] = [];
            let apis = self.swaggerOptions.apis;

            for (var i = 0, len = apis.length; i < len; i++) {
                let path = Utils.getFullRouteFrom(apis[i]);
                if(path != null && path != ""){
                    apisPath.push(path);
                }
              }
            
            //self.swaggerOptions.apis = apisPath;
            //append new calculated paths to old paths
            self.swaggerOptions.apis = self.swaggerOptions.apis.concat(apisPath);
        }

        this.app.get('/swagger.json', function (req: Express.Request, res: Express.Response) {
            res.setHeader('Content-Type', 'application/json');            

            if(self.swaggerOptions == undefined){
                res.setHeader('Content-Type', 'application/json');
                res.status(500);
                res.send({Error: "Swagger options are undefined."});
                return;
            }

            var options = {
                swaggerDefinition: self.swaggerOptions.swaggerDefinition,
                apis: self.swaggerOptions.apis,
            };


            // Initialize swagger-jsdoc -> returns validated swagger spec in json format
            let swaggerSpec = {};
            try{
                swaggerSpec = swaggerJSDoc(options);
                res.status(200);                
            }catch(err){
                if(err instanceof jsYaml.YAMLException){
                    //put error YAMLException
                    swaggerSpec = err;
                }else{
                    //get message, drop it in {err:err.message}
                    swaggerSpec = {err: err != null && err.message ? err.message : err};
                }
                res.status(500);                
            }

            res.send(swaggerSpec);
        });
    }

    private includeHealthChecker() {
        let self = this;
        this.app.get('/health', function (req: Express.Request, res: Express.Response) {
            res.setHeader('Content-Type', 'application/json');
            let status = Global.Instance.isFunctional() ? 200 : 500;
            res.status(status);
            res.send(JSON.stringify({details: Global.Instance.getStatus(), memory: self.getMemoryUse(), status: status}));
        });
    }

    protected getMemoryUse(){
        let memory: NodeJS.MemoryUsage = process.memoryUsage()
        return memory;
    }

    private includeMetrics() {
        //promBundle is singleton, ensure we don't already have a default metrics registred
        promBundle.promClient.register.clear();

        const metricsMiddleware = promBundle({
            buckets: this.routeMetrics.buckets,
            includeMethod: this.routeMetrics.includeMethod,
            includePath: this.routeMetrics.includePath,
            autoregister: this.routeMetrics.autoregister, 
            promClient: {
                collectDefaultMetrics: {
                    timeout: this.routeMetrics.timeout
                }
            }
        });

        this.app.use(/(?:\/[^\/]*)*\/[^\.]*$/, metricsMiddleware);
        Global.Instance.Metrics.get(); //Inform "Metrics" module we actually use the global registry;

        this.app.get('/metrics', function (req, res) {
            res.set('Content-Type', promBundle.promClient.register.contentType);
            let response = Global.Instance.getMergedMetrics().metrics();
            res.send(response);            
        });
    }

    private autorizeForSwagger(){
        this.allowedDomainsRegex();
        let self = this;
        this.app.all('/*', function (req: Express.Request, res: Express.Response, next: Express.NextFunction) {
            if (req.headers.origin != undefined) {
                let origin = req.headers.origin;
                if(origin instanceof Array){
                    origin = origin[0];
                }
                let match = null;
                if(origin != undefined)
                    match = self.allowedDomains.exec(origin);
                if(match != null && match.length > 0){
                    res.setHeader('Access-Control-Allow-Origin', match[0]);
                }
                if(self.swaggerOptions.allowHeaders != null && self.swaggerOptions.allowHeaders != "")
                    res.setHeader('Access-Control-Allow-Headers', self.swaggerOptions.allowHeaders);
                if(self.swaggerOptions.allowMethods != null && self.swaggerOptions.allowMethods != "")
                    res.setHeader('Access-Control-Allow-Methods', self.swaggerOptions.allowMethods);
            }

            //options request need to return as soon as possible
            if(req.method == "OPTIONS"){
                res.status(200);
                res.send();
            }
            else
                next();
        });
    }

    private includeMorgan() {
        let self = this;
        
        //get query token
        morgan.token('query', function getQueryMethod(req: Index.ExpressRequest) {
            let query = {};
            if (req.method == 'POST') {
                query = req.parsed_body;
            } else {
                query = req.query;
            };

            return ((query != undefined) ? JSON.stringify(query) : "");
        });

        morgan.token('body', function (req: Index.ExpressRequest, res: Index.ExpressResponse) {
            let response = null;
            if(req != null){
                let body = req.body;
                if(body != null){
                    response = JSON.stringify(body);
                }
            }
            return (response != null && response != "") ? response : "{}";
        });

        /**
        * attempt to convert ipv6 to ipv4(it will remain ipv6 if it can't be done)
        */
        morgan.token('remote-addr', Utils.getIp);

        //add a custom format
        morgan.format('custom-format', self.morganOptions.format)

        //tell express to use morgan access logger
        this.app.use(morgan('custom-format', { 
            stream: this.logStream,
            immediate: false,
            skip: function (req: Index.ExpressRequest, res: Index.ExpressResponse) { 
                return !self.morganOptions.isActive 
            }
         }));
    }

    private createLogStream(): boolean {
        let use_stdout: boolean | null = null;

        //Get value from enviroment
        if(process.env != null){
            if(process.env.MORGAN_USE_STDOUT == "true"){
                use_stdout = true;
            }else if(process.env.MORGAN_USE_STDOUT == "false"){
                use_stdout = false;
            }
        }

        //Get value from settings
        if(use_stdout == null){
            use_stdout = this.morganOptions != null && typeof this.morganOptions.useStdout == "boolean" ? this.morganOptions.useStdout : true;
        }

        //If is not explicit set on false, will be considerated tru and log to process.stdout
        if(use_stdout != false){
            this.logStream = process.stdout;
            return true;
        }

        //If is already set a logStream will be used
        if(this.logStream != null){
            return false;
        }

        let logSettings = {
            path: this.morganOptions && this.morganOptions.logStream && this.morganOptions.logStream.path ? this.morganOptions.logStream.path : "/logs",
            interval: this.morganOptions && this.morganOptions.logStream && this.morganOptions.logStream.interval ? this.morganOptions.logStream.interval : "1d",
            maxFiles: this.morganOptions && this.morganOptions.logStream && this.morganOptions.logStream.maxFiles ? this.morganOptions.logStream.maxFiles : 7,
            compress: this.morganOptions && this.morganOptions.logStream && this.morganOptions.logStream.compress ? this.morganOptions.logStream.compress : "gzip",
       }

       logSettings.path = Utils.getFullRouteFrom(logSettings.path);

        let logDirectory = logSettings.path;
        
        // ensure log directory exists
        fs.existsSync(logDirectory) || fs.mkdirSync(logDirectory);

        // create a rotating write stream
        this.logStream = rfs(this.logFileGenerator, {
            interval: logSettings.interval,     // rotate daily
            maxFiles: logSettings.maxFiles,       // max files to keep after rotation
            compress: logSettings.compress,   // compress method used

            path: logDirectory
        });
        return true;
    }

    private allowedDomainsRegex(): boolean{
        if(this.swaggerOptions == undefined){
            return false;
        }

        let domains = this.swaggerOptions.allowedDomains;
        if(domains instanceof Array){
            let arrayLength = domains.length;
            let escaped = [];
            for (var i = 0; i < arrayLength; i++) {
                escaped[i] = "("+domains[i].replace(/\./g, '\.')+")"
            }
            this.allowedDomains = new RegExp("^((https?:\/\/)?([^:\/]+\.)*("+escaped.join("|")+")(:[0-9]+)?)$");            
        }else{
            this.allowedDomains = new RegExp("^$");            
        }
        return true;
    }

    /**
     * rotate file name generator
     * @param {*} time 
     * @param {*} index 
     */
    protected customGenerator(time: Date, index: number): string {
        let filename: string = "access.log";        
        if(this.morganOptions != undefined && typeof this.morganOptions.logStream == "object" && this.morganOptions.logStream.logFile != undefined){
            filename = this.morganOptions.logStream.logFile;
        }

        if (!time)
            return filename;

        var month = time.getFullYear() + "" + Utils.pad(time.getMonth() + 1);
        var day = Utils.pad(time.getDate());
        var hour = Utils.pad(time.getHours());
        var minute = Utils.pad(time.getMinutes());

        return month + day + "-" + hour + minute + "-" + index + "-" + filename + ".gz";
    }
}