'use strict';

/* global angular */
/* global $ */
/* global async */
/* global RSTATES */
/* global ISTATES */
/* global ERROR */
/* global Chart */
/* global Clipboard */
/* global SECRET_PLACEHOLDER */

angular.module('Application').controller('AppController', ['$scope', '$location', '$timeout', '$interval', '$route', '$routeParams', 'Client', function ($scope, $location, $timeout, $interval, $route, $routeParams, Client) {
    Client.onReady(function () { if (!Client.getUserInfo().isAtLeastAdmin) $location.path('/'); });

    // List is from http://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region
    $scope.s3Regions = [
        { name: 'Asia Pacific (Mumbai)', value: 'ap-south-1' },
        { name: 'Asia Pacific (Osaka-Local)', value: 'ap-northeast-3' },
        { name: 'Asia Pacific (Seoul)', value: 'ap-northeast-2' },
        { name: 'Asia Pacific (Singapore)', value: 'ap-southeast-1' },
        { name: 'Asia Pacific (Sydney)', value: 'ap-southeast-2' },
        { name: 'Asia Pacific (Tokyo)', value: 'ap-northeast-1' },
        { name: 'Canada (Central)', value: 'ca-central-1' },
        { name: 'China (Beijing)', value: 'cn-north-1' },
        { name: 'China (Ningxia)', value: 'cn-northwest-1' },
        { name: 'EU (Frankfurt)', value: 'eu-central-1' },
        { name: 'EU (Ireland)', value: 'eu-west-1' },
        { name: 'EU (London)', value: 'eu-west-2' },
        { name: 'EU (Paris)', value: 'eu-west-3' },
        { name: 'EU (Stockholm)', value: 'eu-north-1' },
        { name: 'South America (São Paulo)', value: 'sa-east-1' },
        { name: 'US East (N. Virginia)', value: 'us-east-1' },
        { name: 'US East (Ohio)', value: 'us-east-2' },
        { name: 'US West (N. California)', value: 'us-west-1' },
        { name: 'US West (Oregon)', value: 'us-west-2' },
    ];

    $scope.wasabiRegions = [
        { name: 'EU Central 1', value: 'https://s3.eu-central-1.wasabisys.com' },
        { name: 'US East 1', value: 'https://s3.us-east-1.wasabisys.com' },
        { name: 'US East 2', value: 'https://s3.us-east-2.wasabisys.com ' },
        { name: 'US West 1', value: 'https://s3.us-west-1.wasabisys.com' }
    ];

    $scope.doSpacesRegions = [
        { name: 'AMS3', value: 'https://ams3.digitaloceanspaces.com' },
        { name: 'FRA1', value: 'https://fra1.digitaloceanspaces.com' },
        { name: 'NYC3', value: 'https://nyc3.digitaloceanspaces.com' },
        { name: 'SFO2', value: 'https://sfo2.digitaloceanspaces.com' },
        { name: 'SFO3', value: 'https://sfo3.digitaloceanspaces.com' },
        { name: 'SGP1', value: 'https://sgp1.digitaloceanspaces.com' }
    ];

    $scope.exoscaleSosRegions = [
        { name: 'AT-VIE-1', value: 'https://sos-at-vie-1.exo.io' },
        { name: 'CH-DK-2', value: 'https://sos-ch-dk-2.exo.io' },
        { name: 'CH-GVA-2', value: 'https://sos-ch-gva-2.exo.io' },
        { name: 'DE-FRA-1', value: 'https://sos-de-fra-1.exo.io' },
    ];

    // https://www.scaleway.com/docs/object-storage-feature/
    $scope.scalewayRegions = [
        { name: 'FR-PAR', value: 'https://s3.fr-par.scw.cloud', region: 'fr-par' }, // default
        { name: 'NL-AMS', value: 'https://s3.nl-ams.scw.cloud', region: 'nl-ams' }
    ];

    $scope.linodeRegions = [
        { name: 'Newark', value: 'us-east-1.linodeobjects.com', region: 'us-east-1' }, // default
        { name: 'Frankfurt', value: 'eu-central-1.linodeobjects.com', region: 'us-east-1' },
        { name: 'Singapore', value: 'ap-south-1.linodeobjects.com', region: 'us-east-1' },
    ];

    // note: ovh also has a storage endpoint but that only supports path style access
    $scope.ovhRegions = [
        { name: 'Beauharnois (BHS)', value: 'https://s3.bhs.cloud.ovh.net', region: 'bhs' }, // default
        { name: 'Frankfurt (DE)', value: 'https://s3.de.cloud.ovh.net', region: 'de' },
        { name: 'Gravelines (GRA)', value: 'https://s3.gra.cloud.ovh.net', region: 'gra' },
        { name: 'Strasbourg (SBG)', value: 'https://s3.sbg.cloud.ovh.net', region: 'sbg' },
        { name: 'London (UK)', value: 'https://s3.uk.cloud.ovh.net', region: 'uk' },
        { name: 'Sydney (SYD)', value: 'https://s3.syd.cloud.ovh.net', region: 'syd' },
        { name: 'Warsaw (WAW)', value: 'https://s3.waw.cloud.ovh.net', region: 'waw' },
    ];

    // https://devops.ionos.com/api/s3/
    $scope.ionosRegions = [
        { name: 'DE', value: 'https://s3-de-central.profitbricks.com', region: 's3-de-central' }, // default
    ];

    $scope.storageProvider = [
        { name: 'Amazon S3', value: 's3' },
        { name: 'Backblaze B2 (S3 API)', value: 'backblaze-b2' },
        { name: 'CIFS Mount', value: 'cifs' },
        { name: 'DigitalOcean Spaces', value: 'digitalocean-spaces' },
        { name: 'Exoscale SOS', value: 'exoscale-sos' },
        { name: 'Filesystem', value: 'filesystem' },
        { name: 'Google Cloud Storage', value: 'gcs' },
        { name: 'IONOS (Profitbricks)', value: 'ionos-objectstorage' },
        { name: 'Linode Object Storage', value: 'linode-objectstorage' },
        { name: 'Minio', value: 'minio' },
        { name: 'NFS Mount', value: 'nfs' },
        { name: 'OVH Object Storage', value: 'ovh-objectstorage' },
        { name: 'S3 API Compatible (v4)', value: 's3-v4-compat' },
        { name: 'Scaleway Object Storage', value: 'scaleway-objectstorage' },
        { name: 'SSHFS Mount', value: 'sshfs' },
        // { name: 'No-op (Only for testing)', value: 'noop' },
        { name: 'Wasabi', value: 'wasabi' }
    ];

    $scope.formats = [
        { name: 'Tarball (zipped)', value: 'tgz' },
        { name: 'rsync', value: 'rsync' }
    ];

    // Avoid full reload on path change
    // https://stackoverflow.com/a/22614334
    // reloadOnUrl: false in $routeProvider did not work!
    var lastRoute = $route.current;
    $scope.$on('$locationChangeSuccess', function (/* event */) {
        if (lastRoute.$$route.originalPath === $route.current.$$route.originalPath) {
            $route.current = lastRoute;
        }
    });

    var appId = $routeParams.appId;
    if (!appId) return $location.path('/apps');

    $scope.view = '';
    $scope.app = null;
    $scope.config = Client.getConfig();
    $scope.user = Client.getUserInfo();
    $scope.domains = [];
    $scope.volumes = [];
    $scope.groups = [];
    $scope.users = [];
    $scope.backupConfig = null;

    $scope.HOST_PORT_MIN = 1;
    $scope.HOST_PORT_MAX = 65535;
    $scope.ROBOTS_DISABLE_INDEXING_TEMPLATE = '# Disable search engine indexing\n\nUser-agent: *\nDisallow: /';

    $scope.setView = function (view, skipViewShow) {
        if ($scope.view === view) return;

        $route.updateParams({ view: view });
        if (!skipViewShow) $scope[view].show();
        $scope.view = view;
    };

    $scope.stopAppTask = function (taskId) {
        Client.stopTask(taskId, function (error) {
            // we can ignore a call trying to cancel an already done task
            if (error && error.statusCode !== 409) Client.error(error);
        });
    };

    $scope.postInstallMessage = {
        confirmed: false,
        openApp: false,

        show: function (openApp) {
            $scope.postInstallMessage.confirmed = false;
            $scope.postInstallMessage.openApp = !!openApp;

            if (!$scope.app.manifest.postInstallMessage) return;
            $('#postInstallModal').modal('show');
        },

        submit: function () {
            if (!$scope.postInstallMessage.confirmed) return;

            $scope.app.pendingPostInstallConfirmation = false;
            delete localStorage['confirmPostInstall_' + $scope.app.id];

            $('#postInstallModal').modal('hide');
        }
    };

    $scope.display = {
        busy: false,
        error: {},
        success: false,

        tags: '',
        label: '',
        icon: { data: null },

        iconUrl: function () {
            if (!$scope.app) return '';

            if ($scope.display.icon.data === '__original__') { // user clicked reset
                return $scope.app.iconUrl + '&original=true';
            } else if ($scope.display.icon.data) { // user uploaded icon
                return $scope.display.icon.data;
            } else { // current icon
                return $scope.app.iconUrl;
            }
        },

        resetCustomIcon: function () {
            $scope.display.icon.data = '__original__';
        },

        showCustomIconSelector: function () {
            $('#iconFileInput').click();
        },

        show: function () {
            var app = $scope.app;

            $scope.display.error = {};

            // translate for tag-input
            $scope.display.tags = app.tags ? app.tags.join(' ') : '';

            $scope.display.label = $scope.app.label || '';
            $scope.display.icon = { data: null };
        },

        submit: function () {
            $scope.display.busy = true;
            $scope.display.error = {};

            function done(error) {
                if (error) Client.error(error);

                $scope.displayForm.$setPristine();
                $scope.display.success = true;

                refreshApp($scope.app.id, function (error) {
                    if (error) Client.error(error);

                    $scope.display.show(); // "refresh" view with latest data

                    $timeout(function () { $scope.display.busy = false; }, 1000);
                });
            }

            var NOOP = function (next) { return next(); };
            var configureLabel = $scope.display.label === $scope.app.label ? NOOP : Client.configureApp.bind(null, $scope.app.id, 'label', { label: $scope.display.label });

            configureLabel(function (error) {
                if (error) return done(error);

                var tags = $scope.display.tags.split(' ').map(function (t) { return t.trim(); }).filter(function (t) { return !!t; });

                var configureTags = angular.equals(tags, $scope.app.tags) ? NOOP : Client.configureApp.bind(null, $scope.app.id, 'tags', { tags: tags });

                configureTags(function (error) {
                    if (error) return done(error);

                    // skip if icon is unchanged
                    if ($scope.display.icon.data === null) return done();

                    var icon;
                    if ($scope.display.icon.data === '__original__') { // user reset the icon
                        icon = '';
                    } else if ($scope.display.icon.data) { // user loaded custom icon
                        icon = $scope.display.icon.data.replace(/^data:image\/[a-z]+;base64,/, '');
                    }

                    Client.configureApp($scope.app.id, 'icon', { icon: icon }, function (error) {
                        if (error) return done(error);
                        done();
                    });
                });
            });
        }
    };

    $scope.location = {
        busy: false,
        error: {},
        domainCollisions: [],

        domain: null,
        location: '',
        alternateDomains: [],
        aliasDomains: [],
        portBindings: {},
        portBindingsEnabled: {},
        portBindingsInfo: {},

        addAlternateDomain: function (event) {
            event.preventDefault();
            $scope.location.alternateDomains.push({
                domain: $scope.domains.filter(function (d) { return d.domain === $scope.app.domain; })[0], // pre-select app's domain by default
                subdomain: ''
            });
        },

        delAlternateDomain: function (event, index) {
            event.preventDefault();
            $scope.location.alternateDomains.splice(index, 1);
        },

        addAliasDomain: function (event) {
            event.preventDefault();
            $scope.location.aliasDomains.push({
                domain: $scope.domains.filter(function (d) { return d.domain === $scope.app.domain; })[0], // pre-select app's domain by default
                subdomain: ''
            });
        },

        delAliasDomain: function (event, index) {
            event.preventDefault();
            $scope.location.aliasDomains.splice(index, 1);
        },

        show: function () {
            var app = $scope.app;

            $scope.location.error = {};
            $scope.location.domainCollisions = [];
            $scope.location.location = app.location;
            $scope.location.domain = $scope.domains.filter(function (d) { return d.domain === app.domain; })[0];
            $scope.location.portBindingsInfo = angular.extend({}, app.manifest.tcpPorts, app.manifest.udpPorts); // Portbinding map only for information
            $scope.location.alternateDomains = app.alternateDomains.map(function (a) { return { subdomain: a.subdomain, domain: $scope.domains.filter(function (d) { return d.domain === a.domain; })[0] };});
            $scope.location.aliasDomains = app.aliasDomains.map(function (a) { return { subdomain: a.subdomain, domain: $scope.domains.filter(function (d) { return d.domain === a.domain; })[0] };});

            // fill the portBinding structures. There might be holes in the app.portBindings, which signalizes a disabled port
            for (var env in $scope.location.portBindingsInfo) {
                if (app.portBindings && app.portBindings[env]) {
                    $scope.location.portBindings[env] = app.portBindings[env];
                    $scope.location.portBindingsEnabled[env] = true;
                } else {
                    $scope.location.portBindings[env] = $scope.location.portBindingsInfo[env].defaultValue || 0;
                    $scope.location.portBindingsEnabled[env] = false;
                }
            }
        },

        submit: function (overwriteDns) {
            $('#domainCollisionsModal').modal('hide');

            $scope.location.busy = true;
            $scope.location.error = {};
            $scope.location.domainCollisions = [];

            // only use enabled ports from portBindings
            var portBindings = {};
            for (var env in $scope.location.portBindings) {
                if ($scope.location.portBindingsEnabled[env]) {
                    portBindings[env] = $scope.location.portBindings[env];
                }
            }

            var data = {
                overwriteDns: !!overwriteDns,
                location: $scope.location.location,
                domain: $scope.location.domain.domain,
                portBindings: portBindings,
                alternateDomains: $scope.location.alternateDomains.map(function (a) { return { subdomain: a.subdomain, domain: a.domain.domain };}),
                aliasDomains: $scope.location.aliasDomains.map(function (a) { return { subdomain: a.subdomain, domain: a.domain.domain };})
            };

            // pre-flight only for changed domains
            var domains = [];
            if ($scope.app.domain !== data.domain || $scope.app.location !== data.location) domains.push({ subdomain: data.location, domain: data.domain, type: 'main' });
            data.alternateDomains.forEach(function (a) {
                if ($scope.app.alternateDomains.some(function (d) { return d.domain === a.domain && d.subdomain === a.subdomain; })) return;
                domains.push({ subdomain: a.subdomain, domain: a.domain, type: 'redirect' });
            });
            data.aliasDomains.forEach(function (a) {
                if ($scope.app.aliasDomains.some(function (d) { return d.domain === a.domain && d.subdomain === a.subdomain; })) return;
                domains.push({ subdomain: a.subdomain, domain: a.domain, type: 'alias' });
            });

            async.eachSeries(domains, function (domain, callback) {
                if (overwriteDns) return callback();

                Client.checkDNSRecords(domain.domain, domain.subdomain, function (error, result) {
                    if (error) return callback(error);
                    if (result.error) {
                        if (domain.type === 'main') {
                            $scope.location.error.location = domain.domain + ' ' + result.error.message;
                        } else if (domain.type === 'alias') {
                            $scope.location.error.aliasDomains = domain.domain + ' ' + result.error.message;
                        } else {
                            $scope.location.error.alternateDomains = domain.domain + ' ' + result.error.message;
                        }
                        $scope.location.busy = false;
                        return;
                    }

                    if (result.needsOverwrite) $scope.location.domainCollisions.push(domain);

                    callback();
                });
            }, function (error) {
                if (error) {
                    $scope.location.busy = false;
                    return Client.error(error);
                }

                if ($scope.location.domainCollisions.length) {
                    $scope.location.busy = false;
                    return $('#domainCollisionsModal').modal('show');
                }

                Client.configureApp($scope.app.id, 'location', data, function (error) {
                    if (error && (error.statusCode === 409 || error.statusCode === 400)) {
                        if ((error.subdomain && error.domain) || error.field === 'location') {
                            if (data.domain === error.domain && data.location === error.subdomain) { // the primary
                                $scope.location.error.location = error.message;
                                $scope.locationForm.$setPristine();
                            } else { // FIXME: check error in aliasDomains
                                $scope.location.error.alternateDomains = error.message;
                            }
                        } else if (error.portName || error.field === 'portBindings') {
                            $scope.location.error.port = error.message;
                        }

                        $scope.location.busy = false;
                        return;
                    }
                    if (error) return Client.error(error);

                    refreshApp($scope.app.id, function (error) {
                        if (error) return Client.error(error);

                        $scope.locationForm.$setPristine();
                        $timeout(function () { $scope.location.busy = false; }, 1000);
                    });
                });
            });
        }
    };

    $scope.access = {
        busy: false,
        error: {},
        success: false,

        ftp: false,
        ssoAuth: false,
        accessRestrictionOption: 'any',
        accessRestriction: { users: [], groups: [] },

        isAccessRestrictionValid: function () {
            var tmp = $scope.access.accessRestriction;
            return !!(tmp.users.length || tmp.groups.length);
        },

        show: function () {
            var app = $scope.app;

            $scope.access.error = {};
            $scope.access.ftp = app.manifest.addons.localstorage && app.manifest.addons.localstorage.ftp;
            $scope.access.ssoAuth = (app.manifest.addons['ldap'] || app.manifest.addons['proxyAuth']) && app.sso;
            $scope.access.accessRestrictionOption = app.accessRestriction ? 'groups' : 'any';
            $scope.access.accessRestriction = { users: [], groups: [] };

            if (app.accessRestriction) {
                var userSet = { };
                app.accessRestriction.users.forEach(function (uid) { userSet[uid] = true; });
                $scope.users.forEach(function (u) { if (userSet[u.id] === true) $scope.access.accessRestriction.users.push(u); });

                var groupSet = { };
                app.accessRestriction.groups.forEach(function (gid) { groupSet[gid] = true; });
                $scope.groups.forEach(function (g) { if (groupSet[g.id] === true) $scope.access.accessRestriction.groups.push(g); });
            }
        },

        submit: function () {
            $scope.access.busy = true;
            $scope.access.error = {};

            var accessRestriction = null;
            if ($scope.access.accessRestrictionOption === 'groups') {
                accessRestriction = { users: [], groups: [] };
                accessRestriction.users = $scope.access.accessRestriction.users.map(function (u) { return u.id; });
                accessRestriction.groups = $scope.access.accessRestriction.groups.map(function (g) { return g.id; });
            }

            Client.configureApp($scope.app.id, 'access_restriction', { accessRestriction: accessRestriction }, function (error) {
                if (error) return Client.error(error);

                $timeout(function () {
                    $scope.access.success = true;
                    $scope.access.busy = false;
                }, 1000);
            });
        }
    };

    $scope.resources = {
        error: {},

        busy: false,
        currentMemoryLimit: 0,
        memoryLimit: 0,
        memoryTicks: [],

        busyCpuShares: false,
        currentCpuShares: 0,
        cpuShares: 0,

        show: function () {
            var app = $scope.app;

            $scope.resources.error = {};
            $scope.resources.currentMemoryLimit = app.memoryLimit || app.manifest.memoryLimit || (256 * 1024 * 1024);
            $scope.resources.memoryLimit = $scope.resources.currentMemoryLimit;
            $scope.resources.currentCpuShares = $scope.resources.cpuShares = app.cpuShares;

            Client.memory(function (error, memory) {
                if (error) return console.error(error);

                // create ticks starting from manifest memory limit. the memory limit here is currently split into ram+swap (and thus *2 below)
                // TODO: the *2 will overallocate since 4GB is max swap that cloudron itself allocates
                $scope.resources.memoryTicks = [];
                var npow2 = Math.pow(2, Math.ceil(Math.log(memory.memory)/Math.log(2)));
                for (var i = 256; i <= (npow2*2/1024/1024); i *= 2) {
                    if (i >= (app.manifest.memoryLimit/1024/1024 || 0)) $scope.resources.memoryTicks.push(i * 1024 * 1024);
                }
                if (app.manifest.memoryLimit && $scope.resources.memoryTicks[0] !== app.manifest.memoryLimit) {
                    $scope.resources.memoryTicks.unshift(app.manifest.memoryLimit);
                }
            });
        },

        submitMemoryLimit: function () {
            $scope.resources.busy = true;
            $scope.resources.error = {};

            var memoryLimit = $scope.resources.memoryLimit === $scope.resources.memoryTicks[0] ? 0 : $scope.resources.memoryLimit;
            Client.configureApp($scope.app.id, 'memory_limit', { memoryLimit: memoryLimit }, function (error) {
                if (error && error.statusCode === 400) {
                    $scope.resources.busy = false;
                    $scope.resources.error.memoryLimit = true;
                    return;
                }
                if (error) return Client.error(error);

                $scope.resources.currentMemoryLimit = $scope.resources.memoryLimit;

                refreshApp($scope.app.id, function (error) {
                    if (error) return Client.error(error);

                    $timeout(function () { $scope.resources.busy = false; }, 1000);
                });
            });
        },

        submitCpuShares: function () {
            $scope.resources.busyCpuShares = true;
            $scope.resources.error = {};

            Client.configureApp($scope.app.id, 'cpu_shares', { cpuShares: $scope.resources.cpuShares }, function (error) {
                if (error) return Client.error(error);

                $scope.resources.currentCpuShares = $scope.resources.cpuShares;

                refreshApp($scope.app.id, function (error) {
                    if (error) return Client.error(error);

                    $timeout(function () { $scope.resources.busyCpuShares = false; }, 1000);
                });
            });
        },
    };

    $scope.storage = {
        error: {},

        busy: false,

        busyDataDir: false,
        dataDir: null,

        busyBinds: false,
        mounts: [], // { volume, readOnly }

        show: function () {
            var app = $scope.app;

            $scope.storage.error = {};
            $scope.storage.dataDir = app.dataDir;
            $scope.storage.mounts = [];

            app.mounts.forEach(function (mount) { // { volumeId, readOnly }
                var volume = $scope.volumes.find(function (v) { return v.id === mount.volumeId; });
                $scope.storage.mounts.push({ volume: volume, readOnly: mount.readOnly });
            });
        },

        submitDataDir: function () {
            $scope.storage.busyDataDir = true;
            $scope.storage.error = {};

            Client.configureApp($scope.app.id, 'data_dir', { dataDir: $scope.storage.dataDir || null }, function (error) {
                if (error && error.statusCode === 400) {
                    $scope.storage.error.dataDir = error.message;
                    $scope.storage.busyDataDir = false;
                    return;
                }
                if (error) return Client.error(error);

                $scope.storageDataDirForm.$setPristine();

                refreshApp($scope.app.id, function (error) {
                    if (error) return Client.error(error);

                    $timeout(function () { $scope.storage.busyDataDir = false; }, 1000);
                });
            });
        },

        addMount: function (event) {
            event.preventDefault();
            $scope.storage.mounts.push({
                volume: $scope.volumes[0],
                readOnly: true
            });
        },

        delMount: function (event, index) {
            event.preventDefault();
            $scope.storage.mounts.splice(index, 1);
        },

        submitMounts: function () {
            $scope.storage.busyMounts = true;
            $scope.storage.error = {};

            var data = [];
            $scope.storage.mounts.forEach(function (mount) {
                data.push({ volumeId: mount.volume.id, readOnly: mount.readOnly });
            });

            Client.configureApp($scope.app.id, 'mounts', { mounts: data }, function (error) {
                if (error && error.statusCode === 400) {
                    $scope.storage.error.mounts = error.message;
                    $scope.storage.busyMounts = false;
                    return;
                }
                if (error) return Client.error(error);

                refreshApp($scope.app.id, function (error) {
                    if (error) return Client.error(error);

                    $timeout(function () { $scope.storage.busyMounts = false; }, 1000);
                });
            });
        }
    };

    $scope.graphs = {
        error: {},

        period: 12, // set as 12 because disk graphs is only collected twice a day
        periodLabel: '12 hours',
        memoryChart: null,
        diskChart: null,

        setPeriod: function (hours, label) {
            $scope.graphs.period = hours;
            $scope.graphs.periodLabel = label;
            $scope.graphs.show();
        },

        show: function () {
            // both in minutes
            var timePeriod = $scope.graphs.period * 60;
            var timeBucketSize = $scope.graphs.period > 24 ? (6*60) : 5;

            function fillGraph(canvasId, data, label, chartPropertyName, max) {
                if (!data) return; // no data available yet

                // fill holes with previous value
                var cur = 0;
                data.datapoints.forEach(function (d) {
                    if (d[0] === null) d[0] = cur;
                    else cur = d[0];
                });

                // translate the data from bytes to MB
                var datapoints = data.datapoints.map(function (d) { return parseInt((d[0] / 1024 / 1024).toFixed(2)); });

                // we calculate the labels based on timePeriod of chart / datapoints
                var minuteSteps = timePeriod / datapoints.length;
                var labels = datapoints.map(function (d, index) {
                    var dateTime = new Date(Date.now() - ((timePeriod - (index * minuteSteps)) * 60 * 1000));

                    if ($scope.graphs.period > 24) {
                        return dateTime.toLocaleDateString();
                    } else {
                        return dateTime.toLocaleTimeString();
                    }
                });

                var graphData = {
                    labels: labels,
                    datasets: [{
                        label: label,
                        backgroundColor: '#82C4F844',
                        borderColor: '#2196F3',
                        borderWidth: 1,
                        radius: 0,
                        data: datapoints
                    }]
                };

                var options = {
                    maintainAspectRatio: true,
                    aspectRatio: 2.5,
                    legend: {
                        display: false
                    },
                    tooltips: {
                        intersect: false
                    },
                    scales: {
                        xAxes: [{
                            ticks: {
                                autoSkipPadding: 20,
                            }
                        }],
                        yAxes: [{
                            ticks: {
                                min: 0,
                                max: max,
                                beginAtZero: true
                            }
                        }]
                    }
                };

                var ctx = $(canvasId).get(0).getContext('2d');

                if ($scope.graphs[chartPropertyName]) $scope.graphs[chartPropertyName].destroy();
                $scope.graphs[chartPropertyName] = new Chart(ctx, { type: 'line', data: graphData, options: options });
            }

            var memoryQuery = 'summarize(sum(collectd.localhost.table-' + appId + '-memory.gauge-rss, collectd.localhost.table-' + appId + '-memory.gauge-swap), "' + timeBucketSize + 'min", "avg")';

            Client.graphs([ memoryQuery ], '-' + timePeriod + 'min', {}, function (error, result) {
                if (error) return console.error(error);

                var currentMemoryLimit = $scope.app.memoryLimit || $scope.app.manifest.memoryLimit || (256 * 1024 * 1024);

                fillGraph('#graphsMemoryChart', result[0], 'Memory', 'memoryChart', currentMemoryLimit / 1024 / 1024);
            });
        }
    };

    $scope.email = {
        busy: false,
        error: {},

        enableMailbox: true,
        mailboxName: '',
        mailboxDomain: '',
        currentMailboxName: '',
        currentMailboxDomainName: '',

        show: function () {
            var app = $scope.app;

            $scope.emailForm.$setPristine();
            $scope.email.error = {};
            $scope.email.enableMailbox = app.enableMailbox;
            $scope.email.mailboxName = app.mailboxName || '';
            $scope.email.mailboxDomain = $scope.domains.filter(function (d) { return d.domain === app.mailboxDomain; })[0];
            $scope.email.currentMailboxName = app.mailboxName || '';
            $scope.email.currentMailboxDomainName = $scope.email.mailboxDomain.domain;
        },

        submit: function () {
            $scope.email.error = {};
            $scope.email.busy = true;

            Client.configureApp($scope.app.id, 'mailbox', { enable: $scope.email.enableMailbox === '1', mailboxName: $scope.email.mailboxName || null, mailboxDomain: $scope.email.mailboxDomain.domain }, function (error) {
                if (error && error.statusCode === 400) {
                    $scope.email.busy = false;
                    $scope.email.error.mailboxName = error.message;
                    $scope.emailForm.$setPristine();
                    return;
                }
                if (error) return Client.error(error);

                $scope.emailForm.$setPristine();

                refreshApp($scope.app.id, function (error) {
                    if (error) return Client.error(error);

                    // when the mailboxName is 'reset', this will fill it up with the default again
                    $scope.email.enableMailbox = $scope.app.enableMailbox;
                    $scope.email.mailboxName = $scope.app.mailboxName || '';
                    $scope.email.mailboxDomain = $scope.domains.filter(function (d) { return d.domain === $scope.app.mailboxDomain; })[0];
                    $scope.email.currentMailboxName = $scope.app.mailboxName || '';
                    $scope.email.currentMailboxDomainName = $scope.email.mailboxDomain.domain;

                    $timeout(function () { $scope.email.busy = false; }, 1000);
                });
            });
        }
    };

    $scope.security = {
        busy: false,
        error: {},
        success: false,

        robotsTxt: '',
        csp: '',

        show: function () {
            $scope.security.error = {};
            $scope.security.robotsTxt = $scope.app.reverseProxyConfig.robotsTxt || '';
            $scope.security.csp = $scope.app.reverseProxyConfig.csp || '';
        },

        submit: function () {
            $scope.security.busy = true;
            $scope.security.error = {};

            var reverseProxyConfig = {
                robotsTxt: $scope.security.robotsTxt || null, // empty string resets
                csp: $scope.security.csp || null // empty string resets
            };

            Client.configureApp($scope.app.id, 'reverse_proxy', reverseProxyConfig, function (error) {
                if (error) return Client.error(error);

                $timeout(function () {
                    $scope.security.success = true;
                    $scope.security.busy = false;
                }, 1000);
            });
        }
    };

    $scope.updates = {
        busy: false,
        busyCheck: false,
        busyUpdate: false,
        busyAutomaticUpdates: false,
        skipBackup: false,

        enableAutomaticUpdate: false,

        show: function () {
            var app = $scope.app;

            $scope.updates.enableAutomaticUpdate = app.enableAutomaticUpdate;
            $scope.updates.skipBackup = !app.enableAutomaticUpdate && !app.enableBackup;
        },

        toggleAutomaticUpdates: function () {
            $scope.updates.busyAutomaticUpdates = true;

            Client.configureApp($scope.app.id, 'automatic_update', { enable: !$scope.updates.enableAutomaticUpdate }, function (error) {
                if (error) return Client.error(error);

                $timeout(function () {
                    $scope.updates.enableAutomaticUpdate = !$scope.updates.enableAutomaticUpdate;
                    $scope.updates.busyAutomaticUpdates = false;
                }, 1000);
            });
        },

        check: function () {
            $scope.updates.busyCheck = true;

            Client.checkForUpdates(function (error) {
                if (error) Client.error(error);

                $scope.updates.busyCheck = false;
            });
        },

        askUpdate: function () {
            $scope.updates.busyUpdate = false;
            $('#updateModal').modal('show');
        },

        confirmUpdate: function () {
            $scope.updates.busyUpdate = true;

            Client.updateApp($scope.app.id, $scope.config.update[$scope.app.id].manifest, { skipBackup: $scope.updates.skipBackup }, function (error) {
                $scope.updates.busyUpdate = false;
                if (error) return Client.error(error);

                $('#updateModal').modal('hide');
                refreshApp($scope.app.id);
            });
        }
    };

    $scope.backups = {
        busy: false,
        busyCreate: false,
        busyAutomaticBackups: false,
        error: {},

        enableBackup: false,
        backups: [],

        createBackup: function () {
            $scope.backups.busyCreate = true;

            Client.backupApp($scope.app.id, function (error) {
                if (error) Client.error(error);

                refreshApp($scope.app.id, function () {
                    $scope.backups.busyCreate = false;

                    waitForAppTask(function (error) {
                        if (error) return Client.error(error);

                        $scope.backups.show(); // refresh backup listing
                    });
                });
            });
        },

        show: function () {
            var app = $scope.app;

            $scope.backups.error = {};
            $scope.backups.enableBackup = app.enableBackup;

            Client.getAppBackups(app.id, function (error, backups) {
                if (error) return Client.error(error);

                $scope.backups.backups = backups;
            });
        },

        toggleAutomaticBackups: function () {
            $scope.backups.busyAutomaticBackups = true;
            $scope.backups.error = {};

            Client.configureApp($scope.app.id, 'automatic_backup', { enable: !$scope.backups.enableBackup }, function (error) {
                if (error) return Client.error(error);

                $timeout(function () {
                    $scope.backups.enableBackup = !$scope.backups.enableBackup;
                    $scope.backups.busyAutomaticBackups = false;
                }, 1000);
            });
        }
    };

    $scope.s3like = function (provider) {
        return provider === 's3' || provider === 'minio' || provider === 's3-v4-compat' ||
               provider === 'exoscale-sos' || provider === 'digitalocean-spaces' ||
               provider === 'scaleway-objectstorage' || provider === 'wasabi' || provider === 'backblaze-b2' ||
               provider === 'linode-objectstorage' || provider === 'ovh-objectstorage' || provider === 'ionos-objectstorage';
    };

    $scope.mountlike = function (provider) {
        return provider === 'sshfs' || provider === 'cifs' || provider === 'nfs';
    };

    $scope.importBackup = {
        busy: false,
        error: {},

        // variables here have to match the import config logic!
        provider: '',
        bucket: '',
        prefix: '',
        mountPoint: '',
        accessKeyId: '',
        secretAccessKey: '',
        gcsKey: { keyFileName: '', content: '' },
        region: '',
        endpoint: '',
        acceptSelfSignedCerts: false,
        format: 'tgz',
        backupId: '',
        password: '',

        clearForm: function () {
            // $scope.importBackup.provider = ''; // do not clear since we call this function on provider change
            $scope.importBackup.bucket = '';
            $scope.importBackup.prefix = '';
            $scope.importBackup.mountPoint = '';
            $scope.importBackup.accessKeyId = '';
            $scope.importBackup.secretAccessKey = '';
            $scope.importBackup.gcsKey.keyFileName = '';
            $scope.importBackup.gcsKey.content = '';
            $scope.importBackup.endpoint = '';
            $scope.importBackup.region = '';
            $scope.importBackup.format = 'tgz';
            $scope.importBackup.acceptSelfSignedCerts = false;
            $scope.importBackup.password = '';
            $scope.importBackup.backupId = '';
        },

        submit: function () {
            $scope.importBackup.error = {};
            $scope.importBackup.busy = true;

            var backupConfig = {
                provider: $scope.importBackup.provider,
            };
            if ($scope.importBackup.password) backupConfig.password = $scope.importBackup.password;

            var backupId = $scope.importBackup.backupId;

            // only set provider specific fields, this will clear them in the db
            if ($scope.s3like(backupConfig.provider)) {
                backupConfig.bucket = $scope.importBackup.bucket;
                backupConfig.prefix = $scope.importBackup.prefix;
                backupConfig.accessKeyId = $scope.importBackup.accessKeyId;
                backupConfig.secretAccessKey = $scope.importBackup.secretAccessKey;

                if ($scope.importBackup.endpoint) backupConfig.endpoint = $scope.importBackup.endpoint;

                if (backupConfig.provider === 's3') {
                    if ($scope.importBackup.region) backupConfig.region = $scope.importBackup.region;
                    delete backupConfig.endpoint;
                } else if (backupConfig.provider === 'minio' || backupConfig.provider === 's3-v4-compat') {
                    backupConfig.region = backupConfig.region || 'us-east-1';
                    backupConfig.acceptSelfSignedCerts = $scope.importBackup.acceptSelfSignedCerts;
                    backupConfig.s3ForcePathStyle = true; // might want to expose this in the UI
                } else if (backupConfig.provider === 'exoscale-sos') {
                    backupConfig.region = 'us-east-1';
                    backupConfig.signatureVersion = 'v4';
                } else if (backupConfig.provider === 'wasabi') {
                    backupConfig.region = $scope.wasabiRegions.find(function (x) { return x.value === $scope.importBackup.endpoint; }).region;
                    backupConfig.signatureVersion = 'v4';
                } else if (backupConfig.provider === 'scaleway-objectstorage') {
                    backupConfig.region = $scope.scalewayRegions.find(function (x) { return x.value === $scope.importBackup.endpoint; }).region;
                    backupConfig.signatureVersion = 'v4';
                } else if (backupConfig.provider === 'linode-objectstorage') {
                    backupConfig.region = $scope.linodeRegions.find(function (x) { return x.value === $scope.importBackup.endpoint; }).region;
                    backupConfig.signatureVersion = 'v4';
                } else if (backupConfig.provider === 'ovh-objectstorage') {
                    backupConfig.region = $scope.ovhRegions.find(function (x) { return x.value === $scope.importBackup.endpoint; }).region;
                    backupConfig.signatureVersion = 'v4';
                } else if (backupConfig.provider === 'ionos-objectstorage') {
                    backupConfig.region = $scope.ionosRegions.find(function (x) { return x.value === $scope.importBackup.endpoint; }).region;
                    backupConfig.signatureVersion = 'v4';
                } else if (backupConfig.provider === 'digitalocean-spaces') {
                    backupConfig.region = 'us-east-1';
                }
            } else if (backupConfig.provider === 'gcs') {
                backupConfig.bucket = $scope.importBackup.bucket;
                backupConfig.prefix = $scope.importBackup.prefix;
                try {
                    var serviceAccountKey = JSON.parse($scope.importBackup.gcsKey.content);
                    backupConfig.projectId = serviceAccountKey.project_id;
                    backupConfig.credentials = {
                        client_email: serviceAccountKey.client_email,
                        private_key: serviceAccountKey.private_key
                    };

                    if (!backupConfig.projectId || !backupConfig.credentials || !backupConfig.credentials.client_email || !backupConfig.credentials.private_key) {
                        throw 'fields_missing';
                    }
                } catch (e) {
                    $scope.importBackup.error.generic = 'Cannot parse Google Service Account Key: ' + e.message;
                    $scope.importBackup.error.gcsKeyInput = true;
                    $scope.importBackup.busy = false;
                    return;
                }
            } else if (backupConfig.provider === 'sshfs' || backupConfig.provider === 'cifs' || backupConfig.provider === 'nfs') {
                backupConfig.mountPoint = $scope.importBackup.mountPoint;
                backupConfig.prefix = $scope.importBackup.prefix;
            } else if (backupConfig.provider === 'filesystem') {
                var parts = backupId.split('/');
                backupId = parts.pop() || parts.pop(); // removes any trailing slash. this is basename()
                backupConfig.backupFolder = parts.join('/'); // this is dirname()
            }

            if ($scope.importBackup.format === 'tgz') {
                if (backupId.substring(backupId.length - '.tar.gz'.length, backupId.length) === '.tar.gz') { // endsWith
                    backupId = backupId.replace(/.tar.gz$/, '');
                } else if (backupId.substring(backupId.length - '.tar.gz.enc'.length, backupId.length) === '.tar.gz.enc') { // endsWith
                    backupId = backupId.replace(/.tar.gz.enc$/, '');
                }
            }

            Client.importBackup($scope.app.id, backupId, $scope.importBackup.format, backupConfig, function (error) {
                if (error) {
                    $scope.importBackup.busy = false;

                    if (error.statusCode === 424) {
                        $scope.importBackup.error.generic = error.message;

                        if (error.message.indexOf('AWS Access Key Id') !== -1) {
                            $scope.importBackup.error.accessKeyId = true;
                            $scope.importBackupForm.accessKeyId.$setPristine();
                            $('#inputImportBackupAccessKeyId').focus();
                        } else if (error.message.indexOf('not match the signature') !== -1 || error.message.indexOf('Signature') !== -1) {
                            $scope.importBackup.error.secretAccessKey = true;
                            $scope.importBackupForm.secretAccessKey.$setPristine();
                            $('#inputImportBackupSecretAccessKey').focus();
                        } else if (error.message.toLowerCase() === 'access denied') {
                            $scope.importBackup.error.accessKeyId = true;
                            $scope.importBackupForm.accessKeyId.$setPristine();
                            $('#inputImportBackupBucket').focus();
                        } else if (error.message.indexOf('ECONNREFUSED') !== -1) {
                            $scope.importBackup.error.generic = 'Unknown region';
                            $scope.importBackup.error.region = true;
                            $scope.importBackupForm.region.$setPristine();
                            $('#inputImportBackupDORegion').focus();
                        } else if (error.message.toLowerCase() === 'wrong region') {
                            $scope.importBackup.error.generic = 'Wrong S3 Region';
                            $scope.importBackup.error.region = true;
                            $scope.importBackupForm.region.$setPristine();
                            $('#inputImportBackupS3Region').focus();
                        } else {
                            $scope.importBackup.error.bucket = true;
                            $('#inputImportBackupBucket').focus();
                            $scope.importBackupForm.bucket.$setPristine();
                        }
                    } else if (error.statusCode === 400) {
                        $scope.importBackup.error.generic = error.message;

                        if ($scope.importBackup.provider === 'filesystem') {
                            $scope.importBackup.error.backupFolder = true;
                        }
                    } else {
                        Client.error(error);
                    }

                    return;
                }

                $('#importBackupModal').modal('hide');

                refreshApp($scope.app.id, function (error) {
                    if (error) return Client.error(error);

                    $timeout(function () { $scope.importBackup.busy = false; }, 1000);
                });
            });
        },

        show: function () {
            $scope.importBackup.clearForm();
            $('#importBackupModal').modal('show');
        },
    };

    $scope.uninstall = {
        busy: false,
        error: {},
        busyRunState: false,
        startButton: false,

        toggleRunState: function () {
            var func  = $scope.app.runState === RSTATES.STOPPED ? Client.startApp : Client.stopApp;
            $scope.uninstall.busyRunState = true;

            func($scope.app.id, function (error) {
                if (error) return Client.error(error);

                refreshApp($scope.app.id, function (error) {
                    if (error) return Client.error(error);

                    $timeout(function () { $scope.uninstall.busyRunState = false; }, 1000);
                });
            });
        },

        show: function () {
            $scope.uninstall.error = {};
        },

        ask: function () {
            $('#uninstallModal').modal('show');
        },

        submit: function () {
            $scope.uninstall.busy = true;

            var NOOP = function (next) { return next(); };
            var stopAppTask = $scope.app.taskId ? Client.stopTask.bind(null, $scope.app.taskId) : NOOP;

            stopAppTask(function () { // ignore error
                Client.uninstallApp($scope.app.id, function (error) {
                    if (error && error.statusCode === 402) { // unpurchase failed
                        Client.error('Relogin to Cloudron App Store');
                    } else if (error) {
                        Client.error(error);
                    } else {
                        $('#uninstallModal').modal('hide');
                        $location.path('/apps');
                    }

                    $scope.uninstall.busy = false;
                });
            });
        }
    };

    $scope.restore = {
        busy: false,
        error: {},

        backup: null,

        show: function (backup) {
            $scope.restore.error = {};
            $scope.restore.backup = backup;

            $('#restoreModal').modal('show');
        },

        submit: function () {
            $scope.restore.busy = true;

            Client.restoreApp($scope.app.id, $scope.restore.backup.id, function (error) {
                if (error) {
                    Client.error(error);
                    $scope.restore.busy = false;
                    return;
                }

                $('#restoreModal').modal('hide');

                refreshApp($scope.app.id);
            });
        }
    };

    $scope.clone = {
        busy: false,
        error: {},

        backup: null,
        location: '',
        domain: null,
        portBindings: {},
        portBindingsInfo: {},
        portBindingsEnabled: {},

        show: function (backup) {
            var app = $scope.app;

            $scope.clone.error = {};
            $scope.clone.backup = backup;
            $scope.clone.domain = $scope.domains.find(function (d) { return app.domain === d.domain; }); // pre-select the app's domain
            $scope.clone.portBindingsInfo = angular.extend({}, app.manifest.tcpPorts, app.manifest.udpPorts);   // Portbinding map only for information
            // set default ports
            for (var env in $scope.clone.portBindingsInfo) {
                $scope.clone.portBindings[env] = $scope.clone.portBindingsInfo[env].defaultValue || 0;
                $scope.clone.portBindingsEnabled[env] = true;
            }

            $('#cloneModal').modal('show');
        },

        submit: function () {
            $scope.clone.busy = true;

            // only use enabled ports from portBindings
            var finalPortBindings = {};
            for (var env in $scope.clone.portBindings) {
                if ($scope.clone.portBindingsEnabled[env]) {
                    finalPortBindings[env] = $scope.clone.portBindings[env];
                }
            }

            var data = {
                location: $scope.clone.location,
                domain: $scope.clone.domain.domain,
                portBindings: finalPortBindings,
                backupId: $scope.clone.backup.id
            };

            Client.checkDNSRecords(data.domain, data.location, function (error, result) {
                if (error) {
                    Client.error(error);
                    $scope.clone.busy = false;
                    return;
                }
                if (result.error) {
                    if (result.error.reason === ERROR.ACCESS_DENIED) {
                        $scope.clone.error.location = 'DNS credentials for ' + data.domain + ' are invalid. Update it in Domains & Certs view';
                    } else {
                        $scope.clone.error.location = result.error.message;
                    }
                    $scope.clone.needsOverwrite = true;
                    $scope.clone.busy = false;
                    return;
                }
                if (result.needsOverwrite) {
                    $scope.clone.error.location = 'DNS Record already exists. Confirm that the domain is not in use for services external to Cloudron';
                    $scope.clone.needsOverwrite = true;
                    $scope.clone.busy = false;
                    return;
                }

                Client.cloneApp($scope.app.id, data, function (error/*, clonedApp */) {
                    $scope.clone.busy = false;

                    if (error) {
                        if (error.statusCode === 409) {
                            if (error.portName) {
                                $scope.clone.error.port = error.message;
                            } else if (error.domain) {
                                $scope.clone.error.location = 'This location is already taken.';
                                $('#cloneLocationInput').focus();
                            } else {
                                Client.error(error);
                            }
                        } else {
                            Client.error(error);
                        }
                        return;
                    }

                    $('#cloneModal').modal('hide');

                    $location.path('/apps');
                });
            });
        }
    };

    $scope.repair = {
        retryBusy: false,
        error: {},

        location: null,
        domain: null,
        alternateDomains: [],
        aliasDomains: [],
        backups: [],

        backupId: '',

        show: function () {},

        // this prepares the repair dialog with whatever is required for repair action
        confirm: function () {
            $scope.repair.error = {};
            $scope.repair.retryBusy = false;
            $scope.repair.location = null;
            $scope.repair.domain = null;
            $scope.repair.alternateDomains = [];
            $scope.repair.aliasDomains = [];
            $scope.repair.backupId = '';

            var app = $scope.app;

            var errorState = ($scope.app.error && $scope.app.error.installationState) || ISTATES.PENDING_CONFIGURE;

            if (errorState === ISTATES.PENDING_LOCATION_CHANGE) {
                $scope.repair.location = app.location;
                $scope.repair.domain = $scope.domains.filter(function (d) { return d.domain === app.domain; })[0];

                $scope.repair.aliasDomains = $scope.app.aliasDomains;
                $scope.repair.aliasDomains = $scope.app.aliasDomains.map(function (aliasDomain) {
                    return {
                        subdomain: aliasDomain.subdomain,
                        enabled: true,
                        domain: $scope.domains.filter(function (d) { return d.domain === aliasDomain.domain; })[0]
                    };
                });

                $scope.repair.alternateDomains = $scope.app.alternateDomains;
                $scope.repair.alternateDomains = $scope.app.alternateDomains.map(function (altDomain) {
                    return {
                        subdomain: altDomain.subdomain,
                        enabled: true,
                        domain: $scope.domains.filter(function (d) { return d.domain === altDomain.domain; })[0]
                    };
                });
            }

            if (errorState === ISTATES.PENDING_RESTORE) {
                Client.getAppBackups($scope.app.id, function (error, backups) {
                    if (error) return Client.error(error);

                    $scope.repair.backups = backups;
                    $scope.repair.backupId = '';

                    $('#repairModal').modal('show');
                });
                return;
            }

            $('#repairModal').modal('show');
        },

        submit: function () {
            $scope.repair.error = {};
            $scope.repair.retryBusy = true;

            var errorState = ($scope.app.error && $scope.app.error.installationState) || ISTATES.PENDING_CONFIGURE;
            var data = {};
            var repairFunc;

            switch (errorState) {
            case ISTATES.PENDING_INSTALL:
            case ISTATES.PENDING_CLONE: // if manifest or bad image, use CLI to provide new manifest
                repairFunc = Client.repairApp.bind(null, $scope.app.id, {}); // this will trigger a re-install
                break;

            case ISTATES.PENDING_LOCATION_CHANGE:
                data.location = $scope.repair.location;
                data.domain = $scope.repair.domain.domain;
                data.aliasDomains = $scope.repair.aliasDomains.filter(function (a) { return a.enabled; })
                    .map(function (d) { return { subdomain: d.subdomain, domain: d.domain.domain }; });
                data.alternateDomains = $scope.repair.alternateDomains.filter(function (a) { return a.enabled; })
                    .map(function (d) { return { subdomain: d.subdomain, domain: d.domain.domain }; });
                data.overwriteDns = true; // always overwriteDns. user can anyway check and uncheck above
                repairFunc = Client.configureApp.bind(null, $scope.app.id, 'location', data);
                break;

            case ISTATES.PENDING_DATA_DIR_MIGRATION:
                repairFunc = Client.configureApp.bind(null, $scope.app.id, 'data_dir', { dataDir: null });
                break;

            // this also happens for import faliures. this UI can only show backup listing. use CLI for arbit id/config
            case ISTATES.PENDING_RESTORE:
                repairFunc = Client.restoreApp.bind(null, $scope.app.id, $scope.repair.backupId);
                break;

            case ISTATES.PENDING_UNINSTALL:
                repairFunc = Client.uninstallApp.bind(null, $scope.app.id);
                break;

            case ISTATES.PENDING_START:
            case ISTATES.PENDING_STOP:
            case ISTATES.PENDING_RESTART:
            case ISTATES.PENDING_RESIZE:
            case ISTATES.PENDING_DEBUG:
            case ISTATES.PENDING_RECREATE_CONTAINER:
            case ISTATES.PENDING_CONFIGURE:
            case ISTATES.PENDING_BACKUP: // can happen if the backup task was killed/rebooted
            case ISTATES.PENDING_UPDATE: // when update failed, just bring it back to current state and user can click update again
            default:
                repairFunc = Client.repairApp.bind(null, $scope.app.id, {});
                break;
            }

            repairFunc(function (error) {
                $scope.repair.retryBusy = false;
                if (error) return Client.error(error);

                $scope.repair.retryBusy = false;
                $('#repairModal').modal('hide');
            });
        },

        restartBusy: false,
        restartApp: function () {
            $scope.repair.restartBusy = true;

            Client.restartApp($scope.app.id, function (error) {
                if (error) return console.error(error);

                refreshApp($scope.app.id, function (error) {
                    if (error) return Client.error(error);

                    $timeout(function () { $scope.repair.restartBusy = false; }, 1000);
                });
            });
        },

        pauseBusy: false,

        pauseAppBegin: function () {
            $scope.repair.pauseBusy = true;

            Client.debugApp($scope.app.id, true, function (error) {
                if (error) return console.error(error);

                refreshApp($scope.app.id, function (error) {
                    if (error) return Client.error(error);

                    $timeout(function () { $scope.repair.pauseBusy = false; }, 1000);
                });
            });
        },

        pauseAppDone: function () {
            $scope.repair.pauseBusy = true;

            Client.debugApp($scope.app.id, false, function (error) {
                if (error) return console.error(error);

                refreshApp($scope.app.id, function (error) {
                    if (error) return Client.error(error);

                    $timeout(function () { $scope.repair.pauseBusy = false; }, 1000);
                });
            });
        }

    };

    function fetchUsers(callback) {
        Client.getUsers(function (error, users) {
            if (error) return callback(error);

            // ensure we have something to work with in the access restriction dropdowns
            users.forEach(function (user) { user.display = user.username || user.email; });

            $scope.users = users;

            callback();
        });
    }

    function fetchGroups(callback) {
        Client.getGroups(function (error, groups) {
            if (error) return callback(error);

            $scope.groups = groups;

            callback();
        });
    }

    function getDomains(callback) {
        Client.getDomains(function (error, result) {
            if (error) return callback(error);

            $scope.domains = result;

            callback();
        });
    }

    function getVolumes(callback) {
        Client.getVolumes(function (error, result) {
            if (error) return callback(error);

            $scope.volumes = result;

            callback();
        });
    }

    function getBackupConfig(callback) {
        Client.getBackupConfig(function (error, backupConfig) {
            if (error) return callback(error);

            $scope.backupConfig = backupConfig;

            callback();
        });
    }

    function refreshApp(appId, callback) {
        callback = callback || function () {};

        Client.getAppWithTask(appId, function (error, app) {
            if (error && error.statusCode === 404) return $location.path('/apps');
            if (error) return callback(error);

            $scope.app = app;

            // show 'Start App' if app is starting or is stopped
            if (app.installationState === ISTATES.PENDING_START || app.installationState === ISTATES.PENDING_STOP) {
                $scope.uninstall.startButton = app.installationState === ISTATES.PENDING_START;
            } else {
                $scope.uninstall.startButton = app.runState === RSTATES.STOPPED;
            }

            callback();
        });
    }

    function waitForAppTask(callback) {
        callback = callback || function () {};

        if (!$scope.app.taskId) return callback();

        // app will be refreshed on interval
        $timeout(waitForAppTask.bind(null, callback), 2000); // not yet done
    }

    // https://stackoverflow.com/questions/3665115/how-to-create-a-file-in-memory-for-user-to-download-but-not-through-server#18197341
    function download(filename, text) {
        var element = document.createElement('a');
        element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
        element.setAttribute('download', filename);

        element.style.display = 'none';
        document.body.appendChild(element);

        element.click();

        document.body.removeChild(element);
    }

    $scope.downloadConfig = function (backup) {
        // secrets and tokens already come with placeholder characters we remove them
        var tmp = {
            backupId: backup.id,
            encrypted: !!$scope.backupConfig.password  // we add this just to help the import UI
        };

        Object.keys($scope.backupConfig).forEach(function (k) {
            if ($scope.backupConfig[k] !== SECRET_PLACEHOLDER) tmp[k] = $scope.backupConfig[k];
        });

        var filename = 'app-backup-config-' + (new Date()).toISOString().replace(/:|T/g,'-').replace(/\..*/,'') + ' (' + $scope.app.fqdn + ')' + '.json';
        download(filename, JSON.stringify(tmp));
    };

    document.getElementById('backupConfigFileInput').onchange = function (event) {
        var reader = new FileReader();
        reader.onload = function (result) {
            if (!result.target || !result.target.result) return console.error('Unable to read backup config');

            var backupConfig;
            try {
                backupConfig = JSON.parse(result.target.result);
                if (backupConfig.provider === 'filesystem') { // patch the backupId to have the full path
                    backupConfig.backupId = backupConfig.backupFolder + '/' + backupConfig.backupId;
                    delete backupConfig.backupFolder;
                }
            } catch (e) {
                console.error('Unable to parse backup config');
                return;
            }

            $scope.$apply(function () {
                // we assume property names match here, this does not yet work for gcs keys
                Object.keys(backupConfig).forEach(function (k) {
                    if (k in $scope.importBackup) {
                        $scope.importBackup[k] = backupConfig[k];
                    }
                });
            });
        };
        reader.readAsText(event.target.files[0]);
    };

    Client.onReady(function () {
        refreshApp(appId, function (error) {
            if (error) return Client.error(error);

            // skipViewShow because we don't have all the values like domains/users to init the view yet
            if ($routeParams.view) { // explicit route in url bar
                $scope.setView($routeParams.view, true /* skipViewShow */);
            } else { // default
                $scope.setView($scope.app.error ? 'repair' : 'display', true /* skipViewShow */);
            }

            async.series([
                fetchUsers,
                fetchGroups,
                getDomains,
                getVolumes,
                getBackupConfig
            ], function (error) {
                if (error) return Client.error(error);

                $scope[$scope.view].show(); // initialize now that we have all the values

                var refreshTimer = $interval(function () { refreshApp($scope.app.id); }, 5000); // call with inline function to avoid iteration argument passed see $interval docs
                $scope.$on('$destroy', function () {
                    $interval.cancel(refreshTimer);
                });
            });
        });
    });

    $('#iconFileInput').get(0).onchange = function (event) {
        var fr = new FileReader();
        fr.onload = function () {
            $scope.$apply(function () {
                // var file = event.target.files[0];
                $scope.display.icon.data = fr.result;
            });
        };
        fr.readAsDataURL(event.target.files[0]);
    };

    // setup all the dialog focus handling
    ['appUninstallModal', 'appUpdateModal', 'appRestoreModal'].forEach(function (id) {
        $('#' + id).on('shown.bs.modal', function () {
            $(this).find('[autofocus]:first').focus();
        });
    });

    var clipboard = new Clipboard('.clipboard');
    clipboard.on('success', function () {
        $scope.$apply(function () { $scope.copyBackupIdDone = true; });
        $timeout(function () { $scope.copyBackupIdDone = false; }, 5000);
    });

    $('.modal-backdrop').remove();
}]);
