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

import ch.ergon.restal.jsonapi.document.error.ErrorData;
import com.airlock.iam.authentication.application.service.password.PasswordChange;
import com.airlock.iam.authentication.application.service.password.SetPassword;
import com.airlock.iam.authentication.application.service.password.UsernamePasswordAuthentication;
import com.airlock.iam.authentication.infrastructure.transfer.request.AuthenticationSetPasswordRequest;
import com.airlock.iam.authentication.infrastructure.transfer.request.PasswordChangeRequest;
import com.airlock.iam.authentication.infrastructure.transfer.request.UsernamePasswordCheckRequest;
import com.airlock.iam.base.api.domain.model.validation.ValidationResult;
import com.airlock.iam.base.infrastructure.mapper.FieldMapper;
import com.airlock.iam.base.infrastructure.mapper.ValidationToErrorMapper;
import com.airlock.iam.common.api.domain.model.password.Password;
import com.airlock.iam.common.domain.model.password.PasswordImpl;
import com.airlock.iam.common.infrastructure.mapper.PrefixFieldMapper;
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.infrastructure.mapper.PasswordPolicyViolationDataMapper;
import com.airlock.iam.login.application.service.attribute.RequestAttributeService;
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.List;
import java.util.Optional;

@Path("/password")
public class UsernamePasswordAuthenticationFlowResource {
   private final FlowResultMapper mapper;
   private final Provider<UsernamePasswordAuthentication> usernamePasswordAuthentication;
   private final Provider<PasswordChange> passwordChange;
   private final Provider<SetPassword> setPassword;
   private final Provider<RequestAttributeService> requestAttributeService;
   private final Provider<ValidationToErrorMapper> errorMapper;
   private final Provider<ValidationResultMapper> stepErrorResultMapper;

   @Inject
   public UsernamePasswordAuthenticationFlowResource(
      FlowResultMapper mapper,
      Provider<UsernamePasswordAuthentication> usernamePasswordAuthentication,
      Provider<PasswordChange> passwordChange,
      Provider<SetPassword> setPassword,
      Provider<RequestAttributeService> requestAttributeService,
      Provider<ValidationToErrorMapper> errorMapper,
      Provider<ValidationResultMapper> stepErrorResultMapper
   ) {
      this.mapper = mapper;
      this.usernamePasswordAuthentication = usernamePasswordAuthentication;
      this.passwordChange = passwordChange;
      this.setPassword = setPassword;
      this.requestAttributeService = requestAttributeService;
      this.errorMapper = errorMapper;
      this.stepErrorResultMapper = stepErrorResultMapper;
   }

   @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 = "<div class=\"iam-error-code\"><table>\n<tr>\n<td>USERNAME_PASSWORD_WRONG</td>\n<td>Username and/or password wrong.</td>\n</tr>\n</table>\n</div>\n<br>Possible next step: <tt>PASSWORD_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 = "> [!important]\n> <strong>CAPTCHA</strong>: When protection is enabled, a challenge is included in the previous response.\n<p>Checks a user's username and password.</p>\n<div class=\"iam-resource-return\">Whether authentication 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 successfully authenticated.</div>",
      tags = "Password",
      summary = "Check credentials"
   )
   @POST
   @Path("/check")
   @Consumes("application/json")
   @Produces({"application/vnd.api+json", "application/json;qs=0.9"})
   public Response checkPassword(
      @Parameter(description = "Contains the user's authentication credentials.") @NotNull @Valid UsernamePasswordCheckRequest request
   ) {
      ValidationResult validationResult = ((RequestAttributeService)this.requestAttributeService.get()).validate(request.getAttributes());
      if (!validationResult.isValid()) {
         return ((ValidationToErrorMapper)this.errorMapper.get())
            .withFieldToSourcePointerMapper(attributesFieldMapper())
            .mapToResponse(validationResult.getErrors());
      }

      ((RequestAttributeService)this.requestAttributeService.get()).consume(request.getAttributes());
      FlowResult result = ((UsernamePasswordAuthentication)this.usernamePasswordAuthentication.get())
         .checkPassword(request.getUsername(), PasswordImpl.passwordOf(request.getPassword()));
      return this.mapper.toResponse(result);
   }

   @ApiResponses(
      {
            @ApiResponse(
               responseCode = "200",
               description = "Password successfully changed and authentication successful 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>EXISTING_PASSWORD_WRONG</td>\n<td>The existing password is wrong.</td>\n</tr>\n<tr>\n<td>PASSWORD_POLICY_VIOLATED</td>\n<td>The new password violates the password policy. See <a href=\"#passwordPolicyDetailCodes\">password policy violation detail codes</a>.</td>\n</tr>\n</table>\n</div>\n<br>Possible next steps: <tt>MANDATORY_PASSWORD_CHANGE</tt>, <tt>PASSWORD_CHANGE_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 = "Performs a password change during the authentication process.\n<div class=\"iam-resource-return\">Whether the password change and the authentication was successful.\n If the attribute <tt>nextAuthStep</tt> is present in the response, further steps are required\n to successfully authenticate. If the attribute is missing, the user is successfully authenticated.</div>",
      tags = "Password",
      summary = "Change password"
   )
   @POST
   @Path("/change")
   @Consumes("application/json")
   @Produces({"application/vnd.api+json", "application/json;qs=0.9"})
   public Response changePassword(@Parameter(description = "Contains the user's current and new password.") @NotNull @Valid PasswordChangeRequest request) {
      Optional<Password> oldPassword = request.getOldPassword().map(PasswordImpl::passwordOf);
      Password newPassword = PasswordImpl.passwordOf(request.getNewPassword());
      FlowResult result = this.getPasswordChangeResult(oldPassword, newPassword);
      ValidationResult validationResult = PasswordPolicyViolationDataMapper.validatePasswordPolicy(result, "newPassword");
      if (!validationResult.isValid()) {
         List<ErrorData> errors = ((ValidationResultMapper)this.stepErrorResultMapper.get()).toErrorData(result.error().orElseThrow(), validationResult);
         return this.mapper.toResponse(result, errors);
      } else {
         return this.mapper.toResponse(result);
      }
   }

   private FlowResult getPasswordChangeResult(Optional<Password> oldPassword, Password newPassword) {
      return oldPassword.<FlowResult>map(oldPwd -> ((PasswordChange)this.passwordChange.get()).changePassword(oldPwd, newPassword))
         .orElseGet(() -> ((PasswordChange)this.passwordChange.get()).changePassword(newPassword));
   }

   @ApiResponses(
      {
            @ApiResponse(
               responseCode = "200",
               description = "Password successfully set and authentication successful 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>PASSWORD_POLICY_VIOLATED</td>\n<td>The new password violates the password policy. See <a href=\"#passwordPolicyDetailCodes\">password policy violation detail codes</a>.</td>\n</tr>\n</table>\n</div>\n<br>Possible next step: <tt>NEW_PASSWORD_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 = "Set password during the authentication process.\n<div class=\"iam-resource-return\">Whether the password was successfully set and the authentication 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 successfully authenticated.</div>",
      tags = "Password",
      summary = "Set password"
   )
   @POST
   @Path("/set")
   @Consumes("application/json")
   @Produces({"application/vnd.api+json", "application/json;qs=0.9"})
   public Response setPassword(@Parameter(description = "Contains the user's new password.") @NotNull @Valid AuthenticationSetPasswordRequest request) {
      FlowResult result = ((SetPassword)this.setPassword.get()).setPassword(PasswordImpl.passwordOf(request.getNewPassword()));
      ValidationResult validationResult = PasswordPolicyViolationDataMapper.validatePasswordPolicy(result, "newPassword");
      if (!validationResult.isValid()) {
         List<ErrorData> errors = ((ValidationResultMapper)this.stepErrorResultMapper.get()).toErrorData(result.error().get(), validationResult);
         return this.mapper.toResponse(result, errors);
      } else {
         return this.mapper.toResponse(result);
      }
   }

   private static FieldMapper attributesFieldMapper() {
      return PrefixFieldMapper.builder().prefix("attributes").build();
   }
}
