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

import com.airlock.iam.authentication.infrastructure.generated.transfer.response.FidoRegistrationAuthenticationChallengeDataResourceDocument;
import com.airlock.iam.authentication.infrastructure.transfer.response.FidoRegistrationAuthenticationChallengeData;
import com.airlock.iam.factor.domain.model.fido.FidoAuthenticatorUnverifiedAttestationResponse;
import com.airlock.iam.factor.domain.model.fido.FidoClientFailure;
import com.airlock.iam.flow.api.domain.model.flow.FlowResult;
import com.airlock.iam.flow.api.domain.model.step.GenericStepError;
import com.airlock.iam.flow.api.infrastructure.mapper.FlowResultMapper;
import com.airlock.iam.flow.auth.api.infrastructure.generated.transfer.response.AuthenticationFlowResultDataResourceDocument;
import com.airlock.iam.flow.infrastructure.mapper.ValidationResultMapper;
import com.airlock.iam.flow.shared.application.service.fido.FidoRegistration;
import com.airlock.iam.flow.shared.infrastructure.transfer.request.fido.FidoRegistrationAuthenticatorResponseRequest;
import com.airlock.iam.flow.shared.infrastructure.transfer.request.fido.FidoRegistrationChallengeRequest;
import com.airlock.iam.flow.shared.infrastructure.transfer.request.fido.FidoRegistrationClientFailureRequest;
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.Consumes;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Response;
import java.util.Collections;
import java.util.Set;
import lombok.NonNull;

@Path("/fido/registration")
public final class FidoRegistrationAuthenticationFlowResource {
   private final Provider<FidoRegistration> fidoRegistration;
   private final FlowResultMapper mapper;
   private final Provider<ValidationResultMapper> stepErrorResultMapper;

   @Inject
   public FidoRegistrationAuthenticationFlowResource(
      @NonNull Provider<FidoRegistration> fidoRegistration, @NonNull FlowResultMapper mapper, Provider<ValidationResultMapper> stepErrorResultMapper
   ) {
      if (fidoRegistration == null) {
         throw new NullPointerException("fidoRegistration is marked non-null but is null");
      }

      if (mapper == null) {
         throw new NullPointerException("mapper is marked non-null but is null");
      }

      this.fidoRegistration = fidoRegistration;
      this.mapper = mapper;
      this.stepErrorResultMapper = stepErrorResultMapper;
   }

   @ApiResponses(
      {
            @ApiResponse(
               responseCode = "200",
               description = "Challenge successfully generated. Possible next steps: <tt>FIDO_REGISTRATION_ATTESTATION_RESPONSE_REQUIRED</tt> (as documented <a href=\"#nextAuthStepCodes\">here</a>).",
               content = {
                     @Content(
                        mediaType = "application/vnd.api+json",
                        schema = @Schema(implementation = FidoRegistrationAuthenticationChallengeDataResourceDocument.class)
                     ),
                     @Content(
                        mediaType = "application/json",
                        schema = @Schema(implementation = FidoRegistrationAuthenticationChallengeDataResourceDocument.class)
                     )
               }
            ),
            @ApiResponse(
               responseCode = "400",
               description = "<div class=\"iam-error-code\"><table>\n<tr>\n<td>VALIDATION_FAILED</td>\n<td>The supplied display name is invalid. See <a href=\"#validationFailures\">validation failures</a> for detail codes.</td>\n</tr>\n</table>\n</div>\n<br>Possible next steps: <tt>FIDO_REGISTRATION_CHALLENGE_RETRIEVAL_REQUIRED</tt> (as documented <a href=\"#nextAuthStepCodes\">here</a>).\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 current authentication flow has been aborted.",
               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 = "Retrieve FIDO challenge.\n<p>This challenge contains information that need to be passed to the FIDO authenticator\nin order to create and register a new credential.</p>\n<div class=\"iam-resource-return\">Challenge to be passed to the FIDO authenticator</div>",
      tags = "FIDO",
      summary = "Get FIDO challenge"
   )
   @POST
   @Path("/challenge/retrieve")
   @Consumes("application/json")
   @Produces({"application/vnd.api+json", "application/json;qs=0.9"})
   public Response createRegistrationChallenge(
      @Parameter(
   description = "Contains the display name of the registered FIDO credential.\n<p>Note that rules specified for the Nickname Profile of the PRECIS FreeformClass (see <a href=\"https://tools.ietf.org/html/rfc8266#section-2.3\">RFC8266 - Section 2.3</a>) will be enforced.\nCertain characters may therefore be replaced (e.g., white-space will be trimmed) or the input may be deemed invalid.</p>"
) @Valid FidoRegistrationChallengeRequest request
   ) {
      String displayName = request.getDisplayName();
      FlowResult result = ((FidoRegistration)this.fidoRegistration.get()).retrieveRegistrationChallenge(displayName);
      return result.error().isPresent() && result.error().get().equals(GenericStepError.VALIDATION_FAILED)
         ? ((ValidationResultMapper)this.stepErrorResultMapper.get()).toValidationResponse(result)
         : this.mapper.toResponseWithData(result, FidoRegistrationAuthenticationChallengeData.class);
   }

   @ApiResponses(
      {
            @ApiResponse(
               responseCode = "200",
               description = "Registration is successfully completed. Authentication successful or further steps required.",
               content = {
                     @Content(mediaType = "application/vnd.api+json", schema = @Schema(implementation = AuthenticationFlowResultDataResourceDocument.class)),
                     @Content(mediaType = "application/json", schema = @Schema(implementation = AuthenticationFlowResultDataResourceDocument.class))
               }
            ),
            @ApiResponse(
               responseCode = "400",
               description = "The request was 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 = "401",
               description = "Invalid or missing authentication.",
               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 = "<div class=\"iam-error-code\"><table>\n<tr>\n<td>FIDO_REGISTRATION_ATTESTATION_INVALID</td>\n<td>The attestation object supplied by the FIDO authenticator could not be successfully verified.\nThis can happen if the issuer CA is not present in the keystore or if the authenticator does not provide an assertion object.</td>\n</tr>\n<tr>\n<td>FIDO_REGISTRATION_FAILED</td>\n<td>The response of the FIDO authenticator to the previously obtained challenge could not be successfully verified.</td>\n</tr>\n<tr>\n<td>FIDO_REGISTRATION_TIMEOUT</td>\n<td>The response of the FIDO authenticator has timed out.</td>\n</tr>\n</table>\n</div>\n<br>As a result, the registration failed and the flow has been terminated. No retries are possible.\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"))
               }
            )
      }
   )
   @Operation(
      description = "Verify FIDO authenticator's response.\n<p>The FIDO authenticator's response to the previously obtained challenge (<tt>POST /fido/registration/challenge/retrieve</tt>)\ncontains information about the newly created FIDO credential as well as metadata that will be verified by IAM.</p>\n<div class=\"iam-resource-return\">Status of registration</div>",
      tags = "FIDO",
      summary = "Verify Authenticator response"
   )
   @POST
   @Path("/attestation-response/check")
   @Consumes("application/json")
   @Produces({"application/vnd.api+json", "application/json;qs=0.9"})
   public Response verify(
      @Parameter(description = "Contains the FIDO authenticator's response to the previously obtained challenge") @NotNull @Valid FidoRegistrationAuthenticatorResponseRequest request
   ) {
      Set<String> transports = request.getPublicKeyCredential().getResponse().getTransports();
      FlowResult result = ((FidoRegistration)this.fidoRegistration.get())
         .verify(
            FidoAuthenticatorUnverifiedAttestationResponse.builder()
               .publicKeyCredentialId(request.getPublicKeyCredential().getId())
               .clientDataJson(request.getPublicKeyCredential().getResponse().getClientDataJSON())
               .attestationObject(request.getPublicKeyCredential().getResponse().getAttestationObject())
               .transports(transports != null ? transports : Collections.emptySet())
               .build()
         );
      return this.mapper.toResponse(result);
   }

   @ApiResponses(
      {
            @ApiResponse(
               responseCode = "200",
               description = "Authentication successful or further steps required. This is only possible if \"On Failure Gotos\" are configured on the step.",
               content = {
                     @Content(mediaType = "application/vnd.api+json", schema = @Schema(implementation = AuthenticationFlowResultDataResourceDocument.class)),
                     @Content(mediaType = "application/json", schema = @Schema(implementation = AuthenticationFlowResultDataResourceDocument.class))
               }
            ),
            @ApiResponse(
               responseCode = "400",
               description = "The request was 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 = "403",
               description = "<div class=\"iam-error-code\"><table>\n<tr>\n\t<td>FIDO_REGISTRATION_ABORTED</td>\n\t<td>The FIDO credential registration has been aborted in the client. Returned if the <tt>reason</tt> in the request is \"ABORTED\".</td>\n</tr>\n<tr>\n\t<td>FIDO_REGISTRATION_NOT_ALLOWED</td>\n\t<td>FIDO credential registration was not allowed in the client. Returned if the <tt>reason</tt> in the request is \"NOT_ALLOWED\".</td>\n</tr>\n<tr>\n\t<td>FIDO_REGISTRATION_NOT_SUPPORTED</td>\n\t<td>The browser/client could not find a FIDO credential that fulfills the registration requirements. Returned if the <tt>reason</tt> in the request is \"NOT_SUPPORTED\".</td>\n</tr>\n<tr>\n\t<td>FIDO_WEB_AUTHN_NOT_AVAILABLE</td>\n\t<td>The browser/client does not support WebAuthn/FIDO. Returned if the <tt>reason</tt> in the request is \"NO_WEB_AUTHN\".</td>\n</tr>\n<tr>\n\t<td>FIDO_REGISTRATION_FAILED</td>\n\t<td>Returned if any other error occurred in the client. Returned if the <tt>reason</tt> in the request is \"UNKNOWN\".</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"))
               }
            )
      }
   )
   @Operation(
      description = "Report a FIDO client failure during registration .<p>Allows the client to report a FIDO failure, resulting in a step failure.</p>\n<div class=\"iam-resource-return\">The status of the flow. If the attribute <tt>nextAuthStep</tt> is present in the response,\n further steps are required to successfully authenticate. If the attribute is missing, the user is\n successfully authenticated.</div>",
      tags = "FIDO",
      summary = "Report FIDO failure"
   )
   @POST
   @Path("/failure/report")
   @Consumes("application/json")
   @Produces({"application/vnd.api+json", "application/json;qs=0.9"})
   public Response handleClientFailure(
      @Parameter(description = "Contains information (reason and message) describing the failure.") @Valid @NotNull FidoRegistrationClientFailureRequest request
   ) {
      FidoClientFailure failure = new FidoClientFailure(FidoClientFailure.FidoClientFailureReason.valueOf(request.getReason().name()), request.getMessage());
      return this.mapper.toResponse(((FidoRegistration)this.fidoRegistration.get()).reportClientFailure(failure));
   }
}
