/**
 * server.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 path = require('path');
var fs = require('fs');
var extfs = require('fs-extra');
var async = require('async');
var _ = require('underscore');
var fork = require('child_process').fork;

var dibs = require('../../core/dibs');
var utils = require('../../lib/utils');
var dfs = require('../../plugins/dibs.dist-fs/dist-fs');
var DError = require('../../core/exception');
var FileSystem = require('../dibs.core/filesystem.js');


/**
 * @module servers/builder/server
 */


/**
 * @constructor
 * @memberOf module:servers/builder/server
 */

function BuilderServer() {
    var self = this;
    /** type {string} */
    this.name = 'builder';

    /** type {string} */
    var workspacePath = null;
    var lockFilePath = null;
    var timeToRetainLog = null;
    /** HASH (job id:string -> job:{@link module:models/job~Job})*/
    var workingJobs = {};
    var jobSvcs = {};

    /**
     * @typedef configuration
     * @property {number} max_jobs
     * @property {string} workspace_path
     * @memberOf module:servers/builder/server
     */

    /**
     * @method getDefaultConfiguration
     * @return {module:servers/builder/server.configuration}
     * : returns for creating server's default configuration
     * @memberOf module:servers/builder/server.BuilderServer
     */
    // CALLBACK: for creating server's default configuration
    this.getDefaultConfiguration = function (baseConfig) {
        var defaultConfig = baseConfig;
        defaultConfig['dfs-size'] = 1024 * 1024 * 1024 * 10; // 10G
        defaultConfig['max_jobs'] = 4;
        defaultConfig['workspace_path'] = utils.osPathJoin(baseConfig.os_type, [baseConfig.config_root_dir, baseConfig.id, 'workspace']);
        defaultConfig['number_of_days_to_retain_log'] = 3;
        defaultConfig['file_job_log'] = true;
        defaultConfig['remote_job_log'] = false;
        return defaultConfig;
    };


    /**
     * @method OnServerStarting
     * @param {module:lib/utils.callback_error} callback
     * @memberOf module:servers/builder/server.BuilderServer
     */
    this.OnServerStarting = function (callback) {
        workspacePath = this.config.get('workspace_path');
        lockFilePath = path.join(workspacePath, 'lock');
        var days = this.config.get('number_of_days_to_retain_log');
        timeToRetainLog = 1000 * 60 * 60 * 24 * days;
        createWorkspace(callback);
    };


    this.OnServerStarted = function (callback) {
        async.series([
            function (cb) {
                dibs.log.info('Clear lockfile DIR...');
                clearLockFile(lockFilePath, cb);
            },
            function (cb) {
                dibs.log.info('Opening DFS repository...');
                dfs.openRepository(self.config.get('dfs-path'),
                    self.config.get('dfs-size'),
                    self.startOptions.cleanStart,
                    self.port + 1,
                    function (err) {
                        if (!err) {
                            dibs.log.info('DFS repository loaded!');
                        }
                        cb(err);
                    });
            },
            function (cb) {
                dfs.connect(cb);
            },
            function (cb) {
                setInterval(function () {
                    cleanWorkspace();
                }, (1000 * 60 * 60 * 4)); // execution per 4h
                cb(null);
            }
        ],
        function (err) {
            callback(err);
        });

    };


    this.OnServerTerminating = function (callback) {
        // close dfs repo
        if (dfs.isOpened()) {
            dfs.closeRepository();
        }

        killAllJobService();

        callback(null);
    };


    /**
     * @method getWorkspacePath
     * @returns {string} workspacePath
     * @memberOf module:servers/builder/server.BuilderServer
     */
    this.getWorkspacePath = function () {
        return workspacePath;
    };


    this.getWorkspaceTempPath = function () {
        return path.join(workspacePath, '.temp');
    };


    /**
     * @method InitializeJobInternal
     * @param {string} jobId
     * @param {module:lib/utils.callback_error} callback
     * @memberOf module:servers/builder/server.BuilderServer
     */
    this.initializeJobInternal = function (job, callback) {
        self.log.info('Received initialization-request of job #' + job.id + '...');
        workingJobs[job.id] = job;

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

        async.series([
            function (cb) {
                utils.removePathIfExist(jobWorkPath, cb);
            },
            function (cb) {
                self.log.info('Create job work space... ' + jobWorkPath);
                extfs.mkdirs(jobWorkPath, cb);
            },
            function (cb) {
                getJobEnv(job, cb);
            }
        ], function (err) {
            if (err) {
                callback(err);
            } else {
                callback(null);
                initJobService(job);
            }
        });
    };


    /**
     * @method buildJobInternal
     * @param {string} jobId
     * @param {module:lib/utils.callback_error} callback
     * @memberOf module:servers/builder/server.BuilderServer
     */
    this.buildJobInternal = function (job, callback) {
        self.log.info('Received build request of job #' + job.id + '...');
        workingJobs[job.id] = job;

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

        async.waterfall([
            function (cb) {
                fs.exists(jobWorkPath, function (exists) {
                    if (exists && (job.initServerId === job.execServerId)) {
                        cb(null, true);
                    } else {
                        cb(null, false);
                    }
                });
            },
            function (isValidJobWorkPath, cb) {
                if (isValidJobWorkPath) {
                    cb(null);
                } else {
                    self.log.info('Create job work space... ' + jobWorkPath);
                    extfs.mkdirs(jobWorkPath, function (err) {
                        cb(err);
                    });
                }
            },
            function (cb) {
                getJobEnv(job, cb);
            }
        ], function (err) {
            if (err) {
                callback(err);
            } else {
                callback(null);
                buildJobService(job);
            }
        });
    };


    /**
     * @method resumeJobInternal
     * @param {string} jobId
     * @param {module:lib/utils.callback_error} callback
     * @memberOf module:servers/builder/server.BuilderServer
     */
    this.resumeJobInternal = function (job, options, callback) {
        self.log.info('Received build resume of job #' + job.id + '...');
        // check job status
        if (job.status === 'FINISHED' || job.status === 'ERROR' || job.status === 'CANCEL') {
            return callback(new DError('BUILDER009', {
                jobId: job.id
            }));
        }

        workingJobs[job.id] = job;

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

        async.series([
            function (cb) {
                fs.exists(jobWorkPath, function (exists) {
                    if (exists) {
                        cb(null);
                    } else {
                        self.log.info('Create job work space... ' + jobWorkPath);
                        extfs.mkdirs(jobWorkPath, cb);
                    }
                });
            },
            function (cb) {
                getJobEnv(job, cb);
            },
            function (cb) {
                resumeJobService(job, options);
                cb(null);
            }
        ], function (err) {
            callback(err);
        });
    };

    /**
     * @method cancelJobInternal
     * @param {string} jobId
     * @param {module:lib/utils.callback_error} callback
     * @memberOf module:servers/builder/server.BuilderServer
     */
    this.cancelJobInternal = function (jobId, callback) {
        self.log.info('Received cancel-request of job #' + jobId + '...');

        var job = workingJobs[jobId];

        // check working jobs
        if (!job) {
            return callback(new DError('BUILDER003', {
                jobId: jobId
            }));
        } else if (job.status === 'CANCELED') {
            return callback(new DError('BUILDER006', {
                jobId: jobId
            }));
        } else if (job.status === 'FINISHED' || job.status === 'ERROR') {
            return callback(new DError('BUILDER007', {
                jobId: jobId,
                fromStatus: job.status,
                toStatus: 'CANCELED'
            }));
        } else {
            cancelJobService(job);
            return callback(null);
        }
    };

    function getJobEnv(job, callback) {
        try {
            // set job environment variable
            var env = _.clone(process.env);
            if (job.options['JOB_ENV'] && job.options['JOB_ENV'] !== '{}') {
                env = _.extend(env, JSON.parse(JSON.stringify((new Function('return' + job.options['JOB_ENV']))())));
                job.env = env;
            }
        } catch (err) {
            return callback(err);
        }
        callback(null, env);
    }

    function updateJobStatus(job, callback) {
        switch (job.status) {
        case 'INITIALIZED':
            self.log.info('The Job #' + job.id + ' is initialized successfully!');
            break;
        case 'FINISHED':
            self.log.info('The Job #' + job.id + ' is finished successfully!');
            break;
        case 'ERROR':
            self.log.error('The Job #' + job.id + ' failed!');
            self.log.error(job.error);
            break;
        case 'CANCELED':
            self.log.info('The Job #' + job.id + ' is canceled! :');
            break;
        case 'WORKING':
            self.log.info('The Job #' + job.id + ' is working now...');
            break;
        case 'PENDING':
            self.log.info('The Job #' + job.id + ' is pending now');
            break;
        default:
            self.log.warn('The Job #' + job.id + ' status is invalid. Status: ' + job.status);
            break;
        }

        workingJobs[job.id] = job;

        // emit event
        self.emitEvent({
            event: 'JOB_STATUS_CHANGED',
            status: job.status,
            jobId: job.id,
            job: job
        });

        if (callback) {
            return callback(null);
        }
    }

    // PRIVATE
    var createWorkspace = function (callback) {
        // check repo exists
        fs.exists(workspacePath, function (exists) {
            if (!exists) {
                self.log.info('Creating new workspace...');
                // create directory
                extfs.mkdirs(workspacePath, callback);
            } else {
                utils.removePathIfExist(self.getWorkspaceTempPath(), function () {
                    //skip error
                    callback(null);
                });
            }
        });
    };

    function clearLockFile(path, callback) {
        extfs.remove(path, function (err) {
            if (err) {
                return callback(err);
            }
            extfs.mkdirs(path, callback);
        });

    }

    function cleanWorkspace() {
        dibs.log.info('workspace clean-up start');
        var workingJobList = _.keys(workingJobs);
        var jobsDirectory = path.join(workspacePath, 'jobs');
        fs.exists(jobsDirectory, function (exists) {
            if (exists) {
                fs.readdir(jobsDirectory, function (err, fileList) {
                    if (err) {
                        dibs.log.info('cannot read workspace directory');
                        dibs.log.info('err: ' + err);
                    } else {
                        var files = _.difference(fileList, workingJobList);
                        var curDate = new Date();
                        var dueTime = curDate - timeToRetainLog;
                        files.forEach(function (file) {
                            var jobDir = path.join(jobsDirectory, file);
                            fs.stat(jobDir, function (ferr, stats) {
                                if (stats.isDirectory()) {
                                    if ((dueTime - stats.mtime) > 0) {
                                        FileSystem.remove(jobDir, function (err) {
                                            if (err) {
                                                dibs.log.info('cannot remove [' + jobDir + '] directory');
                                            } else {
                                                dibs.log.info('[' + jobDir + '] directory is removed');
                                            }
                                        });
                                    }
                                }
                            });
                        });
                    }
                });
            } else {
                dibs.log.info('workspace does not exist');
            }
        });
    }

    function getServers() {
        var servers = dibs.getAllServers();

        var newServers = [];

        _.each(servers, function (s) {
            var server = s;
            server.id = s.id;
            server.type = s.type;

            newServers.push(server);
        });

        return newServers;
    }

    function addJobService(jobId, jobSvc) {
        if (!jobSvcs[jobId]) {
            jobSvcs[jobId] = {};
            jobSvcs[jobId][jobSvc.pid] = jobSvc;
        } else {
            jobSvcs[jobId][jobSvc.pid] = jobSvc;
        }
        return jobSvcs[jobId];
    }

    function deleteJobService(jobId, pid) {
        if (pid) {
            if (jobSvcs[jobId] && jobSvcs[jobId][pid]) {
                delete jobSvcs[jobId][pid];
                if (_.isEmpty(jobSvcs[jobId])) {
                    delete jobSvcs[jobId];
                }
            }
        } else {
            delete jobSvcs[jobId];
        }
        return jobSvcs[jobId];
    }

    function killJobService(jobId) {
        if (jobSvcs[jobId]) {
            _.each(jobSvcs[jobId], function (jobSvc, pid) {
                jobSvc.kill();
                dibs.log.info('Killed Job service #' + jobId + '. Process id:' + pid);
            });
            deleteJobService(jobId);
        }
        return jobSvcs[jobId];
    }

    function killAllJobService() {
        _.each(jobSvcs, function (value, jobId) {
            killJobService(jobId);
        });
    }

    function forkJobService(job) {
        var jobSvc = fork(path.join(__dirname, 'job-service.js'),
            [job.id.toString()],
            {
                execArgv: [],
                silent: true,
                env: job.env
            }
        );
        self.log.info('JOBSVC #' + job.id + ' is forked... Process id:' + jobSvc.pid);

        addJobService(job.id, jobSvc);

        jobSvc.stdout.on('data', function (d) {
            self.log.info('[JOBSVC #' + job.id + ']' + d);
        });

        jobSvc.stderr.on('data', function (d) {
            self.log.error('[JOBSVC #' + job.id + ']' + d);
        });

        jobSvc.on('message', function (m) {
            if (m.action === 'updateStatus') {
                self.log.info('JOBSVC #' + m.job.id + ' status updated... ' + m.job.status);
                updateJobStatus(m.job);
            } else if (m.action === 'end') {
                if (!workingJobs[job.id].status || workingJobs[job.id].status === 'WORKING' || workingJobs[job.id].status === 'INITIALIZING') {
                    job.status = 'ERROR';
                    job.error = new DError('BUILDER012', {status: workingJobs[job.id].status});

                    self.log.error(job.error);

                    updateJobStatus(job);
                }
                exitJobService(job, m.pid);
            } else {
                self.log.error('JOBSVC\'s message is not defined.' + m);
            }
        });

        jobSvc.on('error', function (err) {
            self.log.error('ERROR JOBSVC #' + job.id + '...' + err);
        });

        jobSvc.on('exit', function (code) {
            self.log.info('JOBSVC #' + job.id + ' is exit... Process id:' + jobSvc.pid + ' exit code: ' + code);
            if (code !== null && code !== 0 && code !== 1 && code !== 145) {
                job.status = 'ERROR';
                job.error = new DError('BUILDER011', {code: code});

                self.log.error(job.error);

                updateJobStatus(job);
            }
            delete workingJobs[job.id];

            deleteJobService(job.id, jobSvc.pid);
        });

        return jobSvc;
    }

    function initJobService(job) {
        var jobSvc = forkJobService(job);
        var message = {
            action: 'initialize',
            job: job,
            options: {
                serverId: self.id,
                servers: getServers(),
                config: self.config.getConfigData()
            }
        };

        jobSvc.send(message);
    }

    function buildJobService(job) {
        var jobSvc = forkJobService(job);

        var message = {
            action: 'build',
            job: job,
            options: {
                serverId: self.id,
                servers: getServers(),
                environments: self.environments,
                config: self.config.getConfigData()
            }
        };

        jobSvc.send(message);
    }

    function resumeJobService(job, resumeOptions) {
        var jobSvc = forkJobService(job);

        var message = {
            action: 'resume',
            job: job,
            resumeOptions: resumeOptions,
            options: {
                serverId: self.id,
                servers: getServers(),
                config: self.config.getConfigData()
            }
        };

        jobSvc.send(message);
    }

    function exitJobService(job, pid) {
        if (!jobSvcs[job.id]) {
            self.log.error('JOBSVC\'s is not found. JOB #' + job.id + ' PID:' + pid);
            return;
        }

        var message = {
            action: 'exit',
            job: job
        };

        _.each(jobSvcs[job.id], function (jobSvc) {
            if (jobSvc.pid === pid) {
                jobSvc.send(message);
            }
        });
    }

    function cancelJobService(job) {
        if (!jobSvcs[job.id]) {
            job.status = 'CANCELED';
            updateJobStatus(job);
            return;
        }

        var message = {
            action: 'cancel',
            job: job,
            options: {
                serverId: self.id,
                servers: getServers(),
                config: self.config.getConfigData()
            }
        };

        _.each(jobSvcs[job.id], function (jobSvc) {
            jobSvc.send(message);
        });
    }
}


/**
 * @function createServer
 * @param {string} sid - server id
 * @returns {module:servers/builder/server.BuilderServer}
 * @memberOf module:servers/builder/server
 */

module.exports.createServer = function (sid) {
    BuilderServer.prototype = dibs.BaseServer.createServer(sid, 'builder');

    return new BuilderServer();
};
