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

import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
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.license.PluginLicenseRestriction;
import com.airlock.iam.plugin.api.annotation.license.PluginPropertyLicenseRestriction;
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.PluginListProperty;
import com.airlock.iam.plugin.api.annotation.property.PluginProperty;
import com.airlock.iam.plugin.api.annotation.property.RegexProperty;
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.PropertyConfigurationException;
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 com.airlock.iam.saml2.application.configuration.sp.Saml2FlowSpConfig;
import com.airlock.iam.saml2.infrastructure.util.Saml2LoginappConfigProvider;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.annotation.Nullable;
import java.net.URI;
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.regex.Pattern;

@Plugin(description = "Configures the SAML 2.0 Identity Provider (IdP) and Service Providers (SP) of the login application.", displayName = "SAML 2.0 Config")
@Groups(
   groupSequence = {"Identity Provider Settings (Flow Auth)", "Service Provider Settings (Flow Auth)", "Common"},
   openGroups = {"Identity Provider Settings (Flow Auth)", "Service Provider Settings (Flow Auth)", "Common"}
)
@PluginLicenseRestriction(moduleAffiliation = {"SamlIdp", "SamlSp"})
@SuppressFBWarnings("ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD")
public class SamlConfig {
   static final String FLOW_IDP_SETTINGS = "Identity Provider Settings (Flow Auth)";
   static final String FLOW_SP_SETTINGS = "Service Provider Settings (Flow Auth)";
   static final String COMMON_SETTINGS = "Common";
   private static final ELogger LOG = ELoggerFactory.getLogger(SamlConfig.class);
   @PluginProperty(
      displayName = "SAML 2.0 Identity Provider",
      description = "Configures the SAML Identity Provider (IdP) and the associated service providers (SPs) that rely on this IdP to authenticate a user.",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Identity Provider Settings (Flow Auth)")
   @PluginPropertyLicenseRestriction(moduleAffiliation = "SamlIdp")
   private Saml2FlowIdpConfig samlFlowIdpSettings;
   public static final String SAML_FLOW_IDP_SETTINGS_FIELD = "samlFlowIdpSettings";
   @PluginListProperty(
      displayName = "SAML 2.0 Service Providers",
      description = "Enables and configures the local SAML Service Providers (SPs).",
      optional = true,
      minElements = 1
   )
   @GroupProperty(groupIdentifier = "Service Provider Settings (Flow Auth)")
   @PluginPropertyLicenseRestriction(moduleAffiliation = "SamlSp")
   private List<Saml2FlowSpConfig> samlFlowSpSettings;
   public static final String SAML_FLOW_SP_SETTINGS_FIELD = "samlFlowSpSettings";
   @BooleanProperty(
      description = "When enabled, IAM (as Service Provider) will automatically initiate a\nSingle-Logout with the Identity Provider when the logout endpoint is called.\nIf this flag is disabled, only a local logout will take place.\n",
      optional = true,
      defaultValue = "true",
      displayName = "Use Single-Logout (SLO) Behaviour"
   )
   @PluginPropertyLicenseRestriction(moduleAffiliation = "SamlSp")
   @GroupProperty(groupIdentifier = "Service Provider Settings (Flow Auth)")
   private boolean enableSingleLogoutOnSps;
   @StringProperty(
      description = "<p>The URI to start or continue the correct authentication flow after receiving an assertion from the\nIdentity Provider (either using IdP- or SP-initiated SSO).</p>\n<p><b>Must only be configured when using a custom SPA.</b></p>\n<p>Relative URIs not starting with a slash are resolved against the current context path.</p>\n<p>When customized and behind an Airlock Gateway (WAF), a \"URL Encryption Exception\" must be configured.</p>\n",
      optional = true,
      displayName = "Flow SSO Initialization URI",
      exampleValues = {"https://example.com/custom-ui/saml2/sp/sso/init", "/custom-ui/sp/sso/init"}
   )
   @PluginPropertyLicenseRestriction(moduleAffiliation = "SamlSp")
   @GroupProperty(groupIdentifier = "Service Provider Settings (Flow Auth)")
   private String customSpSsoInitializationUri;
   @StringProperty(
      description = "<p>The URI to start the logout process in the UI.</p><p>This is used during IdP-initiated Single-Logout (the logout is started by an Identity Provider). In this case, the browser must first be redirected to the UI in order to start the regular logout before being able to finish the SAML 2.0 Single-Logout.</p><p><b>Must only be configured when using a custom SPA.</b></p><p>Relative URIs not starting with a slash are resolved against the current context path.</p><p>When customized and behind an Airlock Gateway (WAF), a \"URL Encryption Exception\" must be configured.</p>",
      optional = true,
      displayName = "Logout URI",
      exampleValues = {"https://example.com/custom-ui/logout", "/custom-ui/logout"}
   )
   @PluginPropertyLicenseRestriction(moduleAffiliation = "SamlSp")
   @GroupProperty(groupIdentifier = "Service Provider Settings (Flow Auth)")
   private String customLogoutUri;
   @RegexProperty(
      description = "<p>During SP-Initiated Single-Logout (SLO), the SPA has to send the location where to resume the logout process after SAML 2.0 Single-Logout has been finished using a \"Location\" URL parameter. <p>If not configured, the standard URL for logout resume in the Loginapp UI (ui/app/auth/logout/resume) will be used.</p>That absolute location will be validated against this pattern.</p><p><b>Must only be configured when using a custom SPA.</b></p><p>If behind an Airlock Gateway (WAF), a \"URL Encryption Exception\" must also be configured for this URL.</p>",
      optional = true,
      displayName = "Logout Resume URI Pattern",
      exampleValues = "https://example\\.com/custom-ui/logout/resume"
   )
   @PluginPropertyLicenseRestriction(moduleAffiliation = "SamlSp")
   @GroupProperty(groupIdentifier = "Service Provider Settings (Flow Auth)")
   private Pattern customLogoutResumeUriPattern;
   @PluginProperty(
      displayName = "SAML Federation Settings",
      description = "Federation settings used for all SAML 2.0 use-cases.",
      optional = true,
      defaultValue = SamlFederationConfig.class
   )
   @GroupProperty(groupIdentifier = "Common")
   @PluginPropertyLicenseRestriction(moduleAffiliation = {"SamlIdp", "SamlSp"})
   private SamlFederationConfig samlFederationConfig;
   private static final String SAML_FEDERATION_CONFIG_FIELD = "samlFederationConfig";

   @Nullable
   public static SamlConfig get(ConfigurationContext context) {
      return Saml2LoginappConfigProvider.getSamlConfig(context).orElse(null);
   }

   public static Optional<Saml2FlowIdpConfig> getFlowIdpConfig(ConfigurationContext context) {
      return Saml2LoginappConfigProvider.getSamlConfig(context).flatMap(SamlConfig::getSamlFlowIdpSettings);
   }

   public static Optional<List<Saml2FlowSpConfig>> getFlowSpConfig(ConfigurationContext context) {
      return Saml2LoginappConfigProvider.getSamlConfig(context).map(SamlConfig::getSamlFlowSpSettings);
   }

   public SamlFederationConfig getSamlFederationConfig() {
      return this.samlFederationConfig;
   }

   public Optional<Saml2FlowIdpConfig> getSamlFlowIdpSettings() {
      return Optional.ofNullable(this.samlFlowIdpSettings);
   }

   public List<Saml2FlowSpConfig> getSamlFlowSpSettings() {
      return this.samlFlowSpSettings;
   }

   public Optional<URI> getCustomSpInitUri() {
      return Optional.ofNullable(this.customSpSsoInitializationUri).map(URI::create);
   }

   public Optional<Pattern> getCustomLogoutResumeUriPattern() {
      return Optional.ofNullable(this.customLogoutResumeUriPattern);
   }

   public Optional<URI> getCustomSpLogoutUri() {
      return Optional.ofNullable(this.customLogoutUri).map(URI::create);
   }

   public boolean isSingleLogoutEnabledOnSps() {
      return this.enableSingleLogoutOnSps;
   }

   @ConfigurationValidator
   static void validateSpEntityIdUniqueness(
      PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context, Environment validationEnvironment
   ) throws ConfigurationException {
      Set<String> uniqueSpEntityIds = new HashSet<>();

      for (PluginConfiguration flowSpSettings : PluginListPropertyValueReader.effectivePluginListPropertyValue(config, "samlFlowSpSettings", env, context)) {
         PluginConfiguration spEntityConfig = PluginPropertyValueReader.effectivePluginPropertyValue(flowSpSettings, "spEntityConfig", env, context);
         PluginConfiguration spEntityIdConfig = PluginPropertyValueReader.effectivePluginPropertyValue(spEntityConfig, "entityId", env, context);
         String spEntityId = StringPropertyValueReader.effectiveStringConfigValue(spEntityIdConfig, "spEntityId", env, context);
         if (uniqueSpEntityIds.contains(spEntityId)) {
            throw new PropertyConfigurationException(
               spEntityIdConfig, env, context, "The configured SP Entity ID \"" + spEntityId + "\" is not unique", new String[]{"spEntityId"}
            );
         }

         uniqueSpEntityIds.add(spEntityId);
      }
   }

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

   @SuppressFBWarnings(justification = "generated code")
   public SamlConfig(
      Saml2FlowIdpConfig samlFlowIdpSettings,
      List<Saml2FlowSpConfig> samlFlowSpSettings,
      boolean enableSingleLogoutOnSps,
      String customSpSsoInitializationUri,
      String customLogoutUri,
      Pattern customLogoutResumeUriPattern,
      SamlFederationConfig samlFederationConfig
   ) {
      this.samlFlowIdpSettings = samlFlowIdpSettings;
      this.samlFlowSpSettings = samlFlowSpSettings;
      this.enableSingleLogoutOnSps = enableSingleLogoutOnSps;
      this.customSpSsoInitializationUri = customSpSsoInitializationUri;
      this.customLogoutUri = customLogoutUri;
      this.customLogoutResumeUriPattern = customLogoutResumeUriPattern;
      this.samlFederationConfig = samlFederationConfig;
   }

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

   @SuppressFBWarnings(justification = "generated code")
   public static class SamlConfigBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private Saml2FlowIdpConfig samlFlowIdpSettings;
      @SuppressFBWarnings(justification = "generated code")
      private ArrayList<Saml2FlowSpConfig> samlFlowSpSettings;
      @SuppressFBWarnings(justification = "generated code")
      private boolean enableSingleLogoutOnSps;
      @SuppressFBWarnings(justification = "generated code")
      private String customSpSsoInitializationUri;
      @SuppressFBWarnings(justification = "generated code")
      private String customLogoutUri;
      @SuppressFBWarnings(justification = "generated code")
      private Pattern customLogoutResumeUriPattern;
      @SuppressFBWarnings(justification = "generated code")
      private SamlFederationConfig samlFederationConfig;

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

      @SuppressFBWarnings(justification = "generated code")
      public SamlConfig.SamlConfigBuilder samlFlowIdpSettings(Saml2FlowIdpConfig samlFlowIdpSettings) {
         this.samlFlowIdpSettings = samlFlowIdpSettings;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SamlConfig.SamlConfigBuilder samlFlowSpSetting(Saml2FlowSpConfig samlFlowSpSetting) {
         if (this.samlFlowSpSettings == null) {
            this.samlFlowSpSettings = new ArrayList<>();
         }

         this.samlFlowSpSettings.add(samlFlowSpSetting);
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SamlConfig.SamlConfigBuilder samlFlowSpSettings(Collection<? extends Saml2FlowSpConfig> samlFlowSpSettings) {
         if (samlFlowSpSettings == null) {
            throw new NullPointerException("samlFlowSpSettings cannot be null");
         }

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

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

      @SuppressFBWarnings(justification = "generated code")
      public SamlConfig.SamlConfigBuilder clearSamlFlowSpSettings() {
         if (this.samlFlowSpSettings != null) {
            this.samlFlowSpSettings.clear();
         }

         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SamlConfig.SamlConfigBuilder enableSingleLogoutOnSps(boolean enableSingleLogoutOnSps) {
         this.enableSingleLogoutOnSps = enableSingleLogoutOnSps;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SamlConfig.SamlConfigBuilder customSpSsoInitializationUri(String customSpSsoInitializationUri) {
         this.customSpSsoInitializationUri = customSpSsoInitializationUri;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SamlConfig.SamlConfigBuilder customLogoutUri(String customLogoutUri) {
         this.customLogoutUri = customLogoutUri;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SamlConfig.SamlConfigBuilder customLogoutResumeUriPattern(Pattern customLogoutResumeUriPattern) {
         this.customLogoutResumeUriPattern = customLogoutResumeUriPattern;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SamlConfig.SamlConfigBuilder samlFederationConfig(SamlFederationConfig samlFederationConfig) {
         this.samlFederationConfig = samlFederationConfig;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SamlConfig build() {
         return new SamlConfig(this.samlFlowIdpSettings, switch (this.samlFlowSpSettings == null ? 0 : this.samlFlowSpSettings.size()) {
            case 0 -> Collections.emptyList();
            case 1 -> Collections.singletonList(this.samlFlowSpSettings.get(0));
            default -> Collections.unmodifiableList(new ArrayList<>(this.samlFlowSpSettings));
         }, this.enableSingleLogoutOnSps, this.customSpSsoInitializationUri, this.customLogoutUri, this.customLogoutResumeUriPattern, this.samlFederationConfig);
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "SamlConfig.SamlConfigBuilder(samlFlowIdpSettings="
            + this.samlFlowIdpSettings
            + ", samlFlowSpSettings="
            + this.samlFlowSpSettings
            + ", enableSingleLogoutOnSps="
            + this.enableSingleLogoutOnSps
            + ", customSpSsoInitializationUri="
            + this.customSpSsoInitializationUri
            + ", customLogoutUri="
            + this.customLogoutUri
            + ", customLogoutResumeUriPattern="
            + this.customLogoutResumeUriPattern
            + ", samlFederationConfig="
            + this.samlFederationConfig
            + ")";
      }
   }
}
