package com.airlock.iam.login.app.infrastructure.oauth2.par;

import com.airlock.iam.base.api.domain.model.exception.ViolationException;
import com.airlock.iam.base.api.infrastructure.uri.ExternalUriInfo;
import com.airlock.iam.base.misc.result.Result;
import com.airlock.iam.base.misc.time.TimeProvider;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
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.clientauthentication.OAuth2ClientAuthentication;
import com.airlock.iam.oauth2.api.domain.model.clientauthentication.OAuth2ClientAuthenticationPayload;
import com.airlock.iam.oauth2.api.domain.service.clientauthentication.OAuth2ClientAuthenticationParser;
import com.airlock.iam.oauth2.application.service.par.OAuth2ParEndpointService;
import com.airlock.iam.oauth2.domain.model.AuthorizationErrorResult;
import com.airlock.iam.oauth2.domain.model.OAuth2Response;
import com.airlock.iam.oauth2.domain.model.authorization.OAuth2AuthorizationEndpointMode;
import com.airlock.iam.oauth2.domain.model.codegrant.AuthorizationCodeRequestParser;
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.OAuth2CodeGrantSettings;
import com.airlock.iam.oauth2.domain.model.codegrant.OpenIdConnectAuthenticationRequest;
import com.airlock.iam.oauth2.domain.model.par.OAuth2AuthorizationRequestQuery;
import com.airlock.iam.oauth2.domain.model.par.OAuth2ParError;
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.par.OAuth2ParResponse;
import com.airlock.iam.oauth2.domain.model.settings.OAuth2ParSettings;
import com.airlock.iam.oauth2.domain.model.validation.OAuth2Error;
import com.airlock.iam.oauth2.domain.service.clientauthentication.OAuth2ClientAuthenticationService;
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.settings.OAuth2SettingsExtractor;
import com.airlock.iam.oauth2.infrastructure.request.OAuth2HttpServletRequestWrapper;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import jakarta.servlet.http.HttpServletRequest;
import java.net.URI;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.Optional;

public class DefaultOAuth2ParEndpointService implements OAuth2ParEndpointService {
   private static final ELogger LOG = ELoggerFactory.getLogger(DefaultOAuth2ParEndpointService.class);
   private final Provider<OAuth2SettingsExtractor> settingsExtractor;
   private final Provider<OAuth2ClientAuthenticationService<OAuth2ClientAuthenticationPayload>> authenticationService;
   private final Provider<OAuth2ClientAuthenticationParser<OAuth2ClientAuthenticationPayload>> authenticationParser;
   private final Provider<OAuth2ParRequestRepository> repository;
   private final Provider<ExternalUriInfo> externalUriInfo;
   private final Provider<OAuth2AuthorizationCodeRequestValidator> oauth2RequestValidator;
   private final Provider<OpenIdConnectAuthenticationCodeRequestValidator> oidcRequestValidator;
   private final Provider<TimeProvider> timeProvider;

   @Inject
   public DefaultOAuth2ParEndpointService(
      Provider<OAuth2SettingsExtractor> settingsExtractor,
      Provider<OAuth2ClientAuthenticationService<OAuth2ClientAuthenticationPayload>> authenticationService,
      Provider<OAuth2ClientAuthenticationParser<OAuth2ClientAuthenticationPayload>> authenticationParser,
      Provider<OAuth2ParRequestRepository> repository,
      Provider<ExternalUriInfo> externalUriInfo,
      Provider<OAuth2AuthorizationCodeRequestValidator> oauth2RequestValidator,
      Provider<OpenIdConnectAuthenticationCodeRequestValidator> oidcRequestValidator,
      Provider<TimeProvider> timeProvider
   ) {
      this.settingsExtractor = settingsExtractor;
      this.authenticationService = authenticationService;
      this.authenticationParser = authenticationParser;
      this.repository = repository;
      this.externalUriInfo = externalUriInfo;
      this.oauth2RequestValidator = oauth2RequestValidator;
      this.oidcRequestValidator = oidcRequestValidator;
      this.timeProvider = timeProvider;
   }

   @Override
   public boolean isParDisabled(AuthorizationServerId authorizationServerId) {
      return this.oAuth2ParSettings(authorizationServerId).isEmpty();
   }

   @Override
   public Result<OAuth2ParResponse, OAuth2ParError> handleParRequest(
      HttpServletRequest servletRequest,
      AuthorizationServerId authorizationServerId,
      Optional<OAuth2ClientId> clientIdFromRequest,
      Map<String, List<String>> authorizationParameters
   ) {
      if (this.isParDisabled(authorizationServerId)) {
         throw new IllegalStateException("Pushed Authorization Requests are disabled.");
      }

      Optional<OAuth2ClientAuthentication> authentication = this.oAuth2ClientAuthentication(servletRequest);
      OAuth2ClientId clientId = resolveClientId(clientIdFromRequest, authentication.map(OAuth2ClientAuthentication::getPayload));
      if (authentication.isEmpty()) {
         LOG.info("Failed to validate PAR request: Client authentication failed.");
         return Result.failure(new OAuth2ParError(OAuth2Error.INVALID_CLIENT, "Client authentication failed."));
      }

      try {
         return this.oAuth2ParSettings(authorizationServerId).orElseThrow().endpointMode() == OAuth2AuthorizationEndpointMode.OIDC
            ? this.validateOidcParRequest(
                  ((ExternalUriInfo)this.externalUriInfo.get()).getRequestUri(), servletRequest.getContentType(), authorizationParameters
               )
               .mapResult(attrs -> this.persistAuthorizationRequest(attrs, authorizationServerId, clientId))
               .orMapFailure(Result::failure)
            : this.validateOAuth2ParRequest(
                  ((ExternalUriInfo)this.externalUriInfo.get()).getRequestUri(), servletRequest.getContentType(), authorizationParameters
               )
               .mapResult(requestAttributes -> this.persistAuthorizationRequest(requestAttributes, authorizationServerId, clientId))
               .orMapFailure(Result::failure);
      } catch (OAuth2AuthorizationCodeGrantRequestParseException e) {
         LOG.info("Failed to parse PAR request: " + e.getMessage());
         return Result.failure(new OAuth2ParError(OAuth2Error.INVALID_REQUEST, e.getMessage()));
      }
   }

   private static OAuth2ClientId resolveClientId(Optional<OAuth2ClientId> clientIdFromRequest, Optional<OAuth2ClientAuthenticationPayload> authPayload) {
      return clientIdFromRequest.or(() -> authPayload.flatMap(OAuth2ClientAuthenticationPayload::clientId).map(id -> OAuth2ClientId.oAuth2ClientId(id.id())))
         .orElseThrow(() -> new ViolationException("Could not resolve client ID from request or authentication payload."));
   }

   private Result<OAuth2ParResponse, OAuth2ParError> persistAuthorizationRequest(
      OAuth2AuthorizationRequestQuery query, AuthorizationServerId authorizationServerId, OAuth2ClientId clientId
   ) {
      return this.oAuth2ParSettings(authorizationServerId).map(parSettings -> {
         Instant validUntil = ((TimeProvider)this.timeProvider.get()).getCurrentInstant().plus(parSettings.requestUriLifetime());
         OAuth2ParRequestUri requestUri = ((OAuth2ParRequestRepository)this.repository.get()).insert(clientId, authorizationServerId, query, validUntil);
         return new OAuth2ParResponse(requestUri, parSettings.requestUriLifetime());
      }).map(this::parSucceeded).orElseThrow();
   }

   private Optional<OAuth2ParSettings> oAuth2ParSettings(AuthorizationServerId authorizationServerId) {
      return ((OAuth2SettingsExtractor)this.settingsExtractor.get())
         .extractGrantSettings(authorizationServerId)
         .codeGrantSettings()
         .flatMap(OAuth2CodeGrantSettings::parSettings);
   }

   private Optional<OAuth2ClientAuthentication> oAuth2ClientAuthentication(HttpServletRequest servletRequest) {
      Optional<OAuth2ClientAuthenticationPayload> authenticationPayload = ((OAuth2ClientAuthenticationParser)this.authenticationParser.get())
         .parse(new OAuth2HttpServletRequestWrapper(servletRequest, (ExternalUriInfo)this.externalUriInfo.get()));
      return authenticationPayload.flatMap(((OAuth2ClientAuthenticationService)this.authenticationService.get())::authorize);
   }

   private Result<OAuth2AuthorizationRequestQuery, OAuth2ParError> validateOAuth2ParRequest(
      URI requestUri, String contentType, Map<String, List<String>> authorizationParameters
   ) throws OAuth2AuthorizationCodeGrantRequestParseException {
      OAuth2AuthorizationRequest authorizationRequest = AuthorizationCodeRequestParser.parseOAuth2PushedAuthorizationRequest(
         requestUri, contentType, authorizationParameters
      );
      return ((OAuth2AuthorizationCodeRequestValidator)this.oauth2RequestValidator.get())
         .validate(authorizationRequest)
         .mapResult(attrs -> oauth2ParValidationSucceeded(authorizationRequest))
         .orMapFailure(DefaultOAuth2ParEndpointService::parValidationFailed);
   }

   private Result<OAuth2AuthorizationRequestQuery, OAuth2ParError> validateOidcParRequest(
      URI requestUri, String contentType, Map<String, List<String>> authorizationParameters
   ) throws OAuth2AuthorizationCodeGrantRequestParseException {
      OpenIdConnectAuthenticationRequest authenticationRequest = AuthorizationCodeRequestParser.parseOpenIdConnectPushedAuthorizationRequest(
         requestUri, contentType, authorizationParameters
      );
      return ((OpenIdConnectAuthenticationCodeRequestValidator)this.oidcRequestValidator.get())
         .validate(authenticationRequest)
         .mapResult(attrs -> oidcParValidationSucceeded(authenticationRequest))
         .orMapFailure(DefaultOAuth2ParEndpointService::parValidationFailed);
   }

   private static Result<OAuth2AuthorizationRequestQuery, OAuth2ParError> oidcParValidationSucceeded(OpenIdConnectAuthenticationRequest request) {
      return Result.result(new OAuth2AuthorizationRequestQuery(request.toQueryParametersString()));
   }

   private static Result<OAuth2AuthorizationRequestQuery, OAuth2ParError> oauth2ParValidationSucceeded(OAuth2AuthorizationRequest request) {
      return Result.result(new OAuth2AuthorizationRequestQuery(request.toQueryParametersString()));
   }

   private static Result<OAuth2AuthorizationRequestQuery, OAuth2ParError> parValidationFailed(AuthorizationErrorResult error) {
      OAuth2Error oAuth2Error = error.getErrorRedirect().flatMap(OAuth2Response::error).flatMap(OAuth2Error::fromCode).orElse(OAuth2Error.INVALID_REQUEST);
      String errorDescription = error.getErrorRedirect().flatMap(OAuth2Response::errorDescription).orElse("PAR request validation failed.");
      return Result.failure(new OAuth2ParError(oAuth2Error, errorDescription));
   }

   private Result<OAuth2ParResponse, OAuth2ParError> parSucceeded(OAuth2ParResponse oAuth2ParResponse) {
      return Result.result(oAuth2ParResponse);
   }
}
