package com.airlock.iam.oauth2.domain.model.provider;

import com.airlock.iam.base.api.domain.model.exception.FailureException;
import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.oauth2.api.domain.model.OAuth2ClientId;
import com.airlock.iam.oauth2.api.domain.model.clientauthentication.OAuth2AuthClientId;
import com.airlock.iam.oauth2.api.domain.model.clientauthentication.OAuth2ClientAuthentication;
import com.airlock.iam.oauth2.api.domain.model.clientauthentication.OAuth2ClientAuthenticationPayload;
import com.airlock.iam.oauth2.domain.model.logging.HidingTokenRepresentation;
import com.airlock.iam.oauth2.domain.model.logging.OAuth2TokenRepresentation;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.annotation.Nullable;
import java.io.Serializable;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import lombok.NonNull;

public class OAuth2TokenEndpointRequestBean implements Serializable {
   private String clientIdFromRequest;
   private Optional<OAuth2ClientId> knownClientId = Optional.empty();
   @Nullable
   private String redirectUriFromRequest;
   private boolean requestUriFromRequestValidated;
   private String grantType;
   private String authorizationCode;
   private String refreshToken;
   @NonNull
   private Optional<String> pkceCodeVerifier = Optional.empty();
   private OAuth2TokenRepresentation tokenRepresentation = new HidingTokenRepresentation();
   private OAuth2ClientAuthenticationPayload authenticationPayload;
   private OAuth2ClientAuthentication clientAuthentication;

   @Nullable
   public String getClientIdFromRequest() {
      return this.clientIdFromRequest;
   }

   public OAuth2TokenEndpointRequestBean setClientIdFromRequest(String clientId) {
      this.clientIdFromRequest = clientId;
      return this;
   }

   public OAuth2ClientId knownClientId() {
      return this.knownClientId
         .orElseThrow(() -> new FailureException("Not (yet) verified, that the client " + LogUtil.logSafeQuote(this.getClientIdFromRequest()) + " is known!"));
   }

   public boolean isClientKnown() {
      return this.knownClientId.isPresent();
   }

   public void promoteClientIdToKnownClientId() {
      if (this.clientIdFromRequest == null) {
         throw new FailureException("The clientId can not be promoted to an known clientId because the clientId was not set yet!");
      }

      this.knownClientId = Optional.of(OAuth2ClientId.oAuth2ClientId(this.clientIdFromRequest));
   }

   public void promoteValidatedRedirectUri() {
      this.requestUriFromRequestValidated = true;
   }

   public Optional<OAuth2ClientAuthenticationPayload> authenticationPayload() {
      return Optional.ofNullable(this.authenticationPayload);
   }

   public void authenticationPayload(OAuth2ClientAuthenticationPayload authenticationPayloadFromRequest) {
      this.authenticationPayload = authenticationPayloadFromRequest;
   }

   @Nullable
   public String getRedirectUriFromRequest() {
      return this.redirectUriFromRequest;
   }

   public Optional<String> getValidatedRedirectUri() {
      if (!this.requestUriFromRequestValidated) {
         throw new IllegalStateException("redirect_uri was not yet validated");
      } else {
         return Optional.ofNullable(this.redirectUriFromRequest);
      }
   }

   public void setRedirectUriFromRequest(@Nullable String redirectUriFromRequest) {
      this.redirectUriFromRequest = redirectUriFromRequest;
   }

   @Nullable
   public String getGrantType() {
      return this.grantType;
   }

   public void setGrantType(String grantType) {
      this.grantType = grantType;
   }

   @Nullable
   public String getAuthorizationCode() {
      return this.authorizationCode;
   }

   public void setAuthorizationCode(String authorizationCode) {
      this.authorizationCode = authorizationCode;
   }

   @Nullable
   public String getRefreshToken() {
      return this.refreshToken;
   }

   public void setRefreshToken(String refreshToken) {
      this.refreshToken = refreshToken;
   }

   public void setTokenRepresentation(OAuth2TokenRepresentation tokenRepresentation) {
      this.tokenRepresentation = tokenRepresentation;
   }

   @Override
   public String toString() {
      return this.fieldsMap().entrySet().stream().map(e -> e.getKey() + " = " + e.getValue()).collect(Collectors.joining("\n"));
   }

   private Map<String, Object> fieldsMap() {
      Map<String, Object> fieldsMap = new LinkedHashMap<>();
      addIfNotNull(fieldsMap, "Client ID", this.clientIdFromRequest);
      addIfPresent(fieldsMap, "Client ID from existing client", this.knownClientId.map(OAuth2ClientId::id));
      addIfPresent(
         fieldsMap,
         "Client ID from authentication",
         this.authenticationPayload().flatMap(OAuth2ClientAuthenticationPayload::clientId).map(OAuth2AuthClientId::id)
      );
      addIfNotNull(fieldsMap, "Redirect URI", this.redirectUriFromRequest);
      addIfNotNull(fieldsMap, "Grant Type", this.grantType);
      addIfNotNull(fieldsMap, "Authorization Code", this.maskSecretValue(this.authorizationCode));
      addIfNotNull(fieldsMap, "Refresh Token", this.maskSecretValue(this.refreshToken));
      addIfNotNull(fieldsMap, "PKCE verifier", this.maskSecretValue(this.pkceCodeVerifier.orElse(null)));
      return fieldsMap;
   }

   private static void addIfNotNull(Map<String, Object> map, String key, Object value) {
      if (value != null) {
         map.put(key, value);
      }
   }

   private static void addIfPresent(Map<String, Object> map, String key, Optional<String> value) {
      if (value.isPresent()) {
         map.put(key, value.get());
      }
   }

   private String maskSecretValue(String value) {
      return value != null ? this.tokenRepresentation.apply(value) : null;
   }

   public void setClientAuthentication(OAuth2ClientAuthentication authentication) {
      this.clientAuthentication = authentication;
   }

   public Optional<OAuth2ClientAuthentication> clientAuthentication() {
      return Optional.ofNullable(this.clientAuthentication);
   }

   @NonNull
   @SuppressFBWarnings(justification = "generated code")
   public Optional<String> getPkceCodeVerifier() {
      return this.pkceCodeVerifier;
   }

   @SuppressFBWarnings(justification = "generated code")
   public void setPkceCodeVerifier(@NonNull Optional<String> pkceCodeVerifier) {
      if (pkceCodeVerifier == null) {
         throw new NullPointerException("pkceCodeVerifier is marked non-null but is null");
      }

      this.pkceCodeVerifier = pkceCodeVerifier;
   }
}
