package com.airlock.iam.admin.application.configuration.generic;

import com.airlock.iam.admin.api.domain.model.token.generic.GenericToken;
import com.airlock.iam.admin.api.domain.model.token.generic.GenericTokenAttributeMapping;
import com.airlock.iam.base.misc.date.Dates;
import com.airlock.iam.plugin.api.annotation.config.ConfigurationValidator;
import com.airlock.iam.plugin.api.annotation.config.Plugin;
import com.airlock.iam.plugin.api.annotation.property.PluginMapProperty;
import com.airlock.iam.plugin.api.annotation.property.StringProperty;
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.PropertyConstraintViolationException;
import com.airlock.iam.plugin.api.exception.PropertyTypeMismatchException;
import com.airlock.iam.plugin.api.valuereader.PluginMapPropertyValueReader;
import com.airlock.iam.plugin.api.valuereader.StringPropertyValueReader;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.lang3.StringUtils;

@Plugin(
   description = "Filters and maps attributes from the outside to the generic token and vice versa.\n<p>This plugin is designed to work only with the credential-based repository that is shipped with IAM.\nFor custom repository implementations, custom attribute mappings are needed.</p>",
   displayName = "Credential-based Attribute Mapping"
)
public final class CredentialBasedAttributeMapping implements GenericTokenAttributeMappingConfig {
   private static final Pattern ATTRIBUTE_PATTERN = Pattern.compile("[a-zA-Z0-9_-]+");
   @StringProperty(description = "The serial ID of this token.", optional = true)
   private String serialNumber;
   @StringProperty(description = "Indicates whether this token is enabled. This is a read-only property.", optional = true)
   private String enabled;
   @StringProperty(description = "Date as of which the token is valid.", optional = true)
   private String validFrom;
   @StringProperty(description = "Expiration date of this token.", optional = true)
   private String validTo;
   @StringProperty(description = "The activation date of this token. This is a read-only property.", optional = true)
   private String generationDate;
   @StringProperty(description = "The actual data of this token. Binary data is converted to a base64 representation.", optional = true)
   private String data;
   @StringProperty(description = "Encoding of the data (BINARY or STRING).", optional = true)
   private String encoding;
   @StringProperty(description = "The token delivery date.", optional = true)
   private String deliveryDate;
   @PluginMapProperty(
      description = "Maps internal credential context data keys (left side of the map, labeled KEY) under the mapped name to an external interface (right side, labeled PLUGIN).<p>Mapped context data fields are elements of a nested map with the name 'contextData'. For example an entry with external name 'myField' will be mapped to an external interface as <code>data.attributes.contextData.myField</code>.</p>",
      optional = true
   )
   private Map<String, MappedContextDataField> contextDataFields;
   private static final String CONTEXT_DATA_FIELDS_NAME = "contextDataFields";

   private Optional<String> getMappingForSerialNumber() {
      return Optional.ofNullable(this.serialNumber);
   }

   @Override
   public Optional<String> getMappingForEnabled() {
      return Optional.ofNullable(this.enabled);
   }

   private Optional<String> getMappingForValidFrom() {
      return Optional.ofNullable(this.validFrom);
   }

   private Optional<String> getMappingForValidTo() {
      return Optional.ofNullable(this.validTo);
   }

   private Optional<String> getMappingForGenerationDate() {
      return Optional.ofNullable(this.generationDate);
   }

   private Optional<String> getMappingForData() {
      return Optional.ofNullable(this.data);
   }

   private Optional<String> getMappingForEncoding() {
      return Optional.ofNullable(this.encoding);
   }

   private Optional<String> getMappingForDeliveryDate() {
      return Optional.ofNullable(this.deliveryDate);
   }

   @Override
   public GenericTokenAttributeMapping getMapping() {
      Map<String, MappingEntry> mappings = new HashMap<>();
      this.getMappingForSerialNumber().ifPresent(mapping -> mappings.put(mapping, this.mappingEntry(false, "serialNumber")));
      this.getMappingForEnabled()
         .ifPresent(mapping -> mappings.put(mapping, this.mappingEntry(true, "active", value -> value == null ? Boolean.FALSE : (Boolean)value)));
      this.getMappingForValidFrom().ifPresent(mapping -> mappings.put(mapping, this.mappingEntry(false, "validFrom", Dates::toDate)));
      this.getMappingForValidTo().ifPresent(mapping -> mappings.put(mapping, this.mappingEntry(false, "validTo", Dates::toDate)));
      this.getMappingForGenerationDate().ifPresent(mapping -> mappings.put(mapping, this.mappingEntry(true, "generationDate", Dates::toDate)));
      this.getMappingForData().ifPresent(mapping -> mappings.put(mapping, this.mappingEntry(false, "data")));
      this.getMappingForEncoding().ifPresent(mapping -> mappings.put(mapping, this.mappingEntry(false, "encoding")));
      this.getMappingForDeliveryDate().ifPresent(mapping -> mappings.put(mapping, this.mappingEntry(false, "deliveryDate", Dates::toDate)));
      if (!this.contextDataFields.isEmpty()) {
         mappings.put("contextData", new ContextDataMappingEntry(this.contextDataFields));
      }

      return new TokenAttributeMapping(mappings);
   }

   private MappingEntry mappingEntry(boolean readOnly, String fieldName) {
      return this.mappingEntry(readOnly, fieldName, v -> v);
   }

   private <T> MappingEntry mappingEntry(boolean readOnly, final String fieldName, final Function<Object, T> valueConverter) {
      return new MappingEntry(readOnly) {
         @Override
         void setAttributeValueOnToken(Object value, GenericToken token) {
            token.putAttribute(fieldName, valueConverter.apply(value));
         }

         @Override
         Object getAttributeValueFromToken(GenericToken token) {
            return token.getAttribute(fieldName);
         }
      };
   }

   @ConfigurationValidator
   public static void validateFields(PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context, Environment validationEnvironment) throws ConfigurationException {
      Set<String> attributeNames = new HashSet<>();
      List<CredentialBasedAttributeMapping.AttributeConfiguration> attributeConfigurations = Stream.of(
            CredentialBasedAttributeMapping.class.getDeclaredFields()
         )
         .filter(field -> field.getAnnotation(StringProperty.class) != null)
         .map(field -> CredentialBasedAttributeMapping.AttributeConfiguration.builder().attributeName(field.getName()).parent(config).build())
         .collect(Collectors.toList());
      Map<String, PluginConfiguration> contextDataFields = PluginMapPropertyValueReader.effectivePluginMapPropertyValue(
         config, "contextDataFields", env, context
      );
      attributeConfigurations.addAll(
         contextDataFields.values()
            .stream()
            .map(pc -> CredentialBasedAttributeMapping.AttributeConfiguration.builder().attributeName("name").parent(pc).build())
            .toList()
      );

      for (CredentialBasedAttributeMapping.AttributeConfiguration ac : attributeConfigurations) {
         String attributeName = checkAttributeNameFormat(ac.getAttributeName(), ac.getParent(), env, context);
         if (isAttributeNamedContextData(attributeName, ac)) {
            throw new PropertyConstraintViolationException(
               ac.getAttributeName(),
               ac.getParent(),
               env,
               context,
               String.format("'%s' is a reserved name for context data fields and cannot be used.", "contextData")
            );
         }

         if (attributeName != null && !attributeNames.add(attributeName)) {
            throw new PropertyConstraintViolationException(
               ac.getAttributeName(),
               ac.getParent(),
               env,
               context,
               String.format("The attribute names must be unique. An attribute with name '%s' already exists.", attributeName)
            );
         }
      }
   }

   private static boolean isAttributeNamedContextData(String attributeName, CredentialBasedAttributeMapping.AttributeConfiguration ac) throws ConfigurationException {
      return "contextData".equals(attributeName) && ac.getParent().isOfPluginClass(CredentialBasedAttributeMapping.class);
   }

   private static String checkAttributeNameFormat(String propertyName, PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context) throws PropertyConstraintViolationException, PropertyTypeMismatchException {
      String fieldValue = StringPropertyValueReader.effectiveStringConfigValue(config, propertyName, env, context);
      if (StringUtils.isEmpty(fieldValue)) {
         return null;
      } else if (!ATTRIBUTE_PATTERN.matcher(fieldValue).matches()) {
         throw new PropertyConstraintViolationException(
            propertyName, config, env, context, String.format("The attribute name must match the pattern %s.", ATTRIBUTE_PATTERN.pattern())
         );
      } else {
         return fieldValue;
      }
   }

   @SuppressFBWarnings(justification = "generated code")
   public static CredentialBasedAttributeMapping.CredentialBasedAttributeMappingBuilder builder() {
      return new CredentialBasedAttributeMapping.CredentialBasedAttributeMappingBuilder();
   }

   @SuppressFBWarnings(justification = "generated code")
   public CredentialBasedAttributeMapping() {
   }

   @SuppressFBWarnings(justification = "generated code")
   public CredentialBasedAttributeMapping(
      String serialNumber,
      String enabled,
      String validFrom,
      String validTo,
      String generationDate,
      String data,
      String encoding,
      String deliveryDate,
      Map<String, MappedContextDataField> contextDataFields
   ) {
      this.serialNumber = serialNumber;
      this.enabled = enabled;
      this.validFrom = validFrom;
      this.validTo = validTo;
      this.generationDate = generationDate;
      this.data = data;
      this.encoding = encoding;
      this.deliveryDate = deliveryDate;
      this.contextDataFields = contextDataFields;
   }

   private static final class AttributeConfiguration {
      private final String attributeName;
      private final PluginConfiguration parent;

      @SuppressFBWarnings(justification = "generated code")
      AttributeConfiguration(String attributeName, PluginConfiguration parent) {
         this.attributeName = attributeName;
         this.parent = parent;
      }

      @SuppressFBWarnings(justification = "generated code")
      public static CredentialBasedAttributeMapping.AttributeConfiguration.AttributeConfigurationBuilder builder() {
         return new CredentialBasedAttributeMapping.AttributeConfiguration.AttributeConfigurationBuilder();
      }

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

      @SuppressFBWarnings(justification = "generated code")
      public PluginConfiguration getParent() {
         return this.parent;
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public boolean equals(Object o) {
         if (o == this) {
            return true;
         } else if (!(o instanceof CredentialBasedAttributeMapping.AttributeConfiguration other)) {
            return false;
         } else {
            Object this$attributeName = this.attributeName;
            Object other$attributeName = other.attributeName;
            if (this$attributeName == null ? other$attributeName == null : this$attributeName.equals(other$attributeName)) {
               Object this$parent = this.parent;
               Object other$parent = other.parent;
               return this$parent == null ? other$parent == null : this$parent.equals(other$parent);
            } else {
               return false;
            }
         }
      }

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

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "CredentialBasedAttributeMapping.AttributeConfiguration(attributeName=" + this.getAttributeName() + ", parent=" + this.getParent() + ")";
      }

      @SuppressFBWarnings(justification = "generated code")
      public static class AttributeConfigurationBuilder {
         @SuppressFBWarnings(justification = "generated code")
         private String attributeName;
         @SuppressFBWarnings(justification = "generated code")
         private PluginConfiguration parent;

         @SuppressFBWarnings(justification = "generated code")
         AttributeConfigurationBuilder() {
         }

         @SuppressFBWarnings(justification = "generated code")
         public CredentialBasedAttributeMapping.AttributeConfiguration.AttributeConfigurationBuilder attributeName(String attributeName) {
            this.attributeName = attributeName;
            return this;
         }

         @SuppressFBWarnings(justification = "generated code")
         public CredentialBasedAttributeMapping.AttributeConfiguration.AttributeConfigurationBuilder parent(PluginConfiguration parent) {
            this.parent = parent;
            return this;
         }

         @SuppressFBWarnings(justification = "generated code")
         public CredentialBasedAttributeMapping.AttributeConfiguration build() {
            return new CredentialBasedAttributeMapping.AttributeConfiguration(this.attributeName, this.parent);
         }

         @SuppressFBWarnings(justification = "generated code")
         @Override
         public String toString() {
            return "CredentialBasedAttributeMapping.AttributeConfiguration.AttributeConfigurationBuilder(attributeName="
               + this.attributeName
               + ", parent="
               + this.parent
               + ")";
         }
      }
   }

   public enum ContextDataAttributeAccessType {
      READ_ONLY,
      WRITE_ONLY,
      READ_WRITE;
   }

   public enum ContextDataType {
      STRING,
      BOOLEAN,
      DATE_TIME,
      INTEGER;
   }

   @SuppressFBWarnings(justification = "generated code")
   public static class CredentialBasedAttributeMappingBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private String serialNumber;
      @SuppressFBWarnings(justification = "generated code")
      private String enabled;
      @SuppressFBWarnings(justification = "generated code")
      private String validFrom;
      @SuppressFBWarnings(justification = "generated code")
      private String validTo;
      @SuppressFBWarnings(justification = "generated code")
      private String generationDate;
      @SuppressFBWarnings(justification = "generated code")
      private String data;
      @SuppressFBWarnings(justification = "generated code")
      private String encoding;
      @SuppressFBWarnings(justification = "generated code")
      private String deliveryDate;
      @SuppressFBWarnings(justification = "generated code")
      private Map<String, MappedContextDataField> contextDataFields;

      @SuppressFBWarnings(justification = "generated code")
      CredentialBasedAttributeMappingBuilder() {
      }

      @SuppressFBWarnings(justification = "generated code")
      public CredentialBasedAttributeMapping.CredentialBasedAttributeMappingBuilder serialNumber(String serialNumber) {
         this.serialNumber = serialNumber;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public CredentialBasedAttributeMapping.CredentialBasedAttributeMappingBuilder enabled(String enabled) {
         this.enabled = enabled;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public CredentialBasedAttributeMapping.CredentialBasedAttributeMappingBuilder validFrom(String validFrom) {
         this.validFrom = validFrom;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public CredentialBasedAttributeMapping.CredentialBasedAttributeMappingBuilder validTo(String validTo) {
         this.validTo = validTo;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public CredentialBasedAttributeMapping.CredentialBasedAttributeMappingBuilder generationDate(String generationDate) {
         this.generationDate = generationDate;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public CredentialBasedAttributeMapping.CredentialBasedAttributeMappingBuilder data(String data) {
         this.data = data;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public CredentialBasedAttributeMapping.CredentialBasedAttributeMappingBuilder encoding(String encoding) {
         this.encoding = encoding;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public CredentialBasedAttributeMapping.CredentialBasedAttributeMappingBuilder deliveryDate(String deliveryDate) {
         this.deliveryDate = deliveryDate;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public CredentialBasedAttributeMapping.CredentialBasedAttributeMappingBuilder contextDataFields(Map<String, MappedContextDataField> contextDataFields) {
         this.contextDataFields = contextDataFields;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public CredentialBasedAttributeMapping build() {
         return new CredentialBasedAttributeMapping(
            this.serialNumber,
            this.enabled,
            this.validFrom,
            this.validTo,
            this.generationDate,
            this.data,
            this.encoding,
            this.deliveryDate,
            this.contextDataFields
         );
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "CredentialBasedAttributeMapping.CredentialBasedAttributeMappingBuilder(serialNumber="
            + this.serialNumber
            + ", enabled="
            + this.enabled
            + ", validFrom="
            + this.validFrom
            + ", validTo="
            + this.validTo
            + ", generationDate="
            + this.generationDate
            + ", data="
            + this.data
            + ", encoding="
            + this.encoding
            + ", deliveryDate="
            + this.deliveryDate
            + ", contextDataFields="
            + this.contextDataFields
            + ")";
      }
   }
}
