/**
 * distribution.js
 * Copyright (c) 2000 - 2017 Samsung Electronics Co., Ltd. All rights reserved.
 *
 * Contact:
 * Sungmin Kim <sm.art.kim@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 Job = require('./job.js');
var Project = require('./project.js');
var dibs = require('../../core/dibs.js');
var DError = require('../../core/exception.js');
var utils = require('../../lib/utils.js');

/**
 * @module models/distribution
 */

/**
 * @typedef {object} options
 * @property {string} key - key : value format Hash object for distribution option
 * @memberOf module:models/distribution
 */

/**
 * @constructor
 * @param {string} name
 * @param {module:models/distribution.options} options
 * @memberOf module:models/distribution
 */

function Distribution(id, groupName, name, type, status, access, sync, description, options) {
    var self = this;
    /** id {number} */
    this.id = id || null;
    /** groupName {string} */
    this.groupName = groupName;
    /** name {string} */
    this.name = name;
    /** type {string} */
    this.type = type;
    /** status {string} */
    this.status = status || 'OPEN';
    /** access {string} */
    this.access = access || 'PRIVATE';
    /** sync {string} */
    this.sync = sync;
    /** description {string} */
    this.description = description;
    /** type {module:models/distribution.options} */
    this.options = options;
    /** projects {object} */
    this.projects = {};
}

/**
 * @callback callback_err_distribution
 * @param {error|undefined} error
 * @param {module:models/distribution.Distribution} distribution
 * @memberOf module:models/distribution
 */

/**
 * @function create
 * @param {string} name
 * @param {string} options
 * @param {module:models/distribution.callback_error_distribution} callback - callback
 * @memberOf module:models/distribution
 */

function create(id, groupName, name, type, status, access, sync, description, options, callback) {
    var error;
    if (!dibs.repositoryTypes[type]) {
        error = new DError('DIST004', {
            type: type
        });
        callback(error, null);
        return;
    } else {
        var property = dibs.repositoryTypes[type].property;
        // set Default value
        if (property) {
            _.each(property, function (value, key) {
                if (_.has(value, 'default')) {
                    options[key] = options[key] || value['default'];
                }
            });
        }

        // add default property
        if (options && !options['JOB_ENV']) {
            options['JOB_ENV'] = '{}';
        }
    }

    // verify
    if (!name || !options) {
        error = new DError('DIST001', {
            name: name,
            options: options
        });
        callback(error, null);
    } else {
        callback(null, new Distribution(id, groupName, name, type, status, access, sync, description, options));
    }
}
module.exports.create = create;

/**
 * @callback callback_err_distributionid
 * @param {error|undefined} error
 * @param {string} distribution id
 * @memberOf module:models/distribution
 */


/**
 * @function insert
 * @param {object} conn - db connection object
 * @param {module:models/distribution.Distribution} dist - dist
 * @param {module:models/distribution.callback_err_distributionid} callback - callback
 * @memberOf module:models/distribution
 */

module.exports.insert = insert;
function insert(conn, dist, callback) {
    var id = null;
    async.waterfall([
        function (cb) {
            conn.query('START TRANSACTION', function (err) {
                cb(err);
            });
        },
        function (cb) {
            var sql = 'INSERT INTO distributions SET ' +
                'name =' + utils.DBStr(dist.name) +
                ', type =' + utils.DBStr(dist.type) +
                ', group_id = (SELECT id FROM groups WHERE name = ' + utils.DBStr(dist.groupName) + ')';
            if (dist.status !== undefined) {
                if (dist.status.toUpperCase() === 'OPEN') {
                    sql += ', status = ' + utils.DBStr(dist.status);
                } else {
                    sql += ', status = \'CLOSE\' ';
                }
            }

            if (dist.access !== undefined) {
                sql += ', access = ' + utils.DBStr(dist.access);
            }

            if (dist.sync !== undefined) {
                sql += ', sync = ' + utils.DBStr(dist.sync);
            }

            if (dist.description !== undefined) {
                sql += ', description = ' + utils.DBStr(dist.description);
            }
            conn.query(sql, function (err, result) {
                cb(err, result);
            });
        },
        function (result1, cb) {
            var id = result1.insertId;
            if (_.size(dist.options) !== 0) {
                async.map(_.keys(dist.options),
                    function (index, mcb) {
                        var strNtype = utils.objectToStringAndType(dist.options[index]);
                        var sql = 'INSERT INTO distribution_info SET ' +
                            'distribution_id = ' + id +
                            ', property = ' + utils.DBStr(index) +
                            ', value = ' + utils.DBStr(strNtype.string) +
                            ', type = ' + utils.DBStr(strNtype.type);
                        conn.query(sql, function (err, result) {
                            mcb(err, result);
                        });
                    }, function (err, result) {
                        cb(err, id);
                    });
            } else {
                cb(null, id);
            }
        },
        function (distId, cb) {
            if (dist.options.cloneProject) {
                cloneProjects(conn, dist.options.cloneProject, dist, function (err) {
                    cb(err, distId);
                });
            } else {
                cb(null, distId);
            }
        },
        function (id, cb) {
            conn.query('COMMIT', function (err) {
                cb(err, id);
            });
        }
    ], function (err, id) {
        if (err) {
            conn.query('ROLLBACK', function () {});
        }
        callback(err, id);
    });
}

function cloneProjects(conn, sourceDistName, targetDistribution, callback) {
    async.waterfall([
        function (cb1) {
            select(conn, {
                name: sourceDistName
            }, function (err, distributions) {
                if (distributions.length != 1) {
                    cb1(new DError('DIST005', {
                        name: sourceDistName
                    }, err), null);
                } else {
                    var sourceDistribution = distributions[0];
                    if (sourceDistribution.type != targetDistribution.type) {
                        cb1(new DError('DIST006', {
                            sourceType: sourceDistribution.type,
                            targetType: targetDistribution.type
                        }, err), null);
                    } else {
                        cb1(null, sourceDistribution);
                    }
                }
            });
        },
        function (sourceDistribution, cb1) {
            Project.select(conn, {
                distName: sourceDistName
            }, cb1);
        },
        function (projects, cb1) {
            async.eachSeries(projects,
                function (project, cb2) {
                    Project.clone(conn, sourceDistName, project.name, targetDistribution.name, project.name, function (err, prj) {
                        cb2(err);
                    });
                },
                function (err) {
                    cb1(err);
                });
        }], function (err) {
        callback(err);
    });
}

/**
 * @function update
 * @param {object} conn - db connection object
 * @param {module:models/distribution.Distribution} dist - dist
 * @param {module:models/distribution.callback_err_distributionid} callback - callback
 * @memberOf module:models/distribution
 */

module.exports.update = function (conn, dist, callback) {
    var oldPropertyList = null;

    async.waterfall([
        function (wcb) {
            conn.query('START TRANSACTION', function (err) {
                wcb(err);
            });
        },
        function (wcb) {
            var stmt = 'UPDATE distributions SET ';
            stmt += 'group_id = (SELECT id FROM groups WHERE name = ' + utils.DBStr(dist.groupName) + '), ';
            stmt += 'name = ' + utils.DBStr(dist.name) + ', ';
            stmt += 'description = ' + utils.DBStr(dist.description) + ', ';
            stmt += 'access = ' + utils.DBStr(dist.access) + ', ';
            stmt += 'sync = ' + utils.DBStr(dist.sync) + ', ';
            stmt += 'status = ' + utils.DBStr(dist.status) + ' ';
            stmt += 'WHERE id = ' + dist.id;
            conn.query(stmt, function (err, result) {
                wcb(err);
            });
        },
        function (wcb) {
            var stmt = 'SELECT * FROM distribution_info ' +
                'WHERE distribution_id = ' + dist.id;
            conn.query(stmt, function (err, result) {
                if (!err) {
                    oldPropertyList = result;
                }

                wcb(err);
            });
        },
        function (wcb) {
            if (_.size(dist.options) !== 0) {
                async.map(_.keys(dist.options),
                    function (index, mcb) {
                        var stmt = 'SELECT * FROM distribution_info ' +
                            'WHERE distribution_id = ' + dist.id + ' AND property = ' + utils.DBStr(index);
                        conn.query(stmt, function (err, result) {
                            var stmt2 = null;
                            var strNtype = utils.objectToStringAndType(dist.options[index]);
                            if (err) {
                                mcb(new DError('MODEL003', {
                                    sql: stmt
                                }, err));
                                return;
                            }

                            if (result.length === 0) {
                                stmt2 = 'INSERT INTO distribution_info SET ' +
                                    'distribution_id = ' + dist.id +
                                    ', property = ' + utils.DBStr(index) +
                                    ', value = ' + utils.DBStr(strNtype.string) +
                                    ', type = ' + utils.DBStr(strNtype.type);
                            } else {
                                stmt2 = 'UPDATE distribution_info SET ' +
                                    'value = ' + utils.DBStr(strNtype.string) + ',' +
                                    'type = ' + utils.DBStr(strNtype.type) + ' ' +
                                    'WHERE distribution_id = ' + dist.id + ' AND property = ' + utils.DBStr(index);
                            }
                            conn.query(stmt2, function (err, result) {
                                if (err) {
                                    mcb(new DError('MODEL003', {
                                        sql: stmt2
                                    }, err));
                                } else {
                                    mcb(err);
                                }
                            });
                        });
                    }, function (err, result) {
                        wcb(err);
                    });
            } else {
                wcb(null);
            }
        },
        function (wcb) {
            var deleteList = oldPropertyList.filter(function (old) {
                return (!dist.options ||
                    Object.keys(dist.options).indexOf(old.property) === -1);
            });
            async.eachSeries(deleteList,
                function (entry, ecb) {
                    var sql = 'DELETE FROM distribution_info ' +
                        ' WHERE id = ' + entry.id;

                    conn.query(sql, function (err) {
                        if (err) {
                            ecb(new DError('MODEL003', {
                                sql: sql
                            }, err));
                        } else {
                            ecb(null);
                        }
                    });
                },
                function (err) {
                    wcb(err);
                });
        },
        function (wcb) {
            conn.query('COMMIT', wcb);
        }
    ], function (err) {
        if (err) {
            conn.query('ROLLBACK', function () {});
        }
        callback(err);
    });
};

/**
 * @function select
 * @param {object} conn - db connection object
 * @param {module:models/distribution.jobQuerySql} condition - condition
 * @param {module:lib/utils.callback_error} callback - callback
 * @memberOf module:models/user
 */

function select(conn, condition, callback) {
    var i;
    var where = '';
    var where_list = [];

    var key_list = ['id', 'group_id', 'name', 'type', 'status', 'access', 'sync', 'description'];
    for (i = 0; i < key_list.length; i++) {
        var key = key_list[i];
        if (condition[key] !== undefined) {
            if (key === 'id') {
                where_list.push('distributions.' + key + ' = ' + condition[key] + ' ');
            } else {
                where_list.push('distributions.' + key + ' = ' + utils.DBStr(condition[key]));
            }
        }
    }

    // limit info
    var limit = '';
    if (condition.count !== undefined) {
        if (condition.offset !== undefined) {
            limit += (' LIMIT ' + condition.offset + ' , ' + condition.count);
        } else {
            limit += (' LIMIT ' + condition.count);
        }
    }

    if (where_list.length > 0) {
        where = ' WHERE distributions.group_id = groups.id AND ' + where_list.join(' AND ');
    } else {
        where = ' WHERE distributions.group_id = groups.id ';
    }

    var sql = 'SELECT distributions.id' +
        ', distributions.group_id' +
        ', distributions.name' +
        ', distributions.type' +
        ', distributions.status' +
        ', distributions.access' +
        ', distributions.sync' +
        ', distributions.description' +
        ', groups.name AS groupName' +
        ' FROM distributions, groups ' +
        where + limit;
    conn.query(sql, function (err, distributions) {
        if (err) {
            callback(new DError('MODEL003', {
                sql: sql
            }, err));
            return;
        }
        async.map(distributions, function (distribution, cb) {
            var sql = 'SELECT * FROM distribution_info WHERE distribution_id =' + distribution.id;
            conn.query(sql, function (err, distribution_info) {
                if (err) {
                    callback(new DError('MODEL003', {
                        sql: sql
                    }, err));
                    return;
                }

                var options = {};
                for (i = 0; i < distribution_info.length; i++) {
                    options[distribution_info[i].property] = utils.stringAndTypeToObj(distribution_info[i].value, distribution_info[i].type);
                }

                create(distribution.id, distribution.groupName, distribution.name, distribution.type, distribution.status, distribution.access, distribution.sync, distribution.description, options, function (err, distributionObj) {
                    cb(null, distributionObj);
                });
            });
        },
            function (err, results) {
                if (err) {
                    callback(new DError('USER002', {
                        condition: condition
                    }, err));
                    return;
                }
                callback(null, results);
            });
    });
}

/**
 * @callback callback_err_distributions
 * @param {error|undefined} error
 * @param {Array.<module:models/distribution.Distribution>} list of distribution
 * @memberOf module:models/distribution
 */

/**
 * @function selectAll
 * @param {object} conn - db connection object
 * @param {module:models/distribution.callback_err_distributions} callback - callback
 * @memberOf module:models/distribution
 */


module.exports.selectAll = function (conn, callback) {
    var sql = 'SELECT distributions.*, groups.name AS groupName FROM distributions, groups WHERE distributions.group_id = groups.id ';
    conn.query(sql, function (err, distRows) {
        if (err) {
            callback(err, null); return;
        }
        async.map(distRows,
            function (dist, cb) {
                sql = 'SELECT distribution_info.property,distribution_info.value,distribution_info.type ' +
                    'FROM distribution_info ' +
                    'WHERE distribution_id = ' + utils.DBStr(dist.id);
                conn.query(sql, function (err, optionList) {
                    if (!err) {
                        var options = {};
                        for (var idx in optionList) {
                            options[optionList[idx].property] = utils.stringAndTypeToObj(optionList[idx].value, optionList[idx].type);
                        }
                        // NOTE. If new field added, MUST add it here
                        create(dist.id, dist.groupName, dist.name, dist.type, dist.status, dist.access, dist.sync, dist.description, options, function (err1, newDist) {
                            newDist.id = dist.id;
                            newDist.status = dist.status;
                            cb(err1, newDist);
                        });
                    } else {
                        cb(err, null);
                    }
                });

            },
            function (error, distributions) {
                callback(error, distributions);
            });
    });
};

/**
 * @function selectByUserAccess
 * @param {object} conn - db connection object
 * @param {module:models/distribution.callback_err_distributionList} callback - callback
 * @memberOf module:models/distribution
 */
module.exports.selectByPrivilege = function (conn, condition, callback) {
    var email = cond.email;
    var privilegeDistributions = [];
    var hasProjectPrivilege = 0;
    var hasOwnerPrivilege = 0;
    select(conn, condition, function (err, distributions) {
        if (err) {
            callback(err, distributions);
            return;
        }

        async.eachSeries(distributions,
            function (distribution, ecb) {
                async.series([
                    function (scb) {
                        var sql = 'SELECT COUNT(*) cnt ' +
                            'FROM distributions ' +
                            ', projects ' +
                            ', group_project ' +
                            ', groups ' +
                            ', user_group ' +
                            ', users ' +
                            'WHERE distributions.id = projects.distribution_id ' +
                            ' AND projects.id = group_project.project_id ' +
                            ' AND groups.id = group_project.group_id ' +
                            ' AND groups.id = user_group.group_id ' +
                            ' AND users.id = user_group.user_id ' +
                            ' AND users.email = ' + utils.DBStr(email) +
                            ' AND distributions.id = ' + distribution.id;
                        conn.query(sql, function (err, result) {
                            hasProjectPrivilege = result[0].cnt;
                            scb(err);
                        });
                    },
                    function (scb) {
                        var sql = 'SELECT COUNT(*) cnt ' +
                            'FROM distributions ' +
                            ', groups ' +
                            ', user_group ' +
                            ', users ' +
                            'WHERE distributions.group_id = user_group.group_id ' +
                            ' AND groups.id = user_group.group_id ' +
                            ' AND users.id = user_group.user_id ' +
                            ' AND users.email = ' + utils.DBStr(email) +
                            ' AND distributions.id = ' + distribution.id;
                        conn.query(sql, function (err, result) {
                            hasOwnerPrivilege = result[0].cnt;
                            scb(err);
                        });
                    }],
                    function (err, isPrivilege) {
                        if (privilege === 'CREATE' &&
                            hasAdminPrivilege) {
                            privilegeDistributions.push(distribution);
                        } else if (privilege === 'READ' &&
                            (hasProjectPrivilege || hasOwnerPrivilege)) {
                            privilegeDistributions.push(distribution);
                        } else if (privilege === 'UPDATE' &&
                            (hasAdminPrivilege || hasOwnerPrivilege)) {
                            privilegeDistributions.push(distribution);
                        } else if (privilege === 'DELETE' &&
                            hasAdminPrivilege) {
                            privilegeDistributions.push(distribution);
                        }

                        ecb(err);
                    }
                );
            },
            function (err) {
                callback(err, distributions);
            });
    });

    async.waterfall(
        [
            //Private
            function (cb) {
                var condition = _.extend({
                    access: 'PRIVATE'
                }, cond);
                select(conn, condition, function (err, privateDistributions) {
                    if (err) {
                        cb(err, privateDistributions);
                        return;
                    }

                    var distributions = [];
                    async.eachSeries(privateDistributions,
                        function (distribution, ecb) {
                            async.waterfall([
                                function (wcb) {
                                    var sql = 'SELECT COUNT(*) cnt ' +
                                        'FROM distributions ' +
                                        ', projects ' +
                                        ', group_project ' +
                                        ', groups ' +
                                        ', user_group ' +
                                        ', users ' +
                                        'WHERE distributions.id = projects.distribution_id ' +
                                        ' AND projects.id = group_project.project_id ' +
                                        ' AND groups.id = group_project.group_id ' +
                                        ' AND groups.id = user_group.group_id ' +
                                        ' AND users.id = user_group.user_id ' +
                                        ' AND users.email = ' + utils.DBStr(email) +
                                        ' AND distributions.id = ' + distribution.id;
                                    conn.query(sql, function (err, result) {
                                        if (err) {
                                            wcb(err, null);
                                        } else {
                                            wcb(null, result[0].cnt);
                                        }
                                    });
                                },
                                function (isPrivilege, wcb) {
                                    var sql = 'SELECT COUNT(*) cnt ' +
                                        'FROM distributions ' +
                                        ', groups ' +
                                        ', user_group ' +
                                        ', users ' +
                                        'WHERE distributions.group_id = user_group.group_id ' +
                                        ' AND groups.id = user_group.group_id ' +
                                        ' AND users.id = user_group.user_id ' +
                                        ' AND users.email = ' + utils.DBStr(email) +
                                        ' AND distributions.id = ' + distribution.id;
                                    conn.query(sql, function (err, result) {
                                        if (err) {
                                            wcb(err, null);
                                        } else {
                                            wcb(null, isPrivilege + result[0].cnt);
                                        }
                                    });
                                }],
                                function (err, isPrivilege) {
                                    if (err) {
                                        ecb(err);
                                    } else {
                                        if (isPrivilege > 0) {
                                            distributions.push(distribution);
                                        }
                                        ecb(null);
                                    }
                                }
                            );
                        },
                        function (err) {
                            cb(err, distributions);
                        });
                });
            },
            //Public
            function (distributions, cb) {
                var condition = _.extend({
                    access: 'PUBLIC'
                }, cond);
                select(conn, condition, function (err, publicDistributions) {
                    //Check duplicated item
                    var distribution_id_list = _.pluck(distributions, 'id');
                    _.each(publicDistributions, function (distribution) {
                        if (_.contains(distribution_id_list, distribution.id) !== true) {
                            distributions.push(distribution);
                        }
                    });
                    cb(err, distributions);
                });
            }
        ],
        function (err, results) {
            callback(err, results);
        });
};

module.exports.create = create;
module.exports.select = select;


module.exports.delete = function (conn, condition, callback) {
    conn.query('START TRANSACTION', function (err) {
        if (err) {
            callback(new DError('MODEL001', err), null);
            return;
        }

        // Check condition of key
        if (condition.id === undefined) {
            callback(new DError('DIST003', {
                condition: condition
            }, err), null);
            return;
        }

        // excute transaction sql
        async.waterfall(
            [
                // Find
                function (cb) {
                    select(conn, condition, function (err, items) {
                        if (err) {
                            cb(err, null);
                            return;
                        }
                        if (items.length === 0) {
                            cb(new DError('DIST004', {
                                condition: condition
                            }), null);
                            return;
                        }

                        cb(null, items[0]); // result is only one.
                    });
                },
                // TABLE: distribution_info
                function (item, cb) {
                    var sql = 'DELETE FROM distribution_info WHERE distribution_id = ' + item.id;
                    conn.query(sql, function (err, result) {
                        if (err) {
                            cb(new DError('MODEL003', {
                                sql: sql
                            }, err), null);
                            return;
                        }
                        cb(null, item);
                    });
                },
                // TABLE: projects
                function (item, cb) {
                    Project.select(conn, {
                        distribution_id: item.id
                    }, function (err, prjs) {
                        cb(err, item, prjs);
                    });
                },
                function (item, prjs, cb) {
                    async.eachSeries(prjs,
                        function (prj, cb2) {
                            Project.deleteInternal(conn, {
                                id: prj.id
                            }, cb2);
                        },
                        function (err) {
                            cb(err, item);
                        });
                },
                // TABLE: distributions
                function (item, cb) {
                    var sql = 'DELETE FROM distributions WHERE id = ' + item.id;
                    conn.query(sql, function (err, result) {
                        if (err) {
                            cb(new DError('MODEL003', {
                                sql: sql
                            }, err), null);
                        } else {
                            cb(null, item);
                        }
                    });
                },
                // commit sql
                function (item, cb) {
                    conn.query('COMMIT', function (err) {
                        if (err) {
                            cb(new DError('MODEL002', err), null);
                            return;
                        }
                        cb(err, item);
                    });
                }
            ],
            function (err, result) {
                if (err) {
                    conn.query('ROLLBACK', function () {});
                    callback(new DError('DIST003', {
                        condition: condition
                    }, err), null);
                    return;
                }
                callback(err, result);
            });
    });
};
