package com.airlock.iam.oauth2.infrastructure.resource.rest;

import com.airlock.iam.common.api.infrastructure.annotation.DisableCsrfFilter;
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.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.application.service.revocation.OAuth2TokenRevocationService;
import com.airlock.iam.oauth2.domain.model.clientauthentication.method.KnownClientAuthenticationMethod;
import com.airlock.iam.oauth2.domain.model.revocation.OAuth2TokenRevocationRequest;
import com.airlock.iam.oauth2.domain.model.validation.OAuth2ValidationResult;
import com.airlock.iam.oauth2.domain.model.validation.OAuth2ValidationStatus;
import com.airlock.iam.oauth2.domain.service.clientauthentication.OAuth2TokenRevocationClientAuthenticationService;
import com.airlock.iam.oauth2.infrastructure.validation.AuthorizationServerIdValidator;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.FormParam;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MultivaluedHashMap;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import java.util.Optional;

@Path("/authorization-servers/{authorizationServerId}/revoke")
public class OAuth2TokenRevocationResource {
   private static final ELogger LOG = ELoggerFactory.getLogger(OAuth2TokenRevocationResource.class);
   private final Provider<Optional<OAuth2TokenRevocationService>> service;
   private final Provider<OAuth2TokenRevocationClientAuthenticationService<OAuth2ClientAuthenticationPayload>> authenticationService;
   private final HttpServletRequest request;
   private final Provider<AuthorizationServerIdValidator> authorizationServerIdValidator;

   @Inject
   public OAuth2TokenRevocationResource(
      Provider<Optional<OAuth2TokenRevocationService>> service,
      Provider<OAuth2TokenRevocationClientAuthenticationService<OAuth2ClientAuthenticationPayload>> authenticationService,
      HttpServletRequest request,
      Provider<AuthorizationServerIdValidator> authorizationServerIdValidator
   ) {
      this.service = service;
      this.authenticationService = authenticationService;
      this.request = request;
      this.authorizationServerIdValidator = authorizationServerIdValidator;
   }

   @ApiResponses(
      {
            @ApiResponse(responseCode = "200", description = "Successfully revoked the token(s) or invalid token submitted."),
            @ApiResponse(
               responseCode = "400",
               description = "The request was invalid. Includes an error object as specified in <a href=\"https://tools.ietf.org/html/rfc6749#section-5.2\" target=\"_blank\">RFC6749#5.2</a>.",
               content = {
                     @Content(mediaType = "application/vnd.api+json", schema = @Schema(ref = "json-api.yaml#/components/schemas/ErrorDocumentResponse")),
                     @Content(mediaType = "application/json", schema = @Schema(ref = "#/components/schemas/OAuth2ErrorResponse"))
               }
            ),
            @ApiResponse(
               responseCode = "401",
               description = "The client credentials are invalid.",
               content = {
                     @Content(mediaType = "application/vnd.api+json", schema = @Schema(ref = "json-api.yaml#/components/schemas/ErrorDocumentResponse")),
                     @Content(mediaType = "application/json", schema = @Schema(ref = "#/components/schemas/OAuth2ErrorResponse"))
               }
            ),
            @ApiResponse(
               responseCode = "404",
               description = "<div class=\"iam-error-code\"><table>\n<tr>\n<td>-</td>\n<td>No token revocation endpoint configured.</td>\n</tr>\n<tr>\n<td>AUTHORIZATION_SERVER_NOT_FOUND</td>\n<td>The authorization server ID does not exist.</td>\n</tr>\n</table>\n</div>\n",
               content = {
                     @Content(mediaType = "application/vnd.api+json", schema = @Schema(ref = "json-api.yaml#/components/schemas/ErrorDocumentResponse")),
                     @Content(mediaType = "application/json", schema = @Schema(ref = "#/components/schemas/OAuth2ErrorResponse"))
               }
            )
      }
   )
   @POST
   @Operation(
      description = "Revokes the specified token(s). Client credentials may have to be provided as authorization, if configured.\nRefer to the <a href=\"https://tools.ietf.org/html/rfc7009\" target=\"_blank\">RFC 7009</a> specification\nfor a more detailed description of the endpoint.\n<div class=\"iam-resource-return\">response indicating the success.</div>",
      tags = "OAuth 2.0/OIDC",
      summary = "Revoke tokens"
   )
   @Consumes("application/x-www-form-urlencoded")
   @Produces("application/json")
   @DisableCsrfFilter
   public Response revoke(
      @Parameter(description = "The authorization server ID.") @PathParam("authorizationServerId") String authorizationServerId,
      @Parameter(description = "The access or refresh token.", required = true) @FormParam("token") String token,
      @Parameter(description = "The token type hint.") @FormParam("token_type_hint") String tokenTypeHint,
      MultivaluedMap<String, String> formParams
   ) {
      ((AuthorizationServerIdValidator)this.authorizationServerIdValidator.get()).validateAuthorizationServer(authorizationServerId);
      if (formParams == null) {
         formParams = new MultivaluedHashMap();
      }

      if (((Optional)this.service.get()).isEmpty()) {
         LOG.info("Token revocation endpoint not configured");
         return Response.status(Status.NOT_FOUND).build();
      } else {
         Optional<OAuth2TokenRevocationRequest> revocationRequest = OAuth2TokenRevocationRequest.parse(
            new FormParamHttpServletRequestWrapper(this.request, formParams)
         );
         if (revocationRequest.isEmpty()) {
            LOG.info("Invalid token revocation request");
            return OAuth2ErrorResponse.map(new OAuth2ValidationResult(OAuth2ValidationStatus.REQUEST_NOT_VALID)).build();
         } else {
            Optional<OAuth2ClientId> clientId = this.getAuthorizedClientId(revocationRequest.get());
            if (clientId.isEmpty()) {
               LOG.info("No credentials or wrong credentials provided.");
               return Response.status(Status.UNAUTHORIZED).build();
            } else {
               ((OAuth2TokenRevocationService)((Optional)this.service.get()).get()).revoke(revocationRequest.get().getTokenValue(), clientId.get());
               return Response.ok().build();
            }
         }
      }
   }

   private Optional<OAuth2ClientId> getAuthorizedClientId(OAuth2TokenRevocationRequest revocationRequest) {
      Optional<OAuth2ClientAuthenticationPayload> authenticationPayload = ((OAuth2TokenRevocationClientAuthenticationService)this.authenticationService.get())
         .authorize(revocationRequest.getAuthenticationPayload())
         .map(OAuth2ClientAuthentication::getPayload);
      if (authenticationPayload.isEmpty()) {
         return Optional.empty();
      } else {
         return authenticationPayload.get().method() == KnownClientAuthenticationMethod.NONE
            ? Optional.ofNullable(revocationRequest.getClientId()).map(OAuth2ClientId::oAuth2ClientId)
            : authenticationPayload.flatMap(OAuth2ClientAuthenticationPayload::clientId).map(c -> OAuth2ClientId.oAuth2ClientId(c.id()));
      }
   }
}
