/**
 * deploy-project.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 DError = require('../../core/exception.js');
var Job = require('../dibs.model.common/job.js');
var JobDep = require('../org.tizen.projects/job-dep.js');
var TizenCommon = require('../org.tizen.common/tizen_common.js');

/**
 * Tizen WDK deploy project plugin
 * @module models/tizen-deploy-project/project
 */

module.exports.createJob = createJob;
module.exports.initializeJob = initializeJob;
module.exports.executeJob = executeJob;
module.exports.checkMutualExclusiveJob = checkMutualExclusiveJob;
module.exports.checkJobPrivilege = checkJobPrivilege;

/**
 * Tizen WSDK deploy job
 * @constructor
 * @augments module:models/job
 * @param {module:models/job~Job} baseJob - base job object
 */

function TizenWSDKDeployJob(baseJob) {
    Job.copy(this, baseJob);

    /**
     * compat job
     * @type {module:models/job~Job}
     */
    this.compatJob = null;
    /**
     * snapshot infomation
     * @type {string}
     */
    this.snapshot = null;
    /**
     * package infos from pkginfo.manifest
     * @type {Array}
     */
    //this.options.packages = [];
    /**
     * tizen environments
     * @type {environment}
     */
    this.initEnvironments = dibs.projectTypes['Tizen-WSDK-Deploy'].environments;
}


/**
 * Create Tizen WSDK deploy job instance
 * @function createJob
 * @param {string} userEmail - user email which creates this job
 * @param {string} distName - distribution name
 * @param {string} prjName - project name
 * @param {string} prjType - project type
 * @param {module:models/job.options} options - job options
 * @param {module:lib/utils.callback_error} callback - callback(error)
 * @memberOf module:models/tizen-project/project
 */

function createJob(userEmail, distName, prjName, prjType, environmentName, parentId, distType, options, callback) {
    Job.create(userEmail, distName, distType, prjName, prjType, environmentName, parentId, null, options,
        function (err, newJob) {
            callback(err, new TizenWSDKDeployJob(newJob));
        });
}


/**
 * Initialize Tizen WSDK deploy job
 * @function initializeJob
 * @param {module:models/job~Job} job - job
 * @param {string} jobWorkPath - jobWorkPath
 * @param {module:core/base-server.BaseServer} server - server
 * @param {module:lib/utils.callback_error} callback - callback(error)
 * @memberOf module:models/tizen-project/project
 */
function initializeJob(job, options, callback) {
    var monitor = options.monitor;

    var subJobIds = null;
    var jobEnvironment = null;

    if (job.status === 'INITIALIZED') {
        return callback(new DError('WSDKDEPLOY006', { jobId: job.id }), job);
    }

    async.waterfall([
        function (cb) {
            monitor.updateProgress('add builder servers ...', function () {});

            var deployServers = job.options.DEPLOY_SERVERS;
            var deployServerPort = job.options.BUILDER_PORT;

            async.map(_.uniq(_.pluck(deployServers, 'targetServer')),
                function (serverId, cb1) {
                    // deployServerPort += 10;
                    addBuilderServer(job.id, serverId, deployServerPort, monitor, cb1);
                },
                function (err1, results) {
                    cb(err1, results);
                }
            );
        },
        function (buildServers, cb) {
            monitor.updateProgress('Searching projects...');
            dibs.rpc.datamgr.searchProjects(
                {
                    distName: job.distName,
                    name: job.projectName,
                    type: job.projectType
                },
                function (err, projects) {
                    jobEnvironment = projects[0].environments[0];
                    cb(err, buildServers);
                });
        },
        // create sub jobs
        function (buildServers, cb) {
            monitor.updateProgress('Creating sub jobs...');
            createSubJobs(job.id, jobEnvironment, buildServers, job.options, monitor, cb);
        },
        // wait for sub jobs initialized
        function (jobIds, cb) {
            subJobIds = jobIds;

            monitor.updateProgress('Waiting for intializing sub jobs...');
            waitForSubJobsStatus(subJobIds, 'INITIALIZED', monitor, cb);
        },
        function (cb) {
            monitor.updateProgress('Initializing sub jobs done ', cb);
        },
        // update subJobs
        function (cb) {
            dibs.log.info('update sub-jobs');
            updateSubJobs(job, subJobIds, cb);
        }
    ],
    function (err) {
        if (err) {
            async.waterfall([
                function (cb1) {
                    dibs.log.info('remove deloy servers!');
                    var deployServers = job.options.DEPLOY_SERVERS;

                    async.each(_.uniq(_.pluck(deployServers, 'targetServer')),
                        function (target, cb2) {
                            removeBuilderServer(target + '_' + job.id, cb2);
                        },
                        function (err1) {
                            cb1(err1);
                        }
                    );
                },
                function (cb1) {
                    monitor.updateProgress('Canceling all sub jobs ');
                    cancelAllSubjobs(subJobIds, monitor, cb1);
                },
                function (cb1) {
                    updateSubJobs(job, subJobIds, cb1);
                },
                function (cb1) {
                    monitor.updateProgress('Canceling all sub jobs done!', cb1);
                }
            ],
            function (error) {
                if (error) {
                    monitor.updateProgress('Error :' + error.message);
                }
                callback(err, job);
            });
        } else {
            callback(err, job);
        }
    });
}


/**
 * Execute Tizen WSDK deploy job
 * @function executeJob
 * @param {module:models/job~Job} job - job
 * @param {string} jobWorkPath - jobWorkPath
 * @param {module:core/base-server.BaseServer} server - server
 * @param {module:lib/utils.callback_error} callback - callback(error)
 * @memberOf module:models/tizen-project/project
 */
function executeJob(job, options, callback) {
    var monitor = options.monitor;

    if (job.status !== 'WORKING') {
        return callback(new DError('WSDKDEPLOY007', { jobId: job.id }), job);
    }

    var subJobIds;
    if (job.subjobs) {
        subJobIds = job.subJobs.map(function (subJob) {
            return subJob.id;
        });
    }

    async.series([
        function (cb) {
            // update subJobs: INITIALIZE >> WORKING
            updateSubJobs(job, subJobIds, cb);
        },
        function (cb) {
            monitor.updateProgress('Waiting for executing sub jobs ...');
            waitForSubJobsStatus(subJobIds, 'FINISHED', monitor, cb);
        },
        function (cb) {
            monitor.updateProgress('Waiting for sub jobs finished is done!', cb);
        },
        function (cb) {
            // update subJobs: WORKING >> FINISHED
            updateSubJobs(job, subJobIds, cb);
        },
        function (cb) {
            job.options.packages = [];
            _.each(job.subJobs, function (j) {
                if(j && j.options && j.options.packages) {
                    job.options.packages.push(j.options.packages);
                }
            });

            monitor.updateProgress('Terminating Tizen-WSDK-Deploy job...', cb);
        }
    ],
    function (err) {
        if (err) {
            async.waterfall([
                function (cb1) {
                    dibs.log.info('remove deloy servers!');
                    var deployServers = job.options.DEPLOY_SERVERS;

                    async.each(_.uniq(_.pluck(deployServers, 'targetServer')),
                        function (target, cb2) {
                            removeBuilderServer(target + '_' + job.id, cb2);
                        },
                        function (err1) {
                            cb1(err1);
                        }
                    );
                },
                function (cb1) {
                    monitor.updateProgress('Canceling all sub jobs');
                    cancelAllSubjobs(subJobIds, monitor, cb1);
                },
                function (cb1) {
                    updateSubJobs(job, subJobIds, cb1);
                },
                function (cb1) {
                    monitor.updateProgress('Canceling all sub jobs done!');
                    cb1(null);
                },
                function (cb1) {
                    if (dibs.getServersByType('messenger')[0] && job.userEmail !== 'admin@user') {
                        monitor.updateProgress('sending email');
                        TizenCommon.sendEmail(err, job, monitor, cb1);
                    } else {
                        monitor.updateProgress('Failed to send an email', cb1);
                    }
                }
            ],
            function (error) {
                if (error) {
                    monitor.updateProgress('Error :' + error.message);
                }
                TizenCommon.stripJob(job);
                callback(err, job);
            });
        } else {
            TizenCommon.stripJob(job);

            dibs.log.info('remove deloy servers!');
            var deployServers = job.options.DEPLOY_SERVERS;

            async.each(_.uniq(_.pluck(deployServers, 'targetServer')),
                function (target, cb1) {
                    removeBuilderServer(target + '_' + job.id, cb1);
                },
                function (err1) {
                    if (err1) {
                        dibs.log.error('remove deloy server fail!');
                        dibs.log.error(err1);
                    }
                    callback(err, job);
                });
        }

    });
}


function checkMutualExclusiveJob(srcJob, tarJob) {
    if (srcJob.distName !== tarJob.distName) {
        return false;
    }
    if (JobDep.hasSamePackages(srcJob, tarJob) ||
        JobDep.hasBuildDependency(srcJob, tarJob) ||
        JobDep.hasBuildDependency(tarJob, srcJob)) {
        return true;
    }
    return false;
}


function checkJobPrivilege(user, groups, distribution, project, jobOptions, callback) {
    var subJobInfo = jobOptions.SUB_JOBS;
    async.eachSeries(_.pluck(subJobInfo, 'projectName'),
        function (prjName, ecb) {
            if (jobOptions.UPLOAD_TEMP) {
                ecb(null);
            } else {
                dibs.rpc.datamgr.checkJobPrivilege(user.email, distribution.name, prjName, jobOptions, ecb);
            }
        },
        function (err) {
            callback(err);
        });
}


function createSubJobs(parentJobId, parentJobEnv, buildServers, options, monitor, callback) {
    dibs.log.info('create sub-jobs for job #' + parentJobId);

    if (!options || !options.DEPLOY_SERVERS) {
        return callback(new DError('WSDKDEPLOY009', {jobId: parentJobId}), null);
    }

    var subJobPrjType = 'Tizen-WSDK-Deploy-Action';
    var projectEnv = getProjectEnvironment(parentJobEnv.replace('-', ''), subJobPrjType);

    var deployServers = options.DEPLOY_SERVERS;
    var deployServerOrder = _.uniq(_.pluck(_.sortBy(deployServers, 'deployOrder'), 'deployOrder'));
    var jobIds = [];

    dibs.log.info((options.INSTALL === 'TRUE' ? 'install' : 'uninstall') + ' deploy-project serverOrder ... ');
    if (options.INSTALL === 'FALSE') {
        var descendingServerOrder = [];
        for (var i = _.size(deployServerOrder); i > 0; i--) {
            descendingServerOrder.push(deployServerOrder[i - 1]);
        }

        deployServerOrder = descendingServerOrder;
    }

    async.eachSeries(deployServerOrder, function (serverOrder, cb) {
        var serverGroup = _.where(deployServers, {deployOrder: serverOrder});

        async.mapSeries(serverGroup,
            function (deploySvr, cb1) {
                var builderId = _.filter(buildServers, function (server) {
                    return server.indexOf(deploySvr.targetServer) > -1;
                });

                // use project type name to create anonymous projects
                dibs.rpc.jobmgr.addSubJob(parentJobId, subJobPrjType, projectEnv,
                    {
                        BUILDER_ID: builderId[0],
                        TARGET_OS: parentJobEnv,
                        DEPLOY_SERVER: deploySvr,
                        INSTALL: options.INSTALL,
                        SNAPSHOT_NAME: (options.SNAPSHOT_NAME === undefined ? null : options.SNAPSHOT_NAME)
                    },
                    function (err2, jobId) {
                        if (err2) {
                            dibs.log.error(err2);
                        } else {
                            dibs.log.info('assign #job ' + jobId + ' into ' + builderId[0]);
                        }
                        cb1(err2, jobId);
                    }
                );
            },
            function (err1, results) {
                if (err1) {
                    dibs.log.error(err1);
                }
                jobIds.push(results);
                cb(err1);
            });
    },
    function (err) {
        callback(err, jobIds);
    });
}


function cancelAllSubjobs(subJobIds, monitor, callback) {
    if (!subJobIds || subJobIds.length === 0) {
        callback(null);
        // callback(new DError('WSDKDEPLOY014'));
        return;
    }

    async.each(subJobIds,
        function (jobId, cb) {
            monitor.updateProgress(' - sending cancel request for ... ' + jobId);
            dibs.rpc.jobmgr.cancelJob(jobId, cb);
        },
        function (err) {
            if (err) {
                dibs.log.error(err);
            }
            callback(err);
        }
    );
}


function updateSubJobs(parentJob, subJobIds, callback) {
    if (!subJobIds || subJobIds.length === 0) {
        callback(null);
        // callback(new DError('WSDKDEPLOY014'));
        return;
    }

    async.mapLimit(subJobIds, 5,
        function (jobId, cb) {
            dibs.rpc.jobmgr.queryJob(jobId, cb);
        },
        function (err, subJobs) {
            if (!err) {
                parentJob.subJobs = subJobs;
            }
            callback(err);
        }
    );
}


function waitForSubJobsStatus(jobIds, jobStatus, monitor, callback) {
    if (!jobIds || jobIds.length === 0) {
        callback(new DError('WSDKDEPLOY014'));
        return;
    }

    async.each(jobIds,
        function (jobId, cb) {
            dibs.rpc.jobmgr.waitForJobStatus(jobId, jobStatus, function (err1, status) {
                if (err1) {
                    if (status && status !== jobStatus) {
                        monitor.updateProgress(' - failed with status \'' + status + '\' ...' + jobId);
                        if (jobStatus === 'INITIALIZED') {
                            cb(new DError('WSDKDEPLOY003', { jobId: jobId }));
                        } else if (jobStatus === 'FINISHED') {
                            cb(new DError('WSDKDEPLOY011', { jobId: jobId }));
                        }
                    } else {
                        monitor.updateProgress(' - failed with error \'' + err1.errno + '\' ...' + jobId);
                        cb(err1);
                    }
                } else {
                    monitor.updateProgress(' - succeeded ...' + jobId);
                    cb(err1);
                }
            });
        },
        function (err) {
            if (err) {
                monitor.updateProgress('Canceling all sub jobs...');
                cancelAllSubjobs(jobIds, monitor, function (err2) {
                    if (err2) {
                        dibs.log.error(err2);
                    }
                    monitor.updateProgress('Canceling all sub jobs done!');
                    callback(err);
                });
            } else {
                callback(err);
            }
        }
    );
}


function getProjectEnvironment(environmentName, projectType) {
    // var prjExt = utils.getEnvironmentIds(projectType);

    var exts = dibs.plugin.getExtensions('dibs.base.projectType');
    var prjExt = _.find(exts, function (ext) {
        return ext.projectType === projectType;
    });

    if (prjExt === undefined) {
        dibs.log.error('cannot get projectType plugin for ' + projectType);
        return null;
    }

    var projectEnvs = _.filter(prjExt.environments, function (env) {
        return env.indexOf(environmentName) > 0;
    });

    if (_.size(projectEnvs) > 0) {
        exts = dibs.plugin.getExtensions('dibs.base.environment');
        var env = _.find(exts, function (ext) {
            return ext.id === projectEnvs[0];
        });

        if (env) {
            return env.name;
        } else {
            return null;
        }
    } else {
        dibs.log.error('cannot get environment info from project ' + projectType);
        return null;
    }
}


function addBuilderServer(jobId, serverId, serverPort, monitor, callback) {
    async.waterfall([
        function (cb) {
            dibs.rpc.datamgr.searchServers({id: serverId}, function (err1, serverInfo) {
                if (err1) {
                    dibs.log.error('Failed to search server using ' + serverId);
                    cb(err1, null);
                } else {
                    cb(err1, serverInfo[0]);
                }
            });
        },
        function (server, cb) {
            var serverHostInfo = {
                id: serverId + '_' + jobId,
                host: server.host,
                port: serverPort
            };

            monitor.updateProgress('add deployment server ...' + serverHostInfo.id, function () {});
            dibs.log.info('add a deployment server for ' + serverHostInfo.id);

            dibs.rpc.master.addServer('builder', serverHostInfo, { parent_id: serverId, max_jobs: 1 }, function (err) {
                if (err) {
                    monitor.updateProgress('Failed to add the deployment server ' + serverHostInfo.id, function () {});
                    dibs.log.error('Failed to add the deployment server ' + serverHostInfo.id);
                    cb(new DError('WSDKDEPLOY019', {serverId: serverId}), null);
                } else {
                    monitor.updateProgress('Add the deployment server successfully ' + serverHostInfo.id);
                    cb(err, serverHostInfo.id);
                }
            });
        },
        function (serverId, cb) {
            dibs.log.info('wait for builder status ' + serverId);
            dibs.rpc.jobmgr.waitForBuilderStatus(serverId, function (err) {
                cb(err, serverId);
            });
        }
    ],
    function (err, serverId) {
        if (err) {
            dibs.log.error(err);
        }
        callback(err, serverId);
    });
}


function removeBuilderServer(serverId, callback) {
    dibs.rpc.datamgr.searchServers({id: serverId}, function (err, serverInfo) {
        if (err) {
            dibs.log.error(err);
            callback(err);
        } else {
            if (_.size(serverInfo) === 0) {
                dibs.log.warn('cannot search servers by ' + serverId);
                callback(new DError('WSDKDEPLOY013', {serverId: serverId}));
            } else {
                dibs.log.info('remove build server for ' + serverInfo[0].id);
                dibs.rpc.master.removeServer(serverInfo[0].id, {cleanResource: false}, function (err) {
                    callback(err);
                });
            }
        }
    });
}
