package com.airlock.iam.oauth2.infrastructure.service.tokenexchange;

import com.airlock.iam.base.api.infrastructure.uri.ExternalUriInfo;
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.api.domain.model.OAuth2ClientId;
import com.airlock.iam.oauth2.api.domain.model.OAuth2Scope;
import com.airlock.iam.oauth2.api.domain.model.clientauthentication.OAuth2ClientAuthenticationPayload;
import com.airlock.iam.oauth2.api.domain.model.tokenexchange.OAuth2TokenExchangeGrantRequest;
import com.airlock.iam.oauth2.api.domain.model.tokenexchange.OAuth2TokenExchangeRawToken;
import com.airlock.iam.oauth2.api.domain.model.tokenexchange.OAuth2TokenExchangeTargetAudience;
import com.airlock.iam.oauth2.api.domain.model.tokenexchange.OAuth2TokenExchangeTargetResource;
import com.airlock.iam.oauth2.api.domain.model.tokenexchange.OAuth2TokenExchangeTokenType;
import com.airlock.iam.oauth2.api.domain.service.clientauthentication.OAuth2ClientAuthenticationParser;
import com.airlock.iam.oauth2.domain.model.OAuth2Param;
import com.airlock.iam.oauth2.domain.model.logger.IntegrationLoggerFactory;
import com.airlock.iam.oauth2.domain.model.logging.OAuth2TokenRepresentation;
import com.airlock.iam.oauth2.domain.model.tokenexchange.OAuth2TokenExchangeErrorResponse;
import com.airlock.iam.oauth2.domain.model.validation.OAuth2Error;
import com.airlock.iam.oauth2.infrastructure.request.OAuth2HttpServletRequestWrapper;
import com.airlock.iam.oauth2.infrastructure.service.OAuth2GrantRequestParserUtils;
import com.nimbusds.oauth2.sdk.GrantType;
import com.nimbusds.oauth2.sdk.ParseException;
import com.nimbusds.oauth2.sdk.Scope;
import com.nimbusds.oauth2.sdk.TokenRequest;
import com.nimbusds.oauth2.sdk.http.HTTPRequest;
import com.nimbusds.oauth2.sdk.http.JakartaServletUtils;
import com.nimbusds.oauth2.sdk.id.Audience;
import com.nimbusds.oauth2.sdk.token.TokenTypeURI;
import com.nimbusds.oauth2.sdk.tokenexchange.TokenExchangeGrant;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.net.URI;
import java.util.List;
import java.util.Optional;
import java.util.Set;

public final class OAuth2TokenExchangeGrantRequestParser {
   private static final String GRANT_TYPE_TOKEN_EXCHANGE = GrantType.TOKEN_EXCHANGE.getValue();
   static final String PARSE_ERROR_MESSAGE_PREFIX = "Invalid Token Exchange Grant request: ";
   static final String PARSE_SUCCESS_MESSAGE_PREFIX = "Token Exchange Grant Request received: ";
   private static final String AUDIENCE = "audience";
   private final Provider<IntegrationLoggerFactory> loggerFactory;
   private final Provider<OAuth2ClientAuthenticationParser<OAuth2ClientAuthenticationPayload>> authenticationParser;
   private final Provider<OAuth2TokenRepresentation> tokenRepresentation;
   private final Provider<ExternalUriInfo> externalUriInfo;

   @Inject
   public OAuth2TokenExchangeGrantRequestParser(
      Provider<IntegrationLoggerFactory> loggerFactory,
      Provider<OAuth2ClientAuthenticationParser<OAuth2ClientAuthenticationPayload>> authenticationParser,
      Provider<OAuth2TokenRepresentation> tokenRepresentation,
      Provider<ExternalUriInfo> externalUriInfo
   ) {
      this.loggerFactory = loggerFactory;
      this.authenticationParser = authenticationParser;
      this.tokenRepresentation = tokenRepresentation;
      this.externalUriInfo = externalUriInfo;
   }

   public Result<OAuth2TokenExchangeGrantRequest, OAuth2TokenExchangeErrorResponse> parse(HttpServletRequest servletRequest) {
      if (!GRANT_TYPE_TOKEN_EXCHANGE.equals(servletRequest.getParameter(OAuth2Param.GRANT_TYPE.getParameterName()))) {
         return this.parseError(OAuth2Error.INVALID_GRANT, "The grant type is invalid.");
      }

      if ("".equals(servletRequest.getParameter("audience"))) {
         return this.parseError(OAuth2Error.INVALID_REQUEST, "The audience parameter is empty.");
      }

      try {
         TokenRequest tokenRequest = this.extractTokenExchangeRequest(servletRequest);
         if (tokenRequest.getAuthorizationGrant() instanceof TokenExchangeGrant tokenExchangeGrant) {
            Optional<OAuth2ClientAuthenticationPayload> authenticationPayload = ((OAuth2ClientAuthenticationParser)this.authenticationParser.get())
               .parse(new OAuth2HttpServletRequestWrapper(servletRequest, (ExternalUriInfo)this.externalUriInfo.get()));
            return OAuth2GrantRequestParserUtils.getClientId(servletRequest, authenticationPayload)
               .map(id -> this.parseTokenExchangeRequestAttributes(tokenRequest, tokenExchangeGrant, id, authenticationPayload).mapResult(this::parseSuccess))
               .orElseGet(() -> this.parseError(OAuth2Error.INVALID_CLIENT, "Client could not be identified."));
         } else {
            return this.parseError(OAuth2Error.INVALID_GRANT, "The grant type is invalid.");
         }
      } catch (IOException e) {
         return this.parseError(OAuth2Error.SERVER_ERROR, e.getMessage());
      } catch (ParseException | IllegalArgumentException e) {
         return this.parseError(OAuth2Error.INVALID_REQUEST, e.getMessage());
      }
   }

   private Result<OAuth2TokenExchangeGrantRequest, OAuth2TokenExchangeErrorResponse> parseTokenExchangeRequestAttributes(
      TokenRequest tokenRequest,
      TokenExchangeGrant tokenExchangeGrant,
      OAuth2ClientId clientId,
      Optional<OAuth2ClientAuthenticationPayload> authenticationPayload
   ) {
      return this.getScope(tokenRequest)
         .mapResult(
            scopes -> OAuth2TokenExchangeGrantRequest.builder()
               .clientId(clientId)
               .authenticationPayload(authenticationPayload)
               .resource(this.getResource(tokenRequest))
               .audience(this.getAudience(tokenExchangeGrant))
               .scope(scopes)
               .requestedTokenType(this.getRequestedTokenType(tokenExchangeGrant))
               .subjectToken(this.getSubjectToken(tokenExchangeGrant))
               .actorToken(this.getActorToken(tokenExchangeGrant))
               .customParameters(tokenRequest.getCustomParameters())
               .build()
         );
   }

   private List<OAuth2TokenExchangeTargetResource> getResource(TokenRequest tokenRequest) {
      return OAuth2TokenExchangeTargetResource.convertToResources(Optional.ofNullable(tokenRequest.getResources()).orElse(List.of()));
   }

   private List<OAuth2TokenExchangeTargetAudience> getAudience(TokenExchangeGrant tokenExchangeGrant) {
      return OAuth2TokenExchangeTargetAudience.convertToAudiences(
         Optional.ofNullable(Audience.toStringList(tokenExchangeGrant.getAudience())).orElse(List.of())
      );
   }

   private Result<Set<OAuth2Scope>, OAuth2TokenExchangeErrorResponse> getScope(TokenRequest tokenRequest) {
      List<String> scopeTokens = Optional.ofNullable(tokenRequest.getScope()).<List<String>>map(Scope::toStringList).orElse(List.of());
      return scopeTokens.stream().anyMatch(name -> !OAuth2Scope.isAllowedScopeName(name))
         ? Result.failure(
            OAuth2TokenExchangeErrorResponse.tokenExchangeErrorResponse(
               OAuth2Error.INVALID_REQUEST, "OAuth 2.0 scope does not conform the RFC6749 specification."
            )
         )
         : Result.result(OAuth2Scope.convertToScopes(scopeTokens));
   }

   private Optional<OAuth2TokenExchangeTokenType> getRequestedTokenType(TokenExchangeGrant tokenExchangeGrant) {
      return Optional.ofNullable(tokenExchangeGrant.getRequestedTokenType()).<URI>map(TokenTypeURI::getURI).map(OAuth2TokenExchangeTokenType::of);
   }

   private OAuth2TokenExchangeRawToken getSubjectToken(TokenExchangeGrant tokenExchangeGrant) {
      return OAuth2TokenExchangeRawToken.builder()
         .value(tokenExchangeGrant.getSubjectToken().getValue())
         .type(OAuth2TokenExchangeTokenType.of(tokenExchangeGrant.getSubjectTokenType().getURI()))
         .build();
   }

   private Optional<OAuth2TokenExchangeRawToken> getActorToken(TokenExchangeGrant tokenExchangeGrant) {
      return Optional.of(tokenExchangeGrant)
         .filter(teg -> teg.getActorToken() != null)
         .map(
            teg -> OAuth2TokenExchangeRawToken.builder()
               .value(teg.getActorToken().getValue())
               .type(OAuth2TokenExchangeTokenType.of(teg.getActorTokenType().getURI()))
               .build()
         );
   }

   private TokenRequest extractTokenExchangeRequest(HttpServletRequest servletRequest) throws IOException, ParseException {
      HTTPRequest httpRequest = JakartaServletUtils.createHTTPRequest(servletRequest);
      return TokenRequest.parse(httpRequest);
   }

   private OAuth2TokenExchangeGrantRequest parseSuccess(OAuth2TokenExchangeGrantRequest tokenExchangeGrantRequest) {
      StringBuilder logMessage = new StringBuilder()
         .append("Token Exchange Grant Request received: ")
         .append("clientId: ")
         .append(LogUtil.logSafeQuote(tokenExchangeGrantRequest.getClientId().id()))
         .append(", subject token: ")
         .append(
            LogUtil.logSafeQuote(((OAuth2TokenRepresentation)this.tokenRepresentation.get()).apply(tokenExchangeGrantRequest.getSubjectToken().getValue()))
         )
         .append(", subject token type: ")
         .append(LogUtil.logSafeQuote(tokenExchangeGrantRequest.getSubjectToken().getType().toURI()));
      if (tokenExchangeGrantRequest.getActorToken().isPresent()) {
         logMessage.append(", actor token: ")
            .append(
               LogUtil.logSafeQuote(
                  ((OAuth2TokenRepresentation)this.tokenRepresentation.get()).apply(tokenExchangeGrantRequest.getActorToken().get().getValue())
               )
            )
            .append(", actor token type: ")
            .append(LogUtil.logSafeQuote(tokenExchangeGrantRequest.getActorToken().get().getType().toURI()));
      }

      logMessage.append(", resource: ")
         .append(LogUtil.logSafeQuote(tokenExchangeGrantRequest.getResource().stream().map(OAuth2TokenExchangeTargetResource::resource).toList()))
         .append(", audience: ")
         .append(LogUtil.logSafeQuote(tokenExchangeGrantRequest.getAudience().stream().map(OAuth2TokenExchangeTargetAudience::audience).toList()));
      logMessage.append(
         tokenExchangeGrantRequest.getRequestedTokenType().map(type -> ", requested token type: " + LogUtil.logSafeQuote(type.toURI())).orElse("")
      );
      this.getLogger().debug(logMessage.toString());
      return tokenExchangeGrantRequest;
   }

   private Result<OAuth2TokenExchangeGrantRequest, OAuth2TokenExchangeErrorResponse> parseError(OAuth2Error error, String errorDescription) {
      this.getLogger().debug("Invalid Token Exchange Grant request: " + LogUtil.logSafeString(errorDescription));
      return Result.failure(OAuth2TokenExchangeErrorResponse.tokenExchangeErrorResponse(error, errorDescription));
   }

   private ELogger getLogger() {
      return ((IntegrationLoggerFactory)this.loggerFactory.get()).create(OAuth2TokenExchangeGrantRequestParser.class);
   }
}
