package com.airlock.iam.admin.infrastructure.resource.prot.token;

import com.airlock.iam.admin.application.service.token.airlock2fa.Airlock2FAHardwareTokenService;
import com.airlock.iam.admin.infrastructure.generated.transfer.response.user.tokens.Airlock2FAHardwareTokenDataCollectionDocument;
import com.airlock.iam.admin.infrastructure.transfer.request.SynchronizeAirlock2FAHardwareTokenRequest;
import com.airlock.iam.admin.infrastructure.transfer.response.user.tokens.Airlock2FAHardwareTokenData;
import com.airlock.iam.base.api.domain.model.exception.FailureException;
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.infrastructure.validation.SanitizedPageParameter;
import com.airlock.iam.base.infrastructure.validation.ValidPageParameter;
import com.airlock.iam.factor.domain.model.airlock2fa.HardwareTokenArchiveError;
import com.airlock.iam.factor.domain.model.airlock2fa.HardwareTokenSynchronizeError;
import com.airlock.iam.factor.domain.model.airlock2fa.hardware.Airlock2FAAdminHardwareToken;
import com.airlock.iam.factor.domain.model.airlock2fa.hardware.Airlock2FAAdminHardwareTokenSynchronizationPasscodes;
import com.airlock.iam.factor.domain.model.airlock2fa.hardware.Airlock2FAHardwareTokenId;
import com.airlock.iam.factor.domain.model.airlock2fa.hardware.Airlock2FAHardwareTokenFilter.Airlock2FAHardwareTokenFilterBuilder;
import com.airlock.iam.factor.domain.model.airlock2fa.otp.Airlock2FAPasscode;
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.validation.Valid;
import jakarta.validation.constraints.NotNull;
import jakarta.ws.rs.BeanParam;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
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 java.util.List;
import java.util.Optional;

@Path("/tokens/airlock-2fa/hardware-tokens")
public class Airlock2FAHardwareTokenResource {
   private final Provider<Airlock2FAHardwareTokenService> service;
   private final RestMapper mapper;
   private final Airlock2FAHardwareTokenFilterValidator validator;

   @Inject
   public Airlock2FAHardwareTokenResource(Provider<Airlock2FAHardwareTokenService> service, RestMapper mapper, Airlock2FAHardwareTokenFilterValidator validator) {
      this.service = service;
      this.mapper = mapper;
      this.validator = validator;
   }

   @ApiResponse(
      responseCode = "200",
      description = "Retrieved Airlock 2FA tokens.",
      content = {
            @Content(mediaType = "application/vnd.api+json", schema = @Schema(implementation = Airlock2FAHardwareTokenDataCollectionDocument.class)),
            @Content(mediaType = "application/json", schema = @Schema(implementation = Airlock2FAHardwareTokenDataCollectionDocument.class))
      }
   )
   @GET
   @Operation(
      tags = "Airlock 2FA",
      description = "Retrieves all Airlock 2FA hardware tokens.\n<ul>\n<li><b>filter</b>: Restricts the set of tokens to those matching the filter criteria. See <a href=\"#filtering\">Filtering</a>.\n<p>\nSupported filter parameters are:\n<ul>\n<li><tt>archived (boolean)</tt>: controls if only archived or non-archived tokens should be returned.</li>\n<li><tt>assigned (boolean)</tt>: controls if only assigned or unassigned tokens should be returned.</li>\n<li><tt>serialNumber (string)</tt>: filters the returned tokens by this serial number substring. At least 3 characters must be provided.</li>\n</ul>\nAND filtering is supported, OR filtering is currently not supported.\n</p>\n</li>\n</ul>\n<p>\nExample:\n<p>\n<tt>GET /rest/tokens/airlock-2fa/hardware-tokens?filter=assigned==false&amp;filter=archived==false&amp;filter=serialNumber=@abc</tt>\nretrieves all unarchived tokens that are not assigned to a user\nand contain abc in their serial number.\n</p>\n</p>\n<div class=\"iam-resource-return\">A collection document with all Airlock 2FA hardware devices.</div>",
      summary = "List hardware tokens"
   )
   @Produces({"application/vnd.api+json", "application/json;qs=0.9"})
   public Response retrieveHardwareTokens(
      @Parameter(description = "Restricts the set of tokens to those on the requested page.") @ValidPageParameter @BeanParam SanitizedPageParameter pageParameter,
      @Parameter(description = "See above") @BeanParam DocumentedFilterParameter filterParameter
   ) {
      Airlock2FAHardwareTokenFilterValidator.TokenFilterValidationResult validationResult = this.validator.validate(filterParameter);
      if (validationResult.errorResponse().isPresent()) {
         return validationResult.errorResponse().get();
      }

      Airlock2FAHardwareTokenFilterBuilder tokenFilter = validationResult.tokenFilterBuilder().orElseThrow();
      tokenFilter.offset(Integer.max(0, pageParameter.getOffset()));
      tokenFilter.limit(Integer.max(0, pageParameter.getLimit()));
      List<Airlock2FAAdminHardwareToken> tokens = ((Airlock2FAHardwareTokenService)this.service.get()).searchTokens(tokenFilter.build());
      return Response.ok(this.mapper.forEntities(tokens, Airlock2FAHardwareTokenData.class).toCollection()).build();
   }

   @ApiResponses(
      {
            @ApiResponse(responseCode = "204", description = "The hardware token has been archived or was already archived."),
            @ApiResponse(
               responseCode = "403",
               description = "The hardware token is assigned to a different service within the organization.",
               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 = "The hardware token was not found.",
               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"))
               }
            )
      }
   )
   @DELETE
   @Operation(
      tags = "Airlock 2FA",
      description = "Archives an Airlock 2FA hardware token. All assignments to Airlock 2FA accounts are removed upon archiving.\nUnlike unassignment, archiving results in a token that can no longer be assigned. It models a\nlost or broken hardware token.\n<div class=\"iam-resource-return\">Empty response</div>",
      summary = "Archive hardware token"
   )
   @Path("{tokenId}")
   @Produces({"application/vnd.api+json", "application/json;qs=0.9"})
   public Response archiveHardwareToken(@Parameter(description = "The ID of the hardware token to be archived.") @PathParam("tokenId") String tokenId) {
      Optional<HardwareTokenArchiveError> error = ((Airlock2FAHardwareTokenService)this.service.get())
         .archiveToken(Airlock2FAHardwareTokenId.airlock2FAHardwareTokenId(tokenId));
      if (error.isEmpty()) {
         return Response.noContent().build();
      }

      switch ((HardwareTokenArchiveError)error.get()) {
         case NOT_FOUND:
            return ErrorResponse.notFound().build().toResponse();
         case ASSIGNED_TO_DIFFERENT_SERVICE:
            return ErrorResponse.forbidden().build().toResponse();
         default:
            throw new FailureException("Unexpected error from service: " + error.get());
      }
   }

   @ApiResponses(
      {
            @ApiResponse(responseCode = "204", description = "The hardware token has been successfully synchronized."),
            @ApiResponse(
               responseCode = "400",
               description = "The request was invalid, the specified token does not support <tt>PASSCODE</tt> or the token could\nnot be synchronized.\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 = "403",
               description = "The hardware token is archived or assigned to a different service within the organization.",
               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 = "The hardware token was not found.",
               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"))
               }
            )
      }
   )
   @POST
   @Operation(
      tags = "Airlock 2FA",
      description = "Synchronizes the given hardware token with the backend.\nSynchronization becomes necessary if the clock drift between the hardware token and the backend becomes too large which\nresults in rejection of passcodes.\nUnder normal operation, small clock drifts are synchronized automatically upon successful usage.\n<p>\nThe synchronization is only possible for hardware tokens that support the <tt>PASSCODE</tt> factor.\n</p>\n<div class=\"iam-resource-return\">Empty response</div>",
      summary = "Sync hardware token"
   )
   @Path("{tokenId}/synchronize")
   @Produces({"application/vnd.api+json", "application/json;qs=0.9"})
   @Consumes("application/json")
   public Response synchronizeHardwareToken(
      @Parameter(description = "The ID of the hardware token to be synchronized.") @PathParam("tokenId") String tokenId,
      @Parameter(description = "The request containing the two subsequent passcodes required for synchronization.") @Valid @NotNull SynchronizeAirlock2FAHardwareTokenRequest request
   ) {
      Optional<HardwareTokenSynchronizeError> error = ((Airlock2FAHardwareTokenService)this.service.get())
         .synchronizeToken(
            Airlock2FAHardwareTokenId.airlock2FAHardwareTokenId(tokenId),
            Airlock2FAAdminHardwareTokenSynchronizationPasscodes.builder()
               .passcode1(Airlock2FAPasscode.airlock2FAPasscode(request.getPasscode1()))
               .passcode2(Airlock2FAPasscode.airlock2FAPasscode(request.getPasscode2()))
               .build()
         );
      if (error.isEmpty()) {
         return Response.noContent().build();
      }

      switch ((HardwareTokenSynchronizeError)error.get()) {
         case NOT_FOUND:
            return ErrorResponse.notFound().build().toResponse();
         case ASSIGNED_TO_DIFFERENT_SERVICE:
         case ARCHIVED:
            return ErrorResponse.forbidden().build().toResponse();
         case GENERIC_FAILURE:
            return ErrorResponse.badRequest().build().toResponse();
         default:
            throw new FailureException("Unexpected error from service: " + error.get());
      }
   }
}
