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

var DError = require('../../core/exception');
var dibs = require('../../core/dibs.js');
var Utils = require('../../lib/utils.js');
var Job = require('../dibs.model.common/job.js');
var Package = require('../org.tizen.common/package.js');
var Tutils = require('../org.tizen.common/tizen_utils');
var ReverseBuildChecker = require('../org.tizen.projects/reverse-build.js');
var Snapshot = require('../org.tizen.repository/snapshot.js');
var Distribution = require('../org.tizen.repository/distribution.js');
var RemoteRepo = require('../org.tizen.repository/remote-repo.js');
var TizenCommon = require('../org.tizen.common/tizen_common.js');

/**
 * Tizen sync distribution project plugin
 * @module models/tizen-sync distribution-project/project
 */

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


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

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

    this.compatJob = null;
    this.snapshot = null;
    this.options.packages = [];
    this.initEnvironments = dibs.projectTypes['Tizen-Sync-Dist'].environments
        .filter(function (e) {
            return (e.indexOf('windows') === -1);
        });
    this.execEnvironments = dibs.projectTypes['Tizen-Sync-Dist'].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) {
            callback(err, new TizenSyncDistJob(newJob));
        });

}


/**
 * Initialize Tizen sync distribution 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/tizenGit/project
 */
// TODO: dist sync must be process when no working jobs
// TODO: search remote distribution testcase
function initializeJob(job, options, callback) {
    var monitor = options.monitor;
    var server = dibs.getServer(options.parentServerId);

    var syncURL = job.options.SYNC_URL;
    var syncDistName = job.options.SYNC_DIST_NAME;

    var currDist = null;
    var syncDist = null;
    var updatedPkgs = null;
    var subJobIds = null;
    async.waterfall([
        // check updated packages
        function (cb) {
            checkUpdatedBinaryProjectPackages(job.distName,
                syncURL, syncDistName, monitor, cb);
        },
        // check integrity before registered
        function (pkgs, cDist, sDist, cb) {
            updatedPkgs = pkgs;
            currDist = cDist;
            syncDist = sDist;
            if (currDist.options.SKIP_INTEGRITY_CHECK) {
                cb(null);
            } else {
                monitor.updateProgress('Checking package integrity...');
                checkPackageIntegrity(currDist, updatedPkgs, cb);
            }
        },
        // download the updated packages
        function (cb) {
            monitor.updateProgress('Downloading remote packages...');
            downloadPackagesFromRemote(server, updatedPkgs, syncDist, monitor, cb);
        },
        // create sub jobs for registering downloaded packages
        function (cb) {
            monitor.updateProgress('Creating sub jobs...');
            createSubJobs(job, updatedPkgs, cb);
        },
        // wait for sub jobs initialized
        function (jobIds, cb) {
            subJobIds = jobIds;
            monitor.updateProgress('Waiting for all sub jobs initialized...');
            waitForSubJobsInitialized(subJobIds, cb);
        },
        function (cb) {
            monitor.updateProgress('Initializing sub jobs done!', cb);
        },
        // update information of sub jobs
        function (cb) {
            monitor.updateProgress('Updating sub jobs info...');
            updateSubJobs(job, subJobIds, cb);
        }], function (err) {
        callback(err, job);
    });
}


// callback( err, pkgs, currDist, syncDist )
function checkUpdatedBinaryProjectPackages(distName, syncURL, syncDistName, monitor, callback) {
    var currDist = null;
    var syncDist = null;

    async.waterfall([
        // get current distribution
        function (cb) {
            getCurrentDistribution(distName, cb);
        },
        // get remote distribution
        function (dist, cb) {
            currDist = dist;
            monitor.updateProgress('Loading remote distribution...');
            RemoteRepo.getRemoteDistribution(syncURL, syncDistName, {
                snapshotName: null
            }, cb);
        },
        // get updated packages for binary projects
        function (dist, cb) {
            syncDist = dist;
            getUpdatedBinaryProjectPackages(currDist, syncDist, cb);
        }],
        function (err, pkgs) {
            callback(err, pkgs, currDist, syncDist);
        });
}


function getCurrentDistribution(distName, callback) {
    dibs.rpc.repo.searchDistributions({
        repoType: 'tizen',
        distName: distName
    }, function (err, dists) {
        if (err) {
            callback(err, null);
        } else if (dists.length === 0) {
            callback(new Error('No such a distribution: ' + distName), null);
        } else {
            callback(null, dists[0]);
        }
    });
}


function getUpdatedBinaryProjectPackages(currDist, targetDist, callback) {
    var currSnapshot = currDist.latestSnapshot;
    var targetSnapshot = targetDist.latestSnapshot;

    // get all updateds
    var uPkgs = Snapshot.getUpdatedPackages(targetSnapshot, currSnapshot);

    // filter using binary project
    dibs.rpc.datamgr.searchProjects({
        distName: currDist.name,
        type: 'Tizen-Binary'
    }, function (err, prjs) {
        if (err) {
            callback(err, null); return;
        }
        var result = [];
        for (var i = 0; i < uPkgs.length; i++) {
            var pkg = uPkgs[i];
            var uPrjs = prjs.filter(function (prj) {
                return prj.options.PKG_NAME === pkg.name;
            });
            if (uPrjs.length > 0) {
                result.push({
                    package: pkg,
                    project: uPrjs[0]
                });
            }
        }
        callback(null, result);
    });
}


function checkPackageIntegrity(currDist, updatedPkgs, callback) {
    var pkgs = updatedPkgs.map(function (upkg) {
        return upkg.package;
    });
    // gather all deps
    var deps = Package.getAllDepsOfPackages(pkgs);

    var snapshot = currDist.latestSnapshot;
    var unmetDeps = [];
    deps.forEach(function (dep) {
        if (dep.os && (!snapshot.osPackages[dep.os] || !snapshot.osPackages[dep.os][dep.name])) {
            if (!snapshot.archivePackages || (snapshot.archivePackages.indexOf[dep.name] === -1)) {
                unmetDeps.push(dep);
            }
        }
        if (!dep.os && (snapshot.archivePackages.indexOf[dep.name] === -1)) {
            unmetDeps.push(dep);
        }
    });

    if (unmetDeps.length > 0) {
        callback(new Error('Unmet dependency :' + unmetDeps.map(function (dep) {
            return '(' + dep.name + ':' + dep.os + ')';
        }).join(',')));
    } else {
        callback(null);
    }
}


function downloadPackagesFromRemote(server, updatedPkgs, targetDist, monitor, callback) {
    var snapshot = targetDist.latestSnapshot;
    var tempPath = null;
    async.waterfall([
        //  create temp dir
        function (cb) {
            Utils.genTemp(cb);
        },
        // download
        function (tempDir, cb) {
            tempPath = tempDir;
            var pkgs = updatedPkgs.map(function (e) {
                return e.package;
            });
            var progress = function (msg) {
                monitor.updateProgress(msg);
            };
            Distribution.downloadPackagesFromRemote(pkgs, snapshot, tempPath,
                targetDist.path, progress, cb);
        },
        // add
        function (lpaths, cb) {
            addFiles(server, lpaths, cb);
        }], function (err, rpaths) {
        if (tempPath !== null) {
            extfs.removeSync(tempPath);
        }
        if (!err) {
            for (var i = 0; i < updatedPkgs.length; i++) {
                updatedPkgs[i].dfsPath = rpaths[i];
            }
        }

        callback(err);
    });
}


function addFiles(server, lpaths, callback) {
    async.mapLimit(lpaths, 8,
        function (lpath, cb) {
            server.dfsAddFile(null, lpath, {
                lifetime: 60 * 60 * 1000
            }, cb);
        },
        function (err, rpaths) {
            callback(err, rpaths);
        });
}


function createSubJobs(parentJob, updatedPkgs, callback) {
    async.mapLimit(updatedPkgs, 8,
        function (uPkg, cb) {
            createSubJob(parentJob, uPkg, cb);
        },
        function (err, results) {
            callback(err, results);
        });
}


function createSubJob(parentJob, updatedPkg, callback) {
    var pkgInfo = Tutils.getInfoFromPackageFileName(
        path.basename(updatedPkg.dfsPath));
    var envName = pkgInfo ? pkgInfo.os : 'ubuntu-32';

    dibs.rpc.jobmgr.addSubJob(parentJob.id, updatedPkg.project.name, envName, {
        FILEPATH: updatedPkg.dfsPath,
        FORCE_REBUILD: false,
        UPLOAD: false,
        NO_REVERSE_CHECK: true
    }, callback);
}


function waitForSubJobsInitialized(jobIds, 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) {
                        cb(err);
                    } else if (status !== 'INITIALIZED') {
                        cb(new DError('TIZENMULTIJOB003', {
                            jobId: jobId
                        }));
                    } else {
                        cb(err);
                    }
                });
        },
        function (err) {
            callback(err);
        });
}


function updateSubJobs(parentJob, subJobIds, callback) {
    async.map(subJobIds,
        function (jobId, cb) {
            dibs.rpc.jobmgr.queryJob(jobId, cb);
        },
        function (err, results) {
            if (!err) {
                parentJob.subJobs = results;
                // update result pkgs
                for (var i = 0; i < results.length; i++) {
                    // skip if not "FINISHED"
                    if (results[i].status !== 'FINISHED') {
                        continue;
                    }
                    parentJob.options.packages = parentJob.options.packages.concat(results[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);
        });
}


/**
 * Execute Tizen sync distribution job
 * - it wait for sub-jobs finished & upload them
 * @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/tizenGit/project
 */
function executeJob(job, options, callback) {
    var jobWorkPath = options.jobWorkPath;
    var monitor = options.monitor;

    async.series([
        // 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);
        },
        // 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) {
            monitor.updateProgress('Preparing to upload result files...');
            prepareToUpload(job, monitor, function (err, rpaths) {
                if (!err) {
                    job.resultFiles = rpaths;
                }
                cb(err);
            });
        },
        // check reverse-build
        function (cb) {
            monitor.updateProgress('Checking reverse build...', cb);
        },
        function (cb) {
            ReverseBuildChecker.check(job, monitor, function (err, result) {
                updateReverseJobs(job, result, function (err1) {
                    // ignore error, but change its result files empty
                    if (err && !err1) {
                        monitor.updateProgress('WARNING! Reverse build failed!');
                        job.resultFiles = [];
                    } else if (!err && err1) {
                        monitor.updateProgress('WARNING! Job information update failed!');
                        job.resultFiles = [];
                    } else if (err && err1) {
                        monitor.updateProgress('WARNING! Reverse build failed!');
                        monitor.updateProgress('WARNING! Job information update failed!');
                        job.resultFiles = [];
                    } else {
                        // nothing to do
                    }
                    // ignore error
                    cb(null);
                });
            });
        },
        function (cb) {
            monitor.updateProgress('Checking uploading...');
            monitor.updateProgress(' - uploading...');
            if (job.resultFiles.length > 0) {
                upload(job, jobWorkPath, monitor, false, cb);
            } else {
                monitor.updateProgress('WARNING! Nothing to be uploaded!');
                cb(null);
            }
        },
        function (cb) {
            monitor.updateProgress('Terminating sync distribution job...', cb);
        }], function (err) {
        // NOTE. MUST strip unnecessary information for reducing object size
        //       if not, DNODE RPC cannnot receive callback
        TizenCommon.stripJob(job);

        callback(err, job);
    });
}


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 (err.errno && err.errno === 'JOBMGR005') {
                            monitor.updateProgress('Sub sync-job is finished with ' +
                                status + '...' + subJob.projectName + '(' + subJob.id + ')');
                            cb(null);
                        } else {
                            cb(err);
                        }
                    } else {
                        cb(err);
                    }
                });
        },
        function (err) {
            callback(err);
        });
}


function prepareToUpload(job, monitor, callback) {
    monitor.updateProgress('Preparing to upload result files... ');

    var result = [];
    async.eachSeries(job.subJobs,
        function (subJob, cb) {
            getSubJobPackageFiles(subJob.id, function (err, pkgs) {
                if (!err) {
                    result = result.concat(pkgs);
                }
                cb(err);
            });
        },
        function (err) {
            if (err) {
                monitor.updateProgress({
                    log: err.message,
                    logType: 'error'
                });
            }
            callback(err, result);
        });
}


function upload(job, jobWorkPath, monitor, force, callback) {
    monitor.updateProgress('Uploading... ');

    var repoServers = dibs.getServersByType('repo');
    if (repoServers.length === 0) {
        var error = new DError('TIZENGITJOB007');
        callback(error);
        return;
    }

    // register the files of all sub-jobs
    if (job.resultFiles.length !== 0) {
        // NOTE. sync must consider compatibility
        repoServers[0].registerRemotePackages(job.resultFiles, {
            repoType: 'tizen',
            distName: job.distName,
            forceUpload: force,
            noChangeLogCheck: true,
            progress: function (msg) {
                monitor.updateProgress(' - ' + msg);
            }
        }, callback);
    } else {
        callback(null);
    }
}


function getSubJobPackageFiles(subJobId, callback) {
    async.waterfall([
        function (cb) {
            dibs.rpc.jobmgr.queryJob(subJobId, cb);
        },
        function (subJob, cb) {
            // check job succeeded first and get its result files
            if (subJob.status === 'FINISHED' && subJob.resultFiles !== undefined) {
                cb(null, subJob.resultFiles);
            } else {
                cb(null, []);
            }
        }], function (err, result) {
        callback(err, result);
    });
}


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