'use strict';

/* global angular:false */
/* global $:false */
/* global ERROR */
/* global RSTATES */
/* global moment */

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

    $scope.HOST_PORT_MIN = 1024;
    $scope.HOST_PORT_MAX = 65535;

    $scope.ready = false;
    $scope.apps = [];
    $scope.popularApps = [];
    $scope.config = Client.getConfig();
    $scope.user = Client.getUserInfo();
    $scope.users = [];
    $scope.groups = [];
    $scope.domains = [];
    $scope.category = '';
    $scope.cachedCategory = ''; // used to cache the selected category while searching
    $scope.searchString = '';
    $scope.validSubscription = false;
    $scope.unstableApps = false;
    $scope.subscription = {};
    $scope.memory = null; // { memory, swap }

    $scope.showView = function (view) {
        // wait for dialog to be fully closed to avoid modal behavior breakage when moving to a different view already
        $('.modal').on('hidden.bs.modal', function () {
            $scope.$apply(function () {
                $scope.appInstall.reset();
                $('.modal').off('hidden.bs.modal');
                $location.path(view);
            });
        });

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

    // If new categories added make sure the translation below exists
    $scope.categories = [
        { id: 'analytics', icon: 'fa fa-chart-line', label: 'Analytics'},
        { id: 'blog', icon: 'fa fa-font', label: 'Blog'},
        { id: 'chat', icon: 'fa fa-comments', label: 'Chat'},
        { id: 'git', icon: 'fa fa-code-branch', label: 'Code Hosting'},
        { id: 'crm', icon: 'fab fa-connectdevelop', label: 'CRM'},
        { id: 'document', icon: 'fa fa-file-word', label: 'Documents'},
        { id: 'email', icon: 'fa fa-envelope', label: 'Email'},
        { id: 'federated', icon: 'fa fa-project-diagram', label: 'Federated'},
        { id: 'sync', icon: 'fa fa-sync-alt', label: 'File Sync'},
        { id: 'finance', icon: 'fa fa-hand-holding-usd', label: 'Finance'},
        { id: 'forum', icon: 'fa fa-users', label: 'Forum'},
        { id: 'gallery', icon: 'fa fa-images', label: 'Gallery'},
        { id: 'game', icon: 'fa fa-gamepad', label: 'Games'},
        { id: 'learning', icon: 'fas fa-graduation-cap', label: 'Learning'},
        { id: 'media', icon: 'fas fa-photo-video', label: 'Media'},
        { id: 'notes', icon: 'fa fa-sticky-note', label: 'Notes'},
        { id: 'project', icon: 'fas fa-project-diagram', label: 'Project Management'},
        { id: 'vpn', icon: 'fa fa-user-secret', label: 'VPN'},
        { id: 'hosting', icon: 'fa fa-server', label: 'Web Hosting'},
        { id: 'wiki', icon: 'fab fa-wikipedia-w', label: 'Wiki'},
    ];

    // Translation IDs are generated as "appstore.category.<categoryId>"
    $translate($scope.categories.map(function (c) { return 'appstore.category.' + c.id; })).then(function (tr) {
        Object.keys(tr).forEach(function (key) {
            if (key === tr[key]) return; // missing translation use default label

            var category = $scope.categories.find(function (c) { return key.endsWith(c.id); });
            if (category) category.label = tr[key];
        });
    });

    $scope.categoryButtonLabel = function (category) {
        var categoryLabel = $translate.instant('appstore.categoryLabel')

        if (category === 'new') return categoryLabel;
        if (category === 'recent') return categoryLabel;

        var tmp = $scope.categories.find(function (c) { return c.id === category; });
        if (tmp) return tmp.label;

        return categoryLabel;
    };

    $scope.appInstall = {
        busy: false,
        state: 'appInfo',
        error: {},
        app: {},
        needsOverwrite: false,
        location: '',
        domain: null,
        portBindings: {},
        mediaLinks: [],
        certificateFile: null,
        certificateFileName: '',
        keyFile: null,
        keyFileName: '',
        accessRestrictionOption: '',
        accessRestriction: { users: [], groups: [] },
        customAuth: false,
        optionalSso: false,
        subscriptionErrorMesssage: '',

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

        reset: function () {
            $scope.appInstall.app = {};
            $scope.appInstall.error = {};
            $scope.appInstall.needsOverwrite = false;
            $scope.appInstall.location = '';
            $scope.appInstall.domain = null;
            $scope.appInstall.portBindings = {};
            $scope.appInstall.state = 'appInfo';
            $scope.appInstall.mediaLinks = [];
            $scope.appInstall.certificateFile = null;
            $scope.appInstall.certificateFileName = '';
            $scope.appInstall.keyFile = null;
            $scope.appInstall.keyFileName = '';
            $scope.appInstall.accessRestrictionOption = '';
            $scope.appInstall.accessRestriction = { users: [], groups: [] };
            $scope.appInstall.optionalSso = false;
            $scope.appInstall.customAuth = false;
            $scope.appInstall.subscriptionErrorMesssage = '';

            $('#collapseInstallForm').collapse('hide');
            $('#collapseResourceConstraint').collapse('hide');
            $('#collapseSubscriptionRequired').collapse('hide');
            $('#collapseMediaLinksCarousel').collapse('show');

            if ($scope.appInstallForm) {
                $scope.appInstallForm.$setPristine();
                $scope.appInstallForm.$setUntouched();
            }
        },

        showForm: function (force) {
            var app = $scope.appInstall.app;
            var DEFAULT_MEMORY_LIMIT = 1024 * 1024 * 256;

            var needed = app.manifest.memoryLimit || DEFAULT_MEMORY_LIMIT; // RAM+Swap
            var used = Client.getInstalledApps().reduce(function (prev, cur) {
                if (cur.runState === RSTATES.STOPPED) return prev;

                return prev + (cur.memoryLimit || cur.manifest.memoryLimit || DEFAULT_MEMORY_LIMIT);
            }, 0);
            var totalMemory = ($scope.memory.memory + $scope.memory.swap) * 1.5;
            var available = (totalMemory || 0) - used;

            var enoughResourcesAvailable = (available - needed) >= 0;

            if (enoughResourcesAvailable || force) {
                $scope.appInstall.state = 'installForm';
                $('#collapseMediaLinksCarousel').collapse('hide');
                $('#collapseResourceConstraint').collapse('hide');
                $('#collapseInstallForm').collapse('show');
                $('#appInstallLocationInput').focus();
            } else {
                $scope.appInstall.state = 'resourceConstraint';
                $('#collapseMediaLinksCarousel').collapse('hide');
                $('#collapseResourceConstraint').collapse('show');
            }
        },

        show: function (app) { // this is an appstore app object!
            $scope.appInstall.reset();

            // make a copy to work with in case the app object gets updated while polling
            angular.copy(app, $scope.appInstall.app);

            $scope.appInstall.mediaLinks = $scope.appInstall.app.manifest.mediaLinks || [];
            $scope.appInstall.domain = $scope.domains.find(function (d) { return $scope.config.adminDomain === d.domain; }); // pre-select the adminDomain
            $scope.appInstall.portBindingsInfo = angular.extend({}, $scope.appInstall.app.manifest.tcpPorts, $scope.appInstall.app.manifest.udpPorts);   // Portbinding map only for information
            $scope.appInstall.portBindings = {};                            // This is the actual model holding the env:port pair
            $scope.appInstall.portBindingsEnabled = {};                     // This is the actual model holding the enabled/disabled flag

            var manifest = app.manifest;
            $scope.appInstall.optionalSso = !!manifest.optionalSso;
            $scope.appInstall.customAuth = !(manifest.addons['ldap'] || manifest.addons['proxyAuth']);

            $scope.appInstall.accessRestrictionOption = $scope.groups.length ? '' : 'any'; // make the user select an ACL conciously if groups are used
            $scope.appInstall.accessRestriction = { users: [], groups: [] };

            // set default ports
            var allPorts = angular.extend({}, $scope.appInstall.app.manifest.tcpPorts, $scope.appInstall.app.manifest.udpPorts);
            for (var env in allPorts) {
                $scope.appInstall.portBindings[env] = allPorts[env].defaultValue || 0;
                $scope.appInstall.portBindingsEnabled[env] = true;
            }

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

        submit: function () {
            $scope.appInstall.busy = true;
            $scope.appInstall.error.other = null;
            $scope.appInstall.error.location = null;
            $scope.appInstall.error.port = null;

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

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

            var data = {
                overwriteDns: $scope.appInstall.needsOverwrite,
                location: $scope.appInstall.location || '',
                domain: $scope.appInstall.domain.domain,
                portBindings: finalPortBindings,
                accessRestriction: finalAccessRestriction,
                cert: $scope.appInstall.certificateFile,
                key: $scope.appInstall.keyFile,
                sso: !$scope.appInstall.optionalSso ? undefined : ($scope.appInstall.accessRestrictionOption !== 'nosso')
            };

            Client.checkDNSRecords(data.domain, data.location, function (error, result) {
                if (error) return Client.error(error);

                if (!data.overwriteDns) {
                    if (result.error || result.needsOverwrite) {
                        if (result.error) {
                            if (result.error.reason === ERROR.ACCESS_DENIED) {
                                $scope.appInstall.error.location = 'DNS credentials for ' + data.domain + ' are invalid. Update it in Domains & Certs view';
                            } else {
                                $scope.appInstall.error.location = result.error.message;
                            }
                        } else {
                            $scope.appInstall.error.location = 'DNS Record already exists. Confirm that the domain is not in use for services external to Cloudron';
                            $scope.appInstall.needsOverwrite = true;
                        }
                        $scope.appInstall.busy = false;
                        $scope.appInstallForm.location.$setPristine();
                        $('#appInstallLocationInput').focus();
                        return;
                    }
                }

                Client.installApp($scope.appInstall.app.id, $scope.appInstall.app.manifest, $scope.appInstall.app.title, data, function (error, newAppId) {
                    if (error) {
                        if (error.statusCode === 402) {
                            $scope.appInstall.state = 'subscriptionRequired';
                            $scope.appInstall.subscriptionErrorMesssage = error.message;
                            $('#collapseMediaLinksCarousel').collapse('hide');
                            $('#collapseResourceConstraint').collapse('hide');
                            $('#collapseInstallForm').collapse('hide');
                            $('#collapseSubscriptionRequired').collapse('show');
                        } else if (error.statusCode === 409) {
                            if (error.portName) {
                                $scope.appInstall.error.port = error.message;
                            } else if (error.domain) {
                                $scope.appInstall.error.location = error.message;
                                $scope.appInstallForm.location.$setPristine();
                                $('#appInstallLocationInput').focus();
                            } else {
                                $scope.appInstall.error.other = error.message;
                            }
                        } else if (error.statusCode === 400) {
                            if (error.field === 'cert') {
                                $scope.appInstall.error.cert = error.message;
                                $scope.appInstall.certificateFileName = '';
                                $scope.appInstall.certificateFile = null;
                                $scope.appInstall.keyFileName = '';
                                $scope.appInstall.keyFile = null;
                            } else {
                                $scope.appInstall.error.other = error.message;
                            }
                        } else {
                            $scope.appInstall.error.other = error.message;
                        }

                        $scope.appInstall.busy = false;
                        return;
                    }

                    $scope.appInstall.busy = false;

                    // stash new app id for later
                    $scope.appInstall.app.id = newAppId;

                    // we track the postinstall confirmation for the current user's browser
                    // TODO later we might want to have a notification db to track the state across admins and browsers
                    if ($scope.appInstall.app.manifest.postInstallMessage) {
                        localStorage['confirmPostInstall_' + $scope.appInstall.app.id] = true;
                    }

                    // wait for dialog to be fully closed to avoid modal behavior breakage when moving to a different view already
                    $('#appInstallModal').on('hidden.bs.modal', function () {
                        $scope.$apply(function () {
                            $location.path('/apps').search({ });
                        });
                    });

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

    $scope.appNotFound = {
        appId: '',
        version: ''
    };

    $scope.appstoreLogin = {
        busy: false,
        error: {},
        email: '',
        password: '',
        totpToken: '',
        register: true,
        termsAccepted: false,
        purpose: '',

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

            Client.registerCloudron($scope.appstoreLogin.email, $scope.appstoreLogin.password, $scope.appstoreLogin.totpToken, $scope.appstoreLogin.register, $scope.appstoreLogin.purpose, function (error) {
                if (error) {
                    $scope.appstoreLogin.busy = false;

                    if (error.statusCode === 409) {
                        $scope.appstoreLogin.error.email = 'An account with this email already exists';
                        $scope.appstoreLogin.password = '';
                        $scope.appstoreLoginForm.email.$setPristine();
                        $scope.appstoreLoginForm.password.$setPristine();
                        $('#inputAppstoreLoginEmail').focus();
                    } else if (error.statusCode === 412) {
                        if (error.message.indexOf('TOTP token missing') !== -1) {
                            $scope.appstoreLogin.error.totpToken = 'A 2FA token is required';
                            setTimeout(function () { $('#inputAppstoreLoginTotpToken').focus(); }, 0);
                        } else if (error.message.indexOf('TOTP token invalid') !== -1) {
                            $scope.appstoreLogin.error.totpToken = 'Wrong 2FA token';
                            $scope.appstoreLogin.totpToken = '';
                            setTimeout(function () { $('#inputAppstoreLoginTotpToken').focus(); }, 0);
                        } else {
                            $scope.appstoreLogin.error.password = 'Wrong email or password';
                            $scope.appstoreLogin.password = '';
                            $('#inputAppstoreLoginPassword').focus();
                            $scope.appstoreLoginForm.password.$setPristine();
                        }
                    } else if (error.statusCode === 424) {
                        if (error.message === 'wrong user') {
                            $scope.appstoreLogin.error.generic = 'Wrong cloudron.io account';
                            $scope.appstoreLogin.email = '';
                            $scope.appstoreLogin.password = '';
                            $scope.appstoreLoginForm.email.$setPristine();
                            $scope.appstoreLoginForm.password.$setPristine();
                            $('#inputAppstoreLoginEmail').focus();
                        } else {
                            console.error(error);
                            $scope.appstoreLogin.error.generic = error.message;
                        }
                    } else {
                        console.error(error);
                        $scope.appstoreLogin.error.generic = error.message || 'Please retry later';
                    }

                    return;
                }

                getSubscription(function (error) {
                    if (error) return console.error(error);

                    onSubscribed(function (error) { if (error) console.error(error); });
                });
            });
        }
    };

    function onSubscribed(callback) {
        Client.getAppstoreApps(function (error) {
            if (error) return callback(error);

            // start with all apps listing. this also sets $scope.apps accordingly
            $scope.showCategory('');

            // do this in background
            fetchUsers();
            fetchGroups();

            // domains is required since we populate the dropdown with domains[0]
            Client.getDomains(function (error, result) {
                if (error) return callback(error);

                $scope.domains = result;

                // show install app dialog immediately if an app id was passed in the query
                // hashChangeListener calls $apply, so make sure we don't double digest here
                setTimeout(hashChangeListener, 1);

                setTimeout(function () { $('#appstoreSearch').focus(); }, 1);

                callback();
            });
        });
    }

    // TODO does not support testing apps in search
    $scope.search = function () {
        if (!$scope.searchString) return $scope.showCategory($scope.cachedCategory);

        $scope.category = '';

        Client.getAppstoreAppsFast(function (error, apps) {
            if (error) return $timeout($scope.search, 1000);

            var token = $scope.searchString.toUpperCase();

            $scope.popularApps = [];
            $scope.apps = apps.filter(function (app) {
                if (app.manifest.id.toUpperCase().indexOf(token) !== -1) return true;
                if (app.manifest.title.toUpperCase().indexOf(token) !== -1) return true;
                if (app.manifest.tagline.toUpperCase().indexOf(token) !== -1) return true;
                if (app.manifest.tags.join().toUpperCase().indexOf(token) !== -1) return true;
                if (app.manifest.description.toUpperCase().indexOf(token) !== -1) return true;
                return false;
                });
        });
    };

    function filterForNewApps(apps) {
        var minApps = apps.length < 12 ? apps.length : 12; // prevent endless loop
        var tmp = [];
        var i = 0;

        do {
            var offset = moment().subtract(i++, 'days');
            tmp = apps.filter(function (app) { return moment(app.ts).isAfter(offset); }); // ts here is from appstore's apps table
        } while(tmp.length < minApps);

        return tmp;
    }

    function filterForRecentlyUpdatedApps(apps) {
        var minApps = apps.length < 12 ? apps.length : 12; // prevent endless loop
        var tmp = [];
        var i = 0;

        do {
            var offset = moment().subtract(i++, 'days');
            tmp = apps.filter(function (app) { return moment(app.creationDate).isAfter(offset); }); // creationDate here is from appstore's appversions table
        } while(tmp.length < minApps);

        return tmp;
    }

    $scope.showCategory = function (category) {
        $scope.category = category;

        $scope.cachedCategory = $scope.category;

        Client.getAppstoreAppsFast(function (error, apps) {
            if (error) return $timeout($scope.showCategory.bind(null, category), 1000);

            if (!$scope.category) {
                $scope.apps = apps.slice(0).filter(function (app) { return !app.featured; }).sort(function (a1, a2) { return a1.manifest.title.localeCompare(a2.manifest.title); });
                $scope.popularApps = apps.slice(0).filter(function (app) { return app.featured; }).sort(function (a1, a2) { return a2.ranking - a1.ranking; });
            } else if ($scope.category === 'new') {
                $scope.apps = filterForNewApps(apps);
            } else if ($scope.category === 'recent') {
                $scope.apps = filterForRecentlyUpdatedApps(apps);
            } else {
                $scope.apps = apps.filter(function (app) {
                    return app.manifest.tags.some(function (tag) { return $scope.category === tag; }); // reverse sort;
                }).sort(function (a1, a2) { return a2.ranking - a1.ranking; });
            }

            // ensure we scroll to top
            document.getElementById('ng-view').scrollTop = 0;
        });
    };

    $scope.openSubscriptionSetup = function () {
        Client.getSubscription(function (error, subscription) {
            if (error) return console.error('Unable to get subscription.', error);

            Client.openSubscriptionSetup(subscription);
        });
    };

    document.getElementById('appInstallCertificateFileInput').onchange = function (event) {
        $scope.$apply(function () {
            $scope.appInstall.certificateFile = null;
            $scope.appInstall.certificateFileName = event.target.files[0].name;

            var reader = new FileReader();
            reader.onload = function (result) {
                if (!result.target || !result.target.result) return console.error('Unable to read local file');
                $scope.appInstall.certificateFile = result.target.result;
            };
            reader.readAsText(event.target.files[0]);
        });
    };

    document.getElementById('appInstallKeyFileInput').onchange = function (event) {
        $scope.$apply(function () {
            $scope.appInstall.keyFile = null;
            $scope.appInstall.keyFileName = event.target.files[0].name;

            var reader = new FileReader();
            reader.onload = function (result) {
                if (!result.target || !result.target.result) return console.error('Unable to read local file');
                $scope.appInstall.keyFile = result.target.result;
            };
            reader.readAsText(event.target.files[0]);
        });
    };

    $scope.showAppNotFound = function (appId, version) {
        $scope.appNotFound.appId = appId;
        $scope.appNotFound.version = version || 'latest';

        $('#appNotFoundModal').modal('show');
    };

    $scope.gotoApp = function (app) {
        $location.path('/appstore/' + app.manifest.id, false).search({ version : app.manifest.version });
    };

    function hashChangeListener() {
        // event listener is called from DOM not angular, need to use $apply
        $scope.$apply(function () {
            var appId = $location.path().slice('/appstore/'.length);
            var version = $location.search().version;

            if (appId) {
                Client.getAppstoreAppByIdAndVersion(appId, version || 'latest', function (error, result) {
                    if (error) {
                        $scope.showAppNotFound(appId, version);
                        console.error(error);
                        return;
                    }

                    $scope.appInstall.show(result);
                });
            } else {
                $scope.appInstall.reset();
            }
        });
    }

    function fetchUsers() {
        Client.getUsers(function (error, users) {
            if (error) {
                console.error(error);
                return $timeout(fetchUsers, 5000);
            }

            $scope.users = users;
        });
    }

    function fetchGroups() {
        Client.getGroups(function (error, groups) {
            if (error) {
                console.error(error);
                return $timeout(fetchGroups, 5000);
            }

            $scope.groups = groups;
        });
    }

    function getSubscription(callback) {
        Client.getSubscription(function (error, subscription) {
            if (error) {
                if (error.statusCode === 412) { // not registered yet
                    $scope.validSubscription = false;
                } else if (error.statusCode === 402) { // invalid token, license error
                    $scope.validSubscription = false;
                } else { // 424/external error?
                    return callback(error);
                }
            } else {
                $scope.validSubscription = true;
                $scope.subscription = subscription;
            }

            // clear busy state when a login/signup was performed
            $scope.appstoreLogin.busy = false;

            // also update the root controller status
            if ($scope.$parent) $scope.$parent.updateSubscriptionStatus();

            callback();
        });
    }

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

            $scope.memory = memory;

            callback();
        });
    }

    function init() {
        $scope.ready = false;

        getSubscription(function (error) {
            if (error) {
                console.error(error);
                return $timeout(init, 1000);
            }

            if (!$scope.validSubscription) { // show the login form
                $scope.ready = true;
                return;
            }

            onSubscribed(function (error) {
                if (error) console.error(error);

                $scope.ready = true;
            });
        });
    }

    Client.onReady(function () {
        getMemory(function () {
            init();
        });
    });

    // note: do not use hide.bs.model since it is called immediately from switchToAppsView which is already in angular scope
    $('#appInstallModal').on('hidden.bs.modal', function () {
        // clear the appid and version in the search bar when dialog is cancelled
        $scope.$apply(function () {
            $location.path('/appstore', false).search({ }); // 'false' means do not reload
        });
    });

    window.addEventListener('hashchange', hashChangeListener);

    $scope.$on('$destroy', function handler() {
        window.removeEventListener('hashchange', hashChangeListener);
    });

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

    // autofocus if appstore login is shown
    $scope.$watch('validSubscription', function (newValue/*, oldValue */) {
        if (!newValue) setTimeout(function () { $('[name=appstoreLoginForm]').find('[autofocus]:first').focus(); }, 1000);
    });

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