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

import com.airlock.iam.core.domain.model.http.HttpRequestHeader;
import com.airlock.iam.core.domain.model.http.HttpRequestParameter;
import com.airlock.iam.oauth2.api.domain.model.clientauthentication.ClientAuthenticationMethod;
import com.airlock.iam.oauth2.domain.model.client.OAuth2ClientSideCredentials;
import com.airlock.iam.oauth2.domain.model.client.communication.OAuth2TokenRequestClientSecretProcessor;
import com.airlock.iam.plugin.api.annotation.config.ConfigurationValidator;
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.property.BooleanProperty;
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 edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.List;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;

@Plugin(description = "Specifies the format of the client secret passed in the header of OAuth 2.0 requests.", displayName = "OAuth 2.0 Header Client Secret")
@PluginLicenseRestriction(moduleAffiliation = "OAuthClient")
public class OAuth2HeaderClientSecretConfig implements OAuth2TokenRequestClientSecretProcessor {
   private static final String CLIENT_ID_PLACEHOLDER = "$CLIENT_ID$";
   private static final String CLIENT_SECRET_PLACEHOLDER = "$CLIENT_SECRET$";
   private static final String CLIENT_SECRET_CUSTOM_HEADER = "client_secret_custom_header";
   @StringProperty(description = "Name of the header field containing the client secret.", optional = true, defaultValue = "Authorization")
   private String headerField;
   @StringProperty(
      description = "Header field value prefix. The prefix must match exactly. May be empty.<br>A header prefix denotes the first part of a space delimited header value.<br>E.g. \"Authorization: Basic 2q93nf8q23UIZFR2qfh98\" where \"Authorization\" denotes the header field name, \"Basic\" the header prefix and \"2q93nf8q23UIZFR2qfh98\" the client secret value.",
      optional = true,
      suggestedValues = "Basic"
   )
   private String headerPrefix;
   @StringProperty(
      description = "Client secret extraction pattern. If left blank, the client secret is the whole string. Otherwise this field must contain \"$CLIENT_SECRET$\" exactly once and may additionally contain \"$CLIENT_ID$\". When building requests the placeholders get replaced by their respective values, as specified in the OAuth 2.0 identity propagator plugin.",
      suggestedValues = "$CLIENT_ID$:$CLIENT_SECRET$",
      optional = true
   )
   private String clientSecretFormat;
   private static final String CLIENT_SECRET_FORMAT_FIELD = "clientSecretFormat";
   private static final OAuth2HeaderClientSecretConfigValidator CLIENT_SECRET_CONFIG_VALIDATOR = new OAuth2HeaderClientSecretConfigValidator(
      "clientSecretFormat", "$CLIENT_SECRET$"
   );
   @BooleanProperty(
      description = "Flag indicating whether the entire value (behind the optional prefix) is encoded in Base64; must be enabled for Basic-Auth.",
      optional = true,
      defaultValue = "false",
      displayName = "Base64-Encoded Client Secret Value"
   )
   private boolean base64EncodeClientSecretValue;

   @Override
   public void putClientSecretInRequestData(
      URI baseUri, List<HttpRequestParameter> params, List<HttpRequestHeader> headers, OAuth2ClientSideCredentials clientCredentials
   ) {
      headers.add(HttpRequestHeader.httpRequestHeader(this.headerField, this.buildHeaderFieldValue(clientCredentials)));
   }

   @Override
   public ClientAuthenticationMethod clientAuthenticationMethod() {
      return new ClientAuthenticationMethod() {
         @Override
         public String getIdentifier() {
            return "client_secret_custom_header";
         }
      };
   }

   private String buildHeaderFieldValue(OAuth2ClientSideCredentials client) {
      StringBuilder clientSecretValue = new StringBuilder();
      if (StringUtils.isBlank(this.clientSecretFormat)) {
         clientSecretValue.append(client.clientSecret().orElseThrow(OAuth2HeaderClientSecretConfig::noClientIdException));
      } else {
         clientSecretValue.append(this.replacePlaceHolders(client, this.clientSecretFormat));
      }

      if (this.base64EncodeClientSecretValue) {
         String base64EncodedClientSecretValue = Base64.encodeBase64String(clientSecretValue.toString().getBytes(StandardCharsets.UTF_8));
         clientSecretValue = new StringBuilder(base64EncodedClientSecretValue);
      }

      if (StringUtils.isNotBlank(this.headerPrefix)) {
         clientSecretValue.insert(0, this.headerPrefix + " ");
      }

      return clientSecretValue.toString();
   }

   private String replacePlaceHolders(OAuth2ClientSideCredentials client, String sourceString) {
      return sourceString.replace("$CLIENT_ID$", client.clientId().id())
         .replace("$CLIENT_SECRET$", client.clientSecret().orElseThrow(OAuth2HeaderClientSecretConfig::noClientIdException));
   }

   private static IllegalArgumentException noClientIdException() {
      return new IllegalArgumentException("Cannot use OAuth2Client with absent client secret in this context.");
   }

   @ConfigurationValidator
   public static void checkExactlyOneSecretPlaceholder(
      PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context, Environment validationEnvironment
   ) throws ConfigurationException {
      CLIENT_SECRET_CONFIG_VALIDATOR.checkExactlyOneSecretPlaceholder(config, env, context);
   }

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

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

   @SuppressFBWarnings(justification = "generated code")
   public OAuth2HeaderClientSecretConfig(String headerField, String headerPrefix, String clientSecretFormat, boolean base64EncodeClientSecretValue) {
      this.headerField = headerField;
      this.headerPrefix = headerPrefix;
      this.clientSecretFormat = clientSecretFormat;
      this.base64EncodeClientSecretValue = base64EncodeClientSecretValue;
   }

   @SuppressFBWarnings(justification = "generated code")
   public static class OAuth2HeaderClientSecretConfigBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private String headerField;
      @SuppressFBWarnings(justification = "generated code")
      private String headerPrefix;
      @SuppressFBWarnings(justification = "generated code")
      private String clientSecretFormat;
      @SuppressFBWarnings(justification = "generated code")
      private boolean base64EncodeClientSecretValue;

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

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2HeaderClientSecretConfig.OAuth2HeaderClientSecretConfigBuilder headerField(String headerField) {
         this.headerField = headerField;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2HeaderClientSecretConfig.OAuth2HeaderClientSecretConfigBuilder headerPrefix(String headerPrefix) {
         this.headerPrefix = headerPrefix;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2HeaderClientSecretConfig.OAuth2HeaderClientSecretConfigBuilder clientSecretFormat(String clientSecretFormat) {
         this.clientSecretFormat = clientSecretFormat;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2HeaderClientSecretConfig.OAuth2HeaderClientSecretConfigBuilder base64EncodeClientSecretValue(boolean base64EncodeClientSecretValue) {
         this.base64EncodeClientSecretValue = base64EncodeClientSecretValue;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2HeaderClientSecretConfig build() {
         return new OAuth2HeaderClientSecretConfig(this.headerField, this.headerPrefix, this.clientSecretFormat, this.base64EncodeClientSecretValue);
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "OAuth2HeaderClientSecretConfig.OAuth2HeaderClientSecretConfigBuilder(headerField="
            + this.headerField
            + ", headerPrefix="
            + this.headerPrefix
            + ", clientSecretFormat="
            + this.clientSecretFormat
            + ", base64EncodeClientSecretValue="
            + this.base64EncodeClientSecretValue
            + ")";
      }
   }
}
