'use strict';

// netdata
// real-time performance and health monitoring, done right!
// (C) 2016 Costa Tsaousis <costa@tsaousis.gr>
// SPDX-License-Identifier: GPL-3.0-or-later

var url = require('url');
var http = require('http');
var util = require('util');

/*
var netdata = require('netdata');

var example_chart = {
    id: 'id',                       // the unique id of the chart
    name: 'name',                   // the name of the chart
    title: 'title',                 // the title of the chart
    units: 'units',                 // the units of the chart dimensions
    family: 'family',               // the family of the chart
    context: 'context',             // the context of the chart
    type: netdata.chartTypes.line,  // the type of the chart
    priority: 0,                    // the priority relative to others in the same family
    update_every: 1,                // the expected update frequency of the chart
    dimensions: {
        'dim1': {
            id: 'dim1',             // the unique id of the dimension
            name: 'name',           // the name of the dimension
            algorithm: netdata.chartAlgorithms.absolute,    // the id of the netdata algorithm
            multiplier: 1,          // the multiplier
            divisor: 1,             // the divisor
            hidden: false,          // is hidden (boolean)
        },
        'dim2': {
            id: 'dim2',             // the unique id of the dimension
            name: 'name',           // the name of the dimension
            algorithm: 'absolute',  // the id of the netdata algorithm
            multiplier: 1,          // the multiplier
            divisor: 1,             // the divisor
            hidden: false,          // is hidden (boolean)
        }
        // add as many dimensions as needed
    }
};
*/

var netdata = {
    options: {
        filename: __filename,
        DEBUG: false,
        update_every: 1
    },

    chartAlgorithms: {
        incremental: 'incremental',
        absolute: 'absolute',
        percentage_of_absolute_row: 'percentage-of-absolute-row',
        percentage_of_incremental_row: 'percentage-of-incremental-row'
    },

    chartTypes: {
        line: 'line',
        area: 'area',
        stacked: 'stacked'
    },

    services: new Array(),
    modules_configuring: 0,
    charts: {},

    processors: {
        http: {
            name: 'http',

            process: function(service, callback) {
                var __DEBUG = netdata.options.DEBUG;

                if(__DEBUG === true)
                    netdata.debug(service.module.name + ': ' + service.name + ': making ' + this.name + ' request: ' + netdata.stringify(service.request));

                var req = http.request(service.request, function(response) {
                    if(__DEBUG === true) netdata.debug(service.module.name + ': ' + service.name + ': got server response...');

                    var end = false;
                    var data = '';
                    response.setEncoding('utf8');

                    if(response.statusCode !== 200) {
                        if(end === false) {
                            service.error('Got HTTP code ' + response.statusCode + ', failed to get data.');
                            end = true;
                            return callback(null);
                        }
                    }

                    response.on('data', function(chunk) {
                        if(end === false) data += chunk;
                    });

                    response.on('error', function() {
                        if(end === false) {
                            service.error(': Read error, failed to get data.');
                            end = true;
                            return callback(null);
                        }
                    });

                    response.on('end', function() {
                        if(end === false) {
                            if(__DEBUG === true) netdata.debug(service.module.name + ': ' + service.name + ': read completed.');
                            end = true;
                            return callback(data);
                        }
                    });
                });

                req.on('error', function(e) {
                    if(__DEBUG === true) netdata.debug('Failed to make request: ' + netdata.stringify(service.request) + ', message: ' + e.message);
                    service.error('Failed to make request, message: ' + e.message);
                    return callback(null);
                });

                // write data to request body
                if(typeof service.postData !== 'undefined' && service.request.method === 'POST') {
                    if(__DEBUG === true) netdata.debug(service.module.name + ': ' + service.name + ': posting data: ' + service.postData);
                    req.write(service.postData);
                }

                req.end();
            }
        }
    },

    stringify: function(obj) {
        return util.inspect(obj, {depth: 10});
    },

    zeropad2: function(s) {
        return ("00" + s).slice(-2);
    },

    logdate: function(d) {
        if(typeof d === 'undefined') d = new Date();
        return d.getFullYear().toString() + '-' + this.zeropad2(d.getMonth() + 1) + '-' + this.zeropad2(d.getDate())
            + ' ' + this.zeropad2(d.getHours()) + ':' + this.zeropad2(d.getMinutes()) + ':' + this.zeropad2(d.getSeconds());
    },

    // show debug info, if debug is enabled
    debug: function(msg) {
        if(this.options.DEBUG === true) {
            console.error(this.logdate() + ': ' + netdata.options.filename + ': DEBUG: ' + ((typeof(msg) === 'object')?netdata.stringify(msg):msg).toString());
        }
    },

    // log an error
    error: function(msg) {
        console.error(this.logdate() + ': ' + netdata.options.filename + ': ERROR: ' + ((typeof(msg) === 'object')?netdata.stringify(msg):msg).toString());
    },

    // send data to netdata
    send: function(msg) {
        console.log(msg.toString());
    },

    service: function(service) {
        if(typeof service === 'undefined')
            service = {};

        var now = Date.now();

        service._current_chart = null;  // the current chart we work on
        service._queue = '';            // data to be sent to netdata

        service.error_reported = false; // error log flood control

        service.added = false;          // added to netdata.services
        service.enabled = true;
        service.updates = 0;
        service.running = false;
        service.started = 0;
        service.ended = 0;

        if(typeof service.module === 'undefined') {
            service.module = { name: 'not-defined-module' };
            service.error('Attempted to create service without a module.');
            service.enabled = false;
        }

        if(typeof service.name === 'undefined') {
            service.name = 'unnamed@' + service.module.name + '/' + now;
        }

        if(typeof service.processor === 'undefined')
            service.processor = netdata.processors.http;

        if(typeof service.update_every === 'undefined')
            service.update_every = service.module.update_every;

        if(typeof service.update_every === 'undefined')
            service.update_every = netdata.options.update_every;

        if(service.update_every < netdata.options.update_every)
            service.update_every = netdata.options.update_every;

        // align the runs
        service.next_run = now - (now % (service.update_every * 1000)) + (service.update_every * 1000);

        service.commit = function() {
            if(this.added !== true) {
                this.added = true;
                
                var now = Date.now();
                this.next_run = now - (now % (service.update_every * 1000)) + (service.update_every * 1000);

                netdata.services.push(this);
                if(netdata.options.DEBUG === true) netdata.debug(this.module.name + ': ' + this.name + ': service committed.');
            }
        };

        service.execute = function(responseProcessor) {
            var __DEBUG = netdata.options.DEBUG;

            if(service.enabled === false)
                return responseProcessor(null);

            this.module.active++;
            this.running = true;
            this.started = Date.now();
            this.updates++;

            if(__DEBUG === true)
                netdata.debug(this.module.name + ': ' + this.name + ': making ' + this.processor.name + ' request: ' + netdata.stringify(this));

            this.processor.process(this, function(response) {
                service.ended = Date.now();
                service.duration = service.ended - service.started;

                if(typeof response === 'undefined')
                    response = null;

                if(response !== null)
                    service.errorClear();

                if(__DEBUG === true)
                    netdata.debug(service.module.name + ': ' + service.name + ': processing ' + service.processor.name + ' response (received in ' + (service.ended - service.started).toString() + ' ms)');

                try {
                    responseProcessor(service, response);
                }
                catch(e) {
                    netdata.error(e);
                    service.error("responseProcessor failed process response data.");
                }

                service.running = false;
                service.module.active--;
                if(service.module.active < 0) {
                    service.module.active = 0;
                    if(__DEBUG === true)
                        netdata.debug(service.module.name + ': active module counter below zero.');
                }

                if(service.module.active === 0) {
                    // check if we run under configure
                    if(service.module.configure_callback !== null) {
                        if(__DEBUG === true)
                            netdata.debug(service.module.name + ': configuration finish callback called from processResponse().');

                        var configure_callback = service.module.configure_callback;
                        service.module.configure_callback = null;
                        configure_callback();
                    }
                }
            });
        };

        service.update = function() {
            if(netdata.options.DEBUG === true)
                netdata.debug(this.module.name + ': ' + this.name + ': starting data collection...');

            this.module.update(this, function() {
                if(netdata.options.DEBUG === true)
                    netdata.debug(service.module.name + ': ' + service.name + ': data collection ended in ' + service.duration.toString() + ' ms.');
            });
        };

        service.error = function(message) {
            if(this.error_reported === false) {
                netdata.error(this.module.name + ': ' + this.name + ': ' + message);
                this.error_reported = true;
            }
            else if(netdata.options.DEBUG === true)
                netdata.debug(this.module.name + ': ' + this.name + ': ' + message);
        };

        service.errorClear = function() {
            this.error_reported = false;
        };

        service.queue = function(txt) {
            this._queue += txt + '\n';
        };

        service._send_chart_to_netdata = function(chart) {
            // internal function to send a chart to netdata
            this.queue('CHART "' + chart.id + '" "' + chart.name + '" "' + chart.title + '" "' + chart.units + '" "' + chart.family + '" "' + chart.context + '" "' + chart.type + '" ' + chart.priority.toString() + ' ' + chart.update_every.toString());

            if(typeof(chart.dimensions) !== 'undefined') {
                var dims = Object.keys(chart.dimensions);
                var len = dims.length;
                while(len--) {
                    var d = chart.dimensions[dims[len]];

                    this.queue('DIMENSION "' + d.id + '" "' + d.name + '" "' + d.algorithm + '" ' + d.multiplier.toString() + ' ' + d.divisor.toString() + ' ' + ((d.hidden === true) ? 'hidden' : '').toString());
                    d._created = true;
                    d._updated = false;
                }
            }

            chart._created = true;
            chart._updated = false;
        };

        // begin data collection for a chart
        service.begin = function(chart) {
            if(this._current_chart !== null && this._current_chart !== chart) {
                this.error('Called begin() for chart ' + chart.id + ' while chart ' + this._current_chart.id + ' is still open. Closing it.');
                this.end();
            }

            if(typeof(chart.id) === 'undefined' || netdata.charts[chart.id] !== chart) {
                this.error('Called begin() for chart ' + chart.id + ' that is not mine. Where did you find it? Ignoring it.');
                return false;
            }

            if(netdata.options.DEBUG === true) netdata.debug('setting current chart to ' + chart.id);
            this._current_chart = chart;
            this._current_chart._began = true;

            if(this._current_chart._dimensions_count !== 0) {
                if(this._current_chart._created === false || this._current_chart._updated === true)
                    this._send_chart_to_netdata(this._current_chart);

                var now = this.ended;
                this.queue('BEGIN ' + this._current_chart.id + ' ' + ((this._current_chart._last_updated > 0)?((now - this._current_chart._last_updated) * 1000):'').toString());
            }
            // else this.error('Called begin() for chart ' + chart.id + ' which is empty.');

            this._current_chart._last_updated = now;
            this._current_chart._began = true;
            this._current_chart._counter++;

            return true;
        };

        // set a collected value for a chart
        // we do most things on the first value we attempt to set
        service.set = function(dimension, value) {
            if(this._current_chart === null) {
                this.error('Called set(' + dimension + ', ' + value + ') without an open chart.');
                return false;
            }

            if(typeof(this._current_chart.dimensions[dimension]) === 'undefined') {
                this.error('Called set(' + dimension + ', ' + value + ') but dimension "' + dimension + '" does not exist in chart "' + this._current_chart.id + '".');
                return false;
            }

            if(typeof value === 'undefined' || value === null)
                return false;

            if(this._current_chart._dimensions_count !== 0)
                this.queue('SET ' + dimension + ' = ' + value.toString());

            return true;
        };

        // end data collection for the current chart - after calling begin()
        service.end = function() {
            if(this._current_chart !== null && this._current_chart._began === false) {
                this.error('Called end() without an open chart.');
                return false;
            }

            if(this._current_chart._dimensions_count !== 0) {
                this.queue('END');
                netdata.send(this._queue);
            }

            this._queue = '';
            this._current_chart._began = false;
            if(netdata.options.DEBUG === true) netdata.debug('sent chart ' + this._current_chart.id);
            this._current_chart = null;
            return true;
        };

        // discard the collected values for the current chart - after calling begin()
        service.flush = function() {
            if(this._current_chart === null || this._current_chart._began === false) {
                this.error('Called flush() without an open chart.');
                return false;
            }

            this._queue = '';
            this._current_chart._began = false;
            this._current_chart = null;
            return true;
        };

        // create a netdata chart
        service.chart = function(id, chart) {
            var __DEBUG = netdata.options.DEBUG;

            if(typeof(netdata.charts[id]) === 'undefined') {
                netdata.charts[id] = {
                    _created: false,
                    _updated: true,
                    _began: false,
                    _counter: 0,
                    _last_updated: 0,
                    _dimensions_count: 0,
                    id: id,
                    name: id,
                    title: 'untitled chart',
                    units: 'a unit',
                    family: '',
                    context: '',
                    type: netdata.chartTypes.line,
                    priority: 50000,
                    update_every: netdata.options.update_every,
                    dimensions: {}
                };
            }

            var c = netdata.charts[id];

            if(typeof(chart.name) !== 'undefined' && chart.name !== c.name) {
                if(__DEBUG === true) netdata.debug('chart ' + id + ' updated its name');
                c.name = chart.name;
                c._updated = true;
            }

            if(typeof(chart.title) !== 'undefined' && chart.title !== c.title) {
                if(__DEBUG === true) netdata.debug('chart ' + id + ' updated its title');
                c.title = chart.title;
                c._updated = true;
            }

            if(typeof(chart.units) !== 'undefined' && chart.units !== c.units) {
                if(__DEBUG === true) netdata.debug('chart ' + id + ' updated its units');
                c.units = chart.units;
                c._updated = true;
            }

            if(typeof(chart.family) !== 'undefined' && chart.family !== c.family) {
                if(__DEBUG === true) netdata.debug('chart ' + id + ' updated its family');
                c.family = chart.family;
                c._updated = true;
            }

            if(typeof(chart.context) !== 'undefined' && chart.context !== c.context) {
                if(__DEBUG === true) netdata.debug('chart ' + id + ' updated its context');
                c.context = chart.context;
                c._updated = true;
            }

            if(typeof(chart.type) !== 'undefined' && chart.type !== c.type) {
                if(__DEBUG === true) netdata.debug('chart ' + id + ' updated its type');
                c.type = chart.type;
                c._updated = true;
            }

            if(typeof(chart.priority) !== 'undefined' && chart.priority !== c.priority) {
                if(__DEBUG === true) netdata.debug('chart ' + id + ' updated its priority');
                c.priority = chart.priority;
                c._updated = true;
            }

            if(typeof(chart.update_every) !== 'undefined' && chart.update_every !== c.update_every) {
                if(__DEBUG === true) netdata.debug('chart ' + id + ' updated its update_every from ' + c.update_every + ' to ' + chart.update_every);
                c.update_every = chart.update_every;
                c._updated = true;
            }

            if(typeof(chart.dimensions) !== 'undefined') {
                var dims = Object.keys(chart.dimensions);
                var len = dims.length;
                while(len--) {
                    var x = dims[len];

                    if(typeof(c.dimensions[x]) === 'undefined') {
                        c._dimensions_count++;

                        c.dimensions[x] = {
                            _created: false,
                            _updated: false,
                            id: x,                  // the unique id of the dimension
                            name: x,                // the name of the dimension
                            algorithm: netdata.chartAlgorithms.absolute,    // the id of the netdata algorithm
                            multiplier: 1,          // the multiplier
                            divisor: 1,             // the divisor
                            hidden: false           // is hidden (boolean)
                        };

                        if(__DEBUG === true) netdata.debug('chart ' + id + ' created dimension ' + x);
                        c._updated = true;
                    }

                    var dim = chart.dimensions[x];
                    var d = c.dimensions[x];

                    if(typeof(dim.name) !== 'undefined' && d.name !== dim.name) {
                        if(__DEBUG === true) netdata.debug('chart ' + id + ', dimension ' + x + ' updated its name');
                        d.name = dim.name;
                        d._updated = true;
                    }

                    if(typeof(dim.algorithm) !== 'undefined' && d.algorithm !== dim.algorithm) {
                        if(__DEBUG === true) netdata.debug('chart ' + id + ', dimension ' + x + ' updated its algorithm from ' + d.algorithm + ' to ' + dim.algorithm);
                        d.algorithm = dim.algorithm;
                        d._updated = true;
                    }

                    if(typeof(dim.multiplier) !== 'undefined' && d.multiplier !== dim.multiplier) {
                        if(__DEBUG === true) netdata.debug('chart ' + id + ', dimension ' + x + ' updated its multiplier');
                        d.multiplier = dim.multiplier;
                        d._updated = true;
                    }

                    if(typeof(dim.divisor) !== 'undefined' && d.divisor !== dim.divisor) {
                        if(__DEBUG === true) netdata.debug('chart ' + id + ', dimension ' + x + ' updated its divisor');
                        d.divisor = dim.divisor;
                        d._updated = true;
                    }

                    if(typeof(dim.hidden) !== 'undefined' && d.hidden !== dim.hidden) {
                        if(__DEBUG === true) netdata.debug('chart ' + id + ', dimension ' + x + ' updated its hidden status');
                        d.hidden = dim.hidden;
                        d._updated = true;
                    }

                    if(d._updated) c._updated = true;
                }
            }

            //if(netdata.options.DEBUG === true) netdata.debug(netdata.charts);
            return netdata.charts[id];
        };

        return service;
    },

    runAllServices: function() {
        if(netdata.options.DEBUG === true) netdata.debug('runAllServices()');

        var now = Date.now();
        var len = netdata.services.length;
        while(len--) {
            var service = netdata.services[len];

            if(service.enabled === false || service.running === true) continue;
            if(now <= service.next_run) continue;

            service.update();

            now = Date.now();
            service.next_run = now - (now % (service.update_every * 1000)) + (service.update_every * 1000);
        }

        // 1/10th of update_every in pause
        setTimeout(netdata.runAllServices, netdata.options.update_every * 100);
    },

    start: function() {
        if(netdata.options.DEBUG === true) this.debug('started, services: ' + netdata.stringify(this.services));

        if(this.services.length === 0) {
            this.disableNodePlugin();

            // eslint suggested way to exit
            var exit = process.exit;
            exit(1);
        }
        else this.runAllServices();
    },

    // disable the whole node.js plugin
    disableNodePlugin: function() {
        this.send('DISABLE');

        // eslint suggested way to exit
        var exit = process.exit;
        exit(1);
    },

    requestFromParams: function(protocol, hostname, port, path, method) {
        return {
            protocol: protocol,
            hostname: hostname,
            port: port,
            path: path,
            //family: 4,
            method: method,
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded',
                'Connection': 'keep-alive'
            },
            agent: new http.Agent({
                keepAlive: true,
                keepAliveMsecs: netdata.options.update_every * 1000,
                maxSockets: 2, // it must be 2 to work
                maxFreeSockets: 1
            })
        };
    },

    requestFromURL: function(a_url) {
        var u = url.parse(a_url);
        return netdata.requestFromParams(u.protocol, u.hostname, u.port, u.path, 'GET');
    },

    configure: function(module, config, callback) {
        if(netdata.options.DEBUG === true) this.debug(module.name + ': configuring (update_every: ' + this.options.update_every + ')...');

        module.active = 0;
        module.update_every = this.options.update_every;

        if(typeof config.update_every !== 'undefined')
            module.update_every = config.update_every;

        module.enable_autodetect = (config.enable_autodetect)?true:false;

        if(typeof(callback) === 'function')
            module.configure_callback = callback;
        else
            module.configure_callback = null;

        var added = module.configure(config);

        if(netdata.options.DEBUG === true) this.debug(module.name + ': configured, reporting ' + added + ' eligible services.');

        if(module.configure_callback !== null && added === 0) {
            if(netdata.options.DEBUG === true) this.debug(module.name + ': configuration finish callback called from configure().');
            var configure_callback = module.configure_callback;
            module.configure_callback = null;
            configure_callback();
        }

        return added;
    }
};

if(netdata.options.DEBUG === true) netdata.debug('loaded netdata from:', __filename);
module.exports = netdata;
