package com.airlock.iam.authentication.application.configuration.targetapp;

import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.common.application.configuration.loginhistory.LoginHistoryRepositoryConfig;
import com.airlock.iam.common.application.configuration.rememberme.RememberMeConfig;
import com.airlock.iam.common.application.configuration.templock.TemporaryLockingConfig;
import com.airlock.iam.flow.api.application.configuration.selection.FlowConditionConfig;
import com.airlock.iam.flow.api.domain.model.flow.ApplicationId;
import com.airlock.iam.flow.application.configuration.selection.condition.AlwaysFalseConditionConfig;
import com.airlock.iam.login.api.application.configuration.location.interpret.LocationInterpretersConfig;
import com.airlock.iam.login.application.configuration.existingsessionbehavior.ExistingUserSessionBehaviorConfig;
import com.airlock.iam.login.application.configuration.existingsessionbehavior.IgnoreExistingUserSessionsConfig;
import com.airlock.iam.plugin.api.annotation.config.ConfigurationValidator;
import com.airlock.iam.plugin.api.annotation.config.Groups;
import com.airlock.iam.plugin.api.annotation.config.Plugin;
import com.airlock.iam.plugin.api.annotation.property.BooleanProperty;
import com.airlock.iam.plugin.api.annotation.property.GroupProperty;
import com.airlock.iam.plugin.api.annotation.property.IntegerProperty;
import com.airlock.iam.plugin.api.annotation.property.PluginListProperty;
import com.airlock.iam.plugin.api.annotation.property.PluginProperty;
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.PropertyConfigurationException;
import com.airlock.iam.plugin.api.exception.PropertyConstraintViolationException;
import com.airlock.iam.plugin.api.exception.PropertyTypeMismatchException;
import com.airlock.iam.plugin.api.valuereader.PluginListPropertyValueReader;
import com.airlock.iam.plugin.api.valuereader.PluginPropertyValueReader;
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.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Stream;

@Plugin(
   description = "Configuration of target applications including authentication and authorization flows.",
   displayName = "Target Applications and Authentication"
)
@Groups(groupSequence = {"Basic Settings", "One-Shot Authentication", "Advanced Settings"}, openGroups = "Basic Settings")
public class TargetApplicationsConfig {
   static final String ONE_SHOT_GROUP = "One-Shot Authentication";
   @PluginProperty(
      displayName = "Default Application",
      description = "The default application that is selected if a user/client directly accesses an authentication flow resource without explicitly selecting a flow by using the authorization resource.",
      optional = false
   )
   @GroupProperty(groupIdentifier = "Basic Settings")
   private TargetApplicationConfig defaultApplication;
   public static final String DEFAULT_APPLICATION_PROPERTY = "defaultApplication";
   public static final String DEFAULT_APPLICATION_DISPLAY_NAME = "Default Application";
   @PluginListProperty(description = "List of other protected applications. The default application must not be part of this list.", optional = true)
   @GroupProperty(groupIdentifier = "Basic Settings")
   private List<TargetApplicationConfig> applications;
   public static final String APPLICATIONS_PROPERTY = "applications";
   @PluginListProperty(description = "Applications to be accessed via the one-shot access endpoint.", displayName = "One-Shot Applications", optional = true)
   @GroupProperty(groupIdentifier = "One-Shot Authentication")
   private List<OneShotTargetApplicationConfig> oneShotApplications;
   public static final String ONE_SHOT_APPLICATIONS_PROPERTY = "oneShotApplications";
   @IntegerProperty(
      displayName = "Max Failed Factor Attempts",
      description = "Maximal number of allowed login attempts. The user is locked if the number of failed attempts for some credential exceeds this limit.",
      exampleValues = {3, 5},
      defaultValue = 5,
      minValue = 1,
      optional = true
   )
   @GroupProperty(groupIdentifier = "Basic Settings")
   private int maxFailedLogins;
   @PluginProperty(
      description = "Locks users temporarily after unsuccessful authentication attempts. This helps to prevent password brute force attacks.<p>This configuration can be enabled or disabled in each \"Authentication Flow\" with the setting \"Enable Temporary Locking\".</p>",
      displayName = "Temporary Locking",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Basic Settings")
   private TemporaryLockingConfig temporaryLockingConfig;
   @PluginProperty(
      description = "<p>Configuration enabling the Remember-Me feature. This feature allows a user to skip certain authentication steps if a valid token is presented with the request.</p>",
      displayName = "Remember-Me Settings",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Basic Settings")
   private RememberMeConfig rememberMeConfig;
   public static final String REMEMBER_ME_PROPERTY = "rememberMeConfig";
   @PluginProperty(
      description = "Repository to store the history of successful logins. Login history entries are only written in flows where the corresponding flow processor is enabled.",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Basic Settings")
   private LoginHistoryRepositoryConfig loginHistoryRepository;
   private static final String LOGIN_HISTORY_RESPOSITORY_FLOW_PROPERTY = "loginHistoryRepository";
   @PluginProperty(
      description = "<p>Defines what action is taken when a user already has another authenticated Airlock Gateway session\nwhen logging in or logging out. For example, this can happen if the user left a previous session\nwithout an explicit logout or attempts to authenticate multiple concurrent sessions\n(with different devices or browsers).</p>\n<p>The configured behavior may depend on the user store being able to read (and write) the session ID\nto/from the database/directory. Verify that the corresponding column or attribute is mapped in the used\nuser store (\"Col Latest GSID\" for default database plugins; \"Last GSID Value Attribute\" in LDAP plugins).</p>",
      optional = true,
      defaultValue = IgnoreExistingUserSessionsConfig.class
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private ExistingUserSessionBehaviorConfig behaviorUponExistingSession;
   @BooleanProperty(
      description = "Disables the \"Behavior Upon Existing Session\" for represented sessions: no stored session IDs are updated, nor are existing sessions terminated. This prevents any interference with regular sessions from the represented user.",
      optional = true,
      defaultValue = "true"
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private boolean disableSessionBehaviorWhenRepresented;
   @PluginListProperty(
      description = "<p>Enables the REST endpoint used for interpreting a forward location URI prior to starting an authentication flow (REST endpoint <code>/&lt;loginapp-uri&gt;/rest/public/authentication/location/interpret/</code>).</p><p>This endpoint could extract the display language or other information from the given URI so that the client can configure itself.</p><p>The order of the configured plugins is relevant. The first plugin which can handle the forward location URI is used, all remaining plugins are ignored. If no plugins are configured or no plugin can handle the provided URI, an empty interpretation result is returned.</p>",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private List<LocationInterpretersConfig> locationInterpreters;
   @PluginProperty(
      description = "<p>Condition for allowing unauthenticated access to the /public/authentication endpoint. Once a user is\nauthenticated, access to this endpoint is always allowed, regardless of this setting.</p>\n<p>By default, access to the information endpoint is not allowed for unauthenticated users.</p>\n<p>Security Note: If publicly available, this endpoint could be exploited for user enumeration attacks.\nTo prevent these attacks, it is recommended that access to the endpoint is restricted to authenticated users only.</p>",
      optional = true,
      defaultValue = AlwaysFalseConditionConfig.class
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private FlowConditionConfig authenticationInformationAccessibleCondition;

   public Optional<TemporaryLockingConfig> getTemporaryLockingConfig() {
      return Optional.ofNullable(this.temporaryLockingConfig);
   }

   @ConfigurationValidator
   static void validateApplicationIdsUnique(PluginConfiguration pc, ConfigurationEnvironment env, ConfigurationContext context, Environment valEnv) throws PropertyConfigurationException {
      Set<String> seenApplIds = new HashSet<>();

      for (PluginConfiguration appl : allApplicationConfigs(pc, env, context)) {
         String applId = applicationId(appl, env, context);
         if (!seenApplIds.add(applId)) {
            throw new PropertyConstraintViolationException("applications", pc, env, context, "Application ID " + LogUtil.quote(applId) + " is not unique");
         }
      }
   }

   private static String applicationId(PluginConfiguration applicationConfig, ConfigurationEnvironment env, ConfigurationContext context) throws PropertyTypeMismatchException {
      PluginConfiguration idConfig = PluginPropertyValueReader.effectivePluginPropertyValue(applicationConfig, "applicationId", env, context);
      return idConfig == null ? null : StringPropertyValueReader.effectiveStringConfigValue(idConfig, "applicationId", env, context);
   }

   @ConfigurationValidator
   static void validateNoTransactionApprovalPluginsConfigured(
      PluginConfiguration pc, ConfigurationEnvironment env, ConfigurationContext context, Environment valEnv
   ) throws ConfigurationException {
      ValidateNoTransactionApprovalPlugins.validateNoTransactionApprovalPlugins(pc, env, context, valEnv);
   }

   @ConfigurationValidator
   static void validateNoRememberMeStepsPresentIfNoRememberMeConfig(
      PluginConfiguration pc, ConfigurationEnvironment env, ConfigurationContext context, Environment valEnv
   ) throws ConfigurationException {
      if (hasNoRememberMeSettings(pc, env, context)) {
         try {
            ValidateNoRememberMePlugins.validateNoRememberMePlugins(pc, env, context, valEnv);
         } catch (PropertyConfigurationException e) {
            throw new PropertyConstraintViolationException("rememberMeConfig", pc, env, context, e.getDescription());
         }
      }
   }

   @ConfigurationValidator
   static void validateLoginHistoryIsPresentIfLoginHistoryProcessorIsPresent(
      PluginConfiguration pc, ConfigurationEnvironment env, ConfigurationContext context, Environment valEnv
   ) throws ConfigurationException {
      if (hasNoLoginHistoryRepository(pc, env, context)) {
         try {
            ValidateNoLoginHistoryPlugins.validateNoLoginHistoryPlugins(pc, env, context, valEnv);
         } catch (PropertyConfigurationException e) {
            throw new PropertyConstraintViolationException("loginHistoryRepository", pc, env, context, e.getDescription());
         }
      }
   }

   private static boolean hasNoLoginHistoryRepository(PluginConfiguration pc, ConfigurationEnvironment env, ConfigurationContext context) throws PropertyTypeMismatchException {
      return PluginPropertyValueReader.effectivePluginPropertyValue(pc, "loginHistoryRepository", env, context) == null;
   }

   private static boolean hasNoRememberMeSettings(PluginConfiguration pc, ConfigurationEnvironment env, ConfigurationContext context) throws PropertyTypeMismatchException {
      return PluginPropertyValueReader.effectivePluginPropertyValue(pc, "rememberMeConfig", env, context) == null;
   }

   private static List<PluginConfiguration> allApplicationConfigs(PluginConfiguration pc, ConfigurationEnvironment env, ConfigurationContext context) throws PropertyConfigurationException {
      List<PluginConfiguration> applConfigs = new ArrayList<>();
      applConfigs.add(PluginPropertyValueReader.effectivePluginPropertyValue(pc, "defaultApplication", env, context));
      applConfigs.addAll(PluginListPropertyValueReader.effectivePluginListPropertyValue(pc, "applications", env, context));
      applConfigs.addAll(PluginListPropertyValueReader.effectivePluginListPropertyValue(pc, "oneShotApplications", env, context));
      return applConfigs;
   }

   public Optional<TargetApplicationConfigSpec> getApplicationConfig(ApplicationId applicationId) {
      return this.defaultApplication.getApplicationId().equals(applicationId)
         ? Optional.of(this.defaultApplication)
         : Stream.concat(this.applications.stream(), this.oneShotApplications.stream()).filter(app -> app.getApplicationId().equals(applicationId)).findAny();
   }

   public Optional<LoginHistoryRepositoryConfig> getLoginHistoryRepository() {
      return Optional.ofNullable(this.loginHistoryRepository);
   }

   public TargetApplicationConfig getDefaultApplication() {
      return this.defaultApplication;
   }

   public List<TargetApplicationConfig> getApplications() {
      return this.applications;
   }

   public List<OneShotTargetApplicationConfig> getOneShotApplications() {
      return this.oneShotApplications;
   }

   public int getMaxFailedLogins() {
      return this.maxFailedLogins;
   }

   public RememberMeConfig getRememberMeConfig() {
      return this.rememberMeConfig;
   }

   public ExistingUserSessionBehaviorConfig getBehaviorUponExistingSession() {
      return this.behaviorUponExistingSession;
   }

   public boolean isDisableSessionBehaviorWhenRepresented() {
      return this.disableSessionBehaviorWhenRepresented;
   }

   public List<LocationInterpretersConfig> getLocationInterpreters() {
      return this.locationInterpreters;
   }

   public FlowConditionConfig getAuthenticationInformationAccessibleCondition() {
      return this.authenticationInformationAccessibleCondition;
   }

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

   @SuppressFBWarnings(justification = "generated code")
   public TargetApplicationsConfig(
      TargetApplicationConfig defaultApplication,
      List<TargetApplicationConfig> applications,
      List<OneShotTargetApplicationConfig> oneShotApplications,
      int maxFailedLogins,
      TemporaryLockingConfig temporaryLockingConfig,
      RememberMeConfig rememberMeConfig,
      LoginHistoryRepositoryConfig loginHistoryRepository,
      ExistingUserSessionBehaviorConfig behaviorUponExistingSession,
      boolean disableSessionBehaviorWhenRepresented,
      List<LocationInterpretersConfig> locationInterpreters,
      FlowConditionConfig authenticationInformationAccessibleCondition
   ) {
      this.defaultApplication = defaultApplication;
      this.applications = applications;
      this.oneShotApplications = oneShotApplications;
      this.maxFailedLogins = maxFailedLogins;
      this.temporaryLockingConfig = temporaryLockingConfig;
      this.rememberMeConfig = rememberMeConfig;
      this.loginHistoryRepository = loginHistoryRepository;
      this.behaviorUponExistingSession = behaviorUponExistingSession;
      this.disableSessionBehaviorWhenRepresented = disableSessionBehaviorWhenRepresented;
      this.locationInterpreters = locationInterpreters;
      this.authenticationInformationAccessibleCondition = authenticationInformationAccessibleCondition;
   }

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

   @SuppressFBWarnings(justification = "generated code")
   public static class TargetApplicationsConfigBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private TargetApplicationConfig defaultApplication;
      @SuppressFBWarnings(justification = "generated code")
      private ArrayList<TargetApplicationConfig> applications;
      @SuppressFBWarnings(justification = "generated code")
      private ArrayList<OneShotTargetApplicationConfig> oneShotApplications;
      @SuppressFBWarnings(justification = "generated code")
      private int maxFailedLogins;
      @SuppressFBWarnings(justification = "generated code")
      private TemporaryLockingConfig temporaryLockingConfig;
      @SuppressFBWarnings(justification = "generated code")
      private RememberMeConfig rememberMeConfig;
      @SuppressFBWarnings(justification = "generated code")
      private LoginHistoryRepositoryConfig loginHistoryRepository;
      @SuppressFBWarnings(justification = "generated code")
      private ExistingUserSessionBehaviorConfig behaviorUponExistingSession;
      @SuppressFBWarnings(justification = "generated code")
      private boolean disableSessionBehaviorWhenRepresented;
      @SuppressFBWarnings(justification = "generated code")
      private List<LocationInterpretersConfig> locationInterpreters;
      @SuppressFBWarnings(justification = "generated code")
      private FlowConditionConfig authenticationInformationAccessibleCondition;

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

      @SuppressFBWarnings(justification = "generated code")
      public TargetApplicationsConfig.TargetApplicationsConfigBuilder defaultApplication(TargetApplicationConfig defaultApplication) {
         this.defaultApplication = defaultApplication;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public TargetApplicationsConfig.TargetApplicationsConfigBuilder application(TargetApplicationConfig application) {
         if (this.applications == null) {
            this.applications = new ArrayList<>();
         }

         this.applications.add(application);
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public TargetApplicationsConfig.TargetApplicationsConfigBuilder applications(Collection<? extends TargetApplicationConfig> applications) {
         if (applications == null) {
            throw new NullPointerException("applications cannot be null");
         }

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

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

      @SuppressFBWarnings(justification = "generated code")
      public TargetApplicationsConfig.TargetApplicationsConfigBuilder clearApplications() {
         if (this.applications != null) {
            this.applications.clear();
         }

         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public TargetApplicationsConfig.TargetApplicationsConfigBuilder oneShotApplication(OneShotTargetApplicationConfig oneShotApplication) {
         if (this.oneShotApplications == null) {
            this.oneShotApplications = new ArrayList<>();
         }

         this.oneShotApplications.add(oneShotApplication);
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public TargetApplicationsConfig.TargetApplicationsConfigBuilder oneShotApplications(
         Collection<? extends OneShotTargetApplicationConfig> oneShotApplications
      ) {
         if (oneShotApplications == null) {
            throw new NullPointerException("oneShotApplications cannot be null");
         }

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

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

      @SuppressFBWarnings(justification = "generated code")
      public TargetApplicationsConfig.TargetApplicationsConfigBuilder clearOneShotApplications() {
         if (this.oneShotApplications != null) {
            this.oneShotApplications.clear();
         }

         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public TargetApplicationsConfig.TargetApplicationsConfigBuilder maxFailedLogins(int maxFailedLogins) {
         this.maxFailedLogins = maxFailedLogins;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public TargetApplicationsConfig.TargetApplicationsConfigBuilder temporaryLockingConfig(TemporaryLockingConfig temporaryLockingConfig) {
         this.temporaryLockingConfig = temporaryLockingConfig;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public TargetApplicationsConfig.TargetApplicationsConfigBuilder rememberMeConfig(RememberMeConfig rememberMeConfig) {
         this.rememberMeConfig = rememberMeConfig;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public TargetApplicationsConfig.TargetApplicationsConfigBuilder loginHistoryRepository(LoginHistoryRepositoryConfig loginHistoryRepository) {
         this.loginHistoryRepository = loginHistoryRepository;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public TargetApplicationsConfig.TargetApplicationsConfigBuilder behaviorUponExistingSession(ExistingUserSessionBehaviorConfig behaviorUponExistingSession) {
         this.behaviorUponExistingSession = behaviorUponExistingSession;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public TargetApplicationsConfig.TargetApplicationsConfigBuilder disableSessionBehaviorWhenRepresented(boolean disableSessionBehaviorWhenRepresented) {
         this.disableSessionBehaviorWhenRepresented = disableSessionBehaviorWhenRepresented;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public TargetApplicationsConfig.TargetApplicationsConfigBuilder locationInterpreters(List<LocationInterpretersConfig> locationInterpreters) {
         this.locationInterpreters = locationInterpreters;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public TargetApplicationsConfig.TargetApplicationsConfigBuilder authenticationInformationAccessibleCondition(
         FlowConditionConfig authenticationInformationAccessibleCondition
      ) {
         this.authenticationInformationAccessibleCondition = authenticationInformationAccessibleCondition;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public TargetApplicationsConfig build() {
         List applications = switch (this.applications == null ? 0 : this.applications.size()) {
            case 0 -> Collections.emptyList();
            case 1 -> Collections.singletonList(this.applications.get(0));
            default -> Collections.unmodifiableList(new ArrayList<>(this.applications));
         };

         return new TargetApplicationsConfig(
            this.defaultApplication,
            applications,
            switch (this.oneShotApplications == null ? 0 : this.oneShotApplications.size()) {
               case 0 -> Collections.emptyList();
               case 1 -> Collections.singletonList(this.oneShotApplications.get(0));
               default -> Collections.unmodifiableList(new ArrayList<>(this.oneShotApplications));
            },
            this.maxFailedLogins,
            this.temporaryLockingConfig,
            this.rememberMeConfig,
            this.loginHistoryRepository,
            this.behaviorUponExistingSession,
            this.disableSessionBehaviorWhenRepresented,
            this.locationInterpreters,
            this.authenticationInformationAccessibleCondition
         );
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "TargetApplicationsConfig.TargetApplicationsConfigBuilder(defaultApplication="
            + this.defaultApplication
            + ", applications="
            + this.applications
            + ", oneShotApplications="
            + this.oneShotApplications
            + ", maxFailedLogins="
            + this.maxFailedLogins
            + ", temporaryLockingConfig="
            + this.temporaryLockingConfig
            + ", rememberMeConfig="
            + this.rememberMeConfig
            + ", loginHistoryRepository="
            + this.loginHistoryRepository
            + ", behaviorUponExistingSession="
            + this.behaviorUponExistingSession
            + ", disableSessionBehaviorWhenRepresented="
            + this.disableSessionBehaviorWhenRepresented
            + ", locationInterpreters="
            + this.locationInterpreters
            + ", authenticationInformationAccessibleCondition="
            + this.authenticationInformationAccessibleCondition
            + ")";
      }
   }
}
