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

import com.airlock.iam.base.api.infrastructure.uri.ExternalUriInfo;
import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.common.infrastructure.ResponseBodyWriter;
import com.airlock.iam.common.infrastructure.request.FormParamHttpServletRequestWrapper;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.login.app.misc.oauth2.OAuth2Utils;
import com.airlock.iam.login.app.misc.oauth2.provider.parser.OAuth2TokenEndpointRequestParser;
import com.airlock.iam.login.app.misc.oauth2.provider.parser.OpenIDConnectTokenEndpointRequestParser;
import com.airlock.iam.login.app.misc.oauth2.provider.servlet.OAuth2ProviderRequestHandler;
import com.airlock.iam.login.app.misc.oauth2.provider.servlet.v3.OAuth2TechnicalErrorRequestHandler;
import com.airlock.iam.login.app.misc.oauth2.provider.servlet.v3.clientcredentialsgrant.OAuth2V3ClientCredentialsGrantRequestHandler;
import com.airlock.iam.login.app.misc.oauth2.provider.servlet.v3.clientcredentialsgrant.OAuth2V3ClientCredentialsGrantRequestHandlerResult;
import com.airlock.iam.oauth2.api.domain.model.AuthorizationServerId;
import com.airlock.iam.oauth2.api.domain.model.OAuth2Scope;
import com.airlock.iam.oauth2.application.service.token.OAuth2TokenEndpointService;
import com.airlock.iam.oauth2.domain.model.OAuth2Param;
import com.airlock.iam.oauth2.domain.model.authorization.OAuth2GrantSettings;
import com.airlock.iam.oauth2.domain.model.codegrant.OAuth2CodeGrantRequestParserSettings;
import com.airlock.iam.oauth2.domain.model.codegrant.OAuth2CodeGrantSettings;
import com.airlock.iam.oauth2.domain.model.codegrant.OpenIDConnectCodeGrantRequestParserSettings;
import com.airlock.iam.oauth2.domain.model.tokenexchange.OAuth2TokenExchangeErrorResponse;
import com.airlock.iam.oauth2.domain.model.tokenexchange.OAuth2TokenExchangeGrantAccessToken;
import com.airlock.iam.oauth2.domain.model.validation.OAuth2Error;
import com.airlock.iam.oauth2.domain.model.validation.OAuth2RequestHandlingException;
import com.airlock.iam.oauth2.domain.service.token.OAuth2V3TokenExchangeGrantRequestHandler;
import com.nimbusds.oauth2.sdk.AccessTokenResponse;
import com.nimbusds.oauth2.sdk.ErrorObject;
import com.nimbusds.oauth2.sdk.Scope;
import com.nimbusds.oauth2.sdk.TokenErrorResponse;
import com.nimbusds.oauth2.sdk.TokenResponse;
import com.nimbusds.oauth2.sdk.http.HTTPResponse;
import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
import com.nimbusds.oauth2.sdk.token.Tokens;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Map.Entry;
import org.apache.commons.lang3.StringUtils;

public class OAuth2TokenEndpointServiceImpl implements OAuth2TokenEndpointService {
   private static final String ISSUED_TOKEN_TYPE_RESPONSE_ATTRIBUTE = "issued_token_type";
   private static final ELogger LOG = ELoggerFactory.getLogger(OAuth2TokenEndpointServiceImpl.class);
   private final Provider<HttpServletRequest> request;
   private final Provider<HttpServletResponse> response;
   private final Provider<OAuth2GrantSettings> grantSettings;
   private final Provider<OAuth2TokenEndpointRequestParser> oAuth2RequestParser;
   private final Provider<OpenIDConnectTokenEndpointRequestParser> openIDConnectRequestParser;
   private final Provider<AuthorizationServerId> authorizationServerId;
   private final Provider<OAuth2V3ClientCredentialsGrantRequestHandler> clientCredentialsGrantRequestHandler;
   private final Provider<OAuth2V3TokenExchangeGrantRequestHandler> tokenExchangeGrantRequestHandler;
   private final Provider<ExternalUriInfo> externalUriInfo;

   @Inject
   public OAuth2TokenEndpointServiceImpl(
      Provider<HttpServletRequest> request,
      Provider<HttpServletResponse> response,
      Provider<OAuth2GrantSettings> grantSettings,
      Provider<OAuth2TokenEndpointRequestParser> oAuth2RequestParser,
      Provider<OpenIDConnectTokenEndpointRequestParser> openIDConnectRequestParser,
      Provider<AuthorizationServerId> authorizationServerId,
      Provider<OAuth2V3ClientCredentialsGrantRequestHandler> clientCredentialsGrantRequestHandler,
      Provider<OAuth2V3TokenExchangeGrantRequestHandler> tokenExchangeGrantRequestHandler,
      Provider<ExternalUriInfo> externalUriInfo
   ) {
      this.request = request;
      this.response = response;
      this.grantSettings = grantSettings;
      this.oAuth2RequestParser = oAuth2RequestParser;
      this.openIDConnectRequestParser = openIDConnectRequestParser;
      this.authorizationServerId = authorizationServerId;
      this.clientCredentialsGrantRequestHandler = clientCredentialsGrantRequestHandler;
      this.tokenExchangeGrantRequestHandler = tokenExchangeGrantRequestHandler;
      this.externalUriInfo = externalUriInfo;
   }

   @Override
   public void handleTokenEndpoint(Map<String, List<String>> formParams) {
      HttpServletRequest requestWrapper = new FormParamHttpServletRequestWrapper((HttpServletRequest)this.request.get(), formParams);
      this.handleRequest(requestWrapper);
   }

   public void handleRequest(HttpServletRequest requestWrapper) {
      OAuth2ProviderRequestHandler.OAuth2TokenEndpointState state = new OAuth2ProviderRequestHandler.OAuth2TokenEndpointState(
         requestWrapper, (HttpServletResponse)this.response.get(), (ExternalUriInfo)this.externalUriInfo.get()
      );
      String grantType = parseGrantType(requestWrapper);
      OAuth2GrantSettings settings = (OAuth2GrantSettings)this.grantSettings.get();
      if (OAuth2Utils.GRANT_TYPE_AUTHORIZATION_CODE.equals(grantType) || OAuth2Utils.GRANT_TYPE_REFRESH.equals(grantType)) {
         settings.codeGrantSettings()
            .ifPresentOrElse(
               codeGrantSettings -> this.handleAuthorizationCodeOrRefreshGrant(state, codeGrantSettings), () -> this.handleUnconfiguredGrantType(state)
            );
      } else if (OAuth2Utils.GRANT_TYPE_CLIENT_CREDENTIALS.equals(grantType)) {
         if (settings.isClientCredentialsGrantConfigured()) {
            this.handleClientCredentialsGrant(requestWrapper);
         } else {
            this.handleUnconfiguredGrantType(state);
         }
      } else if (OAuth2Utils.GRANT_TYPE_TOKEN_EXCHANGE.equals(grantType)) {
         if (settings.isTokenExchangeGrantConfigured()) {
            this.handleTokenExchangeGrant(requestWrapper);
         } else {
            this.handleUnconfiguredGrantType(state);
         }
      } else {
         this.handleUnconfiguredGrantType(state);
      }
   }

   private void handleAuthorizationCodeOrRefreshGrant(OAuth2ProviderRequestHandler.OAuth2TokenEndpointState state, OAuth2CodeGrantSettings settings) {
      Optional<OAuth2CodeGrantRequestParserSettings> oauth2ParserSettings = settings.oauthRequestParserSettings();
      Optional<OpenIDConnectCodeGrantRequestParserSettings> oidcParserSettings = settings.oidcRequestParserSettings();
      if (oauth2ParserSettings.isPresent()) {
         handleAuthorizationCodeOrRefreshGrant((OAuth2TokenEndpointRequestParser)this.oAuth2RequestParser.get(), state);
      } else {
         if (!oidcParserSettings.isPresent()) {
            throw new IllegalStateException("No OAuth 2.0 nor OpenID Connect token endpoint available.");
         }

         handleAuthorizationCodeOrRefreshGrant((OAuth2TokenEndpointRequestParser)this.openIDConnectRequestParser.get(), state);
      }
   }

   private void handleClientCredentialsGrant(HttpServletRequest requestWrapper) {
      OAuth2V3ClientCredentialsGrantRequestHandlerResult result = ((OAuth2V3ClientCredentialsGrantRequestHandler)this.clientCredentialsGrantRequestHandler
            .get())
         .handleRequest(requestWrapper);
      applyHTTPResponse(result.response().toHTTPResponse(), (HttpServletResponse)this.response.get());
   }

   private void handleTokenExchangeGrant(HttpServletRequest requestWrapper) {
      TokenResponse result = ((OAuth2V3TokenExchangeGrantRequestHandler)this.tokenExchangeGrantRequestHandler.get())
         .handleRequest(requestWrapper)
         .mapResult(OAuth2TokenEndpointServiceImpl::issuedBearerAccessTokenToTokenResponse)
         .orMapFailure(OAuth2TokenEndpointServiceImpl::tokenExchangeErrorToTokenResponse);
      applyHTTPResponse(result.toHTTPResponse(), (HttpServletResponse)this.response.get());
   }

   private static TokenResponse issuedBearerAccessTokenToTokenResponse(OAuth2TokenExchangeGrantAccessToken token) {
      return new AccessTokenResponse(
         new Tokens(
            new BearerAccessToken(
               token.getAccessToken(), token.getTokenLifetime().getSeconds(), Scope.parse(token.getScope().stream().map(OAuth2Scope::name).toList())
            ),
            null
         ),
         Map.of("issued_token_type", token.getIssuedTokenType().toURI().toASCIIString())
      );
   }

   private static TokenResponse tokenExchangeErrorToTokenResponse(OAuth2TokenExchangeErrorResponse tokenExchangeErrorResponse) {
      return new TokenErrorResponse(
         new ErrorObject(
            tokenExchangeErrorResponse.getErrorCode().getParameterValue(),
            tokenExchangeErrorResponse.getDescription().orElse(null),
            tokenExchangeErrorResponse.getStatusCode().orElse(400)
         )
      );
   }

   private static void handleAuthorizationCodeOrRefreshGrant(
      OAuth2TokenEndpointRequestParser parser, OAuth2ProviderRequestHandler.OAuth2TokenEndpointState state
   ) {
      try {
         parser.getRequestHandler(state).handleRequest();
      } catch (OAuth2RequestHandlingException e) {
         respondWithTechnicalError(state, OAuth2Error.INVALID_REQUEST, e.getMessage());
      }
   }

   private void handleUnconfiguredGrantType(OAuth2ProviderRequestHandler.OAuth2TokenEndpointState state) {
      String grantType = parseGrantType(state.request());
      if (StringUtils.isBlank(grantType)) {
         String message = "Required parameter " + LogUtil.quote(OAuth2Param.GRANT_TYPE.getParameterName()) + " is missing or empty.";
         LOG.info(message);
         respondWithTechnicalError(state, OAuth2Error.INVALID_REQUEST, message);
      } else {
         String message = "Requested grant type ("
            + LogUtil.logSafeString(grantType)
            + ") is not configured for AS "
            + LogUtil.quote(((AuthorizationServerId)this.authorizationServerId.get()).id());
         LOG.info(message);
         respondWithTechnicalError(state, OAuth2Error.UNSUPPORTED_GRANT_TYPE, message);
      }
   }

   private static String parseGrantType(HttpServletRequest request) {
      return request.getParameter(OAuth2Param.GRANT_TYPE.getParameterName());
   }

   private static void respondWithTechnicalError(OAuth2ProviderRequestHandler.OAuth2TokenEndpointState state, OAuth2Error errorCode, String description) {
      new OAuth2TechnicalErrorRequestHandler(state, errorCode, description).handleRequest();
   }

   private static void applyHTTPResponse(HTTPResponse httpResponse, HttpServletResponse servletResponse) {
      servletResponse.setStatus(httpResponse.getStatusCode());

      for (Entry<String, List<String>> header : httpResponse.getHeaderMap().entrySet()) {
         for (String headerValue : header.getValue()) {
            servletResponse.addHeader(header.getKey(), headerValue);
         }
      }

      if (httpResponse.getEntityContentType() != null) {
         servletResponse.setContentType(httpResponse.getEntityContentType().toString());
      }

      String content = httpResponse.getBody();
      if (content != null) {
         ResponseBodyWriter.writeBodyWithoutCommittingResponse(content, servletResponse);
      }
   }
}
