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

import ch.ergon.restal.jsonapi.document.data.ResourceDocument;
import ch.ergon.restal.jsonapi.document.data.ResourceObject;
import com.airlock.iam.common.application.jsonapi.ResourceIdentifierUtils;
import com.airlock.iam.core.domain.model.cronto.CrontoOtp;
import com.airlock.iam.flow.api.domain.model.flow.FlowResult;
import com.airlock.iam.flow.api.infrastructure.mapper.FlowResultMapper;
import com.airlock.iam.flow.infrastructure.mapper.FlowResultToCollectionDocumentMapper;
import com.airlock.iam.flow.shared.application.service.cronto.CrontoAuthentication;
import com.airlock.iam.flow.shared.domain.model.cronto.CrontoAuthChallenge;
import com.airlock.iam.flow.shared.domain.model.cronto.CrontoPushDeviceChoices;
import com.airlock.iam.flow.shared.infrastructure.transfer.request.GenericOtpCheckRequest;
import com.airlock.iam.publicselfservice.api.infrastructure.generated.transfer.response.PublicSelfServiceFlowResultDataResourceDocument;
import com.airlock.iam.publicselfservice.infrastructure.generated.transfer.PublicSelfServiceCrontoChallengeDataResourceDocument;
import com.airlock.iam.publicselfservice.infrastructure.generated.transfer.PublicSelfServiceCrontoPushDeviceDataCollectionDocument;
import com.airlock.iam.publicselfservice.infrastructure.transfer.PublicSelfServiceCrontoChallengeData;
import com.airlock.iam.publicselfservice.infrastructure.transfer.PublicSelfServiceCrontoPushDeviceData;
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.validation.constraints.Size;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.Response;
import ma.glasnost.orika.MapperFacade;

@Path("/approval/cronto")
public class PublicSelfServiceCrontoApprovalResource {
   private final FlowResultMapper mapper;
   private final MapperFacade mapperFacade;
   private final Provider<CrontoAuthentication> crontoAuthentication;

   @Inject
   public PublicSelfServiceCrontoApprovalResource(FlowResultMapper mapper, MapperFacade mapperFacade, Provider<CrontoAuthentication> crontoAuthentication) {
      this.mapper = mapper;
      this.mapperFacade = mapperFacade;
      this.crontoAuthentication = crontoAuthentication;
   }

   @ApiResponse(
      responseCode = "200",
      description = "A challenge has been returned.",
      content = {
            @Content(mediaType = "application/vnd.api+json", schema = @Schema(implementation = PublicSelfServiceCrontoChallengeDataResourceDocument.class)),
            @Content(mediaType = "application/json", schema = @Schema(implementation = PublicSelfServiceCrontoChallengeDataResourceDocument.class))
      }
   )
   @Deprecated
   @Operation(
      description = "> [!caution]\n> This endpoint is deprecated. The challenge information is returned as <a href=\"#additionalPublicSelfServiceAttributes\">additional attribute</a>\nwith the <tt>CRONTO_OTP_REQUIRED</tt> next step code.\n<p>Returns a challenge for Cronto public self-service.</p>\n<p>The challenge is delivered as a base64-encoded image ('cryptogram') and as a string representation for\napp-to-app authentication.</p>\n<p>\nThe two attributes \"onlineValidation\" and \"pushed\" have only informative character and indicate\nthe recommended behavior for clients:\n<ul>\n<li>If the \"onlineValidation\" attribute is true, the client application should not ask the user for the\nresponse OTP, as the app will send the response directly to the server (Scan&amp;Login or Push use-cases).\nInstead, the client application should regularly poll (<tt>POST /cronto/otp/poll</tt>) to check if a response\nhas been received. There should be an option for the user to fall back to entering the OTP manually. Sending the\nmanually entered OTP to (<tt>POST /cronto/otp/check</tt>) cancels the online validation</li>\n<li>If also the \"pushed\" attribute is true, the challenge has been pushed to a Cronto app. In this case also\nthe cryptogram should not be displayed. Instead, the user should be informed about this and asked to confirm\nthe transaction on their Cronto app. Again, a fallback for offline situations should be available.\nIf \"pushed\" is true, \"onlineValidation\" is always also true.</li>\n</ul>\n</p>\n<div class=\"iam-resource-return\">A Cronto challenge.</div>",
      tags = "Cronto",
      summary = "Get challenge"
   )
   @POST
   @Produces({"application/vnd.api+json", "application/json;qs=0.9"})
   @Path("/challenge/retrieve")
   public Response retrieveChallenge() {
      FlowResult result = ((CrontoAuthentication)this.crontoAuthentication.get()).getCurrentChallenge();
      if (result.isFailed()) {
         return this.mapper.toResponse(result);
      }

      CrontoAuthChallenge currentChallenge = result.<CrontoAuthChallenge>data().get();
      PublicSelfServiceCrontoChallengeData attributes = (PublicSelfServiceCrontoChallengeData)this.mapperFacade
         .map(currentChallenge, PublicSelfServiceCrontoChallengeData.class);
      ResourceDocument<PublicSelfServiceCrontoChallengeData> document = ResourceObject.builder(ResourceIdentifierUtils.getIdFor(attributes), attributes)
         .document();
      return Response.ok(document).build();
   }

   @ApiResponses(
      {
            @ApiResponse(
               responseCode = "200",
               description = "OTP successfully verified.",
               content = {
                     @Content(mediaType = "application/vnd.api+json", schema = @Schema(implementation = PublicSelfServiceFlowResultDataResourceDocument.class)),
                     @Content(mediaType = "application/json", schema = @Schema(implementation = PublicSelfServiceFlowResultDataResourceDocument.class))
               }
            ),
            @ApiResponse(
               responseCode = "400",
               description = "<div class=\"iam-error-code\"><table>\n<tr>\n<td>CRONTO_OTP_WRONG</td>\n<td>OTP could not be validated successfully. Retry with correct OTP.</td>\n</tr>\n</table>\n</div>\n<br>Possible next step: <tt>CRONTO_OTP_REQUIRED</tt> (as documented <a href=\"#nextPublicSelfServiceStepCodes\">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 = "<div class=\"iam-error-code\"><table>\n<tr>\n<td>CRONTO_OTP_WRONG</td>\n<td>The OTP could not be validated successfully and no retries are allowed.</td>\n</tr>\n</table>\n</div>\n<br>The current public self-service flow has been aborted.\n<br>Possible next step: none.\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 = "Verifies the submitted Cronto OTP.\nThe OTP was entered by the user (after scanning the cryptogram image) or returned by the Cronto app\n(when using the secure channel challenge in app-to-app communication).\n<p>Use this call for manual OTP validation for a <tt>CRONTO_OTP_REQUIRED</tt> next step\n(when \"onlineValidation\" was false, or offline fallback was selected by the user). Note that in any case\nmanually OTP checking <em>cancels online validation</em>. It is recommended to obtain the current\nchallenge again after each failed OTP check, as the \"onlineValidation\" flag could have changed.</p>\nNote that unlike identity verification, approval REST calls require an existing user and cannot prevent username enumeration.\n<div class=\"iam-resource-return\">Whether the check was successful.</div>",
      tags = "Cronto",
      summary = "Verify Cronto OTP"
   )
   @POST
   @Path("/otp/check")
   @Consumes("application/json")
   @Produces({"application/vnd.api+json", "application/json;qs=0.9"})
   public Response checkCrontoOtp(@Parameter(description = "Contains the Cronto OTP.") @NotNull @Valid GenericOtpCheckRequest request) {
      FlowResult result = ((CrontoAuthentication)this.crontoAuthentication.get()).checkOtp(new CrontoOtp(request.getOtp()));
      return this.mapper.toResponse(result);
   }

   @ApiResponses(
      {
            @ApiResponse(
               responseCode = "200",
               description = "OTP successfully verified.",
               content = {
                     @Content(mediaType = "application/vnd.api+json", schema = @Schema(implementation = PublicSelfServiceFlowResultDataResourceDocument.class)),
                     @Content(mediaType = "application/json", schema = @Schema(implementation = PublicSelfServiceFlowResultDataResourceDocument.class))
               }
            ),
            @ApiResponse(
               responseCode = "400",
               description = "Challenge not yet validated. Retry later or check OTP manually.",
               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>CRONTO_OTP_WRONG</td>\n<td>The OTP could not be validated successfully.</td>\n</tr>\n<tr>\n<td>CANCELLED_BY_USER</td>\n<td>The process has been cancelled by the user in the app.</td>\n</tr>\n</table>\n</div>\n<br>The current public self-service flow has been aborted.\n<br>Possible next step: none.\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 = "Allows polling whether the Cronto challenge has been answered by the app (\"online validation\"). If there\nhas not been any response from the app, keep polling in regular intervals or check the OTP manually\n(<tt>POST /cronto/otp/check</tt>).\n<div class=\"iam-resource-return\">The status of the online validation (still waiting/success/cancelled/failed).\n If the attribute <tt>nextStep</tt> is present in the response, further steps are required.</div>",
      tags = "Cronto",
      summary = "Poll status"
   )
   @POST
   @Path("/otp/poll")
   @Produces({"application/vnd.api+json", "application/json;qs=0.9"})
   public Response pollOnlineValidation(
      @Parameter(
   description = "Whether the final result should be returned (optional, default <tt>false</tt>). See <a href=\"#pollingEndpoints\">polling endpoints</a> for details."
) @QueryParam("queryOnly") boolean queryOnly
   ) {
      FlowResult result = ((CrontoAuthentication)this.crontoAuthentication.get()).pollOnlineValidation(queryOnly);
      return this.mapper.toResponse(result);
   }

   @ApiResponse(
      responseCode = "200",
      description = "Available push devices retrieved.",
      content = {
            @Content(mediaType = "application/vnd.api+json", schema = @Schema(implementation = PublicSelfServiceCrontoPushDeviceDataCollectionDocument.class)),
            @Content(mediaType = "application/json", schema = @Schema(implementation = PublicSelfServiceCrontoPushDeviceDataCollectionDocument.class))
      }
   )
   @Deprecated
   @Operation(
      description = "> [!caution]\n> This endpoint is deprecated. The device choices are returned as <a href=\"#additionalPublicSelfServiceAttributes\">additional attributes</a>\nwith the <tt>CRONTO_DEVICE_CHOICE_REQUIRED</tt> next step code.\n<p>Retrieves the set of active Cronto push devices.</p>\n<div class=\"iam-resource-return\">The collection of active Cronto push devices</div>",
      tags = "Cronto",
      summary = "List devices"
   )
   @POST
   @Produces({"application/vnd.api+json", "application/json;qs=0.9"})
   @Path("/push-devices/retrieve")
   public Response retrievePushDeviceChoices() {
      FlowResult result = ((CrontoAuthentication)this.crontoAuthentication.get()).getPushDeviceChoices();
      return FlowResultToCollectionDocumentMapper.flowResultDataToCollectionDocumentMapper(this.mapper, this.mapperFacade)
         .map(result, CrontoPushDeviceChoices.class, PublicSelfServiceCrontoPushDeviceData.class, device -> device.getId());
   }

   @ApiResponses(
      {
            @ApiResponse(
               responseCode = "200",
               description = "Successfully selected an offline device.",
               content = {
                     @Content(mediaType = "application/vnd.api+json", schema = @Schema(implementation = PublicSelfServiceFlowResultDataResourceDocument.class)),
                     @Content(mediaType = "application/json", schema = @Schema(implementation = PublicSelfServiceFlowResultDataResourceDocument.class))
               }
            ),
            @ApiResponse(
               responseCode = "403",
               description = "Offline selection failed. The current public self-service 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 = "Selects offline mode instead of a push device. No push notification is sent to a device and no subsequent online validation\nof the response will be possible.\n<div class=\"iam-resource-return\">Whether the selection of offline mode was successful. Gives information about the next step required.</div>",
      tags = "Cronto",
      summary = "Select offline mode"
   )
   @POST
   @Produces({"application/vnd.api+json", "application/json;qs=0.9"})
   @Path("/offline/")
   public Response selectOfflineAuthentication() {
      FlowResult result = ((CrontoAuthentication)this.crontoAuthentication.get()).selectOfflineAuthentication();
      return this.mapper.toResponse(result);
   }

   @ApiResponses(
      {
            @ApiResponse(
               responseCode = "200",
               description = "Push device successfully selected, notification sent.",
               content = {
                     @Content(mediaType = "application/vnd.api+json", schema = @Schema(implementation = PublicSelfServiceFlowResultDataResourceDocument.class)),
                     @Content(mediaType = "application/json", schema = @Schema(implementation = PublicSelfServiceFlowResultDataResourceDocument.class))
               }
            ),
            @ApiResponse(
               responseCode = "400",
               description = "<div class=\"iam-error-code\"><table>\n<tr>\n<td>CRONTO_DEVICE_CHOICE_FAILED</td>\n<td>Invalid ID selected. Retry with valid ID.</td>\n</tr>\n</table>\n</div>\n<br>Possible next step: <tt>CRONTO_DEVICE_CHOICE_REQUIRED</tt> (as documented <a href=\"#nextPublicSelfServiceStepCodes\">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 = "<div class=\"iam-error-code\"><table>\n<tr>\n<td>CRONTO_DEVICE_CHOICE_FAILED</td>\n<td>Invalid ID selected.</td>\n</tr>\n</table>\n</div>\n<br>The current public self-service flow has been aborted.\n<br>Possible next step: none.\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 = "Selects one of the active push devices to which the push notification will be sent.\n<div class=\"iam-resource-return\">Whether the device selection was successful. Gives information about the next step required.</div>",
      tags = "Cronto",
      summary = "Select push device"
   )
   @POST
   @Produces({"application/vnd.api+json", "application/json;qs=0.9"})
   @Path("/push-devices/{id}/select")
   public Response selectPushDevice(@Parameter(description = "The ID of the selected push device.") @PathParam("id") @Size(min = 1, max = 50) String id) {
      FlowResult result = ((CrontoAuthentication)this.crontoAuthentication.get()).selectPushDevice(id);
      return this.mapper.toResponse(result);
   }
}
