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

import com.airlock.iam.oauth2.api.domain.model.OAuth2ClientId;
import com.airlock.iam.oauth2.domain.model.OAuth2RequestedResponseMode;
import com.airlock.iam.oauth2.domain.model.State;
import com.airlock.iam.oauth2.domain.model.validation.OAuth2Error;
import com.nimbusds.oauth2.sdk.AuthorizationRequest;
import com.nimbusds.oauth2.sdk.ParseException;
import com.nimbusds.oauth2.sdk.PushedAuthorizationRequest;
import com.nimbusds.oauth2.sdk.Scope;
import com.nimbusds.oauth2.sdk.AuthorizationRequest.Builder;
import com.nimbusds.oauth2.sdk.http.HTTPRequest;
import com.nimbusds.oauth2.sdk.http.HTTPRequest.Method;
import com.nimbusds.oauth2.sdk.id.Identifier;
import com.nimbusds.oauth2.sdk.util.MultivaluedMapUtils;
import com.nimbusds.oauth2.sdk.util.URLUtils;
import com.nimbusds.openid.connect.sdk.AuthenticationRequest;
import java.net.URI;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.apache.commons.lang3.StringUtils;

public final class AuthorizationCodeRequestParser {
   public static OAuth2AuthorizationRequest parseOAuth2AuthorizationRequest(String authorizationRequestQueryString) throws OAuth2AuthorizationCodeGrantRequestParseException {
      try {
         AuthorizationRequest authorizationRequest = parse(authorizationRequestQueryString);
         return new OAuth2AuthorizationRequest(authorizationRequest);
      } catch (ParseException e) {
         throw mapParseException(e, "Unknown OAuth 2.0 error: ");
      } catch (RuntimeException e) {
         throw new OAuth2AuthorizationCodeGrantRequestParseException(e.getMessage());
      }
   }

   private static AuthorizationRequest parse(String authorizationRequestQueryString) throws ParseException {
      Map<String, List<String>> params = URLUtils.parseParameters(authorizationRequestQueryString);
      AuthorizationRequest parsedAuthorizationRequest = AuthorizationRequest.parse(null, params);
      Builder authorizationRequest = new Builder(parsedAuthorizationRequest);
      String scope = (String)MultivaluedMapUtils.getFirstValue(params, "scope");
      if (params.containsKey("scope") && StringUtils.isBlank(scope)) {
         authorizationRequest.scope(new Scope());
      }

      return authorizationRequest.build();
   }

   public static OpenIdConnectAuthenticationRequest parseOpenIdConnectAuthenticationRequest(String authenticationRequestQueryString) throws OAuth2AuthorizationCodeGrantRequestParseException {
      try {
         AuthenticationRequest parsedAuthenticationRequest = AuthenticationRequest.parse(authenticationRequestQueryString);
         return new OpenIdConnectAuthenticationRequest(parsedAuthenticationRequest);
      } catch (ParseException e) {
         throw mapParseException(e, "Unknown OpenID Connect error: ");
      } catch (RuntimeException e) {
         throw new OAuth2AuthorizationCodeGrantRequestParseException(e.getMessage());
      }
   }

   public static OAuth2AuthorizationRequest parseOAuth2PushedAuthorizationRequest(URI requestUri, String contentType, Map<String, List<String>> parameters) throws OAuth2AuthorizationCodeGrantRequestParseException {
      try {
         HTTPRequest request = httpRequest(requestUri, contentType, parameters);
         PushedAuthorizationRequest pushedAuthorizationRequest = PushedAuthorizationRequest.parse(request);
         if (pushedAuthorizationRequest.getAuthorizationRequest() instanceof AuthenticationRequest) {
            throw new OAuth2AuthorizationCodeGrantRequestParseException("Pushed Authorization Requests (PAR) must use OAuth 2.0 (not OIDC).");
         } else {
            return new OAuth2AuthorizationRequest(pushedAuthorizationRequest.getAuthorizationRequest());
         }
      } catch (ParseException e) {
         throw mapParseException(e, "Unknown OAuth 2.0 Pushed Authorization Request error: ");
      }
   }

   public static OpenIdConnectAuthenticationRequest parseOpenIdConnectPushedAuthorizationRequest(
      URI requestUri, String contentType, Map<String, List<String>> parameters
   ) throws OAuth2AuthorizationCodeGrantRequestParseException {
      try {
         HTTPRequest request = httpRequest(requestUri, contentType, parameters);
         PushedAuthorizationRequest pushedAuthorizationRequest = PushedAuthorizationRequest.parse(request);
         if (pushedAuthorizationRequest.getAuthorizationRequest() instanceof AuthenticationRequest authenticationRequest) {
            return new OpenIdConnectAuthenticationRequest(authenticationRequest);
         } else {
            throw new OAuth2AuthorizationCodeGrantRequestParseException("Pushed Authorization Requests (PAR) must use OIDC (not OAuth 2.0).");
         }
      } catch (ParseException e) {
         throw mapParseException(e, "Unknown OIDC Pushed Authorization Request error: ");
      }
   }

   private static HTTPRequest httpRequest(URI requestUri, String contentType, Map<String, List<String>> parameters) throws ParseException {
      HTTPRequest request = new HTTPRequest(Method.POST, requestUri);
      request.setContentType(contentType);
      String form = URLUtils.serializeParameters(parameters);
      request.setBody(form);
      return request;
   }

   private static OAuth2AuthorizationCodeGrantRequestParseException mapParseException(ParseException e, String logMessage) {
      return e.getErrorObject() != null
         ? OAuth2Error.fromCode(e.getErrorObject().getCode())
            .map(error -> mapParseExceptionWithError(e, error, e.getErrorObject().getDescription()))
            .orElseGet(() -> new OAuth2AuthorizationCodeGrantRequestParseException(logMessage + e.getErrorObject().getCode()))
         : new OAuth2AuthorizationCodeGrantRequestParseException(e.getMessage());
   }

   private static OAuth2AuthorizationCodeGrantRequestParseException mapParseExceptionWithError(ParseException e, OAuth2Error error, String description) {
      return OAuth2AuthorizationCodeGrantRequestParseException.builder()
         .message(e.getMessage())
         .error(error)
         .errorDescription(description)
         .clientId(toClientId(e))
         .state(toState(e))
         .redirectUri(e.getRedirectionURI())
         .responseMode(toRequestedResponseMode(e))
         .build();
   }

   private static OAuth2RequestedResponseMode toRequestedResponseMode(ParseException e) {
      return Optional.ofNullable(e.getResponseMode()).<String>map(Identifier::getValue).map(OAuth2RequestedResponseMode::new).orElse(null);
   }

   private static OAuth2ClientId toClientId(ParseException e) {
      return Optional.ofNullable(e.getClientID()).<String>map(Identifier::getValue).map(OAuth2ClientId::oAuth2ClientId).orElse(null);
   }

   private static State toState(ParseException e) {
      return Optional.ofNullable(e.getState()).<String>map(Identifier::getValue).map(State::state).orElse(null);
   }

   private AuthorizationCodeRequestParser() {
      throw new AssertionError();
   }
}
