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

import com.airlock.iam.admin.application.service.activation.ActivationStateWatcher;
import com.airlock.iam.admin.application.service.activation.DeploymentUtils;
import com.airlock.iam.base.misc.io.Files;
import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.configeditor.shared.application.ConfigurationDiffLogger;
import com.airlock.iam.core.misc.plugin.config.activation.ActivationState;
import com.airlock.iam.core.misc.plugin.config.activation.ActivationStateName;
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.log.usertrail.UserTrailLogger;
import com.airlock.iam.plugin.configeditor.ConfigEditorApplication;
import com.airlock.iam.plugin.framework.application.service.ConfigWriter;
import com.airlock.iam.plugin.framework.misc.config.ConfigHistoryItem;
import com.airlock.iam.system.property.IamSystemProperties;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.Path;
import java.time.Duration;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import org.apache.commons.io.FileUtils;

public class ApplyConfigThread extends Thread {
   private static final ELogger LOG = ELoggerFactory.getLogger(ApplyConfigThread.class);
   private static final String APPLYING_MESSAGE = "Applying configuration...";
   private static final String SUCCESS_MESSAGE = "<b>Deployment successful!</b>";
   private static final String FAILURE_MESSAGE = "<b>Deployment failed!</b>";
   private static final String TIMEOUT_MESSAGE = "<b>Timeout occurred!</b>";
   private static final Duration POLLING_INTERVAL = Duration.ofMillis(300L);
   private final ApplyConfigThreadListener listener;
   private final ConfigEditorApplication app;
   private final ConfigHistoryItem configMetadata;
   private final String editorUserName;
   private final boolean shouldLogConfigurationChanges;

   public ApplyConfigThread(
      ConfigEditorApplication app,
      ApplyConfigThreadListener listener,
      ConfigHistoryItem configMetadata,
      String editorUserName,
      boolean shouldLogConfigurationChanges
   ) {
      this.listener = listener;
      this.app = app;
      this.configMetadata = configMetadata;
      this.editorUserName = editorUserName;
      this.shouldLogConfigurationChanges = shouldLogConfigurationChanges;
      this.setDaemon(true);
   }

   @SuppressFBWarnings("RV_RETURN_VALUE_IGNORED_BAD_PRACTICE")
   @Override
   public void run() {
      this.listener.beforeApplyConfig();
      this.listener.updateStatusMessage("Applying configuration...");
      File tempFile = null;

      try {
         Optional<String> configFileToLoad = this.app.getConfigFileToLoad();
         if (configFileToLoad.isEmpty()) {
            this.listener.displayError("No config file has been loaded");
            return;
         }

         File targetFile = new File(configFileToLoad.get());
         File deploymentDirectory = targetFile.getParentFile();
         if (deploymentDirectory.isDirectory()) {
            tempFile = TempConfigFiles.writeTempConfigFile(this.app, this.configMetadata, deploymentDirectory);
            if (this.shouldLogConfigurationChanges) {
               new ConfigurationDiffLogger().logDifferences(targetFile, tempFile);
            }

            Optional<Path> activationStateDirectory = IamSystemProperties.getActivatedConfigsDirectory();
            if (activationStateDirectory.isPresent() && !activationStateDirectory.get().toFile().exists() && !activationStateDirectory.get().toFile().mkdir()) {
               throw new IOException(
                  "Could not create the activated configs directory "
                     + LogUtil.quote(activationStateDirectory.get().toAbsolutePath())
                     + ". Hint: the location (and whether to use it at all) can be configured."
               );
            }

            ActivationStateWatcher activationStateWatcher = ActivationStateWatcher.activationStateWatcher(
               activationStateDirectory, this.app.getActivationTimeout()
            );
            Files.replaceFile(tempFile, targetFile);
            this.listener.configurationStored();
            this.app.getConfigContainer().deleteDraft();
            Files.copyFile(targetFile, this.app.getConfigContainer().makeWorkingCopyFile(this.app.getWorkingCopyDirectory()));
            activationStateWatcher.startActivation();

            while (activationStateWatcher.isOngoing()) {
               Thread.sleep(POLLING_INTERVAL.toMillis());
               this.listener.updateStatusMessage("Applying configuration...<br/>" + this.renderStates(activationStateWatcher.getActivationStates(), true));
            }

            ActivationStateWatcher.DeploymentResult result = activationStateWatcher.waitForDeploymentResult();
            String xhtmlState = this.renderStates(result.activationStates(), true);
            String textState = this.renderStates(result.activationStates(), false);
            if (result.activationResult() != ActivationStateWatcher.ActivationResult.SUCCESS
               && result.activationResult() != ActivationStateWatcher.ActivationResult.NO_ACTIVATION_STATES) {
               if (DeploymentUtils.deploymentOngoing(result.activationStates())) {
                  this.listener.displayError("<b>Timeout occurred!</b><br/>" + xhtmlState);
                  UserTrailLogger.log(this.editorUserName, "Configuration activation incurred a timeout. File: " + targetFile + ", " + textState);
               } else {
                  this.listener.displayError("<b>Deployment failed!</b><br/>" + xhtmlState);
                  UserTrailLogger.log(this.editorUserName, "Configuration activation failed. File: " + targetFile + ", " + textState);
               }

               return;
            }

            this.listener.displaySuccess("<b>Deployment successful!</b><br/>" + xhtmlState);
            this.app.getConfigContainer().updatePersistentConfiguration(targetFile, IamSystemProperties.activeConfigFormat());
            UserTrailLogger.log(this.editorUserName, "Configuration activation successful. File: " + targetFile + ", " + textState);
            return;
         }

         this.listener.displayError("Not a valid directory: " + deploymentDirectory.getAbsolutePath());
      } catch (FileNotFoundException e) {
         LOG.error(LogTypes.RESOURCE_NOT_FOUND, "FileNotFoundException: " + e.getMessage(), e);
         this.listener.displayError(e.getMessage());
         return;
      } catch (ConfigWriter.ConfigWriterException e) {
         LOG.error(LogTypes.CONFIGURATION_ERROR, "ConfigurationException: " + e.getMessage(), e);
         this.listener.displayError(e.getMessage());
         return;
      } catch (IOException e) {
         LOG.error(LogTypes.SERVER_INFO_SYS, "IOException: " + e.getMessage(), e);
         this.listener.displayError(e.getMessage());
         return;
      } catch (InterruptedException e) {
         this.listener.displayError(e.getMessage());
         return;
      } finally {
         this.listener.afterApplyConfig();
         FileUtils.deleteQuietly(tempFile);
      }
   }

   private String renderStates(List<ActivationState> states, boolean useXHTML) {
      if (states.isEmpty()) {
         return "(no activation states)";
      } else {
         return useXHTML
            ? states.stream().map(ApplyConfigThread::stateToXHTML).collect(Collectors.joining())
            : states.stream().<CharSequence>map(ActivationState::toString).collect(Collectors.joining("; "));
      }
   }

   private static String stateToXHTML(ActivationState state) {
      return "<p class=\"" + colorForActivationState(state) + "\">" + state + "</p>";
   }

   private static String colorForActivationState(ActivationState state) {
      ActivationStateName stateName = state.getName();
      if (ActivationStateName.SUCCESS.equals(stateName)) {
         return "validation-success";
      } else {
         return ActivationStateName.FAILED.equals(stateName) ? "validation-error" : "validation-info";
      }
   }
}
