package com.airlock.iam.authentication.infrastructure.resource;

import ch.ergon.restal.jsonapi.document.data.ResourceDocument;
import ch.ergon.restal.jsonapi.document.data.ResourceObject;
import com.airlock.iam.authentication.application.service.AuthenticationInformationService;
import com.airlock.iam.authentication.infrastructure.generated.transfer.response.AuthenticationDataResourceDocument;
import com.airlock.iam.authentication.infrastructure.transfer.response.AuthenticationData;
import com.airlock.iam.base.api.domain.model.user.UserId;
import com.airlock.iam.base.api.infrastructure.uri.ExternalUriInfo;
import com.airlock.iam.base.infrastructure.error.ErrorResponse;
import com.airlock.iam.common.api.domain.model.user.UserIdentity;
import com.airlock.iam.common.api.infrastructure.annotation.DisableCsrfFilter;
import com.airlock.iam.common.api.infrastructure.annotation.DisableResponseDelay;
import com.airlock.iam.common.api.infrastructure.annotation.DisableSystemAvailabilityCheck;
import com.airlock.iam.common.api.infrastructure.annotation.RequiresWritableSession;
import com.airlock.iam.common.application.jsonapi.ResourceIdentifierUtils;
import com.airlock.iam.common.application.service.logoutaction.LogoutActionService;
import com.airlock.iam.common.domain.model.principal.UserPrincipal;
import com.airlock.iam.common.domain.model.user.PrincipalInfo;
import com.airlock.iam.common.domain.service.request.AirlockEnvCookiesInformationService;
import com.airlock.iam.flow.shared.api.domain.model.session.UserSession;
import com.airlock.iam.flow.shared.domain.model.idpropagation.SingleLogoutRedirectUrisAttribute;
import com.airlock.iam.log.api.helpdesk.HelpdeskLogger;
import io.swagger.v3.oas.annotations.Operation;
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.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.ResponseBuilder;
import jakarta.ws.rs.core.Response.Status;
import java.net.URI;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.function.Predicate;
import ma.glasnost.orika.MapperFacade;

@Path("/")
public final class AuthenticationFlowResource {
   private static final String SLO_HEADER_NAME_PREFIX = "X-SLO-REDIRECT-URI";
   private final HelpdeskLogger helpdeskLogger;
   private final Provider<UserSession> userSession;
   private final Provider<UserPrincipal> principal;
   private final Provider<PrincipalInfo> principalInfo;
   private final Provider<LogoutActionService> logoutActionService;
   private final MapperFacade mapper;
   private final Provider<AuthenticationInformationService> informationService;

   @Inject
   public AuthenticationFlowResource(
      HelpdeskLogger helpdeskLogger,
      Provider<UserSession> userSession,
      Provider<UserPrincipal> principal,
      Provider<PrincipalInfo> principalInfo,
      Provider<LogoutActionService> logoutActionService,
      MapperFacade mapper,
      Provider<AuthenticationInformationService> informationService
   ) {
      this.helpdeskLogger = helpdeskLogger;
      this.userSession = userSession;
      this.principal = principal;
      this.principalInfo = principalInfo;
      this.logoutActionService = logoutActionService;
      this.mapper = mapper;
      this.informationService = informationService;
   }

   @ApiResponse(responseCode = "204", description = "Authentication session successfully terminated.")
   @Operation(
      description = "Terminates the authenticated user session. If IAM is behind an Airlock WAF,\nthe WAF session is terminated, even if no user session exists\nlocally in IAM.\n<p>\nOn logout, IAM may send a series of single logout URIs in separate headers to the client. A UI client (typically an SPA) is required\nto execute a redirect to each of the returned URIs before displaying any kind of 'after logout target'.\nThe headers are named <tt>X-SLO-REDIRECT-URI(i)</tt>, where i runs from 1 to the number of sent single logout URIs.\n<p>\nExample: <tt>X-SLO-REDIRECT-URI1</tt>, <tt>X-SLO-REDIRECT-URI2</tt>, <tt>X-SLO-REDIRECT-URI3</tt>...\n</p>\nThe logout URIs can either be absolute URIs (including protocol, host and optional port) or they can be relative to the current host. Relative URIs start with a '/'.\n</p>\n<div class=\"iam-resource-return\">the response</div>",
      tags = "Flow Control",
      summary = "Terminate session"
   )
   @DELETE
   @Produces({"application/vnd.api+json", "application/json;qs=0.9"})
   @DisableCsrfFilter(AuthenticationFlowResource.HasWafSessionTerminationReason.class)
   @DisableResponseDelay
   @DisableSystemAvailabilityCheck
   public Response logout() {
      Optional<UserId> userId = ((PrincipalInfo)this.principalInfo.get()).getKnownIdentity().map(UserIdentity::id);
      ((LogoutActionService)this.logoutActionService.get()).executeActions();
      Response response = this.addSloRedirectUris(Response.noContent()).build();
      ((UserSession)this.userSession.get()).terminate();
      this.helpdeskLogger.log(userId.orElse(null), "User called logout");
      return response;
   }

   private ResponseBuilder addSloRedirectUris(ResponseBuilder responseBuilder) {
      List<URI> sloUris = ((UserSession)this.userSession.get())
         .userSessionAttributes()
         .stream()
         .filter(SingleLogoutRedirectUrisAttribute.class::isInstance)
         .map(SingleLogoutRedirectUrisAttribute.class::cast)
         .map(SingleLogoutRedirectUrisAttribute::getSloRedirectUris)
         .flatMap(Collection::stream)
         .toList();

      for (int i = 1; i <= sloUris.size(); i++) {
         responseBuilder.header("X-SLO-REDIRECT-URI" + i, sloUris.get(i - 1).toString());
      }

      return responseBuilder;
   }

   @ApiResponses(
      {
            @ApiResponse(
               responseCode = "200",
               description = "Retrieved the user's authentication information.",
               content = {
                     @Content(mediaType = "application/vnd.api+json", schema = @Schema(implementation = AuthenticationDataResourceDocument.class)),
                     @Content(mediaType = "application/json", schema = @Schema(implementation = AuthenticationDataResourceDocument.class))
               }
            ),
            @ApiResponse(
               responseCode = "403",
               description = "User is not identified by a previous step.",
               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"))
               }
            )
      }
   )
   @Operation(
      description = "Retrieves the user's authentication information.\n<div class=\"iam-resource-return\">The authentication information.</div>",
      tags = "Flow Control",
      summary = "Get auth info"
   )
   @GET
   @Produces({"application/vnd.api+json", "application/json;qs=0.9"})
   @DisableResponseDelay
   @RequiresWritableSession
   public Response retrieveAuthenticationData() {
      if (!((AuthenticationInformationService)this.informationService.get()).isAuthenticationInformationAccessible()) {
         return ErrorResponse.forbidden().build().toResponse();
      }

      AuthenticationData authData = (AuthenticationData)this.mapper.map(((UserPrincipal)this.principal.get()).user(), AuthenticationData.class);
      ResourceDocument<AuthenticationData> document = ResourceObject.builder(ResourceIdentifierUtils.getIdFor(this.principal.get()), authData).document();
      return Response.status(Status.OK).entity(document).build();
   }

   public static final class HasWafSessionTerminationReason implements Predicate<ExternalUriInfo> {
      private final Provider<AirlockEnvCookiesInformationService> requestCookieService;

      @Inject
      HasWafSessionTerminationReason(Provider<AirlockEnvCookiesInformationService> requestCookieService) {
         this.requestCookieService = requestCookieService;
      }

      public boolean test(ExternalUriInfo uriInfo) {
         return ((AirlockEnvCookiesInformationService)this.requestCookieService.get()).getWafSessionTerminationReason().isPresent();
      }
   }
}
