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

import ch.ergon.restal.jsonapi.document.data.ResourceDocument;
import ch.ergon.restal.jsonapi.document.request.RequestDocument;
import com.airlock.iam.base.api.domain.model.validation.ValidationResult;
import com.airlock.iam.base.infrastructure.error.ErrorResponse;
import com.airlock.iam.base.infrastructure.mapper.ValidationToErrorMapper;
import com.airlock.iam.base.infrastructure.mapper.entity.RestMapper;
import com.airlock.iam.base.misc.result.Result;
import com.airlock.iam.common.api.infrastructure.annotation.RequiresWritableSession;
import com.airlock.iam.common.application.exception.RequestDocumentAttributesMissingException;
import com.airlock.iam.oauth2.application.service.OAuth2ExtendedSessionService;
import com.airlock.iam.oauth2.application.service.OAuth2SessionAttributeUpdateService;
import com.airlock.iam.oauth2.domain.model.credential.OAuth2AccessInfo;
import com.airlock.iam.oauth2.domain.model.session.OAuth2ExtendedSession;
import com.airlock.iam.oauth2.domain.model.session.OAuth2SessionId;
import com.airlock.iam.oauth2.domain.model.validation.OAuth2ValidationResult;
import com.airlock.iam.oauth2.infrastructure.generated.transfer.response.OAuth2CustomAttributesDataResourceDocument;
import com.airlock.iam.oauth2.infrastructure.generated.transfer.response.OAuth2SessionDataResourceDocument;
import com.airlock.iam.oauth2.infrastructure.transfer.request.OAuth2SessionUpdateRequest;
import com.airlock.iam.oauth2.infrastructure.transfer.response.OAuth2CustomAttributesData;
import com.airlock.iam.oauth2.infrastructure.transfer.response.OAuth2SessionData;
import com.airlock.iam.oauth2.infrastructure.validation.AuthorizationServerIdValidator;
import com.airlock.iam.oauth2.infrastructure.validation.OAuth2SessionIdValidator;
import com.airlock.iam.oauth2.infrastructure.validation.OAuth2SessionManagementAccess;
import com.airlock.iam.oauth2.infrastructure.validation.availability.OAuth2SessionManagementEndpointAvailability;
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.ExampleObject;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
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.validation.Valid;
import jakarta.validation.constraints.NotNull;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.PATCH;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Response;
import java.util.List;
import java.util.Optional;

@Path("/authorization-servers/{authorizationServerId}/session")
public class OAuth2CurrentSessionResource {
   private static final String SESSION_UPDATE_REQ_EXAMPLE = "{\n\t\"data\" : {\n\t\t\"type\" : \"oauth2-session-custom-attributes\",\n\t\t\"id\" : \"kr01EzwkLbEM9mOyz5yRQQp3foUmxpCnfkyPw6sQ\",\n\t\t\"attributes\" : {\n\t\t\t\"UpdatableAttribute\" : \"someAllowedValue\",\n\t\t\t\"NotUpdatableAttribute\" : \"someAllowedValue\"\n\t\t}\n\t}\n}\n";
   private static final String SESSION_UPDATE_REQ_EXAMPLE_NAME = "Session data";
   private static final String SESSION_RESPONSE_EXAMPLE = "{\n\t\"meta\": {\n\t\t\"type\": \"jsonapi.metadata.document\",\n\t\t\"timestamp\": \"2025-10-23T13:48:13.428+02:00\"\n\t},\n\t\"data\": {\n\t\t\"type\": \"oauth2-session\",\n\t\t\"id\": \"ii0t9QfQDZ50u7tx1yvGGViG5WZ1dBqpFHSwt7vo\",\n\t\t\"attributes\": {\n\t\t\t\"customAttributes\": {\n\t\t\t\t\"someAttribute\": \"someValue\"\n\t\t\t},\n\t\t\t\"clientId\": \"oauth2-client\",\n\t\t\t\"consentTimestamp\": \"2025-10-23T13:38:09.213+02:00\",\n\t\t\t\"authenticationTimestamp\": \"2025-10-23T13:38:09.213+02:00\"\n\t\t}\n\t}\n}\n";
   private final Provider<OAuth2SessionManagementEndpointAvailability> sessionManagementEndpointAvailability;
   private final Provider<OAuth2SessionManagementAccess> accessValidator;
   private final Provider<OAuth2ExtendedSessionService> sessionService;
   private final Provider<OAuth2SessionAttributeUpdateService> sessionAttributeService;
   private final Provider<RestMapper> restMapper;
   private final Provider<ValidationToErrorMapper> errorMapper;
   private final Provider<HttpServletRequest> request;
   private final Provider<AuthorizationServerIdValidator> authorizationServerIdValidator;

   @Inject
   public OAuth2CurrentSessionResource(
      Provider<OAuth2SessionManagementEndpointAvailability> sessionManagementEndpointAvailability,
      Provider<OAuth2SessionManagementAccess> accessValidator,
      Provider<OAuth2ExtendedSessionService> sessionService,
      Provider<OAuth2SessionAttributeUpdateService> sessionAttributeService,
      Provider<RestMapper> restMapper,
      Provider<ValidationToErrorMapper> errorMapper,
      Provider<HttpServletRequest> request,
      Provider<AuthorizationServerIdValidator> authorizationServerIdValidator
   ) {
      this.sessionManagementEndpointAvailability = sessionManagementEndpointAvailability;
      this.accessValidator = accessValidator;
      this.sessionService = sessionService;
      this.sessionAttributeService = sessionAttributeService;
      this.restMapper = restMapper;
      this.errorMapper = errorMapper;
      this.request = request;
      this.authorizationServerIdValidator = authorizationServerIdValidator;
   }

   @ApiResponses(
      {
            @ApiResponse(
               responseCode = "200",
               description = "The retrieved OAuth 2.0 session.",
               content = {
                     @Content(
                        mediaType = "application/vnd.api+json",
                        schema = @Schema(implementation = OAuth2SessionDataResourceDocument.class),
                        examples = @ExampleObject(
                           name = "Get session",
                           value = "{\n\t\"meta\": {\n\t\t\"type\": \"jsonapi.metadata.document\",\n\t\t\"timestamp\": \"2025-10-23T13:48:13.428+02:00\"\n\t},\n\t\"data\": {\n\t\t\"type\": \"oauth2-session\",\n\t\t\"id\": \"ii0t9QfQDZ50u7tx1yvGGViG5WZ1dBqpFHSwt7vo\",\n\t\t\"attributes\": {\n\t\t\t\"customAttributes\": {\n\t\t\t\t\"someAttribute\": \"someValue\"\n\t\t\t},\n\t\t\t\"clientId\": \"oauth2-client\",\n\t\t\t\"consentTimestamp\": \"2025-10-23T13:38:09.213+02:00\",\n\t\t\t\"authenticationTimestamp\": \"2025-10-23T13:38:09.213+02:00\"\n\t\t}\n\t}\n}\n"
                        )
                     ),
                     @Content(
                        mediaType = "application/json",
                        schema = @Schema(implementation = OAuth2SessionDataResourceDocument.class),
                        examples = @ExampleObject(
                           name = "Get session",
                           value = "{\n\t\"meta\": {\n\t\t\"type\": \"jsonapi.metadata.document\",\n\t\t\"timestamp\": \"2025-10-23T13:48:13.428+02:00\"\n\t},\n\t\"data\": {\n\t\t\"type\": \"oauth2-session\",\n\t\t\"id\": \"ii0t9QfQDZ50u7tx1yvGGViG5WZ1dBqpFHSwt7vo\",\n\t\t\"attributes\": {\n\t\t\t\"customAttributes\": {\n\t\t\t\t\"someAttribute\": \"someValue\"\n\t\t\t},\n\t\t\t\"clientId\": \"oauth2-client\",\n\t\t\t\"consentTimestamp\": \"2025-10-23T13:38:09.213+02:00\",\n\t\t\t\"authenticationTimestamp\": \"2025-10-23T13:38:09.213+02:00\"\n\t\t}\n\t}\n}\n"
                        )
                     )
               }
            ),
            @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 = "json-api.yaml#/components/schemas/ErrorDocumentResponse"))
               }
            ),
            @ApiResponse(
               responseCode = "404",
               description = "<div class=\"iam-error-code\"><table>\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 = "json-api.yaml#/components/schemas/ErrorDocumentResponse"))
               }
            ),
            @ApiResponse(responseCode = "501", description = "The session management endpoint is required.")
      }
   )
   @GET
   @Operation(
      description = "Retrieves the current OAuth 2.0 session. An access token in the bearer token format\nmust be provided as authorization.\n<div class=\"iam-resource-return\">A document containing the OAuth 2.0 session.</div>",
      tags = "OAuth 2.0/OIDC",
      summary = "Get session"
   )
   @Produces({"application/vnd.api+json", "application/json;qs=0.9"})
   @RequiresWritableSession
   public Response retrieveSession(@Parameter(description = "The authorization server ID.") @PathParam("authorizationServerId") String authorizationServerId) {
      ((AuthorizationServerIdValidator)this.authorizationServerIdValidator.get()).validateAuthorizationServer(authorizationServerId);
      if (!((OAuth2SessionManagementEndpointAvailability)this.sessionManagementEndpointAvailability.get()).isAvailable()) {
         return ErrorResponse.notImplemented().build().toResponse();
      }

      Result<OAuth2AccessInfo, OAuth2ValidationResult> accessResult = ((OAuth2SessionManagementAccess)this.accessValidator.get())
         .validateAccess((HttpServletRequest)this.request.get());
      if (accessResult.isFailure()) {
         return ErrorResponse.unauthorized().build().toResponse();
      }

      OAuth2AccessInfo accessInfo = accessResult.get();
      OAuth2SessionId sessionId = accessInfo.getAccessToken().getSessionId();
      Optional<OAuth2ExtendedSession> session = ((OAuth2ExtendedSessionService)this.sessionService.get()).findOAuth2ExtendedSession(sessionId);
      if (session.isEmpty()) {
         return ErrorResponse.internalServerError().build().toResponse();
      }

      ResourceDocument<OAuth2SessionData> document = ((RestMapper)this.restMapper.get())
         .forElement(session.get(), OAuth2SessionData.class)
         .withIdSupplier(s -> s.id().asString())
         .toResourceDocument();
      return Response.ok(document).build();
   }

   @ApiResponses(
      {
            @ApiResponse(
               responseCode = "200",
               description = "The OAuth 2.0 session was updated successfully.",
               content = {
                     @Content(mediaType = "application/vnd.api+json", schema = @Schema(implementation = OAuth2CustomAttributesDataResourceDocument.class)),
                     @Content(mediaType = "application/json", schema = @Schema(implementation = OAuth2CustomAttributesDataResourceDocument.class))
               }
            ),
            @ApiResponse(responseCode = "204", description = "The OAuth 2.0 session has not been changed."),
            @ApiResponse(responseCode = "401", description = "The access token is missing or invalid."),
            @ApiResponse(
               responseCode = "404",
               description = "<div class=\"iam-error-code\"><table>\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"
            ),
            @ApiResponse(responseCode = "409", description = "The given session ID in the request document does not match the session of the access token."),
            @ApiResponse(responseCode = "501", description = "The session management endpoint is not configured.")
      }
   )
   @PATCH
   @Path("/custom-attributes")
   @Operation(
      description = "Updates custom attributes of the current OAuth 2.0 session. The JSON:API request resource object must include\nthe session ID as top-level <tt>id</tt> member. An access token in the bearer token format\nmust be provided as authorization.\n<div class=\"iam-resource-return\">The updated OAuth 2.0 session or an error document.</div>",
      tags = "OAuth 2.0/OIDC",
      summary = "Update attributes"
   )
   @Consumes({"application/vnd.api+json", "application/json"})
   @Produces({"application/vnd.api+json", "application/json;qs=0.9"})
   public Response updateSession(
      @Parameter(description = "The authorization server ID.") @PathParam("authorizationServerId") String authorizationServerId,
      @NotNull @Valid @RequestBody(
   description = "The document containing the custom attributes that should be updated. See example for details.",
   content = {
         @Content(
            mediaType = "application/vnd.api+json",
            examples = @ExampleObject(
               name = "Session data",
               value = "{\n\t\"data\" : {\n\t\t\"type\" : \"oauth2-session-custom-attributes\",\n\t\t\"id\" : \"kr01EzwkLbEM9mOyz5yRQQp3foUmxpCnfkyPw6sQ\",\n\t\t\"attributes\" : {\n\t\t\t\"UpdatableAttribute\" : \"someAllowedValue\",\n\t\t\t\"NotUpdatableAttribute\" : \"someAllowedValue\"\n\t\t}\n\t}\n}\n"
            )
         ),
         @Content(
            mediaType = "application/json",
            examples = @ExampleObject(
               name = "Session data",
               value = "{\n\t\"data\" : {\n\t\t\"type\" : \"oauth2-session-custom-attributes\",\n\t\t\"id\" : \"kr01EzwkLbEM9mOyz5yRQQp3foUmxpCnfkyPw6sQ\",\n\t\t\"attributes\" : {\n\t\t\t\"UpdatableAttribute\" : \"someAllowedValue\",\n\t\t\t\"NotUpdatableAttribute\" : \"someAllowedValue\"\n\t\t}\n\t}\n}\n"
            )
         )
   }
) RequestDocument<OAuth2SessionUpdateRequest> requestDocument
   ) {
      ((AuthorizationServerIdValidator)this.authorizationServerIdValidator.get()).validateAuthorizationServer(authorizationServerId);
      if (!((OAuth2SessionManagementEndpointAvailability)this.sessionManagementEndpointAvailability.get()).isAvailable()) {
         return ErrorResponse.notImplemented().build().toResponse();
      }

      Result<OAuth2AccessInfo, OAuth2ValidationResult> accessResult = ((OAuth2SessionManagementAccess)this.accessValidator.get())
         .validateAccess((HttpServletRequest)this.request.get());
      if (accessResult.isFailure()) {
         return ErrorResponse.unauthorized().build().toResponse();
      }

      OAuth2AccessInfo accessInfo = accessResult.get();
      OAuth2SessionId sessionId = accessInfo.getAccessToken().getSessionId();
      ValidationResult sessionIdValidationResult = OAuth2SessionIdValidator.validateSessionId(sessionId.asString(), requestDocument);
      if (sessionIdValidationResult.isNotValid()) {
         return ((ValidationToErrorMapper)this.errorMapper.get()).mapToResponse(sessionIdValidationResult.getErrors());
      }

      OAuth2SessionUpdateRequest updateRequest = RequestDocumentAttributesMissingException.checkAttributesPresent(requestDocument);
      if (updateRequest.getCustomAttributes().isEmpty()) {
         return Response.noContent().build();
      }

      Optional<OAuth2ExtendedSession> oAuth2Session = ((OAuth2ExtendedSessionService)this.sessionService.get()).findOAuth2Session(sessionId);
      if (oAuth2Session.isEmpty()) {
         return ErrorResponse.unauthorized().build().toResponse();
      }

      ValidationResult validationResult = ((OAuth2SessionAttributeUpdateService)this.sessionAttributeService.get())
         .validateAndExecuteUpdateRequest(oAuth2Session.get(), getUpdatedAttributes(updateRequest));
      if (validationResult.isNotValid()) {
         return ((ValidationToErrorMapper)this.errorMapper.get()).withJsonApiAttributesMapper().mapToResponse(validationResult.getErrors());
      }

      OAuth2ExtendedSession updatedSession = ((OAuth2ExtendedSessionService)this.sessionService.get()).updateCustomAttributes(oAuth2Session.get());
      ResourceDocument<OAuth2CustomAttributesData> document = ((RestMapper)this.restMapper.get())
         .forElement(updatedSession, OAuth2CustomAttributesData.class)
         .withIdSupplier(s -> s.id().asString())
         .toResourceDocument();
      return Response.ok(document).build();
   }

   private static List<OAuth2SessionAttributeUpdateService.AttributeUpdate> getUpdatedAttributes(OAuth2SessionUpdateRequest updateRequest) {
      return updateRequest.getCustomAttributes()
         .entrySet()
         .stream()
         .map(e -> new OAuth2SessionAttributeUpdateService.AttributeUpdate(e.getKey(), e.getValue()))
         .toList();
   }
}
