<commit_before>var _ = require('lodash');

var ScheduleFactory = require('../lib/schedule-factory');
var ProxyHelper = require('../lib/proxy-helper');
var RedisWatcher = require('../services/redis-watcher');
var M2mSupervisor = require('../processes/m2m-supervisor');

var logger = require('../lib/logger')('api');
var schema = require('../lib/redis-schema');
var helpers = require('../lib/hash-helpers');
var configTemplate = require('../lib/config-hashkeys');
var deviceTemplate = require('../lib/device-hashkeys');

var express = require('express');
var router = express.Router();

function checkRedis(callback){
    if (!RedisWatcher.instance) new RedisWatcher();
    if (RedisWatcher.instance.started())
        callback();
    else {
        RedisWatcher.instance.start();
        _.defer(callback);
    }
}

function requireRedis(res,callback){
    checkRedis(function(){
        if (RedisWatcher.instance.ready())
            callback();
        else
            res.send({error: 'Redis not ready'});
    });
}

function requestHash(res,hashKey,resultKey,template,filter){
    RedisWatcher.instance.client.hgetall(hashKey).thenHint('requestHash - ' + resultKey,function(hash){
        var result = {};
        result[resultKey] = template ? helpers.hash2groups(hash || {},template) : hash;
        filter && filter(result[resultKey]);
        res.send(result);
    });
}

function updateHash(updates,callback){
    RedisWatcher.instance.client.send('hmset',updates).thenHint('updateHash',function () {
        callback();
    });
}

function changeHash(req,res,hashKey,callback){
    helpers.hash2sets(hashKey,req.body,function(updates,deletes){
        if (!updates && !deletes)
            res.send({error: 'No changes requested'});
        else if (!deletes)
            updateHash(updates,callback);
        else
            RedisWatcher.instance.client.send('hdel',deletes).thenHint('deleteHash',function(){
                if (updates)
                    updateHash(updates,callback);
                else
                    callback();
            });
    });
}

function findIDs(keyPattern,callback){
    RedisWatcher.instance.client.keys(keyPattern).thenHint('findIDs - ' + keyPattern,callback);
}

function declareRouteList(model,schemaEntry){
    router.get('/' + model,function(req,res,next){
        requireRedis(res,function(){
            findIDs(schemaEntry.keysPattern(),function(keys){
                var result = {};
                result[model] = _.map(keys,function(key){ return schemaEntry.getParam(key); });
                res.send(result);
            })
        });
    });
}

function declareRouteGetByID(model,responder){
    router.get('/' + model + '/:id',function(req,res,next){
        requireRedis(res,function(){
            responder(res,req.params.id);
        });
    });
}

// CHECK ----------------

router.get('/check',function(req,res,next){
    res.send({});
});

// PROXY ----------------

router.get('/proxy',function(req,res,next){
    setProxy(req,res,req.query);
});

router.post('/proxy',function(req,res,next){
    setProxy(req,res,req.body);
});

function setProxy(req,res,config){
    requireRedis(res,function() {
        var helper = new ProxyHelper(RedisWatcher.instance.client,config).checkConfig(function (error, config) {
            req.session.proxy = config;
            if (error)
                throw(error);
            else
                res.redirect('/');
        });
    });
}

function proxiedGET(proxy,path,res){
    var helper = new ProxyHelper(RedisWatcher.instance.client,proxy).get(path,res);
}

function proxiedPOST(proxy,path,body,res){
    var helper = new ProxyHelper(RedisWatcher.instance.client,proxy).post(path,body,res);
}

// CONFIG ----------------

router.get('/config',function(req,res,next){
    if (req.session.proxy) return proxiedGET(req.session.proxy,'/config',res);

    requireRedis(res,_.bind(requestConfig,this,res));
});

router.post('/config',function(req,res,next){
    if (req.session.proxy) return proxiedPOST(req.session.proxy,'/config',req.body,res);

    requireRedis(res,function(){
        logger.info('config changes: ' + JSON.stringify(req.body));
        changeHash(req,res,schema.config.key,function(){
            // istanbul ignore if - TODO consider how to test...
            if (M2mSupervisor.instance) M2mSupervisor.instance.configWatcher.emit('checkReady');
            requestConfig(res);
        });
    });
});

function requestConfig(res){
    requestHash(res,schema.config.key,'config',configTemplate);
}

// DEVICE ----------------

declareRouteList('devices',schema.device.settings);

declareRouteGetByID('device',requestDevice);

router.post('/device/:id',function(req,res,next){
    requireRedis(res,function(){
        logger.info('device changes(' + req.params.id + '): ' + JSON.stringify(req.body));
        changeHash(req,res,schema.device.settings.useParam(req.params.id),function(){
            requestDevice(res,req.params.id);
            var deviceRouter = null;
            // istanbul ignore next - TODO consider how to test
            if (M2mSupervisor.instance && M2mSupervisor.instance.queueRouter &&
                (deviceRouter = M2mSupervisor.instance.queueRouter.routes[schema.device.queue.useParam(req.params.id)]))
                deviceRouter.settingsWatcher.emit('checkReady');
        });
    });
});

router.get('/device',function(req,res,next){
    requireRedis(res,function(){
        var profileKeys = _.select(_.collect(RedisWatcher.instance.keys,_.bind(schema.command.profile.getParam,schema.command.profile,_)));
        var scheduleKeys = _.select(_.collect(RedisWatcher.instance.keys,_.bind(schema.schedule.periods.getParam,schema.schedule.periods,_)));
        if (profileKeys.length !== 1) {
            var defaults = _.defaults(helpers.hash2groups({},deviceTemplate));
            makeOptionEditable(defaults['Connection']);
            makeOptionEditable(defaults['Commands']);
            res.send({'new-device': defaults});
        } else {
            var profileKey = profileKeys[0];
            var scheduleKey = _.indexOf(scheduleKeys,profileKey) >= 0 ? profileKey : null;
            RedisWatcher.instance.client.hgetall(schema.command.profile.useParam(profileKey)).thenHint('requestHash - ' + profileKey,function(hash){
                hash[deviceTemplate.commands.profile.key] = profileKey;
                hash[deviceTemplate.commands.schedule.key] = scheduleKey;
                var defaults = _.defaults(helpers.hash2groups(hash,deviceTemplate));

                if (scheduleKey) {
                    var routingOption = _.detect(defaults['Commands'],function(option){ return option.key === 'command:routing'});
                    routingOption.options.push('scheduled');
                    routingOption.default = 'scheduled';
                    routingOption.status = 'editable';
                }

                makeOptionEditable(defaults['Connection']);
                deleteOptionExists(defaults['Connection']);
                deleteOptionExists(defaults['Commands']); // TODO figure out how to allow profile & schedule options

                res.send({'new-device': defaults});
            });
        }
    });
});

function makeOptionEditable(options){
    _.each(options,function(option){ option.status = 'editable'; });
}

function deleteOptionExists(options){
    _.each(options,function(option){ delete option.exists; });
}

router.post('/device',function(req,res,next){
    requireRedis(res,function(){
        var id = req.body.id;
        delete req.body.id;
        if (!id)
            res.send({error: 'Device ID not provided'});
        else
            findIDs(schema.device.settings.keysPattern(),function(keys){
                id = id.replace(/[ :]/g,'-');
                var queueKey = schema.device.queue.useParam(id);
                var settingsKey = schema.device.settings.useParam(id);
                if (_.indexOf(keys,settingsKey) >= 0)
                    res.send({error: 'Device ID already used'});
                else {
                    logger.info('device creation(' + id + '): ' + JSON.stringify(req.body));
                    RedisWatcher.instance.client.incr(schema.command.nextRouteID.key).thenHint('nextRoute',function(nextID){
                        RedisWatcher.instance.client.hset(schema.command.routes.key,nextID,queueKey).thenHint('setRoute',function(){
                            changeHash(req,res,settingsKey,_.bind(requestDevice,this,res,id));                   // TODO use device factory
                            // istanbul ignore next - TODO consider how to test
                            if (M2mSupervisor.instance && M2mSupervisor.instance.routeWatcher) M2mSupervisor.instance.routeWatcher.emit('checkReady');
                        })
                    });
                }
            });
    });
});

function requestDevice(res,id){
    requestHash(res,schema.device.settings.useParam(id),'device:' + id,deviceTemplate,function(groups){
        var routingOption = _.detect(groups['Commands'],function(option){ return option.key === 'command:routing'});
        var scheduleOption = _.detect(groups['Commands'],function(option){ return option.key === 'command:schedule'});
        if (routingOption && scheduleOption && scheduleOption.value) routingOption.options.push('scheduled');
    });
}

// SCHEDULES ----------------

declareRouteList('schedules',schema.schedule.periods);

declareRouteGetByID('schedule',function(res,id){
    var factory = new ScheduleFactory(RedisWatcher.instance.client);
    factory.exportSchedules(id,function(schedules){
        var result = {};
        result['schedule:' + id] = schedules;
        res.send(result);
    });
});

// PROFILES ----------------

declareRouteList('profiles',schema.schedule.periods);

declareRouteGetByID('profile',function(res,id){
    requestHash(res,schema.command.profile.useParam(id),'profile:' + id,null);
});

declareRouteGetByID('options',function(res,id){
    requestHash(res,schema.command.options.useParam(id),'options:' + id,null);
});

declareRouteGetByID('definitions',function(res,id){
    requestHash(res,schema.command.definitions.useParam(id),'definitions:' + id,null);
});<commit_msg>correct parameter list<commit_after>var _ = require('lodash');

var ScheduleFactory = require('../lib/schedule-factory');
var ProxyHelper = require('../lib/proxy-helper');
var RedisWatcher = require('../services/redis-watcher');
var M2mSupervisor = require('../processes/m2m-supervisor');

var logger = require('../lib/logger')('api');
var schema = require('../lib/redis-schema');
var helpers = require('../lib/hash-helpers');
var configTemplate = require('../lib/config-hashkeys');
var deviceTemplate = require('../lib/device-hashkeys');

var express = require('express');
var router = express.Router();

function checkRedis(callback){
    if (!RedisWatcher.instance) new RedisWatcher();
    if (RedisWatcher.instance.started())
        callback();
    else {
        RedisWatcher.instance.start();
        _.defer(callback);
    }
}

function requireRedis(res,callback){
    checkRedis(function(){
        if (RedisWatcher.instance.ready())
            callback();
        else
            res.send({error: 'Redis not ready'});
    });
}

function requestHash(res,hashKey,resultKey,template,filter){
    RedisWatcher.instance.client.hgetall(hashKey).thenHint('requestHash - ' + resultKey,function(hash){
        var result = {};
        result[resultKey] = template ? helpers.hash2groups(hash || {},template) : hash;
        filter && filter(result[resultKey]);
        res.send(result);
    });
}

function updateHash(updates,callback){
    RedisWatcher.instance.client.send('hmset',updates).thenHint('updateHash',function () {
        callback();
    });
}

function changeHash(req,res,hashKey,callback){
    helpers.hash2sets(hashKey,req.body,function(updates,deletes){
        if (!updates && !deletes)
            res.send({error: 'No changes requested'});
        else if (!deletes)
            updateHash(updates,callback);
        else
            RedisWatcher.instance.client.send('hdel',deletes).thenHint('deleteHash',function(){
                if (updates)
                    updateHash(updates,callback);
                else
                    callback();
            });
    });
}

function findIDs(keyPattern,callback){
    RedisWatcher.instance.client.keys(keyPattern).thenHint('findIDs - ' + keyPattern,callback);
}

function declareRouteList(model,allowProxy,schemaEntry){
    router.get('/' + model,function(req,res,next){
        if (allowProxy && req.session.proxy) return proxiedGET(req.session.proxy,'/' + model,res);

        requireRedis(res,function(){
            findIDs(schemaEntry.keysPattern(),function(keys){
                var result = {};
                result[model] = _.map(keys,function(key){ return schemaEntry.getParam(key); });
                res.send(result);
            })
        });
    });
}

function declareRouteGetByID(model,allowProxy,responder){
    router.get('/' + model + '/:id',function(req,res,next){
        if (allowProxy && req.session.proxy) return proxiedGET(req.session.proxy,'/' + model + '/' + req.params.id,res);

        requireRedis(res,function(){
            responder(res,req.params.id);
        });
    });
}

// CHECK ----------------

router.get('/check',function(req,res,next){
    res.send({});
});

// PROXY ----------------

router.get('/proxy',function(req,res,next){
    setProxy(req,res,req.query);
});

router.post('/proxy',function(req,res,next){
    setProxy(req,res,req.body);
});

function setProxy(req,res,config){
    requireRedis(res,function() {
        new ProxyHelper(RedisWatcher.instance.client,config).checkConfig(function (error, config) {
            req.session.proxy = config;
            if (error)
                throw(error);
            else
                res.redirect('/');
        });
    });
}

function proxiedGET(proxy,path,res){
    new ProxyHelper(RedisWatcher.instance.client,proxy).get(path,res);
}

function proxiedPOST(proxy,path,body,res){
    new ProxyHelper(RedisWatcher.instance.client,proxy).post(path,body,res);
}

// CONFIG ----------------

router.get('/config',function(req,res,next){
    if (req.session.proxy) return proxiedGET(req.session.proxy,'/config',res);

    requireRedis(res,_.bind(requestConfig,this,res));
});

router.post('/config',function(req,res,next){
    if (req.session.proxy) return proxiedPOST(req.session.proxy,'/config',req.body,res);

    requireRedis(res,function(){
        logger.info('config changes: ' + JSON.stringify(req.body));
        changeHash(req,res,schema.config.key,function(){
            // istanbul ignore if - TODO consider how to test...
            if (M2mSupervisor.instance) M2mSupervisor.instance.configWatcher.emit('checkReady');
            requestConfig(res);
        });
    });
});

function requestConfig(res){
    requestHash(res,schema.config.key,'config',configTemplate);
}

// DEVICE ----------------

declareRouteList('devices',true,schema.device.settings);

declareRouteGetByID('device',true,requestDevice);

router.post('/device/:id',function(req,res,next){
    if (req.session.proxy) return proxiedPOST(req.session.proxy,'/device/' + req.params.id,req.body,res);

    requireRedis(res,function(){
        logger.info('device changes(' + req.params.id + '): ' + JSON.stringify(req.body));
        changeHash(req,res,schema.device.settings.useParam(req.params.id),function(){
            requestDevice(res,req.params.id);
            var deviceRouter = null;
            // istanbul ignore next - TODO consider how to test
            if (M2mSupervisor.instance && M2mSupervisor.instance.queueRouter &&
                (deviceRouter = M2mSupervisor.instance.queueRouter.routes[schema.device.queue.useParam(req.params.id)]))
                deviceRouter.settingsWatcher.emit('checkReady');
        });
    });
});

router.get('/device',function(req,res,next){
    if (req.session.proxy) return proxiedGET(req.session.proxy,'/device',null,res);

    requireRedis(res,function(){
        var profileKeys = _.select(_.collect(RedisWatcher.instance.keys,_.bind(schema.command.profile.getParam,schema.command.profile,_)));
        var scheduleKeys = _.select(_.collect(RedisWatcher.instance.keys,_.bind(schema.schedule.periods.getParam,schema.schedule.periods,_)));
        if (profileKeys.length !== 1) {
            var defaults = _.defaults(helpers.hash2groups({},deviceTemplate));
            makeOptionEditable(defaults['Connection']);
            makeOptionEditable(defaults['Commands']);
            res.send({'new-device': defaults});
        } else {
            var profileKey = profileKeys[0];
            var scheduleKey = _.indexOf(scheduleKeys,profileKey) >= 0 ? profileKey : null;
            RedisWatcher.instance.client.hgetall(schema.command.profile.useParam(profileKey)).thenHint('requestHash - ' + profileKey,function(hash){
                hash[deviceTemplate.commands.profile.key] = profileKey;
                hash[deviceTemplate.commands.schedule.key] = scheduleKey;
                var defaults = _.defaults(helpers.hash2groups(hash,deviceTemplate));

                if (scheduleKey) {
                    var routingOption = _.detect(defaults['Commands'],function(option){ return option.key === 'command:routing'});
                    routingOption.options.push('scheduled');
                    routingOption.default = 'scheduled';
                    routingOption.status = 'editable';
                }

                makeOptionEditable(defaults['Connection']);
                deleteOptionExists(defaults['Connection']);
                deleteOptionExists(defaults['Commands']); // TODO figure out how to allow profile & schedule options

                res.send({'new-device': defaults});
            });
        }
    });
});

function makeOptionEditable(options){
    _.each(options,function(option){ option.status = 'editable'; });
}

function deleteOptionExists(options){
    _.each(options,function(option){ delete option.exists; });
}

router.post('/device',function(req,res,next){
    if (req.session.proxy) return proxiedPOST(req.session.proxy,'/device',req.body,res);

    requireRedis(res,function(){
        var id = req.body.id;
        delete req.body.id;
        if (!id)
            res.send({error: 'Device ID not provided'});
        else
            findIDs(schema.device.settings.keysPattern(),function(keys){
                id = id.replace(/[ :]/g,'-');
                var queueKey = schema.device.queue.useParam(id);
                var settingsKey = schema.device.settings.useParam(id);
                if (_.indexOf(keys,settingsKey) >= 0)
                    res.send({error: 'Device ID already used'});
                else {
                    logger.info('device creation(' + id + '): ' + JSON.stringify(req.body));
                    RedisWatcher.instance.client.incr(schema.command.nextRouteID.key).thenHint('nextRoute',function(nextID){
                        RedisWatcher.instance.client.hset(schema.command.routes.key,nextID,queueKey).thenHint('setRoute',function(){
                            changeHash(req,res,settingsKey,_.bind(requestDevice,this,res,id));                   // TODO use device factory
                            // istanbul ignore next - TODO consider how to test
                            if (M2mSupervisor.instance && M2mSupervisor.instance.routeWatcher) M2mSupervisor.instance.routeWatcher.emit('checkReady');
                        })
                    });
                }
            });
    });
});

function requestDevice(res,id){
    requestHash(res,schema.device.settings.useParam(id),'device:' + id,deviceTemplate,function(groups){
        var routingOption = _.detect(groups['Commands'],function(option){ return option.key === 'command:routing'});
        var scheduleOption = _.detect(groups['Commands'],function(option){ return option.key === 'command:schedule'});
        if (routingOption && scheduleOption && scheduleOption.value) routingOption.options.push('scheduled');
    });
}

// SCHEDULES ----------------

declareRouteList('schedules',false,schema.schedule.periods);

declareRouteGetByID('schedule',false,function(res,id){
    var factory = new ScheduleFactory(RedisWatcher.instance.client);
    factory.exportSchedules(id,function(schedules){
        var result = {};
        result['schedule:' + id] = schedules;
        res.send(result);
    });
});

// PROFILES ----------------

declareRouteList('profiles',false,schema.schedule.periods);

declareRouteGetByID('profile',false,function(res,id){
    requestHash(res,schema.command.profile.useParam(id),'profile:' + id,null);
});

declareRouteGetByID('options',false,function(res,id){
    requestHash(res,schema.command.options.useParam(id),'options:' + id,null);
});

declareRouteGetByID('definitions',false,function(res,id){
    requestHash(res,schema.command.definitions.useParam(id),'definitions:' + id,null);
});
{'fraction_non_alphanumeric': 0.16051555217349947, 'fraction_numerical': 0.0017121658898506611, 'mean_word_length': 2.9141846612062547, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 7, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7339680', 'n_tokens_mistral': 6010, 'n_tokens_neox': 5895, 'n_words': 810}