/**
 * server.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 express = require('express.io');
var async = require('async');
var _ = require('underscore');

var fs = require('fs');
var extfs = require('fs-extra');
var yaml = require('js-yaml');

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

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

    return new RepositoryServer();
};

var DEFAULT_REPO_DIR_NAME = 'repository';
var REPO_CONFIG_FILE = 'repo.yaml';

function RepositoryServer() {
    var self = this;
    this.name = 'repository';

    var app = express();
    var repoPath = null;
    var DEFAULT_REPO_TYPE = 'basic';
    var repoTypes = null;
    // For managing distribution syncronization
    var distributionsInSync = {};

    var distributions = {};

    // repo configure
    this.repoConfig = {};

    this.lockFilePath = path.join(dibs.config.getConfigDir(), self.id, '.lock');

    // CALLBACK: for creating server's default configuration
    this.getDefaultConfiguration = function (baseConfig) {
        var defaultConfig = baseConfig;
        defaultConfig['dfs-size'] = 1024 * 1024 * 1024 * 10; // 10G
        defaultConfig['repo_path'] = utils.osPathJoin(baseConfig.os_type, [baseConfig.config_root_dir, baseConfig.id, DEFAULT_REPO_DIR_NAME]);
        defaultConfig['web_port'] = 3278;
        return defaultConfig;
    };


    this.OnServerStarting = function (callback) {
        repoPath = this.config.get('repo_path');

        startWebServer(repoPath, this.config.get('web_port'));

        async.series([
            function (cb) {
                extfs.remove(self.lockFilePath, cb);
            },
            function (cb) {
                extfs.mkdirs(self.lockFilePath, cb);
            }
        ], function (err) {
            callback(err);
        });
    };


    this.OnServerStarted = function (callback) {
        var datamgr = dibs.getServersByType('datamgr');
        if (_.isEmpty(datamgr)) {
            dibs.log.info('Repo-server is in standalone mode');
            self.standAlone = true;
        } else {
            dibs.log.info('Repo-server is not in standalone mode');
            self.standAlone = false;
        }


        // get repo types
        dibs.log.info('Initialize Repo...');
        initializeRepoTypes();

        async.series([
            function (cb) {
                // open repository
                dibs.log.info('Open Repo configuration...');
                openRepoConfig(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) {
                dibs.log.info('Connecting DFS repository...');
                self.addConnectionHandler('dfs', dfs.server);
                dfs.connect(cb);
            },
            function (cb) {
                dibs.log.info('Open repository...');
                openRepository(cb);
            }
        ],
        function (err) {
            if (err) {
                self.log.error(err);
            }

            distributions = self.repoConfig.distributions;
            var tizen = self.repoConfig.tizen;
            _.each(tizen.distOptions, function (opts) {
                if (!distributions[opts.distName]) {
                    distributions[opts.distName] = {
                        name: opts.distName,
                        type: 'tizen',
                        options: _.omit(opts, 'distName')
                    };
                }
            });

            _.each(distributions, function (dist) {
                if (!dist.options.URL) {
                    dist.options.URL = getServerURL() + '/' + DEFAULT_REPO_DIR_NAME + '/' + dist.name;
                }
            });
            callback(err);
        });
    };


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

        callback(null);
    };


    this.getRepositoryPath = function () {
        return repoPath;
    };


    this.getRepositoryURL = function (callback) {
        var repoServer = dibs.getServer(self.id);
        var thisServerId;

        if (dibs.thisServer && dibs.thisServer.id) {
            thisServerId = dibs.thisServer.id;
        }

        if (thisServerId !== repoServer.id) { //remote
            repoServer.getServerURL(function (err, serverURL) {
                if (!err) {
                    if (serverURL) {
                        return callback(null, serverURL);
                    } else {
                        return callback(null, 'http://localhost');
                    }
                } else {
                    return callback(err, null);
                }
            });
        } else { //local
            self.getServerURLInternal(callback);
        }
    };


    this.getServerURLInternal = function (callback) {
        return callback(null, getServerURL());
    };

    function getServerURL() {
        var serverURL = self.config.get('serverURL');
        if (serverURL) {
            return serverURL;
        } else {
            var webPort = self.config.get('web_port');
            var hostIp = utils.getHostIp();
            return 'http://' + hostIp + ':' + webPort;
        }
    }


    /**
     * Register local files to remote repository server
     * @function registerFiles
     * @param {Array.<string>} path - list of local file path
     * @param {Object} opts - options for registering files
     * @param {module:lib/utils.callback_error} callback - callback
     * @memberOf module:servers/repository
    */
    // LOCAL
    this.registerPackages = function (paths, opts, callback) {
        // init repo
        initializeRepoTypes();

        var rtype = opts.repoType ? opts.repoType : DEFAULT_REPO_TYPE;

        repoTypes[rtype].registerPackages(paths, opts, callback);
    };


    this.registerRemotePackagesInternal = function (rpaths, opts, callback) {
        self.log.info('Registering remote packages...');
        if (!distributions[opts.distName]) {
            return callback(new DError('REPO007', {
                distName: opts.distName
            }), null);
        }

        var rtype = getRepoType(opts.distName);

        repoTypes[rtype].registerRemotePackages(rpaths, opts, callback);
    };


    this.removePackagesInternal = function (names, opts, callback) {
        self.log.info('Removing packages...');

        if (!distributions[opts.distName]) {
            return callback(new DError('REPO007', {
                distName: opts.distName
            }), null);
        }

        var rtype = getRepoType(opts.distName);

        repoTypes[rtype].removePackages(names, opts, callback);
    };


    this.createDistributionInternal = function (name, rtype, opts, callback) {

        self.log.info('Creating new distribution...' + name);

        if (!rtype) {
            rtype = DEFAULT_REPO_TYPE;
        }

        repoTypes[rtype].createDistribution(name, opts, function (err, dist) {
            if (!err) {
                dist.type = rtype;
                addDistributionToRepoConfig(dist);
                writeRepoConfig();
                dist.options['REPO_URL'] = getServerURL() + '/' + DEFAULT_REPO_DIR_NAME + '/' + dist.name;
            }
            callback(err, dist);
        });
    };


    this.removeDistributionInternal = function (name, opts, callback) {
        self.log.info('Removing distribution... ' + name);

        if (!distributions[name]) {
            return callback(new DError('REPO007', {
                distName: name
            }), null);
        }

        var rtype = getRepoType(name);

        self.removeSyncAction(name);

        repoTypes[rtype].removeDistribution(name, opts, function (err) {
            if (!err) {
                removeDistributionToRepoConfig(name);
                writeRepoConfig();
            }
            callback(err);
        });
    };


    this.updateDistributionConfigInternal = function (name, opts, callback) {
        self.log.info('Updating distribution\'s configuration... ' + name);
        if (!distributions[name]) {
            return callback(new DError('REPO007', {
                distName: name
            }), null);
        }

        var rtype = getRepoType(name);

        repoTypes[rtype].updateDistributionConfig(name, opts, function (err) {
            if (!err) {
                var dist = {
                    name: name,
                    type: rtype,
                    options: opts
                };
                updateDistributionToRepoConfig(dist);
                writeRepoConfig();
            }
            callback(err);
        });
    };


    this.searchDistributionsInternal = function (opts, callback) {
        self.log.info('Searching distributions...');

        var rtype = opts.repoType ? opts.repoType : DEFAULT_REPO_TYPE;

        repoTypes[rtype].searchDistributions(opts, callback);
    };


    this.synchronizeDistributionInternal = function (name, opts, callback) {
        self.log.info('Synchronizing distribution... ' + name);

        if (!distributions[name]) {
            return callback(new DError('REPO007', {
                distName: name
            }), null);
        }

        if (distributionsInSync[name]) {
            return callback(new DError('REPO006', {
                distName: name
            }), null);
        } else {
            distributionsInSync[name] = true;
        }

        // WEB require quick response, so call CALLBACK befor executing
        if (opts.ResponseAtStart) {
            callback(null, null);
        }

        var rtype = getRepoType(name);

        repoTypes[rtype].synchronizeDistribution(name, opts, function (err, dist) {
            if (err) {
                self.log.info('Synchronizing distribution failed... ' + name);
                self.log.info(err.message);
            } else {
                self.log.info('Synchronizing distribution succeeded... ' + name);
            }
            delete distributionsInSync[name];
            if (!opts.ResponseAtStart) {
                return callback(err, dist);
            }
        });
    };


    this.generateSnapshotInternal = function (sname, opts, callback) {
        if (sname !== null) {
            self.log.info('Generate named snapshot... ' + sname);
        } else {
            self.log.info('Generate automatic snapshot...');
        }

        if (!distributions[opts.distName]) {
            return callback(new DError('REPO007', {
                distName: opts.distName
            }), null);
        }

        var rtype = getRepoType(opts.distName);

        repoTypes[rtype].generateSnapshot(sname, opts, callback);
    };


    this.removeSnapshotInternal = function (sname, opts, callback) {
        self.log.info('Removing snapshot... ' + sname);

        if (!distributions[opts.distName]) {
            return callback(new DError('REPO007', {
                distName: opts.distName
            }), null);
        }

        var rtype = getRepoType(opts.distName);

        repoTypes[rtype].removeSnapshot(sname, opts, function (err, files) {
            if (!err) {
                self.log.info('Removed the snapshot \'' + sname + '\' ' +
                    ((files && files.length > 0) ? 'and package files => [' + files.join(',') + ']' : ''));
            }
            callback(err, files);
        });
    };


    this.searchSnapshotsInternal = function (opts, callback) {
        self.log.info('Searching snapshots...');
        if (!distributions[opts.distName]) {
            return callback(new DError('REPO007', {
                distName: opts.distName
            }), null);
        }

        var rtype = getRepoType(opts.distName);

        repoTypes[rtype].searchSnapshots(opts, callback);
    };


    // LOCAL
    this.downloadPackage = function (name, opts, callback) {
        // init repo
        initializeRepoTypes();

        var rtype = opts.repoType ? opts.repoType : DEFAULT_REPO_TYPE;

        repoTypes[rtype].downloadPackage(name, opts, callback);
    };


    this.downloadRemotePackageInternal = function (name, opts, callback) {
        self.log.info('Downloading remote package... ' + name);
        if (!distributions[opts.distName]) {
            return callback(new DError('REPO007', {
                distName: opts.distName
            }), null);
        }

        var rtype = getRepoType(opts.distName);

        repoTypes[rtype].downloadRemotePackage(name, opts, callback);
    };


    this.addSyncAction = function (distName, action, nextTick, period) {
        var old = self.getScheduledAction(distName);
        if (old && old.period === period) {
            return;
        }

        self.log.info('Adding/Updating synchronization action... ' + distName);
        self.addScheduledAction(distName, {
            startTime: nextTick,
            period: period
        },
            function (cb) {
                // skip sync when launching server
                var master = dibs.getMasterServer();
                if (master && master.status !== 'RUNNING') {
                    cb(null); return;
                }

                self.log.info('Executing Synchronizing distribution...' + distName);
                action(function (err) {
                    if (!err) {
                        dibs.log.info('Synchronizing distribution is done...' + distName);
                    } else {
                        dibs.log.warn('Synchronizing distribution failed...' + distName);
                        dibs.log.warn(err);
                    }
                    cb(err);
                });
            });
    };


    this.removeSyncAction = function (distName) {
        self.log.info('Removing synchronization action...' + distName);
        self.removeScheduledAction(distName);
    };


    this.waitForNextSyncAction = function (distName, callback) {
        self.waitForNextScheduledAction(distName, callback);
    };


    this.createReleaseInternal = function (releaseName, distName, opts, callback) {
        self.log.info('Creating release... ' + releaseName);

        if (!distributions[distName]) {
            return callback(new DError('REPO007', {
                distName: distName
            }), null);
        }

        var rtype = getRepoType(distName);

        if (repoTypes[rtype].createRelease) {
            repoTypes[rtype].createRelease(releaseName, distName, opts, callback);
        } else {
            return callback(
                new DError('REPO008', {
                    distName: distName,
                    apiName: 'createRelease'
                }), null);
        }
    };


    this.removeReleaseInternal = function (releaseName, distName, opts, callback) {
        self.log.info('Removing release... ' + releaseName);

        if (!distributions[distName]) {
            return callback(new DError('REPO007', {
                distName: distName
            }), null);
        }

        var rtype = getRepoType(distName);

        if (repoTypes[rtype].removeRelease) {
            repoTypes[rtype].removeRelease(releaseName, distName, opts, callback);
        } else {
            return callback(
                new DError('REPO008', {
                    distName: distName,
                    apiName: 'removeRelease'
                }), null);
        }
    };


    this.updateReleaseInternal = function (release, distName, opts, callback) {
        self.log.info('Updating release... ' + release.name);

        if (!distributions[distName]) {
            return callback(new DError('REPO007', {
                distName: distName
            }), null);
        }

        var rtype = getRepoType(distName);

        if (repoTypes[rtype].updateRelease) {
            repoTypes[rtype].updateRelease(release, distName, opts, callback);
        } else {
            return callback(
                new DError('REPO008', {
                    distName: distName,
                    apiName: 'updateRelease'
                }), null);
        }
    };


    this.searchReleasesInternal = function (distName, opts, callback) {
        self.log.info('Searching releases... ' + distName);

        if (!distributions[distName]) {
            return callback(new DError('REPO007', {
                distName: distName
            }), null);
        }

        var rtype = getRepoType(distName);

        if (repoTypes[rtype].searchReleases) {
            repoTypes[rtype].searchReleases(distName, opts, callback);
        } else {
            return callback(
                new DError('REPO008', {
                    distName: distName,
                    apiName: 'searchReleases'
                }), null);
        }
    };


    this.getReleaseInternal = function (releaseName, distName, callback) {
        self.log.info('Getting release... ' + releaseName);

        if (!distributions[distName]) {
            return callback(new DError('REPO007', {
                distName: distName
            }), null);
        }

        var rtype = getRepoType(distName);

        if (repoTypes[rtype].getRelease) {
            repoTypes[rtype].getRelease(releaseName, distName, callback);
        } else {
            return callback(
                new DError('REPO008', {
                    distName: distName,
                    apiName: 'getRelease'
                }), null);
        }
    };

    //
    // PRIVATE
    //

    //Logger format for DIBS
    express.logger.format('dibs', function (tokens, req, res) {
        var len = parseInt(res.getHeader('Content-Length'), 10);

        len = isNaN(len) ? '' : len = ' - ' + len;

        return res.statusCode +
            req.ip + ' ' +
            req.method + ' ' +
            req.originalUrl + ' ' +
            res.statusCode + ' ' +
            (new Date() - req._startTime) + 'ms' + len + ' ';
    });

    var startWebServer = function (repoPath, port) {
        self.log.info('Start web server - listen: ' + port);
        app.http().io();

        app.use(express.logger({
            format: 'dibs',
            stream: dibsLogStream
        }));
        app.use('/' + DEFAULT_REPO_DIR_NAME, express.static(repoPath));
        app.use('/' + DEFAULT_REPO_DIR_NAME, express.directory(repoPath));
        app.use(express.methodOverride());
        app.use(express.cookieParser());
        app.use(express.errorHandler());
        app.use(express.bodyParser({
            limit: 1024 * 1024 * 1024 * 4
        })); // Max file size is 4G
        app.use(appCtrl.router.middleware);
        app.use('/', function (req, res) {
            res.redirect('/' + DEFAULT_REPO_DIR_NAME);
        });

        // start
        app.listen(port);
    };


    // Logger stream to DIBS
    var dibsLogStream = {
        write: function (data) {
            var status = data.slice(0, 3);
            var message = data.slice(3);

            //remove new line
            message = message.replace(/\n/g, '');

            if (status < 400) {
                self.log.info(message);
            } else if (status >= 500) {
                self.log.error(message);
            } else if (status >= 400) {
                self.log.warn(message);
            }
        }
    };


    var openRepository = function (callback) {
        async.eachSeries(Object.keys(repoTypes),
            function (repoName, cb) {
                var config = self.repoConfig[repoName];
                if (config === undefined) {
                    self.repoConfig[repoName] = {};
                    config = self.repoConfig[repoName];
                }
                repoTypes[repoName].open(config, cb);
            },
            function (err) {
                callback(err);
            });
    };


    function initializeRepoTypes() {
        if (repoTypes !== null) {
            return;
        }
        repoTypes = {};
        var exts = dibs.plugin.getExtensions('dibs.repository.type');
        for (var i = 0; i < exts.length; i++) {
            var ext = exts[i];
            var ptype = ext.type;
            repoTypes[ptype] = ext.module.createRepo(self);
        }
    }


    function getRepoType(distName) {
        var distConfig = self.repoConfig['distributions'];

        if (distConfig[distName]) {
            return distConfig[distName].type;
        } else {
            return DEFAULT_REPO_TYPE;
        }
    }


    function openRepoConfig(callback) {
        var repoConfigPath = path.join(dibs.config.getConfigDir(), self.id, REPO_CONFIG_FILE);

        fs.readFile(repoConfigPath, 'utf8', function (err, data) {
            if (err) {
                self.repoConfig.distributions = {};
                fs.writeFile(repoConfigPath, yaml.safeDump(self.repoConfig), callback);
            } else {
                self.repoConfig = yaml.safeLoad(data);
                return callback(null);
            }
        });
    }


    function writeRepoConfig() {
        var repoConfigPath = path.join(dibs.config.getConfigDir(), self.id, REPO_CONFIG_FILE);
        fs.writeFileSync(repoConfigPath, yaml.safeDump(self.repoConfig));
    }


    function getDistributionConfig() {
        var distConfig = self.repoConfig['distributions'];

        if (!distConfig) {
            self.repoConfig.distributions = {};
            distConfig = self.repoConfig['distributions'];
        }

        return distConfig;
    }


    function addDistributionToRepoConfig(dist) {
        var distConfig = getDistributionConfig();

        distConfig[dist.name] = {
            name: dist.name,
            type: dist.type,
            options: dist.options
        };
    }


    function updateDistributionToRepoConfig(dist) {
        var distConfig = getDistributionConfig();

        distConfig[dist.name] = {
            name: dist.name,
            type: dist.type,
            options: dist.options
        };
    }


    function removeDistributionToRepoConfig(distName) {
        var distConfig = getDistributionConfig();

        if (distConfig[distName]) {
            delete distConfig[distName];
        }
    }
}

/**
 * @function createServer
 * @param {string} sid - sid
 * @returns {string}
 * @memberOf module:servers/repository/server
 */


/**
 * @module servers/repository/server
 */
