/**
 * distribution.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 _ = require('underscore');
var async = require('async');
var fs = require('fs');
var path = require('path');

var DError = require('../../core/exception.js');
var fsDist = require('./lib/fs-distribution-api.js');
var webDist = require('./lib/web-distribution-api.js');
var Snapshot = require('./snapshot.js');
var utils = require('../../lib/utils.js');
var util = require('../org.tizen.ts.base.common/util.js');

module.exports = TizenDistribution;

function TizenDistribution(name, distPath, options) {
    this.name = name;
    this.type = 'tizen';
    this.time = options.time || utils.generateTimeStamp();
    this.snapshots = [];
    this.releases = [];
    this.latestSnapshot = null;
    this.path = distPath;
    this.options = options;
}


TizenDistribution.prototype.searchSnapshots = searchSnapshots;
TizenDistribution.prototype.loadSnapshots = loadSnapshots;

function loadSnapshots(callback) {
    var self = this;

    // in case of web-repo
    async.waterfall([
        function (cb) {
            // load snapshot.info
            if (utils.isURL(self.path)) {
                webDist.loadSnapshots(self.path, function (err, snapshotInfos) {
                    if (err) {
                        // TODO: should handle errors.
                    } else {
                        self.snapshots = snapshotInfos;
                    }
                    cb(err);
                });
            } else {
                fsDist.loadSnapshots(self.path, function (err, snapshotInfos) {
                    if (err) {
                        // TODO: should handle errors.
                    } else {
                        self.snapshots = snapshotInfos;
                    }
                    cb(err);
                });
            }
        },
        function (cb) {
            if (self.snapshots && self.snapshots.length > 0) {
                var latestSnapshot = getLatestSnapshot(self.snapshots);

                self.searchSnapshots(
                    {
                        distPath: self.path,
                        name: latestSnapshot
                    },
                    function (err, results) {
                        if (!err) {
                            self.latestSnapshot = results[0];

                            var snapshots = [];
                            _.each(self.snapshots, function (snapshot) {
                                if (snapshot.name === self.latestSnapshot.name) {
                                    snapshot = _.extend(snapshot, self.latestSnapshot);
                                }
                                snapshots.push(snapshot);
                            });
                            self.snapshots = snapshots;
                        }
                        cb(err);
                    }
                );
            } else {
                cb(null);
            }
        }
    ],
    function (err) {
        callback(err);
    });
}


TizenDistribution.prototype.create = function (callback) {
    var self = this;
};


TizenDistribution.prototype.remove = function (callback) {
    var self = this;
};


function searchSnapshots(conditions, callback) {
    var self = this;

    if (conditions.latest || conditions.name === null) {
        if (self.latestSnapshot) {
            return callback(null, [self.latestSnapshot]);
        } else {
            conditions.name = getLatestSnapshot(self.snapshots).name;
        }
    }

    var snapshotInfos = _.filter(self.snapshots, function (snapshot) {
        return snapshot.name === conditions.name;
    });

    if (snapshotInfos.length === 0) {
        // console.log('\'' + path.basename(self.path) + '\' distribution does not have any snapshot!');
        return callback(null, []);
    }

    async.mapLimit(snapshotInfos, 4, function (snapshotInfo, cb) {
        var snapshot = new Snapshot(snapshotInfo.name);
        snapshot.time = snapshotInfo.time;
        snapshot.type = snapshotInfo.type;
        snapshot.path = snapshotInfo.path;

        loadPackages(conditions.distPath, snapshot, function (err1, results) {
            if (!err1) {
                snapshot.archivePackages = results.archivePackages;
                snapshot.osPackages = results.osPackages;
                snapshot.changes = results.changes;
            }
            cb(err1, snapshot);
        });

    },
    function (err, results) {
        callback(err, results);
    });
}


TizenDistribution.prototype.removeSnapshot = function (snapshotList, options, callback) {
    var self = this;

    /*
    async.eachLimit(snapshotList, 1, function (snapshot, cb) {
        // remove packages and update snapshot.info, archive_pkg_list, os_info and pkg_list_xxx files
        self.remove();

        // remove snapshot object from distribution.snapshots array.
        delete self.snapshots[name];
    },
    function (err) {
        callback(err);
    });
    */
    callback(null);
};


TizenDistribution.prototype.removePackages = function (callback) {
    var self = this;

    callback(null);
};


TizenDistribution.prototype.generateSnapshot = function (options, callback) {
    var self = this;

    var baseSnapshotName = null;
    if (options.refSnapshot) {
        baseSnapshotName = options.refSnapshot;
    } else if (self.latestSnapshot) {
        baseSnapshotName = self.latestSnapshot.name;
    } else {
        // at the first snapshot.
    }

    options.name = name;

    async.waterfall([
        function (cb) {
            createSnapshot(baseSnapshotName, options, cb);
        },
        function (newSnapshotData, oldSnapshotData, cb) {
            saveSnapshot(self, [], newSnapshotData, options, cb);
        }
    ], function (err, newSnapshot) {
        callback(err, newSnapshot);
    });
};


TizenDistribution.prototype.registerPackages = function (packagePathList, options, monitor, callback) {
    var self = this;

    var baseSnapshotName = null;
    if (self.latestSnapshot) {
        baseSnapshotName = self.latestSnapshot.name;
    }

    // options.name = name;

    var archives = [];
    var binaries = [];
    var updatedPkgs = null;

    async.waterfall([
        function (cb) {
            monitor.updateProgress(' # - Getting a list of package infos from files...');
            getPackageInfoListFromFiles(packagePathList, monitor, function (err, results) {
                updatedPkgs = results;
                cb(err);
            });
        },
        function (cb) {
            _.each(updatedPkgs, function (value, key, object) {
                var pkg = updatedPkgs[key];
                if (_.isString(pkg)) {
                    archives.push(path.basename(pkg));
                } else {
                    binaries.push(pkg);
                }
            });
            cb(null);
        },
        function (cb) {
            monitor.updateProgress(' # - Creating new snapshot...');
            createSnapshot(self, baseSnapshotName, options, monitor, cb);
        },
        function (newSnapshot, oldSnapshot, cb) {
            newSnapshot.overwritePkgs = [];

            newSnapshot.archivePackages = _.union(newSnapshot.archivePackages, archives);
            async.each(binaries, function (pkg, cb1) {
                var os = pkg.os;
                var pkgName = pkg.name;

                if (newSnapshot.osPackages[os] === undefined) {
                    newSnapshot.osPackages[os] = {};
                }

                if (oldSnapshot && oldSnapshot.osPackages) {
                    var oldPkg = oldSnapshot.osPackages[os][pkgName];
                    if (oldPkg) {
                        var compared = utils.compareVersion(pkg.version, oldPkg.version);
                        if (compared < 0 || (compared === 0 && !options.force)) {
                            return cb1('version conflict');
                            //return cb1(new DError('FSREPO001', {
                            //    repo: 'aaaa',
                            //    new: pkg.version,
                            //    old: oldPkg.version
                            //}));
                        }
                    }
                }

                if (options.isTemp) {
                    pkg.path = '/temp/jobs/' + options.tempJobId + '/';
                    pkg.status = 'TEMP';
                } else {
                    pkg.path = '/binary/' + util.package.getPackageFileNameFromInfo(pkg.name, pkg.version, pkg.os);
                    pkg.status = 'OPEN';
                }

                newSnapshot.osPackages[os][pkg.name] = pkg;
                cb1(null);
            },
            function (err) {
                cb(err, newSnapshot);
            });
        },
        function (newSnapshotData, cb) {
            monitor.updateProgress(' # - Saving new snapshot into filesystem');
            saveSnapshot(self, updatedPkgs, newSnapshotData, options, monitor, function (err, newSnapshot) {
                cb(err, newSnapshot);
            });
        },
        // function (newSnapshot, cb) {
        //     // validate
        //     if (!options.skipIntegrityCheck) {
        //         validateSnapshot(newSnapshot, function (err) {
        //             cb(err, newSnapshot);
        //         });
        //     } else {
        //         return cb(null, newSnapshot);
        //     }
        // },
        function (newSnapshot, cb) {
            monitor.updateProgress(' # - Update new snapshot into distribution \'' + newSnapshot.name + '\'');
            addSnapshotToDist(self, newSnapshot);
            cb(null, newSnapshot);
        }
    ],
    function (err, newSnapshot) {
        callback(err, newSnapshot);
    });
};


function createSnapshot(Distribution, baseSnapshotName, options, monitor, callback) {
    var self = Distribution;

    var timestamp = utils.generateTimeStamp(true);
    var snapshotName = timestamp;
    var snapshotPath = '/snapshots/' + snapshotName;
    var attribute = 'auto';
    var uploader = 'DIBS';

    if (options.userEmail) {
        uploader = options.userEmail;
    }

    /*
    if (options && options.isTemp) {
        snapshotName = 'TEMP_JOB' + options.tempJobId;
    } else if (options && options.name) {
        snapshotName = options.name;
    } else {
        snapshotName = timestamp;
    }

    if (options && options.snapshotType === 'manual') {
        attribute = options.snapshotType;
    } else if (options && options.isTemp) {
        attribute = 'temp';
    } else {
        attribute = 'auto';
    }

    if (options && options.isTemp) {
        snapshotPath = '/temp/jobs/' + options.tempJobId + '/';
    } else if (options && options.path) {
        snapshotPath = options.path;
    } else {
        snapshotPath = '/snapshots/' + snapshotName;
    }
    */

    async.waterfall([
        function (cb) {
            self.searchSnapshots({ distPath: self.path, name: snapshotName }, function (err, results) {
                if (err) {
                    return cb(err);
                } else if (results && results.length > 0) {
                    if (options.isForce === undefined || options.isForce === false) {
                        return cb(new DError('TREPO022', { name: snapshotName }));
                    } else {
                        // remove snapshot by force
                        self.removeSnapshots([snapshotName], options, cb);
                    }
                } else {
                    return cb(null);
                }
            });
        },
        function (cb) {
            if (!baseSnapshotName) {
                /*
                options['__ENVIRONMENTS'] = [];
                _.each(dibs.plugin.getExtensions('dibs.base.environment'), function (env) {
                    if (env.attributes.type === 'tizen') {
                        options['__ENVIRONMENTS'].push(env.attributes.name);
                    }
                });
                options.changes = '';
                */
                monitor.updateProgress(' # - Create \'' + snapshotName + '\' snapshot');

                var newSnapshot = new Snapshot(snapshotName);
                newSnapshot.time = timestamp;
                newSnapshot.path = snapshotPath;
                newSnapshot.attribute = attribute;
                newSnapshot.uploader = uploader;
                return cb(null, newSnapshot, null);
            } else {
                self.searchSnapshots({ distPath: self.path, name: baseSnapshotName }, function (err, results) {
                    if (err) {
                        return cb(err);
                    } else if (results.length !== 1) {
                        return cb(new DError('TREPO023', { ref: baseSnapshotName }));
                    } else {
                        monitor.updateProgress(' # - Create snapshot based on \'' + results[0].name + '\'');

                        var newSnapshot = _.clone(results[0]);
                        newSnapshot.name = snapshotName;
                        newSnapshot.path = snapshotPath;
                        newSnapshot.time = timestamp;
                        newSnapshot.attribute = attribute;
                        newSnapshot.uploader = uploader;

                        cb(null, newSnapshot, results[0]);
                    }
                });
            }
        }
    ],
    function (err, snapshot, oldSnapshot) {
        callback(err, snapshot, oldSnapshot);
    });
}


function saveSnapshot(Distribution, updatedPkgs, snapshot, options, monitor, callback) {
    var self = Distribution;
    var conditions = {
        distPath: self.path,
        name: snapshot.name
    };

    if (utils.isURL(self.path)) {
        callback(null, snapshot);
    } else {
        async.series([
            function (cb) {
                getChangeLogs(snapshot);
                cb(null);
            },
            function (cb) {
                monitor.updateProgress(' # - Check new snapshot if alreay exist in repostiroy ...');
                self.searchSnapshots(conditions, function (err, results) {
                    if (err) {
                        return cb(err);
                    } else if (results && results.length > 0) {
                        return cb(new DError('TREPO022', { snapshot: snapshot.name}));
                    } else {
                        return cb(null);
                    }
                });
            },
            function (cb) {
                if (_.isEmpty(updatedPkgs)) {
                    return cb(null);
                } else {
                    monitor.updateProgress(' # - Copying update pacakges into repository...');
                    fsDist.copyFilesToRepository(self.path, updatedPkgs, options, monitor, cb);
                }
            },
            function (cb) {
                monitor.updateProgress(' # - Saving new snapshot into snapshot.info...');
                fsDist.saveSnapshot(self.path, snapshot, monitor, cb);
            }
        ],
        function (err) {
            callback(err, snapshot);
        });
    }
}


function addSnapshotToDist(distribution, newSnapshot) {
    var self = distribution;

    // var updatedPackages = Snapshot.getUpdatedPackages(newSnapshot, dist.latestSnapshot);
    // dibs.thisServer.emitEvent({
    //     event: 'TIZEN_REPO_SNAPSHOT_GENERATED',
    //     distributionName: dist.name,
    //     updatedPackages: updatedPackages
    // });

    var snapshotInfo = _.clone(newSnapshot);

    // reset unnecessary info
    snapshotInfo.archivePackages = null;
    snapshotInfo.osPackages = null;
    snapshotInfo.removedPkgs = null;
    snapshotInfo.newPkgs = null;
    snapshotInfo.updatedPkgs = null;

    self.snapshots.push(snapshotInfo);
    // only "auto" snapshot can be the latest
    if (newSnapshot.type === 'auto') {
        self.latestSnapshot = newSnapshot;
    }
}

// TODO: need to fix it
function validateSnapshot(snapshot, callback) {
    var errMsgs = [];
    // check only for nomal packages
    for (var os in snapshot.osPackages) {
        for (var pname in snapshot.osPackages[os]) {
            var pkg = snapshot.osPackages[os][pname];
            var result = findUnmetDependencies(pkg.installDepList, pkg.os, snapshot);
            if (result.length > 0) {
                errMsgs.push('- Invalid install dependency of ' + pkg.name + '(' + pkg.os +
                        '). The following package(s) must be in the distribution:' + result.join(','));
            }

            result = findUnmetDependencies(pkg.buildDepList, pkg.os, snapshot);
            if (result.length > 0) {
                errMsgs.push('- Invalid build dependency of ' + pkg.name + '(' + pkg.os +
                        '). The following package(s) must be in the distribution:' + result.join(','));
            }

            result = findSourceUnmetDependencies(pkg, snapshot);
            if (result.length > 0) {
                errMsgs.push('- Invalid source dependency of ' + pkg.name + '(' + pkg.os +
                        '). The following package(s) must be in the distribution:' + result.join(','));
            }
        }
    }

    if (errMsgs.length > 0) {
        return callback(new DError('TREPO016', {
            snapshot: snapshot.name,
            errMsg: errMsgs.join('\n')
        }));
    } else {
        return callback(null);
    }
}

// TODO: need to fix it
function getChangeLogs(snapshot) {
    snapshot.changes = '';

    // date , uploader
    var dateFormat = utils.getTimeString(utils.getDateFromTimeStamp(snapshot.time));
    if (snapshot.uploader) {
        snapshot.changes += dateFormat + ' <' + snapshot.uploader + '>\n';
    } else {
        snapshot.changes += dateFormat + ' <' + 'admin@user' + '>\n';
    }

    // new pkgs
    if (snapshot.newPkgs && snapshot.newPkgs.length > 0) {
        appendNewPackagesToChangeLog(snapshot);
    }
    // removed pkgs
    if (snapshot.removedPkgs && snapshot.removedPkgs.length > 0) {
        appendRemovedPackagesToChangeLog(snapshot);
    }
    // update pkgs
    if (snapshot.updatedPkgs && snapshot.updatedPkgs.length > 0) {
        appendUpdatedPackagesToChangeLog(snapshot);
    }
}


function loadPackages(distPath, snapshot, callback) {
    if (utils.isURL(distPath)) {
        loadPackagesFromWeb(distPath, snapshot.path, callback);
    } else {
        loadPackagesFromFile(distPath, snapshot.path, callback);
    }
}


function loadPackagesFromFile(distPath, snapshotPath, callback) {
    // var self = snapshot;
    var snapshot = {};

    async.series([
        function (cb) {
            fsDist.loadPackages(path.join(distPath, snapshotPath), function (err, results) {
                if (err) {
                    // cb(new DError('FSREPO004', { name: self.name, repo: distPath }), null);
                    cb(err, null);
                } else {
                    snapshot.archivePackages = results.archivePackages;
                    snapshot.osPackages = results.osPackages;
                    cb(null);
                }
            });
        },
        function (cb) {
            fsDist.loadChangeLog(distPath, path.basename(snapshotPath), function (err, results) {
                snapshot.changes = results;
                cb(err);
            });
        }
    ],
    function (err) {
        callback(err, snapshot);
    });
}


function loadPackagesFromWeb(distUrl, snapshotPath, callback) {
    var snapshot = {};

    async.series([
        function (cb) {
            webDist.loadPackages(distUrl + '/' + snapshotPath, function (err, results) {
                if (err) {
                    cb(err, null);
                } else {
                    snapshot.archivePackages = results.archivePackages;
                    snapshot.osPackages = results.osPackages;
                    cb(null);
                }
            });
        },
        function (cb) {
            webDist.loadChangeLog(distUrl, path.basename(snapshotPath), function (err, results) {
                snapshot.changes = results;
                cb(err);
            });
        }
    ],
    function (err) {
        callback(err, snapshot);
    });
}


function getPackageInfoListFromFiles(pkgPathList, monitor, callback) {
    var fileInfos = {};

    async.eachLimit(pkgPathList, 8, function (pkgPath, cb) {
        if (path.extname(pkgPath) === '.zip') {
            getPackageInfo(pkgPath, monitor, function (err, pkg) {
                fileInfos[pkgPath] = pkg;
                cb(err);
            });
        } else {
            // archive packages
            fileInfos[pkgPath] = pkgPath;
            cb(null);
        }
    }, function (err) {
        callback(err, fileInfos);
    });
}

function getPackageInfo(pkgPath, monitor, callback) {
    async.waterfall([
        function (cb) {
            fs.stat(pkgPath, cb);
        },
        function (stat, cb) {
            utils.getCheckSum(pkgPath, function (err, checksum) {
                cb(err, {
                    size: stat.size,
                    checksum: checksum
                });
            });
        },
        function (info, cb) {
            monitor.updateProgress(' # - extracting pkginfo.manifest...' + pkgPath);
            util.package.getPkgInfoFromPkgFile(pkgPath, function (err, pkg) {
                if (err) {
                    monitor.updateProgress(' # - failed extract pkginfo.manifest...' + pkgPath);
                    return cb(err);
                } else {
                    pkg.checksum = info.checksum;
                    pkg.size = info.size;
                    pkg.origin = 'local';
                    monitor.updateProgress(' # - succeeded to extract pkginfo.manifest...' + pkgPath);
                    return cb(null, pkg);
                }
            });
        }], callback);
}


function appendNewPackagesToChangeLog(snapshot) {
    var pkgs = {};
    snapshot.newPkgs.forEach(function (e) {
        if (pkgs[e.name]) {
            pkgs[e.name].osList.push(e.os);
        } else {
            if (typeof e === 'string') {
                pkgs[e] = {};
            } else {
                var changelog = findChangeLogForVersion(e.changelog, e.version);
                pkgs[e.name] = {
                    version: e.version,
                    osList: [e.os],
                    changeLog: changelog
                };
            }
        }
    });

    snapshot.changes += '* New Packages\n';
    for (var pkgName in pkgs) {
        if (pkgs[pkgName].version) {
            snapshot.changes += ' - ' + pkgName + ' (' + pkgs[pkgName].version + ') [' + pkgs[pkgName].osList.join(',') + ']\n';
            snapshot.changes += pkgs[pkgName].changeLog + '\n';
        } else {
            snapshot.changes += ' - ' + pkgName + '\n';
        }
    }
}


function appendRemovedPackagesToChangeLog(snapshot) {
    var pkgs = {};
    snapshot.removedPkgs.forEach(function (e) {
        if (pkgs[e.name]) {
            pkgs[e.name].osList.push(e.os);
        } else {
            if (typeof e === 'string') {
                pkgs[e] = {};
            } else {
                pkgs[e.name] = {
                    version: e.version,
                    osList: [e.os]
                };
            }
        }
    });

    snapshot.changes += '* Removed Packages\n';
    for (var pkgName in pkgs) {
        if (pkgs[pkgName].version) {
            snapshot.changes += ' - ' + pkgName + ' (' + pkgs[pkgName].version + ') [' + pkgs[pkgName].osList.join(',') + ']\n';
        } else {
            snapshot.changes += ' - ' + pkgName + '\n';
        }
    }
}


function appendUpdatedPackagesToChangeLog(snapshot) {
    var pkgs = {};
    snapshot.updatedPkgs.forEach(function (e) {
        if (pkgs[e.name]) {
            pkgs[e.name].osList.push(e.os);
        } else {
            if (typeof e === 'string') {
                pkgs[e] = {};
            } else {
                var changelog = findChangeLogForVersion(e.changelog, e.version);
                pkgs[e.name] = {
                    version: e.version,
                    osList: [e.os],
                    changeLog: changelog
                };
            }
        }
    });

    snapshot.changes += '* Updated Packages\n';
    for (var pkgName in pkgs) {
        if (pkgs[pkgName].version) {
            snapshot.changes += ' - ' + pkgName + ' (' + pkgs[pkgName].version + ') [' + pkgs[pkgName].osList.join(',') + ']\n';
            snapshot.changes += pkgs[pkgName].changeLog + '\n';
        } else {
            snapshot.changes += ' - ' + pkgName + '\n';
        }
    }
}


function findChangeLogForVersion(changelogs, version) {
    if (!changelogs) {
        return '';
    }

    var logs = changelogs.filter(function (e) {
        return e.version === version;
    });
    if (logs.length > 0) {
        return logs[0].contents.split('\n').map(function (e) {
            return '    ' + e;
        }).join('\n');
    } else {
        return '';
    }
}

function getLatestSnapshot(list) {
    var snapshots = _.filter(list, function (snapshot) {
        return snapshot.type === 'auto' || snapshot.type === 'manual';
    });
    return snapshots[snapshots.length - 1].name;
}
