package com.airlock.iam.plugin.configeditor.ui.plugintrees;

import com.airlock.iam.configeditor.shared.domain.IamModuleConfigRoot;
import com.airlock.iam.plugin.api.config.ConfigurationContext;
import com.airlock.iam.plugin.api.config.ConfigurationEnvironment;
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.configeditor.ConfigEditorApplication;
import com.airlock.iam.plugin.configeditor.command.CreateNewChildPluginConfigurationCommand;
import com.airlock.iam.plugin.configeditor.data.ConfigContainer;
import com.airlock.iam.plugin.configeditor.data.tree.EmptyConfigurationTreeItem;
import com.airlock.iam.plugin.configeditor.data.tree.LabelTreeItem;
import com.airlock.iam.plugin.configeditor.data.tree.PluginConfigurationTreeItem;
import com.airlock.iam.plugin.configeditor.data.tree.PropertyTreeItem;
import com.airlock.iam.plugin.configeditor.data.tree.TreeItem;
import com.airlock.iam.plugin.configeditor.ui.components.PluginConfigurationTreeItemDescriptionGenerator;
import com.airlock.iam.plugin.configeditor.ui.dialog.DeletePluginConfigurationDialog;
import com.airlock.iam.plugin.configeditor.ui.dialog.NewPluginDialog;
import com.airlock.iam.plugin.framework.application.configuration.MutablePluginConfiguration;
import com.airlock.iam.plugin.framework.infrastructure.service.validation.ConfigTreeValidationService;
import com.airlock.iam.plugin.framework.misc.config.PluginConfigurationDictionary;
import com.airlock.iam.plugin.framework.misc.config.ValidationEnvironment;
import com.airlock.iam.sensitivevalues.application.service.SensitiveValuesProvider;
import com.google.common.annotations.VisibleForTesting;
import com.vaadin.event.Action;
import com.vaadin.event.ShortcutAction;
import com.vaadin.event.Action.Handler;
import com.vaadin.server.Resource;
import com.vaadin.shared.MouseEventDetails.MouseButton;
import com.vaadin.ui.Notification;
import com.vaadin.ui.UI;
import com.vaadin.ui.Notification.Type;
import com.vaadin.ui.Window.CloseListener;
import com.vaadin.v7.data.Container;
import com.vaadin.v7.data.Container.ItemSetChangeListener;
import com.vaadin.v7.data.Property.ValueChangeEvent;
import com.vaadin.v7.data.Property.ValueChangeListener;
import com.vaadin.v7.data.util.HierarchicalContainer;
import com.vaadin.v7.ui.Tree;
import com.vaadin.v7.ui.Tree.ItemStyleGenerator;
import com.vaadin.v7.ui.Tree.TreeDragMode;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;

public abstract class ConfigurationTree extends Tree implements Handler {
   protected static final String ICON_ITEM_ID = "icon";
   private static final Action ACTION_DELETE = new Action("Delete This Configuration");
   private static final Action ACTION_CREATE_CHILD = new Action("Create New Child");
   private static final Action ACTION_RESET = new Action("Reset Tree");
   private static final Action ACTION_EXPAND = new ShortcutAction("Expand/Collapse Recursively", 13, null);
   private static final Action[] PLUGIN_ACTIONS = new Action[]{ACTION_RESET, ACTION_EXPAND, ACTION_CREATE_CHILD, ACTION_DELETE};
   private final Path rootDirectory;
   private PluginConfigurationDictionary pluginConfigDict;
   private boolean needsUpdate;
   private final String displayName;
   private final ItemSetChangeListener itemSetChangeListener = event -> this.setTreeLeafsUnexpandable();
   private final ConfigurationTree.PluginSelection pluginSelection = new ConfigurationTree.PluginSelection(this);
   private final ConfigurationTree.TreeItems treeItems = new ConfigurationTree.TreeItems(this);
   private final boolean canHaveMultipleInstancesOfOnePlugin;

   @SuppressFBWarnings(value = "MC_OVERRIDABLE_METHOD_CALL_IN_CONSTRUCTOR", justification = "See ALIAM-15315")
   public ConfigurationTree(String displayName, Path rootDirectory, boolean canHaveMultipleInstancesOfOnePlugin) {
      this.displayName = displayName;
      this.rootDirectory = rootDirectory;
      this.addItem(new EmptyConfigurationTreeItem());
      this.getContainerDataSource().addItemSetChangeListener(this.itemSetChangeListener);
      this.setItemDescriptionGenerator(new PluginConfigurationTreeItemDescriptionGenerator());
      this.setSelectable(true);
      this.canHaveMultipleInstancesOfOnePlugin = canHaveMultipleInstancesOfOnePlugin;
      this.setMultiSelect(canHaveMultipleInstancesOfOnePlugin);
      this.setDragMode(TreeDragMode.NODE);
      this.setImmediate(true);
      this.addContainerProperty("icon", Resource.class, null);
      this.setItemIconPropertyId("icon");
      this.addValueChangeListener(this.pluginSelection);
      this.addItemClickListener(event -> {
         if (event.getButton() == MouseButton.LEFT && event.isDoubleClick()) {
            this.expandCollapseRecursively(event.getItemId());
         }
      });
      this.addActionHandler(this);
   }

   public boolean canHaveMultipleInstancesOfOnePlugin() {
      return this.canHaveMultipleInstancesOfOnePlugin;
   }

   public ConfigurationTree.PluginSelection pluginSelection() {
      return this.pluginSelection;
   }

   ConfigurationTree.TreeItems items() {
      return this.treeItems;
   }

   public void setContainerDataSource(Container newDataSource) {
      if (this.getContainerDataSource() != null) {
         this.getContainerDataSource().removeItemSetChangeListener(this.itemSetChangeListener);
      }

      super.setContainerDataSource(newDataSource);
      if (this.itemSetChangeListener != null) {
         this.getContainerDataSource().addItemSetChangeListener(this.itemSetChangeListener);
      }
   }

   public final void updateTree() {
      if (!this.isVisible()) {
         this.needsUpdate = true;
      } else {
         this.needsUpdate = false;
         this.getContainerDataSource().removeItemSetChangeListener(this.itemSetChangeListener);
         ConfigurationTree.ExpansionStates expansionStates = new ConfigurationTree.ExpansionStates(this);
         expansionStates.backupItemExpansionStates();
         this.removeAllItems();
         this.updateTreeImpl();
         this.setTreeLeafsUnexpandable();
         this.setItemStyleGenerator(new ConfigurationTree.CompositionItemStyleGenerator());
         expansionStates.restoreItemExpansionStates();
         this.sort();
         this.getContainerDataSource().addItemSetChangeListener(this.itemSetChangeListener);
      }
   }

   public void setVisible(boolean visible) {
      super.setVisible(visible);
      if (visible && this.needsUpdate) {
         this.updateTree();
      }
   }

   protected abstract void updateTreeImpl();

   public boolean setParent(Object itemId, Object newParentId) {
      if (newParentId != null) {
         this.setChildrenAllowed(newParentId, true);
      }

      return super.setParent(itemId, newParentId);
   }

   protected abstract void addPluginConfigurationItem(
      TreeItem<?> var1,
      MutablePluginConfiguration var2,
      String var3,
      ConfigurationEnvironment var4,
      ConfigurationContext var5,
      Map<PluginConfiguration, List<ConfigurationException>> var6
   );

   public void setConfiguration(ConfigContainer configContainer) {
      this.setConfiguration(configContainer.getDictionary());
   }

   public void setConfiguration(PluginConfigurationDictionary aPluginConfigDict) {
      this.removeAllItems();
      this.pluginConfigDict = aPluginConfigDict;
   }

   private void expandParentsRecursively(Object item) {
      if (item != null) {
         this.expandItem(item);
         this.expandParentsRecursively(this.getParent(item));
      }
   }

   private void expandCollapseRecursively(Object itemId) {
      if (itemId != null) {
         if (this.isExpanded(itemId)) {
            this.collapseItemsRecursively(itemId);
         } else {
            this.expandItemsRecursively(itemId);
         }
      }
   }

   private void resetTree() {
      for (Object itemId : this.getItemIds()) {
         this.collapseItem(itemId);
      }
   }

   public Action[] getActions(Object target, Object sender) {
      return target instanceof PluginConfigurationTreeItem ? this.getPluginActions() : new Action[0];
   }

   private Action[] getPluginActions() {
      return PLUGIN_ACTIONS;
   }

   public void handleAction(Action action, Object sender, Object target) {
      if (target != null) {
         if (action.equals(ACTION_CREATE_CHILD)) {
            this.handleCreateChildPluginConfigurationAction(target);
         } else if (action.equals(ACTION_DELETE)) {
            this.handleDeleteAction(target);
         } else if (action.equals(ACTION_RESET)) {
            this.resetTree();
         } else if (action.equals(ACTION_EXPAND)) {
            this.expandCollapseRecursively(target);
         }
      }
   }

   private void handleCreateChildPluginConfigurationAction(Object parent) {
      MutablePluginConfiguration pc = ((PluginConfigurationTreeItem)parent).getPluginConfiguration();

      try {
         NewPluginDialog dialog = new NewPluginDialog(null, pc, pc.getPluginClass());
         UI.getCurrent().addWindow(dialog);
         dialog.addCloseListener(this.handleCreateChildPluginConfigurationDialogClose());
      } catch (InitializeException var4) {
      }
   }

   private CloseListener handleCreateChildPluginConfigurationDialogClose() {
      return e -> {
         NewPluginDialog dialog = (NewPluginDialog)e.getSource();
         MutablePluginConfiguration newPc = dialog.getPluginConfiguration();
         if (newPc != null) {
            new CreateNewChildPluginConfigurationCommand(this.getPluginConfigDict(), newPc).execute();
            this.getApplication().updateValidationState();
            if (dialog.isEditAfterCreatingPlugin()) {
               this.getApplication().selectConfiguration(newPc);
               this.getApplication().getPluginTreeUI().selectUnconnectedPluginsTree();
            }

            Notification.show("New Plugin configuration created.", Type.TRAY_NOTIFICATION);
         }
      };
   }

   private void handleDeleteAction(Object target) {
      MutablePluginConfiguration toBeDeleted = ((PluginConfigurationTreeItem)target).getPluginConfiguration();
      this.showDeletePluginConfigurationDialog(toBeDeleted);
   }

   private void showDeletePluginConfigurationDialog(MutablePluginConfiguration toBeDeleted) {
      DeletePluginConfigurationDialog dialog = new DeletePluginConfigurationDialog(toBeDeleted);
      dialog.addCloseListener(e -> {
         DeletePluginConfigurationDialog dialog1 = (DeletePluginConfigurationDialog)e.getSource();
         if (dialog1.didIt()) {
            if (toBeDeleted.equals(this.getApplication().getSelectedConfiguration())) {
               this.getApplication().selectConfiguration(null);
               Notification.show("Plugin configuration deleted.", Type.TRAY_NOTIFICATION);
            }

            this.getApplication().updateValidationState();
            this.getApplication().updateFormFields();
         }
      });
   }

   public PluginConfigurationDictionary getPluginConfigDict() {
      return this.pluginConfigDict;
   }

   protected ConfigEditorApplication getApplication() {
      return (ConfigEditorApplication)UI.getCurrent();
   }

   public String toString() {
      return this.displayName;
   }

   protected void setTreeLeafsUnexpandable() {
      for (Object itemId : this.getItemIds()) {
         if (!this.hasChildren(itemId)) {
            this.setChildrenAllowed(itemId, false);
         }
      }
   }

   public final HierarchicalContainer getContainerDataSource() {
      return (HierarchicalContainer)super.getContainerDataSource();
   }

   public void sort() {
      this.getContainerDataSource().sort(null, null);
   }

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

   protected Map<PluginConfiguration, List<ConfigurationException>> collectGlobalValidationErrors(Set<MutablePluginConfiguration> roots) {
      return this.collectGlobalValidationErrors(roots.stream().map(PluginConfiguration.class::cast).toList());
   }

   protected Map<PluginConfiguration, List<ConfigurationException>> collectGlobalValidationErrors(List<PluginConfiguration> roots) {
      ValidationEnvironment validationEnvironment = ValidationEnvironment.builder()
         .environment(this.getApplication().getValidationEnvironment())
         .rootDir(this.rootDirectory)
         .externalSecretsKeys(this.getExternalDefinedSecretKeys())
         .instanceEnvironment(this.getApplication().getInstanceEnvironment())
         .build();
      ConfigTreeValidationService validationService = new ConfigTreeValidationService(roots, this.getApplication().getSelectedConfigurationEnvironment(), true);
      return this.createValidationMap(validationService.validateTreeAndCollect(this.getPluginConfigDict().getConfigurationContexts(), validationEnvironment));
   }

   protected Map<PluginConfiguration, List<ConfigurationException>> createValidationMap(Collection<ConfigurationException> exceptions) {
      Map<PluginConfiguration, List<ConfigurationException>> validations = new IdentityHashMap<>();
      exceptions.forEach(e -> validations.computeIfAbsent(e.getPluginConfiguration(), v -> new ArrayList<>()).add(e));
      return validations;
   }

   protected static class CompositionItemStyleGenerator implements ItemStyleGenerator {
      public String getStyle(Tree source, Object itemId) {
         if (itemId instanceof PluginConfigurationTreeItem pci) {
            if (IamModuleConfigRoot.isModuleTreeRoot(pci.getPluginConfiguration())) {
               return "root";
            }
         } else if (itemId instanceof LabelTreeItem lti && lti.getConfigurationContext() != null) {
            return "context";
         }

         return null;
      }
   }

   private static final class ExpansionStates {
      private final ConfigurationTree tree;
      private final Set<Object> expandedTreeItemIdentifiers = new HashSet<>();

      private ExpansionStates(ConfigurationTree tree) {
         this.tree = tree;
      }

      private void backupItemExpansionStates() {
         this.expandedTreeItemIdentifiers.clear();

         for (TreeItem<?> item : this.tree.items().all()) {
            if (this.tree.isExpanded(item)) {
               this.expandedTreeItemIdentifiers.add(item.getTreeItemIdentifier());
            }
         }
      }

      private void restoreItemExpansionStates() {
         for (TreeItem<?> item : this.tree.items().all()) {
            if (this.expandedTreeItemIdentifiers.contains(item.getTreeItemIdentifier())) {
               this.tree.expandItem(item);
            }
         }
      }
   }

   private static final class PluginItemPredicate implements Predicate<TreeItem<?>> {
      private final MutablePluginConfiguration plugin;

      public static ConfigurationTree.PluginItemPredicate forPlugin(MutablePluginConfiguration plugin) {
         return new ConfigurationTree.PluginItemPredicate(plugin);
      }

      private PluginItemPredicate(MutablePluginConfiguration plugin) {
         this.plugin = plugin;
      }

      public boolean test(TreeItem<?> item) {
         return item instanceof PluginConfigurationTreeItem pluginItem ? pluginItem.getPluginConfiguration() == this.plugin : false;
      }
   }

   static final class PluginSelection implements ValueChangeListener {
      private final ConfigurationTree tree;
      private final ConfigEditorApplication application;

      @VisibleForTesting
      PluginSelection(ConfigurationTree tree) {
         this.tree = tree;
         this.application = (ConfigEditorApplication)UI.getCurrent();
      }

      public Optional<MutablePluginConfiguration> getSelectedPluginConfiguration() {
         return this.tree.items().findSelectedPluginItem().isEmpty()
            ? Optional.empty()
            : Optional.of(this.tree.items().findSelectedPluginItem().get().getPluginConfiguration());
      }

      public void selectPluginConfiguration(MutablePluginConfiguration plugin, boolean selectAllInstancesOfPlugin) {
         if (plugin == null) {
            this.unselectPluginConfiguration();
         } else if (!this.isPluginSelected(plugin) || this.tree.canHaveMultipleInstancesOfOnePlugin() && selectAllInstancesOfPlugin) {
            this.application.setHandleTreeEvents(false);
            if (this.tree.canHaveMultipleInstancesOfOnePlugin()) {
               if (selectAllInstancesOfPlugin) {
                  this.selectAllTreeItemsForPlugin(plugin);
               } else {
                  boolean selectedClosestItem = this.selectClosestChildItem(plugin);
                  if (!selectedClosestItem) {
                     this.unselectPluginConfiguration();
                  }
               }
            } else {
               this.selectPluginTreeItem(this.tree.items().findWithPlugin(plugin).orElse(null));
            }

            this.application.setHandleTreeEvents(true);
         }
      }

      private boolean isPluginSelected(MutablePluginConfiguration plugin) {
         return this.getSelectedPluginConfiguration().equals(Optional.of(plugin));
      }

      private boolean selectClosestChildItem(MutablePluginConfiguration plugin) {
         Optional<PluginConfigurationTreeItem> currentItem = this.tree.items().findSelectedPluginItem();
         if (currentItem.isEmpty()) {
            return false;
         } else {
            Optional<PluginConfigurationTreeItem> matchingItem = this.tree.items().findWithPluginInSubtree(currentItem.get(), plugin);
            if (matchingItem.isPresent()) {
               this.unselectPluginConfiguration();
               this.selectPluginTreeItem(matchingItem.get());
               return true;
            } else {
               return false;
            }
         }
      }

      public void unselectPluginConfiguration() {
         if (this.tree.isMultiSelect()) {
            for (Object item : (Iterable)this.tree.getValue()) {
               this.tree.unselect(item);
            }
         } else {
            this.tree.unselect(this.tree.getValue());
         }
      }

      public void valueChange(ValueChangeEvent event) {
         if (this.application.isHandleTreeEvents()) {
            this.application.setHandleTreeEvents(false);
            this.expandSelectedItems();
            Optional<PropertyTreeItem> propertyTreeItem = this.propertyTreeItemSelected();
            if (propertyTreeItem.isPresent()) {
               Optional<MutablePluginConfiguration> parent = this.tree
                  .items()
                  .findParentPluginItem(propertyTreeItem.get())
                  .map(PluginConfigurationTreeItem::getPluginConfiguration);
               parent.ifPresent(p -> this.application.selectConfigurationField(p, propertyTreeItem.get().getPropertyConfiguration()));
            } else if (this.tree.items().findSelectedPluginItem().isPresent()) {
               this.application.selectConfiguration(this.tree.items().findSelectedPluginItem().get().getPluginConfiguration());
            }

            this.application.setHandleTreeEvents(true);
         }
      }

      private Optional<PropertyTreeItem> propertyTreeItemSelected() {
         return this.tree.items().allSelected().stream().filter(PropertyTreeItem.class::isInstance).map(PropertyTreeItem.class::cast).findAny();
      }

      private void expandSelectedItems() {
         for (TreeItem<?> item : this.tree.items().allSelected()) {
            this.tree.expandItem(item);
         }
      }

      private void selectAllTreeItemsForPlugin(MutablePluginConfiguration plugin) {
         this.unselectPluginConfiguration();

         for (PluginConfigurationTreeItem item : this.tree.items().allWithPlugin(plugin)) {
            this.selectPluginTreeItem(item);
         }
      }

      private void selectPluginTreeItem(Object item) {
         this.tree.select(item);
         this.tree.expandParentsRecursively(item);
      }
   }

   static final class TreeItems {
      private final ConfigurationTree tree;

      @VisibleForTesting
      TreeItems(ConfigurationTree tree) {
         this.tree = tree;
      }

      Collection<TreeItem<?>> all() {
         return this.tree.getItemIds();
      }

      Collection<PluginConfigurationTreeItem> allPluginItems() {
         return this.all()
            .stream()
            .filter(PluginConfigurationTreeItem.class::isInstance)
            .map(PluginConfigurationTreeItem.class::cast)
            .collect(Collectors.toList());
      }

      Collection<PluginConfigurationTreeItem> allWithPlugin(MutablePluginConfiguration plugin) {
         return this.allPluginItems().stream().filter(ConfigurationTree.PluginItemPredicate.forPlugin(plugin)).collect(Collectors.toList());
      }

      Optional<PluginConfigurationTreeItem> findWithPlugin(MutablePluginConfiguration plugin) {
         return this.allPluginItems().stream().filter(ConfigurationTree.PluginItemPredicate.forPlugin(plugin)).findFirst();
      }

      Collection<TreeItem<?>> allSelected() {
         Object selection = this.tree.getValue();
         if (selection instanceof Collection) {
            return new ArrayList<>((Collection<? extends TreeItem<?>>)selection);
         } else {
            return selection instanceof TreeItem ? Arrays.asList((TreeItem)selection) : Collections.emptyList();
         }
      }

      Optional<PluginConfigurationTreeItem> findSelectedPluginItem() {
         Collection<TreeItem<?>> selectedItems = this.allSelected();
         return selectedItems.size() == 1 ? this.findParentPluginItem(selectedItems.iterator().next()) : Optional.empty();
      }

      private Optional<PluginConfigurationTreeItem> findParentPluginItem(Object treeItem) {
         if (treeItem instanceof PluginConfigurationTreeItem) {
            return Optional.of((PluginConfigurationTreeItem)treeItem);
         } else {
            return treeItem == null ? Optional.empty() : this.findParentPluginItem(this.tree.getParent(treeItem));
         }
      }

      private Optional<PluginConfigurationTreeItem> findWithPluginInSubtree(PluginConfigurationTreeItem rootItem, MutablePluginConfiguration plugin) {
         Optional<TreeItem<?>> match = this.getTreeItemChildren(rootItem).stream().filter(ConfigurationTree.PluginItemPredicate.forPlugin(plugin)).findFirst();
         return match.isPresent() ? Optional.of((PluginConfigurationTreeItem)match.get()) : this.findWithPluginInSubtreeRecursively(rootItem, plugin);
      }

      private Optional<PluginConfigurationTreeItem> findWithPluginInSubtreeRecursively(TreeItem<?> rootItem, MutablePluginConfiguration plugin) {
         if (ConfigurationTree.PluginItemPredicate.forPlugin(plugin).test(rootItem)) {
            return Optional.of((PluginConfigurationTreeItem)rootItem);
         }

         for (TreeItem<?> childItem : this.getTreeItemChildren(rootItem)) {
            Optional<PluginConfigurationTreeItem> result = this.findWithPluginInSubtreeRecursively(childItem, plugin);
            if (result.isPresent()) {
               return result;
            }
         }

         return Optional.empty();
      }

      private Collection<TreeItem<?>> getTreeItemChildren(TreeItem<?> treeItem) {
         return Optional.ofNullable(this.tree.getChildren(treeItem)).orElse(Collections.emptyList());
      }
   }
}
