/**
 * project-manager.js
 * Copyright (c) 2000 - 2015 Samsung Electronics Co., Ltd. All rights reserved.
 *
 * Contact:
 * DongHee Yang <donghee.yang@samsung.com>
 * Sungmin Kim <sm.art.kim@samsung.com>
 * Jiil Hyoun <jiil.hyoun@samsung.com>
 * Jonghwan Park <iwin100.park@samsung.com>
 * Kitae Kim <kt920.kim@samsung.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * Contributors:
 * - S-Core Co., Ltd
**/

var async = require('async');
var _ = require('underscore');
var dibs = require('../../core/dibs.js');
var Project = require('../../plugins/dibs.model.common/project.js');
var Notification = require('../../plugins/dibs.model.common/notification.js');
var Distribution = require('../../plugins/dibs.model.common/distribution.js');
var DError = require('../../core/exception');

/**
 * @module servers/data-manager/project-manager
 */

/**
 * @constructor
 * @memberOf module:servers/data-manager/project-manager
 */
function ProjectManager(parent) {
    this.server = parent;

    var self = this;
    var distributions = {};
    var isInitialized = false;


    this.initialize = function (callback) {
        self.server.log.info('Initializing project manager ...');

        self.server.getDBHandle().getConnection(function (err, conn) {
            if (err) {
                callback(err);
            } else {
                async.series([
                    function (cb) {
                        selectAllDistributions(conn, cb);
                    },
                    function (cb) {
                        selectAllProjects(conn, cb);
                    }],
                    function (err) {
                        conn.release();
                        callback(err);
                    });
            }
        });
        isInitialized = true;
    };

    this.loadProjects = function (callback) {
        self.server.log.info('Re-loading project list...');

        self.server.getDBHandle().getConnection(function (err, conn) {
            if (err) {
                callback(err);
            } else {
                selectAllProjects(conn, function (err) {
                    if (err) {
                        dibs.log.error(err);
                    }
                    conn.release();
                });
            }
        });
    };


    this.getProject = function (name, distName) {
        var dist = distributions[distName];

        if (dist !== undefined && dist.projects[name] !== undefined) {
            return distributions[distName].projects[name];
        }

        // if not found, check project type & create anonymous project
        if (dist !== undefined && dibs.projectTypes[name] !== undefined &&
            dibs.projectTypes[name].useAnonymousProject === true) {

            return Project.createAnonymous(null, name, null, {}, [], [], [], distName);
        }

        return null;
    };


    this.addProject = function (name, type, options, environments, triggers, notifications, distName, callback) {
        self.server.log.info('Adding project...' + distName + ':' + name);
        var dist = distributions[distName];
        async.waterfall([
            function (cb) {
                if (!dist) {
                    var error = new DError('PRJMGR005', {
                        dName: distName
                    });
                    self.server.log.info(error.message);
                    cb(error);
                } else {
                    cb(null);
                }
            }, function (cb) {
                if (dist.projects[name] !== undefined) {
                    var error = new DError('PRJMGR001', {
                        pName: name,
                        dName: distName
                    });
                    self.server.log.info(error.message);
                    cb(error);
                } else {
                    cb(null);
                }
            }, function (cb) {
                self.server.getDBHandle().getConnection(cb);
            }, function (conn, cb) {
                self.server.log.info('Set notifications ...');
                var notiList = _.map(notifications, function (n) {
                    return Notification.create(n.id, n.groupId, n.event, n.method, n.targetType, n.targetData, n.description);
                });
                //insert notifications infomation
                async.reduce(notiList, null, function (groupId, noti, rcb) {
                    noti.groupId = groupId;
                    Notification.insert(conn, noti, function (err, rst) {
                        if (err) {
                            rcb(new DError('PRJMGR002', {}, err));
                        } else {
                            rcb(err, rst);
                        }
                    });
                }, function (err, groupId) {
                    cb(err, conn, notiList, groupId);
                });
            }, function (conn, notiList, notificationGroupId, cb) {
                // add notificationGroupId info
                for (var i in notifications) {
                    notifications[i] = _.extend(notifications[i], {
                        groupId: notificationGroupId
                    });
                }
                cb(null, conn);
            }, function (conn, cb) {
                self.server.log.info('Creating project ...');
                Project.create(null, name, type, null, options, environments, triggers, notifications, distName, function (err, newPrj) {
                    if (err) {
                        cb(new DError('PRJMGR002', {}, err));
                    } else {
                        cb(err, conn, newPrj);
                    }
                });
            }, function (conn, newPrj, cb) {
                // add to DB
                self.server.log.info('Inserting project into DB ...');
                Project.insert(conn, newPrj, function (error, prjId) {
                    if (error) {
                        cb(error);
                    } else {
                        cb(error, conn, prjId);
                    }
                });
            }, function (conn, prjId, cb) {
                // refresh project infomation
                Project.select(conn, {
                    id: prjId
                }, function (error, prjObj) {
                    if (error) {
                        cb(error);
                    } else {
                        cb(error, conn, prjObj[0]);
                    }
                });
            }], function (err, conn, newPrj) {
            if (conn) {
                conn.release();
            }
            if (err) {
                self.server.log.info('Failed to add project!');
                self.server.log.error(err);
                callback(err, null);
            } else {
                dist.projects[name] = newPrj;

                if (newPrj.triggers) {
                    addTriggerActions(newPrj.triggers, function (err) {
                        if (err) {
                            self.server.log.info('Failed to add trigger!');
                            self.server.log.info('Failed to add project!');
                            self.server.log.error(err);
                        } else {
                            self.server.log.info('Adding trigger successfully!');
                            self.server.log.info('Adding project successfully!');
                        }
                        callback(err, newPrj);
                    });
                } else {
                    self.server.log.info('Adding project successfully!');
                    callback(null, newPrj);
                }
            }
        });
    };


    this.removeProject = function (name, distName, callback) {
        var dist = distributions[distName];
        async.waterfall([
            function (cb) {
                if (dist === undefined) {
                    var error = new DError('PRJMGR005', {
                        dName: distName
                    });
                    self.server.log.info(error.message);
                    cb(error);
                } else {
                    cb(null);
                }
            }, function (cb) {
                if (!dist.projects[name]) {
                    var error = new DError('PRJMGR007', {
                        pName: name,
                        dName: distName
                    });
                    self.server.log.error(error.message);
                    cb(error);
                } else {
                    cb(null);
                }
            }, function (cb) {
                self.server.log.info('Removing project ...');
                self.server.getDBHandle().getConnection(cb);
            }, function (conn, cb) {
                // remove from DB
                Project.delete(conn, {
                    id: dist.projects[name].id
                }, function (err, result) {
                    if (err) {
                        dibs.log.error(err);
                        cb(err);
                    } else {
                        var notifications;
                        if (dist.projects[name].notifications) {
                            notifications = dist.projects[name].notifications;
                        }

                        if (_.size(dist.projects[name].triggers) > 0) {
                            var triggers = dist.projects[name].triggers;

                            removeTriggerActions(triggers, function (err) {
                                if (err) {
                                    self.server.log.info('Failed to remove trigger action');
                                    self.server.log.info('Failed to remove project');
                                    self.server.log.error(err);
                                } else {
                                    self.server.log.info('Removing trigger successfully!');
                                    self.server.log.info('Removing project successfully!');
                                }
                                cb(err, conn, notifications);
                            });
                        } else {
                            self.server.log.info('Removing project successfully!');
                            cb(null, conn, notifications);
                        }
                    }
                });
            }, function (conn, notifications, cb) {
                async.eachSeries(notifications, function (noti, acb) {
                    Notification.delete(conn, {
                        id: noti.id
                    }, acb);
                }, function (err) {
                    if (err) {
                        dibs.log.error(err);
                    }
                    cb(err, conn);
                });
            }], function (err, conn) {
            if (conn) {
                conn.release();
            }
            if (err) {
                self.server.log.info('Removing project failed!');
                callback(err);
            } else {
                delete dist.projects[name];
                self.server.log.info('Removing project succeeded!');
                callback(err);
            }
        });
    };

    this.updateProject = function (prj, callback) {
        var dist = distributions[prj.distName];
        var oldPrj = _.findWhere(dist.projects, {
            id: prj.id
        });
        var currConn = null;

        async.waterfall([
            function (cb) {
                if (dist === undefined) {
                    cb(new DError('PRJMGR005', {
                        dName: prj.distName
                    }));
                } else {
                    cb(null);
                }
            }, function (cb) {
                self.server.getDBHandle().getConnection(cb);
            }, function (conn, cb) {
                currConn = conn;

                // update notification infomation
                // get old noti infomation
                Project.select(conn, {
                    id: prj.id
                }, function (error, prjObj) {
                    if (error) {
                        cb(error);
                    } else {
                        cb(error, conn, prjObj[0].notifications);
                    }
                });
            }, function (conn, oldNotiList, cb) {
                // remove old noti info if exists
                var notiGroupId;
                if (oldNotiList.length > 0) {
                    notiGroupId = oldNotiList[0].groupId;
                    async.eachSeries(oldNotiList, function (oldNoti, ecb) {
                        Notification.delete(conn, {
                            id: oldNoti.id
                        }, ecb);
                    }, function (error) {
                        cb(error, conn, notiGroupId);
                    });
                } else {
                    cb(null, conn, null);
                }
            }, function (conn, notiGroupId, cb) {
                // insert new noti info
                var notiList = _.map(prj.notifications, function (n) {
                    return Notification.create(n.id, notiGroupId, n.event, n.method, n.targetType, n.targetData, n.description);
                });
                async.reduce(notiList, notiGroupId, function (groupId, noti, rcb) {
                    noti.groupId = groupId;
                    Notification.insert(conn, noti, function (err, rst) {
                        if (err) {
                            rcb(new DError('PRJMGR002', {}, err));
                        } else {
                            rcb(err, rst);
                        }
                    });
                }, function (err, groupId) {
                    cb(err, conn, groupId);
                });
            }, function (conn, notificationGroupId, cb) {
                // add notificationGroupId info
                for (var i in prj.notifications) {
                    prj.notifications[i] = _.extend(prj.notifications[i], {
                        groupId: notificationGroupId
                    });
                }
                cb(null, conn);
            }, function (conn, cb) {
                // create new OBJ
                self.server.log.info('updating project ... ' + prj.id + '(' + prj.name + ')');
                Project.create(prj.id, prj.name, prj.type, prj.status, prj.options, prj.environments, prj.triggers, prj.notifications, prj.distName, function (err, newPrj) {
                    if (err) {
                        cb(new DError('PRJMGR002', {}, err));
                    } else {
                        cb(err, conn, newPrj);
                    }
                });
            }, function (conn, newPrj, cb) {
                // add to DB
                self.server.log.info('updating project into DB ...');
                if (prj.groups) {
                    newPrj.groups = prj.groups;
                }
                Project.update(conn, newPrj, function (err) {
                    if (err) {
                        cb(err);
                    } else {
                        cb(err, conn);
                    }
                });
            }, function (conn, cb) {
                // refresh project infomation
                Project.select(conn, {
                    id: prj.id
                }, function (error, prjObj) {
                    if (error) {
                        cb(error);
                    } else {
                        cb(error, prjObj[0]);
                    }
                });
            }], function (error, newPrj) {
            // release connection
            if (currConn) {
                currConn.release();
            }

            if (error) {
                self.server.log.info('Failed to update project!');
                self.server.log.info(error);
                callback(error);
            } else {
                // update memory
                delete dist.projects[oldPrj.name];
                dist.projects[newPrj.name] = newPrj;

                if (_.size(oldPrj.triggers) > 0 || _.size(newPrj.triggers)) {
                    modifyTriggerActions(oldPrj.triggers, newPrj.triggers, function (err) {
                        if (err) {
                            self.server.log.info('Failed to update triggers!');
                            self.server.log.info('Failed to update project!');
                            self.server.log.error(err);
                        } else {
                            self.server.log.info('Updating triggers successfully!');
                            self.server.log.info('Updating project successfully!');
                        }
                        callback(err, newPrj);
                    });
                } else {
                    self.server.log.info('Updating project successfully!');
                    callback(null, newPrj);
                }
            }
        });
    };

    this.searchProjects = function (cond, callback) {
        var distName = cond.distName;
        var result = [];

        if (distName === undefined) {
            for (distName in distributions) {
                result = result.concat(searchProjectsInDistributionCache(cond, distName));
            }
        } else {
            var dist = distributions[cond.distName];
            if (dist === undefined) {
                var error = new DError('PRJMGR005', {
                    dName: cond.distName
                });
                self.server.log.info(error.message);
                callback(error, null);
                return;
            }

            result = searchProjectsInDistributionCache(cond, distName);
        }

        self.server.getDBHandle().getConnection(function (err, conn) {
            if (err) {
                return callback(err);
            }

            async.mapSeries(result,
                function (cachedPrj, cb) {
                    // refresh project infomation
                    Project.select(conn, {
                        id: cachedPrj.id
                    }, function (err1, prjObjs) {
                        if (err1) {
                            cb(err1, null);
                        } else {
                            cb(err1, prjObjs[0]);
                        }
                    });
                },
                function (err, projects) {
                    conn.release();
                    if (!err) {
                        projects.forEach(function (prj) {
                            var distName = prj.distName;
                            distributions[distName].projects[prj.name] = prj;
                        });
                    }
                    callback(null, projects);
                });
        });
    };


    this.getDistribution = function (distName) {
        if (distributions[distName] !== undefined) {
            return distributions[distName];
        } else {
            return null;
        }
    };


    this.addDistribution = function (groupName, name, type, status, access, sync, description, options, callback) {
        if (!dibs.repositoryTypes[type]) {
            var error = new DError('PRJMGR008', {
                dType: type
            });
            self.server.log.info(error.message);
            callback(error, null);
            return;
        }

        if (distributions[name] !== undefined) {
            var error2 = new DError('PRJMGR003', {
                dName: name
            });
            self.server.log.info(error2.message);
            callback(error2, null);
            return;
        }

        if (description === undefined || description === null) {
            description = 'N/A';
        }

        self.server.log.info('Adding distribution ...');
        self.server.getDBHandle().getConnection(function (err, conn) {
            if (err) {
                callback(err, null);
            } else {
                // create new OBJ
                self.server.log.info('Creating distribution ...');
                Distribution.create(null, groupName, name, type, status, access, sync, description, options, function (err, newDist) {
                    if (err) {
                        var error = new DError('PRJMGR004', {}, err);
                        conn.release();
                        callback(error, null);
                    } else {
                        // add to DB
                        self.server.log.info('Inserting distribution into DB ...');
                        Distribution.insert(conn, newDist, function (err, id) {
                            newDist.id = id;
                            conn.release();
                            if (err) {
                                callback(err, null);
                            } else {
                                self.server.log.info('Adding distribution succeeded!');
                                distributions[name] = newDist;
                                callback(null, newDist);
                            }
                        });
                    }
                });
            }
        });
    };


    this.removeDistribution = function (name, callback) {

        var dist = distributions[name];
        if (dist === undefined) {
            var error = new DError('PRJMGR005', {
                dName: name
            });
            self.server.log.info(error.message);
            callback(error);
            return;
        }

        self.server.log.info('Removing distribution ...');
        self.server.getDBHandle().getConnection(function (err, conn) {
            if (err) {
                callback(err);
            } else {
                // remove from DB
                Distribution.delete(conn, {
                    id: dist.id
                }, function (err) {
                    conn.release();
                    if (err) {
                        dibs.log.error(err);
                        callback(err);
                    } else {
                        self.server.log.info('Removing distribution succeeded!');
                        delete distributions[name];
                        callback(null);
                    }
                });
            }
        });
    };

    this.updateDistribution = function (dist, callback) {

        var name = dist.name;
        if (distributions[name] === undefined) {
            var error = new DError('PRJMGR005', {
                dName: name
            });
            self.server.log.info(error.message);
            callback(error, null);
            return;
        }

        self.server.log.info('Modifying distribution ...');
        self.server.getDBHandle().getConnection(function (err, conn) {
            if (err) {
                var error = new DError('PRJMGR004', {}, err);
                callback(error, null);
            } else {
                distributions[name].groupName = dist.groupName;
                distributions[name].status = dist.status;
                distributions[name].access = dist.access;
                distributions[name].sync = dist.sync;
                distributions[name].description = dist.description;
                distributions[name].options = dist.options;

                self.server.log.info('Updating distribution into DB ...');
                Distribution.update(conn, distributions[name], function (err) {
                    conn.release();
                    if (err) {
                        callback(err, null);
                    } else {
                        self.server.log.info('Modifying distribution succeeded!');
                        callback(null, distributions[name]);
                    }
                });
            }
        });
    };


    this.searchDistributions = function (cond, callback) {

        var result = [];
        for (var name in distributions) {
            var dist = distributions[name];

            if (cond.id !== undefined && cond.id !== dist.id) {
                continue;
            }
            if (cond.name !== undefined && cond.name !== dist.name) {
                continue;
            }
            if (cond.type !== undefined && cond.type !== dist.type) {
                continue;
            }
            if (cond.status !== undefined && cond.status !== dist.status) {
                continue;
            }
            if (cond.access !== undefined && cond.access !== dist.access) {
                continue;
            }
            if (cond.sync !== undefined && cond.sync !== dist.sync) {
                continue;
            }

            result.push(distributions[name]);
        }

        self.server.getDBHandle().getConnection(function (err, conn) {
            if (err) {
                return callback(err);
            }

            async.mapSeries(result,
                function (cachedDist, cb) {
                    Distribution.select(conn, {
                        id: cachedDist.id
                    }, function (err1, distObjs) {
                        if (err1) {
                            cb(err1, null);
                        } else {
                            // NOTE: project information should be set manually
                            distObjs[0].projects = cachedDist.projects;
                            cb(err1, distObjs[0]);
                        }
                    });
                },
                function (err, dists) {
                    conn.release();
                    if (!err) {
                        dists.forEach(function (dist) {
                            distributions[dist.name] = dist;
                        });
                    }
                    callback(err, dists);
                });
        });
    };


    function selectAllDistributions(conn, callback) {
        self.server.log.info('Loading distribution information...');
        Distribution.selectAll(conn, function (err, results) {
            if (err) {
                self.server.log.error('Loading distribution information failed!');
                self.server.log.error(err.message);
                callback(err);
                return;
            }
            for (var i = 0; i < results.length; i++) {
                var dist = results[i];
                self.server.log.info('Loaded distribution:' + dist.name);
                distributions[dist.name] = dist;
            }
            self.server.log.info('Loading distribution information succeeded!');
            callback(null);
        });
    }


    function selectAllProjects(conn, callback) {
        self.server.log.info('Loading project information...');
        Project.selectAll(conn, function (err, results) {
            if (err) {
                self.server.log.error('Loading project information failed!');
                self.server.log.error(err.message);
                callback(err);
                return;
            }
            for (var i = 0; i < results.length; i++) {
                var prj = results[i];
                self.server.log.info('Loaded project:' + prj.name + '(' + prj.distName + ')');
                distributions[prj.distName].projects[prj.name] = prj;
            }
            self.server.log.info('Loading project information succeeded!');
            callback(null);
        });
    }


    function searchProjectsInDistributionCache(cond, distName) {
        var result = [];

        // If you distribution name is not specified, use first one
        if (cond.distName !== undefined && cond.distName !== distName) {
            return [];
        }

        var projects = distributions[distName].projects;

        // by id
        var prjName = null;
        if (cond.id !== undefined) {
            for (prjName in projects) {
                if (Number(projects[prjName].id) === Number(cond.id)) {
                    result.push(projects[prjName]);
                }
            }
            return result;
        }

        // by name
        if (cond.name !== undefined) {
            if (projects[cond.name] !== undefined) {
                return [projects[cond.name]];
            } else {
                return [];
            }
        }

        // by type
        prjName = null;
        if (cond.type !== undefined) {
            for (prjName in projects) {
                if (projects[prjName].type === cond.type) {
                    result.push(projects[prjName]);
                }
            }
            return result;
        }

        // by trigger_type
        if (cond.triggerType !== undefined) {
            _.each(projects, function (project) {
                var projectTriggers = _.where(project.triggers, {
                    type: cond.triggerType
                });
                if (projectTriggers.length !== 0) {
                    result.push(project);
                }
            });

            return result;
        }

        // otherwise, return all
        for (prjName in projects) {
            if (projects.hasOwnProperty(prjName)) {
                result.push(projects[prjName]);
            }
        }
        return result;
    }
}


/**
 * @function create
 * @param {module:core/base-server.BaseServer} parent - parent server
 * @returns {module:servers/data-manager/project-manager.ProjectManager}
 * @memberOf module:servers/data-manager/project-manager
 */

module.exports.create = function (parent) {
    parent.log.info('Creating project manager...');
    return new ProjectManager(parent);
};


function addTriggerActions(triggers, callback) {
    async.each(triggers, function (trigger, cb) {
        var servers = dibs.getServersByType(trigger.serverType);
        if (servers) {
            async.each(servers, function (server, cb1) {
                dibs.log.info('add triggers into ' + trigger.serverType);
                server.addTriggerAction(trigger, cb1);
            }, function (err1) {
                cb(err1);
            });
        } else {
            dibs.log.info('failed to get ' + trigger.serverType + ' server instance');
            cb(null);
        }
    },
        function (err) {
            if (err) {
                dibs.log.error(err);
            }
            callback(err);
        });
}


function removeTriggerActions(triggers, callback) {

    async.each(triggers, function (trigger, cb) {
        var servers = dibs.getServersByType(trigger.serverType);
        if (servers) {
            async.each(servers, function (server, cb1) {
                dibs.log.info('remove ' + trigger.id + ' trigger from ' + trigger.serverType);
                server.removeTriggerAction(trigger.id, cb1);
            }, function (err1) {
                cb(err1);
            });
        } else {
            dibs.log.info('failed to get ' + trigger.serverType + ' server instance');
            cb(null);
        }
    },
        function (err) {
            if (err) {
                dibs.log.error(err);
            }
            callback(err);
        });
}


function modifyTriggerActions(oldTriggers, newTriggers, callback) {
    var modifyList = [];
    var removeList = [];
    var addList = [];

    _.each(oldTriggers, function (trigger) {
        var index = _.findIndex(newTriggers, {
            id: trigger.id
        });

        if (index === -1) {
            removeList.push(trigger);
        } else {
            if (!_.isEqual(trigger, newTriggers[index])) {
                modifyList.push(trigger);
            }
        }
    });

    _.each(newTriggers, function (trigger) {
        var index = _.findIndex(oldTriggers, {
            id: trigger.id
        });

        if (index === -1) {
            addList.push(trigger);
        }
    });

    if (modifyList.length !== 0 || addList.length !== 0) {
        removeList = oldTriggers;
        addList = newTriggers;
    }

    async.waterfall([
        function (cb) {
            async.each(removeList, function (trigger) {
                var servers = dibs.getServersByType(trigger.serverType);
                if (servers) {
                    async.each(servers, function (server, cb1) {
                        server.removeTriggerAction(trigger.id, cb1);
                    },
                        function (err2) {
                            cb(err2);
                        });
                } else {
                    dibs.log.info('failed to get ' + trigger.serverType + ' server instance');
                    cb(null);
                }
            },
                function (err1) {
                    if (err1) {
                        dibs.log.error(err1);
                    }
                    cb(err1);
                });
        },
        function (cb) {
            async.each(addList, function (trigger) {
                var servers = dibs.getServersByType(trigger.serverType);
                if (servers) {
                    async.each(servers, function (server, cb1) {
                        server.addTriggerAction(trigger, cb1);
                    },
                        function (err2) {
                            cb(err2);
                        });
                } else {
                    dibs.log.info('failed to get ' + trigger.serverType + ' server instance');
                    cb(null);
                }
            },
                function (err1) {
                    cb(err1);
                });
        }
    ],
    function (err) {
        if (err) {
            dibs.log.error(err);
        }
        callback(err);
    });
}


function searchTriggers(triggers, callback) {
    callback(null);
}
