package com.airlock.iam.saml2.application.configuration;

import com.airlock.iam.core.misc.authen.Authentee;
import com.airlock.iam.core.misc.impl.sso.AssertionAttributeSpec;
import com.airlock.iam.core.misc.impl.sso.IdentityPropagatorUtils;
import com.airlock.iam.core.misc.impl.sso.ValueTransformation;
import com.airlock.iam.core.misc.util.ticket.KeyMultiValue;
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.PluginListProperty;
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.valuereader.StringPropertyValueReader;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Strings;

@Plugin(description = "Holds information about an attribute to put into the assertion.")
public final class AssertionAttribute implements AssertionAttributeSpec {
   @StringProperty(
      description = "The <i>name</i> of an additional <i>SAML2 Attribute</i> to be added to the Assertion. The value of the attribute is defined by the corresponding <code>value</code>-property or <code>static-value</code> property. One of <code>value</code>or <code>static-value</code> must be set, but not both at the same time.",
      exampleValues = {"username", "lang", "authentication-method"},
      optional = false
   )
   private String name;
   @StringProperty(
      description = "The <i>value(s)</i> of an additional <i>SAML2 Attribute</i> to be added to the Assertion. The specified value is interpreted as follows:<ul> <li>The value <code>@username</code> refers to the user's name.</li> <li>The value <code>@roles</code> refers to the user's roles.</li> <li>The value <code>@info:key</code> refers to the element of the additional input data with the given key.</li> <li>The value <code>@param:key</code> refers to the element of the parameter map with the given key.</li> <li>Any other value is retrieved from the user's context data container.</li></ul>",
      optional = true,
      exampleValues = {"@username", "language", "auth_method", "@info:authLevel"}
   )
   private String value;
   private static final String VALUE_FIELD = "value";
   @PluginListProperty(
      description = "A list of <i>Value Transformations</i> that is applied to the <i>Value</i>. If multiple transformations are defined, the first matching one will be executed and the later ones ignored.",
      optional = true
   )
   private List<ValueTransformation> valueTransformations;
   @StringProperty(
      description = "The static <i>value(s)</i> of an additional <i>SAML2 Attribute</i> to be added to the Assertion. ",
      optional = true,
      exampleValues = {"security-level", "language", "Airlock"}
   )
   private String staticValue;
   private static final String STATIC_VALUE_FIELD = "staticValue";

   @Override
   public String[] getAttributeValue(Authentee authentee, String userName, List<KeyMultiValue> additionalInformationToPropagate, Map<String, Object> parameter) {
      if (this.isStaticValue()) {
         return new String[]{this.staticValue};
      }

      String[] dynamicValues = this.getDynamicAttributeValue(authentee, userName, additionalInformationToPropagate, parameter);

      for (int i = 0; i < dynamicValues.length; i++) {
         dynamicValues[i] = this.transformValueIfNecessary(dynamicValues[i]);
      }

      return dynamicValues;
   }

   private String transformValueIfNecessary(String originalValue) {
      for (ValueTransformation transformation : this.valueTransformations) {
         if (transformation.doesMatch(originalValue)) {
            return transformation.transform(originalValue);
         }
      }

      return originalValue;
   }

   private boolean isStaticValue() {
      return this.value == null;
   }

   private String[] getDynamicAttributeValue(
      Authentee authentee, String userName, List<KeyMultiValue> additionalInformationToPropagate, Map<String, Object> parameter
   ) {
      String key = this.value.trim();
      if ("@username".equalsIgnoreCase(key)) {
         return new String[]{userName};
      }

      if ("@roles".equalsIgnoreCase(key)) {
         return IdentityPropagatorUtils.getRolesFromMap(authentee, parameter);
      }

      if (Strings.CI.startsWith(key, "@info:")) {
         String searchKey = StringUtils.substring(key, "@info:".length());

         for (KeyMultiValue keyMultiValue : additionalInformationToPropagate) {
            if (Strings.CS.equals(searchKey, keyMultiValue.getKey())) {
               return keyMultiValue.getValues();
            }
         }

         return new String[0];
      } else {
         if (Strings.CI.startsWith(key, "@param:")) {
            String searchKey = StringUtils.substring(key, "@param:".length());
            String val = (String)parameter.get(searchKey);
            return val != null ? new String[]{val} : new String[0];
         }

         Object val = authentee.getContextDataContainer().get(key);
         if (val == null) {
            return new String[0];
         }

         if (val instanceof String[]) {
            return (String[])val;
         }

         if (val instanceof Collection) {
            List<String> valueList = new ArrayList<>(((Collection)val).size());

            for (Object valueItem : (Collection)val) {
               if (valueItem != null) {
                  valueList.add(valueItem.toString());
               }
            }

            return valueList.toArray(String[]::new);
         } else {
            return new String[]{Objects.toString(val)};
         }
      }
   }

   @Override
   public String toString() {
      return this.name + "=" + (this.value == null ? this.staticValue : this.value);
   }

   @Override
   public String getName() {
      return this.name;
   }

   public String getValue() {
      return this.value;
   }

   @ConfigurationValidator
   public static void validateConfiguration(PluginConfiguration config, ConfigurationEnvironment configEnv, ConfigurationContext context, Environment env) throws ConfigurationException {
      String value = StringPropertyValueReader.effectiveStringConfigValue(config, "value", configEnv, context);
      String staticValue = StringPropertyValueReader.effectiveStringConfigValue(config, "staticValue", configEnv, context);
      if (StringUtils.isEmpty(value) && StringUtils.isEmpty(staticValue)) {
         throw new PropertyConstraintViolationException(
            "value", config, configEnv, context, "No value defined for \"Value\" nor \"Static Value\". One of both must be set."
         );
      }

      if (StringUtils.isNotEmpty(value) && StringUtils.isNotEmpty(staticValue)) {
         throw new PropertyConstraintViolationException(
            "value", config, configEnv, context, "Values are defined for both \"Value\" and \"Static Value\". Only one of them may be set."
         );
      }
   }

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

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

   @SuppressFBWarnings(justification = "generated code")
   public AssertionAttribute(String name, String value, List<ValueTransformation> valueTransformations, String staticValue) {
      this.name = name;
      this.value = value;
      this.valueTransformations = valueTransformations;
      this.staticValue = staticValue;
   }

   @SuppressFBWarnings(justification = "generated code")
   public static class AssertionAttributeBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private String name;
      @SuppressFBWarnings(justification = "generated code")
      private String value;
      @SuppressFBWarnings(justification = "generated code")
      private ArrayList<ValueTransformation> valueTransformations;
      @SuppressFBWarnings(justification = "generated code")
      private String staticValue;

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

      @SuppressFBWarnings(justification = "generated code")
      public AssertionAttribute.AssertionAttributeBuilder name(String name) {
         this.name = name;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public AssertionAttribute.AssertionAttributeBuilder value(String value) {
         this.value = value;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public AssertionAttribute.AssertionAttributeBuilder valueTransformation(ValueTransformation valueTransformation) {
         if (this.valueTransformations == null) {
            this.valueTransformations = new ArrayList<>();
         }

         this.valueTransformations.add(valueTransformation);
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public AssertionAttribute.AssertionAttributeBuilder valueTransformations(Collection<? extends ValueTransformation> valueTransformations) {
         if (valueTransformations == null) {
            throw new NullPointerException("valueTransformations cannot be null");
         }

         if (this.valueTransformations == null) {
            this.valueTransformations = new ArrayList<>();
         }

         this.valueTransformations.addAll(valueTransformations);
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public AssertionAttribute.AssertionAttributeBuilder clearValueTransformations() {
         if (this.valueTransformations != null) {
            this.valueTransformations.clear();
         }

         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public AssertionAttribute.AssertionAttributeBuilder staticValue(String staticValue) {
         this.staticValue = staticValue;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public AssertionAttribute build() {
         return new AssertionAttribute(this.name, this.value, switch (this.valueTransformations == null ? 0 : this.valueTransformations.size()) {
            case 0 -> Collections.emptyList();
            case 1 -> Collections.singletonList(this.valueTransformations.get(0));
            default -> Collections.unmodifiableList(new ArrayList<>(this.valueTransformations));
         }, this.staticValue);
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "AssertionAttribute.AssertionAttributeBuilder(name="
            + this.name
            + ", value="
            + this.value
            + ", valueTransformations="
            + this.valueTransformations
            + ", staticValue="
            + this.staticValue
            + ")";
      }
   }
}
