package com.airlock.iam.saml2.infrastructure.plugin.configuration;

import com.airlock.iam.core.misc.util.config.PropertiesFileHelper;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.plugin.api.config.ConfigurationContext;
import com.airlock.iam.saml2.application.configuration.Saml2FlowIdpConfig;
import com.airlock.iam.saml2.application.configuration.Saml2IDPEntityConfig;
import com.airlock.iam.saml2.application.configuration.Saml2SPEntityConfig;
import com.airlock.iam.saml2.application.configuration.Saml2ServiceProviderAccessConfig;
import com.airlock.iam.saml2.application.configuration.SamlConfig;
import com.airlock.iam.saml2.application.configuration.sp.Saml2FlowSpConfig;
import com.airlock.iam.saml2.domain.model.logging.Saml2LogTypes;
import com.airlock.iam.saml2.infrastructure.util.Saml2LoginappConfigProvider;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.sun.identity.plugin.configuration.ConfigurationException;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.builder.ToStringBuilder;

public final class FlowSaml2Configuration extends InitializableConfiguration {
   private static final ELogger LOG = ELoggerFactory.getLogger(FlowSaml2Configuration.class);

   public FlowSaml2Configuration() {
      super("SAML2");
   }

   @Override
   protected void initializeConfiguration() throws ConfigurationException {
      LOG.debug("initSAML2() - initializing SAML2 Configuration...");
      Set<ConfigurationContext> allContexts = Saml2LoginappConfigProvider.getAllLoginappConfigurationContexts();
      Map<String, FlowSaml2Configuration.EntityConfig> configs = Maps.newHashMap();

      for (ConfigurationContext context : allContexts) {
         SamlConfig samlConfig = SamlConfig.get(context);
         if (samlConfig != null) {
            Optional<Saml2FlowIdpConfig> idpConfig = samlConfig.getSamlFlowIdpSettings();

            for (Saml2ServiceProviderAccessConfig sp : idpConfig.map(Saml2FlowIdpConfig::getServiceProviders).orElseGet(Collections::emptyList)) {
               Saml2SPEntityConfig entityConfig = sp.getSpEntityConfig();
               this.checkAndAddConfig(
                  new FlowSaml2Configuration.EntityConfig(
                     entityConfig.getEntityId(), entityConfig.getMetaDataFile().getPath(), entityConfig.getExtendedMetaDataFile().getPath()
                  ),
                  configs,
                  "Remote SP"
               );
            }

            if (idpConfig.isPresent()) {
               Saml2IDPEntityConfig entityConfig = idpConfig.get().getIdpEntitySettings();
               this.checkAndAddConfig(
                  new FlowSaml2Configuration.EntityConfig(
                     entityConfig.getEntityId(),
                     entityConfig.getMetaDataFile().getPath(),
                     entityConfig.getExtendedMetaDataFile().getPath(),
                     new ExtendedIdpMetadataTransformer(samlConfig.getSamlFederationConfig())
                  ),
                  configs,
                  "Local Airlock IAM IdP"
               );
            }

            for (Saml2FlowSpConfig spConfig : samlConfig.getSamlFlowSpSettings()) {
               Saml2SPEntityConfig spEntityConfig = spConfig.getSpEntityConfig();
               this.checkAndAddConfig(
                  new FlowSaml2Configuration.EntityConfig(
                     spEntityConfig.getEntityId(),
                     spEntityConfig.getMetaDataFile().getPath(),
                     spEntityConfig.getExtendedMetaDataFile().getPath(),
                     new ExtendedSpMetadataTransformer(samlConfig.getSamlFederationConfig())
                  ),
                  configs,
                  "Local Airlock IAM SP"
               );
               Saml2IDPEntityConfig remoteIdpConfig = spConfig.getRemoteIdpEntitySettings();
               this.checkAndAddConfig(
                  new FlowSaml2Configuration.EntityConfig(
                     remoteIdpConfig.getEntityId(), remoteIdpConfig.getMetaDataFile().getPath(), remoteIdpConfig.getExtendedMetaDataFile().getPath()
                  ),
                  configs,
                  "Remote IdP"
               );
            }
         }
      }

      for (FlowSaml2Configuration.EntityConfig entity : configs.values()) {
         LOG.debug("initSAML2() - loading SAML2 entity config: " + entity.toString());
         this.loadSAML2Config(entity);
      }
   }

   private void checkAndAddConfig(
      FlowSaml2Configuration.EntityConfig entityConfig, Map<String, FlowSaml2Configuration.EntityConfig> configs, String exceptionText
   ) throws ConfigurationException {
      String entityId = entityConfig.getEntityId();
      if (configs.containsKey(entityId)) {
         if (!entityConfig.equals(configs.get(entityId))) {
            throw new ConfigurationException(exceptionText + " SAML entity \"" + entityId + "\" is already configured with different metadata paths!");
         }
      } else {
         configs.put(entityId, entityConfig);
      }
   }

   private void loadSAML2Config(FlowSaml2Configuration.EntityConfig entity) throws ConfigurationException {
      Objects.requireNonNull(entity, "entity config null");
      Objects.requireNonNull(entity.getEntityId(), "entityId null");
      Objects.requireNonNull(entity.getMetadataPath(), "metaFile null");
      Objects.requireNonNull(entity.getExtendedMetadataPath(), "metaFileExt null");
      String entityID = entity.getEntityId();
      String metaFilePath = entity.getMetadataPath();
      String extMetaFilePath = entity.getExtendedMetadataPath();
      if (!StringUtils.isBlank(entityID) && !StringUtils.isBlank(metaFilePath) && !StringUtils.isBlank(extMetaFilePath)) {
         if (this.getConfigMap().containsKey(entityID)) {
            throw new ConfigurationException("Entity already defined: " + entityID);
         }

         String metadata;
         String metadataExt;
         try {
            if (LOG.isDebugEnabled()) {
               LOG.debug("Loading metadata file \"" + metaFilePath + "\" for entityID \"" + entityID + "\"");
            }

            metadata = readFile(metaFilePath);
            if (LOG.isDebugEnabled()) {
               LOG.debug("Loading extended metadata file \"" + extMetaFilePath + "\" for entityID \"" + entityID + "\"");
            }

            metadataExt = entity.getExtendedMetadataTransformer().apply(readFile(extMetaFilePath));
         } catch (IOException | RuntimeException e) {
            LOG.error(Saml2LogTypes.FEDERATION_LIBRARY_MESSAGE, "Error loading/processing metadata file: " + e.getMessage(), e);
            throw new ConfigurationException(e);
         }

         Map<String, Set<String>> samlConfig = new HashMap<>();
         samlConfig.put("sun-fm-saml2-metadata", Sets.newHashSet(new String[]{metadata}));
         samlConfig.put("sun-fm-saml2-entityconfig", Sets.newHashSet(new String[]{metadataExt}));
         this.getConfigMap().put(entityID, samlConfig);
         LOG.debug("Successfully loaded SAML2 configuration for entity " + entityID);
      } else {
         throw new ConfigurationException("Entity not correctly configured: " + entityID);
      }
   }

   @VisibleForTesting
   static String readFile(String fileName) throws IOException {
      try (InputStream input = PropertiesFileHelper.getInputStream(fileName)) {
         return StringUtils.trimToEmpty(IOUtils.toString(input, StandardCharsets.UTF_8));
      }
   }

   private static final class EntityConfig {
      private final String entityId;
      private final String metadataPath;
      private final String extendedMetadataPath;
      private final Function<String, String> extendedMetadataTransformer;

      public EntityConfig(String entityId, String metadataPath, String extendedMetadataPath) {
         this.entityId = entityId;
         this.metadataPath = metadataPath;
         this.extendedMetadataPath = extendedMetadataPath;
         this.extendedMetadataTransformer = Function.identity();
      }

      @Override
      public String toString() {
         return new ToStringBuilder(this)
            .append("ID", this.entityId)
            .append("metadata path", this.metadataPath)
            .append("extended metadata path", this.extendedMetadataPath)
            .toString();
      }

      @SuppressFBWarnings(justification = "generated code")
      public String getEntityId() {
         return this.entityId;
      }

      @SuppressFBWarnings(justification = "generated code")
      public String getMetadataPath() {
         return this.metadataPath;
      }

      @SuppressFBWarnings(justification = "generated code")
      public String getExtendedMetadataPath() {
         return this.extendedMetadataPath;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Function<String, String> getExtendedMetadataTransformer() {
         return this.extendedMetadataTransformer;
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public boolean equals(Object o) {
         if (o == this) {
            return true;
         } else if (!(o instanceof FlowSaml2Configuration.EntityConfig other)) {
            return false;
         } else {
            Object this$entityId = this.entityId;
            Object other$entityId = other.entityId;
            if (this$entityId == null ? other$entityId == null : this$entityId.equals(other$entityId)) {
               Object this$metadataPath = this.metadataPath;
               Object other$metadataPath = other.metadataPath;
               if (this$metadataPath == null ? other$metadataPath == null : this$metadataPath.equals(other$metadataPath)) {
                  Object this$extendedMetadataPath = this.extendedMetadataPath;
                  Object other$extendedMetadataPath = other.extendedMetadataPath;
                  return this$extendedMetadataPath == null ? other$extendedMetadataPath == null : this$extendedMetadataPath.equals(other$extendedMetadataPath);
               } else {
                  return false;
               }
            } else {
               return false;
            }
         }
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public int hashCode() {
         int PRIME = 59;
         int result = 1;
         Object $entityId = this.entityId;
         result = result * 59 + ($entityId == null ? 43 : $entityId.hashCode());
         Object $metadataPath = this.metadataPath;
         result = result * 59 + ($metadataPath == null ? 43 : $metadataPath.hashCode());
         Object $extendedMetadataPath = this.extendedMetadataPath;
         return result * 59 + ($extendedMetadataPath == null ? 43 : $extendedMetadataPath.hashCode());
      }

      @SuppressFBWarnings(justification = "generated code")
      public EntityConfig(String entityId, String metadataPath, String extendedMetadataPath, Function<String, String> extendedMetadataTransformer) {
         this.entityId = entityId;
         this.metadataPath = metadataPath;
         this.extendedMetadataPath = extendedMetadataPath;
         this.extendedMetadataTransformer = extendedMetadataTransformer;
      }
   }
}
