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

import com.airlock.iam.core.domain.model.http.client.HttpClient;
import com.airlock.iam.core.misc.util.httpclient.HttpClientConfig;
import com.airlock.iam.oauth2.api.domain.model.clientauthentication.ClientAuthenticationMethod;
import com.airlock.iam.oauth2.domain.model.client.communication.OAuth2TokenRequestClientSecretProcessor;
import com.airlock.iam.oauth2.infrastructure.discovery.OpenIDConnectProviderMetadataRequestClient;
import com.airlock.iam.oauth2.infrastructure.discovery.OpenIDConnectProviderMetadataRequestException;
import com.airlock.iam.oauth2.infrastructure.discovery.compatibility.OpenIDConnectCompatibility;
import com.airlock.iam.oauth2.infrastructure.discovery.compatibility.OpenIDConnectCompatibilityResult;
import com.airlock.iam.oauth2.infrastructure.discovery.compatibility.OpenIDConnectConfigurationTokenEndpointAuthMethods;
import com.airlock.iam.plugin.api.config.ConfigurationContext;
import com.airlock.iam.plugin.api.config.ConfigurationEnvironment;
import com.airlock.iam.plugin.api.config.PluginConfiguration;
import com.airlock.iam.plugin.api.exception.InitializeException;
import com.airlock.iam.plugin.api.exception.PropertyConfigurationException;
import com.airlock.iam.plugin.api.exception.PropertyTypeMismatchException;
import com.airlock.iam.plugin.api.testlet.Testlet;
import com.airlock.iam.plugin.api.testlet.Testlet.TestletResult;
import com.airlock.iam.plugin.api.testlet.Testlet.TestletType;
import com.airlock.iam.plugin.api.testlet.Testlet.TestletResult.TestResult;
import com.airlock.iam.plugin.api.valuereader.PluginPropertyValueReader;
import com.airlock.iam.plugin.api.valuereader.StringPropertyValueReader;
import com.airlock.iam.plugin.framework.misc.ioc.PluginLoader;
import com.google.common.annotations.VisibleForTesting;
import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.io.IOException;
import java.net.URI;
import java.security.GeneralSecurityException;
import java.util.Optional;
import org.apache.commons.lang3.StringUtils;

public class OpenIDConnectDiscoveryTestlet implements Testlet {
   public static final String INCOMPATIBLE_PROVIDER_TITLE = "Incompatible provider";
   public static final String VALID_PROVIDER_TITLE = "Valid discovery URL";
   public static final String FAILED_TO_FETCH_DATA_TITLE = "Failed to fetch data";

   public TestletResult performTest(PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context) throws PropertyConfigurationException {
      PluginConfiguration httpClientConfig = PluginPropertyValueReader.effectivePluginPropertyValue(config, "httpClient", env, context);
      PluginConfiguration clientSecretConfig = PluginPropertyValueReader.effectivePluginPropertyValue(config, "tokenEndpointAuthentication", env, context);

      try {
         HttpClient testletHttpClient = this.initHttpClient(env, context, httpClientConfig);
         String discoveryURL = StringPropertyValueReader.effectiveStringConfigValue(config, "discoveryEndpointURL", env, context);
         OpenIDConnectProviderMetadataRequestClient client = new OpenIDConnectProviderMetadataRequestClient(testletHttpClient, URI.create(discoveryURL));
         OIDCProviderMetadata metadata = client.executeRequest();
         Optional<ClientAuthenticationMethod> configuredAuthMethod = this.getConfiguredClientAuthMethod(clientSecretConfig, env, context);
         OpenIDConnectCompatibility openIDConnectCompatibility = OpenIDConnectCompatibility.builder()
            .metadata(metadata)
            .configuration(new OpenIDConnectConfigurationTokenEndpointAuthMethods(configuredAuthMethod))
            .build();
         OpenIDConnectCompatibilityResult compatibilityResult = openIDConnectCompatibility.validate();
         Optional<String> incompatibilityMessage = compatibilityResult.getIncompatibilityMessage();
         return incompatibilityMessage.isPresent()
            ? new TestletResult(TestResult.FAILED, "Incompatible provider", incompatibilityMessage.get())
            : new TestletResult(
               TestResult.SUCCESSFUL,
               "Valid discovery URL",
               new OpenIDConnectDiscoveryTestlet.OIDCProviderMetadataSummary(metadata, compatibilityResult).summary()
            );
      } catch (InitializeException | IOException | GeneralSecurityException | OpenIDConnectProviderMetadataRequestException e) {
         return new TestletResult("Failed to fetch data", "Exception while fetching discovery data: " + e.getMessage(), e);
      }
   }

   private Optional<ClientAuthenticationMethod> getConfiguredClientAuthMethod(
      PluginConfiguration clientSecretConfig, ConfigurationEnvironment env, ConfigurationContext context
   ) throws InitializeException {
      if (clientSecretConfig == null) {
         return Optional.empty();
      }

      OAuth2TokenRequestClientSecretProcessor clientSecretProcessor = (OAuth2TokenRequestClientSecretProcessor)PluginLoader.get()
         .loadPlugin(clientSecretConfig, env, context);
      return Optional.of(clientSecretProcessor.clientAuthenticationMethod());
   }

   @VisibleForTesting
   HttpClient initHttpClient(ConfigurationEnvironment env, ConfigurationContext context, PluginConfiguration httpClientConfig) throws InitializeException, IOException, GeneralSecurityException {
      HttpClientConfig httpClientPlugin = (HttpClientConfig)PluginLoader.get().loadPlugin(httpClientConfig, env, context);
      return httpClientPlugin.createHttpClientForDomainClasses();
   }

   public boolean areRequiredPropertiesConfigured(PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context) {
      try {
         PluginConfiguration httpClientConfig = PluginPropertyValueReader.effectivePluginPropertyValue(config, "httpClient", env, context);
         String discoveryURL = StringPropertyValueReader.effectiveStringConfigValue(config, "discoveryEndpointURL", env, context);
         return httpClientConfig != null && StringUtils.isNotEmpty(discoveryURL);
      } catch (PropertyTypeMismatchException e) {
         return false;
      }
   }

   public String getRequiredPropertiesMissingMessage() {
      return "Discovery URL is not configured properly.";
   }

   public TestletType getType() {
      return TestletType.RESULT;
   }

   private static final class OIDCProviderMetadataSummary {
      private final OIDCProviderMetadata metadata;
      private final OpenIDConnectCompatibilityResult compatibilityResult;

      public String summary() {
         return "The OpenID Provider is compatible with IAM.<br/><b>Summary</b>:<br/>Issuer: "
            + this.metadata.getIssuer()
            + "<br/>Authorization Endpoint: "
            + this.metadata.getAuthorizationEndpointURI()
            + "<br/>Token Endpoint: "
            + this.metadata.getTokenEndpointURI()
            + this.formatOptional("End Session Endpoint", this.metadata.getEndSessionEndpointURI())
            + "<br/>JWKs URL:"
            + this.metadata.getJWKSetURI()
            + "<br/>ID Token signing algorithms: "
            + this.metadata.getIDTokenJWSAlgs()
            + this.warnings();
      }

      private String formatOptional(String title, Object discoveryObject) {
         return discoveryObject == null ? "" : "<br/>" + title + ": " + discoveryObject;
      }

      private String warnings() {
         return this.compatibilityResult
            .getWarningMessage()
            .map(warning -> warning.replace("\n", "<br/>"))
            .map(warning -> "<br/><b style=\"color:orange;\">" + warning + "</b>")
            .orElse("");
      }

      @SuppressFBWarnings(justification = "generated code")
      public OIDCProviderMetadataSummary(OIDCProviderMetadata metadata, OpenIDConnectCompatibilityResult compatibilityResult) {
         this.metadata = metadata;
         this.compatibilityResult = compatibilityResult;
      }
   }
}
