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

import com.airlock.iam.base.misc.result.Result;
import com.airlock.iam.common.api.domain.model.user.PersistentUser;
import com.airlock.iam.common.api.infrastructure.annotation.DisableCsrfFilter;
import com.airlock.iam.common.api.infrastructure.annotation.RequiresWritableSession;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.oauth2.application.service.userinfo.OpenIDConnectUserInfoService;
import com.airlock.iam.oauth2.domain.model.credential.OAuth2AccessInfo;
import com.airlock.iam.oauth2.domain.model.token.OAuth2AccessToken;
import com.airlock.iam.oauth2.domain.model.userinfo.OpenIDConnectUserInfo;
import com.airlock.iam.oauth2.domain.model.validation.OAuth2ValidationResult;
import com.airlock.iam.oauth2.infrastructure.transfer.documentation.OpenIdConnectUserInfoResponseData;
import com.airlock.iam.oauth2.infrastructure.validation.AuthorizationServerIdValidator;
import com.airlock.iam.oauth2.infrastructure.validation.OpenIdConnectUserInfoAccess;
import com.airlock.iam.oauth2.infrastructure.validation.availability.OpenIdConnectUserInfoEndpointAvailability;
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.GET;
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.Response;
import jakarta.ws.rs.core.Response.Status;

@Path("/authorization-servers/{authorizationServerId}/userinfo")
public class OpenIdConnectUserInfoEndpointResource {
   private static final ELogger LOG = ELoggerFactory.getLogger(OpenIdConnectUserInfoEndpointResource.class);
   private final Provider<OpenIdConnectUserInfoEndpointAvailability> userInfoAvailability;
   private final Provider<OpenIdConnectUserInfoAccess> accessValidator;
   private final Provider<OpenIDConnectUserInfoService> userInfoService;
   private final HttpServletRequest request;
   private final Provider<AuthorizationServerIdValidator> authorizationServerIdValidator;

   @Inject
   public OpenIdConnectUserInfoEndpointResource(
      Provider<OpenIdConnectUserInfoEndpointAvailability> userInfoAvailability,
      Provider<OpenIdConnectUserInfoAccess> accessValidator,
      Provider<OpenIDConnectUserInfoService> userInfoService,
      HttpServletRequest request,
      Provider<AuthorizationServerIdValidator> authorizationServerIdValidator
   ) {
      this.userInfoAvailability = userInfoAvailability;
      this.accessValidator = accessValidator;
      this.userInfoService = userInfoService;
      this.request = request;
      this.authorizationServerIdValidator = authorizationServerIdValidator;
   }

   @ApiResponses(
      {
            @ApiResponse(
               responseCode = "200",
               description = "Retrieved the UserInfo claims.",
               content = @Content(mediaType = "application/json", schema = @Schema(implementation = OpenIdConnectUserInfoResponseData.class))
            ),
            @ApiResponse(
               responseCode = "401",
               description = "The access token is missing or 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 UserInfo Endpoint available.</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"))
               }
            )
      }
   )
   @GET
   @Operation(
      description = "Retrieves the OpenID Connect UserInfo Endpoint resource with claims about the End-User. An access token in the\nbearer token format must be provided as authorization.\nRefer to the <a href=\"https://openid.net/specs/openid-connect-core-1_0.html#UserInfo\" target=\"_blank\">UserInfo Endpoint</a> specification\nfor a more detailed description of the endpoint.\n<div class=\"iam-resource-return\">The standard claims about the authenticated End-User.\n Depending on the configuration of the authorization server, additional custom claims may also be present.</div>",
      tags = "OAuth 2.0/OIDC",
      summary = "Get UserInfo"
   )
   @Produces("application/json")
   @DisableCsrfFilter
   @RequiresWritableSession
   public Response retrieveResourceUsingGet(
      @Parameter(description = "The authorization server ID.") @PathParam("authorizationServerId") String authorizationServerId
   ) {
      return this.retrieveUserInfo(authorizationServerId);
   }

   @ApiResponses(
      {
            @ApiResponse(
               responseCode = "200",
               description = "Retrieved the UserInfo claims.",
               content = @Content(mediaType = "application/json", schema = @Schema(implementation = OpenIdConnectUserInfoResponseData.class))
            ),
            @ApiResponse(
               responseCode = "401",
               description = "The access token is missing or invalid.",
               content = @Content(mediaType = "application/json", schema = @Schema(ref = "json-api.yaml#/components/schemas/ErrorDocumentResponse"))
            ),
            @ApiResponse(
               responseCode = "404",
               description = "<div class=\"iam-error-code\"><table>\n<tr>\n<td>-</td>\n<td>No UserInfo Endpoint available.</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/json", schema = @Schema(ref = "json-api.yaml#/components/schemas/ErrorDocumentResponse"))
            )
      }
   )
   @POST
   @Operation(
      description = "Retrieves the OpenID Connect UserInfo Endpoint resource with claims about the End-User. An access token in the\nbearer token format must be provided as authorization.\nRefer to the <a href=\"https://openid.net/specs/openid-connect-core-1_0.html#UserInfo\" target=\"_blank\">UserInfo Endpoint</a> specification\nfor a more detailed description of the endpoint.\n<div class=\"iam-resource-return\">The standard claims about the authenticated End-User.\n Depending on the configuration of the authorization server, additional custom claims may also be present.</div>",
      tags = "OAuth 2.0/OIDC",
      summary = "Get UserInfo"
   )
   @Produces("application/json")
   @DisableCsrfFilter
   public Response retrieveResourceUsingPost(
      @Parameter(description = "The authorization server ID.") @PathParam("authorizationServerId") String authorizationServerId
   ) {
      return this.retrieveUserInfo(authorizationServerId);
   }

   private Response retrieveUserInfo(String authorizationServerId) {
      ((AuthorizationServerIdValidator)this.authorizationServerIdValidator.get()).validateAuthorizationServer(authorizationServerId);
      if (!((OpenIdConnectUserInfoEndpointAvailability)this.userInfoAvailability.get()).isAvailable()) {
         LOG.info("No UserInfo resource available because there is no OpenID Connect Grant configured.");
         return Response.status(Status.NOT_FOUND).build();
      }

      Result<OAuth2AccessInfo, OAuth2ValidationResult> accessResult = ((OpenIdConnectUserInfoAccess)this.accessValidator.get()).validateAccess(this.request);
      if (accessResult.isFailure()) {
         return OAuth2ErrorResponse.map(accessResult.getFailure()).build();
      }

      OAuth2AccessInfo accessInfo = accessResult.get();
      OAuth2AccessToken accessToken = accessInfo.getAccessToken();
      PersistentUser user = accessInfo.getValidUser();
      OpenIDConnectUserInfo userInfoResponse = ((OpenIDConnectUserInfoService)this.userInfoService.get()).userInfo(accessToken, user);
      return Response.ok(userInfoResponse.getUserInfo().toJSONObject().toJSONString()).build();
   }
}
