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

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.oauth2.application.configuration.OAuth2ConsentConfig;
import com.airlock.iam.oauth2.domain.model.AuthorizationErrorResult;
import com.airlock.iam.oauth2.domain.model.OAuth2RequestedResponseMode;
import com.airlock.iam.oauth2.domain.model.OAuth2ResponseMode;
import com.airlock.iam.oauth2.domain.model.authorization.OAuth2ResponseType;
import com.airlock.iam.oauth2.domain.model.codegrant.OAuth2AuthorizationRequestAttributes;
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.logger.IntegrationLoggerFactory;
import com.airlock.iam.oauth2.domain.model.logging.OAuth2LogTypes;
import com.airlock.iam.oauth2.domain.model.response.OAuth2ErrorResponseBuilder;
import com.airlock.iam.oauth2.domain.model.settings.HybridFlowSettings;
import com.airlock.iam.oauth2.domain.model.settings.OpenIdConnectAuthorizationCodeGrantSettings;
import com.airlock.iam.oauth2.domain.model.validation.OAuth2Error;
import com.airlock.iam.oauth2.domain.service.OpenIdProviderUserAgentStateService;
import com.google.common.annotations.VisibleForTesting;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

public class OpenIdConnectAuthenticationRequestValidatorImpl implements OpenIdConnectAuthenticationCodeRequestValidator {
   private final Provider<CommonAuthorizationCodeRequestValidator> commonAuthorizationCodeRequestValidator;
   private final Provider<Optional<OAuth2ConsentConfig>> consentConfig;
   private final Provider<OpenIdProviderUserAgentStateService> sessionStateService;
   private final Provider<OpenIdConnectAuthorizationCodeGrantSettings> settings;
   private final ELogger logger;

   @Inject
   public OpenIdConnectAuthenticationRequestValidatorImpl(
      Provider<IntegrationLoggerFactory> loggerFactory,
      Provider<CommonAuthorizationCodeRequestValidator> oauth2RequestValidator,
      Provider<Optional<OAuth2ConsentConfig>> consentConfig,
      Provider<OpenIdProviderUserAgentStateService> sessionStateService,
      Provider<OpenIdConnectAuthorizationCodeGrantSettings> settings
   ) {
      this.commonAuthorizationCodeRequestValidator = oauth2RequestValidator;
      this.consentConfig = consentConfig;
      this.sessionStateService = sessionStateService;
      this.settings = settings;
      this.logger = getLogger(loggerFactory);
   }

   @Override
   public Result<OpenIdConnectAuthenticationRequestAttributes, AuthorizationErrorResult> validate(OpenIdConnectAuthenticationRequest request) {
      try {
         Result<OAuth2AuthorizationRequestAttributes, AuthorizationErrorResult> result = ((CommonAuthorizationCodeRequestValidator)this.commonAuthorizationCodeRequestValidator
               .get())
            .validate(request, this::validateResponseTypeAndMode);
         if (result.isFailure()) {
            return Result.failure(result.getFailure());
         }

         OAuth2ErrorResponseBuilder errorResponseBuilder = OAuth2ErrorResponseBuilder.errorResponseBuilder(
            ((OpenIdProviderUserAgentStateService)this.sessionStateService.get()).state(),
            request.getClientId(),
            result.get().effectiveRedirectUri(),
            result.get().state(),
            result.get().responseMode()
         );
         this.validateRequestObject(request, errorResponseBuilder);
         List<PromptValue> promptValues = this.validatePrompt(request, errorResponseBuilder);
         return Result.result(
            new OpenIdConnectAuthenticationRequestAttributes(
               result.get(), request.getAcr(), request.getNonce(), promptValues, request.getLoginHint(), request.getMaxAge()
            )
         );
      } catch (OAuth2AuthorizationRequestValidationException e) {
         return Result.failure(e.getErrorResult().getErrorRedirect().map(AuthorizationErrorResult::errorResult).orElse(AuthorizationErrorResult.errorResult()));
      }
   }

   private void validateRequestObject(OpenIdConnectAuthenticationRequest request, OAuth2ErrorResponseBuilder errorResponseBuilder) throws OAuth2AuthorizationRequestValidationException {
      if (request.hasRequestObject()) {
         this.logger.info(OAuth2LogTypes.OAUTH2_MESSAGE, "Invalid OIDC Authentication Code Request: 'request' parameter is not supported");
         throw new OAuth2AuthorizationRequestValidationException(
            AuthorizationErrorResult.errorResult(errorResponseBuilder.buildResponse(OAuth2Error.REQUEST_NOT_SUPPORTED, "'request' parameter is not supported"))
         );
      }
   }

   private List<PromptValue> validatePrompt(OpenIdConnectAuthenticationRequest request, OAuth2ErrorResponseBuilder errorResponseBuilder) throws OAuth2AuthorizationRequestValidationException {
      List<String> prompts = request.getPrompts();
      if (prompts.isEmpty()) {
         return Collections.emptyList();
      }

      List<PromptValue> promptValues = this.validatePromptParamIsValid(errorResponseBuilder, prompts);
      this.validatePromptParamNone(errorResponseBuilder, promptValues);
      this.validatePromptParamConsent(errorResponseBuilder, promptValues);
      return promptValues;
   }

   private List<PromptValue> validatePromptParamIsValid(OAuth2ErrorResponseBuilder errorResponseBuilder, List<String> prompts) throws OAuth2AuthorizationRequestValidationException {
      String invalidPromptErrorMessage = "Invalid OIDC Authentication Code Request: Prompt contains invalid values. Only "
         + allowedPromptValues()
         + " are allowed.";
      if (!prompts.stream().allMatch(PromptValue::isSupported)) {
         this.logger.info(OAuth2LogTypes.OAUTH2_MESSAGE, invalidPromptErrorMessage);
         throw new OAuth2AuthorizationRequestValidationException(
            AuthorizationErrorResult.errorResult(errorResponseBuilder.buildResponse(OAuth2Error.INVALID_REQUEST, invalidPromptErrorMessage))
         );
      } else {
         return prompts.stream().map(PromptValue::fromString).toList();
      }
   }

   private void validatePromptParamNone(OAuth2ErrorResponseBuilder errorResponseBuilder, List<PromptValue> prompts) throws OAuth2AuthorizationRequestValidationException {
      if (prompts.contains(PromptValue.NONE)) {
         String errorMessage = "Invalid OIDC Authentication Code Request: Client suppressed user interaction (prompt="
            + PromptValue.NONE.getParamValue()
            + "), but consent is required.";
         if (((Optional)this.consentConfig.get()).isPresent()) {
            this.logger.info(OAuth2LogTypes.OAUTH2_MESSAGE, errorMessage);
            throw new OAuth2AuthorizationRequestValidationException(
               AuthorizationErrorResult.errorResult(errorResponseBuilder.buildResponse(OAuth2Error.CONSENT_REQUIRED, errorMessage))
            );
         }
      }
   }

   private void validatePromptParamConsent(OAuth2ErrorResponseBuilder errorResponseBuilder, List<PromptValue> prompts) throws OAuth2AuthorizationRequestValidationException {
      String noConsentRequiredPromptErrorMessage = "Invalid OIDC Authentication Code Request: Client enforced consent page (prompt="
         + PromptValue.CONSENT
         + "), but 'No Consent' is configured.";
      if (prompts.contains(PromptValue.CONSENT) && ((Optional)this.consentConfig.get()).isEmpty()) {
         this.logger.info(OAuth2LogTypes.OAUTH2_MESSAGE, noConsentRequiredPromptErrorMessage);
         throw new OAuth2AuthorizationRequestValidationException(
            AuthorizationErrorResult.errorResult(errorResponseBuilder.buildResponse(OAuth2Error.CONSENT_REQUIRED, noConsentRequiredPromptErrorMessage))
         );
      }
   }

   private static String allowedPromptValues() {
      return Arrays.stream(PromptValue.values())
         .filter(PromptValue::isSupported)
         .map(PromptValue::getParamValue)
         .map(value -> "'" + value + "'")
         .collect(Collectors.joining(", "));
   }

   @VisibleForTesting
   ResponseModeAndTypeValidator.ValidatedResponseModeAndType validateResponseTypeAndMode(
      Optional<OAuth2RequestedResponseMode> requestedResponseMode,
      Optional<OAuth2ResponseType> requestedResponseType,
      OAuth2ErrorResponseBuilder errorResponseBuilder
   ) throws OAuth2AuthorizationRequestValidationException {
      if (requestedResponseType.isEmpty()) {
         this.logger.warn(OAuth2LogTypes.OAUTH2_MESSAGE, "Invalid OIDC Authentication Code request: missing response type.");
         throw new OAuth2AuthorizationRequestValidationException(
            AuthorizationErrorResult.errorResult(errorResponseBuilder.buildResponse(OAuth2Error.UNSUPPORTED_RESPONSE_TYPE, "Missing response type"))
         );
      }

      OAuth2ResponseType oAuth2ResponseType = requestedResponseType.get();
      if (oAuth2ResponseType.impliesHybridFlow()) {
         return this.validateHybridFlowResponseTypeAndMode(oAuth2ResponseType, requestedResponseMode, errorResponseBuilder);
      }

      if (oAuth2ResponseType.impliesCodeFlow()) {
         return new ResponseModeAndTypeValidator.ValidatedResponseModeAndType(
            this.validateAuthCodeFlowResponseMode(requestedResponseMode, errorResponseBuilder), oAuth2ResponseType
         );
      }

      this.logger
         .warn(
            OAuth2LogTypes.OAUTH2_MESSAGE,
            "Invalid OIDC Authentication Code request: Invalid response type: " + LogUtil.logSafeQuote(oAuth2ResponseType.responseType())
         );
      throw new OAuth2AuthorizationRequestValidationException(
         AuthorizationErrorResult.errorResult(
            errorResponseBuilder.buildResponse(
               OAuth2Error.UNSUPPORTED_RESPONSE_TYPE, "Invalid response type: " + LogUtil.logSafeQuote(oAuth2ResponseType.responseType())
            )
         )
      );
   }

   private ResponseModeAndTypeValidator.ValidatedResponseModeAndType validateHybridFlowResponseTypeAndMode(
      OAuth2ResponseType oAuth2ResponseType, Optional<OAuth2RequestedResponseMode> requestedResponseMode, OAuth2ErrorResponseBuilder errorResponseBuilder
   ) throws OAuth2AuthorizationRequestValidationException {
      if (((OpenIdConnectAuthorizationCodeGrantSettings)this.settings.get()).getHybridFlowSettings().isEmpty()) {
         this.logger
            .warn(
               OAuth2LogTypes.OAUTH2_MESSAGE,
               "Invalid OIDC Authentication Code request: hybrid flow disabled but response type implies it: "
                  + LogUtil.logSafeQuote(oAuth2ResponseType.responseType())
            );
         throw new OAuth2AuthorizationRequestValidationException(
            AuthorizationErrorResult.errorResult(
               errorResponseBuilder.buildResponse(
                  OAuth2Error.UNSUPPORTED_RESPONSE_TYPE, "Invalid response type: " + LogUtil.logSafeQuote(oAuth2ResponseType.responseType())
               )
            )
         );
      } else {
         this.validateAccessTokenResponseType(oAuth2ResponseType, errorResponseBuilder);
         this.validateIdTokenResponseType(oAuth2ResponseType, errorResponseBuilder);
         OAuth2ResponseMode validatedResponseMode = this.validateHybridFlowResponseMode(requestedResponseMode, errorResponseBuilder);
         return new ResponseModeAndTypeValidator.ValidatedResponseModeAndType(validatedResponseMode, oAuth2ResponseType);
      }
   }

   private void validateIdTokenResponseType(OAuth2ResponseType oAuth2ResponseType, OAuth2ErrorResponseBuilder errorResponseBuilder) throws OAuth2AuthorizationRequestValidationException {
      if (isIdTokenRequested(oAuth2ResponseType) && !this.isHybridFlowIdTokenConfigured()) {
         this.logger.warn(OAuth2LogTypes.OAUTH2_MESSAGE, "Invalid OIDC Hybrid Flow request: ID token requested but not configured.");
         throw new OAuth2AuthorizationRequestValidationException(
            AuthorizationErrorResult.errorResult(
               errorResponseBuilder.buildResponse(
                  OAuth2Error.UNSUPPORTED_RESPONSE_TYPE, "Invalid response type: " + LogUtil.logSafeQuote(oAuth2ResponseType.responseType())
               )
            )
         );
      }
   }

   private OAuth2ResponseMode validateAuthCodeFlowResponseMode(
      Optional<OAuth2RequestedResponseMode> requestedResponseMode, OAuth2ErrorResponseBuilder errorResponseBuilder
   ) throws OAuth2AuthorizationRequestValidationException {
      OAuth2RequestedResponseMode responseMode = requestedResponseMode.orElse(OAuth2RequestedResponseMode.QUERY_STRING);
      if (!((OpenIdConnectAuthorizationCodeGrantSettings)this.settings.get()).getSupportedResponseModes().isSupportedInAuthCodeFlow(responseMode)) {
         this.logger
            .info(
               OAuth2LogTypes.OAUTH2_MESSAGE,
               String.format(
                  "Invalid OpenID Connect authorization code flow authorization request: unsupported response mode requested: %s",
                  LogUtil.logSafeQuote(responseMode.value())
               )
            );
         throw new OAuth2AuthorizationRequestValidationException(
            AuthorizationErrorResult.errorResult(
               errorResponseBuilder.buildResponse(OAuth2Error.UNSUPPORTED_RESPONSE_MODE, "Invalid response mode: " + LogUtil.logSafeQuote(responseMode.value()))
            )
         );
      } else {
         return responseMode.toSupportedResponseMode().orElseThrow();
      }
   }

   private OAuth2ResponseMode validateHybridFlowResponseMode(
      Optional<OAuth2RequestedResponseMode> requestedResponseMode, OAuth2ErrorResponseBuilder errorResponseBuilder
   ) throws OAuth2AuthorizationRequestValidationException {
      OAuth2RequestedResponseMode responseMode = requestedResponseMode.orElse(OAuth2RequestedResponseMode.FRAGMENT);
      if (((OpenIdConnectAuthorizationCodeGrantSettings)this.settings.get()).getSupportedResponseModes().isSupportedInHybridFlow(responseMode)
         && !responseMode.toSupportedResponseMode().isEmpty()) {
         return responseMode.toSupportedResponseMode().orElseThrow();
      }

      this.logger
         .info(
            OAuth2LogTypes.OAUTH2_MESSAGE,
            String.format(
               "Invalid OpenID Connect hybrid flow authorization request: unsupported response mode requested: %s", LogUtil.logSafeQuote(responseMode.value())
            )
         );
      throw new OAuth2AuthorizationRequestValidationException(
         AuthorizationErrorResult.errorResult(
            errorResponseBuilder.buildResponse(OAuth2Error.UNSUPPORTED_RESPONSE_MODE, "Invalid response mode: " + LogUtil.logSafeQuote(responseMode.value()))
         )
      );
   }

   private boolean isHybridFlowIdTokenConfigured() {
      return ((OpenIdConnectAuthorizationCodeGrantSettings)this.settings.get())
         .getHybridFlowSettings()
         .flatMap(HybridFlowSettings::getIdTokenSettings)
         .isPresent();
   }

   private void validateAccessTokenResponseType(OAuth2ResponseType oAuth2ResponseType, OAuth2ErrorResponseBuilder errorResponseBuilder) throws OAuth2AuthorizationRequestValidationException {
      if (isAccessTokenRequested(oAuth2ResponseType) && !this.isAccessTokenConfigured()) {
         this.logger.warn(OAuth2LogTypes.OAUTH2_MESSAGE, "Invalid OIDC Hybrid Flow request: Access token requested but not configured.");
         throw new OAuth2AuthorizationRequestValidationException(
            AuthorizationErrorResult.errorResult(
               errorResponseBuilder.buildResponse(
                  OAuth2Error.UNSUPPORTED_RESPONSE_TYPE, "Invalid response type: " + LogUtil.logSafeQuote(oAuth2ResponseType.responseType())
               )
            )
         );
      }
   }

   private boolean isAccessTokenConfigured() {
      return ((OpenIdConnectAuthorizationCodeGrantSettings)this.settings.get())
         .getHybridFlowSettings()
         .flatMap(HybridFlowSettings::getAccessTokenSettings)
         .isPresent();
   }

   private static boolean isIdTokenRequested(OAuth2ResponseType oAuth2ResponseType) {
      return oAuth2ResponseType.requestsIdToken();
   }

   private static boolean isAccessTokenRequested(OAuth2ResponseType oAuth2ResponseType) {
      return oAuth2ResponseType.requestsAccessToken();
   }

   private static ELogger getLogger(Provider<IntegrationLoggerFactory> loggerFactory) {
      return ((IntegrationLoggerFactory)loggerFactory.get()).create(OpenIdConnectAuthenticationRequestValidatorImpl.class);
   }

   interface ResponseMode {
      String getValue();
   }

   enum SupportedResponseMode implements OpenIdConnectAuthenticationRequestValidatorImpl.ResponseMode {
      QUERY("query"),
      FRAGMENT("fragment");

      private final String value;

      @SuppressFBWarnings(justification = "generated code")
      SupportedResponseMode(final String value) {
         this.value = value;
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String getValue() {
         return this.value;
      }
   }
}
