/**
 * job-service.js
 * Copyright (c) 2000 - 2015 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 path = require('path');
var fs = require('fs');
var async = require('async');
var _ = require('underscore');
var process = require('process');

var dibs = require('../../core/dibs');
var DError = require('../../core/exception');
var DHandle = require('../../core/error-handler');
var JobMonitor = require('../../lib/monitor.js');

var jobsvc = null;

process.on('message', function (m) {
    DHandle.run({
        try: function () {
            if (!jobsvc) {
                jobsvc = new JobService(m.job, m.options);
            }

            if (m.action === 'initialize') {
                jobsvc.initializeJob(m.job, function (err, job) {
                    jobsvc.endJobHandler(err, job);
                });
            } else if (m.action === 'build') {
                jobsvc.buildJob(m.job, function (err, job) {
                    jobsvc.endJobHandler(err, job);
                });
            } else if (m.action === 'resume') {
                jobsvc.resumeJob(m.job, m.resumeOptions, function (err, job) {
                    jobsvc.endJobHandler(err, job);
                });
            } else if (m.action === 'cancel') {
                jobsvc.cancelJob(m.job);
            } else if (m.action === 'exit') {
                jobsvc.exitJob(m.job);
            } else {
                console.error('Not defined server message' + m);
                process.exit(-1);
            }
        },
        catch: function (e) {
            console.error('catch exception');
            console.error(e);
            process.exit(-1);
        }
    });
});

/**
 * @constructor
 * @memberOf module:servers/builder/server
 */
function JobService(job, options) {
    var self = this;
    /** type {string} */
    self.name = 'jobsvc';
    self.job = job;
    self.parentServerId = options.serverId;

    dibs.initialize();

    createRpcServers(options.servers);

    var environments = options.environments;
    /** type {string} */
    var workspacePath = options.config['workspace_path'];

    /** type {string} */
    var workspaceTempPath = path.join(workspacePath, '.temp');

    /** type {string} */
    var lockFilePath = path.join(workspacePath, 'lock');

    // specify job directory
    var jobWorkPath = path.join(workspacePath, 'jobs', job.id.toString());

    // create job log
    var jobLog = createJobLog(options.config, jobWorkPath, job.id);
    job.log = jobLog;

    var monitor = null;
    job.monitor = monitor;

    dibs.log = dibs.log.open('JOBSVC-#' + job.id.toString());

    self.initializeJob = initializeJob;
    self.buildJob = buildJob;
    self.resumeJob = resumeJob;
    self.cancelJob = cancelJob;
    self.exitJob = exitJob;
    self.updateJobStatus = updateJobStatus;

    /**
     * @method InitializeJob
     * @param {string} jobId
     * @param {module:lib/utils.callback_error} callback
     * @memberOf module:servers/builder/server.JobService
     */
    function initializeJob(job, callback) {
        jobLog.info('Initializing the job #' + job.id + ' ...');

        async.waterfall([
            function (cb) {
                fs.exists(jobWorkPath, function (exists) {
                    if (exists) {
                        return cb(null);
                    } else {
                        return cb(new DError('BUILDER010', {jobWorkPath: jobWorkPath}));
                    }
                });
            },
            // initialize job
            function (cb) {
                monitor = new JobMonitor({
                    onProgress: function (info, cb1) {
                        if (info.logType) {
                            jobLog[info.logType](info.log);
                        }
                        cb1(null);
                    }
                });

                monitor.monitor(function (cb1) {
                    jobLog.info('Initialize' + job.projectType + '...');
                    var initOptions = {
                        jobSvc: self,
                        parentServerId: self.parentServerId,
                        environments: environments,
                        jobWorkPath: jobWorkPath,
                        lockFilePath: lockFilePath,
                        workspacePath: workspacePath,
                        workspaceTempPath: workspaceTempPath,
                        monitor: monitor
                    };

                    dibs.projectTypes[job.projectType].module.initializeJob(job, initOptions, cb1);

                }, cb);
            }
        ],
        function (err) {
            var nextStatus = 'INITIALIZED';

            handleJobStatus(err, 'initialize', job, jobLog, nextStatus, function (err1) {
                callback(err1, job);
            });
        });
    }

    /**
     * @method buildJobInternal
     * @param {string} jobId
     * @param {module:lib/utils.callback_error} callback
     * @memberOf module:servers/builder/server.JobService
     */
    function buildJob(job, callback) {
        jobLog.info('Executing the job #' + job.id + ' ...');

        async.waterfall([
            function (cb) {
                fs.exists(jobWorkPath, function (exists) {
                    if (exists) {
                        return cb(null);
                    } else {
                        return cb(new DError('BUILDER010', {jobWorkPath: jobWorkPath}));
                    }
                });
            },
            // execute pre-build events
            function (cb) {
                executePrebuild(job, cb);
            },
            // execute build script
            function (cb) {
                monitor = new JobMonitor({
                    onProgress: function (info, cb1) {
                        if (info.logType) {
                            jobLog[info.logType](info.log);
                        }
                        cb1(null);
                    }
                });

                monitor.monitor(function (cb1) {
                    var buildOptions = {
                        jobSvc: self,
                        parentServerId: self.parentServerId,
                        environments: environments,
                        lockFilePath: lockFilePath,
                        jobWorkPath: jobWorkPath,
                        workspacePath: workspacePath,
                        workspaceTempPath: workspaceTempPath,
                        monitor: monitor
                    };

                    dibs.projectTypes[job.projectType].module.executeJob(job, buildOptions, cb1);

                }, cb);
            },
            // execute post-build events
            function (job1, cb) {
                executePostbuild(job1, cb);
            }
        ],
        function (err, job1) {
            job1 = job1 || job;

            var nextStatus = 'FINISHED';

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

            handleJobStatus(err, 'build', job1, jobLog, nextStatus, function (err1) {
                callback(err1, job1);
            });
        });
    }

    /**
     * @method buildJobInternal
     * @param {string} jobId
     * @param {module:lib/utils.callback_error} callback
     * @memberOf module:servers/builder/server.JobService
     */
    function resumeJob(job, resumeOptions, callback) {
        jobLog.info('Resuming the job #' + job.id + ' ...');

        async.waterfall([
            function (cb) {
                fs.exists(jobWorkPath, function (exists) {
                    if (exists) {
                        return cb(null);
                    } else {
                        return cb(new DError('BUILDER010', {jobWorkPath: jobWorkPath}));
                    }
                });
            },
            // execute build script
            function (cb) {
                monitor = new JobMonitor({
                    onProgress: function (info, cb1) {
                        if (info.logType) {
                            jobLog[info.logType](info.log);
                        }
                        cb1(null);
                    }
                });

                monitor.monitor(function (cb1) {
                    var buildOptions = {
                        jobSvc: self,
                        parentServerId: self.parentServerId,
                        environments: environments,
                        jobWorkPath: jobWorkPath,
                        lockFilePath: lockFilePath,
                        workspacePath: workspacePath,
                        workspaceTempPath: workspaceTempPath,
                        monitor: monitor,
                        resumeOptions: resumeOptions
                    };

                    dibs.projectTypes[job.projectType].module.resumeJob(job, buildOptions, cb1);

                }, cb);
            }
        ],
        function (err, job1) {
            job1 = job1 || job;
            var nextStatus = 'FINISHED';

            handleJobStatus(err, 'build', job1, jobLog, nextStatus, function (err1) {
                callback(err1, job1);
            });
        });
    }

    /**
     * @method buildJobInternal
     * @param {string} jobId
     * @param {module:lib/utils.callback_error} callback
     * @memberOf module:servers/builder/server.JobService
     */
    function cancelJob(job) {
        if (checkCancelJob()) {
            jobLog.info('Canceling the job #' + job.id + ' ...');

            _.each(job.subJobs, function (subjob) {
                jobLog.info('Request cancel the sub-job #' + subjob.id + ' ...');
                dibs.rpc.jobmgr.cancelJob(subjob.id, function (err) {
                    if (err) {
                        console.log(err);
                    }
                });
            });

            handleJobStatus(null, 'cancel', job, jobLog, 'CANCELED', function () {
                self.endJobHandler(null, job);
            });
        } else {
            jobLog.info('Fail canceling the job #' + job.id + ': impossible cancel in job stage');
        }
    }

    /**
     * @method exitjob
     * @param {string} jobId
     * @param {module:lib/utils.callback_error} callback
     * @memberOf module:servers/builder/server.JobService
     */
    function exitJob(job) {
        console.log('#' + job.id + '\' s  process is terminated');
        process.exit(0);
    }

    function checkCancelJob() {
        if (!monitor) {
            return true;
        } else {
            if (monitor.possibleCancel()) {
                return true;
            } else {
                return false;
            }
        }
    }

    this.endJobHandler = function (err, job) {
        var message = {
            job: job,
            error: err,
            action: 'end',
            pid: process.pid
        };
        process.send(message);
    };


    function updateJobStatus(job, status, callback) {

        // Cannot change if terminatd status
        if (job.status === 'FINISHED' || job.status === 'ERROR' ||
            job.status === 'CANCELED') {

            return callback(new DError('BUILDER006', {
                jobId: job.id
            }));
        }

        // WORKING -> ERROR, CANCELED, PENDING, FINISHED
        if (job.status === 'WORKING' && status !== 'FINISHED' && status !== 'PENDING' &&
            status !== 'ERROR' && status !== 'CANCELED') {

            return callback(new DError('BUILDER007', {
                jobId: job.id,
                fromStatus: job.status,
                toStatus: status
            }));
        }

        // PENDING -> ERROR, CANCELED, WORKING
        if (job.status === 'PENDING' && status !== 'WORKING' &&
            status !== 'ERROR' && status !== 'CANCELED') {

            return callback(new DError('BUILDER007', {
                jobId: job.id,
                fromStatus: job.status,
                toStatus: status
            }));
        }

        // update status
        job.status = status;

        var message = {
            job: job,
            action: 'updateStatus'
        };
        process.send(message);

        callback(null);
    }

    function createJobLog(config, jobWorkPath, jobId) {
        var logId = 'JOB-' + jobId;
        var logOptions = {};

        if (config['remote_job_log']) {
            var logDbHost = config['remote_log_db_host'] || null;
            var logDbName = config['remote_log_db_name'] || null;
            logOptions = _.extend(logOptions, {
                remoteLogDbHost: logDbHost,
                remoteLogDbName: logDbName
            });
        }

        if (config['file_job_log']) {
            logOptions = _.extend(logOptions, {
                filename: path.join(jobWorkPath, 'log'),
                colorize: false
            });
        }

        return dibs.log.open(logId, logOptions);
    }

    function executePrebuild(job, callback) {
        var extensions = dibs.plugin.getExtensions('dibs.server.builder.preBuild');
        var preEvents = _.sortBy(job.options.PRE_BUILD, 'property'['ORDER']);

        jobLog.info('** Pre-build events: START **');

        monitor = new JobMonitor({
            onProgress: function (info, cb) {
                if (info.logType) {
                    jobLog[info.logType](info.log);
                }
                cb(null);
            }
        });

        monitor.monitor(function (cb) {
            executeEvents(extensions, preEvents, job, monitor, {}, cb);
        }, function (err) {
            if (err) {
                jobLog.info('** Pre-build events: FAILED **');
            } else {
                jobLog.info('** Pre-build events: DONE **');
            }
            callback(err);
        });
    }


    function executePostbuild(job, callback) {
        var extensions = dibs.plugin.getExtensions('dibs.server.builder.postBuild');
        var postEvents = _.sortBy(job.options.POST_BUILD, 'property'['ORDER']);
        var extsForIntegrationRuns = _.filter(extensions, function (extension) {
            return extension.integrationRuns;
        });
        job.log = jobLog;

        async.waterfall([
            function (cb) {
                jobLog.info('** Post-build events: START **');
                monitor = new JobMonitor({
                    onProgress: function (info, cb1) {
                        if (info.logType) {
                            jobLog[info.logType](info.log);
                        }
                        cb1(null);
                    }
                });

                monitor.monitor(function (cb1) {
                    executeEvents(extensions, postEvents, job, monitor, {}, cb1);
                }, cb);
            }, function (cb) {
                if (!job.parentId) {
                    jobLog.info('** Post-build events: Integration-Runs **');

                    var subPostEvents = _.map(extsForIntegrationRuns, function (ext) {
                        return ext.module.getIntegrationRunsList(ext.eventType, job.subJobs);
                    });
                    subPostEvents = _.flatten(subPostEvents);

                    monitor = new JobMonitor({
                        onProgress: function (info, cb1) {
                            if (info.logType) {
                                jobLog[info.logType](info.log);
                            }
                            cb1(null);
                        }
                    });

                    monitor.monitor(function (cb1) {
                        executeEvents(extsForIntegrationRuns, subPostEvents, job, monitor, {}, cb1);
                    }, cb);
                } else {
                    cb(null);
                }
            }
        ], function (err) {
            if (err) {
                jobLog.info('** Post-build events: FAILED **');
            } else {
                jobLog.info('** Post-build events: DONE **');
            }
            callback(err, job);
        });
    }


    function executeEvents(extensions, events, job, monitor, options, callback) {
        async.eachSeries(events, function (event, cb) {
            var eventObj = _.find(extensions, function (m) {
                return m.eventType === event.eventType;
            });

            // set options
            var opts = options || {};
            opts.workspacePath = workspacePath;
            opts.workspaceTempPath = workspaceTempPath;
            opts.environments = environments;

            eventObj.module.executeEvent(event.property, job, monitor, opts, cb);
        }, callback);
    }


    function handleJobStatus(err, stage, job, jobLog, nextStatus, callback) {
        var error = null;

        if (err) {
            if (err.message === 'cancel') {
                jobLog.error('Job #' + job.id + ' canceled!');

                error = new DError('BUILDER005', {
                    jobId: job.id
                });
                nextStatus = 'CANCELED';
            } else {
                jobLog.error('Job #' + job.id + ' failed!');
                jobLog.error(err.message + (err.inner ? '=> ' + err.inner.message : ''));
                jobLog.warn('DEBUG => ' + err.stack);

                if (err.inner) {
                    jobLog.warn('DEBUG => ' + err.inner.stack);
                }
                error = _.clone(err);
                nextStatus = 'ERROR';
            }
        } else {
            jobLog.info(stage + ' job #' + job.id + ' successfully!');
        }

        // update to terminating status
        job.error = error;

        updateJobStatus(job, nextStatus, function (err1) {
            if (err1) {
                jobLog.error(err1);
                return callback(err1);
            } else {
                return callback(error);
            }
        });
    }

    function createRpcServers(servers) {
        _.each(servers, function (s) {
            var server = dibs.createServer(s.id, s.type);
            server.host = s.host;
            server.port = s.port;
            server.status = s.status;
            server.environments = s.environments || [];
            dibs.addServer(server);
        });
    }

    process.on('SIGTERM', function () {
        jobLog.info('Canceled job #' + job.id + '!');
        setTimeout(function () {
            process.exit(130 + 15); //SIGTERM: 15
        }, 3000);
    });
}
