/**
 * multi-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 path = require('path');

var DError = require('../../core/exception');
var dibs = require('../../core/dibs');
var Job = require('../dibs.model.common/job.js');
var GitProject = require('./git-project.js');
var JobDep = require('./job-dep.js');
var TizenCommon = require('../org.tizen.common/tizen_common.js');
var Tutils = require('../org.tizen.common/tizen_utils.js');
var ReverseBuildChecker = require('./reverse-build.js');

/**
 * Tizen multi project plugin
 * @module models/tizen-multi-project/project
 */

module.exports.createJob = createJob;
module.exports.initializeJob = initializeJob;
module.exports.executeJob = executeJob;
module.exports.resumeJob = resumeJob;
module.exports.pendingJobHandler = pendingJobHandler;
module.exports.checkMutualExclusiveJob = TizenCommon.checkMutualExclusiveJob;
module.exports.checkJobPrivilege = checkJobPrivilege;


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

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

    this.compatJob = null;
    this.snapshot = null;
    if (!this.options.packages) {
        this.options.packages = [];
    }
    this.initEnvironments = dibs.projectTypes['Tizen-Multi'].environments;
    // NOTE. Executing on windows is often failing.
    this.execEnvironments = dibs.projectTypes['Tizen-Multi'].environments
        .filter(function (e) {
            return (e.indexOf('windows') === -1);
        });
}

/**
 * Create tizen git job instance
 * @function createJob
 * @param {string} prjName - project name
 * @param {string} prjType - project type
 * @param {string} distName - distribution name
 * @param {module:models/job.options} options - job options
 * @param {module:lib/utils.callback_error} callback - callback(error)
 * @memberOf module:models/tizenGit/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) {
            if (err) {
                return callback(err, null);
            } else {
                TizenCommon.checkReleasePhase(new TizenMultiJob(newJob), callback);
            }
        });
}


/**
 * Initialize Tizen multi job
 * @function executeJob
 * @param {module:models/job~Job} job - job
 * @param {string} workPath - workPath
 * @param {module:core/base-server.BaseServer} server - server
 * @param {module:lib/utils.callback_error} callback - callback(error)
 * @memberOf module:models/tizenGit/project
 */
function initializeJob(job, options, callback) {
    /*
     *  1. state check
     */
    if ((job.status !== 'INITIALIZING')) {
        return callback(new DError('TIZENJOB008', {
            jobId: job.id
        }), job);
    }

    /*
     *  2. set variable
     */
    // get projets for creating sub-jobs
    var monitor = options.monitor;

    /*
     *  3. options check
     */

    var subJobIds = null;
    async.waterfall([
        /*
         *  4. prepare target
         */
        // create sub jobs
        function (cb) {
            createSubJobs(job, job.options.SUB_JOBS, function (err, jobIds) {
                subJobIds = jobIds;
                cb(err);
            });
        },
        /*
         *  5. get infomation from target
         */
        function (cb) {
            monitor.updateProgress('Initializing sub jobs...', cb);
        },
        function (cb) {
            updateSubJobs(job, subJobIds, cb);
        },
        function (cb) {
            waitForSubJobsInitialized(subJobIds, monitor, cb);
        },
        function (cb) {
            monitor.updateProgress('Initializing sub jobs done ', cb);
        },
        // update subJobs
        function (cb) {
            updateSubJobs(job, subJobIds, cb);
        },
        /*
         *  7. check dependency list in repo
         */
        // check repository can support all sub jobs dependencies?
        function (cb) {
            monitor.updateProgress('Checking the dependencies of all sub jobs...', cb);
        },
        //check package duplications
        function (cb) {
            var all = _.map(job.options.packages, function (pkg) {
                return pkg.name + '[' + pkg.os + ']';
            });
            var duplicates = [];
            _.reduce(all, function (acc, pkg) {
                if (_.indexOf(acc, pkg) === -1) {
                    acc.push(pkg);
                    return acc;
                } else {
                    duplicates.push(pkg);
                    return acc;
                }
            }, []);
            if (duplicates.length > 0) {
                cb(new DError('TIZENMULTIJOB005', {
                    pkgs: duplicates.join(', ')
                }));
            } else {
                cb(null);
            }
        },
        function (cb) {
            job.depList = TizenCommon.getDepList(job.options.packages);
            cb(null);
        },
        function (cb) {
            if (job.depList.length > 0) {
                TizenCommon.checkDependencyExistInRepo(job, cb);
            } else {
                cb(null);
            }
        },
        function (cb) {
            monitor.updateProgress('Analyzing Dependencies for scheduling...', cb);
        },
        // analyze & set inter-job dependency
        function (cb) {
            updateSubJobsDependencies(job);
            cb(null);
        }],
        function (err) {
            if (err && subJobIds.length !== 0) {
                async.waterfall([
                    function (acb) {
                        monitor.updateProgress('Canceling all sub jobs ');
                        cancelAllSubjobs(subJobIds, monitor, acb);
                    },
                    function (acb) {
                        updateSubJobs(job, subJobIds, acb);
                    },
                    function (acb) {
                        monitor.updateProgress('Canceling all sub jobs done!', acb);
                    },
                    function (acb) {
                        if (dibs.getServersByType('messenger')[0] && job.userEmail !== 'admin@user') {
                            monitor.updateProgress('sending email');
                            TizenCommon.sendEmail(err, job, monitor, acb);
                        } else {
                            acb(null);
                        }
                    }
                ], function (error) {
                    if (error) {
                        monitor.updateProgress('Error :' + error.message);
                    }
                    callback(err, job);
                });
            } else {
                callback(err, job);
            }
        });
}

function createSubJobs(parentJob, prjInfos, callback) {
    async.mapLimit(prjInfos, 10,
        function (prjInfo, cb) {
            createSubJob(parentJob, prjInfo, cb);
        },
        function (err, results) {
            callback(err, results);
        });
}


function createSubJob(parentJob, prjInfo, callback) {
    var result = '';

    if (prjInfo.length === 0) {
        return callback(null, result);
    }

    async.waterfall([
        function (cb) {
            // query project
            dibs.rpc.datamgr.searchProjects({
                name: prjInfo.projectName,
                distName: parentJob.distName
            }, function (err, prjs) {
                if (err) {
                    cb(err);
                } else if (prjs.length === 0) {
                    cb(new DError('TIZENMULTIJOB001', {
                        name: prjInfo.projectName,
                        distName: parentJob.distName
                    }));
                } else {
                    cb(null, prjs[0]);
                }
            });
        }, function (prj, cb) {
            var forceRebuild = parentJob.options.FORCE_REBUILD ? true : false;
            var testBuild = parentJob.options.UPLOAD_TEMP ? true : false;
            var envName;
            var buildOpts;

            // create job options
            if (prj.type === 'Tizen-Source') {
                buildOpts = {
                    TARGET_OS: prjInfo.targetOS,
                    FORCE_REBUILD: forceRebuild,
                    UPLOAD_TEMP: testBuild,
                    UPLOAD: false,
                    NO_REVERSE_CHECK: true
                };
                // set GIT PATCHSET option if needed
                if (prjInfo.gitPatchset) {
                    buildOpts.GIT_PATCHSET = prjInfo.gitPatchset;
                } else if (parentJob.options.GIT_PATCHSET) {
                    buildOpts.GIT_PATCHSET = parentJob.options.GIT_PATCHSET;
                } else {
                    // Nothing to do
                }
                // set GIT COMMIT ID option if needed
                if (prjInfo.gitCommit) {
                    buildOpts.GIT_COMMIT = prjInfo.gitCommit;
                } else if (parentJob.options.GIT_COMMIT) {
                    buildOpts.GIT_COMMIT = parentJob.options.GIT_COMMIT;
                } else {
                    // Nothing to do
                }

                // set GITHUB PULLREQUEST option if needed
                if (prjInfo.ghpr) {
                    buildOpts.GITHUB_PULLREQUEST = prjInfo.ghpr;
                } else if (parentJob.options.GITHUB_PULLREQUEST) {
                    buildOpts.GITHUB_PULLREQUEST = parentJob.options.GITHUB_PULLREQUEST;
                } else {
                    // Nothing to do
                }

                envName = prjInfo.targetOS;
            } else if (prj.type === 'Tizen-Binary') {
                buildOpts = {
                    FILEPATH: prjInfo.filePath,
                    FORCE_REBUILD: forceRebuild,
                    UPLOAD_TEMP: testBuild,
                    UPLOAD: false,
                    NO_REVERSE_CHECK: true
                };
                var pkgInfo = Tutils.getInfoFromPackageFileName(path.basename(prjInfo.filePath));
                envName = pkgInfo ? pkgInfo.os : 'ubuntu-32';
            } else if (prj.type === 'Tizen-Archive') {
                buildOpts = {
                    FILEPATH: prjInfo.filePath,
                    UPLOAD_TEMP: testBuild,
                    UPLOAD: false
                };
                envName = 'ubuntu-32';
            } else {
                // nothing to do
            }

            if (envName && buildOpts) {
                dibs.rpc.jobmgr.addSubJob(parentJob.id, prjInfo.projectName, envName, buildOpts,
                        function (err, jobId) {
                            if (!err) {
                                result = jobId;
                            }
                            cb(err);
                        });
            } else {
                return cb(new DError('TIZENMULTIJOB002', {
                    type: prj.type
                }));
            }
        }], function (err) {
        callback(err, result);
    });
}


function cancelAllSubjobs(subJobIds, monitor, callback) {
    if (subJobIds.length === 0) {
        callback(null); return;
    }

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


function waitForSubJobsInitialized(jobIds, monitor, callback) {
    if (jobIds.length === 0) {
        callback(null); return;
    }

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


function updateSubJobs(parentJob, subJobIds, callback) {
    async.mapLimit(subJobIds, 5,
        function (jobId, cb) {
            dibs.rpc.jobmgr.queryJob(jobId, cb);
        },
        function (err, sjobs) {
            if (!err) {
                parentJob.subJobs = sjobs;
                parentJob.options.packages = [];
                // update result pkgs
                for (var i = 0; i < sjobs.length; i++) {
                    parentJob.options.packages = parentJob.options.packages.concat(sjobs[i].options.packages);
                }
            }
            callback(err);
        });
}


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

function updateSubJobsDependencies(parentJob) {
    // analyze sub jobs and update its dependency
    var deps = JobDep.analyzeDependencies(parentJob.subJobs);
    for (var i = 0; i < parentJob.subJobs.length; i++) {
        var subJob = parentJob.subJobs[i];
        subJob.depJobs = deps[subJob.id];
    }
}


/**
 * Execute Tizen multi job
 * - it wait for sub-jobs finished & upload them
 * @function executeJob
 * @param {module:models/job~Job} job - job
 * @param {string} workPath - workPath
 * @param {module:core/base-server.BaseServer} server - server
 * @param {module:lib/utils.callback_error} callback - callback(error)
 * @memberOf module:models/tizenGit/project
 */
function executeJob(job, options, callback) {
    var monitor = options.monitor;
    var jobSvc = options.jobSvc;

    async.waterfall([
        // get latest snapshots of repository
        function (cb) {
            monitor.updateProgress('Getting the latest snapshot of repository ...');
            dibs.rpc.repo.searchSnapshots({
                name: null,
                repoType: 'tizen',
                distName: job.distName
            },
            function (err, snapshots) {
                if (!err && snapshots.length > 0) {
                    job.snapshot = snapshots[0];
                    monitor.updateProgress(' - ' + job.snapshot.name);
                    job.snapshot.distName = job.distName;
                }
                cb(err);
            });
        },
        function (cb) {
            monitor.updateProgress('Waiting for sub jobs finished...', cb);
        },
        // update subJobs
        function (cb) {
            var subJobIds = job.subJobs.map(function (subJob) {
                return subJob.id;
            });
            updateSubJobs(job, subJobIds, cb);
        },
        // wait for all job finished
        function (cb) {
            waitForSubJobsFinished(job, monitor, cb);
        },
        function (cb) {
            monitor.updateProgress('Waiting for all sub jobs finished is done!', cb);
        },
        // update subJobs
        function (cb) {
            var subJobIds = job.subJobs.map(function (subJob) {
                return subJob.id;
            });
            updateSubJobs(job, subJobIds, cb);
        },
        function (cb) {
            if (!job.options.BUILD_ONLY) {
                monitor.updateProgress('Preparing to upload result files...');
                prepareToUpload(job, monitor, function (err, rpaths) {
                    if (!err) {
                        job.resultFiles = rpaths;
                    }
                    cb(err);
                });
            } else {
                cb(null);
            }
        },
        function (cb) {
            if (!job.options.BUILD_ONLY) {
                monitor.updateProgress('Collecting build information...');
                var result = [];
                job.subJobs.forEach(function (s) {
                    if (s.resultFiles) {
                        result = result.concat(s.buildInfo);
                    }
                });
                job.buildInfo = result;

                // save build informations for recover
                job.options.buildInfo = job.buildInfo;
            }
            cb(null);
        },
        // check reverse-build
        function (cb) {
            monitor.updateProgress('Checking reverse build...', cb);
        },
        function (cb) {
            if (job.options.NO_REVERSE_CHECK) {
                monitor.updateProgress('Skipped checking reverse build by NO_REVERSE_CHECK option', cb);
            } else {
                ReverseBuildChecker.check(job, monitor, function (err, result) {
                    updateReverseJobs(job, result, function (err1) {
                        if (err && !err1) {
                            monitor.updateProgress('WARNING! Reverse build failed!');
                            cb(err);
                        } else if (!err && err1) {
                            monitor.updateProgress('WARNING! Job information update failed!');
                            cb(err1);
                        } else if (err && err1) {
                            monitor.updateProgress('WARNING! Reverse build failed!');
                            monitor.updateProgress('WARNING! Job information update failed!');
                            cb(err);
                        } else {
                            cb(null);
                        }
                    });
                });
            }
        },
        function (cb) {
            monitor.updateProgress('Checking uploading...', cb);
        },
        function (cb) {
            TizenCommon.binaryUploadApprovalProcess(jobSvc, job, monitor, cb);
        }
    ], function (err, job1) {
        job1 = job1 || job;

        if (!err && job1.status === 'PENDING') {
            return callback(null, job1);
        }

        finalizeBuild(err, job1, monitor, callback);
    });
}

function pendingJobHandler(job, callback) {
    TizenCommon.pendingJobHandler(job, callback);
}

function resumeJob(job, options, callback) {
    var jobSvc = options.jobSvc;
    var monitor = options.monitor;
    var resumeOptions = options.resumeOptions;

    // recover build information
    job.buildInfo = job.options.buildInfo;

    TizenCommon.resumeJob(jobSvc, job, resumeOptions, monitor, function (err) {
        finalizeBuild(err, job, monitor, callback);
    });

}

function waitForSubJobsFinished(job, monitor, callback) {

    if (job.subJobs.length === 0) {
        callback(null); return;
    }

    async.each(job.subJobs,
        function (subJob, cb) {
            dibs.rpc.jobmgr.waitForJobStatus(subJob.id, 'FINISHED',
                function (err, status) {
                    if (err) {
                        if (status && status !== 'FINISHED') {
                            monitor.updateProgress(' - failed with status \'' +
                                status + '\' ...' + subJob.id);
                            cb(new DError('TIZENMULTIJOB004', {
                                jobId: subJob.id
                            }));
                        } else {
                            monitor.updateProgress(' - failed with error \'' +
                                err.errno + '\' ...' + subJob.id);
                            cb(err);
                        }
                    } else {
                        monitor.updateProgress(' - succeeded ...' + subJob.id, cb);
                    }
                });
        },
        function (err) {
            if (err) {
                var subJobIds = _.map(job.subJobs, function (subjob) {
                    return subjob.id;
                });
                monitor.updateProgress('Canceling all sub jobs...');
                cancelAllSubjobs(subJobIds, monitor, function () {
                    monitor.updateProgress('Canceling all sub jobs done!');
                    callback(err);
                });
            } else {
                callback(err);
            }
        });
}


function prepareToUpload(job, monitor, callback) {
    var result = [];
    job.subJobs.forEach(function (s) {
        if (s.resultFiles) {
            result = result.concat(s.resultFiles);
        }
    });
    callback(null, result);
}


function saveBuildInfo(job, monitor, callback) {
    async.eachSeries(job.subJobs,
        function (subJob, cb) {
            if (subJob.projectType === 'Tizen-Source') {
                monitor.updateProgress('Updating last build info for sub-job ...' + subJob.id);
                GitProject.saveBuildInfo(subJob, cb);
            } else {
                cb(null);
            }
        },
        function (err) {
            callback(err);
        });

}

function finalizeBuild(err, job, monitor, callback) {
    async.waterfall([
        function (cb) {
            if (job.options.UPLOAD) {
                saveBuildInfo(job, monitor, cb);
            } else {
                cb(null);
            }
        },
        function (cb) {
            finalize(err, job, monitor, cb);
        }
    ], function (err, job1) {
        callback(err, job1);
    });
}

function finalize(err, job, monitor, callback) {
    var subJobIds;
    async.series([
        function (cb) {
            subJobIds = _.map(job.subJobs, function (sjob) {
                return sjob.id;
            });
            if (err) {
                monitor.updateProgress('Canceling all sub jobs');
                cancelAllSubjobs(subJobIds, monitor, cb);
            } else {
                cb(null);
            }
        },
        function (cb) {
            updateSubJobs(job, subJobIds, cb);
        },
        function (cb) {
            if (err && dibs.getServersByType('messenger')[0] && job.userEmail !== 'admin@user') {
                monitor.updateProgress('Canceling all sub jobs done!');
                monitor.updateProgress('sending email');
                TizenCommon.sendEmail(err, job, monitor, cb);
            } else if (err) {
                monitor.updateProgress('Canceling all sub jobs done!', cb);
            } else if (!err && dibs.getServersByType('messenger')[0] && job.userEmail !== 'admin@user') {
                monitor.updateProgress('sending email');
                TizenCommon.sendEmail(err, job, monitor, cb);
            } else {
                cb(null);
            }
        }
    ], function (error) {
        // NOTE. MUST strip unnecessary information for reducing object size
        //       if not, DNODE RPC cannnot receive callback
        TizenCommon.stripJob(job);

        if (error) {
            monitor.updateProgress('Error :' + error.message);
        }
        callback(err, job);
    });
}

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);
        });
}
