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

import com.airlock.iam.authentication.infrastructure.generated.transfer.response.FidoAuthenticationChallengeDataResourceDocument;
import com.airlock.iam.authentication.infrastructure.transfer.response.FidoAuthenticationChallengeData;
import com.airlock.iam.factor.domain.model.fido.FidoAuthenticatorUnverifiedAssertionResponse;
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.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.FidoCheck;
import com.airlock.iam.flow.shared.application.service.fido.FidoCredentialDisplayNameChange;
import com.airlock.iam.flow.shared.infrastructure.transfer.request.fido.FidoAuthenticationPublicKeyCredentialRequest;
import com.airlock.iam.flow.shared.infrastructure.transfer.request.fido.FidoClientFailureRequest;
import com.airlock.iam.flow.shared.infrastructure.transfer.request.fido.FidoCredentialDisplayNameChangeRequest;
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 lombok.NonNull;

@Path("/fido")
public class FidoAuthenticationFlowResource {
   private final Provider<FidoCheck> fidoAuthentication;
   private final FlowResultMapper mapper;
   private final Provider<FidoCredentialDisplayNameChange> displayNameChange;
   private final Provider<ValidationResultMapper> stepErrorResultMapper;

   @Inject
   public FidoAuthenticationFlowResource(
      @NonNull Provider<FidoCheck> fidoAuthentication,
      @NonNull FlowResultMapper mapper,
      @NonNull Provider<FidoCredentialDisplayNameChange> displayNameChange,
      @NonNull Provider<ValidationResultMapper> stepErrorResultMapper
   ) {
      if (fidoAuthentication == null) {
         throw new NullPointerException("fidoAuthentication is marked non-null but is null");
      }

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

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

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

      this.fidoAuthentication = fidoAuthentication;
      this.mapper = mapper;
      this.displayNameChange = displayNameChange;
      this.stepErrorResultMapper = stepErrorResultMapper;
   }

   @ApiResponse(
      responseCode = "200",
      description = "Further steps required.\n<br>Possible next step: <tt>FIDO_AUTHENTICATION_ASSERTION_RESPONSE_REQUIRED</tt> (as documented <a href=\"#nextAuthStepCodes\">here</a>).\n",
      content = {
            @Content(mediaType = "application/vnd.api+json", schema = @Schema(implementation = FidoAuthenticationChallengeDataResourceDocument.class)),
            @Content(mediaType = "application/json", schema = @Schema(implementation = FidoAuthenticationChallengeDataResourceDocument.class))
      }
   )
   @Deprecated
   @Operation(
      description = "> [!caution]\n> This endpoint is deprecated. The challenge information is returned as\n<a href=\"#additionalAuthAttributes\">additional attribute</a> with the <tt>FIDO_AUTHENTICATION_CHALLENGE_RETRIEVAL_REQUIRED</tt> next step code.\n<p>Retrieve FIDO challenge to be passed to FIDO authenticator.</p>\n<div class=\"iam-resource-return\">the authentication challenge</div>",
      tags = "FIDO",
      summary = "Get challenge"
   )
   @POST
   @Path("/challenge/retrieve")
   @Produces({"application/vnd.api+json", "application/json;qs=0.9"})
   public Response createAuthenticationChallenge() {
      FlowResult result = ((FidoCheck)this.fidoAuthentication.get()).retrieveAuthenticationChallenge();
      return this.mapper.toResponseWithData(result, FidoAuthenticationChallengeData.class);
   }

   @ApiResponses(
      {
            @ApiResponse(
               responseCode = "200",
               description = "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 = "403",
               description = "<div class=\"iam-error-code\"><table>\n<tr>\n<td>FIDO_AUTHENTICATION_FAILED</td>\n<td>The response of the FIDO authenticator to the previously obtained challenge could not be successfully verified.\nAs a result, the authentication has failed and the flow has been terminated. No retries are possible.</td>\n</tr>\n<tr>\n<td>FIDO_AUTHENTICATION_TIMEOUT</td>\n<td>The response of the FIDO authenticator has timed out.</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 = "Verify the FIDO authenticator's response.\n<p>The FIDO authenticator's response to the challenge (obtained previously as <a href=\"#additionalAuthAttributes\">additional attribute</a>\nwith the <tt>FIDO_AUTHENTICATION_CHALLENGE_RETRIEVAL_REQUIRED</tt> next step code) contains information about the FIDO credential\nused for this authentication attempt as well as metadata that will be verified by IAM.</p>\n<div class=\"iam-resource-return\">Whether the check was successful. 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 = "Verify response"
   )
   @POST
   @Path("/assertion-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 FidoAuthenticationPublicKeyCredentialRequest request
   ) {
      FlowResult result = ((FidoCheck)this.fidoAuthentication.get())
         .verify(
            FidoAuthenticatorUnverifiedAssertionResponse.builder()
               .publicKeyCredentialId(request.getPublicKeyCredential().getId())
               .authenticatorData(request.getPublicKeyCredential().getResponse().getAuthenticatorData())
               .signature(request.getPublicKeyCredential().getResponse().getSignature())
               .userHandle(request.getPublicKeyCredential().getResponse().getUserHandle())
               .clientDataJson(request.getPublicKeyCredential().getResponse().getClientDataJSON())
               .build()
         );
      return this.mapper.toResponse(result);
   }

   @ApiResponses(
      {
            @ApiResponse(
               responseCode = "200",
               description = "Display name successfully set.",
               content = {
                     @Content(mediaType = "application/json", schema = @Schema(implementation = AuthenticationFlowResultDataResourceDocument.class)),
                     @Content(mediaType = "application/json", schema = @Schema(implementation = AuthenticationFlowResultDataResourceDocument.class))
               }
            ),
            @ApiResponse(
               responseCode = "400",
               description = "<div class=\"iam-error-code\"><table>\n<tr>\n<td>VALIDATION_FAILED</td>\n<td>The supplied display name could not be validated successfully. See <a href=\"#validationFailures\">validation failures</a> for detail codes.</td>\n</tr>\n</table>\n</div>\n<br>Possible next step: <tt>FIDO_CREDENTIAL_EDIT_POSSIBLE</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 = "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 = "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(
      summary = "Edit name",
      description = "Edit FIDO credential display name. Allows editing the display name of the FIDO credential that has been registered previously in the flow.\n<div class=\"iam-resource-return\">Whether the new display name has been accepted and set. This endpoint returns a <tt>nextStep</tt> with\n value <tt>FIDO_CREDENTIAL_EDIT_POSSIBLE</tt> if the display name is valid and a follow-up call to the <tt>continue</tt> endpoint\n will advance the flow.\n <p>\n On error, this endpoint returns a detailed list of validation failures.\n </p></div>",
      tags = "FIDO"
   )
   @POST
   @Path("/credential/edit")
   @Consumes("application/json")
   @Produces({"application/vnd.api+json", "application/json;qs=0.9"})
   public Response edit(@Parameter(description = "the new display name to set") @NotNull @Valid FidoCredentialDisplayNameChangeRequest request) {
      FlowResult result = ((FidoCredentialDisplayNameChange)this.displayNameChange.get()).editDisplayName(request.getDisplayName());
      return ((ValidationResultMapper)this.stepErrorResultMapper.get()).toValidationResponse(result);
   }

   @ApiResponses(
      {
            @ApiResponse(
               responseCode = "200",
               description = "The display name is accepted and authentication flow completed if no 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 = "<div class=\"iam-error-code\"><table>\n<tr>\n<td>VALIDATION_FAILED</td>\n<td>The display name could not be validated successfully. See <a href=\"#validationFailures\">validation failures</a> for detail codes.</td>\n</tr>\n</table>\n</div>\n<br>Possible next step: <tt>FIDO_CREDENTIAL_EDIT_POSSIBLE</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 = "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 = "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(
      summary = "Validate and continue",
      description = "Validate the current FIDO credential display name and continue to the next step if possible.\n<div class=\"iam-resource-return\">If the attribute <tt>nextStep</tt> is present in the response, further steps are required to\n successfully complete the flow. If the attribute is missing, the user has successfully completed the authentication flow.\n <p>\n On error, this endpoint returns a detailed list of validation failures.\n </p></div>",
      tags = "FIDO"
   )
   @Path("credential/edit/continue")
   @POST
   @Produces({"application/vnd.api+json", "application/json;qs=0.9"})
   public Response validateAndContinue() {
      FlowResult result = ((FidoCredentialDisplayNameChange)this.displayNameChange.get()).validateAndContinue();
      return ((ValidationResultMapper)this.stepErrorResultMapper.get()).toValidationResponse(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<td>FIDO_AUTHENTICATION_ABORTED</td>\n\t<td>The FIDO authentication has been aborted in the client. Returned if the <tt>reason</tt> in the request was \"ABORTED\".</td>\n</tr>\n<tr>\n\t<td>FIDO_AUTHENTICATION_NOT_ALLOWED</td>\n\t<td>FIDO authentication was not allowed in the client. Returned if the <tt>reason</tt> in the request was \"NOT_ALLOWED\".</td>\n</tr>\n<tr>\n\t<td>FIDO_AUTHENTICATION_FAILED</td>\n\t<td>Returned if any other error occurred in the client. Returned if the <tt>reason</tt> in the request was \"UNKNOWN\".</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 was \"NO_WEB_AUTHN\".</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.<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 (name and message) describing the failure.") @Valid @NotNull FidoClientFailureRequest request
   ) {
      FidoClientFailure failure = new FidoClientFailure(FidoClientFailure.FidoClientFailureReason.valueOf(request.getReason().name()), request.getMessage());
      return this.mapper.toResponse(((FidoCheck)this.fidoAuthentication.get()).reportClientFailure(failure));
   }
}
