package com.airlock.iam.authentication.application.service.oauth2;

import com.airlock.iam.authentication.application.service.ApplicationAccessService;
import com.airlock.iam.authentication.application.service.InternalApplicationAccessService;
import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.base.misc.result.Result;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.flow.api.domain.model.flow.ApplicationId;
import com.airlock.iam.flow.api.domain.model.flow.FlowResult;
import com.airlock.iam.flow.api.domain.model.redflag.RedFlag;
import com.airlock.iam.flow.api.domain.model.step.StepError;
import com.airlock.iam.flow.api.domain.model.step.StepResultBuilder;
import com.airlock.iam.flow.domain.model.result.FlowResultImpl;
import com.airlock.iam.flow.impl.domain.model.step.DefaultStepResultFactory;
import com.airlock.iam.oauth2.api.domain.model.AuthorizationServerId;
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.AuthorizationErrorResult;
import com.airlock.iam.oauth2.domain.model.OAuth2RequestedResponseMode;
import com.airlock.iam.oauth2.domain.model.OAuth2Response;
import com.airlock.iam.oauth2.domain.model.OAuth2ResponseMode;
import com.airlock.iam.oauth2.domain.model.State;
import com.airlock.iam.oauth2.domain.model.authentication.OpenIdConnectLoginHintFlowHandler;
import com.airlock.iam.oauth2.domain.model.authentication.OpenIdConnectUiLocaleFlowHandler;
import com.airlock.iam.oauth2.domain.model.authorization.OAuth2AuthorizationEndpointMode;
import com.airlock.iam.oauth2.domain.model.client.OAuth2Client;
import com.airlock.iam.oauth2.domain.model.client.OAuth2ClientLoader;
import com.airlock.iam.oauth2.domain.model.codegrant.AuthorizationCodeRequestParser;
import com.airlock.iam.oauth2.domain.model.codegrant.OAuth2AuthorizationCodeGrantFlowSessionAttribute;
import com.airlock.iam.oauth2.domain.model.codegrant.OAuth2AuthorizationCodeGrantRequestParseException;
import com.airlock.iam.oauth2.domain.model.codegrant.OAuth2AuthorizationRequest;
import com.airlock.iam.oauth2.domain.model.codegrant.OAuth2AuthorizationRequestAttributes;
import com.airlock.iam.oauth2.domain.model.codegrant.OAuth2OidcAuthorizationRequest;
import com.airlock.iam.oauth2.domain.model.codegrant.OAuth2OidcAuthorizationRequestAttributes;
import com.airlock.iam.oauth2.domain.model.codegrant.OpenIdConnectAuthenticationRequest;
import com.airlock.iam.oauth2.domain.model.codegrant.OpenIdConnectAuthenticationRequestAttributes;
import com.airlock.iam.oauth2.domain.model.codegrant.PromptValue;
import com.airlock.iam.oauth2.domain.model.logging.OAuth2LogTypes;
import com.airlock.iam.oauth2.domain.model.par.OAuth2AuthorizationRequestQuery;
import com.airlock.iam.oauth2.domain.model.par.OAuth2ParRequestRepository;
import com.airlock.iam.oauth2.domain.model.par.OAuth2ParRequestUri;
import com.airlock.iam.oauth2.domain.model.response.OAuth2ErrorResponseBuilder;
import com.airlock.iam.oauth2.domain.model.scopepolicy.ScopePolicyInput;
import com.airlock.iam.oauth2.domain.model.settings.OAuth2OidcAuthorizationCodeGrantSettings;
import com.airlock.iam.oauth2.domain.model.validation.OAuth2Error;
import com.airlock.iam.oauth2.domain.service.OAuth2ApplicationFlowSelection;
import com.airlock.iam.oauth2.domain.service.OpenIdProviderUserAgentStateService;
import com.airlock.iam.oauth2.domain.service.codegrant.CommonAuthorizationCodeRequestValidator;
import com.airlock.iam.oauth2.domain.service.codegrant.OAuth2AuthorizationCodeRequestValidator;
import com.airlock.iam.oauth2.domain.service.codegrant.OpenIdConnectAuthenticationCodeRequestValidator;
import com.airlock.iam.oauth2.domain.service.codegrant.OpenIdConnectUserSessionValidationService;
import com.airlock.iam.oauth2.domain.service.scopepolicy.ScopePolicy;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import jakarta.inject.Provider;
import java.net.URI;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.function.Function;

public class OAuth2AuthorizationService {
   private static final ELogger LOG = ELoggerFactory.getLogger(OAuth2AuthorizationService.class);
   private static final String UI_LOCALE_ADDITIONAL_ATTR_KEY = "uiLocale";
   private static final RedFlag OAUTH_2_CONSENT_REQUIRED = new RedFlag("OAUTH2_CONSENT_REQUIRED");
   private final Provider<AuthorizationServerId> authorizationServerId;
   private final Provider<OAuth2AuthorizationEndpointMode> endpointMode;
   private final Provider<OAuth2AuthorizationCodeRequestValidator> oauth2RequestValidator;
   private final Provider<OpenIdConnectAuthenticationCodeRequestValidator> oidcRequestValidator;
   private final Provider<CommonAuthorizationCodeRequestValidator> commonRequestValidator;
   private final Provider<OAuth2ClientLoader> clientLoader;
   private final Provider<ApplicationAccessService> applicationAccessService;
   private final Provider<OAuth2ApplicationFlowSelection> flowSelection;
   private final Provider<OpenIdConnectUiLocaleFlowHandler> uiLocaleHandler;
   private final Provider<Optional<OpenIdConnectLoginHintFlowHandler>> loginHintHandler;
   private final Provider<OpenIdProviderUserAgentStateService> userAgentStateService;
   private final Provider<OpenIdConnectUserSessionValidationService> userSessionValidationService;
   private final Provider<ScopePolicy> scopePolicy;
   private final Provider<OAuth2OidcAuthorizationCodeGrantSettings> settings;
   private final Provider<OpenIdConnectPromptNoneFlowResultHandler> openIdConnectPromptNoneFlowResultHandler;
   private final Provider<OAuth2ParRequestRepository> parRequestRepository;

   @Inject
   public OAuth2AuthorizationService(
      Provider<AuthorizationServerId> authorizationServerId,
      Provider<OAuth2AuthorizationEndpointMode> endpointMode,
      Provider<OAuth2AuthorizationCodeRequestValidator> oauth2RequestValidator,
      Provider<OpenIdConnectAuthenticationCodeRequestValidator> oidcRequestValidator,
      Provider<CommonAuthorizationCodeRequestValidator> commonRequestValidator,
      Provider<OAuth2ClientLoader> clientLoader,
      Provider<ApplicationAccessService> applicationAccessService,
      Provider<OAuth2ApplicationFlowSelection> flowSelection,
      Provider<OpenIdConnectUiLocaleFlowHandler> uiLocaleHandler,
      Provider<Optional<OpenIdConnectLoginHintFlowHandler>> loginHintHandler,
      Provider<OpenIdProviderUserAgentStateService> userAgentStateService,
      Provider<OpenIdConnectUserSessionValidationService> userSessionValidationService,
      @Named("AUTHORIZATION_GRANT_SCOPE_POLICY") Provider<ScopePolicy> scopePolicy,
      Provider<OAuth2OidcAuthorizationCodeGrantSettings> settings,
      Provider<OpenIdConnectPromptNoneFlowResultHandler> openIdConnectPromptNoneFlowResultHandler,
      Provider<OAuth2ParRequestRepository> parRequestRepository
   ) {
      this.authorizationServerId = authorizationServerId;
      this.endpointMode = endpointMode;
      this.oauth2RequestValidator = oauth2RequestValidator;
      this.oidcRequestValidator = oidcRequestValidator;
      this.commonRequestValidator = commonRequestValidator;
      this.clientLoader = clientLoader;
      this.applicationAccessService = applicationAccessService;
      this.flowSelection = flowSelection;
      this.uiLocaleHandler = uiLocaleHandler;
      this.loginHintHandler = loginHintHandler;
      this.userAgentStateService = userAgentStateService;
      this.userSessionValidationService = userSessionValidationService;
      this.scopePolicy = scopePolicy;
      this.settings = settings;
      this.openIdConnectPromptNoneFlowResultHandler = openIdConnectPromptNoneFlowResultHandler;
      this.parRequestRepository = parRequestRepository;
   }

   public FlowResult access(String authorizationRequestQueryString) {
      return switch ((OAuth2AuthorizationEndpointMode)this.endpointMode.get()) {
         case OIDC -> this.authenticationRequest(authorizationRequestQueryString);
         case OAUTH2 -> this.authorizationRequest(authorizationRequestQueryString);
      };
   }

   FlowResult authenticationRequest(String authenticationRequestQueryString) {
      return this.authenticationRequest(authenticationRequestQueryString, false);
   }

   FlowResult authenticationRequest(String authenticationRequestQueryString, boolean isForwardedPromptNoneRequest) {
      OpenIdConnectAuthenticationRequest authenticationRequest;
      try {
         authenticationRequest = AuthorizationCodeRequestParser.parseOpenIdConnectAuthenticationRequest(authenticationRequestQueryString);
         this.validatePar(authenticationRequest);
         if (authenticationRequest.impliesPushedAuthorizationRequest()) {
            authenticationRequest = this.loadOriginalAuthenticationRequest(authenticationRequest);
         }
      } catch (OAuth2AuthorizationCodeGrantRequestParseException parseException) {
         LOG.warn(OAuth2LogTypes.OAUTH2_MESSAGE, "Invalid OpenID Connect Authentication Code request: " + parseException.getMessage());
         return this.mapParseException(parseException);
      }

      Optional<Locale> updateLocale = ((OpenIdConnectUiLocaleFlowHandler)this.uiLocaleHandler.get())
         .determineLocaleAndUpdateCurrentLocale(authenticationRequest);
      FlowResult flowResult = ((OpenIdConnectAuthenticationCodeRequestValidator)this.oidcRequestValidator.get())
         .validate(authenticationRequest)
         .mapResult(requestAttributes -> this.handleValidatedRequest(requestAttributes, isForwardedPromptNoneRequest))
         .orMapFailure(OAuth2AuthorizationService::failure);
      updateLocale.ifPresent(locale -> flowResult.additionalAttributes().put("uiLocale", locale.toLanguageTag()));
      return flowResult;
   }

   private OpenIdConnectAuthenticationRequest loadOriginalAuthenticationRequest(OpenIdConnectAuthenticationRequest authenticationRequest) throws OAuth2AuthorizationCodeGrantRequestParseException {
      String originalQueryString = this.getPushedAuthorizationRequest(authenticationRequest);
      return AuthorizationCodeRequestParser.parseOpenIdConnectAuthenticationRequest(originalQueryString);
   }

   private String getPushedAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest) throws OAuth2AuthorizationCodeGrantRequestParseException {
      OAuth2ParRequestUri requestURI = authorizationRequest.getRequestURI().orElseThrow();
      Optional<OAuth2AuthorizationRequestQuery> originalQuery = ((OAuth2ParRequestRepository)this.parRequestRepository.get())
         .getAndDelete(
            OAuth2ClientId.oAuth2ClientId(authorizationRequest.getClientId().id()), (AuthorizationServerId)this.authorizationServerId.get(), requestURI
         );
      if (originalQuery.isEmpty()) {
         throw new OAuth2AuthorizationCodeGrantRequestParseException(
            String.format("Pushed Authorization Request for request_uri=%s not found. Maybe it already expired?", LogUtil.logSafeQuote(requestURI))
         );
      } else {
         return originalQuery.get().queryString();
      }
   }

   private FlowResult authorizationRequest(String authorizationRequestQueryString) {
      return this.parseAndValidate(authorizationRequestQueryString).mapResult(this::handleValidatedRequest).orMapFailure(Function.identity());
   }

   private FlowResult handleValidatedRequest(OpenIdConnectAuthenticationRequestAttributes authenticationRequest, boolean isForwardedPromptNoneRequest) {
      return this.applyScopePolicyAndValidateResult(authenticationRequest)
         .mapResult(
            scopes -> {
               FlowResult result = this.access(authenticationRequest, scopes, isForwardedPromptNoneRequest);
               if (authenticationRequest.prompt().contains(PromptValue.NONE) && !result.flowCompletedSuccessfully()) {
                  return ((OpenIdConnectPromptNoneFlowResultHandler)this.openIdConnectPromptNoneFlowResultHandler.get())
                     .handleFailedFlowResultForPromptNone(
                        result,
                        (errorCode, errorDescription) -> failure(
                           OAuth2AuthorizationService.OAuth2AuthorizationEndpointErrors.OAUTH2_AUTHORIZATION_SERVER_INVALID_AUTHORIZATION_REQUEST,
                           this.oauth2ErrorResult(authenticationRequest, errorCode, errorDescription)
                        )
                     );
               }

               this.handleLoginHintAfterAccess(result, authenticationRequest);
               return result;
            }
         )
         .orMapFailure(OAuth2AuthorizationService::failure);
   }

   private FlowResult handleValidatedRequest(OAuth2AuthorizationRequestAttributes authorizationRequest) {
      return this.applyScopePolicyAndValidateResult(authorizationRequest)
         .mapResult(scopes -> this.access(authorizationRequest, scopes))
         .orMapFailure(OAuth2AuthorizationService::failure);
   }

   private Result<List<OAuth2Scope>, AuthorizationErrorResult> applyScopePolicyAndValidateResult(OAuth2OidcAuthorizationRequestAttributes request) {
      OAuth2Client oAuth2Client = ((OAuth2ClientLoader)this.clientLoader.get()).loadClient(request.clientId());
      List<OAuth2Scope> scopes = this.applyScopePolicy(request, oAuth2Client);
      if (!((OAuth2OidcAuthorizationCodeGrantSettings)this.settings.get()).isAllowEmptyScope()
         && scopes.isEmpty()
         && oAuth2Client.alwaysGrantedScopes().isEmpty()) {
         LOG.info(OAuth2LogTypes.OAUTH2_MESSAGE, "Invalid OAuth 2.0 / OpenID Connect authorization request: empty requested scope after applying scope policy");
         return Result.failure(this.emptyScopeErrorResult(request));
      } else {
         return Result.result(scopes);
      }
   }

   private AuthorizationErrorResult emptyScopeErrorResult(OAuth2OidcAuthorizationRequestAttributes request) {
      return this.oauth2ErrorResult(request, OAuth2Error.INVALID_SCOPE, "Invalid requested scope");
   }

   private AuthorizationErrorResult oauth2ErrorResult(OAuth2OidcAuthorizationRequestAttributes request, OAuth2Error oAuth2Error, String errorDescription) {
      return this.errorResponseBuilder(request)
         .mapResult(builder -> builder.buildResponse(oAuth2Error, errorDescription))
         .mapResult(AuthorizationErrorResult::errorResult)
         .orMapFailure(Function.identity());
   }

   private List<OAuth2Scope> applyScopePolicy(OAuth2OidcAuthorizationRequestAttributes request, OAuth2Client oAuth2Client) {
      return ((ScopePolicy)this.scopePolicy.get()).processScopes(new ScopePolicyInput(request.requestedScopes(), oAuth2Client.allowedScopes()));
   }

   private void handleLoginHintBeforeAccess(OpenIdConnectAuthenticationRequestAttributes authenticationRequest) {
      if (((Optional)this.loginHintHandler.get()).isPresent() && authenticationRequest.loginHint().isPresent()) {
         ((OpenIdConnectLoginHintFlowHandler)((Optional)this.loginHintHandler.get()).get()).beforeAccess(authenticationRequest.loginHint().get());
      }
   }

   private void handleLoginHintAfterAccess(FlowResult flowResult, OpenIdConnectAuthenticationRequestAttributes authenticationRequest) {
      if (((Optional)this.loginHintHandler.get()).isPresent() && authenticationRequest.loginHint().isPresent()) {
         ((OpenIdConnectLoginHintFlowHandler)((Optional)this.loginHintHandler.get()).get()).afterAccess(flowResult, authenticationRequest.loginHint().get());
      }
   }

   private Result<OAuth2AuthorizationRequestAttributes, FlowResult> parseAndValidate(String authorizationRequestQueryString) {
      OAuth2AuthorizationRequest authorizationRequest;
      try {
         authorizationRequest = AuthorizationCodeRequestParser.parseOAuth2AuthorizationRequest(authorizationRequestQueryString);
         this.validatePar(authorizationRequest);
         if (authorizationRequest.impliesPushedAuthorizationRequest()) {
            authorizationRequest = this.loadOriginalAuthorizationRequest(authorizationRequest);
         }
      } catch (OAuth2AuthorizationCodeGrantRequestParseException parseException) {
         LOG.warn(OAuth2LogTypes.OAUTH2_MESSAGE, "Invalid OAuth 2.0 Authorization Code request: " + parseException.getMessage());
         return Result.failure(this.mapParseException(parseException));
      }

      return ((OAuth2AuthorizationCodeRequestValidator)this.oauth2RequestValidator.get())
         .validate(authorizationRequest)
         .or(authorizationErrorResult -> Result.failure(failure(authorizationErrorResult)));
   }

   private OAuth2AuthorizationRequest loadOriginalAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest) throws OAuth2AuthorizationCodeGrantRequestParseException {
      String originalQueryString = this.getPushedAuthorizationRequest(authorizationRequest);
      return AuthorizationCodeRequestParser.parseOAuth2AuthorizationRequest(originalQueryString);
   }

   private void validatePar(OAuth2AuthorizationRequest authorizationRequest) throws OAuth2AuthorizationCodeGrantRequestParseException {
      if (this.parRequestedButNotSupported(authorizationRequest)) {
         parException(OAuth2Error.REQUEST_URI_NOT_SUPPORTED, "'request_uri' parameter is not supported", authorizationRequest);
      }

      if (this.parEnforcedButNotRequested(authorizationRequest)) {
         parException(
            OAuth2Error.INVALID_REQUEST, "'request_uri' parameter is absent but only pushed authorization requests are supported", authorizationRequest
         );
      }
   }

   private boolean parEnforcedButNotRequested(OAuth2OidcAuthorizationRequest authenticationRequest) {
      return ((OAuth2OidcAuthorizationCodeGrantSettings)this.settings.get()).isPushedAuthorizationRequestsRequired()
         && !authenticationRequest.impliesPushedAuthorizationRequest();
   }

   private boolean parRequestedButNotSupported(OAuth2OidcAuthorizationRequest authenticationRequest) {
      return authenticationRequest.impliesPushedAuthorizationRequest()
         && !((OAuth2OidcAuthorizationCodeGrantSettings)this.settings.get()).isPushedAuthorizationRequestsEnabled();
   }

   private static void parException(OAuth2Error error, String errorDescription, OAuth2OidcAuthorizationRequest authenticationRequest) throws OAuth2AuthorizationCodeGrantRequestParseException {
      OAuth2AuthorizationCodeGrantRequestParseException.OAuth2AuthorizationCodeGrantRequestParseExceptionBuilder parExceptionBuilder = OAuth2AuthorizationCodeGrantRequestParseException.builder()
         .message(errorDescription)
         .errorDescription(errorDescription)
         .clientId(authenticationRequest.getClientId())
         .error(error);
      authenticationRequest.getRedirectURI().ifPresent(parExceptionBuilder::redirectUri);
      authenticationRequest.getState().ifPresent(parExceptionBuilder::state);
      authenticationRequest.getRequestedResponseMode().ifPresent(parExceptionBuilder::responseMode);
      throw parExceptionBuilder.build();
   }

   private FlowResult access(OAuth2AuthorizationRequestAttributes request, List<OAuth2Scope> validatedScopes) {
      ApplicationId targetFlowId = ((OAuth2ApplicationFlowSelection)this.flowSelection.get()).select(List.of(), request.clientId());
      LOG.info(
         "OAuth 2.0 Authorization Code Flow initiated by "
            + LogUtil.quote(request.clientId().id())
            + ". Starting authentication flow "
            + LogUtil.quote(targetFlowId)
            + "."
      );
      return ((ApplicationAccessService)this.applicationAccessService.get())
         .startNewFlowForApplication(targetFlowId, this.storeAuthorizationRequestAndRaiseConsentRequiredRedFlag(request, validatedScopes));
   }

   private FlowResult access(OpenIdConnectAuthenticationRequestAttributes request, List<OAuth2Scope> validatedScopes, boolean isForwardedPromptNoneRequest) {
      Optional<FlowResult> currentUserSessionValidationResult = this.validateCurrentUserSession(request);
      if (currentUserSessionValidationResult.isPresent()) {
         return currentUserSessionValidationResult.get();
      }

      ApplicationId targetFlowId = ((OAuth2ApplicationFlowSelection)this.flowSelection.get()).select(request.acrValues(), request.clientId());
      LOG.info(
         "OpenID Connect Authentication Code Flow initiated by "
            + LogUtil.quote(request.clientId().id())
            + ". Starting authentication flow "
            + LogUtil.quote(targetFlowId)
            + "."
      );
      return ((ApplicationAccessService)this.applicationAccessService.get())
         .startNewFlowForApplication(
            targetFlowId, this.storeLoginHintAndAuthenticationRequestAndRaiseConsentRequiredRedFlag(request, validatedScopes, isForwardedPromptNoneRequest)
         );
   }

   private Optional<FlowResult> validateCurrentUserSession(OpenIdConnectAuthenticationRequestAttributes request) {
      if (((OpenIdConnectUserSessionValidationService)this.userSessionValidationService.get()).isReauthenticationRequired(request)) {
         LOG.info("OpenID Connect Authentication Request requires reauthentication of user. Authentication flow not started.");
         return Optional.of(failure(OAuth2AuthorizationService.OAuth2AuthorizationEndpointErrors.OAUTH2_AUTHORIZATION_SERVER_REAUTHENTICATION_REQUIRED));
      } else {
         return Optional.empty();
      }
   }

   private InternalApplicationAccessService.FlowSessionUpdater storeAuthorizationRequestAndRaiseConsentRequiredRedFlag(
      OAuth2AuthorizationRequestAttributes authorizationRequest, List<OAuth2Scope> validatedScopes
   ) {
      return flowSession -> {
         flowSession.flowAttributes()
            .putOrMergeAttribute(
               new OAuth2AuthorizationCodeGrantFlowSessionAttribute(
                  (AuthorizationServerId)this.authorizationServerId.get(), authorizationRequest, validatedScopes
               )
            );
         flowSession.raiseRedFlag(OAUTH_2_CONSENT_REQUIRED);
      };
   }

   private InternalApplicationAccessService.FlowSessionUpdater storeLoginHintAndAuthenticationRequestAndRaiseConsentRequiredRedFlag(
      OpenIdConnectAuthenticationRequestAttributes authenticationRequest, List<OAuth2Scope> validatedScopes, boolean isForwardedPromptNoneRequest
   ) {
      return flowSession -> {
         this.handleLoginHintBeforeAccess(authenticationRequest);
         flowSession.flowAttributes()
            .putOrMergeAttribute(
               new OAuth2AuthorizationCodeGrantFlowSessionAttribute(
                  (AuthorizationServerId)this.authorizationServerId.get(), authenticationRequest, validatedScopes, isForwardedPromptNoneRequest
               )
            );
         flowSession.raiseRedFlag(OAUTH_2_CONSENT_REQUIRED);
      };
   }

   private FlowResult mapParseException(OAuth2AuthorizationCodeGrantRequestParseException parseException) {
      return parseException.getClientId()
         .flatMap(((OAuth2ClientLoader)this.clientLoader.get())::tryLoadClient)
         .flatMap(client -> this.mapParseException(client, parseException))
         .orElseGet(() -> {
            LOG.info("Unknown OAuth 2.0 Client in invalid request");
            return failure(OAuth2AuthorizationService.OAuth2AuthorizationEndpointErrors.OAUTH2_AUTHORIZATION_SERVER_INVALID_AUTHORIZATION_REQUEST);
         });
   }

   private Optional<FlowResult> mapParseException(OAuth2Client client, OAuth2AuthorizationCodeGrantRequestParseException parseException) {
      OAuth2ResponseMode responseMode = parseException.getResponseMode()
         .flatMap(OAuth2RequestedResponseMode::toSupportedResponseMode)
         .orElse(OAuth2ResponseMode.QUERY_STRING);
      Optional<URI> redirectUri = ((CommonAuthorizationCodeRequestValidator)this.commonRequestValidator.get())
         .validateClientRedirectUri(client, parseException.getRedirectUri(), parseException.getResponseMode());
      return redirectUri.<OAuth2Response>flatMap(
            uri -> this.errorResponseFromValidatedParseException(
               client.clientId(), uri, responseMode, parseException.getState(), parseException.getError(), parseException.getErrorDescription()
            )
         )
         .map(AuthorizationErrorResult::errorResult)
         .map(OAuth2AuthorizationService::failure);
   }

   private Optional<OAuth2Response> errorResponseFromValidatedParseException(
      OAuth2ClientId clientId,
      URI redirectUri,
      OAuth2ResponseMode responseMode,
      Optional<State> state,
      Optional<OAuth2Error> errorCode,
      Optional<String> errorDescription
   ) {
      OAuth2ErrorResponseBuilder errorResponseBuilder = OAuth2ErrorResponseBuilder.errorResponseBuilder(
         ((OpenIdProviderUserAgentStateService)this.userAgentStateService.get()).state(), clientId, redirectUri, state, responseMode
      );
      return errorCode.map(
         error -> errorDescription.<OAuth2Response>map(description -> errorResponseBuilder.buildResponse(error, description))
            .orElse(errorResponseBuilder.buildResponse(error))
      );
   }

   private static FlowResult failure(StepError error) {
      return FlowResultImpl.of(new DefaultStepResultFactory().fail(error).build());
   }

   private static FlowResult failure(AuthorizationErrorResult errorResult) {
      return failure(OAuth2AuthorizationService.OAuth2AuthorizationEndpointErrors.OAUTH2_AUTHORIZATION_SERVER_INVALID_AUTHORIZATION_REQUEST, errorResult);
   }

   private static FlowResultImpl failure(OAuth2AuthorizationService.OAuth2AuthorizationEndpointErrors error, AuthorizationErrorResult errorResult) {
      StepResultBuilder stepResultBuilder = new DefaultStepResultFactory().fail(error);
      if (errorResult.getErrorRedirect().isPresent()) {
         stepResultBuilder.addAdditionalAttribute("clientRedirectUri", errorResult.getErrorRedirect().get().redirectUri());
         errorResult.getErrorRedirect().get().formPostParams().ifPresent(params -> stepResultBuilder.addAdditionalAttribute("formPostParameters", params));
      }

      return FlowResultImpl.of(stepResultBuilder.build());
   }

   private Result<OAuth2ErrorResponseBuilder, AuthorizationErrorResult> errorResponseBuilder(OAuth2OidcAuthorizationRequestAttributes request) {
      Optional<OAuth2Client> client = ((OAuth2ClientLoader)this.clientLoader.get()).tryLoadClient(request.clientId());
      if (client.isEmpty()) {
         LOG.info(
            OAuth2LogTypes.OAUTH2_MESSAGE,
            String.format("Invalid OAuth 2.0 request: Client with ID %s not found", LogUtil.logSafeQuote(request.clientId().id()))
         );
         return Result.failure(AuthorizationErrorResult.errorResult());
      } else {
         return Result.result(
            OAuth2ErrorResponseBuilder.errorResponseBuilder(
               ((OpenIdProviderUserAgentStateService)this.userAgentStateService.get()).state(),
               request.clientId(),
               request.effectiveRedirectUri(),
               request.state(),
               request.responseMode()
            )
         );
      }
   }

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

   public enum OAuth2AuthorizationEndpointErrors implements StepError {
      OAUTH2_AUTHORIZATION_SERVER_INVALID_AUTHORIZATION_REQUEST,
      OAUTH2_AUTHORIZATION_SERVER_REAUTHENTICATION_REQUIRED;
   }

   @SuppressFBWarnings(justification = "generated code")
   public static class OAuth2AuthorizationServiceBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private Provider<AuthorizationServerId> authorizationServerId;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<OAuth2AuthorizationEndpointMode> endpointMode;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<OAuth2AuthorizationCodeRequestValidator> oauth2RequestValidator;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<OpenIdConnectAuthenticationCodeRequestValidator> oidcRequestValidator;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<CommonAuthorizationCodeRequestValidator> commonRequestValidator;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<OAuth2ClientLoader> clientLoader;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<ApplicationAccessService> applicationAccessService;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<OAuth2ApplicationFlowSelection> flowSelection;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<OpenIdConnectUiLocaleFlowHandler> uiLocaleHandler;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<Optional<OpenIdConnectLoginHintFlowHandler>> loginHintHandler;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<OpenIdProviderUserAgentStateService> userAgentStateService;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<OpenIdConnectUserSessionValidationService> userSessionValidationService;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<ScopePolicy> scopePolicy;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<OAuth2OidcAuthorizationCodeGrantSettings> settings;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<OpenIdConnectPromptNoneFlowResultHandler> openIdConnectPromptNoneFlowResultHandler;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<OAuth2ParRequestRepository> parRequestRepository;

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

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2AuthorizationService.OAuth2AuthorizationServiceBuilder authorizationServerId(Provider<AuthorizationServerId> authorizationServerId) {
         this.authorizationServerId = authorizationServerId;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2AuthorizationService.OAuth2AuthorizationServiceBuilder endpointMode(Provider<OAuth2AuthorizationEndpointMode> endpointMode) {
         this.endpointMode = endpointMode;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2AuthorizationService.OAuth2AuthorizationServiceBuilder oauth2RequestValidator(
         Provider<OAuth2AuthorizationCodeRequestValidator> oauth2RequestValidator
      ) {
         this.oauth2RequestValidator = oauth2RequestValidator;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2AuthorizationService.OAuth2AuthorizationServiceBuilder oidcRequestValidator(
         Provider<OpenIdConnectAuthenticationCodeRequestValidator> oidcRequestValidator
      ) {
         this.oidcRequestValidator = oidcRequestValidator;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2AuthorizationService.OAuth2AuthorizationServiceBuilder commonRequestValidator(
         Provider<CommonAuthorizationCodeRequestValidator> commonRequestValidator
      ) {
         this.commonRequestValidator = commonRequestValidator;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2AuthorizationService.OAuth2AuthorizationServiceBuilder clientLoader(Provider<OAuth2ClientLoader> clientLoader) {
         this.clientLoader = clientLoader;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2AuthorizationService.OAuth2AuthorizationServiceBuilder applicationAccessService(Provider<ApplicationAccessService> applicationAccessService) {
         this.applicationAccessService = applicationAccessService;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2AuthorizationService.OAuth2AuthorizationServiceBuilder flowSelection(Provider<OAuth2ApplicationFlowSelection> flowSelection) {
         this.flowSelection = flowSelection;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2AuthorizationService.OAuth2AuthorizationServiceBuilder uiLocaleHandler(Provider<OpenIdConnectUiLocaleFlowHandler> uiLocaleHandler) {
         this.uiLocaleHandler = uiLocaleHandler;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2AuthorizationService.OAuth2AuthorizationServiceBuilder loginHintHandler(
         Provider<Optional<OpenIdConnectLoginHintFlowHandler>> loginHintHandler
      ) {
         this.loginHintHandler = loginHintHandler;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2AuthorizationService.OAuth2AuthorizationServiceBuilder userAgentStateService(
         Provider<OpenIdProviderUserAgentStateService> userAgentStateService
      ) {
         this.userAgentStateService = userAgentStateService;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2AuthorizationService.OAuth2AuthorizationServiceBuilder userSessionValidationService(
         Provider<OpenIdConnectUserSessionValidationService> userSessionValidationService
      ) {
         this.userSessionValidationService = userSessionValidationService;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2AuthorizationService.OAuth2AuthorizationServiceBuilder scopePolicy(Provider<ScopePolicy> scopePolicy) {
         this.scopePolicy = scopePolicy;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2AuthorizationService.OAuth2AuthorizationServiceBuilder settings(Provider<OAuth2OidcAuthorizationCodeGrantSettings> settings) {
         this.settings = settings;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2AuthorizationService.OAuth2AuthorizationServiceBuilder openIdConnectPromptNoneFlowResultHandler(
         Provider<OpenIdConnectPromptNoneFlowResultHandler> openIdConnectPromptNoneFlowResultHandler
      ) {
         this.openIdConnectPromptNoneFlowResultHandler = openIdConnectPromptNoneFlowResultHandler;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2AuthorizationService.OAuth2AuthorizationServiceBuilder parRequestRepository(Provider<OAuth2ParRequestRepository> parRequestRepository) {
         this.parRequestRepository = parRequestRepository;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2AuthorizationService build() {
         return new OAuth2AuthorizationService(
            this.authorizationServerId,
            this.endpointMode,
            this.oauth2RequestValidator,
            this.oidcRequestValidator,
            this.commonRequestValidator,
            this.clientLoader,
            this.applicationAccessService,
            this.flowSelection,
            this.uiLocaleHandler,
            this.loginHintHandler,
            this.userAgentStateService,
            this.userSessionValidationService,
            this.scopePolicy,
            this.settings,
            this.openIdConnectPromptNoneFlowResultHandler,
            this.parRequestRepository
         );
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "OAuth2AuthorizationService.OAuth2AuthorizationServiceBuilder(authorizationServerId="
            + this.authorizationServerId
            + ", endpointMode="
            + this.endpointMode
            + ", oauth2RequestValidator="
            + this.oauth2RequestValidator
            + ", oidcRequestValidator="
            + this.oidcRequestValidator
            + ", commonRequestValidator="
            + this.commonRequestValidator
            + ", clientLoader="
            + this.clientLoader
            + ", applicationAccessService="
            + this.applicationAccessService
            + ", flowSelection="
            + this.flowSelection
            + ", uiLocaleHandler="
            + this.uiLocaleHandler
            + ", loginHintHandler="
            + this.loginHintHandler
            + ", userAgentStateService="
            + this.userAgentStateService
            + ", userSessionValidationService="
            + this.userSessionValidationService
            + ", scopePolicy="
            + this.scopePolicy
            + ", settings="
            + this.settings
            + ", openIdConnectPromptNoneFlowResultHandler="
            + this.openIdConnectPromptNoneFlowResultHandler
            + ", parRequestRepository="
            + this.parRequestRepository
            + ")";
      }
   }
}
