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

import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.common.application.configuration.accountlink.persistence.AccountLinkPersisterConfig;
import com.airlock.iam.common.domain.model.provider.ProviderId;
import com.airlock.iam.oauth2.application.configuration.OAuth2ClientSettingsConfigSpec;
import com.airlock.iam.oauth2.application.configuration.OAuth2SsoFlowClientSettingsBase;
import com.airlock.iam.oauth2.application.configuration.client.OAuth2LegacyClientEndpointSupport;
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.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.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.util.PropertyDisplayNameReader;
import com.airlock.iam.plugin.api.valuereader.BooleanPropertyValueReader;
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.google.common.collect.Sets;
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.Optional;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;

@Plugin(description = "Configuration for OAuth 2.0 / OpenID Connect SSO (Airlock IAM as Client).", displayName = "OAuth 2.0/OIDC Clients")
@PluginLicenseRestriction(moduleAffiliation = "OAuthClient")
@Groups(groupSequence = {"Flow Client", "Account Linking"}, openGroups = {"Flow Client", "Account Linking"})
public class OAuth2SSOClientSettings implements OAuth2ClientSettingsConfigSpec {
   static final String FLOW_CLIENT_SETTINGS = "Flow Client";
   static final String ACCOUNT_LINKING = "Account Linking";
   @PluginListProperty(
      description = "The settings for the OAuth 2.0 / OpenID Connect providers for flow based authorizations.",
      displayName = "AS Setting For Flow Clients",
      minElements = 1,
      optional = false
   )
   @GroupProperty(groupIdentifier = "Flow Client")
   private List<OAuth2SsoFlowClientSettingsBase> flowClientSettings;
   public static final String FLOW_BASED_CLIENT_SETTINGS_FIELD = "flowClientSettings";
   public static final String FLOW_BASED_CLIENT_SETTINGS_DISPLAY_NAME = "AS Setting For Flow Clients";
   @PluginProperty(
      description = "<p>OAuth 2.0 legacy client endpoint (<i>/oauth2-client</i>) compatibility support.</p> <p>This property must be configured when flow-based OAuth 2.0/OpenID Connect is used, and the client endpoint (redirect_uri) URI cannot be changed on the Authorization Server to the new endpoint URI:</p> <p><code>/&lt;loginapp-uri&gt;/ui/app/oauth2/client</code></p> <p>When this property is set, the browser accessing the legacy client endpoint URI will be redirected to the configured target, if and only if all of the following conditions apply: <ul><li>The request contains an authorization response</li><li>\"AS Setting For Flow Clients\" has at least one entry configured</li></ul></p><p>All OAuth 2.0 URL parameters will be retained in the redirect.</p>",
      displayName = "Legacy Client Endpoint Support",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Flow Client")
   private OAuth2LegacyClientEndpointSupport legacyClientEndpointSupport;
   public static final String CLIENT_ENDPOINT_REDIRECT_FIELD = "legacyClientEndpointSupport";
   public static final String CLIENT_ENDPOINT_REDIRECT_DISPLAY_NAME = "Legacy Client Endpoint Support";
   @PluginProperty(
      description = "Persists links from IAM accounts to provider accounts.<p>The database entry contains<ul><li>The IAM user name</li><li>The configured 'Provider Identifier' of the 'OAuth 2.0 Flow Client' or 'OIDC Flow Client'</li><li>The provider's username defined by the 'OAuth 2.0 Remote Username Resource' resource mapping</li><li>Optionally the additional information to help the user identify the provider's account defined by 'Account Info Resource Key' of the 'Account Linking Self-Service'</li></ul></p><p>If a user chooses to login in with a provider that has 'Account Linking Self-Service' enabled and a link matching the 'Provider Identifier' and the provider's username is found, the IAM user of the entry will be authenticated.</p><p>If users are allowed to change their usernames, a 'Account Link Consistency User Change Listener' in the Loginapp's user persister should be configured.</p>",
      displayName = "Account Link Persister",
      optional = true
   )
   @PluginPropertyLicenseRestriction(moduleAffiliation = {"OAuthAccountLinking", "OAuthSocialRegistration"})
   @GroupProperty(groupIdentifier = "Account Linking")
   private AccountLinkPersisterConfig accountLinkPersister;
   public static final String ACCOUNT_LINK_PERSISTER_PROPERTY_NAME = "accountLinkPersister";

   @ConfigurationValidator
   public static void validateFlowProviderIdUniqueness(
      PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context, Environment validationEnvironment
   ) throws ConfigurationException {
      Collection<String> flowProviderIds = new ArrayList<>();

      for (PluginConfiguration flowClientSetting : PluginListPropertyValueReader.effectivePluginListPropertyValue(config, "flowClientSettings", env, context)) {
         PluginConfiguration providerIdConfig = PluginPropertyValueReader.effectivePluginPropertyValue(flowClientSetting, "providerId", env, context);
         if (providerIdConfig != null) {
            String providerId = StringPropertyValueReader.effectiveStringConfigValue(providerIdConfig, "identifier", env, context);
            if (StringUtils.isNotBlank(providerId)) {
               flowProviderIds.add(providerId);
            }
         }
      }

      validateUniqueness(config, env, context, flowProviderIds);
   }

   private static void validateUniqueness(
      PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context, Collection<String> providerIds
   ) throws PropertyConstraintViolationException {
      Collection<String> duplicates = CollectionUtils.disjunction(providerIds, Sets.newHashSet(providerIds));
      if (!duplicates.isEmpty()) {
         throw new PropertyConstraintViolationException(
            "flowClientSettings", config, env, context, duplicatedProviderIdErrorMessage(duplicates, "AS Setting For Flow Clients")
         );
      }
   }

   @ConfigurationValidator
   public static void validateAccountLinkPersister(
      PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context, Environment validationEnvironment
   ) throws ConfigurationException {
      if (!isAccountLinkPersisterConfigured(config, env, context)) {
         Optional<OAuth2SSOClientSettings.AccountLinkPersisterDependency> dependency = findFirstClientsAccountLinkPersisterDependency(config, env, context);
         if (!dependency.isEmpty()) {
            throw new PropertyConstraintViolationException("accountLinkPersister", config, env, context, dependency.get().getUnmetDependencyMessage());
         }
      }
   }

   private static Optional<OAuth2SSOClientSettings.AccountLinkPersisterDependency> findFirstClientsAccountLinkPersisterDependency(
      PluginConfiguration ssoClientConfig, ConfigurationEnvironment env, ConfigurationContext context
   ) throws ConfigurationException {
      for (PluginConfiguration client : PluginListPropertyValueReader.effectivePluginListPropertyValue(ssoClientConfig, "flowClientSettings", env, context)) {
         if (BooleanPropertyValueReader.effectiveBooleanPropertyValue(client, "enableAccountLinking", env, context)) {
            return Optional.of(new OAuth2SSOClientSettings.AccountLinkPersisterDependency(ssoClientConfig, client, "Enable Account Linking"));
         }
      }

      return Optional.empty();
   }

   @Override
   public Optional<OAuth2SsoFlowClientSettingsBase> findFlowClientSettings(ProviderId providerId) {
      return this.flowClientSettings.stream().filter(settings -> settings.getProviderId().equals(providerId)).findFirst();
   }

   @Override
   public List<OAuth2SsoFlowClientSettingsBase> flowClientSettings() {
      return this.flowClientSettings;
   }

   public static boolean isAccountLinkPersisterConfigured(PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context) throws ConfigurationException {
      return PluginPropertyValueReader.effectivePluginPropertyValue(config, "accountLinkPersister", env, context) != null;
   }

   private static String duplicatedProviderIdErrorMessage(Collection<String> duplicates, String clientSettingsDisplayName) {
      return "The 'Provider' properties of the '"
         + clientSettingsDisplayName
         + "' must have unique values, but found duplicates: "
         + String.join(",", duplicates);
   }

   public List<OAuth2SsoFlowClientSettingsBase> getFlowClientSettings() {
      return this.flowClientSettings;
   }

   public OAuth2LegacyClientEndpointSupport getLegacyClientEndpointSupport() {
      return this.legacyClientEndpointSupport;
   }

   @Override
   public AccountLinkPersisterConfig getAccountLinkPersister() {
      return this.accountLinkPersister;
   }

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

   @SuppressFBWarnings(justification = "generated code")
   public OAuth2SSOClientSettings(
      List<OAuth2SsoFlowClientSettingsBase> flowClientSettings,
      OAuth2LegacyClientEndpointSupport legacyClientEndpointSupport,
      AccountLinkPersisterConfig accountLinkPersister
   ) {
      this.flowClientSettings = flowClientSettings;
      this.legacyClientEndpointSupport = legacyClientEndpointSupport;
      this.accountLinkPersister = accountLinkPersister;
   }

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

   private static final class AccountLinkPersisterDependency {
      private final PluginConfiguration ssoClientSettings;
      private final PluginConfiguration clientConfig;
      private final String dependantPropertyDisplayName;

      String getUnmetDependencyMessage() {
         String persisterDisplayName = PropertyDisplayNameReader.propertyDisplayName(this.ssoClientSettings, "accountLinkPersister");
         String message = LogUtil.quote(persisterDisplayName)
            + " must be configured when "
            + LogUtil.quote(this.dependantPropertyDisplayName)
            + " is configured on any configured client settings";
         if (StringUtils.isNotBlank(this.clientConfig.getPluginDisplayName())) {
            message = message
               + ". Plugin with display name "
               + LogUtil.quote(this.clientConfig.getPluginDisplayName())
               + " has "
               + LogUtil.quote(this.dependantPropertyDisplayName)
               + " configured.";
         }

         return message;
      }

      @SuppressFBWarnings(justification = "generated code")
      public AccountLinkPersisterDependency(PluginConfiguration ssoClientSettings, PluginConfiguration clientConfig, String dependantPropertyDisplayName) {
         this.ssoClientSettings = ssoClientSettings;
         this.clientConfig = clientConfig;
         this.dependantPropertyDisplayName = dependantPropertyDisplayName;
      }
   }

   @SuppressFBWarnings(justification = "generated code")
   public static class OAuth2SSOClientSettingsBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private ArrayList<OAuth2SsoFlowClientSettingsBase> flowClientSettings;
      @SuppressFBWarnings(justification = "generated code")
      private OAuth2LegacyClientEndpointSupport legacyClientEndpointSupport;
      @SuppressFBWarnings(justification = "generated code")
      private AccountLinkPersisterConfig accountLinkPersister;

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

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2SSOClientSettings.OAuth2SSOClientSettingsBuilder flowClientSetting(OAuth2SsoFlowClientSettingsBase flowClientSetting) {
         if (this.flowClientSettings == null) {
            this.flowClientSettings = new ArrayList<>();
         }

         this.flowClientSettings.add(flowClientSetting);
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2SSOClientSettings.OAuth2SSOClientSettingsBuilder flowClientSettings(Collection<? extends OAuth2SsoFlowClientSettingsBase> flowClientSettings) {
         if (flowClientSettings == null) {
            throw new NullPointerException("flowClientSettings cannot be null");
         }

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

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

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2SSOClientSettings.OAuth2SSOClientSettingsBuilder clearFlowClientSettings() {
         if (this.flowClientSettings != null) {
            this.flowClientSettings.clear();
         }

         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2SSOClientSettings.OAuth2SSOClientSettingsBuilder legacyClientEndpointSupport(OAuth2LegacyClientEndpointSupport legacyClientEndpointSupport) {
         this.legacyClientEndpointSupport = legacyClientEndpointSupport;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2SSOClientSettings.OAuth2SSOClientSettingsBuilder accountLinkPersister(AccountLinkPersisterConfig accountLinkPersister) {
         this.accountLinkPersister = accountLinkPersister;
         return this;
      }

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

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "OAuth2SSOClientSettings.OAuth2SSOClientSettingsBuilder(flowClientSettings="
            + this.flowClientSettings
            + ", legacyClientEndpointSupport="
            + this.legacyClientEndpointSupport
            + ", accountLinkPersister="
            + this.accountLinkPersister
            + ")";
      }
   }
}
