package com.airlock.iam.plugin.configeditor.data;

import com.airlock.iam.core.misc.plugin.config.PluginConfigurationDictionaryFactory;
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.PluginConfiguration;
import com.airlock.iam.plugin.api.exception.InitializeException;
import com.airlock.iam.plugin.configeditor.ConfigEditorApplication;
import com.airlock.iam.plugin.framework.application.configuration.MutablePluginConfiguration;
import com.airlock.iam.plugin.framework.application.service.ConfigWriter;
import com.airlock.iam.plugin.framework.application.service.IncludeAllFilter;
import com.airlock.iam.plugin.framework.application.service.TranslationFilter;
import com.airlock.iam.plugin.framework.domain.service.configchecker.PluginConfigurationChecker;
import com.airlock.iam.plugin.framework.misc.PluginFramework;
import com.airlock.iam.plugin.framework.misc.config.ConfigHistoryItem;
import com.airlock.iam.plugin.framework.misc.config.PluginConfigurationDictionary;
import com.airlock.iam.plugin.framework.misc.frompersistence.TranslatedMedusaConfiguration;
import com.airlock.iam.system.config.ConfigFormat;
import com.airlock.iam.system.property.IamSystemProperties;
import com.google.common.annotations.VisibleForTesting;
import com.vaadin.ui.Notification;
import com.vaadin.ui.Notification.Type;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.StandardOpenOption;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.FastDateFormat;

public class ConfigContainer {
   private static final ELogger LOG = ELoggerFactory.getLogger(ConfigContainer.class);
   private static final Map<String, Object> FILE_LOCKS = Collections.synchronizedMap(new HashMap<>());
   private static final FastDateFormat CONFIG_FILE_DATE_FORMAT = FastDateFormat.getInstance(
      "yyyy-MM-dd_HH-mm-ss-SSS", ConfigEditorApplication.CONFIG_FILE_NAME_DATEFORMAT_LOCALE
   );
   private final File configFileToLoad;
   private final File draftConfigFile;
   private final String instanceName;
   private final String pluginConfigBaseName;
   private final String pluginConfigExtension;
   private PluginConfigurationDictionary dict;
   private PluginConfigurationDictionary persistedConfiguration;

   @VisibleForTesting
   private ConfigContainer(
      File configFileToLoad,
      File draftConfigFile,
      PluginConfigurationDictionary currentConfiguration,
      PluginConfigurationDictionary persistedConfiguration,
      String instanceName,
      String pluginConfigBaseName,
      String pluginConfigExtension
   ) {
      this.configFileToLoad = configFileToLoad;
      this.draftConfigFile = draftConfigFile;
      this.dict = currentConfiguration;
      this.persistedConfiguration = persistedConfiguration;
      this.instanceName = instanceName;
      this.pluginConfigBaseName = pluginConfigBaseName;
      this.pluginConfigExtension = pluginConfigExtension;
   }

   public ConfigContainer(File configFile) {
      Objects.requireNonNull(configFile, "configFile null");
      this.configFileToLoad = configFile;
      this.draftConfigFile = IamSystemProperties.getPluginConfigDraft().toFile();
      this.instanceName = IamSystemProperties.getInstanceName();
      this.pluginConfigBaseName = IamSystemProperties.getPluginConfigBaseName();
      this.pluginConfigExtension = IamSystemProperties.getPluginConfigExtension();
   }

   public void loadConfig(ConfigFormat configFormat, boolean loadDraftIfAvailable) throws InitializeException {
      File fileToLoad = this.getFileToLoad(loadDraftIfAvailable);
      this.dict = loadConfigWithLock(fileToLoad, configFormat);
      this.updatePersistentConfiguration(fileToLoad, configFormat);
   }

   @VisibleForTesting
   File getFileToLoad(boolean loadDraftIfAvailable) {
      return loadDraftIfAvailable && this.isDraftAvailableAndNewerThanConfig() ? this.draftConfigFile : this.configFileToLoad;
   }

   public boolean isDraftAvailableAndNewerThanConfig() {
      return this.draftConfigFile.exists() && this.draftConfigFile.lastModified() > this.configFileToLoad.lastModified();
   }

   public Optional<ConfigHistoryItem> getDraftConfigFileMetaData() throws InitializeException {
      if (this.draftConfigFile.exists()) {
         List<ConfigHistoryItem> items;
         synchronized (getLock(this.draftConfigFile)) {
            items = PluginFramework.getTranslatedConfigReader(IamSystemProperties.activeConfigFormat()).loadConfigHistory(this.draftConfigFile);
         }

         if (!items.isEmpty()) {
            return Optional.of(items.getLast());
         }
      }

      return Optional.empty();
   }

   private static PluginConfigurationDictionary loadConfigWithLock(File fileToLoad, ConfigFormat configFormat) throws InitializeException {
      TranslatedMedusaConfiguration config;
      synchronized (getLock(fileToLoad)) {
         config = PluginFramework.getTranslatedConfigReader(configFormat).loadConfig(fileToLoad);
      }

      return PluginConfigurationDictionaryFactory.fromTranslatedConfiguration(config);
   }

   public void updatePersistentConfiguration(File persistedConfig, ConfigFormat configFormat) {
      try {
         this.persistedConfiguration = loadConfigWithLock(persistedConfig, configFormat);
      } catch (InitializeException e) {
         LOG.error(LogTypes.SERVER_INFO_SYS, "Failed to load the persistent configuration (" + persistedConfig.getAbsolutePath() + "). " + e.getMessage(), e);
      }
   }

   public boolean isConfigurationModified() {
      Map<String, MutablePluginConfiguration> currentRootConfigs = this.getRootPluginsUuidToConfigMap(this.dict);
      Map<String, MutablePluginConfiguration> persistedRootConfigs = this.getRootPluginsUuidToConfigMap(this.persistedConfiguration);
      if (!currentRootConfigs.keySet().equals(persistedRootConfigs.keySet())) {
         return true;
      }

      PluginConfigurationChecker checker = PluginConfigurationChecker.pluginConfigurationCheckerWithAllChecks();
      return !currentRootConfigs.entrySet().stream().allMatch(entry -> checker.areEqual(entry.getValue(), persistedRootConfigs.get(entry.getKey())).isEqual());
   }

   private Map<String, MutablePluginConfiguration> getRootPluginsUuidToConfigMap(PluginConfigurationDictionary pluginConfigDictionary) {
      return pluginConfigDictionary.getConfigTreeRoots().stream().collect(Collectors.toMap(PluginConfiguration::getUuid, Function.identity()));
   }

   public void deleteDraft() {
      FileUtils.deleteQuietly(this.draftConfigFile);
   }

   public void saveDraft() {
      try {
         synchronized (getLock(this.draftConfigFile)) {
            try (OutputStream out = Files.newOutputStream(this.draftConfigFile.toPath(), StandardOpenOption.CREATE)) {
               this.saveConfiguration(IamSystemProperties.activeConfigFormat(), out);
            }
         }
      } catch (ConfigWriter.ConfigWriterException | IOException e) {
         LOG.error(LogTypes.RESOURCE_NOT_FOUND, "Error saving draft configuration: " + e.getMessage(), e);
         Notification.show("Could not save draft configuration: " + e.getMessage(), Type.ERROR_MESSAGE);
      }

      this.updatePersistentConfiguration(this.draftConfigFile, IamSystemProperties.activeConfigFormat());
   }

   public void saveConfiguration(ConfigFormat format, OutputStream out, ConfigHistoryItem configMetaData) throws ConfigWriter.ConfigWriterException {
      if (configMetaData != null) {
         this.getDictionary().addHistoryItem(configMetaData);
      }

      this.saveConfiguration(format, out);
   }

   private void saveConfiguration(ConfigFormat format, OutputStream out) throws ConfigWriter.ConfigWriterException {
      this.saveConfiguration(format, out, new IncludeAllFilter());
   }

   public void saveConfiguration(ConfigFormat format, OutputStream out, TranslationFilter filter) throws ConfigWriter.ConfigWriterException {
      PluginFramework.getTranslatedConfigWriter(format).writeConfig(this.getDictionary().toTranslatedConfiguration(), filter, out);
   }

   public void saveWorkingCopy(String workingCopyDirectory, ConfigFormat configFormat) {
      if (StringUtils.isBlank(workingCopyDirectory)) {
         Notification.show(
            "No directory for working copies has been specified. The working copy directory can be specified in the Preferences menu.\nNo working copy saved!",
            Type.ERROR_MESSAGE
         );
      } else if (this.makeWorkingCopyDirectoryIfNotExistent(workingCopyDirectory)) {
         this.saveWorkingCopyAndUpdateConfiguration(workingCopyDirectory, configFormat);
      }
   }

   private boolean makeWorkingCopyDirectoryIfNotExistent(String workingCopyDirectory) {
      File workingCopyDirectoryAsFile = new File(workingCopyDirectory);
      return !workingCopyDirectoryAsFile.exists() ? this.makeWorkingCopyDirectory(workingCopyDirectoryAsFile) : true;
   }

   private boolean makeWorkingCopyDirectory(File workingCopyDirectoryAsFile) {
      try {
         if (!workingCopyDirectoryAsFile.mkdirs()) {
            Notification.show(
               "Working copy directory does not exist and could not be created.\nDirectory: " + workingCopyDirectoryAsFile.getAbsolutePath(),
               Type.ERROR_MESSAGE
            );
            return false;
         } else {
            return true;
         }
      } catch (SecurityException e) {
         Notification.show(
            "Working copy directory does not exist and could not be created because of missing privileges.\nDirectory: "
               + workingCopyDirectoryAsFile.getAbsolutePath(),
            Type.ERROR_MESSAGE
         );
         return false;
      }
   }

   private void saveWorkingCopyAndUpdateConfiguration(String workingCopyDirectory, ConfigFormat configFormat) {
      try {
         File workingCopy = this.makeWorkingCopyFile(workingCopyDirectory);

         try (OutputStream out = Files.newOutputStream(workingCopy.toPath())) {
            this.saveConfiguration(configFormat, out);
            this.updatePersistentConfiguration(workingCopy, configFormat);
         }
      } catch (FileNotFoundException e) {
         LOG.error(LogTypes.RESOURCE_NOT_FOUND, "FileNotFoundException: " + e.getMessage(), e);
         Notification.show("File could not be created: " + e.getMessage(), Type.ERROR_MESSAGE);
      } catch (IOException e) {
         LOG.error(LogTypes.RESOURCE_NOT_FOUND, "IOException: " + e.getMessage(), e);
         Notification.show("File could not be created: " + e.getMessage(), Type.ERROR_MESSAGE);
      } catch (ConfigWriter.ConfigWriterException e) {
         LOG.error(LogTypes.CONFIGURATION_ERROR, "Configuration Exception: " + e.getMessage(), e);
         Notification.show("File could not be created: " + e.getMessage(), Type.ERROR_MESSAGE);
      }

      Notification.show("Saved working copy.", Type.TRAY_NOTIFICATION);
   }

   public File makeWorkingCopyFile(String workingCopyDirectory) throws IOException {
      File directory = new File(workingCopyDirectory);
      if (!directory.exists() && !directory.mkdir()) {
         throw new IOException("Could not create directory \"" + directory.getAbsolutePath() + "\".");
      } else {
         return new File(directory.getAbsoluteFile(), this.makeConfigWorkingCopyFileName());
      }
   }

   private String makeConfigWorkingCopyFileName() {
      String timestampSuffix = CONFIG_FILE_DATE_FORMAT.format(new Date());
      return this.pluginConfigBaseName + "-" + timestampSuffix + this.pluginConfigExtension;
   }

   public void updateDictionary(PluginConfigurationDictionary dictionary) {
      this.dict = dictionary;
   }

   public PluginConfigurationDictionary getDictionary() {
      return this.dict;
   }

   public File getFile() {
      return this.configFileToLoad;
   }

   public String getConfigFileLabel() {
      return "Instance: <b>" + this.instanceName + "</b>";
   }

   private static Object getLock(File file) {
      return FILE_LOCKS.computeIfAbsent(file.getPath(), path -> new Object());
   }

   @SuppressFBWarnings(justification = "generated code")
   static ConfigContainer.ConfigContainerBuilder builder() {
      return new ConfigContainer.ConfigContainerBuilder();
   }

   @SuppressFBWarnings(justification = "generated code")
   static class ConfigContainerBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private File configFileToLoad;
      @SuppressFBWarnings(justification = "generated code")
      private File draftConfigFile;
      @SuppressFBWarnings(justification = "generated code")
      private PluginConfigurationDictionary currentConfiguration;
      @SuppressFBWarnings(justification = "generated code")
      private PluginConfigurationDictionary persistedConfiguration;
      @SuppressFBWarnings(justification = "generated code")
      private String instanceName;
      @SuppressFBWarnings(justification = "generated code")
      private String pluginConfigBaseName;
      @SuppressFBWarnings(justification = "generated code")
      private String pluginConfigExtension;

      @SuppressFBWarnings(justification = "generated code")
      ConfigContainer.ConfigContainerBuilder configFileToLoad(File configFileToLoad) {
         this.configFileToLoad = configFileToLoad;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      ConfigContainer.ConfigContainerBuilder draftConfigFile(File draftConfigFile) {
         this.draftConfigFile = draftConfigFile;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      ConfigContainer.ConfigContainerBuilder currentConfiguration(PluginConfigurationDictionary currentConfiguration) {
         this.currentConfiguration = currentConfiguration;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      ConfigContainer.ConfigContainerBuilder persistedConfiguration(PluginConfigurationDictionary persistedConfiguration) {
         this.persistedConfiguration = persistedConfiguration;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      ConfigContainer.ConfigContainerBuilder instanceName(String instanceName) {
         this.instanceName = instanceName;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      ConfigContainer.ConfigContainerBuilder pluginConfigBaseName(String pluginConfigBaseName) {
         this.pluginConfigBaseName = pluginConfigBaseName;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      ConfigContainer.ConfigContainerBuilder pluginConfigExtension(String pluginConfigExtension) {
         this.pluginConfigExtension = pluginConfigExtension;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      ConfigContainer build() {
         return new ConfigContainer(
            this.configFileToLoad,
            this.draftConfigFile,
            this.currentConfiguration,
            this.persistedConfiguration,
            this.instanceName,
            this.pluginConfigBaseName,
            this.pluginConfigExtension
         );
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "ConfigContainer.ConfigContainerBuilder(configFileToLoad="
            + this.configFileToLoad
            + ", draftConfigFile="
            + this.draftConfigFile
            + ", currentConfiguration="
            + this.currentConfiguration
            + ", persistedConfiguration="
            + this.persistedConfiguration
            + ", instanceName="
            + this.instanceName
            + ", pluginConfigBaseName="
            + this.pluginConfigBaseName
            + ", pluginConfigExtension="
            + this.pluginConfigExtension
            + ")";
      }
   }
}
