'use strict';

/* global $ */
/* global angular */
/* global EventSource */
/* global async */

// keep in sync with box/src/apps.js
var ISTATES = {
    PENDING_INSTALL: 'pending_install',
    PENDING_CLONE: 'pending_clone',
    PENDING_CONFIGURE: 'pending_configure',
    PENDING_UNINSTALL: 'pending_uninstall',
    PENDING_RESTORE: 'pending_restore',
    PENDING_UPDATE: 'pending_update',
    PENDING_BACKUP: 'pending_backup',
    PENDING_RECREATE_CONTAINER: 'pending_recreate_container', // env change or addon change
    PENDING_LOCATION_CHANGE: 'pending_location_change',
    PENDING_DATA_DIR_MIGRATION: 'pending_data_dir_migration',
    PENDING_RESIZE: 'pending_resize',
    PENDING_DEBUG: 'pending_debug',
    PENDING_START: 'pending_start',
    PENDING_STOP: 'pending_stop',
    PENDING_RESTART: 'pending_restart',
    ERROR: 'error',
    INSTALLED: 'installed'
};

var HSTATES = {
    HEALTHY: 'healthy',
    UNHEALTHY: 'unhealthy',
    ERROR: 'error',
    DEAD: 'dead'
};

var RSTATES ={
    RUNNING: 'running',
    STOPPED: 'stopped'
};

var ERROR = {
    ACCESS_DENIED: 'Access Denied',
    ALREADY_EXISTS: 'Already Exists',
    BAD_FIELD: 'Bad Field',
    COLLECTD_ERROR: 'Collectd Error',
    CONFLICT: 'Conflict',
    DATABASE_ERROR: 'Database Error',
    DNS_ERROR: 'DNS Error',
    DOCKER_ERROR: 'Docker Error',
    EXTERNAL_ERROR: 'External Error',
    FS_ERROR: 'FileSystem Error',
    INTERNAL_ERROR: 'Internal Error',
    LOGROTATE_ERROR: 'Logrotate Error',
    NETWORK_ERROR: 'Network Error',
    NOT_FOUND: 'Not found',
    REVERSEPROXY_ERROR: 'ReverseProxy Error',
    TASK_ERROR: 'Task Error',
    UNKNOWN_ERROR: 'Unknown Error' // only used for portin,
};

var ROLES = {
    OWNER: 'owner',
    ADMIN: 'admin',
    USER_MANAGER: 'usermanager',
    USER: 'user'
};

// sync up with tasks.js
var TASK_TYPES = {
    TASK_APP: 'app',
    TASK_BACKUP: 'backup',
    TASK_UPDATE: 'update',
    TASK_RENEW_CERTS: 'renewcerts',
    TASK_SETUP_DNS_AND_CERT: 'setupDnsAndCert',
    TASK_CLEAN_BACKUPS: 'cleanBackups',
    TASK_SYNC_EXTERNAL_LDAP: 'syncExternalLdap',
    TASK_CHANGE_MAIL_LOCATION: 'changeMailLocation',
    TASK_SYNC_DNS_RECORDS: 'syncDnsRecords',
};

var SECRET_PLACEHOLDER = String.fromCharCode(0x25CF).repeat(8);

// ----------------------------------------------
// Shared Angular Filters
// ----------------------------------------------

// binary units (non SI) 1024 based
function prettyByteSize(size, fallback) {
    if (!size) return fallback || 0;

    var i = Math.floor(Math.log(size) / Math.log(1024));
    return (size / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i];
}

angular.module('Application').filter('prettyByteSize', function () {
    return function (size, fallback) { return prettyByteSize(size, fallback) || '0 kb'; };
});

angular.module('Application').filter('prettyDiskSize', function () {
    return function (size, fallback) { return prettyByteSize(size, fallback) || 'Not available yet'; };
});

angular.module('Application').filter('prettyDate', function () {
    // http://ejohn.org/files/pretty.js
    return function prettyDate(utc) {
        var date = new Date(utc), // this converts utc into browser timezone and not cloudron timezone!
            diff = (((new Date()).getTime() - date.getTime()) / 1000) + 30, // add 30seconds for clock skew
            day_diff = Math.floor(diff / 86400);

        if (isNaN(day_diff) || day_diff < 0)
            return 'just now';

        return day_diff === 0 && (
            diff < 60 && 'just now' ||
                diff < 120 && '1 minute ago' ||
                diff < 3600 && Math.floor( diff / 60 ) + ' minutes ago' ||
                diff < 7200 && '1 hour ago' ||
                diff < 86400 && Math.floor( diff / 3600 ) + ' hours ago') ||
            day_diff === 1 && 'Yesterday' ||
            day_diff < 7 && day_diff + ' days ago' ||
            day_diff < 31 && Math.ceil( day_diff / 7 ) + ' weeks ago' ||
            day_diff < 365 && Math.round( day_diff / 30 ) +  ' months ago' ||
                              Math.round( day_diff / 365 ) + ' years ago';
    };
});

angular.module('Application').filter('prettyLongDate', function () {
    return function prettyLongDate(utc) {
        return moment(utc).format('MMMM Do YYYY, h:mm:ss a'); // this converts utc into browser timezone and not cloudron timezone!
    };
});

angular.module('Application').filter('prettyShortDate', function () {
    return function prettyShortDate(utc) {
        return moment(utc).format('MMMM Do YYYY'); // this converts utc into browser timezone and not cloudron timezone!
    };
});

angular.module('Application').filter('markdown2html', function () {
    var converter = new showdown.Converter({
        simplifiedAutoLink: true,
        strikethrough: true,
        tables: true,
        openLinksInNewWindow: true
    });

    // without this cache, the code runs into some infinite loop (https://github.com/angular/angular.js/issues/3980)
    var cache = {};

    return function (text) {
        if (cache[text]) return cache[text];
        cache[text] = converter.makeHtml(text);
        return cache[text];
    };
});

angular.module('Application').config(['$translateProvider', function ($translateProvider) {
    $translateProvider.useStaticFilesLoader({
        prefix: 'translation/',
        suffix: '.json?' + '<%= revision %>'
    });
    $translateProvider.useLocalStorage();
    $translateProvider.preferredLanguage('en');
    $translateProvider.fallbackLanguage('en');
}]);

// Add shorthand "tr" filter to avoid having ot use "translate"
// This is a copy of the code at https://github.com/angular-translate/angular-translate/blob/master/src/filter/translate.js
// If we find out how to get that function handle somehow dynamically we can use that, otherwise the copy is required
function translateFilterFactory($parse, $translate) {
  var translateFilter = function (translationId, interpolateParams, interpolation, forceLanguage) {
    if (!angular.isObject(interpolateParams)) {
      var ctx = this || {
        '__SCOPE_IS_NOT_AVAILABLE': 'More info at https://github.com/angular/angular.js/commit/8863b9d04c722b278fa93c5d66ad1e578ad6eb1f'
        };
      interpolateParams = $parse(interpolateParams)(ctx);
    }

    return $translate.instant(translationId, interpolateParams, interpolation, forceLanguage);
  };

  if ($translate.statefulFilter()) {
    translateFilter.$stateful = true;
  }

  return translateFilter;
}
translateFilterFactory.displayName = 'translateFilterFactory';
angular.module('Application').filter('tr', translateFilterFactory);


// ----------------------------------------------
// Cloudron REST API wrapper
// ----------------------------------------------

angular.module('Application').service('Client', ['$http', '$interval', '$timeout', 'md5', 'Notification', function ($http, $interval, $timeout, md5, Notification) {
    var client = null;

    // variable available only here to avoid this._property pattern
    var token = null;

    function ClientError(statusCode, messageOrObject) {
        Error.call(this);
        this.name = this.constructor.name;
        this.statusCode = statusCode;
        if (messageOrObject === null || typeof messageOrObject === 'undefined') {
            this.message = 'Empty message or object';
        } else if (typeof messageOrObject === 'string') {
            this.message = messageOrObject;
        } else if (messageOrObject) {
            angular.extend(this, messageOrObject); // status, message, reason and other properties
        }
    }

    function defaultErrorHandler(callback) {
        function handleServerOffline() {
            if (client.offline) return;
            client.offline = true;

            (function onlineCheck() {
                $http.get(client.apiOrigin + '/api/v1/cloudron/status', {}).success(function (data, status) {
                    client.offline = false;
                    client._reconnectListener.forEach(function (handler) { handler(); });
                }).error(function (data, status) {
                    $timeout(onlineCheck, 5000);
                });
            })();
        }

        return function (data, status) {
            // handle request killed by browser (eg. cors issue)
            if (data === null && status === -1) {
                handleServerOffline();
                return callback(new ClientError('Request cancelled by browser'));
            }

            // re-login will make the code get a new token
            if (status === 401) return client.login();

            if (status === 500 || status === 501) {
                if (!client.offline) client.error(data);
            } else if (status === 502 || status === 503 || status === 504) {
                // This means the box service is not reachable. We just show offline banner for now
            }

            if (status >= 500) {
                handleServerOffline();
                return callback(new ClientError(status, data));
            }

            var obj = data;
            try {
                obj = JSON.parse(data);
            } catch (e) {}

            callback(new ClientError(status, obj));
        };
    }

    function defaultSuccessHandler(callback) {
        return function (data, status) {
            return callback(null, data, status);
        };
    }

    // XHR wrapper to set the auth header
    function get(url, config, callback) {
        if (arguments.length !== 3) {
            console.error('GET', arguments);
            throw('Wrong number of arguments');
        }

        config = config || {};
        config.headers = config.headers || {};
        config.headers.Authorization = 'Bearer ' + token;

        return $http.get(client.apiOrigin + url, config)
            .success(defaultSuccessHandler(callback))
            .error(defaultErrorHandler(callback));
    }

    function head(url, config, callback) {
        if (arguments.length !== 3) {
            console.error('HEAD', arguments);
            throw('Wrong number of arguments');
        }

        config = config || {};
        config.headers = config.headers || {};
        config.headers.Authorization = 'Bearer ' + token;

        return $http.head(client.apiOrigin + url, config)
            .success(defaultSuccessHandler(callback))
            .error(defaultErrorHandler(callback));
    }

    function post(url, data, config, callback) {
        if (arguments.length !== 4) {
            console.error('POST', arguments);
            throw('Wrong number of arguments');
        }

        data = data || {};
        config = config || {};
        config.headers = config.headers || {};
        config.headers.Authorization = 'Bearer ' + token;

        return $http.post(client.apiOrigin + url, data, config)
            .success(defaultSuccessHandler(callback))
            .error(defaultErrorHandler(callback));
    }

    function put(url, data, config, callback) {
        if (arguments.length !== 4) {
            console.error('PUT', arguments);
            throw('Wrong number of arguments');
        }

        data = data || {};
        config = config || {};
        config.headers = config.headers || {};
        config.headers.Authorization = 'Bearer ' + token;

        return $http.put(client.apiOrigin + url, data, config)
            .success(defaultSuccessHandler(callback))
            .error(defaultErrorHandler(callback));
    }

    function del(url, config, callback) {
        if (arguments.length !== 3) {
            console.error('DEL', arguments);
            throw('Wrong number of arguments');
        }

        config = config || {};
        config.headers = config.headers || {};
        config.headers.Authorization = 'Bearer ' + token;

        return $http.delete(client.apiOrigin + url, config)
            .success(defaultSuccessHandler(callback))
            .error(defaultErrorHandler(callback));
    }

    function Client() {
        this.offline = false;
        this._ready = false;
        this._configListener = [];
        this._readyListener = [];
        this._reconnectListener = [];
        this._userInfo = {
            id: null,
            username: null,
            email: null,
            twoFactorAuthenticationEnabled: false,
            source: null,
            avatarUrl: null
        };
        this._config = {
            apiServerOrigin: null,
            webServerOrigin: null,
            fqdn: null,
            ip: null,
            revision: null,
            update: { box: null, apps: null },
            progress: {},
            region: null,
            size: null
        };
        this._installedApps = [];
        this._installedAppsById = {};
        this._appTags = [];
        // window.location fallback for websocket connections which do not have relative uris
        this.apiOrigin = '<%= apiOrigin %>' || window.location.origin;
        this.avatar = '';
        this._availableLanguages = ['en'];
        this._appstoreAppCache = [];

        this.resetAvatar();

        this.setToken(localStorage.token);
    }

    Client.prototype.error = function (error, action) {
        var message = '';

        console.error(error);

        if (typeof error === 'object') {
            message = error.message || error;
        } else {
            message = error;
        }

        // give more info in case the error was a request which failed with empty response body,
        // this happens mostly if the box crashes
        if (message === 'Empty message or object') {
            message = 'Got empty response. Click to check the server logs.';
            action = action || '/logs.html?id=box';
        }

        this.notify('Cloudron Error', message, true, 'error', action);
    };

    // handles application startup errors, mostly only when dashboard is loaded and api endpoint is down
    Client.prototype.initError = function (error, initFunction) {
        console.error('Application startup error', error);

        $timeout(initFunction, 5000); // we will try to re-init the app
    };

    Client.prototype.clearNotifications = function () {
        Notification.clearAll();
    };

    /*

    If `action` is a non-empty string, it will be treated as a url, if it is a function, that function will be exectued on click

    */
    Client.prototype.notify = function (title, message, persistent, type, action) {
        var options = { title: title, message: message};

        if (persistent) options.delay = 'never'; // any non Number means never timeout

        if (action) {
            options.onClick = function (/* params */) {
                // if action is a string, we assume it is a link
                if (typeof action === 'string' && action !== '') window.location = action;
                else if (typeof action === 'function') action();
                else console.warn('Notification action is not supported.', action);
            };
        }

        if (type === 'error') Notification.error(options);
        else if (type === 'success') Notification.success(options);
        else if (type === 'info') Notification.info(options);
        else if (type === 'warning') Notification.warning(options);
        else throw('Invalid notification type "' + type + '"');
    };

    Client.prototype.setReady = function () {
        if (this._ready) return;

        this._ready = true;
        this._readyListener.forEach(function (callback) {
            callback();
        });

        // clear the listeners, we only callback once!
        this._readyListener = [];
    };

    Client.prototype.onReady = function (callback) {
        if (this._ready) callback();
        else this._readyListener.push(callback);
    };

    Client.prototype.onConfig = function (callback) {
        this._configListener.push(callback);
        if (this._config && this._config.apiServerOrigin) callback(this._config);
    };

    Client.prototype.onReconnect = function (callback) {
        if (this._ready) callback();
        else this._reconnectListener.push(callback);
    };

    Client.prototype.resetAvatar = function () {
        this.avatar = this.apiOrigin + '/api/v1/cloudron/avatar?' + String(Math.random()).slice(2);

        var favicon = $('#favicon');
        if (favicon) favicon.attr('href', this.avatar);
    };

    Client.prototype.setUserInfo = function (userInfo) {
        // In order to keep the angular bindings alive, set each property individually
        this._userInfo.id = userInfo.id;
        this._userInfo.username = userInfo.username;
        this._userInfo.email = userInfo.email;
        this._userInfo.fallbackEmail = userInfo.fallbackEmail;
        this._userInfo.displayName = userInfo.displayName;
        this._userInfo.twoFactorAuthenticationEnabled = userInfo.twoFactorAuthenticationEnabled;
        this._userInfo.role = userInfo.role;
        this._userInfo.source = userInfo.source;
        this._userInfo.avatarUrl = userInfo.avatarUrl + '?s=128&default=mp&ts=' + Date.now(); // we add the timestamp to avoid caching
        this._userInfo.isAtLeastOwner = [ ROLES.OWNER ].indexOf(userInfo.role) !== -1;
        this._userInfo.isAtLeastAdmin = [ ROLES.OWNER, ROLES.ADMIN ].indexOf(userInfo.role) !== -1;
        this._userInfo.isAtLeastUserManager = [ ROLES.OWNER, ROLES.ADMIN, ROLES.USER_MANAGER ].indexOf(userInfo.role) !== -1;
    };

    Client.prototype.setConfig = function (config) {
        var that = this;

        angular.copy(config, this._config);

<% if (appstore.webOrigin) { -%>
        this._config.webServerOrigin = '<%= appstore.webOrigin %>';
<% } -%>

<% if (appstore.ApiOrigin) { -%>
        this._config.apiServerOrigin = '<%= appstore.apiOrigin %>';
<% } -%>

        // => This is just for easier testing
        // this._config.features.userMaxCount = 5;
        // this._config.features.userRoles = false;
        // this._config.features.userGroups = false;
        // this._config.features.domainMaxCount = 1;
        // this._config.features.externalLdap = false;
        // this._config.features.privateDockerRegistry = false;
        // this._config.features.branding = true;
        // this._config.features.support = true;
        // this._config.features.directoryConfig = true;
        // this._config.features.mailboxMaxCount = 5;
        // this._config.features.emailPremium = false;

        this._configListener.forEach(function (callback) {
            callback(that._config);
        });
    };

    Client.prototype.getInstalledApps = function () {
        return this._installedApps;
    };

    Client.prototype.getAppTags = function () {
        return this._appTags;
    };

    Client.prototype.getUserInfo = function () {
        return this._userInfo;
    };

    Client.prototype.getConfig = function () {
        return this._config;
    };

    Client.prototype.getAvailableLanguages = function () {
        return this._availableLanguages;
    };

    Client.prototype.setToken = function (accessToken) {
        if (!accessToken) localStorage.removeItem('token');
        else localStorage.token = accessToken;

        // set the token closure
        token = accessToken;
    };

    Client.prototype.getToken = function () {
        return token;
    };

    Client.prototype.makeURL = function (url) {
        if (url.indexOf('?') === -1) {
            return this.apiOrigin + url + '?access_token=' + token;
        } else {
            return this.apiOrigin + url + '&access_token=' + token;
        }
    };

    /*
     * Rest API wrappers
     */
    Client.prototype.config = function (callback) {
        get('/api/v1/config', null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200 || typeof data !== 'object') return callback(new ClientError(status, data));
            callback(null, data);
        });
    };

    Client.prototype.userInfo = function (callback) {
        get('/api/v1/profile', null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200 || typeof data !== 'object') return callback(new ClientError(status, data));
            callback(null, data);
        });
    };

    Client.prototype.changeCloudronAvatar = function (avatarFile, callback) {
        var fd = new FormData();
        fd.append('avatar', avatarFile);

        var config = {
            headers: { 'Content-Type': undefined },
            transformRequest: angular.identity
        };

        post('/api/v1/branding/cloudron_avatar', fd, config, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 202) return callback(new ClientError(status, data));
            callback(null);
        });
    };

    Client.prototype.changeCloudronName = function (name, callback) {
        var data = {
            name: name
        };

        post('/api/v1/branding/cloudron_name', data, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 202) return callback(new ClientError(status, data));
            callback(null);
        });
    };

    Client.prototype.installApp = function (id, manifest, title, config, callback) {
        var that = this;
        var data = {
            appStoreId: id + '@' + manifest.version,
            location: config.location,
            domain: config.domain,
            portBindings: config.portBindings,
            accessRestriction: config.accessRestriction,
            cert: config.cert,
            key: config.key,
            sso: config.sso,
            overwriteDns: config.overwriteDns
        };

        post('/api/v1/apps/install', data, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 202) return callback(new ClientError(status, data));

            callback(null, data.id);
        });
    };

    Client.prototype.cloneApp = function (appId, config, callback) {
        var data = {
            location: config.location,
            domain: config.domain,
            portBindings: config.portBindings,
            backupId: config.backupId
        };

        post('/api/v1/apps/' + appId + '/clone', data, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 201) return callback(new ClientError(status, data));

            callback(null, data);
        });
    };

    Client.prototype.restoreApp = function (appId, backupId, callback) {
        var data = { backupId: backupId };

        post('/api/v1/apps/' + appId + '/restore', data, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 202) return callback(new ClientError(status, data));

            callback(null);
        });
    };

    Client.prototype.backupApp = function (appId, callback) {
        var data = {};

        post('/api/v1/apps/' + appId + '/backup', data, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 202) return callback(new ClientError(status, data));

            callback(null);
        });
    };

    Client.prototype.uninstallApp = function (appId, callback) {
        var data = {};

        post('/api/v1/apps/' + appId + '/uninstall', data, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 202) return callback(new ClientError(status, data));

            callback(null);
        });
    };

    Client.prototype.configureApp = function (id, setting, data, callback) {
        post('/api/v1/apps/' + id + '/configure/' + setting, data, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200 && status !== 202) return callback(new ClientError(status, data));

            callback(null, data);
        });
    };

    Client.prototype.repairApp = function (id, data, callback) {
        post('/api/v1/apps/' + id + '/repair', data, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200 && status !== 202) return callback(new ClientError(status, data));

            callback(null, data);
        });
    };

    Client.prototype.updateApp = function (id, manifest, options, callback) {
        var data =  {
            appStoreId: manifest.id + '@' + manifest.version,
            skipBackup: !!options.skipBackup
        };

        post('/api/v1/apps/' + id + '/update', data, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 202) return callback(new ClientError(status, data));

            callback(null, data);
        });
    };

    Client.prototype.startApp = function (id, callback) {
        post('/api/v1/apps/' + id + '/start', {}, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 202) return callback(new ClientError(status, data));

            callback(null, data);
        });
    };

    Client.prototype.stopApp = function (id, callback) {
        post('/api/v1/apps/' + id + '/stop', {}, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 202) return callback(new ClientError(status, data));

            callback(null, data);
        });
    };

    Client.prototype.restartApp = function (id, callback) {
        post('/api/v1/apps/' + id + '/restart', {}, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 202) return callback(new ClientError(status, data));

            callback(null, data);
        });
    };

    Client.prototype.debugApp = function (id, state, callback) {
        var data = {
            debugMode: state ? {
                readonlyRootfs: true,
                cmd: [ '/bin/bash', '-c', 'echo "Repair mode. Use the webterminal or cloudron exec to repair. Sleeping" && sleep infinity' ]
            } : null
        };

        post('/api/v1/apps/' + id + '/configure/debug_mode', data, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 202) return callback(new ClientError(status, data));

            callback(null);
        });
    };

    Client.prototype.version = function (callback) {
        get('/api/v1/cloudron/status', null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));
            callback(null, data);
        });
    };

    Client.prototype.getStatus = function (callback) {
        get('/api/v1/cloudron/status', null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200 || typeof data !== 'object') return callback(new ClientError(status, data));
            callback(null, data);
        });
    };

    Client.prototype.setBackupConfig = function (backupConfig, callback) {
        post('/api/v1/settings/backup_config', backupConfig, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null);
        });
    };

    Client.prototype.getBackupConfig = function (callback) {
        get('/api/v1/settings/backup_config', null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null, data);
        });
    };

    Client.prototype.checkBackupConfig = function (callback) {
        get('/api/v1/backups/check', null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null, data);
        });
    };

    Client.prototype.getSupportConfig = function (callback) {
        get('/api/v1/settings/support_config', null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null, data);
        });
    };

    Client.prototype.setExternalLdapConfig = function (config, callback) {
        post('/api/v1/settings/external_ldap_config', config, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null);
        });
    };

    Client.prototype.getExternalLdapConfig = function (callback) {
        get('/api/v1/settings/external_ldap_config', null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null, data);
        });
    };

    Client.prototype.setDirectoryConfig = function (config, callback) {
        post('/api/v1/settings/directory_config', config, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null);
        });
    };

    Client.prototype.getDirectoryConfig = function (callback) {
        get('/api/v1/settings/directory_config', null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null, data);
        });
    };

    // network
    Client.prototype.setSysinfoConfig = function (config, callback) {
        post('/api/v1/settings/sysinfo_config', config, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null);
        });
    };

    Client.prototype.getServerIp = function (callback) {
        get('/api/v1/cloudron/server_ip', null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null, data.ip);
        });
    };

    Client.prototype.getSysinfoConfig = function (callback) {
        get('/api/v1/settings/sysinfo_config', null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null, data);
        });
    };

    Client.prototype.getBlocklist = function (callback) {
        var config = {};

        get('/api/v1/network/blocklist', config, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));
            callback(null, data.blocklist);
        });
    };

    Client.prototype.setBlocklist = function (blocklist, callback) {
        post('/api/v1/network/blocklist', { blocklist: blocklist }, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null);
        });
    };

    Client.prototype.setDynamicDnsConfig = function (enabled, callback) {
        post('/api/v1/settings/dynamic_dns', { enabled: enabled }, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));
            callback(null);
        });
    };

    Client.prototype.getDynamicDnsConfig = function (callback) {
        get('/api/v1/settings/dynamic_dns', null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null, data.enabled);
        });
    };

    // branding
    Client.prototype.setFooter = function (footer, callback) {
        post('/api/v1/branding/footer', { footer: footer }, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null);
        });
    };

    Client.prototype.getFooter = function (callback) {
        get('/api/v1/branding/footer', null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null, data.footer);
        });
    };

    Client.prototype.setUnstableAppsConfig = function (enabled, callback) {
        post('/api/v1/settings/unstable_apps', { enabled: enabled }, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));
            callback(null);
        });
    };

    Client.prototype.getUnstableAppsConfig = function (callback) {
        get('/api/v1/settings/unstable_apps', null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));
            callback(null, data.enabled);
        });
    };

    Client.prototype.setRegistryConfig = function (rc, callback) {
        post('/api/v1/settings/registry_config', rc, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));
            callback(null);
        });
    };

    Client.prototype.getRegistryConfig = function (callback) {
        get('/api/v1/settings/registry_config', null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));
            callback(null, data);
        });
    };

    Client.prototype.getUpdateInfo = function (callback) {
        if (!this._userInfo.isAtLeastAdmin) return callback(new Error('Not allowed'));

        get('/api/v1/cloudron/update', null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null, data);
        });
    };

    Client.prototype.checkForUpdates = function (callback) {
        post('/api/v1/cloudron/check_for_updates', {}, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            client.refreshConfig(callback);
        });
    };

    Client.prototype.setAutoupdatePattern = function (pattern, callback) {
        post('/api/v1/settings/autoupdate_pattern', { pattern: pattern }, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null);
        });
    };

    Client.prototype.getAutoupdatePattern = function (callback) {
        get('/api/v1/settings/autoupdate_pattern', null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null, data);
        });
    };

    Client.prototype.setTimeZone = function (timeZone, callback) {
        post('/api/v1/settings/time_zone', { timeZone: timeZone }, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null);
        });
    };

    Client.prototype.getTimeZone = function (callback) {
        get('/api/v1/settings/time_zone', null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null, data.timeZone);
        });
    };

    Client.prototype.setLanguage = function (language, callback) {
        post('/api/v1/settings/language', { language: language }, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null);
        });
    };

    Client.prototype.getLanguage = function (callback) {
        get('/api/v1/settings/language', null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null, data.language);
        });
    };

    Client.prototype.getRemoteSupport = function (callback) {
        get('/api/v1/support/remote_support', null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null, data.enabled);
        });
    };

    Client.prototype.enableRemoteSupport = function (enable, callback) {
        post('/api/v1/support/remote_support', { enable: enable }, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 202) return callback(new ClientError(status, data));

            callback(null);
        });
    };

    Client.prototype.getBackups = function (callback) {
        get('/api/v1/backups', null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null, data.backups);
        });
    };

    Client.prototype.getLatestTaskByType = function (type, callback) {
        get('/api/v1/tasks?page=1&per_page=1&type=' + type, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null, data.tasks.length ? data.tasks[0] : null);
        });
    };

    Client.prototype.getTask = function (taskId, callback) {
        get('/api/v1/tasks/' + taskId, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null, data);
        });
    };

    Client.prototype.getTaskLogs = function (taskId, follow, lines, callback) {
        if (follow) {
            var eventSource = new EventSource(client.apiOrigin + '/api/v1/tasks/' + taskId + '/logstream?lines=' + lines + '&access_token=' + token);
            callback(null, eventSource);
        } else {
            get('/api/v1/services/' + taskId + '/logs?lines=' + lines, null, function (error, data, status) {
                if (error) return callback(error);
                if (status !== 200) return callback(new ClientError(status, data));

                callback(null, data);
            });
        }
    };

    Client.prototype.startBackup = function (callback) {
        post('/api/v1/backups/create', {}, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 202) return callback(new ClientError(status, data));

            callback(null, data.taskId);
        });
    };

    Client.prototype.cleanupBackups = function (callback) {
        post('/api/v1/backups/cleanup', {}, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 202) return callback(new ClientError(status, data));

            callback(null, data.taskId);
        });
    };

    Client.prototype.stopTask = function (taskId, callback) {
        post('/api/v1/tasks/' + taskId + '/stop', {}, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 204) return callback(new ClientError(status, data));

            callback(null, data);
        });
    };

    Client.prototype.restore = function (backupConfig, backupId, version, sysinfoConfig, skipDnsSetup, callback) {
        var data = {
            backupConfig: backupConfig,
            backupId: backupId,
            version: version,
            sysinfoConfig: sysinfoConfig,
            skipDnsSetup: skipDnsSetup
        };

        post('/api/v1/cloudron/restore', data, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status));

            callback(null);
        });
    };

    Client.prototype.importBackup = function (appId, backupId, backupFormat, backupConfig, callback) {
        var data = {
            backupId: backupId,
            backupFormat: backupFormat,
            backupConfig: backupConfig,
        };

        post('/api/v1/apps/' + appId + '/import', data, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 202) return callback(new ClientError(status));

            callback(null);
        });
    };

    Client.prototype.getNotifications = function (acknowledged, page, perPage, callback) {
        var config = {
            params: {
                page: page,
                per_page: perPage
            }
        };

        if (typeof acknowledged === 'boolean') config.params.acknowledged = acknowledged;

        get('/api/v1/notifications', config, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null, data.notifications);
        });
    };

    Client.prototype.ackNotification = function (notificationId, callback) {
        post('/api/v1/notifications/' + notificationId, {}, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 204) return callback(new ClientError(status));

            callback(null);
        });
    };

    Client.prototype.getEvent = function (eventId, callback) {
        get('/api/v1/cloudron/eventlog/' + eventId, {}, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null, data.event);
        });
    };

    Client.prototype.getEventLogs = function (actions, search, page, perPage, callback) {
        var config = {
            params: {
                actions: actions,
                search: search,
                page: page,
                per_page: perPage
            }
        };

        get('/api/v1/cloudron/eventlog', config, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null, data.eventlogs);
        });
    };

    Client.prototype.getPlatformLogs = function (unit, follow, lines, callback) {
        if (follow) {
            var eventSource = new EventSource(client.apiOrigin + '/api/v1/cloudron/logstream/' + unit + '?lines=' + lines + '&access_token=' + token);
            callback(null, eventSource);
        } else {
            get('/api/v1/cloudron/logs/' + unit + '?lines=' + lines, null, function (error, data, status) {
                if (error) return callback(error);
                if (status !== 200) return callback(new ClientError(status, data));

                callback(null, data);
            });
        }
    };

    Client.prototype.getServiceLogs = function (serviceName, follow, lines, callback) {
        if (follow) {
            var eventSource = new EventSource(client.apiOrigin + '/api/v1/services/' + serviceName + '/logstream?lines=' + lines + '&access_token=' + token);
            callback(null, eventSource);
        } else {
            get('/api/v1/services/' + serviceName + '/logs?lines=' + lines, null, function (error, data, status) {
                if (error) return callback(error);
                if (status !== 200) return callback(new ClientError(status, data));

                callback(null, data);
            });
        }
    };

    Client.prototype.getApps = function (callback) {
        var that = this;

        get('/api/v1/apps', null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            var apps = data.apps;
            for (var i = 0; i < apps.length; i++) {
                that._appPostProcess(apps[i]); // this will also set the correct iconUrl
            }

            callback(null, apps);
        });
    };

    Client.prototype.getAppLogs = function (appId, follow, lines, callback) {
        if (follow) {
            var eventSource = new EventSource(client.apiOrigin + '/api/v1/apps/' + appId + '/logstream?lines=' + lines + '&access_token=' + token);
            callback(null, eventSource);
        } else {
            get('/api/v1/apps/' + appId + '/logs', null, function (error, data, status) {
                if (error) return callback(error);
                if (status !== 200) return callback(new ClientError(status, data));

                callback(null, data);
            });
        }
    };

    Client.prototype.getAppBackups = function (appId, callback) {
        get('/api/v1/apps/' + appId + '/backups', null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null, data.backups);
        });
    };

    Client.prototype.getServices = function (callback) {
        get('/api/v1/services', null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null, data.services);
        });
    };

    Client.prototype.getService = function (serviceName, callback) {
        get('/api/v1/services/' + serviceName, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null, data.service);
        });
    };

    Client.prototype.configureService = function (serviceName, data, callback) {
        post('/api/v1/services/' + serviceName, data, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 202) return callback(new ClientError(status, data));

            callback(null);
        });
    };

    Client.prototype.restartService = function (serviceName, callback) {
        post('/api/v1/services/' + serviceName + '/restart', {}, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 202) return callback(new ClientError(status, data));

            callback(null);
        });
    };

    Client.prototype.rebuildService = function (serviceName, callback) {
        post('/api/v1/services/' + serviceName + '/rebuild', {}, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 202) return callback(new ClientError(status, data));

            callback(null);
        });
    };

    Client.prototype.getUsers = function (search, page, perPage, callback) {
        if (typeof search === 'function') {
            callback = search;
            search = '';
            page = 1;
            perPage = 5000;
        }

        var config = {
            params: {
                page: page,
                per_page: perPage
            }
        };

        if (search) config.params.search = search;

        get('/api/v1/users', config, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null, data.users);
        });
    };

    Client.prototype.getUser = function (userId, callback) {
        get('/api/v1/users/' + userId, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null, data);
        });
    };

    Client.prototype.getGroups = function (callback) {
        get('/api/v1/groups', null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null, data.groups);
        });
    };

    Client.prototype.setGroups = function (userId, groupIds, callback) {
        put('/api/v1/users/' + userId + '/groups', { groupIds: groupIds }, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 204) return callback(new ClientError(status, data));

            callback(null);
        });
    };

    Client.prototype.getGroup = function (groupId, callback) {
        get('/api/v1/groups/' + groupId, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null, data);
        });
    };

    Client.prototype.createGroup = function (name, callback) {
        var data = {
            name: name
        };

        post('/api/v1/groups', data, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 201) return callback(new ClientError(status, data));

            callback(null, data);
        });
    };

    Client.prototype.updateGroup = function (id, name, callback) {
        var data = {
            name: name
        };

        post('/api/v1/groups/' + id, data, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null, data);
        });
    };

    Client.prototype.setGroupMembers = function (id, userIds, callback) {
        var data = {
            userIds: userIds

        };

        put('/api/v1/groups/' + id + '/members', data, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null, data);
        });
    };

    Client.prototype.removeGroup = function (groupId, callback) {
        var config = {
            data: {},
            headers: {
                'Content-Type': 'application/json'
            }
        };

        del('/api/v1/groups/' + groupId, config, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 204) return callback(new ClientError(status, data));

            callback(null, data);
        });
    };

    Client.prototype.getApp = function (appId, callback) {
        var that = this;

        get('/api/v1/apps/' + appId, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            that._appPostProcess(data);

            callback(null, data);
        });
    };

    Client.prototype.getAppWithTask = function (appId, callback) {
        var that = this;

        this.getApp(appId, function (error, app) {
            if (error) return callback(error);

            if (!app.taskId) return callback(null, app);

            that.getTask(app.taskId, function (error, task) {
                if (error) return callback(error);

                if (task) {
                    app.progress = task.percent;
                    app.message = task.message;
                    app.taskMinutesActive = moment.duration(moment.utc().diff(moment.utc(task.creationTime))).asMinutes();
                } else {
                    app.progress = 0;
                    app.message = '';
                    app.taskMinutesActive = 0;
                }

                callback(null, app);
            });
        });
    };

    Client.prototype.getCachedAppSync = function (appId) {
        var appFound = null;
        this._installedApps.some(function (app) {
            if (app.id === appId) {
                appFound = app;
                return true;
            } else {
                return false;
            }
        });

        return appFound;
    };

    Client.prototype.createInvite = function (userId, callback) {
        post('/api/v1/users/' + userId + '/create_invite', {}, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null, data);
        });
    };

    Client.prototype.sendInvite = function (userId, callback) {
        post('/api/v1/users/' + userId + '/send_invite', {}, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null);
        });
    };

    Client.prototype.setup = function (data, callback) {
        post('/api/v1/cloudron/setup', data, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null);
        });
    };

    Client.prototype.createAdmin = function (data, callback) {
        var that = this;

        post('/api/v1/cloudron/activate', data, null, function (error, result, status) {
            if (error) return callback(error);
            if (status !== 201) return callback(new ClientError(status, result));

            that.setToken(result.token);
            that.setUserInfo({ username: data.username, email: data.email, admin: true, twoFactorAuthenticationEnabled: false, source: '', avatarUrl: null });

            callback(null, result.activated);
        });
    };

    Client.prototype.getTokens = function (callback) {
        get('/api/v1/tokens/', null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null, data.tokens);
        });
    };

    Client.prototype.createToken = function (name, callback) {
        var data = {
            name: name
        };

        post('/api/v1/tokens', data, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 201) return callback(new ClientError(status, data));

            callback(null, data.token);
        });
    };

    // FIXME clashes with existing getToken()
    // Client.prototype.getToken = function (id, callback) {
    //     get('/api/v1/tokens/' + id, null, function (error, data, status) {
    //         if (error) return callback(error);
    //         if (status !== 200) return callback(new ClientError(status, data));

    //         callback(null, data.token);
    //     });
    // };

    Client.prototype.delToken = function (tokenId, callback) {
        del('/api/v1/tokens/' + tokenId, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 204) return callback(new ClientError(status, data));

            callback(null);
        });
    };

    Client.prototype.addAppPassword = function (identifier, name, callback) {
        var data = {
            identifier: identifier,
            name: name
        };

        post('/api/v1/app_passwords', data, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 201) return callback(new ClientError(status, data));

            callback(null, data);
        });
    };

    Client.prototype.getAppPasswords = function (callback) {
        get('/api/v1/app_passwords', null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null, data);
        });
    };

    Client.prototype.delAppPassword = function (id, callback) {
        del('/api/v1/app_passwords/' + id, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 204) return callback(new ClientError(status, data));

            callback(null);
        });
    };

    Client.prototype.update = function (options, callback) {
        var data = {
            skipBackup: !!options.skipBackup
        };

        post('/api/v1/cloudron/update', data, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 202) return callback(new ClientError(status, data));

            callback(null, data.taskId);
        });
    };

    Client.prototype.isRebootRequired = function (callback) {
        get('/api/v1/cloudron/reboot', null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null, data.rebootRequired);
        });
    };

    Client.prototype.reboot = function (callback) {
        post('/api/v1/cloudron/reboot', {}, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 202) return callback(new ClientError(status, data));

            callback(null, data);
        });
    };

    Client.prototype.setCertificate = function (certificateFile, keyFile, callback) {
        var data = {
            cert: certificateFile,
            key: keyFile
        };

        post('/api/v1/settings/certificate', data, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 202) return callback(new ClientError(status, data));

            callback(null);
        });
    };

    Client.prototype.disks = function (callback) {
        get('/api/v1/cloudron/disks', null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null, data);
        });
    };

    Client.prototype.memory = function (callback) {
        get('/api/v1/cloudron/memory', null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null, data);
        });
    };

    Client.prototype.graphs = function (targets, from, options, callback) {
        // if we have a lot of apps, targets can be very large. node will just disconnect since it exceeds header size
        var size = 10, chunks = [];
        for (var i = 0; i < targets.length; i += size) {
            chunks.push(targets.slice(i, i+size));
        }

        var result = [];
        async.eachSeries(chunks, function (chunk, iteratorCallback) {
            var config = {
                params: {
                    target: chunk,
                    format: 'json',
                    from: from,
                    until: 'now'
                }
            };

            if (options.noNullPoints) config.params.noNullPoints = true;

            get('/api/v1/cloudron/graphs', config, function (error, data, status) {
                if (error) return iteratorCallback(error);
                if (status !== 200) return iteratorCallback(new ClientError(status, data));

                // the datapoint returned here is an [value, timestamp]
                result = result.concat(data);
                iteratorCallback(null);
            });
        }, function iteratorDone(error) {
            callback(error, result);
        });
    };

    Client.prototype.createTicket = function (ticket, callback) {
        // just to be eplicit here
        var data = {
            enableSshSupport: !!ticket.enableSshSupport,
            type: ticket.type,
            subject: ticket.subject,
            description: ticket.description,
            appId: ticket.appId || undefined,
            altEmail: ticket.altEmail || undefined
        };

        post('/api/v1/support/ticket', data, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 201) return callback(new ClientError(status, data));

            callback(null, data);
        });
    };

    Client.prototype.createUser = function (user, callback) {
        var data = {
            email: user.email,
            displayName: user.displayName,
            role: user.role
        };

        if (user.username !== null) data.username = user.username;

        post('/api/v1/users', data, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 201) return callback(new ClientError(status, data));

            callback(null, data);
        });
    };

    Client.prototype.updateUser = function (user, callback) {
        var data = {
            email: user.email,
            displayName: user.displayName,
            fallbackEmail: user.fallbackEmail,
            active: user.active,
            role: user.role
        };

        post('/api/v1/users/' + user.id, data, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 204) return callback(new ClientError(status, data));

            callback(null);
        });
    };

    Client.prototype.changeOwnership = function (userId, callback) {
        post('/api/v1/users/' + userId + '/make_owner', {}, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 204) return callback(new ClientError(status, data));

            callback(null);
        });
    };

    Client.prototype.removeUser = function (userId, callback) {
        var config = {
            data: {},
            headers: {
                'Content-Type': 'application/json'
            }
        };

        del('/api/v1/users/' + userId, config, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 204) return callback(new ClientError(status, data));

            callback(null);
        });
    };

    Client.prototype.updateProfile = function (data, callback) {
        post('/api/v1/profile', data, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 204) return callback(new ClientError(status, data));

            callback(null, data);
        });
    };

    Client.prototype.clearAvatar = function (callback) {
        del('/api/v1/profile/avatar', {}, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 202) return callback(new ClientError(status, data));
            callback(null);
        });
    };

    Client.prototype.changeAvatar = function (avatarFile, callback) {
        var fd = new FormData();
        fd.append('avatar', avatarFile);

        var config = {
            headers: { 'Content-Type': undefined },
            transformRequest: angular.identity
        };

        post('/api/v1/profile/avatar', fd, config, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 202) return callback(new ClientError(status, data));
            callback(null);
        });
    };

    Client.prototype.changePassword = function (currentPassword, newPassword, callback) {
        var data = {
            password: currentPassword,
            newPassword: newPassword
        };

        post('/api/v1/profile/password', data, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 204) return callback(new ClientError(status, data));

            callback(null, data);
        });
    };

    Client.prototype.setTwoFactorAuthenticationSecret = function (callback) {
        var data = {};

        post('/api/v1/profile/twofactorauthentication', data, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 201) return callback(new ClientError(status, data));

            callback(null, data);
        });
    };

    Client.prototype.enableTwoFactorAuthentication = function (totpToken, callback) {
        var data = {
            totpToken: totpToken
        };

        post('/api/v1/profile/twofactorauthentication/enable', data, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 202) return callback(new ClientError(status, data));

            callback(null);
        });
    };

    Client.prototype.disableTwoFactorAuthentication = function (password, callback) {
        var data = {
            password: password
        };

        post('/api/v1/profile/twofactorauthentication/disable', data, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 202) return callback(new ClientError(status, data));

            callback(null);
        });
    };

    Client.prototype.startExternalLdapSync = function (callback) {
        post('/api/v1/cloudron/sync_external_ldap', {}, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 202) return callback(new ClientError(status, data));

            callback(null, data.taskId);
        });
    };

    Client.prototype.setUserActive = function (userId, active, callback) {
        var data = {
            active: active
        };

        post('/api/v1/users/' + userId + '/active', data, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null);
        });
    };

    Client.prototype.refreshUserInfo = function (callback) {
        var that = this;

        callback = typeof callback === 'function' ? callback : function () {};

        this.userInfo(function (error, result) {
            if (error) return callback(error);

            that.setUserInfo(result);
            callback(null);
        });
    };

    Client.prototype.refreshConfig = function (callback) {
        var that = this;

        callback = typeof callback === 'function' ? callback : function () {};

        this.config(function (error, result) {
            if (error) return callback(error);

            that.getUpdateInfo(function (error, info) { // note: non-admin users may get access denied for this
                if (!error) result.update = info.update; // attach update information to config object

                that.setConfig(result);
                callback(null);
            });
        });
    };

    Client.prototype.refreshAvailableLanguages = function (callback) {
        var that = this;

        get('/api/v1/cloudron/languages', null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            angular.copy(data.languages, that._availableLanguages);

            callback(null, data.languages);
        });
    };

    Client.prototype._appPostProcess = function (app) {
        // calculate the icon paths
        app.iconUrl = app.iconUrl ? (this.apiOrigin + app.iconUrl + '?access_token=' + token + '&' + app.manifest.version) : null;

        // amend the post install confirm state
        app.pendingPostInstallConfirmation = !!localStorage['confirmPostInstall_' + app.id];

        if (app.manifest.description) { // can be empty for dev apps
            var tmp = app.manifest.description.match(/\<upstream\>(.*)\<\/upstream\>/i);
            app.upstreamVersion = (tmp && tmp[1]) ? tmp[1] : '';
        } else {
            app.upstreamVersion = '';
        }

        if (!app.manifest.title) app.manifest.title = 'Untitled';

        if (app.manifest.postInstallMessage) {
            var text= app.manifest.postInstallMessage;
            // we chose - because underscore has special meaning in markdown
            text = text.replace(/\$CLOUDRON-APP-DOMAIN/g, app.fqdn);
            text = text.replace(/\$CLOUDRON-APP-ORIGIN/g, 'https://' + app.fqdn);
            text = text.replace(/\$CLOUDRON-API-DOMAIN/g, this._config.adminFqdn);
            text = text.replace(/\$CLOUDRON-API-ORIGIN/g, 'https://' + this._config.adminFqdn);
            text = text.replace(/\$CLOUDRON-USERNAME/g, this._userInfo.username);
            text = text.replace(/\$CLOUDRON-APP-ID/g, app.id);

            // [^] matches even newlines. '?' makes it non-greedy
            if (app.sso) text = text.replace(/<nosso>[^]*?<\/nosso>/g, '');
            else text = text.replace(/<sso>[^]*?<\/sso>/g, '');

            app.manifest.postInstallMessage = text;
        }

        return app;
    };

    function binarySearch(array, pred) {
        var lo = -1, hi = array.length;
        while (1 + lo !== hi) {
            var mi = lo + ((hi - lo) >> 1);
            if (pred(array[mi])) {
                hi = mi;
            } else {
                lo = mi;
            }
        }
        return hi;
    }

    Client.prototype._updateAppCache = function (app) {
        var tmp = {};
        angular.copy(app, tmp);

        var foundIndex = this._installedApps.findIndex(function (a) { return a.id === app.id; });

        // we replace new data into the existing reference to keep angular bindings
        if (foundIndex !== -1) {
            angular.copy(tmp, this._installedApps[foundIndex]);
        } else {
            this._installedApps.push(tmp);
        }

        // add reference to object map with appId keys
        this._installedAppsById[app.id] = this._installedApps[foundIndex];

        // TODO this not very elegant
        // update app tags
        tmp = this._installedApps
            .map(function (app) { return app.tags || []; })                     // return array of arrays
            .reduce(function (a, i) { return a.concat(i); }, [])                // merge all arrays into one
            .filter(function (v, i, self) { return self.indexOf(v) === i; })    // filter duplicates
            .sort(function (a, b) { return a.localeCompare(b); });              // sort

        // keep tag array references
        angular.copy(tmp, this._appTags);
    };

    Client.prototype.refreshInstalledApps = function (callback) {
        callback = callback || function (error) { if (error) console.error(error); };
        var that = this;

        this.getApps(function (error, apps) {
            if (error) return callback(error);

            async.eachLimit(apps, 20, function (app, iteratorCallback) {
                // only fetch if we have permissions
                if (!that._userInfo.isAtLeastAdmin) {
                    app.progress = 0;
                    app.message = '';
                    app.taskMinutesActive = 0;

                    that._updateAppCache(app);

                    return iteratorCallback();
                }

                var getTaskFunc = app.taskId ? that.getTask.bind(null, app.taskId) : function (next) { return next(); };
                getTaskFunc(function (error, task) {
                    if (error) return iteratorCallback(error);

                    if (task) {
                        app.progress = task.percent;
                        app.message = task.message;
                        app.taskMinutesActive = moment.duration(moment.utc().diff(moment.utc(task.creationTime))).asMinutes();
                    } else {
                        app.progress = 0;
                        app.message = '';
                        app.taskMinutesActive = 0;
                    }

                    that._updateAppCache(app);

                    iteratorCallback();
                });
            }, function iteratorDone(error) {
                if (error) return callback(error);

                // filter out old apps, going backwards to allow splicing
                for (var i = that._installedApps.length - 1; i >= 0; --i) {
                    if (!apps.some(function (elem) { return (elem.id === that._installedApps[i].id); })) {
                        var removed = that._installedApps.splice(i, 1);
                        delete that._installedAppsById[removed[0].id];
                    }
                }

                callback(null);
            });
        });
    };

    Client.prototype.login = function () {
        this.setToken(null);

        window.location.href = '/login.html?returnTo=/' + encodeURIComponent(window.location.hash);
    };

    Client.prototype.logout = function () {
        var token = this.getToken();
        this.setToken(null);

        // invalidates the token
        window.location.href = client.apiOrigin + '/api/v1/cloudron/logout?access_token=' + token;
    };

    Client.prototype.uploadFile = function (appId, file, progressCallback, callback) {
        var fd = new FormData();
        fd.append('file', file);

        var config = {
            headers: { 'Content-Type': undefined },
            transformRequest: angular.identity,
            uploadEventHandlers: {
                progress: progressCallback
            }
        };

        post('/api/v1/apps/' + appId + '/upload?file=' + encodeURIComponent('/tmp/' + file.name), fd, config, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 202) return callback(new ClientError(status, data));

            callback(null);
        });
    };

    Client.prototype.checkDownloadableFile = function (appId, filePath, callback) {
        var config = {
            headers: { 'Content-Type': undefined }
        };

        head('/api/v1/apps/' + appId + '/download?file=' + encodeURIComponent(filePath), config, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null);
        });
    };

    Client.prototype.sendTestMail = function (domain, to, callback) {
        var data = {
            to: to
        };

        post('/api/v1/mail/' + domain + '/send_test_mail', data, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 202) return callback(new ClientError(status, data));

            callback(null);
        });
    };

    // Domains
    Client.prototype.getDomains = function (callback) {
        get('/api/v1/domains', null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null, data.domains);
        });
    };

    Client.prototype.getDomain = function (domain, callback) {
        get('/api/v1/domains/' + domain, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null, data);
        });
    };

    Client.prototype.checkDNSRecords = function (domain, subdomain, callback) {
        get('/api/v1/domains/' + domain + '/dns_check?subdomain=' + subdomain, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null, data);
        });
    };

    Client.prototype.addDomain = function (domain, zoneName, provider, config, fallbackCertificate, tlsConfig, wellKnown, callback) {
        var data = {
            domain: domain,
            provider: provider,
            config: config,
            tlsConfig: tlsConfig,
            wellKnown: wellKnown
        };
        if (zoneName) data.zoneName = zoneName;
        var that = this;

        if (fallbackCertificate) data.fallbackCertificate = fallbackCertificate;

        // hack until we fix the domains.js
        post('/api/v1/domains', data, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 201) return callback(new ClientError(status, data));

            callback();
        });
    };

    Client.prototype.updateDomain = function (domain, zoneName, provider, config, fallbackCertificate, tlsConfig, wellKnown, callback) {
        var data = {
            provider: provider,
            config: config,
            tlsConfig: tlsConfig,
            wellKnown: wellKnown
        };
        if (zoneName) data.zoneName = zoneName;
        var that = this;

        if (fallbackCertificate) data.fallbackCertificate = fallbackCertificate;

        put('/api/v1/domains/' + domain, data, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 204) return callback(new ClientError(status, data));

            that.setDnsRecords({ domain: domain, type: 'mail' }, callback); // this is done so that an out-of-sync dkim key can be synced
        });
    };

    Client.prototype.renewCerts = function (callback) {
        post('/api/v1/cloudron/renew_certs', {}, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 202) return callback(new ClientError(status, data));

            callback(null, data.taskId);
        });
    };

    Client.prototype.removeDomain = function (domain, callback) {
        var config = {
            data: {
            },
            headers: {
                'Content-Type': 'application/json'
            }
        };

        del('/api/v1/domains/' + domain, config, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 204) return callback(new ClientError(status, data));

            callback(null);
        });
    };

    Client.prototype.prepareDashboardDomain = function (domain, callback) {
        var data = {
            domain: domain
        };

        post('/api/v1/cloudron/prepare_dashboard_domain', data, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 202) return callback(new ClientError(status, data));

            callback(null, data.taskId);
        });
    };

    Client.prototype.setDashboardDomain = function (domain, callback) {
        var data = {
            domain: domain
        };

        post('/api/v1/cloudron/set_dashboard_domain', data, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 204) return callback(new ClientError(status, data));

            callback(null);
        });
    };

    // Email
    Client.prototype.getMailEventLogs = function (search, types, page, perPage, callback) {
        var config = {
            params: {
                page: page,
                types: types,
                per_page: perPage,
                search: search
            }
        };

        get('/api/v1/mailserver/eventlog', config, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));
                callback(null, data.eventlogs);
        });
    };

    Client.prototype.getMailUsage = function (domain, callback) {
        var config = {
            params: {
                domain: domain
            }
        };

        get('/api/v1/mailserver/usage', config, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));
            callback(null, data.usage);
        });
    };

    Client.prototype.getMailLocation = function (callback) {
        var config = {};

        get('/api/v1/mailserver/location', config, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));
            callback(null, data); // { subdomain, domain }
        });
    };

    Client.prototype.setMailLocation = function (subdomain, domain, callback) {
        post('/api/v1/mailserver/location', { subdomain: subdomain, domain: domain }, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 202) return callback(new ClientError(status, data));

            callback(null, { taskId: data.taskId });
        });
    };

    Client.prototype.getMaxEmailSize = function (callback) {
        var config = {};

        get('/api/v1/mailserver/max_email_size', config, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));
            callback(null, data.size);
        });
    };

    Client.prototype.setMaxEmailSize = function (size, callback) {
        post('/api/v1/mailserver/max_email_size', { size: size }, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null);
        });
    };

    Client.prototype.getSolrConfig = function (callback) {
        var config = {};

        get('/api/v1/mailserver/solr_config', config, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));
            callback(null, data);
        });
    };

    Client.prototype.setSolrConfig = function (enabled, callback) {
        post('/api/v1/mailserver/solr_config', { enabled: enabled }, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null);
        });
    };

    Client.prototype.getSpamAcl = function (callback) {
        var config = {};

        get('/api/v1/mailserver/spam_acl', config, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));
            callback(null, data);
        });
    };

    Client.prototype.setSpamAcl = function (acl, callback) {
        post('/api/v1/mailserver/spam_acl', { whitelist: acl.whitelist, blacklist: acl.blacklist }, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null);
        });
    };

    Client.prototype.getSpamCustomConfig = function (callback) {
        var config = {};

        get('/api/v1/mailserver/spam_custom_config', config, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));
            callback(null, data.config);
        });
    };

    Client.prototype.setSpamCustomConfig = function (config, callback) {
        post('/api/v1/mailserver/spam_custom_config', { config: config }, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null);
        });
    };

    Client.prototype.getMailConfigForDomain = function (domain, callback) {
        get('/api/v1/mail/' + domain, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null, data);
        });
    };

    Client.prototype.enableMailForDomain = function (domain, enabled, callback) {
        post('/api/v1/mail/' + domain + '/enable', { enabled: enabled }, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 202) return callback(new ClientError(status, data));

            callback(null);
        });
    };

    Client.prototype.setDnsRecords = function (options, callback) {
        post('/api/v1/cloudron/sync_dns', options, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 201) return callback(new ClientError(status, data));

            callback(null, data.taskId);
        });
    };

    Client.prototype.getMailStatusForDomain = function (domain, callback) {
        get('/api/v1/mail/' + domain + '/status', null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null, data);
        });
    };

    Client.prototype.setMailRelay = function (domain, data, callback) {
        post('/api/v1/mail/' + domain + '/relay', data, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 202) return callback(new ClientError(status, data));

            callback(null);
        });
    };

    Client.prototype.setMailBanner = function (domain, data, callback) {
        post('/api/v1/mail/' + domain + '/banner', data, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 202) return callback(new ClientError(status, data));

            callback(null);
        });
    };

    Client.prototype.setCatchallAddresses = function (domain, addresses, callback) {
        post('/api/v1/mail/' + domain + '/catch_all', { addresses: addresses }, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 202) return callback(new ClientError(status, data));

            callback(null);
        });
    };

    Client.prototype.setMailFromValidation = function (domain, enabled, callback) {
        post('/api/v1/mail/' + domain + '/mail_from_validation', { enabled: enabled }, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 202) return callback(new ClientError(status, data));

            callback(null);
        });
    };

    // Mailboxes
    Client.prototype.getMailboxCount = function (domain, callback) {
        get('/api/v1/mail/' + domain + '/mailbox_count', null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null, data.count);
        });
    };

    Client.prototype.listMailboxes = function (domain, search, page, perPage, callback) {
        var config = {
            params: {
                search: search,
                page: page,
                per_page: perPage
            }
        };

        get('/api/v1/mail/' + domain + '/mailboxes', config, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null, data.mailboxes);
        });
    };

    Client.prototype.getMailbox = function (domain, name, callback) {
        get('/api/v1/mail/' + domain + '/mailboxes/' + name, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null, data.mailbox);
        });
    };

    Client.prototype.addMailbox = function (domain, name, ownerId, ownerType, callback) {
        var data = {
            name: name,
            ownerId: ownerId,
            ownerType: ownerType
        };

        post('/api/v1/mail/' + domain + '/mailboxes', data, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 201) return callback(new ClientError(status, data));

            callback(null);
        });
    };

    Client.prototype.updateMailbox = function (domain, name, ownerId, ownerType, callback) {
        var data = {
            ownerId: ownerId,
            ownerType: ownerType
        };

        post('/api/v1/mail/' + domain + '/mailboxes/' + name, data, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 204) return callback(new ClientError(status, data));

            callback(null);
        });
    };

    Client.prototype.removeMailbox = function (domain, name, deleteMails, callback) {
        var config = {
            data: {
                deleteMails: deleteMails
            },
            headers: {
                'Content-Type': 'application/json'
            }
        };

        del('/api/v1/mail/' + domain + '/mailboxes/' + name, config, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 201) return callback(new ClientError(status, data));

            callback(null);
        });
    };

    Client.prototype.getAliases = function (name, domain, callback) {
        var config = {
            params: {
                page: 1,
                per_page: 1000
            }
        };

        get('/api/v1/mail/' + domain + '/mailboxes/' + name + '/aliases', config, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null, data.aliases);
        });
    };

    Client.prototype.setAliases = function (name, domain, aliases, callback) {
        var data = {
            aliases: aliases
        };

        put('/api/v1/mail/' + domain + '/mailboxes/' + name + '/aliases', data, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 202) return callback(new ClientError(status, data));

            callback(null);
        });
    };

    Client.prototype.listMailingLists = function (domain, search, page, perPage, callback) {
        var config = {
            params: {
                search: search,
                page: page,
                per_page: perPage
            }
        };

        get('/api/v1/mail/' + domain + '/lists', config, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null, data.lists);
        });
    };

    Client.prototype.getMailingList = function (domain, name, callback) {
        get('/api/v1/mail/' + domain + '/lists/' + name, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null, data.list);
        });
    };

    Client.prototype.addMailingList = function (domain, name, members, membersOnly, callback) {
        var data = {
            name: name,
            members: members,
            membersOnly: membersOnly
        };

        post('/api/v1/mail/' + domain + '/lists', data, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 201) return callback(new ClientError(status, data));

            callback(null);
        });
    };

    Client.prototype.updateMailingList = function (domain, name, members, membersOnly, callback) {
        var data = {
            members: members,
            membersOnly: membersOnly
        };

        post('/api/v1/mail/' + domain + '/lists/' + name, data, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 204) return callback(new ClientError(status, data));

            callback(null);
        });
    };

    Client.prototype.removeMailingList = function (domain, name, callback) {
        del('/api/v1/mail/' + domain + '/lists/' + name, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 204) return callback(new ClientError(status, data));

            callback(null);
        });
    };

    // Volumes
    Client.prototype.getVolumes = function (callback) {
        get('/api/v1/volumes', null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null, data.volumes);
        });
    };

    Client.prototype.getVolume = function (volume, callback) {
        get('/api/v1/volumes/' + volume, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null, data);
        });
    };

    Client.prototype.addVolume = function (name, hostPath, callback) {
        var data = {
            name: name,
            hostPath: hostPath
        };
        var that = this;

        post('/api/v1/volumes', data, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 201) return callback(new ClientError(status, data));

            callback();
        });
    };

    Client.prototype.removeVolume = function (volume, callback) {
        var config = {
            data: {
            },
            headers: {
                'Content-Type': 'application/json'
            }
        };

        del('/api/v1/volumes/' + volume, config, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 204) return callback(new ClientError(status, data));

            callback(null);
        });
    };

    Client.prototype.getAppstoreUserToken = function (callback) {
        post('/api/v1/appstore/user_token', {}, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 201) return callback(new ClientError(status, data));

            callback(null, data.accessToken);
        });
    };

    // This will change the location
    Client.prototype.openSubscriptionSetup = function (subscription) {
        // we only allow the owner to do so
        if (!this._userInfo.isAtLeastOwner) return;

        // basically the user has not setup appstore account yet
        if (!subscription.plan) return window.location.href = '/#/appstore';

        var that = this;

        var email = subscription.emailEncoded;
        var cloudronId = subscription.cloudronId;

        if (!this._userInfo.isAtLeastOwner) return window.location.href = that.getConfig().webServerOrigin + '/console.html#/userprofile?view=subscriptions&email=' + email + '&cloudronId=' + cloudronId;

        this.getAppstoreUserToken(function (error, token) {
            if (error) console.error('Unable to get appstore user token.', error);

            var url = that.getConfig().webServerOrigin + '/console.html#/userprofile?view=subscriptions&email=' + email + '&token=' + token;

            // Only open the subscription setup dialog when no subscription exists
            if (!subscription.plan || subscription.plan.id === 'free') url += '&cloudronId=' + cloudronId

            window.location.href = url;
        });
    };

    Client.prototype.getAppstoreAppByIdAndVersion = function (appId, version, callback) {
        var url = '/api/v1/appstore/apps/' + appId;
        if (version && version !== 'latest') url += '/versions/' + version;

        get(url, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null, data);
        });
    };

    Client.prototype.getAppstoreApps = function (callback) {
        var that = this;

        get('/api/v1/appstore/apps', null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            angular.copy(data.apps, that._appstoreAppCache);

            return callback(null, that._appstoreAppCache);
        });
    };

    Client.prototype.getAppstoreAppsFast = function (callback) {
        if (this._appstoreAppCache.length !== 0) return callback(null, this._appstoreAppCache);

        this.getAppstoreApps(callback);
    };

    Client.prototype.getSubscription = function (callback) {
        if (!this._userInfo.isAtLeastAdmin) return callback(new Error('Not allowed'));

        get('/api/v1/appstore/subscription', null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            // just some helper property, since angular bindings cannot dot his easily
            data.emailEncoded = encodeURIComponent(data.email);

            callback(null, data); // { email, plan: { id, name }, cancel_at, status }
        });
    };

    Client.prototype.registerCloudron = function (email, password, totpToken, signup, purpose, callback) {
        var data = {
            email: email,
            password: password,
            signup: signup,
            purpose: purpose
        };

        if (totpToken) data.totpToken = totpToken;

        post('/api/v1/appstore/register_cloudron', data, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 201) return callback(new ClientError(status, data));

            callback(null);
        });
    };

    // FileManager API
    // mode can be 'download', 'open', 'link' or 'data'
    Client.prototype.filesGet = function (id, type, path, mode, callback) {
        var objpath = (type === 'app' ? 'apps/' : 'volumes/') + id;

        if (mode === 'download') {
            window.open(client.apiOrigin + '/api/v1/' + objpath + '/files/' + path + '?download=true&access_token=' + token);
            callback(null);
        } else if (mode === 'open') {
            window.open(client.apiOrigin + '/api/v1/' + objpath + '/files/' + path + '?download=false&access_token=' + token);
            callback(null);
        } else if (mode === 'link') {
            callback(null, client.apiOrigin + '/api/v1/' + objpath + '/files/' + path + '?download=false&access_token=' + token);
        } else {
            function responseHandler(data, headers, status) {
                if (headers()['content-type'] && headers()['content-type'].indexOf('application/json') !== -1) return JSON.parse(data);
                return data;
            }

            get('/api/v1/' + objpath + '/files/' + path, { transformResponse: responseHandler }, function (error, data, status) {
                if (error) return callback(error);
                if (status !== 200) return callback(new ClientError(status, data));

                callback(null, data);
            });
        }
    };

    Client.prototype.filesRemove = function (id, type, path, callback) {
        var objpath = (type === 'app' ? 'apps/' : 'volumes/') + id;

        del('/api/v1/' + objpath + '/files/' + path, null, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null, data);
        });
    };

    Client.prototype.filesExtract = function (id, type, path, callback) {
        var objpath = (type === 'app' ? 'apps/' : 'volumes/') + id;

        put('/api/v1/' + objpath + '/files/' + path, { action: 'extract' }, {}, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null, data);
        });
    };

    Client.prototype.filesChown = function (id, type, path, uid, recursive, callback) {
        var objpath = (type === 'app' ? 'apps/' : 'volumes/') + id;

        put('/api/v1/' + objpath + '/files/' + path, { action: 'chown', uid: uid, recursive: recursive }, {}, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null, data);
        });
    };

    Client.prototype.filesRename = function (id, type, path, newPath, callback) {
        var objpath = (type === 'app' ? 'apps/' : 'volumes/') + id;

        put('/api/v1/' + objpath + '/files/' + path, { action: 'rename', newFilePath: decodeURIComponent(newPath) }, {}, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null, data);
        });
    };

    Client.prototype.filesCopy = function (id, type, path, newPath, callback) {
        var that = this;

        var objpath = (type === 'app' ? 'apps/' : 'volumes/') + id;

        put('/api/v1/' + objpath + '/files/' + path, { action: 'copy', newFilePath: decodeURIComponent(newPath) }, {}, function (error, data, status) {
            if (error && error.statusCode === 409) return that.filesCopy(id, type, path, newPath + '-copy', callback);
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null, data);
        });
    };

    Client.prototype.filesCreateDirectory = function (id, type, path, callback) {
        var objpath = (type === 'app' ? 'apps/' : 'volumes/') + id;

        post('/api/v1/' + objpath + '/files/' + path, { directory: decodeURIComponent(path) }, {}, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null, data);
        });
    };

    Client.prototype.filesCreateFile = function (id, type, path, callback) {
        var objpath = (type === 'app' ? 'apps/' : 'volumes/') + id;

        post('/api/v1/' + objpath + '/files/' + path, {}, {}, function (error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null, data);
        });
    };

    Client.prototype.filesUpload = function (id, type, path, file, overwrite, progressHandler, callback) {
        var objpath = (type === 'app' ? 'apps/' : 'volumes/') + id;

        var fd = new FormData();
        fd.append('file', file);

        if (overwrite) fd.append('overwrite', 'true');

        function done(error, data, status) {
            if (error) return callback(error);
            if (status !== 200) return callback(new ClientError(status, data));

            callback(null, data);
        }

        $http({
            url: client.apiOrigin + '/api/v1/' + objpath + '/files/' + path,
            method: 'POST',
            data: fd,
            transformRequest: angular.identity,
            headers: {
                'Content-Type': undefined,
                Authorization: 'Bearer ' + token
            },

            uploadEventHandlers: {
                progress: function (e) {
                    progressHandler(e.loaded);
                }
            }
        }).success(defaultSuccessHandler(done)).error(defaultErrorHandler(done));
    };

    client = new Client();
    return client;
}]);
