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

import ch.ergon.restal.jsonapi.document.data.CollectionDocument;
import ch.ergon.restal.jsonapi.parameter.FilterParameter;
import ch.ergon.restal.jsonapi.parameter.FilterParameter.Condition;
import ch.ergon.restal.jsonapi.parameter.FilterParameter.Disjunction;
import ch.ergon.restal.jsonapi.parameter.FilterParameter.Operator;
import com.airlock.iam.base.api.domain.model.user.UserId;
import com.airlock.iam.base.infrastructure.error.ErrorResponse;
import com.airlock.iam.base.infrastructure.mapper.entity.RestMapper;
import com.airlock.iam.base.infrastructure.validation.DocumentedFilterParameter;
import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.base.misc.result.Result;
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.log.api.helpdesk.HelpdeskLogger;
import com.airlock.iam.oauth2.api.domain.model.OAuth2ClientId;
import com.airlock.iam.oauth2.application.service.OAuth2ExtendedSessionService;
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.OAuth2SessionDataCollectionDocument;
import com.airlock.iam.oauth2.infrastructure.transfer.response.OAuth2SessionData;
import com.airlock.iam.oauth2.infrastructure.validation.AuthorizationServerIdValidator;
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.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.BeanParam;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Response;
import java.util.Collection;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.stream.Collectors;

@Path("/authorization-servers/{authorizationServerId}/sessions")
public class OAuth2SessionsResource {
   private static final ELogger LOG = ELoggerFactory.getLogger(OAuth2SessionsResource.class);
   private static final String EXCLUDE_CURRENT_SESSION_FILTER_PARAM = "excludeCurrent";
   private static final String EXCLUDE_CURRENT_SESSION_FILTER_VALUE = "true";
   private static final String CLIENT_ID_PROPERTY = "clientId";
   private static final String SESSIONS_RESPONSE_EXAMPLE = "{\n\t\"meta\": {\n\t\t\"type\": \"jsonapi.metadata.document\",\n\t\t\"timestamp\": \"2025-10-23T13:59:43.075+02:00\"\n\t},\n\t\"data\": [\n\t\t{\n\t\t\t\"type\": \"oauth2-session\",\n\t\t\t\"id\": \"rB8WpbiaX0lYwFJpirHkZjSJoklNp5EJ3Skt85aq\",\n\t\t\t\"attributes\": {\n\t\t\t\t\"customAttributes\": {\n\t\t\t\t\t\"someAttribute\": \"someValue\"\n\t\t\t\t},\n\t\t\t\t\"clientId\": \"oauth2-client\",\n\t\t\t\t\"consentTimestamp\": \"2025-10-23T13:55:42.974+02:00\",\n\t\t\t\t\"authenticationTimestamp\": \"2025-10-23T13:55:42.974+02:00\"\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"type\": \"oauth2-session\",\n\t\t\t\"id\": \"l8JfiS03azNxLiADlt5guwzPk0N0PEYOk9yS0wmA\",\n\t\t\t\"attributes\": {\n\t\t\t\t\"customAttributes\": {},\n\t\t\t\t\"clientId\": \"openid-client\",\n\t\t\t\t\"consentTimestamp\": \"2025-10-23T12:15:44.826+02:00\",\n\t\t\t\t\"authenticationTimestamp\": \"2025-10-23T12:15:44.826+02:00\"\n\t\t\t}\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<RestMapper> restMapper;
   private final Provider<HttpServletRequest> request;
   private final Provider<HelpdeskLogger> helpdeskLogger;
   private final Provider<AuthorizationServerIdValidator> authorizationServerIdValidator;

   @Inject
   public OAuth2SessionsResource(
      Provider<OAuth2SessionManagementEndpointAvailability> sessionManagementEndpointAvailability,
      Provider<OAuth2SessionManagementAccess> accessValidator,
      Provider<OAuth2ExtendedSessionService> sessionService,
      Provider<RestMapper> restMapper,
      Provider<HttpServletRequest> request,
      Provider<HelpdeskLogger> helpdeskLogger,
      Provider<AuthorizationServerIdValidator> authorizationServerIdValidator
   ) {
      this.sessionManagementEndpointAvailability = sessionManagementEndpointAvailability;
      this.accessValidator = accessValidator;
      this.sessionService = sessionService;
      this.restMapper = restMapper;
      this.request = request;
      this.helpdeskLogger = helpdeskLogger;
      this.authorizationServerIdValidator = authorizationServerIdValidator;
   }

   @ApiResponses(
      {
            @ApiResponse(
               responseCode = "200",
               description = "The retrieved OAuth 2.0 sessions.",
               content = {
                     @Content(
                        mediaType = "application/vnd.api+json",
                        schema = @Schema(implementation = OAuth2SessionDataCollectionDocument.class),
                        examples = @ExampleObject(
                           name = "List Sessions",
                           value = "{\n\t\"meta\": {\n\t\t\"type\": \"jsonapi.metadata.document\",\n\t\t\"timestamp\": \"2025-10-23T13:59:43.075+02:00\"\n\t},\n\t\"data\": [\n\t\t{\n\t\t\t\"type\": \"oauth2-session\",\n\t\t\t\"id\": \"rB8WpbiaX0lYwFJpirHkZjSJoklNp5EJ3Skt85aq\",\n\t\t\t\"attributes\": {\n\t\t\t\t\"customAttributes\": {\n\t\t\t\t\t\"someAttribute\": \"someValue\"\n\t\t\t\t},\n\t\t\t\t\"clientId\": \"oauth2-client\",\n\t\t\t\t\"consentTimestamp\": \"2025-10-23T13:55:42.974+02:00\",\n\t\t\t\t\"authenticationTimestamp\": \"2025-10-23T13:55:42.974+02:00\"\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"type\": \"oauth2-session\",\n\t\t\t\"id\": \"l8JfiS03azNxLiADlt5guwzPk0N0PEYOk9yS0wmA\",\n\t\t\t\"attributes\": {\n\t\t\t\t\"customAttributes\": {},\n\t\t\t\t\"clientId\": \"openid-client\",\n\t\t\t\t\"consentTimestamp\": \"2025-10-23T12:15:44.826+02:00\",\n\t\t\t\t\"authenticationTimestamp\": \"2025-10-23T12:15:44.826+02:00\"\n\t\t\t}\n\t\t}\n\t]\n}\n"
                        )
                     ),
                     @Content(
                        mediaType = "application/json",
                        schema = @Schema(implementation = OAuth2SessionDataCollectionDocument.class),
                        examples = @ExampleObject(
                           name = "List Sessions",
                           value = "{\n\t\"meta\": {\n\t\t\"type\": \"jsonapi.metadata.document\",\n\t\t\"timestamp\": \"2025-10-23T13:59:43.075+02:00\"\n\t},\n\t\"data\": [\n\t\t{\n\t\t\t\"type\": \"oauth2-session\",\n\t\t\t\"id\": \"rB8WpbiaX0lYwFJpirHkZjSJoklNp5EJ3Skt85aq\",\n\t\t\t\"attributes\": {\n\t\t\t\t\"customAttributes\": {\n\t\t\t\t\t\"someAttribute\": \"someValue\"\n\t\t\t\t},\n\t\t\t\t\"clientId\": \"oauth2-client\",\n\t\t\t\t\"consentTimestamp\": \"2025-10-23T13:55:42.974+02:00\",\n\t\t\t\t\"authenticationTimestamp\": \"2025-10-23T13:55:42.974+02:00\"\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"type\": \"oauth2-session\",\n\t\t\t\"id\": \"l8JfiS03azNxLiADlt5guwzPk0N0PEYOk9yS0wmA\",\n\t\t\t\"attributes\": {\n\t\t\t\t\"customAttributes\": {},\n\t\t\t\t\"clientId\": \"openid-client\",\n\t\t\t\t\"consentTimestamp\": \"2025-10-23T12:15:44.826+02:00\",\n\t\t\t\t\"authenticationTimestamp\": \"2025-10-23T12:15:44.826+02:00\"\n\t\t\t}\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 = "#/components/schemas/OAuth2ErrorResponse"))
               }
            ),
            @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 = "#/components/schemas/OAuth2ErrorResponse"))
               }
            ),
            @ApiResponse(responseCode = "501", description = "The session management endpoint is not configured.")
      }
   )
   @GET
   @Operation(
      description = "Retrieves all valid OAuth 2.0 sessions of a user for a given access token. An access token in the bearer token format\nmust be provided as authorization.\n<p>\n<ul>\n<li><b>filter</b>: Restricts the sessions to be deleted. See <a href=\"#filtering\">Filtering</a>.\nThe following filters are available:\n<ul>\n<li><tt>clientId==client1</tt>: Get all sessions of a particular client.</li>\n</ul>\n</li>\n</ul>\n</p>\n<div class=\"iam-resource-return\">A collection document with all OAuth 2.0 sessions.</div>",
      tags = "OAuth 2.0/OIDC",
      summary = "List sessions"
   )
   @Produces({"application/vnd.api+json", "application/json;qs=0.9"})
   @RequiresWritableSession
   public Response retrieveSessions(
      @Parameter(description = "The authorization server ID.") @PathParam("authorizationServerId") String authorizationServerId,
      @Parameter(description = "Allows filtering the sessions by client ID. See examples for more details.") @BeanParam DocumentedFilterParameter filterParameter
   ) {
      ((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();
      UserId userId = UserId.userId(accessInfo.getValidUser().getName());
      Collection<OAuth2ExtendedSession> sessions = this.clientIdFilter(filterParameter)
         .map(clientId -> this.extendedSessionService().findOAuth2ExtendedSessions(userId, OAuth2ClientId.oAuth2ClientId(clientId)))
         .orElse(this.extendedSessionService().findOAuth2ExtendedSessions(userId));
      CollectionDocument<OAuth2SessionData> collection = ((RestMapper)this.restMapper.get())
         .forElements(sessions, OAuth2SessionData.class)
         .withIdSupplier(s -> s.id().asString())
         .toCollection();
      return Response.ok(collection).build();
   }

   @ApiResponses(
      {
            @ApiResponse(responseCode = "204", description = "All sessions successfully deleted."),
            @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 not configured.")
      }
   )
   @DELETE
   @Operation(
      description = "Deletes all sessions of a user for a given access token. An access token in the bearer token format\nmust be provided as authorization.\n<p>\n<ul>\n<li><b>filter</b>: Restricts the sessions to be deleted. See <a href=\"#filtering\">Filtering</a>.\nThe following filters are available:\n<ul>\n<li><tt>excludeCurrent==true</tt>: Delete all sessions except the one of the given access token.</li>\n</ul>\n</li>\n</ul>\n</p>\n<div class=\"iam-resource-return\">A response indicating whether the sessions were successfully deleted or not.</div>",
      tags = "OAuth 2.0/OIDC",
      summary = "Delete sessions"
   )
   @Produces({"application/vnd.api+json", "application/json;qs=0.9"})
   public Response deleteAllSessions(
      @Parameter(description = "The authorization server ID.") @PathParam("authorizationServerId") String authorizationServerId,
      @Parameter(description = "Restricts the sessions to be deleted.") @BeanParam DocumentedFilterParameter filterParameter
   ) {
      ((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();
      UserId userId = UserId.userId(accessInfo.getValidUser().getName());
      OAuth2SessionId currentSessionId = accessInfo.getAccessToken().getSessionId();
      Collection<OAuth2SessionId> sessionIds = this.extendedSessionService()
         .findOAuth2ExtendedSessions(userId)
         .stream()
         .filter(this.isSessionToBeDeleted(filterParameter, currentSessionId))
         .map(OAuth2ExtendedSession::id)
         .collect(Collectors.toList());
      ((OAuth2ExtendedSessionService)this.sessionService.get()).deleteOAuth2Sessions(sessionIds);
      ((HelpdeskLogger)this.helpdeskLogger.get())
         .log(
            userId,
            "OAuth 2.0 sessions "
               + sessionIds.stream().map(OAuth2SessionId::asString).collect(Collectors.joining(", ", "[", "]"))
               + " were deleted using an access token of client "
               + LogUtil.logSafeQuote(accessInfo.getAccessToken().getClientId())
         );
      return Response.noContent().build();
   }

   private Predicate<OAuth2ExtendedSession> isSessionToBeDeleted(FilterParameter filterParameter, OAuth2SessionId currentSessionId) {
      return this.hasExcludeCurrentSessionParam(filterParameter) ? Predicate.not(s -> s.id().equals(currentSessionId)) : s -> true;
   }

   private boolean hasExcludeCurrentSessionParam(FilterParameter filterParameter) {
      return filterParameter.parse().stream().map(Disjunction::getConditions).flatMap(Collection::stream).anyMatch(this::excludeCurrent);
   }

   private boolean excludeCurrent(Condition c) {
      return c.getOperator().equals(Operator.EQUALS) && "excludeCurrent".equalsIgnoreCase(c.getProperty()) && "true".equalsIgnoreCase(c.getValue());
   }

   private OAuth2ExtendedSessionService extendedSessionService() {
      return (OAuth2ExtendedSessionService)this.sessionService.get();
   }

   private Optional<String> clientIdFilter(FilterParameter filter) {
      return filter.parse()
         .stream()
         .map(Disjunction::getConditions)
         .flatMap(Collection::stream)
         .filter(OAuth2SessionsResource::hasKnownOperator)
         .filter(OAuth2SessionsResource::hasKnownProperty)
         .<String>map(Condition::getValue)
         .findFirst();
   }

   private static boolean hasKnownProperty(Condition condition) {
      boolean hasKnownProperty = "clientId".equals(condition.getProperty());
      if (!hasKnownProperty) {
         LOG.debug(
            String.format(
               "Skipping filter condition '%s %s %s' as it has an unknown property", condition.getProperty(), condition.getOperator(), condition.getValue()
            )
         );
      }

      return hasKnownProperty;
   }

   private static boolean hasKnownOperator(Condition condition) {
      boolean hasKnownOperator = Operator.EQUALS.equals(condition.getOperator());
      if (!hasKnownOperator) {
         LOG.debug(
            String.format(
               "Skipping filter condition '%s %s %s' as it has an unknown operator", condition.getProperty(), condition.getOperator(), condition.getValue()
            )
         );
      }

      return hasKnownOperator;
   }
}
