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

import com.airlock.iam.base.api.domain.model.exception.FailureException;
import com.airlock.iam.base.misc.log.LogUtil;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.regex.Pattern;
import lombok.NonNull;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Strings;

public final class PkceCodeChallenge {
   public static final String CHALLENGE_METHOD_PLAIN = "plain";
   public static final String CHALLENGE_METHOD_S256 = "S256";
   private static final Pattern PKCE_CODE_CHALLENGE_PATTERN = Pattern.compile("[a-zA-Z0-9._~-]{43,128}");
   @NonNull
   private final String challenge;
   @NonNull
   private final String challengeMethod;

   public boolean isValidChallengeMethod() {
      return "plain".equals(this.challengeMethod) || "S256".equals(this.challengeMethod);
   }

   public boolean isValidChallenge() {
      return PKCE_CODE_CHALLENGE_PATTERN.matcher(this.challenge).matches();
   }

   public String serialize() {
      return this.challengeMethod + ":" + this.challenge;
   }

   public boolean verify(String verifier) {
      if (!this.isValidChallenge() || !this.isValidChallengeMethod()) {
         return false;
      } else if (StringUtils.isBlank(verifier)) {
         return false;
      } else {
         return "plain".equals(this.challengeMethod) ? Strings.CS.equals(this.challenge, verifier) : this.verifyS256Challenge(verifier);
      }
   }

   private boolean verifyS256Challenge(String verifier) {
      try {
         MessageDigest digest = MessageDigest.getInstance("SHA-256");
         String result = Base64.encodeBase64URLSafeString(digest.digest(verifier.getBytes(StandardCharsets.US_ASCII)));
         return Strings.CS.equals(this.challenge, result);
      } catch (NoSuchAlgorithmException e) {
         throw new FailureException("SHA-256 must be supported by every JVM", e);
      }
   }

   public static PkceCodeChallenge fromString(String str) {
      String[] parts = StringUtils.split(str, ":");
      if (parts.length != 2) {
         throw new FailureException("Unable to parse pkce code challenge string " + LogUtil.quote(str));
      } else {
         String method = StringUtils.trimToNull(parts[0]);
         if (method == null) {
            throw new FailureException("Unable to parse method from pkce code challenge string " + LogUtil.quote(str));
         } else {
            String challenge = StringUtils.trimToNull(parts[1]);
            if (challenge == null) {
               throw new FailureException("Unable to parse challenge from pkce code challenge string " + LogUtil.quote(str));
            } else {
               return builder().challengeMethod(method).challenge(challenge).build();
            }
         }
      }
   }

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

   @NonNull
   @SuppressFBWarnings(justification = "generated code")
   public String getChallenge() {
      return this.challenge;
   }

   @NonNull
   @SuppressFBWarnings(justification = "generated code")
   public String getChallengeMethod() {
      return this.challengeMethod;
   }

   @SuppressFBWarnings(justification = "generated code")
   @Override
   public boolean equals(Object o) {
      if (o == this) {
         return true;
      } else if (!(o instanceof PkceCodeChallenge other)) {
         return false;
      } else {
         Object this$challenge = this.challenge;
         Object other$challenge = other.challenge;
         if (this$challenge == null ? other$challenge == null : this$challenge.equals(other$challenge)) {
            Object this$challengeMethod = this.challengeMethod;
            Object other$challengeMethod = other.challengeMethod;
            return this$challengeMethod == null ? other$challengeMethod == null : this$challengeMethod.equals(other$challengeMethod);
         } else {
            return false;
         }
      }
   }

   @SuppressFBWarnings(justification = "generated code")
   @Override
   public int hashCode() {
      int PRIME = 59;
      int result = 1;
      Object $challenge = this.challenge;
      result = result * 59 + ($challenge == null ? 43 : $challenge.hashCode());
      Object $challengeMethod = this.challengeMethod;
      return result * 59 + ($challengeMethod == null ? 43 : $challengeMethod.hashCode());
   }

   @SuppressFBWarnings(justification = "generated code")
   @Override
   public String toString() {
      return "PkceCodeChallenge(challenge=" + this.getChallenge() + ", challengeMethod=" + this.getChallengeMethod() + ")";
   }

   @SuppressFBWarnings(justification = "generated code")
   public PkceCodeChallenge(@NonNull String challenge, @NonNull String challengeMethod) {
      if (challenge == null) {
         throw new NullPointerException("challenge is marked non-null but is null");
      }

      if (challengeMethod == null) {
         throw new NullPointerException("challengeMethod is marked non-null but is null");
      }

      this.challenge = challenge;
      this.challengeMethod = challengeMethod;
   }

   @SuppressFBWarnings(justification = "generated code")
   public static class PkceCodeChallengeBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private String challenge;
      @SuppressFBWarnings(justification = "generated code")
      private String challengeMethod;

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

      @SuppressFBWarnings(justification = "generated code")
      public PkceCodeChallenge.PkceCodeChallengeBuilder challenge(@NonNull String challenge) {
         if (challenge == null) {
            throw new NullPointerException("challenge is marked non-null but is null");
         }

         this.challenge = challenge;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public PkceCodeChallenge.PkceCodeChallengeBuilder challengeMethod(@NonNull String challengeMethod) {
         if (challengeMethod == null) {
            throw new NullPointerException("challengeMethod is marked non-null but is null");
         }

         this.challengeMethod = challengeMethod;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public PkceCodeChallenge build() {
         return new PkceCodeChallenge(this.challenge, this.challengeMethod);
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "PkceCodeChallenge.PkceCodeChallengeBuilder(challenge=" + this.challenge + ", challengeMethod=" + this.challengeMethod + ")";
      }
   }
}
