package com.airlock.iam.login.app.misc.configuration.targetapps;

import com.airlock.iam.common.api.domain.model.persistency.exception.NotFoundException;
import com.airlock.iam.common.api.domain.model.persistency.exception.NotUniqueException;
import com.airlock.iam.core.misc.authen.Authentee;
import com.airlock.iam.core.misc.authen.UsernameTransformationManager;
import com.airlock.iam.core.misc.authen.UsernameTransformer;
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.annotation.license.PluginPropertyLicenseRestriction;
import com.airlock.iam.plugin.api.annotation.property.GroupProperty;
import com.airlock.iam.plugin.api.annotation.property.PluginListProperty;
import com.airlock.iam.plugin.api.annotation.property.PluginProperty;
import com.airlock.iam.plugin.api.annotation.property.RegexListProperty;
import com.airlock.iam.plugin.api.annotation.property.RegexProperty;
import com.airlock.iam.plugin.api.annotation.property.StringListProperty;
import com.airlock.iam.plugin.api.annotation.property.StringProperty;
import com.airlock.iam.plugin.api.annotation.property.RegexProperty.CaseSensitivitySelector;
import com.airlock.iam.security.util.crypto.DecryptionException;
import com.airlock.iam.security.util.crypto.StringCipher;
import com.google.common.collect.Sets;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Pattern;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Strings;

public class GenericTargetApplicationBase {
   public static final String URL_SETTINGS = "URL Settings";
   public static final String IDENTITY_PROPAGATION_SETTINGS = "Identity Propagation and Authorization";
   public static final String PORTAL_SETTINGS = "Portal Settings";
   public static final String FEDERATION_SETTINGS = "Federation Settings";
   public static final String ROLE_PROCESSING_SETTINGS = "Roles Processing";
   protected static final String FIXED_PREFIX = "FIXED:";
   private static final ELogger LOG = ELoggerFactory.getLogger(GenericTargetApplicationBase.class);
   @RegexProperty(
      displayName = "URL Pattern",
      description = "The URL pattern (regular expression pattern) to identify this target application.<p>The first pattern (in the list of target applications) that matches the forward URL is used.<br/>The matching is case-insensitive.</p><p>The URL pattern is <b>ignored for the default target application</b>.</p>",
      optional = false,
      exampleValues = {".*", "https://host:8080/app/.*"},
      caseSensitivity = CaseSensitivitySelector.CASE_INSENSITIVE
   )
   @GroupProperty(groupIdentifier = "URL Settings")
   private Pattern urlPattern;
   public static final String URL_PATTERN_FIELD = "urlPattern";
   @StringProperty(
      description = "If a user can have a different username at this target application, it can be specified here how the other username should be obtained. The following options are possible: <ul><li>The name of the context data field in which this username is stored. Note that this field needs to be made persistent via the User Persister.</li><li>A fixed username (which is the same for all users). This should start with FIXED: followed by the username. E.g.: if the username is \"admin\", set this to \"FIXED:admin\".</li><li>Leave this empty, if no username is required or the standard username should be used.</li></ul><p>The resulting username can be transformed further by using the Username Transformation property.",
      optional = true,
      exampleValues = {"applA_username", "email", "FIXED:admin"}
   )
   @GroupProperty(groupIdentifier = "Federation Settings")
   @PluginPropertyLicenseRestriction(moduleAffiliation = "SubIdentities")
   private String useDifferentUsername;
   @PluginListProperty(
      description = "List of transformation plugins which allow various mutations of the username. The transformations are applied in order. Note that some username transformer stop the transformation chain after successful application.<p>These transformations are applied <b>after</b> the \"Use Different Username\" property.<p>",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Identity Propagation and Authorization")
   private List<UsernameTransformer> usernameTransformation;
   @StringProperty(
      description = "If a user needs a password for this target application, it can be specified here how it should be obtained. This is not supported by all plugins though. The following options are possible: <ul><li>If no password is required for this application: leave empty.</li><li>If the user has (or can have) a different password at this application: The name of the context data field in which this password is to be stored. Remember that this field needs to be made persistent via the User Persister.</li><li>If a fixed password is used that is the same for all users: Prefix with FIXED: followed by the password, e.g. \"FIXED:123456\".</li><li>If the user's main password (i.e. the password used to login to Airlock IAM) is used: Leave the field empty, but see the notes below.</li></ul> If the user's <b>main password</b> is also used to sign on to target applications, please note the following points: <ul><li><i>The main password can only be used if the user was required to enter the password upon login.</i> This is not the case for Kerbos and other SSO-Logins. In those cases, this option is not possible.</li><li>Normally, the user's password is only available directly after login. If the user comes back to the Loginapp later, e.g. to access a different application, <i>the password is normally not available anymore</i>.</li><li>If only one target application is configured, this should not be a problem, since the user only needs to be authenticated at the very beginning (directly after the authentication).</li> <li>If the user's password is needed for several target application, then it is has to be available every time the user wants to access another application in the same session. In this case, the password should be saved in the session ticket (see Security Settings).</li></ul>",
      optional = true,
      exampleValues = {"applA_password", "FIXED:123456"}
   )
   @GroupProperty(groupIdentifier = "Federation Settings")
   @PluginPropertyLicenseRestriction(moduleAffiliation = "SubIdentities")
   private String useDifferentPassword;
   @PluginProperty(
      description = "The type of password encryption used to decrypt this password.<br/>Leave empty if the password is not encrypted (not recommended if the password is read from a context data field).",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Federation Settings")
   @PluginPropertyLicenseRestriction(moduleAffiliation = "SubIdentities")
   private StringCipher passwordEncryptionMethod;
   @StringListProperty(
      description = "A list of roles used to access this target application.<p>The user needs <b>at least one of the roles</b> in order to get access to the application.</p><p>If no roles are configured, <b>all authenticated users may access the application</b>.</p><p>The user's roles may be transformed before being compared to this list using the <i>Role Transformation Rules</i> (see separate property).</p><p>If the user doesn't have any of these roles, the \"Step-Up Authenticators\" (in Authentication Settings) are consulted in order to find out whether they can be obtained using a Step-Up.</p>",
      optional = true,
      exampleElements = {"user", "admin", "employee"}
   )
   @GroupProperty(groupIdentifier = "Identity Propagation and Authorization")
   private List<String> requiredRoles;
   @StringListProperty(
      description = "<p>The Airlock Gateway (WAF) roles that should be set when accessing this target application, instead of using the users roles as Gateway roles.</p><p>The name of the role can be followed by a colon and the <em>idle timeout</em> of the role in seconds, e.g. \"myrole:300\" sets the role \"myrole\" that will expire after 5 minutes of client inactivity.</p><p>With a second colon and a second number, the <em>life-time</em> can be set, e.g. \"myrole:300:3600\" will set the role \"myrole\" for a maximum of 1 hour, but it will also expire after 5 minutes of client inactivity.</p><p>Note: If you want to <b>replace</b> (instead of add) target application's Gateway roles in the session upon the first visit of each target application, you have to disable the \"Add Credentials To Session\" flag in the \"Airlock Gateway (WAF) Settings\" of the Login Application.</p>",
      optional = true,
      exampleElements = {"app:300", "authorized:3600"},
      displayName = "Airlock Gateway (WAF) Roles"
   )
   @GroupProperty(groupIdentifier = "Identity Propagation and Authorization")
   private List<String> airlockGatewayRoles;
   @PluginListProperty(
      description = "A list of transformation rules used to modify user roles before being compared to the \"Required Roles\" of an application.",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Roles Processing")
   private List<RoleTransformationRule> roleTransformationRules;
   @RegexListProperty(
      description = "A list of regular expressions. Any role matching one of these expressions is not propagated to the target application, <i>unless it also matches one of the \"Propagated Roles To Keep\"</i>. The matching is performed before any transformation.",
      optional = true,
      matchAgainstWholeInput = true,
      exampleElements = "admin.*"
   )
   @GroupProperty(groupIdentifier = "Roles Processing")
   private List<Pattern> propagatedRolesToDelete;
   @RegexListProperty(
      description = "A list of regular expressions. If set, only roles matching at least one of these expressions are propagated to the target application, even if they match one of the \"Propagated Roles To Delete\". Notice that any \"Propagated Roles To Add\" are <i>always</i> added. The matching is performed before any transformation.",
      optional = true,
      matchAgainstWholeInput = true,
      exampleElements = "cust.*"
   )
   @GroupProperty(groupIdentifier = "Roles Processing")
   private List<Pattern> propagatedRolesToKeep;
   @PluginListProperty(
      description = "A list of transformation rules used to modify roles names that are sent to an application by the identity propagator. All transformations are applied to every role that has not been deleted before as a pipeline.",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Roles Processing")
   private List<RoleTransformationRule> propagatedRolesTransformationRules;
   @StringListProperty(
      description = "The static roles that will always be added to the final list of propagated roles (no transformation is applied to those).",
      optional = true,
      exampleElements = {"customer", "administrator"}
   )
   @GroupProperty(groupIdentifier = "Roles Processing")
   private List<String> propagatedRolesToAdd;

   public String getUsernameToPropagate(Authentee authentee) {
      String nameToTransform;
      if (StringUtils.isBlank(this.useDifferentUsername)) {
         nameToTransform = authentee.getName();
      } else if (Strings.CS.startsWith(this.useDifferentUsername, "FIXED:")) {
         nameToTransform = Strings.CS.removeStart(this.useDifferentUsername, "FIXED:");
      } else {
         nameToTransform = StringUtils.trimToNull(Objects.toString(authentee.getContextDataContainer().get(this.useDifferentUsername), null));
      }

      if (StringUtils.isBlank(nameToTransform)) {
         return null;
      }

      UsernameTransformationManager transformationManager = new UsernameTransformationManager(this.usernameTransformation);

      try {
         return transformationManager.transformUsername(nameToTransform);
      } catch (NotUniqueException | NotFoundException e) {
         throw new IllegalStateException("Username transformation of user \"" + nameToTransform + "\" failed.", e);
      }
   }

   public Optional<String> getApplicationSpecificPassword(Authentee authentee) {
      if (StringUtils.isBlank(this.useDifferentPassword)) {
         return Optional.empty();
      }

      if (Strings.CS.startsWith(this.useDifferentPassword, "FIXED:")) {
         return Optional.of(Strings.CS.removeStart(this.useDifferentPassword, "FIXED:"));
      }

      String readPwd = StringUtils.trimToNull(Objects.toString(authentee.getContextDataContainer().get(this.useDifferentPassword), null));
      if (readPwd == null) {
         return Optional.empty();
      }

      if (this.passwordEncryptionMethod == null) {
         return Optional.of(readPwd);
      }

      try {
         return Optional.of(this.passwordEncryptionMethod.decrypt(readPwd, authentee.getName()));
      } catch (DecryptionException e) {
         LOG.warn(LogTypes.ILLEGAL_ARGUMENT, "Unable to decrypt password: " + e.getMessage(), e);
         throw new IllegalStateException("Unable to decrypt password: " + e.getMessage(), e);
      }
   }

   public String[] getRolesToPropagate(Authentee authentee) {
      String[] inputRoles = authentee.getGrantedRoleNames();
      Set<String> untransformedRoles = Sets.newLinkedHashSet();
      untransformedRoles.addAll(
         Arrays.stream(inputRoles)
            .filter(inputRole -> this.propagatedRolesToDelete.stream().noneMatch(deleteRule -> deleteRule.matcher(inputRole).matches()))
            .toList()
      );
      if (!this.propagatedRolesToKeep.isEmpty() && this.propagatedRolesToDelete.isEmpty()) {
         untransformedRoles = Sets.newLinkedHashSet();
      }

      untransformedRoles.addAll(
         Arrays.stream(inputRoles)
            .filter(inputRole -> this.propagatedRolesToKeep.stream().anyMatch(keepRule -> keepRule.matcher(inputRole).matches()))
            .toList()
      );
      Set<String> transformedRoles = Sets.newLinkedHashSet();

      for (String r : untransformedRoles) {
         for (RoleTransformationRule t : this.propagatedRolesTransformationRules) {
            r = t.transformRole(r);
         }

         transformedRoles.add(r);
      }

      transformedRoles.addAll(this.propagatedRolesToAdd);
      return transformedRoles.toArray(ArrayUtils.EMPTY_STRING_ARRAY);
   }

   public String transformRole(String role) {
      String r = role;

      for (RoleTransformationRule rule : this.roleTransformationRules) {
         r = rule.transformRole(r);
      }

      return r;
   }

   public Pattern getUrlPattern() {
      return this.urlPattern;
   }

   public List<String> getRequiredRoles() {
      return this.requiredRoles;
   }

   public List<String> getAirlockGatewayRoles() {
      return Collections.unmodifiableList(this.airlockGatewayRoles);
   }

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

   @SuppressFBWarnings(justification = "generated code")
   public GenericTargetApplicationBase(
      Pattern urlPattern,
      String useDifferentUsername,
      List<UsernameTransformer> usernameTransformation,
      String useDifferentPassword,
      StringCipher passwordEncryptionMethod,
      List<String> requiredRoles,
      List<String> airlockGatewayRoles,
      List<RoleTransformationRule> roleTransformationRules,
      List<Pattern> propagatedRolesToDelete,
      List<Pattern> propagatedRolesToKeep,
      List<RoleTransformationRule> propagatedRolesTransformationRules,
      List<String> propagatedRolesToAdd
   ) {
      this.urlPattern = urlPattern;
      this.useDifferentUsername = useDifferentUsername;
      this.usernameTransformation = usernameTransformation;
      this.useDifferentPassword = useDifferentPassword;
      this.passwordEncryptionMethod = passwordEncryptionMethod;
      this.requiredRoles = requiredRoles;
      this.airlockGatewayRoles = airlockGatewayRoles;
      this.roleTransformationRules = roleTransformationRules;
      this.propagatedRolesToDelete = propagatedRolesToDelete;
      this.propagatedRolesToKeep = propagatedRolesToKeep;
      this.propagatedRolesTransformationRules = propagatedRolesTransformationRules;
      this.propagatedRolesToAdd = propagatedRolesToAdd;
   }

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

   @SuppressFBWarnings(justification = "generated code")
   public static class GenericTargetApplicationBaseBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private Pattern urlPattern;
      @SuppressFBWarnings(justification = "generated code")
      private String useDifferentUsername;
      @SuppressFBWarnings(justification = "generated code")
      private List<UsernameTransformer> usernameTransformation;
      @SuppressFBWarnings(justification = "generated code")
      private String useDifferentPassword;
      @SuppressFBWarnings(justification = "generated code")
      private StringCipher passwordEncryptionMethod;
      @SuppressFBWarnings(justification = "generated code")
      private ArrayList<String> requiredRoles;
      @SuppressFBWarnings(justification = "generated code")
      private List<String> airlockGatewayRoles;
      @SuppressFBWarnings(justification = "generated code")
      private List<RoleTransformationRule> roleTransformationRules;
      @SuppressFBWarnings(justification = "generated code")
      private List<Pattern> propagatedRolesToDelete;
      @SuppressFBWarnings(justification = "generated code")
      private List<Pattern> propagatedRolesToKeep;
      @SuppressFBWarnings(justification = "generated code")
      private List<RoleTransformationRule> propagatedRolesTransformationRules;
      @SuppressFBWarnings(justification = "generated code")
      private List<String> propagatedRolesToAdd;

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

      @SuppressFBWarnings(justification = "generated code")
      public GenericTargetApplicationBase.GenericTargetApplicationBaseBuilder urlPattern(Pattern urlPattern) {
         this.urlPattern = urlPattern;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public GenericTargetApplicationBase.GenericTargetApplicationBaseBuilder useDifferentUsername(String useDifferentUsername) {
         this.useDifferentUsername = useDifferentUsername;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public GenericTargetApplicationBase.GenericTargetApplicationBaseBuilder usernameTransformation(List<UsernameTransformer> usernameTransformation) {
         this.usernameTransformation = usernameTransformation;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public GenericTargetApplicationBase.GenericTargetApplicationBaseBuilder useDifferentPassword(String useDifferentPassword) {
         this.useDifferentPassword = useDifferentPassword;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public GenericTargetApplicationBase.GenericTargetApplicationBaseBuilder passwordEncryptionMethod(StringCipher passwordEncryptionMethod) {
         this.passwordEncryptionMethod = passwordEncryptionMethod;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public GenericTargetApplicationBase.GenericTargetApplicationBaseBuilder requiredRole(String requiredRole) {
         if (this.requiredRoles == null) {
            this.requiredRoles = new ArrayList<>();
         }

         this.requiredRoles.add(requiredRole);
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public GenericTargetApplicationBase.GenericTargetApplicationBaseBuilder requiredRoles(Collection<? extends String> requiredRoles) {
         if (requiredRoles == null) {
            throw new NullPointerException("requiredRoles cannot be null");
         }

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

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

      @SuppressFBWarnings(justification = "generated code")
      public GenericTargetApplicationBase.GenericTargetApplicationBaseBuilder clearRequiredRoles() {
         if (this.requiredRoles != null) {
            this.requiredRoles.clear();
         }

         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public GenericTargetApplicationBase.GenericTargetApplicationBaseBuilder airlockGatewayRoles(List<String> airlockGatewayRoles) {
         this.airlockGatewayRoles = airlockGatewayRoles;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public GenericTargetApplicationBase.GenericTargetApplicationBaseBuilder roleTransformationRules(List<RoleTransformationRule> roleTransformationRules) {
         this.roleTransformationRules = roleTransformationRules;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public GenericTargetApplicationBase.GenericTargetApplicationBaseBuilder propagatedRolesToDelete(List<Pattern> propagatedRolesToDelete) {
         this.propagatedRolesToDelete = propagatedRolesToDelete;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public GenericTargetApplicationBase.GenericTargetApplicationBaseBuilder propagatedRolesToKeep(List<Pattern> propagatedRolesToKeep) {
         this.propagatedRolesToKeep = propagatedRolesToKeep;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public GenericTargetApplicationBase.GenericTargetApplicationBaseBuilder propagatedRolesTransformationRules(
         List<RoleTransformationRule> propagatedRolesTransformationRules
      ) {
         this.propagatedRolesTransformationRules = propagatedRolesTransformationRules;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public GenericTargetApplicationBase.GenericTargetApplicationBaseBuilder propagatedRolesToAdd(List<String> propagatedRolesToAdd) {
         this.propagatedRolesToAdd = propagatedRolesToAdd;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public GenericTargetApplicationBase build() {
         return new GenericTargetApplicationBase(
            this.urlPattern,
            this.useDifferentUsername,
            this.usernameTransformation,
            this.useDifferentPassword,
            this.passwordEncryptionMethod,
            switch (this.requiredRoles == null ? 0 : this.requiredRoles.size()) {
               case 0 -> Collections.emptyList();
               case 1 -> Collections.singletonList(this.requiredRoles.get(0));
               default -> Collections.unmodifiableList(new ArrayList<>(this.requiredRoles));
            },
            this.airlockGatewayRoles,
            this.roleTransformationRules,
            this.propagatedRolesToDelete,
            this.propagatedRolesToKeep,
            this.propagatedRolesTransformationRules,
            this.propagatedRolesToAdd
         );
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "GenericTargetApplicationBase.GenericTargetApplicationBaseBuilder(urlPattern="
            + this.urlPattern
            + ", useDifferentUsername="
            + this.useDifferentUsername
            + ", usernameTransformation="
            + this.usernameTransformation
            + ", useDifferentPassword="
            + this.useDifferentPassword
            + ", passwordEncryptionMethod="
            + this.passwordEncryptionMethod
            + ", requiredRoles="
            + this.requiredRoles
            + ", airlockGatewayRoles="
            + this.airlockGatewayRoles
            + ", roleTransformationRules="
            + this.roleTransformationRules
            + ", propagatedRolesToDelete="
            + this.propagatedRolesToDelete
            + ", propagatedRolesToKeep="
            + this.propagatedRolesToKeep
            + ", propagatedRolesTransformationRules="
            + this.propagatedRolesTransformationRules
            + ", propagatedRolesToAdd="
            + this.propagatedRolesToAdd
            + ")";
      }
   }
}
