/**
 * validation.js
 * Copyright (c) 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 async = require('async');
var fs = require('fs');
var mysql = require('mysql');
var optimist = require('optimist');
var path = require('path');
var url = require('url');
var _ = require('underscore');


var Distribution = require('../org.tizen.repository/distribution.js');
var Package = require('../org.tizen.common/package.js');
var SnapshotModel = require('../dibs.model.common/snapshot.js');

// print log messages using color code.
function stylize(str, style) {
    var styles = {
        grey: [90, 39],
        red: [31, 39],
        green: [32, 39],
        yellow: [33, 39]
    };
    return '\033[' + styles[style][0] + 'm' + str + '\033[' + styles[style][1] + 'm';
}

['grey', 'yellow', 'red', 'green'].forEach(function (style) {
    String.prototype.__defineGetter__(style, function () {
        return stylize(this, style);
    });
});

// custom setting
var customlog = {
    info: function (str) { console.log(str.toString('utf-8').green); },
    error: function (str) { console.log(str.toString('utf-8').red); },
    warn: function (str) { console.log(str.toString('utf-8').yellow); }
};

var usageMessage =
    'This tool is command line interface for migrating snapshots information into database\n' +
    'Usage: $0 <SUBCOMMAND> [OPTS]\n' +
    'Subcommands:\n' +
    '  validate     Validate snapshot migration process\n' +
    'Subcommand usage:\n' +
    '  validate     snapshot-validation validate -r <reposiotry url> [ -S <snapshot name> ] -H <db host> -u <db user name> -p <db password> -d <db name>\n';

var argv = optimist.usage(usageMessage)
    .describe('r', 'repository path.            { dibs-repo }')
    .describe('S', 'target snapshot.            { snapshot name }')
    .describe('H', 'database host.              { db-host }')
    .describe('u', 'database user name.         { db-user }')
    .describe('p', 'database password.          { db-password }')
    .describe('d', 'target database name.       { db-name } ')
    .describe('h', 'show help')
    .string('u')
    .string('p')
    .string('d')
    .string('S')
    .string('H')
    .alias({ h: 'help', s: 'server', r: 'repo', D: 'dist', H: 'host', u: 'user', p: 'password', d: 'database'})
    .argv;

if (argv.h) {
    optimist.showHelp();
    process.exit(0);
}

// arguments
var repoPath = argv.r;

var dbHost = argv.H;
var dbUser = argv.u;
var dbPassword = argv.p;
var dbName = argv.d;

var targetSnapshot = argv.S;

var subCmds = argv._;

// validate sub-commands
if (!subCmds || subCmds.length === 0) {
    console.log('Usage: snapshot-migration <SUBCOMMAND> [OPTS]');
    console.log('Try \'snapshot-migration --help\' for more information');
    process.exit(-1);
}

// validate sub command
if (argv.r === undefined || argv.r === true) {
    customlog.error('Options should be specified!\n');
    optimist.showHelp();
    process.exit(-1);
}


handleSubcommands(subCmds[0]);

function handleSubcommands(command) {
    switch (command) {
    case 'validate':
        validateMigration(function (err) {
            if (err) {
                customlog.error(err);
                customlog.error('Snapshot between file and db is invalid!!');
                process.exit(-1);
            } else {
                customlog.warn('Snapshot between file and db is good!!');
                process.exit(0);
            }
        });
        break;
    default:
        customlog.error('Invalid sub-command: ' + subCmds);
        process.exit(-1);
    }
}

function checkSnapshot(distName, snapshot, callback) {
    var snapshotFromFile = snapshot;
    var snapshotFromDB = null;

    async.series([
        function (cb) {
            searchSnapshotFromDB({ name: snapshot.name, distName: distName }, function (err1, result) {
                if (err1) {
                    cb(err1);
                } else {
                    snapshotFromDB = result;
                    cb(null);
                }
            });
        },
        function (cb) {
            if (snapshotFromFile.archivePackages) {
                checkArchives(snapshotFromFile, snapshotFromDB);
            }
            cb(null);
        },
        function (cb) {
            checkBinaries(snapshotFromFile, snapshotFromDB, cb);
            cb(null);
        }
    ],
    function (err) {
        callback(err);
    });
}


function validateMigration(callback) {
    console.log('Start validating packages between DB and files.');

    // var snapshotFromFile = null;
    // var snapshotFromDB = null;

    async.waterfall([
        function (cb) {
            loadRepo(function (err1, dist) {
                cb(err1, dist);
            });
        },
        function (dist, cb) {
            customlog.info('# - Distribution Name: ' + dist.name);

            async.eachLimit(dist.snapshots, 1, function (snapshot, cb1) {
                if (snapshot.type === 'temp') {
                    customlog.info('# - Skip temp snapshot: ' + snapshot.name);
                    cb1(null);
                    return;
                }
                customlog.info('# - Latest Snapshot Name: ' + snapshot.name);
                checkSnapshot(dist.name, snapshot, cb1);
            },
            function (err1) {
                cb(err1);
            });
        }
    ],
    function (err) {
        callback(err);
    });
}


function loadPackages(snapshots, distPath, callback) {
    async.mapLimit(snapshots, 1, function (snapshot, cb) {
        loadPackageFromSnapshot(snapshot, distPath, function (err1, result) {
            cb(err1, result);
        });
    },
    function (err, snapshots) {
        callback(err, snapshots);
    });
}


function loadPackageFromSnapshot(snapshot, distPath, callback) {
    var snapshotPath = path.join(distPath, snapshot.path);
    var envs = [];

    customlog.info('# - Read packages from \'' + snapshot.name + '\'');

    async.waterfall([
        function (cb) {
            customlog.info(' ## - Read \'os_info\' file.');
            fs.readFile(path.join(snapshotPath, 'os_info'), { encoding: 'utf8'}, function (err1, data) {
                envs = data.split('\n');
                cb(err1);
            });
        },
        function (cb) {
            customlog.info(' ## - Read \'archive_pkg_list\' file.');
            fs.readFile(path.join(snapshotPath, 'archive_pkg_list'), { encoding: 'utf8'}, function (err1, data) {
                var archives = data.split('\n');
                if (archives[0] === '') {
                    customlog.info(' ## - archive_pkg_list is empty');
                } else {
                    snapshot.archivePackages = archives;
                }
                cb(null);
            });
        },
        function (cb) {
            customlog.info(' ## - Read \'pkg_list_{os}\' file.');

            var osPackages = {};
            async.each(envs, function (env, cb1) {
                osPackages[env] = {};

                var pkgListPath = path.join(snapshotPath, 'pkg_list_' + env);
                fs.readFile(pkgListPath, { encoding: 'utf8' }, function (err1, data) {
                    if (err1) {
                        customlog.error(err1);
                        cb1(err1);
                    } else {
                        Package.getPkgListFromString(data, function (err2, pkgs) {
                            if (err2) {
                                customlog.error(err2);
                            } else {
                                _.each(pkgs, function (pkg) {
                                    osPackages[env][pkg.name] = pkg;
                                });
                            }
                            cb1(err2);
                        });
                    }
                });
            },
            function (err) {
                snapshot.osPackages = osPackages;
                cb(err, snapshot);
            });
        }
    ],
    function (err, snapshot) {
        callback(err, snapshot);
    });
}


function validateUrlFormat(input) {
    var result = url.parse(input);
    return ((result.protocol === 'http:') || (result.protocol === 'https:'));
}


function loadRepo(callback) {
    console.log('Load repository from filesystem');
    var isRemote = validateUrlFormat(repoPath);

    customlog.info('# - Repository Path or URL ' + repoPath);
    if (isRemote) {
        loadRemoteRepo(repoPath, function (err, dist) {
            callback(err, dist);
        });
    } else {
        loadLocalRepo(repoPath, function (err, dist) {
            callback(err, dist);
        });
    }
}


function loadLocalRepo(repoDir, callback) {
    async.waterfall([
        function (cb) {
            fs.exists(repoDir, function (exist) {
                if (!exist) {
                    cb(new Error('Given local repository does not exist.'));
                } else {
                    cb(null);
                }
            });
        },
        function (cb) {
            var repoBasePath = path.dirname(repoPath);
            var distInfoPath = path.join(repoBasePath, 'distribution.info');
            var distName = path.basename(repoPath);

            customlog.info('# - Read \'distribution.info\' file from ' + repoBasePath);
            fs.readFile(distInfoPath, { encoding: 'utf8' }, function (err1, data) {
                if (err1) {
                    customlog.error(err1);
                    cb(err1, null);
                } else {
                    var dists = loadDistributionInfoString(data, repoBasePath);
                    var distributions = _.filter(dists, function (dist) {
                        return (dist.name === distName);
                    });

                    cb(null, distributions[0]);
                }
            });
        },
        function (dist, cb) {
            customlog.info('# - Check if each distribution path is valid or not');
            fs.exists(dist.path, function (exist) {
                if (!exist) {
                    cb(new Error(dist.path + ' does not exist.'));
                } else {
                    var snapshotInfo = path.join(dist.path, 'snapshot.info');
                    fs.readFile(snapshotInfo, { encoding: 'utf8' }, function (err2, data) {
                        if (err2) {
                            cb(err2, null);
                        } else {
                            var results = loadSnapshotInfoString(data);
                            // sort by snapshot time.
                            dist.snapshots = _.sortBy(results, function (snapshot) {
                                return snapshot.time;
                            });
                            cb(null, dist);
                        }
                    });
                }
            });
        },
        function (dist, cb) {
            customlog.info('# - Load package information');
            loadPackages(dist.snapshots, dist.path, function (err1, snapshots) {
                dist.snapshots = snapshots;

                var autoSnapshots = _.filter(snapshots, function (snapshot) {
                    return snapshot.type === 'auto';
                });
                dist.latestSnapshot = autoSnapshots[autoSnapshots.length - 1];
                customlog.info('# - Latest Snapshot: ' + dist.latestSnapshot.name);

                cb(err1, dist);
            });
        }
    ],
    function (err, dist) {
        callback(err, dist);
    });
}


function loadRemoteRepo(repoUrl, callback) {
    var serverURL = null;
    var distributionName = null;

    if (repoUrl.slice(-1) === '/') {
        serverURL = repoUrl.substring(0, repoUrl.length - 1);
    } else {
        serverURL = repoUrl;
    }

    var urlInfo = url.parse(serverURL);
    var pathInfo = urlInfo.path.split('/');

    // get snapshot info if exists
    if (urlInfo.path.indexOf('snapshots') > 0) {
        var msg = 'The repository server url includes snapshots.' +
            ' Please specify the url without snapshots path';
        callback(new Error(msg), null);
    } else {
        var dist = null;
        distributionName = pathInfo[pathInfo.length - 1];

        Distribution.loadRemote(url.resolve(serverURL, './'),
            { distName: distributionName, snapshotName: null },
            function (err, rst) {
                if (err) {
                    customlog.error(err);
                } else {
                    if (rst.length > 0) {
                        dist = rst[0];
                    } else {
                        err = new Error('This distribution is empty');
                    }
                }
                callback(err, dist);
            });
    }
}


function searchSnapshotFromDB(conditions, callback) {
    var conn = null;

    async.waterfall([
        function (cb) {
            createDBConnection(function (err, client) {
                conn = client;
                cb(err);
            });
        },
        function (cb) {
            customlog.info(' ## - Select snapshot \'' + conditions.name);
            SnapshotModel.select(conn, conditions, function (err1, results) {
                if (err1) {
                    cb(err1, null);
                } else {
                    if (results.length !== 0) {
                        cb(null, results[0]);
                    } else {
                        cb(null, null);
                    }
                }
            });
        }
    ],
    function (err, snapshot) {
        conn.end(function (err1) {
            if (err1) {
                customlog.error(err1);
            }

            if (err) {
                customlog.error(err);
                return callback(err, null);
            } else {
                return callback(null, snapshot);
            }
        });
    });
}


function compareArtifacts(osList, packagesFromFile, packagesFromDB, callback) {

    if (!packagesFromFile || !packagesFromDB) {
        customlog.error(' ## - Cannot get osPackages object from srcSnapshot or destSnapshot');
        return;
    }

    var files = {};
    var dbs = {};
    var filePkgs = 0;
    var dbPkgs = 0;

    _.each(osList, function (os) {
        files[os] = 0;
        dbs[os] = 0;

        _.each(packagesFromFile[os], function () {
            files[os]++;
            filePkgs++;
        });

        _.each(packagesFromDB[os], function (dbPkg) {
            dbs[os]++;
            dbPkgs++;
            var filePkg = packagesFromFile[os][dbPkg.name];
            if (filePkg) {
                if (filePkg.version !== dbPkg.version) {
                    customlog.error(' ## - ' + filePkg.name + ' different version file:db ' + filePkg.version + ':' + dbPkg.version);
                }

                if (filePkg.os !== dbPkg.os) {
                    customlog.warn(' ## - ' + filePkg.name + ' different os file:db ' + filePkg.os + ':' + dbPkg.os);
                    if (_.contains(filePkg.osList, dbPkg.os)) {
                        customlog.info(' ### - However, ' + dbPkg.os + ' contains in os-list');
                    } else {
                        customlog.error(' ### - Error: ' + dbPkg.os + ' does not contains in os-list');
                    }
                }

                if (filePkg.path !== dbPkg.path) {
                    customlog.error(' ## - ' + filePkg.name + ' different path file:db ' + filePkg.path + ':' + dbPkg.path);
                }

                if (filePkg.checksum !== dbPkg.checksum) {
                    customlog.error(' ## - ' + filePkg.name + ' different checksum file:db ' + filePkg.checksum + ':' + dbPkg.checksum);
                }
            } else {
                customlog.error(' ## - ' + dbPkg.name + ' does not in \'pkg_list_' + os + '\'');
            }
        });
    });

    customlog.info('# - A number of packages between file and db');
    customlog.info('# - ' + filePkgs + ':' + dbPkgs);

    var isEqual = false;

    _.each(files, function (value, key) {
        if (files[key] !== dbs[key]) {
            isEqual = true;
            customlog.error(key + ': different package count: ' + files[key] + ':' + dbs[key]);
        }
    });

    if (isEqual) {
        callback(new Error('A number of packages between file are db is invalid!!'));
    } else {
        callback(null);
    }
}

/*
 * utility APIs
 */

function createDBConnection(callback) {
    var config = {
        host: dbHost || '127.0.0.1',
        port: 3306,
        user: dbUser || 'root',
        password: dbPassword || 'password',
        database: dbName || 'test'
    };

    var client = mysql.createConnection(config);
    client.connect(function (err) {
        if (err) {
            customlog.error(err);
        }
        callback(err, client);
    });
}

// parse distribution.info and convert it into distribution object.
function loadDistributionInfoString(contents, repo) {
    var lines = contents.split('\n');

    var dists = [];
    var newDist = null;
    for (var i = 0; i < lines.length; i++) {
        var line = lines[i];
        var toks = line.split(/[: ]+/);

        if (toks[0] === 'name') {
            if (newDist !== null) {
                dists.push(newDist);
            }
            newDist = { name: toks[1] };
            newDist.path = path.join(repo, newDist.name);
        } else if (toks[0] === 'time') {
            newDist.time = toks[1];
        } else if (toks[0] === 'id') {
            newDist.uid = toks[1];
        } else {
            // ignore
        }
    }

    if (newDist !== null) {
        dists.push(newDist);
    }

    return dists;
}

// parse snapshot.info and convert it into snapshot object.
function loadSnapshotInfoString(contents) {
    var lines = contents.split('\n');

    var snapshots = [];
    var snapshot = null;
    for (var i = 0; i < lines.length; i++) {
        var line = lines[i];
        var toks = line.split(/[: ]+/);

        if (toks[0] === 'name') {
            if (snapshot !== null) {
                snapshots.push(snapshot);
            }
            snapshot = { name: toks[1] };
        } else if (toks[0] === 'time') {
            snapshot.time = toks[1];
        } else if (toks[0] === 'type') {
            snapshot.type = toks[1];
        } else if (toks[0] === 'path') {
            snapshot.path = toks[1];
        }
    }

    if (snapshot !== null) {
        snapshots.push(snapshot);
    }

    return snapshots;
}


function checkArchives(fileSnapshot, dbSnapshot) {
    customlog.info('# - Check archives');

    var archives = _.filter(dbSnapshot.artifacts, function (artifact) {
        return artifact.type === 'archive';
    });

    var isEqual = _.isEqual(fileSnapshot.archivePackages.length, archives.length);
    if (!isEqual) {
        customlog.warn(' ## - archive packages are invalid');
        customlog.error(' ## - archive packages in DB are different with snapshot file');
        customlog.error(' ## - From DB');
        customlog.error(_.map(archives, function (archive) { return archive.name; }));
        customlog.error(' ## - From File');
        customlog.error(fileSnapshot.archivePackages);
    } else {
        customlog.warn(' ## - archive packages are valid');
    }
}


function checkBinaries(fileSnapshot, dbSnapshot, callback) {
    customlog.info('# - Check Binaries');

    var osPackages = {};
    var envs = dbSnapshot.options['__ENVIRONMENTS'];
    _.each(envs, function (os) {
        osPackages[os] = {};
    });

    var tizenPkgs = [];
    _.each(dbSnapshot.artifacts, function (rawPackage) {
        if (rawPackage.type === 'archive') {
            return;
        }

        tizenPkgs.push(convertRawToTizenPackage(rawPackage));
    });

    _.each(envs, function (os) {
        _.each(tizenPkgs, function (pkg) {
            if (pkg.os === os) {
                osPackages[os][pkg.name] = pkg;
            }
        });
    });

    compareArtifacts(envs, fileSnapshot.osPackages, osPackages, callback);
}

function convertRawToTizenPackage(rawPackage) {
    // convert queried artifacts to Package
    var tizenPkg = {
        name: rawPackage.name,
        version: rawPackage.version,
        os: rawPackage.environment,
        type: rawPackage.type,
        attr: rawPackage.attribute,
        path: rawPackage.path,
        size: rawPackage.size,
        checksum: rawPackage.checksum,
        desc: rawPackage.description,
        options: {}
    };

    var regex = /__[A-Z]/;
    _.each(rawPackage.options, function (value, key) {
        var match = regex.exec(key);

        if (match) {
            tizenPkg.options[match.input] = value;
        } else {
            tizenPkg[key] = value;
        }
    });

    return tizenPkg;
}
