'use strict';

exports = module.exports = {
    changePlan: changePlan,
    upgrade: upgrade,
    sendHeartbeat: sendHeartbeat,
    getBoxAndUserDetails: getBoxAndUserDetails,
    setPtrRecord: setPtrRecord
};

var assert = require('assert'),
    backups = require('./backups.js'),
    config = require('./config.js'),
    debug = require('debug')('box:caas'),
    locker = require('./locker.js'),
    path = require('path'),
    progress = require('./progress.js'),
    shell = require('./shell.js'),
    superagent = require('superagent'),
    util = require('util'),
    _ = require('underscore');

const RETIRE_CMD = path.join(__dirname, 'scripts/retire.sh');
var gBoxAndUserDetails = null;         // cached cloudron details like region,size...

function CaasError(reason, errorOrMessage) {
    assert.strictEqual(typeof reason, 'string');
    assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');

    Error.call(this);
    Error.captureStackTrace(this, this.constructor);

    this.name = this.constructor.name;
    this.reason = reason;
    if (typeof errorOrMessage === 'undefined') {
        this.message = reason;
    } else if (typeof errorOrMessage === 'string') {
        this.message = errorOrMessage;
    } else {
        this.message = 'Internal error';
        this.nestedError = errorOrMessage;
    }
}
util.inherits(CaasError, Error);
CaasError.BAD_FIELD = 'Field error';
CaasError.INTERNAL_ERROR = 'Internal Error';
CaasError.EXTERNAL_ERROR = 'External Error';
CaasError.BAD_STATE = 'Bad state';

var NOOP_CALLBACK = function (error) { if (error) debug(error); };

function retire(reason, info, callback) {
    assert(reason === 'migrate' || reason === 'upgrade');
    info = info || { };
    callback = callback || NOOP_CALLBACK;

    var data = {
        apiServerOrigin: config.apiServerOrigin(),
        adminFqdn: config.adminFqdn(),
        fqdn: config.fqdn()
    };
    shell.sudo('retire', [ RETIRE_CMD, reason, JSON.stringify(info), JSON.stringify(data) ], callback);
}

function doMigrate(options, callback) {
    assert.strictEqual(typeof options, 'object');
    assert.strictEqual(typeof callback, 'function');

    var error = locker.lock(locker.OP_MIGRATE);
    if (error) return callback(new CaasError(CaasError.BAD_STATE, error.message));

    function unlock(error) {
        debug('Failed to migrate', error);
        locker.unlock(locker.OP_MIGRATE);
        progress.set(progress.MIGRATE, -1, 'Backup failed: ' + error.message);
    }

    progress.set(progress.MIGRATE, 10, 'Backing up for migration');

    // initiate the migration in the background
    backups.backupBoxAndApps({ userId: null, username: 'migrator' }, function (error) {
        if (error) return unlock(error);

        debug('migrate: domain: %s size %s region %s', options.domain, options.size, options.region);

        superagent
            .post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/migrate')
            .query({ token: config.token() })
            .send(options)
            .timeout(30 * 1000)
            .end(function (error, result) {
                if (error && !error.response) return unlock(error); // network error
                if (result.statusCode === 409) return unlock(new CaasError(CaasError.BAD_STATE));
                if (result.statusCode === 404) return unlock(new CaasError(CaasError.NOT_FOUND));
                if (result.statusCode !== 202) return unlock(new CaasError(CaasError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));

                progress.set(progress.MIGRATE, 10, 'Migrating');

                retire('migrate', _.pick(options, 'domain', 'size', 'region'));
            });
    });

    callback(null);
}

function changePlan(options, callback) {
    assert.strictEqual(typeof options, 'object');
    assert.strictEqual(typeof callback, 'function');

    if (config.isDemo()) return callback(new CaasError(CaasError.BAD_FIELD, 'Not allowed in demo mode'));

    doMigrate(options, callback);
}

// this function expects a lock
function upgrade(boxUpdateInfo, callback) {
    assert(boxUpdateInfo !== null && typeof boxUpdateInfo === 'object');

    function upgradeError(e) {
        progress.set(progress.UPDATE, -1, e.message);
        callback(e);
    }

    progress.set(progress.UPDATE, 5, 'Backing up for upgrade');

    backups.backupBoxAndApps({ userId: null, username: 'upgrader' }, function (error) {
        if (error) return upgradeError(error);

        superagent.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/upgrade')
            .query({ token: config.token() })
            .send({ version: boxUpdateInfo.version })
            .timeout(30 * 1000)
            .end(function (error, result) {
                if (error && !error.response) return upgradeError(new Error('Network error making upgrade request: ' + error));
                if (result.statusCode !== 202) return upgradeError(new Error(util.format('Server not ready to upgrade. statusCode: %s body: %j', result.status, result.body)));

                progress.set(progress.UPDATE, 10, 'Updating base system');

                // no need to unlock since this is the last thing we ever do on this box
                callback();

                retire('upgrade');
            });
    });
}

function sendHeartbeat() {
    assert(config.provider() === 'caas', 'Heartbeat is only sent for managed cloudrons');

    var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/heartbeat';
    superagent.post(url).query({ token: config.token(), version: config.version() }).timeout(30 * 1000).end(function (error, result) {
        if (error && !error.response) debug('Network error sending heartbeat.', error);
        else if (result.statusCode !== 200) debug('Server responded to heartbeat with %s %s', result.statusCode, result.text);
        else debug('Heartbeat sent to %s', url);
    });
}

function getBoxAndUserDetails(callback) {
    assert.strictEqual(typeof callback, 'function');

    if (gBoxAndUserDetails) return callback(null, gBoxAndUserDetails);

    if (config.provider() !== 'caas') return callback(null, {});

    superagent
        .get(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn())
        .query({ token: config.token() })
        .timeout(30 * 1000)
        .end(function (error, result) {
            if (error && !error.response) return callback(new CaasError(CaasError.EXTERNAL_ERROR, 'Cannot reach appstore'));
            if (result.statusCode !== 200) return callback(new CaasError(CaasError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));

            gBoxAndUserDetails = result.body;

            return callback(null, gBoxAndUserDetails);
        });
}

function setPtrRecord(domain, callback) {
    assert.strictEqual(typeof domain, 'string');
    assert.strictEqual(typeof callback, 'function');

    superagent
        .post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/ptr')
        .query({ token: config.token() })
        .send({ domain: domain })
        .timeout(5 * 1000)
        .end(function (error, result) {
            if (error && !error.response) return callback(new CaasError(CaasError.EXTERNAL_ERROR, 'Cannot reach appstore'));
            if (result.statusCode !== 202) return callback(new CaasError(CaasError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));

            return callback(null);
        });
}
