package com.airlock.iam.plugin.configeditor;

import com.airlock.iam.admin.domain.model.session.AdminappSession;
import com.airlock.iam.base.api.domain.model.exception.FailureException;
import com.airlock.iam.base.infrastructure.reflection.ReflectionHelper;
import com.airlock.iam.configeditor.shared.application.ConfigEditorProperties;
import com.airlock.iam.configeditor.shared.domain.IamModuleConfigRoot;
import com.airlock.iam.configeditor.shared.infrastructure.ConfigFileModificationWatcher;
import com.airlock.iam.core.misc.authorization.AccessControllerException;
import com.airlock.iam.core.misc.util.config.ActiveLicenseEnvironment;
import com.airlock.iam.core.misc.util.config.Config;
import com.airlock.iam.elog.api.EDefaultLogType;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.elog.api.LogTypes;
import com.airlock.iam.plugin.api.config.ConfigurationContext;
import com.airlock.iam.plugin.api.config.ConfigurationEnvironment;
import com.airlock.iam.plugin.api.config.Environment;
import com.airlock.iam.plugin.api.config.PluginConfiguration;
import com.airlock.iam.plugin.api.exception.ConfigurationException;
import com.airlock.iam.plugin.api.exception.InitializeException;
import com.airlock.iam.plugin.api.exception.PropertyConfigurationException;
import com.airlock.iam.plugin.api.property.PropertyConfiguration;
import com.airlock.iam.plugin.config.manipulator.ConfigManipulationService;
import com.airlock.iam.plugin.config.manipulator.DuplicateDisplayNamesDuringSnippetImportException;
import com.airlock.iam.plugin.config.manipulator.DuplicateIdsDuringSnippetImportException;
import com.airlock.iam.plugin.config.manipulator.DuplicatesDuringSnippetImportException;
import com.airlock.iam.plugin.config.manipulator.OnDisplayNameConflict;
import com.airlock.iam.plugin.config.manipulator.SnippetImportResult;
import com.airlock.iam.plugin.configeditor.data.ConfigContainer;
import com.airlock.iam.plugin.configeditor.data.ConfigEditorState;
import com.airlock.iam.plugin.configeditor.data.EnvironmentSelection;
import com.airlock.iam.plugin.configeditor.data.EnvironmentSpecificPropertyBean;
import com.airlock.iam.plugin.configeditor.data.ErrorBean;
import com.airlock.iam.plugin.configeditor.data.PluginConfigurationFormItem;
import com.airlock.iam.plugin.configeditor.data.PluginFieldsFormItem;
import com.airlock.iam.plugin.configeditor.field.TransparentDnDWrapper;
import com.airlock.iam.plugin.configeditor.session.ConfigEditorSessionAccessor;
import com.airlock.iam.plugin.configeditor.ui.FileDropHandler;
import com.airlock.iam.plugin.configeditor.ui.components.FoldBox;
import com.airlock.iam.plugin.configeditor.ui.components.HeaderUI;
import com.airlock.iam.plugin.configeditor.ui.components.PluginConfigurationForm;
import com.airlock.iam.plugin.configeditor.ui.components.PluginDetailTabsheet;
import com.airlock.iam.plugin.configeditor.ui.components.PluginFieldsForm;
import com.airlock.iam.plugin.configeditor.ui.dialog.ApplyConfigDialog;
import com.airlock.iam.plugin.configeditor.ui.dialog.ConfirmationDialog;
import com.airlock.iam.plugin.configeditor.ui.dialog.EditRootNodeDialog;
import com.airlock.iam.plugin.configeditor.ui.dialog.FileUploadDialog;
import com.airlock.iam.plugin.configeditor.ui.dialog.LoadConfigTemplateDialog;
import com.airlock.iam.plugin.configeditor.ui.dialog.LoadWorkingCopyDialog;
import com.airlock.iam.plugin.configeditor.ui.dialog.OpenOrImportSnippetDialog;
import com.airlock.iam.plugin.configeditor.ui.dialog.SaveWorkingCopyDialog;
import com.airlock.iam.plugin.configeditor.ui.dialog.SnippetImportErrorDialog;
import com.airlock.iam.plugin.configeditor.ui.dialog.SnippetImportModificationSummaryDialog;
import com.airlock.iam.plugin.configeditor.ui.dialog.UnsavedChangesDialog;
import com.airlock.iam.plugin.configeditor.ui.notifications.NotificationMessage;
import com.airlock.iam.plugin.configeditor.ui.panel.PanelsTabSheet;
import com.airlock.iam.plugin.configeditor.ui.plugintrees.PluginTreeUI;
import com.airlock.iam.plugin.configeditor.ui.visibility.UiToggles;
import com.airlock.iam.plugin.configeditor.util.ConfigMimeTypes;
import com.airlock.iam.plugin.configeditor.util.shortcuts.ShortCutActionHeaderUI;
import com.airlock.iam.plugin.configeditor.util.shortcuts.ShortCutActionPluginTree;
import com.airlock.iam.plugin.configeditor.util.shortcuts.ShortCutActionViewTabSheet;
import com.airlock.iam.plugin.configeditor.util.shortcuts.ShortCutHandler;
import com.airlock.iam.plugin.framework.application.configuration.MutablePluginConfiguration;
import com.airlock.iam.plugin.framework.application.service.license.LicenseManager;
import com.airlock.iam.plugin.framework.domain.model.verification.exception.PluginPropertyLicenseViolationException;
import com.airlock.iam.plugin.framework.infrastructure.service.validation.ConfigTreeValidationService;
import com.airlock.iam.plugin.framework.misc.PluginFramework;
import com.airlock.iam.plugin.framework.misc.config.CombiningWalkerScope;
import com.airlock.iam.plugin.framework.misc.config.ConfigHistoryItem;
import com.airlock.iam.plugin.framework.misc.config.ConfigurationEnvironmentGlobalPluginWalkerScope;
import com.airlock.iam.plugin.framework.misc.config.ConfigurationEnvironmentPropertiesWalkerScope;
import com.airlock.iam.plugin.framework.misc.config.ParentWalkerScope;
import com.airlock.iam.plugin.framework.misc.config.PluginConfigurationDictionary;
import com.airlock.iam.plugin.framework.misc.config.SubtreeWalker;
import com.airlock.iam.plugin.framework.misc.config.ValidationEnvironment;
import com.airlock.iam.plugin.framework.misc.ioc.PluginDictionary;
import com.airlock.iam.plugin.framework.misc.ioc.PluginDictionaryLoader;
import com.airlock.iam.plugin.framework.misc.template.ConfigTemplateService;
import com.airlock.iam.plugin.framework.misc.template.ConfigurationTemplate;
import com.airlock.iam.plugin.framework.misc.util.ConfigPersistenceException;
import com.airlock.iam.plugin.framework.misc.util.PluginConfigurationDictionaryUtils;
import com.airlock.iam.sensitivevalues.application.service.SensitiveValuesProvider;
import com.airlock.iam.system.config.ConfigFormat;
import com.airlock.iam.system.property.IamSystemProperties;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Sets;
import com.vaadin.annotations.Theme;
import com.vaadin.annotations.Widgetset;
import com.vaadin.event.ShortcutAction;
import com.vaadin.server.CustomizedSystemMessages;
import com.vaadin.server.Page;
import com.vaadin.server.VaadinRequest;
import com.vaadin.server.VaadinService;
import com.vaadin.server.WrappedSession;
import com.vaadin.server.Page.PopStateEvent;
import com.vaadin.server.Page.PopStateListener;
import com.vaadin.server.Sizeable.Unit;
import com.vaadin.ui.HorizontalSplitPanel;
import com.vaadin.ui.Label;
import com.vaadin.ui.Notification;
import com.vaadin.ui.UI;
import com.vaadin.ui.VerticalLayout;
import com.vaadin.ui.VerticalSplitPanel;
import com.vaadin.ui.Window;
import com.vaadin.ui.Notification.Type;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.annotation.Nullable;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.SortedSet;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import lombok.NonNull;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;

@Theme("medusa")
@Widgetset("com.vaadin.v7.Vaadin7WidgetSet")
public class ConfigEditorApplication extends UI implements PopStateListener {
   public static final String CONFIG_FILE_NAME_DATEFORMAT_STRING = "yyyy-MM-dd_HH-mm-ss-SSS";
   public static final Pattern CONFIG_FILE_TIMESTAMP_PATTERN = Pattern.compile("\\b\\d{4}-\\d{2}-\\d{2}_\\d{2}-\\d{2}-\\d{2}-\\d{3}\\b");
   public static final Locale CONFIG_FILE_NAME_DATEFORMAT_LOCALE = Locale.ENGLISH;
   private static final String APPLICATION_TITLE = "Airlock IAM Configuration Editor";
   private static final String CONFIG_SNIPPET_EDIT_MODE_MSG = "You are in config snippet edit mode and cannot activate this config.";
   public static final String DEFAULT_WORKINGCOPY_DIR = "workingcopies";
   public static final String DEFAULT_DELETED_CONFIGS_DIR = ".deleted-configs";
   private static final ELogger LOG = ELoggerFactory.getLogger(ConfigEditorApplication.class);
   private static final int MEDIA_BREAKPOINT = 1000;
   public static final int INITIAL_HORIZTIONAL_SPLIT_POS = 275;
   private LicenseManager licenseManager;
   private final ConfigEditorState state = new ConfigEditorState();
   private PluginTreeUI pluginTreeUI;
   private HeaderUI headerUI;
   private Label banner;
   private HorizontalSplitPanel horizontalSplit;
   private VerticalSplitPanel verticalSplit;
   private PanelsTabSheet panelsTabSheet;
   private PluginConfigurationForm pluginConfigForm;
   private LoadWorkingCopyDialog loadWorkingCopyDialog;
   private SaveWorkingCopyDialog saveWorkingCopyDialog;
   private LoadConfigTemplateDialog loadConfigTemplateDialog;
   private boolean handleTreeEvents;
   private boolean handleValidation = true;
   private boolean handleTreeUpdates = true;
   private ConfigLoader configLoader;
   private ConfigFileModificationWatcher configFileModificationWatcher;
   private PluginDetailTabsheet pluginDetailTabsheet;
   private boolean linkPluginTreeWithEditor = ConfigEditorProperties.configEditorProperties().getPluginTrackingDefault();
   private final ConfigurationLoadListener configurationLoadListener = new ConfigEditorApplication.ApplicationConfigLoadListener();
   private ApplicationRootInfo applicationRootInfo;
   private UiToggles uiToggles;

   protected void init(VaadinRequest vaadinRequest) {
      this.configLoader = ConfigEditorSessionAccessor.resolve(ConfigLoader.class);
      this.configFileModificationWatcher = ConfigEditorSessionAccessor.resolve(ConfigFileModificationWatcher.class);
      this.applicationRootInfo = loadApplicationRootInfo();
      this.setSessionIdleTimeout(vaadinRequest.getWrappedSession(true));
      this.initLoadingIndicatorConfig();
      this.buildLayout();
      this.configLoader.addConfigurationLoadListener(this.configurationLoadListener);
      this.configLoader.addConfigurationLoadListener(this.headerUI);
      this.licenseManager = ActiveLicenseEnvironment.activeLicenseManager();
      this.configLoader.loadActiveConfig();
      this.startWatchingConfigFile("configEditor (init by request)", IamSystemProperties.getPluginConfig());
      VaadinService.getCurrent().setSystemMessagesProvider(systemMessagesInfo -> {
         CustomizedSystemMessages customizedSystemMessages = new CustomizedSystemMessages();
         customizedSystemMessages.setSessionExpiredNotificationEnabled(false);
         return customizedSystemMessages;
      });
   }

   private void initLoadingIndicatorConfig() {
      this.getLoadingIndicatorConfiguration().setFirstDelay(800);
   }

   @VisibleForTesting
   public static ApplicationRootInfo loadApplicationRootInfo() {
      try {
         Class<?> unchecked = Class.forName("com.airlock.iam.admin.app.infrastructure.AdminAppApplicationRootInfo");
         return (ApplicationRootInfo)ReflectionHelper.newInstance(unchecked);
      } catch (ClassNotFoundException | RuntimeException e) {
         throw new RuntimeException("Could not load application root info: " + e.getMessage(), e);
      }
   }

   private void setSessionIdleTimeout(WrappedSession session) {
      session.setMaxInactiveInterval(Math.toIntExact(Config.getSessionIdleTimeout(ConfigurationContext.defaultContext()).toSeconds()));
   }

   public void detach() {
      super.detach();
      this.configLoader.removeConfigurationLoadListener(this.configurationLoadListener);
   }

   public void loadUploadedConfigOrImportSnippet(File configFile, ConfigFormat configFormat) {
      try {
         if (this.state.getConfigContainer().isPresent() && isSnippet(configFile, configFormat)) {
            this.loadOrImportSnippet(configFile, configFormat, this.state.getConfigContainer().get());
         } else {
            this.loadConfig(configFile, configFormat, true);
         }
      } catch (InitializeException e) {
         LOG.warn(LogTypes.CONFIGURATION_ERROR, "Could not load uploaded config file or snippet: " + e.getMessage(), e);
         this.configLoader.fireFailedToLoadConfigurationEvent(e);
      }
   }

   private void loadOrImportSnippet(File configFile, ConfigFormat configFormat, ConfigContainer currentConfigContainer) {
      OpenOrImportSnippetDialog openOrImportDialog = new OpenOrImportSnippetDialog();
      openOrImportDialog.addLoadConfigButtonListener(() -> {
         if (openOrImportDialog.isLoadSnippet()) {
            this.loadConfig(configFile, configFormat, true);
            openOrImportDialog.close();
         } else {
            this.importSnippet(configFile, currentConfigContainer, openOrImportDialog);
            openOrImportDialog.close();
         }
      });
   }

   private void importSnippet(File configFile, ConfigContainer currentConfigContainer, OpenOrImportSnippetDialog dialog) {
      try {
         File tempConfigFile = this.temporarilyPersistActiveConfiguration(currentConfigContainer);
         ConfigManipulationService manipulationService = createManipulationService(tempConfigFile, currentConfigContainer.getDictionary());
         SnippetImportResult result = manipulationService.importConfigurationSnippet(
            configFile.toPath(), this.resolveDisplayNameConflict(dialog), dialog.getIdSuffix()
         );
         FileUtils.deleteQuietly(tempConfigFile);
         if (!result.hasFailures()) {
            this.applyImportResultToActiveConfig(currentConfigContainer, result);
         }

         if (result.hasModificationResults()) {
            this.showModificationSummary(result);
         }
      } catch (DuplicateIdsDuringSnippetImportException e) {
         this.showDuplicateIdsError(e);
      } catch (DuplicateDisplayNamesDuringSnippetImportException e) {
         this.showDuplicateDisplayNamesError(e);
      } catch (ConfigPersistenceException | IOException e) {
         this.showGenericImportError(e);
      }
   }

   @VisibleForTesting
   static ConfigManipulationService createManipulationService(File tempConfigFile, PluginConfigurationDictionary activeDictionary) {
      return new ConfigManipulationService(
         PluginConfigurationDictionaryUtils.loadDictionaryFromFile(
            tempConfigFile.toPath(), ConfigFormat.YAML_FORMAT, activeDictionary::getSensitiveValuesProvider, activeDictionary.getEnvironment()
         ),
         "plugin dictionary"
      );
   }

   private OnDisplayNameConflict resolveDisplayNameConflict(OpenOrImportSnippetDialog dialog) {
      return dialog.isRenameDisplayNameOnConflict() ? OnDisplayNameConflict.RENAME : OnDisplayNameConflict.FAIL;
   }

   private void applyImportResultToActiveConfig(ConfigContainer currentConfigContainer, SnippetImportResult result) {
      currentConfigContainer.updateDictionary(result.dictionary());
      this.configLoader.fireConfigurationLoadedEvent(ConfigurationLoadListener.ConfigType.ACTIVE, currentConfigContainer);
   }

   private void showModificationSummary(SnippetImportResult result) {
      new SnippetImportModificationSummaryDialog(result);
   }

   private void showDuplicateIdsError(DuplicateIdsDuringSnippetImportException e) {
      new SnippetImportErrorDialog(
         e.format(
            DuplicatesDuringSnippetImportException.MULTILINE_FORMATTER,
            "To automatically append a suffix to every ID in the snippet, use the option 'Suffix to append to plugin IDs'."
         )
      );
   }

   private void showDuplicateDisplayNamesError(DuplicateDisplayNamesDuringSnippetImportException e) {
      new SnippetImportErrorDialog(
         e.format(
            DuplicatesDuringSnippetImportException.MULTILINE_FORMATTER,
            "To automatically rename conflicting display names, use the option 'Automatically resolve display name conflicts'."
         )
      );
   }

   private void showGenericImportError(Exception e) {
      new SnippetImportErrorDialog(e.getMessage());
   }

   private static boolean isSnippet(File fileToLoad, ConfigFormat configFormat) throws InitializeException {
      return configFormat == ConfigFormat.XML_FORMAT ? false : PluginFramework.getConfigReader(configFormat).readFromFile(fileToLoad).isDocumentTypeSnippet();
   }

   public void loadConfig(File configFile, ConfigFormat configFormat, boolean deleteConfigAfterLoad) {
      if (this.isConfigurationModified()) {
         ConfirmationDialog confirmDialog = new UnsavedChangesDialog();
         confirmDialog.setId("unsavedChangesDialog");
         confirmDialog.addCloseListener(closeEvent -> {
            if (confirmDialog.isConfirmed()) {
               this.configLoader.loadDetachedConfig(configFile, configFormat, false, deleteConfigAfterLoad);
            }
         });
      } else {
         this.configLoader.loadDetachedConfig(configFile, configFormat, false, deleteConfigAfterLoad);
      }
   }

   private File temporarilyPersistActiveConfiguration(ConfigContainer configContainer) throws IOException {
      File importBaseConfigFile = File.createTempFile("tmp.iam-config", ".yaml", IamSystemProperties.getWorkDirectory().toFile());
      PluginConfigurationDictionaryUtils.atomicWriteDictionaryToFile(configContainer.getDictionary(), ConfigFormat.YAML_FORMAT, importBaseConfigFile.toPath());
      return importBaseConfigFile;
   }

   private void switchEditorToNewConfiguration(ConfigContainer configContainer) {
      if (configContainer != null) {
         this.pluginTreeUI.setConfiguration(configContainer);
         this.state.setConfigContainer(configContainer);
         this.updateHeaderUI(configContainer);
         this.panelsTabSheet.updateConfigurationEnvironment();
         this.selectConfiguration(null);
         this.pluginDetailTabsheet.setDefaultContext();
         this.updateFormFields();
         if (configContainer.getDictionary().isDocumentTypeSnippet()) {
            this.pluginTreeUI.selectUnconnectedPluginsTree();
         } else {
            this.pluginTreeUI.selectModulesTree();
         }

         this.updateValidationState();
         this.panelsTabSheet.updateSearchPanel();
         this.panelsTabSheet.updateVariablesPanel();
      }
   }

   private void updateHeaderUI(ConfigContainer configuration) {
      this.headerUI.enableDownloadConfigMenuItem(true);
      this.headerUI.setConfigDependentHeaderIconsVisibility(true, !isSavingForbidden(configuration));
      this.headerUI.setConfigFileLabel(configuration.getConfigFileLabel());
      SortedSet<ConfigurationEnvironment> environmentsFromConfig = configuration.getDictionary().getConfigurationEnvironments();
      this.headerUI.updateConfigWarning(this.state.getInstanceEnvironment(), environmentsFromConfig);
      configuration.getDictionary().getSensitiveValuesProvider().ifPresent(this.headerUI::setSensitiveValuesProvider);
      this.headerUI.initEnvironmentSelection(environmentsFromConfig);
      this.headerUI.updateDownloadConfigurationResource();
   }

   private static boolean isSavingForbidden(ConfigContainer configuration) {
      return !IamSystemProperties.isYAMLConfigFormatEnabled() && configuration.getDictionary().isDocumentTypeSnippet();
   }

   public void refresh() {
      if (this.state.getConfigContainer().isPresent()) {
         this.updateValidationState();
         this.panelsTabSheet.updateConfigurationEnvironment();
         this.selectConfiguration(this.state.getSelectedConfiguration().orElse(null));
      }
   }

   private void buildLayout() {
      Page.getCurrent().setTitle("Airlock IAM Configuration Editor");
      VerticalLayout mainLayout = new VerticalLayout();
      mainLayout.setSizeFull();
      mainLayout.setSpacing(false);
      mainLayout.setMargin(false);
      this.banner = new Label("You are in config snippet edit mode and cannot activate this config.");
      this.banner.setId("banner");
      this.banner.addStyleName("headbanner");
      this.banner.setWidth(100.0F, Unit.PERCENTAGE);
      this.banner.setVisible(false);
      mainLayout.addComponent(this.banner);
      this.headerUI = new HeaderUI(this.state, this.applicationRootInfo);
      this.headerUI.setId("headerUI");
      mainLayout.addComponent(this.headerUI);
      this.uiToggles = new UiToggles();
      mainLayout.addComponent(this.uiToggles);
      this.horizontalSplit = new HorizontalSplitPanel();
      this.horizontalSplit.addStyleName("mainsplit");
      mainLayout.addComponent(this.horizontalSplit);
      mainLayout.setExpandRatio(this.horizontalSplit, 1.0F);
      this.horizontalSplit.setSplitPosition(275.0F, Unit.PIXELS);
      this.pluginTreeUI = new PluginTreeUI();
      this.pluginTreeUI.addStyleName("plugintreeui");
      this.pluginTreeUI.setId("pluginTreeUI");
      TransparentDnDWrapper pluginTreeDnD = new TransparentDnDWrapper(this.pluginTreeUI);
      pluginTreeDnD.setDropHandler(new FileDropHandler(this));
      pluginTreeDnD.setStyleName("dndplugintree");
      pluginTreeDnD.setSizeFull();
      this.horizontalSplit.setSizeFull();
      this.horizontalSplit.setFirstComponent(pluginTreeDnD);
      this.addResponsivePluginTreeResizeListener();
      VerticalLayout pluginDetailLayout = new VerticalLayout();
      pluginDetailLayout.setSpacing(false);
      pluginDetailLayout.setMargin(false);
      pluginDetailLayout.addStyleName("plugindetail");
      pluginDetailLayout.setId("pluginConfigurationUI");
      this.pluginConfigForm = new PluginConfigurationForm(this);
      this.pluginConfigForm.addStyleName("pluginconfigform");
      pluginDetailLayout.addComponent(this.pluginConfigForm);
      this.pluginDetailTabsheet = new PluginDetailTabsheet();
      this.pluginDetailTabsheet.getTabsheet().setId("pluginContextTabsContainer");
      pluginDetailLayout.addComponent(this.pluginDetailTabsheet.getTabsheet());
      this.verticalSplit = new VerticalSplitPanel();
      this.verticalSplit.setSplitPosition(150.0F, Unit.PIXELS, true);
      this.verticalSplit.setFirstComponent(pluginDetailLayout);
      this.panelsTabSheet = new PanelsTabSheet(this.state, this::errorSelected, this::environmentSpecificPropertySelected);
      this.verticalSplit.setSecondComponent(this.panelsTabSheet.getContent());
      this.horizontalSplit.setSecondComponent(this.verticalSplit);
      this.setFormContentVisible(false);
      this.updateValidationState();
      UI.getCurrent().setContent(mainLayout);
      Page.getCurrent().addPopStateListener(this);
      this.addActionHandler(new ShortCutHandler(this.createShortCutsActions()));
   }

   private void addResponsivePluginTreeResizeListener() {
      this.getPage().addBrowserWindowResizeListener(event -> {
         if (event.getWidth() <= 1000) {
            if (this.getUiToggles().isPluginTreeVisible()) {
               this.getUiToggles().toggleTree();
            }
         } else if (!this.getUiToggles().isPluginTreeVisible()) {
            this.getUiToggles().toggleTree();
         }
      });
   }

   private boolean isConfigurationModified() {
      return this.state.getConfigContainer().map(ConfigContainer::isConfigurationModified).orElse(false);
   }

   public void updateValidationState() {
      if (this.handleValidation) {
         if (this.getValidationEnvironment() == null) {
            this.state.setGlobalErrors(Collections.emptySet());
            this.state.setErrorsOfSelectedPlugin(Collections.emptySet());
            this.headerUI.disableValidationIcons();
            this.updateTrees();
            this.state.setValidationEnabled(false);
            this.panelsTabSheet.updateErrorPanel();
         } else {
            this.state.setValidationEnabled(true);
            this.updateValidationState(this.getValidationEnvironment());
         }
      }
   }

   private void updateValidationState(@NonNull Environment environment) {
      if (environment == null) {
         throw new NullPointerException("environment is marked non-null but is null");
      }

      if (this.state.getConfigContainer().isEmpty()) {
         this.headerUI.disableValidationGlobalIcon();
         this.state.setGlobalErrors(Collections.emptySet());
      } else {
         Set<ConfigurationException> exceptions = this.validateConfiguration(environment);
         if (exceptions.isEmpty()) {
            this.state.setGlobalErrors(Collections.emptySet());
            this.headerUI.setValidationGlobalSuccessful();
         } else {
            this.state.setGlobalErrors(exceptions);
            this.headerUI.setValidationGlobalError(exceptions);
         }

         this.updateErrorsOfSelectedPlugin();
         this.updateTrees();
         this.state.setLastValidatedConfiguration(this.state.getSelectedConfiguration().orElse(null));
      }

      this.panelsTabSheet.updateErrorPanel();
   }

   private void updateErrorsOfSelectedPlugin() {
      if (!this.state.getSelectedConfiguration().isEmpty() && this.getValidationEnvironment() != null) {
         MutablePluginConfiguration selectedPlugin = this.state.getSelectedConfiguration().orElseThrow();
         ConfigurationContext selectedContext = this.pluginDetailTabsheet.getSelectedContext();
         Set<ConfigurationException> exceptions = this.state
            .getGlobalErrors()
            .stream()
            .filter(e -> e.getPluginConfiguration().equals(selectedPlugin))
            .filter(e -> e.getConfigurationContext().equals(selectedContext))
            .collect(Collectors.toSet());
         exceptions.addAll(this.getLicenseManager().getConfigLicenseViolations(selectedPlugin, this.getSelectedConfigurationEnvironment(), selectedContext));
         this.state.setErrorsOfSelectedPlugin(exceptions);
         if (exceptions.isEmpty()) {
            this.headerUI.setValidationSelectionSuccessful();
         } else {
            this.headerUI.setValidationSelectionError(exceptions);
         }

         PluginFieldsForm activePluginFieldsForm = this.pluginDetailTabsheet.getActivePluginFieldsForm();
         if (activePluginFieldsForm != null) {
            activePluginFieldsForm.markAsDirtyRecursive();
         }

         this.pluginDetailTabsheet.updateConfigurationContextTabCaptions(selectedPlugin);
         this.updateGroupValidationErrors();
      } else {
         this.state.setErrorsOfSelectedPlugin(Collections.emptySet());
         this.headerUI.disableValidationSelectionIcon();
         this.updateGroupValidationErrors();
      }
   }

   private Set<ConfigurationException> validateConfiguration(Environment environment) {
      List<String> externalSecretsKeys = this.getExternalDefinedSecretKeys();
      ValidationEnvironment validationEnvironment = ValidationEnvironment.builder()
         .environment(environment)
         .rootDir(IamSystemProperties.getRootDirectory())
         .externalSecretsKeys(externalSecretsKeys)
         .instanceEnvironment(this.state.getInstanceEnvironment())
         .build();
      Set<ConfigurationException> exceptions = new HashSet<>();
      PluginConfigurationDictionary dictionary = this.state.getConfigContainer().orElseThrow().getDictionary();
      List<PluginConfiguration> roots = Arrays.stream(IamModuleConfigRoot.values())
         .map(rootApp -> dictionary.getByDisplayName(rootApp.getPluginDisplayName()))
         .filter(Objects::nonNull)
         .map(PluginConfiguration.class::cast)
         .toList();
      this.validateConfiguration(roots, validationEnvironment).forEach(e -> exceptions.add(getNestedConfigException(e)));
      return exceptions;
   }

   private static ConfigurationException getNestedConfigException(ConfigurationException e) {
      if (e.getCause() == null) {
         return e;
      } else {
         return e.getCause() instanceof ConfigurationException configurationException ? getNestedConfigException(configurationException) : e;
      }
   }

   private Set<ConfigurationException> validateConfiguration(List<PluginConfiguration> plugins, ValidationEnvironment validationEnvironment) {
      Set<ConfigurationContext> contexts = this.state.getConfigContainer().orElseThrow().getDictionary().getConfigurationContexts();
      ConfigurationEnvironment selectedConfigurationEnvironment = this.getSelectedConfigurationEnvironment();
      ConfigTreeValidationService validationService = new ConfigTreeValidationService(plugins, selectedConfigurationEnvironment, true);
      Set<ConfigurationException> exceptions = new LinkedHashSet<>(validationService.validateTreeAndCollect(contexts, validationEnvironment));
      plugins.forEach(pc -> exceptions.addAll(this.licenseManager.getConfigLicenseViolationsRecursive(pc, selectedConfigurationEnvironment, contexts)));
      return exceptions;
   }

   private List<String> getExternalDefinedSecretKeys() {
      Optional<SensitiveValuesProvider> sensitiveValuesProvider = this.getConfigurationDictionary().getSensitiveValuesProvider();
      return sensitiveValuesProvider.map(SensitiveValuesProvider::getAllDefinedKeys).orElse(new ArrayList<>());
   }

   public Environment getValidationEnvironment() {
      return this.headerUI.getSelectedValidationEnvironment();
   }

   public void loadConfigTemplate(File templateFile) {
      this.loadConfig(templateFile, ConfigFormat.XML_FORMAT, true);
   }

   public void uploadConfiguration() {
      FileUploadDialog dialog = new FileUploadDialog();
      UI.getCurrent().addWindow(dialog);
      dialog.focus();
      dialog.setId("uploadConfigDialog");
      dialog.addCloseListener(e -> {
         FileUploadDialog fileUploadDialog = (FileUploadDialog)e.getSource();
         File uploadedFile = fileUploadDialog.getUploadedFile();
         if (uploadedFile != null) {
            String mimeType = fileUploadDialog.getMimeType();
            ConfigFormat configFormat = ConfigMimeTypes.configFormatForMimeType(mimeType);
            LOG.info("Uploading config file. Mime type is '" + mimeType + "'. Assuming config format " + configFormat);
            this.loadUploadedConfigOrImportSnippet(uploadedFile, configFormat);
         }
      });
   }

   private void displayPluginConfigurationDetails(MutablePluginConfiguration pc) {
      this.handleValidation = false;
      this.handleTreeUpdates = false;
      if (pc != null) {
         this.displayTabsheetForPluginConfiguration(pc);
      } else {
         this.disableTabsheet();
      }

      this.handleValidation = true;
      this.handleTreeUpdates = true;
      if (pc != null) {
         this.updateErrorsOfSelectedPlugin();
      }
   }

   private void displayTabsheetForPluginConfiguration(MutablePluginConfiguration pc) {
      this.pluginConfigForm.setItemDataSource(new PluginConfigurationFormItem(pc));
      this.updateTabsheetForPluginConfiguration(pc);
      this.setFormContentVisible(true);
   }

   private void disableTabsheet() {
      this.pluginDetailTabsheet.setHandleTabEvents(false);
      this.pluginConfigForm.setItemDataSource(null);
      this.pluginDetailTabsheet.clean();
      this.setFormContentVisible(false);
   }

   private void updateTabsheetForPluginConfiguration(MutablePluginConfiguration pc) {
      this.pluginDetailTabsheet.setHandleTabEvents(false);
      this.pluginDetailTabsheet.clean();
      boolean suitableForContexts = this.isSuitableForContexts(pc);
      String selectedContextName = ConfigurationContext.defaultContext().toString();
      if (suitableForContexts) {
         selectedContextName = this.pluginDetailTabsheet.getSelectedContext().toString();
      }

      for (ConfigurationContext context : this.getUsableContexts(suitableForContexts)) {
         this.createFieldsFormForContext(pc, selectedContextName, context, suitableForContexts);
      }

      this.pluginDetailTabsheet.hideContextTabs(!suitableForContexts);
      this.pluginDetailTabsheet.updateTabPositions();
      this.pluginDetailTabsheet.setHandleTabEvents(true);
   }

   private Set<ConfigurationContext> getUsableContexts(boolean suitableForContexts) {
      Set<ConfigurationContext> contexts;
      if (suitableForContexts) {
         contexts = this.state.getConfigContainer().orElseThrow().getDictionary().getConfigurationContexts();
      } else {
         contexts = Sets.newHashSet(new ConfigurationContext[]{ConfigurationContext.defaultContext()});
      }

      return contexts;
   }

   private void createFieldsFormForContext(MutablePluginConfiguration pc, String selectedContextName, ConfigurationContext context, boolean suitableForContexts) {
      PluginFieldsForm pff = new PluginFieldsForm(suitableForContexts);
      if (Objects.equals(context.toString(), selectedContextName)) {
         pff.setItemDataSource(new PluginFieldsFormItem(pc, this.getSelectedConfigurationEnvironment(), context));
      }

      this.pluginDetailTabsheet.enableStyleForPropertyGroups(pff.getGroupFormLayout().isGroupsInitiated());
      this.pluginDetailTabsheet.setContextTabCaptions(selectedContextName, context, pff);
   }

   public ConfigurationEnvironment getSelectedConfigurationEnvironment() {
      return this.getEnvironmentSelection().getSelectedEnvironment();
   }

   public EnvironmentSelection getEnvironmentSelection() {
      return this.state.getEnvironmentSelection();
   }

   public ConfigurationEnvironment getInstanceEnvironment() {
      return this.state.getInstanceEnvironment();
   }

   boolean isSuitableForContexts(MutablePluginConfiguration pc) {
      return IamModuleConfigRoot.contextAwareConfigRoots()
         .stream()
         .map(configRoot -> this.getConfigurationDictionary().getByDisplayName(configRoot.getPluginDisplayName()))
         .anyMatch(rootConfig -> this.isPluginConfigConnectedToModuleRoot(pc, rootConfig));
   }

   private boolean isPluginConfigConnectedToModuleRoot(MutablePluginConfiguration pc, MutablePluginConfiguration moduleRoot) {
      if (moduleRoot != null) {
         ConfigurationEnvironment env = this.getSelectedConfigurationEnvironment();
         CombiningWalkerScope scope = CombiningWalkerScope.with()
            .scope(new ConfigurationEnvironmentPropertiesWalkerScope(env))
            .scope(new ParentWalkerScope())
            .scope(new ConfigurationEnvironmentGlobalPluginWalkerScope(env))
            .combine();
         SubtreeWalker walker = new SubtreeWalker(moduleRoot, scope);

         while (walker.hasNext()) {
            MutablePluginConfiguration pcToInspect = walker.next();
            if (pcToInspect.equals(pc) || pcToInspect.equals(pc.getParentConfiguration())) {
               return true;
            }
         }
      }

      return false;
   }

   private void setFormContentVisible(boolean visible) {
      VerticalLayout formContentLayout = (VerticalLayout)((VerticalSplitPanel)this.horizontalSplit.getSecondComponent()).getFirstComponent();
      this.pluginConfigForm.setVisible(visible);
      this.pluginDetailTabsheet.showHideFormTabSheet(visible, formContentLayout);
   }

   public PluginConfigurationDictionary getConfigurationDictionary() {
      return this.state.getConfigContainer().orElseThrow().getDictionary();
   }

   public PluginDictionary getPluginDictionary() {
      PluginDictionaryLoader loader = ConfigEditorSessionAccessor.resolve(PluginDictionaryLoader.class);
      return loader.getPluginDictionary();
   }

   public LicenseManager getLicenseManager() {
      return this.licenseManager;
   }

   public void selectConfigurationButUnselectTreeItems(MutablePluginConfiguration configuration) {
      if (!this.isPluginTreeLinkedWithEditor() && configuration != null) {
         this.setSelectedPluginConfiguration(configuration);
         this.pluginTreeUI.unselectPluginConfiguration();
         this.displayPluginConfigurationDetails(configuration);
      } else {
         this.selectConfiguration(configuration);
      }
   }

   public void selectConfiguration(MutablePluginConfiguration configuration) {
      if (configuration == null) {
         this.unselectConfiguration();
      }

      this.setSelectedPluginConfiguration(configuration);
      this.pluginTreeUI.selectPluginConfiguration(configuration);
      this.displayPluginConfigurationDetails(configuration);
   }

   public void selectConfigurationField(MutablePluginConfiguration parentConfiguration, PropertyConfiguration propertyConfiguration) {
      this.selectConfiguration(parentConfiguration);
      PluginFieldsForm pluginFieldsForm = this.pluginDetailTabsheet.getActivePluginFieldsForm();
      pluginFieldsForm.showPropertyField(parentConfiguration, propertyConfiguration.getName());
   }

   private void unselectConfiguration() {
      this.setURIFragment();
      this.pluginTreeUI.unselectPluginConfiguration();
      this.displayPluginConfigurationDetails(null);
      this.state.setSelectedConfiguration(null);
   }

   public void updateFormFields() {
      this.state.getSelectedConfiguration().ifPresent(selectedConfiguration -> {
         int selectedPosition = this.pluginDetailTabsheet.getSelectedTabPosition();
         this.displayPluginConfigurationDetails(selectedConfiguration);
         this.pluginDetailTabsheet.setSelectedTab(selectedPosition);
         this.pluginDetailTabsheet.setHandleTabEvents(true);
      });
   }

   public void updateTrees() {
      if (this.handleTreeUpdates && this.state.getConfigContainer().isPresent()) {
         this.setHandleTreeEvents(false);
         this.pluginTreeUI.updateTrees();
         this.pluginTreeUI.selectPluginConfiguration(this.state.getSelectedConfiguration().orElse(null));
         this.setHandleTreeEvents(true);
      }
   }

   public void showSaveWorkingCopyDialog() {
      if (this.saveWorkingCopyDialog == null) {
         this.saveWorkingCopyDialog = new SaveWorkingCopyDialog(this);
         UI.getCurrent().addWindow(this.saveWorkingCopyDialog);
         this.saveWorkingCopyDialog.addCloseListener(e -> this.saveWorkingCopyDialog = null);
      }
   }

   public void selectErrorPanelInViewsTab() {
      this.getUiToggles().resetTabsheetPosition();
      this.panelsTabSheet.selectErrorPanel();
   }

   public void selectDocsPanelInViewsTab() {
      this.getUiToggles().resetTabsheetPosition();
      this.panelsTabSheet.selectDocsPanel();
   }

   public void showLoadWorkingCopyDialog() {
      if (this.loadWorkingCopyDialog == null) {
         if (this.getWorkingCopyDirectory() != null) {
            this.loadWorkingCopyDialog = new LoadWorkingCopyDialog();
            UI.getCurrent().addWindow(this.loadWorkingCopyDialog);
            this.loadWorkingCopyDialog.setId("restoreConfigDialog");
            this.loadWorkingCopyDialog.addCloseListener(e -> this.loadWorkingCopyDialog = null);
         } else {
            Notification.show("Path to working copy directory is not configured.", Type.WARNING_MESSAGE);
         }
      }
   }

   public void showLoadConfigTemplateDialog() {
      List<ConfigurationTemplate> templates = IamSystemProperties.getConfigTemplates()
         .map(ConfigTemplateService::new)
         .map(ConfigTemplateService::listTemplates)
         .orElse(Collections.emptyList());
      if (templates.isEmpty()) {
         Notification.show(
            "No configuration templates could be found!"
               + IamSystemProperties.getConfigTemplates().map(templatesDir -> " Looking in directory:" + templatesDir).orElse(""),
            Type.WARNING_MESSAGE
         );
      } else {
         if (this.loadConfigTemplateDialog == null) {
            this.loadConfigTemplateDialog = new LoadConfigTemplateDialog();
            UI.getCurrent().addWindow(this.loadConfigTemplateDialog.getWindow());
            this.loadConfigTemplateDialog.getWindow().setId("loadConfigTemplateDialog");
            this.loadConfigTemplateDialog.getWindow().addCloseListener(e -> this.loadConfigTemplateDialog = null);
         }

         this.loadConfigTemplateDialog.setTemplates(templates);
      }
   }

   public void applyConfiguration() {
      if (this.state.getConfigContainer().isEmpty()) {
         Notification.show("No configuration loaded.", Type.HUMANIZED_MESSAGE);
      } else {
         Optional<AdminappSession> session = ConfigEditorSessionAccessor.getCachedAdminappSession();
         if (session.isEmpty()) {
            UI.getCurrent().getPage().reload();
         } else if (this.isApplyConfigAllowed(session.get())) {
            ConfigurationEnvironment previousEnvironment = this.state.getEnvironmentSelection().getSelectedEnvironment();
            this.state.getEnvironmentSelection().setSelectedEnvironment(this.state.getInstanceEnvironment());
            this.showApplyConfigDialog(() -> this.state.getEnvironmentSelection().setSelectedEnvironment(previousEnvironment));
         } else {
            throw new FailureException("Not allowed to apply configuration.");
         }
      }
   }

   private void showApplyConfigDialog(Runnable afterValidation) {
      ApplyConfigDialog dialog = new ApplyConfigDialog(this.applicationRootInfo.shouldLogConfigurationChanges());
      this.addWindow(dialog);
      int previousPollingInterval = this.getPollInterval();
      this.setPollInterval(100);

      try (ExecutorService service = Executors.newSingleThreadExecutor()) {
         service.execute(() -> this.access(() -> {
            this.updateValidationState(Environment.CURRENT_VM);
            dialog.updateValidationPanel();
            this.setPollInterval(previousPollingInterval);
            afterValidation.run();
         }));
      }
   }

   public boolean isValidConfiguration() {
      return this.state.getGlobalErrors().isEmpty();
   }

   public Set<ConfigurationException> getErrorsOfSelectedPluginConfiguration() {
      return this.state.getErrorsOfSelectedPlugin();
   }

   private void errorSelected(ErrorBean error) {
      MutablePluginConfiguration pc = (MutablePluginConfiguration)error.getException().getPluginConfiguration();
      this.setSelectedContext(error.getException().getConfigurationContext());
      this.selectConfigurationButUnselectTreeItems(pc);
   }

   public MutablePluginConfiguration getSelectedConfiguration() {
      return this.state.getSelectedConfiguration().orElse(null);
   }

   private void setSelectedPluginConfiguration(MutablePluginConfiguration configuration) {
      this.state.setSelectedConfiguration(configuration);
      this.setURIFragment();
   }

   public Optional<String> getConfigFileToLoad() {
      return this.configLoader.getConfigFileToLoad();
   }

   public Duration getActivationTimeout() {
      return this.configLoader.getActivationTimeout();
   }

   public String getWorkingCopyDirectory() {
      return this.configLoader.getWorkingCopyDirectory();
   }

   public boolean isHandleTreeEvents() {
      return this.handleTreeEvents;
   }

   public void uriChanged(PopStateEvent event) {
      String fragment = event.getPage().getUriFragment();
      if (StringUtils.isNotEmpty(fragment)) {
         if (this.state.getConfigContainer().isEmpty()) {
            Page.getCurrent().setUriFragment(null);
            return;
         }

         for (Window childWindow : UI.getCurrent().getWindows()) {
            if (childWindow.isModal()) {
               this.setURIFragment();
               return;
            }
         }

         int fragmentConfigHash;
         try {
            fragmentConfigHash = Integer.parseInt(fragment);
         } catch (NumberFormatException e) {
            this.setURIFragment();
            return;
         }

         if (this.state.getSelectedConfiguration().isPresent() && System.identityHashCode(this.state.getSelectedConfiguration().get()) == fragmentConfigHash) {
            return;
         }

         MutablePluginConfiguration matchingConfig = this.state.getConfigContainer().get().getDictionary().getByIdentityHashCode(fragmentConfigHash);
         if (matchingConfig != null) {
            this.selectConfiguration(matchingConfig);
         } else {
            this.setURIFragment();
         }
      }
   }

   private void setURIFragment() {
      MutablePluginConfiguration pc = this.getSelectedConfiguration();
      if (pc != null) {
         Page.getCurrent().setUriFragment(String.valueOf(System.identityHashCode(pc)));
         Page.getCurrent().setTitle("Airlock IAM Configuration Editor (" + pc + ")");
      } else {
         Page.getCurrent().setUriFragment(Page.getCurrent().getUriFragment());
         Page.getCurrent().setTitle("Airlock IAM Configuration Editor");
      }
   }

   private boolean isApplyConfigAllowed(AdminappSession session) {
      try {
         return session.isAuthenticated() && this.applicationRootInfo.isApplyConfigAllowedFor(session.getAuthentee().orElseThrow());
      } catch (AccessControllerException e) {
         throw new IllegalStateException("Error occurred during access control decision.", e);
      }
   }

   public void close() {
      if (this.state.getConfigContainer().isPresent() && this.isConfigurationModified()) {
         this.state
            .getConfigContainer()
            .get()
            .getDictionary()
            .addHistoryItem(
               new ConfigHistoryItem(
                  ConfigEditorSessionAccessor.getEditorUserName(), new Date(), "Session timeout: Automatically saved draft to prevent data loss."
               )
            );
         this.state.getConfigContainer().get().saveDraft();
         LOG.debug("Application closed with unsaved data: Saved draft configuration to prevent data loss.");
      }

      super.close();
   }

   public PluginTreeUI getPluginTreeUI() {
      return this.pluginTreeUI;
   }

   public void setRootNode() {
      if (this.state.getConfigContainer().isEmpty()) {
         Notification.show("No configuration loaded.", Type.HUMANIZED_MESSAGE);
      } else {
         EditRootNodeDialog dialog = new EditRootNodeDialog(this.getConfigurationDictionary());
         dialog.setId("editRootNodeDialog");
         UI.getCurrent().addWindow(dialog);
         dialog.addCloseListener(e -> {
            this.selectConfiguration(null);
            this.pluginDetailTabsheet.setDefaultContext();
            this.updateFormFields();
            this.updateValidationState();
         });
      }
   }

   @Nullable
   public ConfigContainer getConfigContainer() {
      return this.state.getConfigContainer().orElse(null);
   }

   private void updateGroupValidationErrors() {
      PluginFieldsForm pff = this.pluginDetailTabsheet.getActivePluginFieldsForm();
      if (pff != null) {
         for (FoldBox foldBox : pff.getGroupFormLayout().getPropertyGroups().values()) {
            foldBox.setErrorsOnProperties(false);
            foldBox.setLicenseViolationsOnProperties(false);
            this.checkFoldBoxOnValidationErrorsAndLicenseViolations(foldBox);
            foldBox.setValidationStateOnUI();
         }
      }
   }

   private void checkFoldBoxOnValidationErrorsAndLicenseViolations(FoldBox foldBox) {
      for (ConfigurationException exception : this.getErrorsOfSelectedPluginConfiguration()) {
         if (exception instanceof PropertyConfigurationException propertyConfigException) {
            List<String> propertyNamesWithError = propertyConfigException.getPropertyNames();
            this.setErrorsOnFoldBox(foldBox, propertyConfigException, propertyNamesWithError);
         }
      }
   }

   private void setErrorsOnFoldBox(FoldBox foldBox, PropertyConfigurationException propertyConfigException, List<String> propertyNamesWithError) {
      for (String propertyName : propertyNamesWithError) {
         this.setErrorOnFoldbox(foldBox, propertyConfigException, propertyName);
      }
   }

   private void setErrorOnFoldbox(FoldBox foldBox, PropertyConfigurationException propertyConfigException, String propertyNameWithError) {
      if (foldBox.containsPropertyWithName(propertyNameWithError)) {
         foldBox.setErrorsOnProperties(true);
         if (propertyConfigException instanceof PluginPropertyLicenseViolationException) {
            foldBox.setLicenseViolationsOnProperties(true);
         }
      }
   }

   private ShortcutAction[] createShortCutsActions() {
      return new ShortcutAction[]{new ShortCutActionPluginTree(this), new ShortCutActionHeaderUI(this), new ShortCutActionViewTabSheet(this)};
   }

   public UiToggles getUiToggles() {
      return this.uiToggles;
   }

   public Set<ConfigurationException> getGlobalErrors() {
      return this.state.getGlobalErrors();
   }

   public void displayDocsText(String text) {
      this.state.setDocsText(text);
      this.panelsTabSheet.updateDocsPanel();
   }

   public void setSelectedContext(ConfigurationContext configurationContext) {
      this.pluginDetailTabsheet.setSelectedContext(configurationContext);
   }

   public ConfigurationContext getSelectedContext() {
      return this.pluginDetailTabsheet.getSelectedContext();
   }

   public void setLinkPluginTreeWithEditor(boolean selectPlugins) {
      this.linkPluginTreeWithEditor = selectPlugins;
      if (this.linkPluginTreeWithEditor && this.state.getSelectedConfiguration().isPresent()) {
         this.pluginTreeUI.selectPluginConfiguration(this.state.getSelectedConfiguration().get());
      }
   }

   public boolean isPluginTreeLinkedWithEditor() {
      return this.linkPluginTreeWithEditor;
   }

   public void updateEnvironmentValuesState() {
      this.panelsTabSheet.updateConfigurationEnvironment();
      this.headerUI.updateDownloadConfigurationResource();
   }

   private void environmentSpecificPropertySelected(EnvironmentSpecificPropertyBean environmentSpecificProperty) {
      this.setSelectedContext(environmentSpecificProperty.getConfigurationContext());
      this.selectConfigurationButUnselectTreeItems(environmentSpecificProperty.getPluginConfiguration());
   }

   public void updateVariablesPanel() {
      this.panelsTabSheet.updateVariablesPanel();
   }

   private void showConfigSnippetEditBanner(boolean isSnippet) {
      this.banner.setVisible(isSnippet);
   }

   private void startWatchingConfigFile(String fileWatcherCreator, Path configFile) {
      this.configFileModificationWatcher
         .startWatching(fileWatcherCreator, configFile, () -> this.access(() -> this.showConfigFileModificationMessage(configFile)));
   }

   private void showConfigFileModificationMessage(Path configPath) {
      Optional<ConfigHistoryItem> lastHistoryItem;
      try {
         List<ConfigHistoryItem> history = PluginFramework.getTranslatedConfigReader(IamSystemProperties.activeConfigFormat())
            .loadConfigHistory(configPath.toFile());
         lastHistoryItem = history.stream().reduce((first, last) -> last);
      } catch (InitializeException e) {
         LOG.warn(EDefaultLogType.GENERIC_LOG_TYPE, "Failed to read most recent config history item.", e);
         lastHistoryItem = Optional.empty();
      }

      Notification.show(
         "Active configuration changed.",
         NotificationMessage.externalConfigFileModificationMessage(lastHistoryItem.map(ConfigHistoryItem::getDisplayText)),
         Type.ERROR_MESSAGE
      );
   }

   public void pauseWatchingConfigFile() {
      this.configFileModificationWatcher.pauseWatching();
   }

   public void resumeWatchingConfigFile() {
      CompletableFuture.runAsync(() -> {
         try {
            Thread.sleep(1000L);
         } catch (InterruptedException var2) {
         }

         this.configFileModificationWatcher.resumeWatching();
      });
   }

   @SuppressFBWarnings(justification = "generated code")
   public HeaderUI getHeaderUI() {
      return this.headerUI;
   }

   @SuppressFBWarnings(justification = "generated code")
   public HorizontalSplitPanel getHorizontalSplit() {
      return this.horizontalSplit;
   }

   @SuppressFBWarnings(justification = "generated code")
   public VerticalSplitPanel getVerticalSplit() {
      return this.verticalSplit;
   }

   @SuppressFBWarnings(justification = "generated code")
   public void setHandleTreeEvents(boolean handleTreeEvents) {
      this.handleTreeEvents = handleTreeEvents;
   }

   @SuppressFBWarnings(justification = "generated code")
   public void setHandleValidation(boolean handleValidation) {
      this.handleValidation = handleValidation;
   }

   @SuppressFBWarnings(justification = "generated code")
   public void setHandleTreeUpdates(boolean handleTreeUpdates) {
      this.handleTreeUpdates = handleTreeUpdates;
   }

   private final class ApplicationConfigLoadListener implements ConfigurationLoadListener {
      @Override
      public void newConfigurationLoaded(ConfigurationLoadListener.ConfigurationLoadedEvent event) {
         ConfigEditorApplication.this.showConfigSnippetEditBanner(event.getHandler().getDictionary().isDocumentTypeSnippet());
         ConfigEditorApplication.this.switchEditorToNewConfiguration(event.getHandler());
         this.showConfigLoadedNotification(event);
      }

      @Override
      public void failedToLoadConfig(ConfigurationLoadListener.FailedToLoadConfigurationEvent event) {
         Notification.show("Error: Unable to load the configuration file. Please ensure configuration is a valid XML or YAML file.", Type.ERROR_MESSAGE);
      }

      private void showConfigLoadedNotification(ConfigurationLoadListener.ConfigurationLoadedEvent event) {
         if (!event.isRefired()) {
            if (event.getConfigType().equals(ConfigurationLoadListener.ConfigType.DRAFT)) {
               Notification.show("Successfully loaded draft configuration.", Type.TRAY_NOTIFICATION);
            } else {
               Notification.show("Successfully loaded configuration.", Type.TRAY_NOTIFICATION);
            }
         }
      }
   }
}
