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

import com.airlock.iam.authentication.domain.model.secretquestions.SecretQuestionStatus;
import com.airlock.iam.authentication.domain.model.secretquestions.SecretQuestionStatusList;
import com.airlock.iam.authentication.domain.model.secretquestions.SecretQuestionsProvision;
import com.airlock.iam.authentication.domain.model.secretquestions.SecretQuestionsProvisioningStep;
import com.airlock.iam.authentication.infrastructure.generated.transfer.response.SecretQuestionStatusDataCollectionDocument;
import com.airlock.iam.authentication.infrastructure.transfer.request.SecretQuestionsProvisionRequest;
import com.airlock.iam.authentication.infrastructure.transfer.response.SecretQuestionStatusData;
import com.airlock.iam.base.api.domain.model.exception.FailureException;
import com.airlock.iam.base.api.domain.model.validation.ValidationResult;
import com.airlock.iam.base.infrastructure.mapper.ValidationToErrorMapper;
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.domain.model.ValidationStepResultData;
import com.airlock.iam.flow.domain.model.flow.FlowProvider;
import com.airlock.iam.flow.infrastructure.mapper.FlowResultToCollectionDocumentMapper;
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.ExampleObject;
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.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 ma.glasnost.orika.MapperFacade;

@Path("/secret-questions")
public class SecretQuestionsProvisioningFlowResource {
   private static final String SECRET_QUESTIONS_EXAMPLE = "{\n\t\"meta\": {\n\t\"type\": \"jsonapi.metadata.document\",\n\t\"timestamp\": \"2020-02-07T07:37:10.529+01:00\",\n\t\"missingSecretQuestionAnswers\": 1\n\t},\n\t\"data\": [\n\t\t{\n\t\t\t\"type\": \"authentication.secret-question.status\",\n\t\t\t\"id\": \"secretquestion.pet\",\n\t\t\t\"attributes\": {\n\t\t\t\t \"answerProvisioned\": true\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t \"type\": \"authentication.secret-question.status\",\n\t\t\t \"id\": \"secretquestion.friend\",\n\t\t\t \"attributes\": {\n\t\t\t\t \"answerProvisioned\": false\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"type\": \"authentication.secret-question.status\",\n\t\t\t\"id\": \"secretquestion.mother\",\n\t\t\t\"attributes\": {\n\t\t\t\"answerProvisioned\": false\n\t\t\t}\n\t\t}\n\t]\n}\n";
   private final FlowProvider flowProvider;
   private final FlowResultMapper mapper;
   private final MapperFacade mapperFacade;
   private final ValidationToErrorMapper errorMapper;

   @Inject
   public SecretQuestionsProvisioningFlowResource(
      FlowProvider flowProvider, FlowResultMapper mapper, MapperFacade mapperFacade, ValidationToErrorMapper errorMapper
   ) {
      this.flowProvider = flowProvider;
      this.mapper = mapper;
      this.mapperFacade = mapperFacade;
      this.errorMapper = errorMapper;
   }

   @ApiResponse(
      responseCode = "200",
      description = "Available questions with their provisioning state retrieved.",
      content = {
            @Content(
               mediaType = "application/vnd.api+json",
               schema = @Schema(implementation = SecretQuestionStatusDataCollectionDocument.class),
               examples = @ExampleObject(
                  name = "Secret questions",
                  value = "{\n\t\"meta\": {\n\t\"type\": \"jsonapi.metadata.document\",\n\t\"timestamp\": \"2020-02-07T07:37:10.529+01:00\",\n\t\"missingSecretQuestionAnswers\": 1\n\t},\n\t\"data\": [\n\t\t{\n\t\t\t\"type\": \"authentication.secret-question.status\",\n\t\t\t\"id\": \"secretquestion.pet\",\n\t\t\t\"attributes\": {\n\t\t\t\t \"answerProvisioned\": true\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t \"type\": \"authentication.secret-question.status\",\n\t\t\t \"id\": \"secretquestion.friend\",\n\t\t\t \"attributes\": {\n\t\t\t\t \"answerProvisioned\": false\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"type\": \"authentication.secret-question.status\",\n\t\t\t\"id\": \"secretquestion.mother\",\n\t\t\t\"attributes\": {\n\t\t\t\"answerProvisioned\": false\n\t\t\t}\n\t\t}\n\t]\n}\n"
               )
            ),
            @Content(
               mediaType = "application/json",
               schema = @Schema(implementation = SecretQuestionStatusDataCollectionDocument.class),
               examples = @ExampleObject(
                  name = "Secret questions",
                  value = "{\n\t\"meta\": {\n\t\"type\": \"jsonapi.metadata.document\",\n\t\"timestamp\": \"2020-02-07T07:37:10.529+01:00\",\n\t\"missingSecretQuestionAnswers\": 1\n\t},\n\t\"data\": [\n\t\t{\n\t\t\t\"type\": \"authentication.secret-question.status\",\n\t\t\t\"id\": \"secretquestion.pet\",\n\t\t\t\"attributes\": {\n\t\t\t\t \"answerProvisioned\": true\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t \"type\": \"authentication.secret-question.status\",\n\t\t\t \"id\": \"secretquestion.friend\",\n\t\t\t \"attributes\": {\n\t\t\t\t \"answerProvisioned\": false\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"type\": \"authentication.secret-question.status\",\n\t\t\t\"id\": \"secretquestion.mother\",\n\t\t\t\"attributes\": {\n\t\t\t\"answerProvisioned\": false\n\t\t\t}\n\t\t}\n\t]\n}\n"
               )
            )
      }
   )
   @Deprecated
   @POST
   @Operation(
      description = "> [!caution]\n> This endpoint is deprecated. The secret question provisioning status is returned as\n<a href=\"#additionalAuthAttributes\">additional attributes</a> with the <tt>SECRET_QUESTIONS_PROVISIONING_REQUIRED</tt> next step code.\n<p>Retrieves the provisioning state of secret questions for the current user.</p>\n<div class=\"iam-resource-return\">All secret questions that are currently configured.\nFor each question, it is stated whether the current user has already provisioned an answer.\nAdditionally, the response tells the client how many additional answers must be provisioned (at least) before this step can be finished successfully.\nSee examples for more details.\n</div>",
      tags = "Secret Questions",
      summary = "Get state"
   )
   @Consumes("application/json")
   @Produces({"application/vnd.api+json", "application/json;qs=0.9"})
   @Path("/status/retrieve")
   public Response retrieveSecretQuestions() {
      FlowResult result = this.flowProvider.getCurrentOrStartNew().execute(SecretQuestionsProvisioningStep::retrieve, SecretQuestionsProvisioningStep.class);
      return FlowResultToCollectionDocumentMapper.flowResultDataToCollectionDocumentMapper(this.mapper, this.mapperFacade)
         .map(result, SecretQuestionStatusList.class, SecretQuestionStatusData.class, SecretQuestionStatus::getId);
   }

   @ApiResponses(
      {
            @ApiResponse(
               responseCode = "200",
               description = "Provisioning of all requested secret questions was successful.\n<br>Possible next steps: <tt>SECRET_QUESTIONS_PROVISIONING_REQUIRED</tt>.\n",
               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>If one or more answers could not be accepted all answers will be discarded.\nEvery invalid answer comes with a detailed validation failure. In addition to the generic <a href=\"#validationFailures\">validation failures</a>, the following validation failures may also\noccur:\n\t<ul>\n\t    <li><tt>NOT_FOUND</tt>: if the provided secret question resource key does not exist;</li>\n\t    <li><tt>CONFLICT</tt>: if the provided answer duplicates an existing answer and the configuration of secret questions forbids duplicates.</li>\n\t</ul>\n\t</td>\n</tr>\n<tr>\n</table>\n</div>\n<br>Next step: <tt>SECRET_QUESTIONS_PROVISIONING_REQUIRED</tt>\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"))
               }
            )
      }
   )
   @POST
   @Operation(
      description = "Provisions answers to secret questions.\n<div class=\"iam-resource-return\">Whether the provisioning of secret questions was accepted and set.\n This endpoint returns a <tt>nextAuthStep</tt> with\n value <tt>SECRET_QUESTIONS_PROVISIONING_REQUIRED</tt> if the user must answer additional secret questions.\n <p>\n On error, this endpoints returns a detailed list of validation failures.\n </p></div>",
      tags = "Secret Questions",
      summary = "Provision answers"
   )
   @Consumes("application/json")
   @Produces({"application/vnd.api+json", "application/json;qs=0.9"})
   @Path("/provision")
   public Response provisionSecretQuestions(
      @Parameter(description = "Contains the secret questions to be updated/answered.") @NotNull @Valid SecretQuestionsProvisionRequest request
   ) {
      FlowResult result = this.flowProvider
         .getCurrentOrStartNew()
         .execute(
            step -> step.provision((SecretQuestionsProvision)this.mapperFacade.map(request, SecretQuestionsProvision.class)),
            SecretQuestionsProvisioningStep.class
         );
      return result.isFailed() ? this.handleValidationErrors(result) : this.mapper.toResponse(result);
   }

   private Response handleValidationErrors(FlowResult result) {
      if (validationFailed(result)) {
         ValidationResult validationResult = provisioningValidationResult(result);
         if (!validationResult.isValid()) {
            return this.mapper
               .toResponse(result, this.errorMapper.withFieldToSourcePointerMapper(field -> "/answers/" + field).toErrorData(validationResult.getErrors()));
         }
      }

      return this.mapper.toResponse(result);
   }

   private static boolean validationFailed(FlowResult result) {
      return result.error().filter(reason -> reason.equals(GenericStepError.VALIDATION_FAILED)).isPresent();
   }

   private static ValidationResult provisioningValidationResult(FlowResult provisioningStepResult) {
      return provisioningStepResult.data(ValidationStepResultData.class)
         .map(ValidationStepResultData::result)
         .orElseThrow(() -> new FailureException("Validation failed step result without validation errors."));
   }
}
