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

import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.elog.api.LogTypes;
import com.airlock.iam.oauth2.api.domain.model.OAuth2ClientId;
import com.airlock.iam.oauth2.api.domain.model.OAuth2Scope;
import com.airlock.iam.oauth2.domain.model.OAuth2Language;
import com.airlock.iam.oauth2.domain.model.State;
import com.airlock.iam.oauth2.domain.model.acr.OpenIdConnectAcrValue;
import com.airlock.iam.oauth2.domain.model.authentication.OpenIdConnectLoginHint;
import com.airlock.iam.oauth2.domain.model.authorization.OAuth2ResponseType;
import com.airlock.iam.oauth2.domain.model.claims.ClaimRequirement;
import com.airlock.iam.oauth2.domain.model.session.PkceCodeVerifier;
import com.google.common.collect.ImmutableList;
import com.nimbusds.langtag.LangTag;
import com.nimbusds.langtag.LangTagException;
import com.nimbusds.oauth2.sdk.ParseException;
import com.nimbusds.oauth2.sdk.ResponseType;
import com.nimbusds.oauth2.sdk.Scope;
import com.nimbusds.oauth2.sdk.ResponseType.Value;
import com.nimbusds.oauth2.sdk.id.ClientID;
import com.nimbusds.oauth2.sdk.id.Identifier;
import com.nimbusds.oauth2.sdk.pkce.CodeChallengeMethod;
import com.nimbusds.oauth2.sdk.pkce.CodeVerifier;
import com.nimbusds.openid.connect.sdk.AuthenticationRequest;
import com.nimbusds.openid.connect.sdk.Nonce;
import com.nimbusds.openid.connect.sdk.OIDCClaimsRequest;
import com.nimbusds.openid.connect.sdk.Prompt;
import com.nimbusds.openid.connect.sdk.AuthenticationRequest.Builder;
import com.nimbusds.openid.connect.sdk.claims.ACR;
import com.nimbusds.openid.connect.sdk.claims.ClaimsSetRequest;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.net.URI;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

public class OpenIdConnectAuthenticationRequest extends OAuth2AuthorizationRequest {
   private static final ELogger LOG = ELoggerFactory.getLogger(OpenIdConnectAuthenticationRequest.class);
   private final AuthenticationRequest request;

   OpenIdConnectAuthenticationRequest(AuthenticationRequest request) {
      super(request);
      this.request = request;
   }

   public OpenIdConnectAuthenticationRequest(
      URI authorizationEndpointURI,
      OAuth2ResponseType responseType,
      OAuth2ClientId clientId,
      URI redirectUri,
      List<OAuth2Scope> scopes,
      Optional<State> state,
      List<PromptValue> promptValues,
      boolean includeLanguageParameter,
      OAuth2Language displayLanguage,
      int maxAuthenticationAge,
      List<String> acrValuesClaim,
      Optional<OpenIdConnectAuthenticationRequest.OpenIdConnectClaimsRequest> claimsRequest,
      boolean includeNonce,
      String nonce,
      Map<String, List<String>> customParameters,
      Optional<PkceCodeVerifier> codeVerifier
   ) {
      this(
         buildRequest(
            authorizationEndpointURI,
            responseType,
            clientId,
            redirectUri,
            scopes,
            state,
            promptValues,
            includeLanguageParameter,
            displayLanguage,
            maxAuthenticationAge,
            acrValuesClaim,
            claimsRequest,
            includeNonce,
            nonce,
            customParameters,
            codeVerifier
         )
      );
   }

   private static AuthenticationRequest buildRequest(
      URI authorizationEndpointURI,
      OAuth2ResponseType responseType,
      OAuth2ClientId clientId,
      URI redirectUri,
      List<OAuth2Scope> scopes,
      Optional<State> state,
      List<PromptValue> promptValues,
      boolean includeLanguageParameter,
      OAuth2Language displayLanguage,
      int maxAuthenticationAge,
      List<String> acrValuesClaim,
      Optional<OpenIdConnectAuthenticationRequest.OpenIdConnectClaimsRequest> claimsRequest,
      boolean includeNonce,
      String nonce,
      Map<String, List<String>> customParameters,
      Optional<PkceCodeVerifier> codeVerifier
   ) {
      Builder builder = new Builder(
         new ResponseType(new Value[]{new Value(responseType.responseType())}),
         new Scope(scopes.stream().map(OAuth2Scope::name).toArray(String[]::new)),
         new ClientID(clientId.id()),
         redirectUri
      );
      customParameters.forEach((p, v) -> builder.customParameter(p, v.toArray(new String[0])));
      return builder.endpointURI(authorizationEndpointURI)
         .state(state.<com.nimbusds.oauth2.sdk.id.State>map(value -> new com.nimbusds.oauth2.sdk.id.State(value.getState())).orElse(null))
         .prompt(prompt(promptValues))
         .uiLocales(includeLanguageParameter ? languages(displayLanguage) : null)
         .maxAge(maxAuthenticationAge > 0 ? maxAuthenticationAge : -1)
         .acrValues(!acrValuesClaim.isEmpty() ? acrValuesClaim.stream().map(ACR::new).collect(Collectors.toList()) : null)
         .claims(claimsRequest.map(OpenIdConnectAuthenticationRequest::claims).orElse(null))
         .nonce(includeNonce ? Nonce.parse(nonce) : null)
         .codeChallenge(
            codeVerifier.map(PkceCodeVerifier::verifier).<CodeVerifier>map(CodeVerifier::new).orElse(null),
            codeVerifier.isPresent() ? CodeChallengeMethod.S256 : null
         )
         .build();
   }

   private static OIDCClaimsRequest claims(OpenIdConnectAuthenticationRequest.OpenIdConnectClaimsRequest claimsRequest) {
      OIDCClaimsRequest oidcClaimsRequest = new OIDCClaimsRequest();
      oidcClaimsRequest = oidcClaimsRequest.withIDTokenClaimsRequest(claimsSetRequest(claimsRequest.getIDTokenClaimsRequest()));
      return oidcClaimsRequest.withUserInfoClaimsRequest(claimsSetRequest(claimsRequest.getUserInfoClaimsRequest()));
   }

   private static ClaimsSetRequest claimsSetRequest(
      OpenIdConnectAuthenticationRequest.OpenIdConnectClaimsRequest.OpenIdConnectClaimsSetRequest claimsSetRequest
   ) {
      return claimsSetRequest.claimsSetRequest.orElse(null);
   }

   private static Prompt prompt(List<PromptValue> promptValues) {
      if (promptValues.isEmpty()) {
         return null;
      }

      try {
         return Prompt.parse(promptValues.stream().map(PromptValue::getParamValue).collect(Collectors.toList()));
      } catch (ParseException e) {
         LOG.warn(LogTypes.ILLEGAL_ARGUMENT, "invalid prompt value - omitting prompt", e);
         return null;
      }
   }

   private static List<LangTag> languages(OAuth2Language displayLanguage) {
      try {
         return ImmutableList.of(LangTag.parse(displayLanguage.asLanguageTagString()));
      } catch (LangTagException e) {
         LOG.warn(LogTypes.ILLEGAL_ARGUMENT, "invalid language tag - omitting language tag", e);
         return null;
      }
   }

   @Override
   public List<OAuth2Scope> getRequestedScopes() {
      return super.getRequestedScopes().stream().filter(scope -> !"openid".equals(scope.name())).collect(Collectors.toList());
   }

   public boolean hasRequestObject() {
      return this.request.getRequestObject() != null;
   }

   public List<OAuth2Language> getUiLocales() {
      return Optional.ofNullable(this.request.getUILocales())
         .map(langTags -> langTags.stream().map(langTag -> OAuth2Language.oAuth2Language(langTag.toString())).toList())
         .orElse(List.of());
   }

   public Optional<String> getNonce() {
      return Optional.ofNullable(this.request.getNonce()).map(Identifier::getValue);
   }

   public Optional<OpenIdConnectLoginHint> getLoginHint() {
      return Optional.ofNullable(this.request.getLoginHint()).map(OpenIdConnectLoginHint::new);
   }

   public List<OpenIdConnectAcrValue> getAcr() {
      return this.request.getACRValues() != null && !this.request.getACRValues().isEmpty()
         ? this.request.getACRValues().stream().<String>map(Identifier::getValue).map(OpenIdConnectAcrValue::of).collect(Collectors.toList())
         : List.of();
   }

   public List<String> getPrompts() {
      return this.request.getPrompt() == null ? List.of() : this.request.getPrompt().toStringList();
   }

   public Optional<Duration> getMaxAge() {
      return Optional.of(this.request.getMaxAge()).filter(maxAge -> maxAge >= 0).map(Duration::ofSeconds);
   }

   public Map<String, List<String>> getCustomParameters() {
      return this.request.getCustomParameters();
   }

   public boolean isHybridFlowRequest() {
      return this.getResponseType().isPresent() && this.getResponseType().get().impliesHybridFlow();
   }

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

   @SuppressFBWarnings(justification = "generated code")
   public static class OpenIdConnectAuthenticationRequestBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private URI authorizationEndpointURI;
      @SuppressFBWarnings(justification = "generated code")
      private OAuth2ResponseType responseType;
      @SuppressFBWarnings(justification = "generated code")
      private OAuth2ClientId clientId;
      @SuppressFBWarnings(justification = "generated code")
      private URI redirectUri;
      @SuppressFBWarnings(justification = "generated code")
      private List<OAuth2Scope> scopes;
      @SuppressFBWarnings(justification = "generated code")
      private Optional<State> state;
      @SuppressFBWarnings(justification = "generated code")
      private List<PromptValue> promptValues;
      @SuppressFBWarnings(justification = "generated code")
      private boolean includeLanguageParameter;
      @SuppressFBWarnings(justification = "generated code")
      private OAuth2Language displayLanguage;
      @SuppressFBWarnings(justification = "generated code")
      private int maxAuthenticationAge;
      @SuppressFBWarnings(justification = "generated code")
      private List<String> acrValuesClaim;
      @SuppressFBWarnings(justification = "generated code")
      private Optional<OpenIdConnectAuthenticationRequest.OpenIdConnectClaimsRequest> claimsRequest;
      @SuppressFBWarnings(justification = "generated code")
      private boolean includeNonce;
      @SuppressFBWarnings(justification = "generated code")
      private String nonce;
      @SuppressFBWarnings(justification = "generated code")
      private ArrayList<String> customParameters$key;
      @SuppressFBWarnings(justification = "generated code")
      private ArrayList<List<String>> customParameters$value;
      @SuppressFBWarnings(justification = "generated code")
      private Optional<PkceCodeVerifier> codeVerifier;

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

      @SuppressFBWarnings(justification = "generated code")
      public OpenIdConnectAuthenticationRequest.OpenIdConnectAuthenticationRequestBuilder authorizationEndpointURI(URI authorizationEndpointURI) {
         this.authorizationEndpointURI = authorizationEndpointURI;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OpenIdConnectAuthenticationRequest.OpenIdConnectAuthenticationRequestBuilder responseType(OAuth2ResponseType responseType) {
         this.responseType = responseType;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OpenIdConnectAuthenticationRequest.OpenIdConnectAuthenticationRequestBuilder clientId(OAuth2ClientId clientId) {
         this.clientId = clientId;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OpenIdConnectAuthenticationRequest.OpenIdConnectAuthenticationRequestBuilder redirectUri(URI redirectUri) {
         this.redirectUri = redirectUri;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OpenIdConnectAuthenticationRequest.OpenIdConnectAuthenticationRequestBuilder scopes(List<OAuth2Scope> scopes) {
         this.scopes = scopes;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OpenIdConnectAuthenticationRequest.OpenIdConnectAuthenticationRequestBuilder state(Optional<State> state) {
         this.state = state;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OpenIdConnectAuthenticationRequest.OpenIdConnectAuthenticationRequestBuilder promptValues(List<PromptValue> promptValues) {
         this.promptValues = promptValues;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OpenIdConnectAuthenticationRequest.OpenIdConnectAuthenticationRequestBuilder includeLanguageParameter(boolean includeLanguageParameter) {
         this.includeLanguageParameter = includeLanguageParameter;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OpenIdConnectAuthenticationRequest.OpenIdConnectAuthenticationRequestBuilder displayLanguage(OAuth2Language displayLanguage) {
         this.displayLanguage = displayLanguage;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OpenIdConnectAuthenticationRequest.OpenIdConnectAuthenticationRequestBuilder maxAuthenticationAge(int maxAuthenticationAge) {
         this.maxAuthenticationAge = maxAuthenticationAge;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OpenIdConnectAuthenticationRequest.OpenIdConnectAuthenticationRequestBuilder acrValuesClaim(List<String> acrValuesClaim) {
         this.acrValuesClaim = acrValuesClaim;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OpenIdConnectAuthenticationRequest.OpenIdConnectAuthenticationRequestBuilder claimsRequest(
         Optional<OpenIdConnectAuthenticationRequest.OpenIdConnectClaimsRequest> claimsRequest
      ) {
         this.claimsRequest = claimsRequest;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OpenIdConnectAuthenticationRequest.OpenIdConnectAuthenticationRequestBuilder includeNonce(boolean includeNonce) {
         this.includeNonce = includeNonce;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OpenIdConnectAuthenticationRequest.OpenIdConnectAuthenticationRequestBuilder nonce(String nonce) {
         this.nonce = nonce;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OpenIdConnectAuthenticationRequest.OpenIdConnectAuthenticationRequestBuilder customParameter(
         String customParameterKey, List<String> customParameterValue
      ) {
         if (this.customParameters$key == null) {
            this.customParameters$key = new ArrayList<>();
            this.customParameters$value = new ArrayList<>();
         }

         this.customParameters$key.add(customParameterKey);
         this.customParameters$value.add(customParameterValue);
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OpenIdConnectAuthenticationRequest.OpenIdConnectAuthenticationRequestBuilder customParameters(
         Map<? extends String, ? extends List<String>> customParameters
      ) {
         if (customParameters == null) {
            throw new NullPointerException("customParameters cannot be null");
         }

         if (this.customParameters$key == null) {
            this.customParameters$key = new ArrayList<>();
            this.customParameters$value = new ArrayList<>();
         }

         for (java.util.Map.Entry<? extends String, ? extends List<String>> $lombokEntry : customParameters.entrySet()) {
            this.customParameters$key.add($lombokEntry.getKey());
            this.customParameters$value.add((List<String>)$lombokEntry.getValue());
         }

         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OpenIdConnectAuthenticationRequest.OpenIdConnectAuthenticationRequestBuilder clearCustomParameters() {
         if (this.customParameters$key != null) {
            this.customParameters$key.clear();
            this.customParameters$value.clear();
         }

         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OpenIdConnectAuthenticationRequest.OpenIdConnectAuthenticationRequestBuilder codeVerifier(Optional<PkceCodeVerifier> codeVerifier) {
         this.codeVerifier = codeVerifier;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OpenIdConnectAuthenticationRequest build() {
         return new OpenIdConnectAuthenticationRequest(
            this.authorizationEndpointURI,
            this.responseType,
            this.clientId,
            this.redirectUri,
            this.scopes,
            this.state,
            this.promptValues,
            this.includeLanguageParameter,
            this.displayLanguage,
            this.maxAuthenticationAge,
            this.acrValuesClaim,
            this.claimsRequest,
            this.includeNonce,
            this.nonce,
            switch (this.customParameters$key == null ? 0 : this.customParameters$key.size()) {
               case 0 -> Collections.emptyMap();
               case 1 -> Collections.singletonMap(this.customParameters$key.get(0), this.customParameters$value.get(0));
               default -> {
                  Map<String, List<String>> customParameters = new LinkedHashMap<>(
                     this.customParameters$key.size() < 1073741824
                        ? 1 + this.customParameters$key.size() + (this.customParameters$key.size() - 3) / 3
                        : Integer.MAX_VALUE
                  );

                  for (int $i = 0; $i < this.customParameters$key.size(); $i++) {
                     customParameters.put(this.customParameters$key.get($i), this.customParameters$value.get($i));
                  }

                  yield Collections.unmodifiableMap(customParameters);
               }
            },
            this.codeVerifier
         );
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "OpenIdConnectAuthenticationRequest.OpenIdConnectAuthenticationRequestBuilder(authorizationEndpointURI="
            + this.authorizationEndpointURI
            + ", responseType="
            + this.responseType
            + ", clientId="
            + this.clientId
            + ", redirectUri="
            + this.redirectUri
            + ", scopes="
            + this.scopes
            + ", state="
            + this.state
            + ", promptValues="
            + this.promptValues
            + ", includeLanguageParameter="
            + this.includeLanguageParameter
            + ", displayLanguage="
            + this.displayLanguage
            + ", maxAuthenticationAge="
            + this.maxAuthenticationAge
            + ", acrValuesClaim="
            + this.acrValuesClaim
            + ", claimsRequest="
            + this.claimsRequest
            + ", includeNonce="
            + this.includeNonce
            + ", nonce="
            + this.nonce
            + ", customParameters$key="
            + this.customParameters$key
            + ", customParameters$value="
            + this.customParameters$value
            + ", codeVerifier="
            + this.codeVerifier
            + ")";
      }
   }

   public static final class OpenIdConnectClaimsRequest {
      private final OIDCClaimsRequest claimsRequest;

      private OpenIdConnectClaimsRequest(OIDCClaimsRequest claimsRequest) {
         this.claimsRequest = claimsRequest;
      }

      public OpenIdConnectClaimsRequest() {
         this(new OIDCClaimsRequest());
      }

      public OpenIdConnectAuthenticationRequest.OpenIdConnectClaimsRequest withIDTokenClaimsRequest(
         OpenIdConnectAuthenticationRequest.OpenIdConnectClaimsRequest.OpenIdConnectClaimsSetRequest idToken
      ) {
         return new OpenIdConnectAuthenticationRequest.OpenIdConnectClaimsRequest(
            this.claimsRequest.withIDTokenClaimsRequest(idToken.claimsSetRequest.orElse(null))
         );
      }

      public OpenIdConnectAuthenticationRequest.OpenIdConnectClaimsRequest withUserInfoClaimsRequest(
         OpenIdConnectAuthenticationRequest.OpenIdConnectClaimsRequest.OpenIdConnectClaimsSetRequest userInfo
      ) {
         return new OpenIdConnectAuthenticationRequest.OpenIdConnectClaimsRequest(
            this.claimsRequest.withUserInfoClaimsRequest(userInfo.claimsSetRequest.orElse(null))
         );
      }

      public OpenIdConnectAuthenticationRequest.OpenIdConnectClaimsRequest.OpenIdConnectClaimsSetRequest getIDTokenClaimsRequest() {
         return new OpenIdConnectAuthenticationRequest.OpenIdConnectClaimsRequest.OpenIdConnectClaimsSetRequest(
            Optional.ofNullable(this.claimsRequest.getIDTokenClaimsRequest())
         );
      }

      public OpenIdConnectAuthenticationRequest.OpenIdConnectClaimsRequest.OpenIdConnectClaimsSetRequest getUserInfoClaimsRequest() {
         return new OpenIdConnectAuthenticationRequest.OpenIdConnectClaimsRequest.OpenIdConnectClaimsSetRequest(
            Optional.ofNullable(this.claimsRequest.getUserInfoClaimsRequest())
         );
      }

      public static final class OpenIdConnectClaimsSetRequest {
         private final Optional<ClaimsSetRequest> claimsSetRequest;

         private OpenIdConnectClaimsSetRequest(Optional<ClaimsSetRequest> claimsSetRequest) {
            this.claimsSetRequest = claimsSetRequest;
         }

         public OpenIdConnectClaimsSetRequest(
            List<OpenIdConnectAuthenticationRequest.OpenIdConnectClaimsRequest.OpenIdConnectClaimsSetRequest.Entry> idTokenClaims
         ) {
            this.claimsSetRequest = Optional.of(new ClaimsSetRequest(idTokenClaims.stream().map(entry -> entry.entry).collect(Collectors.toList())));
         }

         public static final class Entry {
            private final com.nimbusds.openid.connect.sdk.claims.ClaimsSetRequest.Entry entry;

            private Entry(com.nimbusds.openid.connect.sdk.claims.ClaimsSetRequest.Entry entry) {
               this.entry = entry;
            }

            public Entry(String claimName) {
               this.entry = new com.nimbusds.openid.connect.sdk.claims.ClaimsSetRequest.Entry(claimName);
            }

            public OpenIdConnectAuthenticationRequest.OpenIdConnectClaimsRequest.OpenIdConnectClaimsSetRequest.Entry withClaimRequirement(
               ClaimRequirement requirement
            ) {
               return switch (requirement) {
                  case ESSENTIAL -> new OpenIdConnectAuthenticationRequest.OpenIdConnectClaimsRequest.OpenIdConnectClaimsSetRequest.Entry(
                     this.entry.withClaimRequirement(com.nimbusds.openid.connect.sdk.claims.ClaimRequirement.ESSENTIAL)
                  );
                  case VOLUNTARY -> new OpenIdConnectAuthenticationRequest.OpenIdConnectClaimsRequest.OpenIdConnectClaimsSetRequest.Entry(
                     this.entry.withClaimRequirement(com.nimbusds.openid.connect.sdk.claims.ClaimRequirement.VOLUNTARY)
                  );
               };
            }

            public OpenIdConnectAuthenticationRequest.OpenIdConnectClaimsRequest.OpenIdConnectClaimsSetRequest.Entry withValue(Object value) {
               return new OpenIdConnectAuthenticationRequest.OpenIdConnectClaimsRequest.OpenIdConnectClaimsSetRequest.Entry(this.entry.withValue(value));
            }

            public OpenIdConnectAuthenticationRequest.OpenIdConnectClaimsRequest.OpenIdConnectClaimsSetRequest.Entry withValues(List<?> values) {
               return new OpenIdConnectAuthenticationRequest.OpenIdConnectClaimsRequest.OpenIdConnectClaimsSetRequest.Entry(this.entry.withValues(values));
            }
         }
      }
   }
}
