text: import * as URI from 'urijs';
  import * as angular from 'angular';
  const templateUrl = require('./config-setup-tool.html');
  const urlParsedField =  require('../config-field-templates/config-parsed-field.html');
  const urlVarField = require('../config-field-templates/config-variable-field.html');
  const urlListField = require('../config-field-templates/config-list-field.html');
  const urlFileField = require('../config-field-templates/config-file-field.html');
  const urlBoolField = require('../config-field-templates/config-bool-field.html');
  const urlNumericField = require('../config-field-templates/config-numeric-field.html');
  const urlContactField = require('../config-field-templates/config-contact-field.html');
  const urlContactsField = require('../config-field-templates/config-contacts-field.html');
  const urlMapField = require('../config-field-templates/config-map-field.html');
  const urlServiceKeyField = require('../config-field-templates/config-service-key-field.html');
  const urlStringField = require('../config-field-templates/config-string-field.html');
  const urlPasswordField = require('../config-field-templates/config-password-field.html');
  
  const urlStringListField = require('../config-field-templates/config-string-list-field.html');
  const urlCertField = require('../config-field-templates/config-certificates-field.html');
  
  
  angular.module("quay-config")
    .directive('configSetupTool', () => {
      var directiveDefinitionObject = {
        priority: 1,
        templateUrl,
        replace: true,
        transclude: true,
        restrict: 'C',
        scope: {
          'isActive': '=isActive',
          'configurationSaved': '&configurationSaved',
          'setupCompleted': '&setupCompleted',
        },
        controller: function($rootScope, $scope, $element, $timeout, ApiService) {
          var authPassword = null;
  
          $scope.HOSTNAME_REGEX = '^[a-zA-Z-0-9\.]+(:[0-9]+)?$';
          $scope.GITHOST_REGEX = '^https?://([a-zA-Z0-9]+\.?\/?)+$';
  
          $scope.SERVICES = [
            {'id': 'redis', 'title': 'Redis'},
  
            {'id': 'registry-storage', 'title': 'Registry Storage'},
  
            {'id': 'time-machine', 'title': 'Time Machine'},
  
            {'id': 'access', 'title': 'Access Settings'},
  
            {'id': 'ssl', 'title': 'SSL certificate and key', 'condition': function(config) {
              return config.PREFERRED_URL_SCHEME == 'https';
            }},
  
            {'id': 'ldap', 'title': 'LDAP Authentication', 'condition': function(config) {
              return config.AUTHENTICATION_TYPE == 'LDAP';
            }},
  
            {'id': 'jwt', 'title': 'JWT Authentication', 'condition': function(config) {
              return config.AUTHENTICATION_TYPE == 'JWT';
            }},
  
            {'id': 'keystone', 'title': 'Keystone Authentication', 'condition': function(config) {
              return config.AUTHENTICATION_TYPE == 'Keystone';
            }},
  
            {'id': 'apptoken-auth', 'title': 'App Token Authentication', 'condition': function(config) {
              return config.AUTHENTICATION_TYPE == 'AppToken';
            }},
  
            {'id': 'signer', 'title': 'ACI Signing', 'condition': function(config) {
              return config.FEATURE_ACI_CONVERSION;
            }},
  
            {'id': 'github-login', 'title': 'Github (Enterprise) Authentication', 'condition': function(config) {
              return config.FEATURE_GITHUB_LOGIN;
            }},
  
            {'id': 'google-login', 'title': 'Google Authentication', 'condition': function(config) {
              return config.FEATURE_GOOGLE_LOGIN;
            }},
  
            {'id': 'github-trigger', 'title': 'GitHub (Enterprise) Build Triggers', 'condition': function(config) {
              return config.FEATURE_GITHUB_BUILD;
            }},
  
            {'id': 'bitbucket-trigger', 'title': 'BitBucket Build Triggers', 'condition': function(config) {
              return config.FEATURE_BITBUCKET_BUILD;
            }},
  
            {'id': 'gitlab-trigger', 'title': 'GitLab Build Triggers', 'condition': function(config) {
              return config.FEATURE_GITLAB_BUILD;
            }},
  
            {'id': 'security-scanner', 'title': 'Quay Security Scanner', 'condition': function(config) {
              return config.FEATURE_SECURITY_SCANNER;
            }},
  
            {'id': 'bittorrent', 'title': 'BitTorrent downloads', 'condition': function(config) {
              return config.FEATURE_BITTORRENT;
            }},
  
            {'id': 'oidc-login', 'title': 'OIDC Login(s)', 'condition': function(config) {
              return $scope.getOIDCProviders(config).length > 0;
            }},
  
            {'id': 'actionlogarchiving', 'title': 'Action Log Rotation', 'condition': function(config) {
              return config.FEATURE_ACTION_LOG_ROTATION;
            }},
  
            {'id': 'repomirroring', 'title': 'Repository Mirroring', 'condition': function(config) {
              return config.FEATURE_REPOSITORY_MIRRORING;
            }},
          ];
  
          $scope.STORAGE_CONFIG_FIELDS = {
            'LocalStorage': [
              {'name': 'storage_path', 'title': 'Storage Directory', 'placeholder': '/some/directory', 'kind': 'text'}
            ],
  
            'S3Storage': [
              {'name': 's3_bucket', 'title': 'S3 Bucket',  'placeholder': 'my-cool-bucket', 'kind': 'text'},
              {'name': 'storage_path', 'title': 'Storage Directory', 'placeholder': '/path/inside/bucket', 'kind': 'text'},
              {'name': 's3_access_key', 'title': 'AWS Access Key (optional if using IAM)', 'placeholder': 'accesskeyhere', 'kind': 'text', 'optional': true},
              {'name': 's3_secret_key', 'title': 'AWS Secret Key (optional if using IAM)', 'placeholder': 'secretkeyhere', 'kind': 'password', 'optional': true},
              {'name': 'host', 'title': 'S3 Host', 'placeholder': 's3.amazonaws.com', 'kind': 'text', 'optional': true},
              {'name': 'port', 'title': 'S3 Port', 'placeholder': '443', 'kind': 'text', 'pattern': '^[0-9]+$', 'optional': true}
            ],
  
            'AzureStorage': [
              {'name': 'azure_container', 'title': 'Azure Storage Container', 'placeholder': 'container', 'kind': 'text'},
              {'name': 'storage_path', 'title': 'Storage Directory', 'placeholder': '/path/inside/container', 'kind': 'text'},
              {'name': 'azure_account_name', 'title': 'Azure Account Name', 'placeholder': 'accountnamehere', 'kind': 'text'},
              {'name': 'azure_account_key', 'title': 'Azure Account Key',  'placeholder': 'accountkeyhere', 'kind': 'text', 'optional': true},
              {'name': 'sas_token', 'title': 'Azure SAS Token',  'placeholder': 'sastokenhere', 'kind': 'text', 'optional': true},
            ],
  
            'GoogleCloudStorage': [
              {'name': 'access_key', 'title': 'Cloud Access Key', 'placeholder': 'accesskeyhere', 'kind': 'text'},
              {'name': 'secret_key', 'title': 'Cloud Secret Key', 'placeholder': 'secretkeyhere', 'kind': 'text'},
              {'name': 'bucket_name', 'title': 'GCS Bucket',  'placeholder': 'my-cool-bucket', 'kind': 'text'},
              {'name': 'storage_path', 'title': 'Storage Directory', 'placeholder': '/path/inside/bucket', 'kind': 'text'}
            ],
  
            'RHOCSStorage': [
              {'name': 'hostname', 'title': 'NooBaa Server Hostname', 'placeholder': 'my.noobaa.hostname', 'kind': 'text'},
              {'name': 'port', 'title': 'Custom Port (optional)', 'placeholder': '443', 'kind': 'text', 'pattern': '^[0-9]+$', 'optional': true},
              {'name': 'is_secure', 'title': 'Is Secure', 'placeholder': 'Require SSL', 'kind': 'bool'},
              {'name': 'access_key', 'title': 'Access Key', 'placeholder': 'accesskeyhere', 'kind': 'text'},
              {'name': 'secret_key', 'title': 'Secret Key', 'placeholder': 'secretkeyhere', 'kind': 'text'},
              {'name': 'bucket_name', 'title': 'Bucket Name',  'placeholder': 'my-cool-bucket', 'kind': 'text'},
              {'name': 'storage_path', 'title': 'Storage Directory', 'placeholder': '/path/inside/bucket', 'kind': 'text'}
            ],
  
            'RadosGWStorage': [
              {'name': 'hostname', 'title': 'Rados Server Hostname', 'placeholder': 'my.rados.hostname', 'kind': 'text'},
              {'name': 'port', 'title': 'Custom Port (optional)', 'placeholder': '443', 'kind': 'text', 'pattern': '^[0-9]+$', 'optional': true},
              {'name': 'is_secure', 'title': 'Is Secure', 'placeholder': 'Require SSL', 'kind': 'bool'},
              {'name': 'access_key', 'title': 'Access Key', 'placeholder': 'accesskeyhere', 'kind': 'text', 'help_url': 'http://ceph.com/docs/master/radosgw/admin/'},
              {'name': 'secret_key', 'title': 'Secret Key', 'placeholder': 'secretkeyhere', 'kind': 'text'},
              {'name': 'bucket_name', 'title': 'Bucket Name',  'placeholder': 'my-cool-bucket', 'kind': 'text'},
              {'name': 'storage_path', 'title': 'Storage Directory', 'placeholder': '/path/inside/bucket', 'kind': 'text'}
            ],
  
            'SwiftStorage': [
              {'name': 'auth_version', 'title': 'Swift Auth Version', 'kind': 'option', 'values': [1, 2, 3]},
              {'name': 'auth_url', 'title': 'Swift Auth URL', 'placeholder': 'http://swiftdomain/auth/v1.0', 'kind': 'text'},
              {'name': 'swift_container', 'title': 'Swift Container Name', 'placeholder': 'mycontainer', 'kind': 'text',
               'help_text': 'The swift container for all objects. Must already exist inside Swift.'},
  
              {'name': 'storage_path', 'title': 'Storage Path', 'placeholder': '/path/inside/container', 'kind': 'text'},
  
              {'name': 'swift_user', 'title': 'Username', 'placeholder': 'accesskeyhere', 'kind': 'text',
               'help_text': 'Note: For Swift V1, this is "username:password" (-U on the CLI).'},
              {'name': 'swift_password', 'title': 'Key/Password', 'placeholder': 'secretkeyhere', 'kind': 'text',
               'help_text': 'Note: For Swift V1, this is the API token (-K on the CLI).'},
  
              {'name': 'ca_cert_path', 'title': 'CA Cert Filename', 'placeholder': 'conf/stack/swift.cert', 'kind': 'text', 'optional': true},
  
              {'name': 'temp_url_key', 'title': 'Temp URL Key (optional)', 'placholder': 'key-here', 'kind': 'text', 'optional': true,
               'help_url': 'https://coreos.com/products/enterprise-registry/docs/latest/swift-temp-url.html',
               'help_text': 'If enabled, will allow for faster pulls directly from Swift.'},
  
              {'name': 'os_options', 'title': 'OS Options', 'kind': 'map',
               'keys': ['tenant_id', 'auth_token', 'service_type', 'endpoint_type', 'tenant_name', 'object_storage_url', 'region_name',
                        'project_id', 'project_name', 'project_domain_name', 'user_domain_name', 'user_domain_id']}
            ],
  
            'CloudFrontedS3Storage': [
              {'name': 's3_bucket', 'title': 'S3 Bucket',  'placeholder': 'my-cool-bucket', 'kind': 'text'},
              {'name': 'storage_path', 'title': 'Storage Directory', 'placeholder': '/path/inside/bucket', 'kind': 'text'},
              {'name': 's3_access_key', 'title': 'AWS Access Key (optional if using IAM)', 'placeholder': 'accesskeyhere', 'kind': 'text', 'optional': true},
              {'name': 's3_secret_key', 'title': 'AWS Secret Key (optional if using IAM)', 'placeholder': 'secretkeyhere', 'kind': 'text', 'optional': true},
              {'name': 'host', 'title': 'S3 Host', 'placeholder': 's3.amazonaws.com', 'kind': 'text', 'optional': true},
              {'name': 'port', 'title': 'S3 Port', 'placeholder': '443', 'kind': 'text', 'pattern': '^[0-9]+$', 'optional': true},
  
              {'name': 'cloudfront_distribution_domain', 'title': 'CloudFront Distribution Domain Name', 'placeholder': 'somesubdomain.cloudfront.net', 'pattern': '^([0-9a-zA-Z]+\\.)+[0-9a-zA-Z]+$', 'kind': 'text'},
              {'name': 'cloudfront_key_id', 'title': 'CloudFront Key ID', 'placeholder': 'APKATHISISAKEYID', 'kind': 'text'},
              {'name': 'cloudfront_privatekey_filename', 'title': 'CloudFront Private Key', 'filesuffix': 'cloudfront-signing-key.pem', 'kind': 'file'},
            ],
          };
  
          $scope.enableFeature = function(config, feature) {
            config[feature] = true;
          };
  
          $scope.validateHostname = function(hostname) {
            if (hostname.indexOf('127.0.0.1') == 0 || hostname.indexOf('localhost') == 0) {
              return 'Please specify a non-localhost hostname. "localhost" will refer to the container, not your machine.'
            }
  
            return null;
          };
  
          $scope.config = null;
          $scope.originalConfig = null;
          $scope.mapped = {
            '$hasChanges': false
          };
  
          $scope.hasfile = {};
          $scope.validating = null;
          $scope.savingConfiguration = false;
  
          $scope.removeOIDCProvider = function(provider) {
            delete $scope.config[provider];
          };
  
          $scope.addOIDCProvider = () => {
            bootbox.prompt('Enter an ID for the OIDC provider', function(result) {
              if (!result) {
                return;
              }
  
              result = result.toUpperCase();
  
              if (!result.match(/^[A-Z0-9]+$/)) {
                bootbox.alert('Invalid ID for OIDC provider: must be alphanumeric');
                return;
              }
  
              if (result == 'GITHUB' || result == 'GOOGLE') {
                bootbox.alert('Invalid ID for OIDC provider: cannot be a reserved name');
                return;
              }
  
              var key = result + '_LOGIN_CONFIG';
              if ($scope.config[key]) {
                bootbox.alert('Invalid ID for OIDC provider: already exists');
                return;
              }
  
              $scope.config[key] = {};
            });
          };
  
          $scope.getOIDCProviderId = function(key) {
            var index = key.indexOf('_LOGIN_CONFIG');
            if (index <= 0) {
              return null;
            }
  
            return key.substr(0, index).toLowerCase();
          };
  
          $scope.getOIDCProviders = function(config) {
            var keys = Object.keys(config || {});
            return keys.filter(function(key) {
              if (key == 'GITHUB_LOGIN_CONFIG' || key == 'GOOGLE_LOGIN_CONFIG') {
                // Has custom UI and config.
                return false;
              }
  
              return !!$scope.getOIDCProviderId(key);
            });
          };
  
          $scope.getServices = function(config) {
            var services = [];
            if (!config) { return services; }
  
            for (var i = 0; i < $scope.SERVICES.length; ++i) {
              var service = $scope.SERVICES[i];
              if (!service.condition || service.condition(config)) {
                services.push({
                  'service': service,
                  'status': 'validating'
                });
              }
            }
  
            return services;
          };
  
          $scope.validationStatus = function(serviceInfos) {
            if (!serviceInfos) { return 'validating'; }
  
            var hasError = false;
            for (var i = 0; i < serviceInfos.length; ++i) {
              if (serviceInfos[i].status == 'validating') {
                return 'validating';
              }
              if (serviceInfos[i].status == 'error') {
                hasError = true;
              }
            }
  
            return hasError ? 'failed' : 'success';
          };
  
          $scope.cancelValidation = function() {
            $('#validateAndSaveModal').modal('hide');
            $scope.validating = null;
            $scope.savingConfiguration = false;
          };
  
          $scope.validateService = function(serviceInfo, opt_password) {
            var params = {
              'service': serviceInfo.service.id
            };
  
            var data = {
              'config': $scope.config,
              'password': opt_password || ''
            };
  
            var errorDisplay = ApiService.errorDisplay(
                'Could not validate configuration. Please report this error.',
                function() {
                  authPassword = null;
                });
  
            ApiService.scValidateConfig(data, params).then(function(resp) {
              serviceInfo.status = resp.status ? 'success' : 'error';
              serviceInfo.errorMessage = $.trim(resp.reason || '');
  
              if (!resp.status) {
                authPassword = null;
              }
  
            }, errorDisplay);
          };
  
          $scope.checkValidateAndSave = function() {
            if ($scope.configform.$valid) {
              saveStorageConfig();
              $scope.validateAndSave();
              return;
            }
  
            var query = $element.find("input.ng-invalid:first");
  
            if (query && query.length) {
              query[0].scrollIntoView();
              query.focus();
            }
          };
  
          $scope.validateAndSave = function() {
            $scope.validating = $scope.getServices($scope.config);
  
            $scope.performValidateAndSave();
          };
  
          $scope.performValidateAndSave = function(opt_password) {
            $scope.savingConfiguration = false;
            $scope.validating = $scope.getServices($scope.config);
  
            authPassword = opt_password;
  
            $('#validateAndSaveModal').modal({
              keyboard: false,
              backdrop: 'static'
            });
  
            for (var i = 0; i < $scope.validating.length; ++i) {
              var serviceInfo = $scope.validating[i];
              $scope.validateService(serviceInfo, opt_password);
            }
          };
  
          $scope.saveConfiguration = function() {
            $scope.savingConfiguration = true;
  
            // Make sure to note that fully verified setup is completed. We use this as a signal
            // in the setup tool.
            $scope.config['SETUP_COMPLETE'] = true;
  
            var data = {
              'config': $scope.config,
              'hostname': window.location.host,
              'password': authPassword || ''
            };
  
            var errorDisplay = ApiService.errorDisplay(
              'Could not save configuration. Please report this error.',
              function() {
                authPassword = null;
              });
  
            ApiService.scUpdateConfig(data).then(function(resp) {
              authPassword = null;
  
              $scope.savingConfiguration = false;
              $scope.mapped.$hasChanges = false;
  
              $('#validateAndSaveModal').modal('hide');
  
              $scope.setupCompleted();
            }, errorDisplay);
          };
  
          // Convert storage config to an array
          var initializeStorageConfig = function($scope) {
            var config = $scope.config.DISTRIBUTED_STORAGE_CONFIG || {};
            var defaultLocations = $scope.config.DISTRIBUTED_STORAGE_DEFAULT_LOCATIONS || [];
            var preference = $scope.config.DISTRIBUTED_STORAGE_PREFERENCE || [];
  
            $scope.serverStorageConfig = angular.copy(config);
            $scope.storageConfig = [];
  
            Object.keys(config).forEach(function(location) {
              $scope.storageConfig.push({
                location: location,
                defaultLocation: defaultLocations.indexOf(location) >= 0,
                data: angular.copy(config[location]),
                error: {},
              });
            });
  
            if (!$scope.storageConfig.length) {
              $scope.addStorageConfig('default');
              return;
            }
  
            // match DISTRIBUTED_STORAGE_PREFERENCE order first, remaining are
            // ordered by unicode point value
            $scope.storageConfig.sort(function(a, b) {
              var indexA = preference.indexOf(a.location);
              var indexB = preference.indexOf(b.location);
  
              if (indexA > -1 && indexB > -1) return indexA < indexB ? -1 : 1;
              if (indexA > -1) return -1;
              if (indexB > -1) return 1;
  
              return a.location < b.location ? -1 : 1;
            });
          };
  
          $scope.allowChangeLocationStorageConfig = function(location) {
            if (!$scope.serverStorageConfig[location]) { return true };
  
            // allow user to change location ID if another exists with the same ID
            return $scope.storageConfig.filter(function(sc) {
              return sc.location === location;
            }).length >= 2;
          };
  
          $scope.allowRemoveStorageConfig = function(location) {
            return $scope.storageConfig.length > 1 && $scope.allowChangeLocationStorageConfig(location);
          };
  
          $scope.canAddStorageConfig = function() {
            return $scope.config &&
              $scope.config.FEATURE_STORAGE_REPLICATION &&
              $scope.storageConfig &&
              (!$scope.storageConfig.length || $scope.storageConfig.length < 10);
          };
  
          $scope.addStorageConfig = function(location) {
            var storageType = 'LocalStorage';
  
            // Use last storage type by default
            if ($scope.storageConfig.length) {
              storageType = $scope.storageConfig[$scope.storageConfig.length-1].data[0];
            }
  
            $scope.storageConfig.push({
              location: location || '',
              defaultLocation: false,
              data: [storageType, {}],
              error: {},
            });
          };
  
          $scope.removeStorageConfig = function(sc) {
            $scope.storageConfig.splice($scope.storageConfig.indexOf(sc), 1);
          };
  
          var saveStorageConfig = function() {
            var config = {};
            var defaultLocations = [];
            var preference = [];
  
            $scope.storageConfig.forEach(function(sc) {
              config[sc.location] = sc.data;
              if (sc.defaultLocation) defaultLocations.push(sc.location);
              preference.push(sc.location);
            });
  
            $scope.config.DISTRIBUTED_STORAGE_CONFIG = config;
            $scope.config.DISTRIBUTED_STORAGE_DEFAULT_LOCATIONS = defaultLocations;
            $scope.config.DISTRIBUTED_STORAGE_PREFERENCE = preference;
          };
  
          var gitlabSelector = function(key) {
            return function(value) {
              if (!value || !$scope.config) { return; }
  
              if (!$scope.config[key]) {
                $scope.config[key] = {};
              }
  
              if (value == 'enterprise') {
                if ($scope.config[key]['GITLAB_ENDPOINT'] == 'https://gitlab.com/') {
                  $scope.config[key]['GITLAB_ENDPOINT'] = '';
                }
              } else if (value == 'hosted') {
                $scope.config[key]['GITLAB_ENDPOINT'] = 'https://gitlab.com/';
              }
            };
          };
  
          var githubSelector = function(key) {
            return function(value) {
              if (!value || !$scope.config) { return; }
  
              if (!$scope.config[key]) {
                $scope.config[key] = {};
              }
  
              if (value == 'enterprise') {
                if ($scope.config[key]['GITHUB_ENDPOINT'] == 'https://github.com/') {
                  $scope.config[key]['GITHUB_ENDPOINT'] = '';
                }
                delete $scope.config[key]['API_ENDPOINT'];
              } else if (value == 'hosted') {
                $scope.config[key]['GITHUB_ENDPOINT'] = 'https://github.com/';
                $scope.config[key]['API_ENDPOINT'] = 'https://api.github.com/';
              }
            };
          };
  
          var getKey = function(config, path) {
            if (!config) {
              return null;
            }
  
            var parts = path.split('.');
            var current = config;
            for (var i = 0; i < parts.length; ++i) {
              var part = parts[i];
              if (!current[part]) { return null; }
              current = current[part];
            }
            return current;
          };
  
          var initializeMappedLogic = function(config) {
            var gle = getKey(config, 'GITHUB_LOGIN_CONFIG.GITHUB_ENDPOINT');
            var gte = getKey(config, 'GITHUB_TRIGGER_CONFIG.GITHUB_ENDPOINT');
  
            $scope.mapped['GITHUB_LOGIN_KIND'] = gle == 'https://github.com/' ? 'hosted' : 'enterprise';
            $scope.mapped['GITHUB_TRIGGER_KIND'] = gte == 'https://github.com/' ? 'hosted' : 'enterprise';
  
            var glabe = getKey(config, 'GITLAB_TRIGGER_KIND.GITHUB_ENDPOINT');
            $scope.mapped['GITLAB_TRIGGER_KIND'] = glabe == 'https://gitlab.com/' ? 'hosted' : 'enterprise';
  
            $scope.mapped['redis'] = {};
            $scope.mapped['redis']['host'] = getKey(config, 'BUILDLOGS_REDIS.host') || getKey(config, 'USER_EVENTS_REDIS.host');
            $scope.mapped['redis']['port'] = getKey(config, 'BUILDLOGS_REDIS.port') || getKey(config, 'USER_EVENTS_REDIS.port');
            $scope.mapped['redis']['password'] = getKey(config, 'BUILDLOGS_REDIS.password') || getKey(config, 'USER_EVENTS_REDIS.password');
  
            $scope.mapped['TLS_SETTING'] = 'none';
            if (config['PREFERRED_URL_SCHEME'] == 'https') {
              if (config['EXTERNAL_TLS_TERMINATION'] === true) {
                $scope.mapped['TLS_SETTING'] = 'external-tls';
              } else {
                $scope.mapped['TLS_SETTING'] = 'internal-tls';
              }
            }
          };
  
          var tlsSetter = function(value) {
              if (value == null || !$scope.config) { return; }
  
              switch (value) {
                case 'none':
                  $scope.config['PREFERRED_URL_SCHEME'] = 'http';
                  delete $scope.config['EXTERNAL_TLS_TERMINATION'];
                  return;
  
                case 'external-tls':
                  $scope.config['PREFERRED_URL_SCHEME'] = 'https';
                  $scope.config['EXTERNAL_TLS_TERMINATION'] = true;
                  return;
  
                case 'internal-tls':
                  $scope.config['PREFERRED_URL_SCHEME'] = 'https';
                  delete $scope.config['EXTERNAL_TLS_TERMINATION'];
                  return;
              }
          };
  
          var redisSetter = function(keyname) {
            return function(value) {
              if (value == null || !$scope.config) { return; }
  
              if (!$scope.config['BUILDLOGS_REDIS']) {
                $scope.config['BUILDLOGS_REDIS'] = {};
              }
  
              if (!$scope.config['USER_EVENTS_REDIS']) {
                $scope.config['USER_EVENTS_REDIS'] = {};
              }
  
              if (!value) {
                delete $scope.config['BUILDLOGS_REDIS'][keyname];
                delete $scope.config['USER_EVENTS_REDIS'][keyname];
                return;
              }
  
              $scope.config['BUILDLOGS_REDIS'][keyname] = value;
              $scope.config['USER_EVENTS_REDIS'][keyname] = value;
            };
          };
  
          // Add mapped logic.
          $scope.$watch('mapped.GITHUB_LOGIN_KIND', githubSelector('GITHUB_LOGIN_CONFIG'));
          $scope.$watch('mapped.GITHUB_TRIGGER_KIND', githubSelector('GITHUB_TRIGGER_CONFIG'));
          $scope.$watch('mapped.GITLAB_TRIGGER_KIND', gitlabSelector('GITLAB_TRIGGER_KIND'));
          $scope.$watch('mapped.TLS_SETTING', tlsSetter);
  
          $scope.$watch('mapped.redis.host', redisSetter('host'));
          $scope.$watch('mapped.redis.port', redisSetter('port'));
          $scope.$watch('mapped.redis.password', redisSetter('password'));
  
          // Remove extra extra fields (which are not allowed) from storage config.
          var updateFields = function(sc) {
            var type = sc.data[0];
            var configObject = sc.data[1];
            var allowedFields = $scope.STORAGE_CONFIG_FIELDS[type];
  
            // Remove any fields not allowed.
            for (var fieldName in configObject) {
              if (!configObject.hasOwnProperty(fieldName)) {
                continue;
              }
  
              var isValidField = $.grep(allowedFields, function(field) {
                return field.name == fieldName;
              }).length > 0;
  
              if (!isValidField) {
                delete configObject[fieldName];
              }
            }
  
            // Set any missing boolean fields to false.
            for (var i = 0; i < allowedFields.length; ++i) {
              if (allowedFields[i].kind == 'bool') {
                configObject[allowedFields[i].name] = configObject[allowedFields[i].name] || false;
              }
            }
          };
  
          // Validate and update storage config on update.
          var refreshStorageConfig = function() {
            if (!$scope.config || !$scope.storageConfig) return;
  
            var locationCounts = {};
            var errors = [];
            var valid = true;
  
            $scope.storageConfig.forEach(function(sc) {
              // remove extra fields from storage config
              updateFields(sc);
  
              if (!locationCounts[sc.location]) locationCounts[sc.location] = 0;
              locationCounts[sc.location]++;
            });
  
            // validate storage config
            $scope.storageConfig.forEach(function(sc) {
              var error = {};
  
              if ($scope.config.FEATURE_STORAGE_REPLICATION && sc.data[0] === 'LocalStorage') {
                error.engine = 'Replication to a locally mounted directory is unsupported as it is only accessible on a single machine.';
                valid = false;
              }
  
              if (locationCounts[sc.location] > 1) {
                error.location = 'Location ID must be unique.';
                valid = false;
              }
  
              errors.push(error);
            });
  
            $scope.storageConfigError = errors;
            $scope.configform.$setValidity('storageConfig', valid);
          };
  
          $scope.$watch('config.INTERNAL_OIDC_SERVICE_ID', function(service_id) {
            if (service_id) {
              $scope.config['FEATURE_DIRECT_LOGIN'] = false;
            }
          });
  
          $scope.$watch('config.FEATURE_STORAGE_REPLICATION', function() {
            refreshStorageConfig();
          });
  
          $scope.$watch('config.FEATURE_USER_CREATION', function(value) {
            if (!value) {
              $scope.config['FEATURE_INVITE_ONLY_USER_CREATION'] = false;
            }
          });
  
          $scope.$watch('storageConfig', function() {
            refreshStorageConfig();
          }, true);
  
          $scope.$watch('config', function(value) {
            $scope.mapped['$hasChanges'] = true;
          }, true);
  
          $scope.$watch('isActive', function(value) {
            if (!value) { return; }
  
            ApiService.scGetConfig().then(function(resp) {
              $scope.config = resp['config'] || {};
              $scope.originalConfig = Object.assign({}, resp['config'] || {});;
              initializeMappedLogic($scope.config);
              initializeStorageConfig($scope);
              $scope.mapped['$hasChanges'] = false;
            }, ApiService.errorDisplay('Could not load config'));
          });
        }
      };
  
      return directiveDefinitionObject;
    })
  
    .directive('configParsedField', function ($timeout) {
      var directiveDefinitionObject = {
        priority: 0,
        templateUrl: urlParsedField,
        replace: false,
        transclude: true,
        restrict: 'C',
        scope: {
          'binding': '=binding',
          'parser': '&parser',
          'serializer': '&serializer'
        },
        controller: function($scope, $element, $transclude) {
          $scope.childScope = null;
  
          $transclude(function(clone, scope) {
            $scope.childScope = scope;
            $scope.childScope['fields'] = {};
            $element.append(clone);
          });
  
          $scope.childScope.$watch('fields', function(value) {
            // Note: We need the timeout here because Angular starts the digest of the
            // parent scope AFTER the child scope, which means it can end up one action
            // behind. The timeout ensures that the parent scope will be fully digest-ed
            // and then we update the binding. Yes, this is a hack :-/.
            $timeout(function() {
              $scope.binding = $scope.serializer({'fields': value});
            });
          }, true);
  
          $scope.$watch('binding', function(value) {
            var parsed = $scope.parser({'value': value});
            for (var key in parsed) {
              if (parsed.hasOwnProperty(key)) {
               $scope.childScope['fields'][key] = parsed[key];
              }
            }
          });
        }
      };
      return directiveDefinitionObject;
    })
  
    .directive('configVariableField', function () {
      var directiveDefinitionObject = {
        priority: 0,
        templateUrl: urlVarField,
        replace: false,
        transclude: true,
        restrict: 'C',
        scope: {
          'binding': '=binding'
        },
        controller: function($scope, $element) {
          $scope.sections = {};
          $scope.currentSection = null;
  
          $scope.setSection = function(section) {
            $scope.binding = section.value;
          };
  
          this.addSection = function(section, element) {
            $scope.sections[section.value] = {
              'title': section.valueTitle,
              'value': section.value,
              'element': element
            };
  
            element.hide();
  
            if (!$scope.binding) {
              $scope.binding = section.value;
            }
          };
  
          $scope.$watch('binding', function(binding) {
            if (!binding) { return; }
  
            if ($scope.currentSection) {
              $scope.currentSection.element.hide();
            }
  
            if ($scope.sections[binding]) {
              $scope.sections[binding].element.show();
              $scope.currentSection = $scope.sections[binding];
            }
          });
        }
      };
      return directiveDefinitionObject;
    })
  
    .directive('variableSection', function () {
      var directiveDefinitionObject = {
        priority: 0,
        templateUrl: urlVarField,
        priority: 1,
        require: '^configVariableField',
        replace: false,
        transclude: true,
        restrict: 'C',
        scope: {
          'value': '@value',
          'valueTitle': '@valueTitle'
        },
        controller: function($scope, $element) {
          var parentCtrl = $element.parent().controller('configVariableField');
          parentCtrl.addSection($scope, $element);
        }
      };
      return directiveDefinitionObject;
    })
  
    .directive('configListField', function () {
      var directiveDefinitionObject = {
        priority: 0,
        templateUrl: urlListField,
        replace: false,
        transclude: false,
        restrict: 'C',
        scope: {
          'binding': '=binding',
          'placeholder': '@placeholder',
          'defaultValue': '@defaultValue',
          'itemTitle': '@itemTitle',
          'itemPattern': '@itemPattern'
        },
        controller: function($scope, $element) {
          $scope.removeItem = function(item) {
            var index = $scope.binding.indexOf(item);
            if (index >= 0) {
              $scope.binding.splice(index, 1);
            }
          };
  
          $scope.addItem = function() {
            if (!$scope.newItemName) {
              return;
            }
  
            if (!$scope.binding) {
              $scope.binding = [];
            }
  
            if ($scope.binding.indexOf($scope.newItemName) >= 0) {
              return;
            }
  
            $scope.binding.push($scope.newItemName);
            $scope.newItemName = null;
          };
  
          $scope.patternMap = {};
  
          $scope.getRegexp = function(pattern) {
            if (!pattern) {
              pattern = '.*';
            }
  
            if ($scope.patternMap[pattern]) {
              return $scope.patternMap[pattern];
            }
  
            return $scope.patternMap[pattern] = new RegExp(pattern);
          };
  
          $scope.$watch('binding', function(binding) {
            if (!binding && $scope.defaultValue) {
              $scope.binding = eval($scope.defaultValue);
            }
          });
        }
      };
      return directiveDefinitionObject;
    })
  
    .directive('configFileField', function () {
      var directiveDefinitionObject = {
        priority: 0,
        templateUrl: urlFileField,
        replace: false,
        transclude: false,
        restrict: 'C',
        scope: {
          'filename': '@filename',
          'skipCheckFile': '@skipCheckFile',
          'hasFile': '=hasFile',
          'binding': '=?binding'
        },
        controller: function($scope, $element, Restangular, $upload) {
          $scope.hasFile = false;
  
          var setHasFile = function(hasFile) {
            $scope.hasFile = hasFile;
            $scope.binding = hasFile ? $scope.filename : null;
          };
  
          $scope.onFileSelect = function(files) {
            if (files.length < 1) {
              setHasFile(false);
              return;
            }
  
            $scope.uploadProgress = 0;
            $scope.upload = $upload.upload({
              url: '/api/v1/superuser/config/file/' + $scope.filename,
              method: 'POST',
              data: {'_csrf_token': window.__token},
              file: files[0],
            }).progress(function(evt) {
              $scope.uploadProgress = parseInt(100.0 * evt.loaded / evt.total);
              if ($scope.uploadProgress == 100) {
                $scope.uploadProgress = null;
                setHasFile(true);
              }
            }).success(function(data, status, headers, config) {
              $scope.uploadProgress = null;
              setHasFile(true);
            });
          };
  
          var loadStatus = function(filename) {
            Restangular.one('superuser/config/file/' + filename).get().then(function(resp) {
              setHasFile(false);
            });
          };
  
          if ($scope.filename && $scope.skipCheckFile != "true") {
            loadStatus($scope.filename);
          }
        }
      };
      return directiveDefinitionObject;
    })
  
    .directive('configBoolField', function () {
      var directiveDefinitionObject = {
        priority: 0,
        templateUrl: urlBoolField,
        replace: false,
        transclude: true,
        restrict: 'C',
        scope: {
          'binding': '=binding'
        },
        controller: function($scope, $element) {
        }
      };
      return directiveDefinitionObject;
    })
  
    .directive('configNumericField', function () {
      var directiveDefinitionObject = {
        priority: 0,
        templateUrl: urlNumericField,
        replace: false,
        transclude: false,
        restrict: 'C',
        scope: {
          'binding': '=binding',
          'placeholder': '@placeholder',
          'defaultValue': '@defaultValue',
        },
        controller: function($scope, $element) {
          $scope.bindinginternal = 0;
  
          $scope.$watch('binding', function(binding) {
            if ($scope.binding == 0 && $scope.defaultValue) {
              $scope.binding = $scope.defaultValue * 1;
            }
  
            $scope.bindinginternal = $scope.binding;
          });
  
          $scope.$watch('bindinginternal', function(binding) {
            var newValue = $scope.bindinginternal * 1;
            if (isNaN(newValue)) {
              newValue = 0;
            }
            $scope.binding = newValue;
          });
        }
      };
      return directiveDefinitionObject;
    })
  
    .directive('configContactsField', function () {
      var directiveDefinitionObject = {
        priority: 0,
        templateUrl: urlContactsField,
        priority: 1,
        replace: false,
        transclude: false,
        restrict: 'C',
        scope: {
          'binding': '=binding'
        },
        controller: function($scope, $element) {
          var padItems = function(items) {
            // Remove the last item if both it and the second to last items are empty.
            if (items.length > 1 && !items[items.length - 2].value && !items[items.length - 1].value) {
              items.splice(items.length - 1, 1);
              return;
            }
  
            // If the last item is non-empty, add a new item.
            if (items.length == 0 || items[items.length - 1].value) {
              items.push({'value': ''});
              return;
            }
          };
  
          $scope.itemHash = null;
          $scope.$watch('items', function(items) {
            if (!items) { return; }
            padItems(items);
  
            var itemHash = '';
            var binding = [];
            for (var i = 0; i < items.length; ++i) {
              var item = items[i];
              if (item.value && (URI(item.value).host() || URI(item.value).path())) {
                binding.push(item.value);
                itemHash += item.value;
              }
            }
  
            $scope.itemHash = itemHash;
            $scope.binding = binding;
          }, true);
  
          $scope.$watch('binding', function(binding) {
            var current = binding || [];
            var items = [];
            var itemHash = '';
            for (var i = 0; i < current.length; ++i) {
              items.push({'value': current[i]})
              itemHash += current[i];
            }
  
            if ($scope.itemHash != itemHash) {
              $scope.items = items;
            }
          });
        }
      };
      return directiveDefinitionObject;
    })
  
    .directive('configContactField', function () {
      var directiveDefinitionObject = {
        priority: 0,
        templateUrl: urlContactField,
        priority: 1,
        replace: false,
        transclude: true,
        restrict: 'C',
        scope: {
          'binding': '=binding'
        },
        controller: function($scope, $element) {
          $scope.kind = null;
          $scope.value = null;
  
          var updateBinding = function() {
            if ($scope.value == null) { return; }
            var value = $scope.value || '';
  
            switch ($scope.kind) {
              case 'mailto':
                $scope.binding = 'mailto:' + value;
                return;
  
              case 'tel':
                $scope.binding = 'tel:' + value;
                return;
  
              case 'irc':
                $scope.binding = 'irc://' + value;
                return;
  
              default:
                $scope.binding = value;
                return;
            }
          };
  
          $scope.$watch('kind', updateBinding);
          $scope.$watch('value', updateBinding);
  
          $scope.$watch('binding', function(value) {
            if (!value) {
              $scope.kind = null;
              $scope.value = null;
              return;
            }
  
            var uri = URI(value);
            $scope.kind = uri.scheme();
  
            switch ($scope.kind) {
              case 'mailto':
              case 'tel':
                $scope.value = uri.path();
                break;
  
              case 'irc':
                $scope.value = value.substr('irc://'.length);
                break;
  
              default:
                $scope.kind = 'http';
                $scope.value = value;
                break;
            }
          });
  
          $scope.getPlaceholder = function(kind) {
            switch (kind) {
              case 'mailto':
                return 'some@example.com';
  
              case 'tel':
                return '555-555-5555';
  
              case 'irc':
                return 'myserver:port/somechannel';
  
              default:
                return 'http://some/url';
            }
          };
        }
      };
      return directiveDefinitionObject;
    })
  
    .directive('configMapField', function () {
      var directiveDefinitionObject = {
        priority: 0,
        templateUrl: urlMapField,
        replace: false,
        transclude: false,
        restrict: 'C',
        scope: {
          'binding': '=binding',
          'keys': '=keys'
        },
        controller: function($scope, $element) {
          $scope.newKey = null;
          $scope.newValue = null;
  
          $scope.hasValues = function(binding) {
            return binding && Object.keys(binding).length;
          };
  
          $scope.removeKey = function(key) {
            delete $scope.binding[key];
          };
  
          $scope.addEntry = function() {
            if (!$scope.newKey || !$scope.newValue) { return; }
  
            $scope.binding = $scope.binding || {};
            $scope.binding[$scope.newKey] = $scope.newValue;
            $scope.newKey = null;
            $scope.newValue = null;
          }
        }
      };
      return directiveDefinitionObject;
    })
  
    .directive('configServiceKeyField', function (ApiService) {
      var directiveDefinitionObject = {
        priority: 0,
        templateUrl: urlServiceKeyField,
        replace: false,
        transclude: false,
        restrict: 'C',
        scope: {
          'serviceName': '@serviceName',
        },
        controller: function($scope, $element) {
          $scope.foundKeys = [];
          $scope.loading = false;
          $scope.loadError = false;
          $scope.hasValidKey = false;
          $scope.hasValidKeyStr = null;
  
          $scope.updateKeys = function() {
            $scope.foundKeys = [];
            $scope.loading = true;
  
            ApiService.listServiceKeys().then(function(resp) {
              $scope.loading = false;
              $scope.loadError = false;
  
              resp['keys'].forEach(function(key) {
                if (key['service'] == $scope.serviceName) {
                  $scope.foundKeys.push(key);
                }
              });
  
              $scope.hasValidKey = checkValidKey($scope.foundKeys);
              $scope.hasValidKeyStr = $scope.hasValidKey ? 'true' : '';
            }, function() {
              $scope.loading = false;
              $scope.loadError = true;
            });
          };
  
          // Perform initial loading of the keys.
          $scope.updateKeys();
  
          $scope.isKeyExpired = function(key) {
            if (key.expiration_date != null) {
              var expiration_date = moment(key.expiration_date);
              return moment().isAfter(expiration_date);
            }
            return false;
          };
  
          $scope.showRequestServiceKey = function(opt_newKey) {
            $scope.requestKeyInfo = {
              'service': $scope.serviceName,
              'newKey': opt_newKey
            };
          };
  
          $scope.handleKeyCreated = function() {
            $scope.updateKeys();
          };
  
          var checkValidKey = function(keys) {
            for (var i = 0; i < keys.length; ++i) {
              var key = keys[i];
              if (!key.approval) {
                continue;
              }
  
              if ($scope.isKeyExpired(key)) {
                continue;
              }
  
              return true;
            }
  
            return false;
          };
        }
      };
      return directiveDefinitionObject;
    })
  
    .directive('configStringField', function () {
      var directiveDefinitionObject = {
        priority: 0,
        templateUrl: urlStringField,
        replace: false,
        transclude: false,
        restrict: 'C',
        scope: {
          'binding': '=binding',
          'placeholder': '@placeholder',
          'pattern': '@pattern',
          'defaultValue': '@defaultValue',
          'validator': '&validator',
          'isOptional': '=isOptional'
        },
        controller: function($scope, $element) {
          var firstSet = true;
  
          $scope.patternMap = {};
  
          $scope.getRegexp = function(pattern) {
            if (!pattern) {
              pattern = '.*';
            }
  
            if ($scope.patternMap[pattern]) {
              return $scope.patternMap[pattern];
            }
  
            return $scope.patternMap[pattern] = new RegExp(pattern);
          };
  
          $scope.$watch('binding', function(binding) {
            if (firstSet && !binding && $scope.defaultValue) {
              $scope.binding = $scope.defaultValue;
              firstSet = false;
            }
  
            $scope.errorMessage = $scope.validator({'value': binding || ''});
          });
        }
      };
      return directiveDefinitionObject;
    })
  
    .directive('configPasswordField', function () {
      var directiveDefinitionObject = {
        priority: 0,
        templateUrl: urlPasswordField,
        replace: false,
        transclude: false,
        restrict: 'C',
        scope: {
          'binding': '=binding',
          'placeholder': '@placeholder',
          'defaultValue': '@defaultValue',
          'validator': '&validator',
          'isOptional': '=isOptional'
        },
        controller: function($scope, $element) {
          var firstSet = true;
  
          $scope.$watch('binding', function(binding) {
            if (firstSet && !binding && $scope.defaultValue) {
              $scope.binding = $scope.defaultValue;
              firstSet = false;
            }
            $scope.errorMessage = $scope.validator({'value': binding || ''});
          });
        }
      };
      return directiveDefinitionObject;
    })
  
    .directive('configStringListField', function () {
      var directiveDefinitionObject = {
        priority: 0,
        templateUrl: urlStringListField,
        replace: false,
        transclude: false,
        restrict: 'C',
        scope: {
          'binding': '=binding',
          'itemTitle': '@itemTitle',
          'itemDelimiter': '@itemDelimiter',
          'placeholder': '@placeholder',
          'isOptional': '=isOptional'
        },
        controller: function($scope, $element) {
          $scope.$watch('internalBinding', function(value) {
            if (value) {
              $scope.binding = value.split($scope.itemDelimiter);
            }
          });
  
          $scope.$watch('binding', function(value) {
            if (value) {
              $scope.internalBinding = value.join($scope.itemDelimiter);
            }
          });
        }
      };
      return directiveDefinitionObject;
    })
  
    .directive('configCertificatesField', function () {
      var directiveDefinitionObject = {
        priority: 0,
        templateUrl: urlCertField,
        replace: false,
        transclude: false,
        restrict: 'C',
        scope: {
        },
        controller: function($scope, $element, $upload, ApiService, UserService) {
          $scope.resetUpload = 0;
          $scope.certsUploading = false;
  
          var loadCertificates = function() {
            $scope.certificatesResource = ApiService.getCustomCertificatesAsResource().get(function(resp) {
              $scope.certInfo = resp;
              $scope.certsUploading = false;
            });
          };
  
          loadCertificates();
  
          $scope.handleCertsSelected = function(files, callback) {
            $scope.certsUploading = true;
            $upload.upload({
              url: '/api/v1/superuser/customcerts/' + files[0].name,
              method: 'POST',
              data: {'_csrf_token': window.__token},
              file: files[0]
            }).success(function() {
              callback(true);
              $scope.resetUpload++;
              loadCertificates();
            }).error(function(r) {
              bootbox.alert('Could not upload certificate')
              callback(false);
              $scope.resetUpload++;
              loadCertificates();
            });
          };
  
          $scope.deleteCert = function(path) {
            var errorDisplay = ApiService.errorDisplay('Could not delete certificate');
            var params = {
              'certpath': path
            };
  
            ApiService.deleteCustomCertificate(null, params).then(loadCertificates, errorDisplay);
          };
        }
      };
      return directiveDefinitionObject;
    });
  