package com.airlock.iam.admin.infrastructure.resource.auth;

import com.airlock.iam.admin.application.service.authentication.AdminappAuthenticationService;
import com.airlock.iam.admin.infrastructure.transfer.request.PasswordAuthenticationAndChangeRequest;
import com.airlock.iam.admin.infrastructure.transfer.request.PasswordAuthenticationRequest;
import com.airlock.iam.base.api.domain.model.user.UserId;
import com.airlock.iam.base.api.domain.model.validation.ValidationResult;
import com.airlock.iam.base.api.domain.model.validation.ValidatorError;
import com.airlock.iam.base.api.infrastructure.uri.ExternalUriInfo;
import com.airlock.iam.base.infrastructure.error.ErrorResponse;
import com.airlock.iam.base.infrastructure.mapper.ValidationToErrorMapper;
import com.airlock.iam.common.api.domain.model.user.UserIdentity;
import com.airlock.iam.common.application.service.password.PasswordPolicyValidator;
import com.airlock.iam.common.domain.model.password.PasswordImpl;
import com.airlock.iam.common.domain.model.user.PrincipalInfo;
import com.airlock.iam.common.infrastructure.exception.IamRestErrorCode;
import com.airlock.iam.log.api.helpdesk.HelpdeskLogger;
import com.airlock.iam.reporting.api.domain.model.data.Action;
import com.airlock.iam.reporting.api.domain.model.data.ActionStatus;
import com.airlock.iam.reporting.api.domain.model.logger.Reporting;
import com.airlock.iam.reporting.domain.model.ReportingDataFactory;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.extensions.Extension;
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
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.DELETE;
import jakarta.ws.rs.GET;
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("/")
public class AdminappAuthenticationResource {
   private final Provider<AdminappAuthenticationService> authService;
   private final HelpdeskLogger helpdeskLogger;
   private final Provider<ExternalUriInfo> uriInfo;
   private final Reporting reporting;
   private final Provider<PrincipalInfo> principalInfo;

   @Inject
   public AdminappAuthenticationResource(
      Provider<AdminappAuthenticationService> authService,
      Provider<PrincipalInfo> principalInfo,
      HelpdeskLogger helpdeskLogger,
      Provider<ExternalUriInfo> uriInfo,
      Reporting reporting
   ) {
      this.authService = authService;
      this.principalInfo = principalInfo;
      this.helpdeskLogger = helpdeskLogger;
      this.uriInfo = uriInfo;
      this.reporting = reporting;
   }

   @POST
   @Operation(extensions = @Extension(properties = @ExtensionProperty(name = "x-internal", value = "true")))
   @ApiResponses(
      {
            @ApiResponse(responseCode = "200", description = "Success"),
            @ApiResponse(
               responseCode = "400",
               description = "Access to the requested service is forbidden. Authentication will not help.",
               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"))
               }
            )
      }
   )
   @Path("/password/check")
   @Consumes("application/json")
   @Produces({"application/vnd.api+json", "application/json;qs=0.9"})
   public Response checkUserPassword(@NotNull @Valid PasswordAuthenticationRequest request) {
      String providedUsername = request.getUsername();
      AdminappAuthenticationService.PasswordCheckResult result = ((AdminappAuthenticationService)this.authService.get())
         .checkPassword(providedUsername, PasswordImpl.passwordOf(request.getPassword()));
      UserId adminId = this.getAdminId();
      if (result != AdminappAuthenticationService.PasswordCheckResult.MANDATORY_PASSWORD_CHANGE) {
         this.reporting
            .report(
               ReportingDataFactory.dataForAction(Action.AUTHN_LOGIN)
                  .status(result == AdminappAuthenticationService.PasswordCheckResult.OK ? ActionStatus.SUCCESS : ActionStatus.FAILURE)
                  .build()
            );
      }

      if (result == AdminappAuthenticationService.PasswordCheckResult.OK) {
         this.helpdeskLogger.log(adminId, "Successfully authenticated");
         return Response.ok().build();
      } else {
         this.helpdeskLogger.log(adminId, "Failed to authenticate: " + result);
         return ErrorResponse.badRequest().errorCode(result::name).build().toResponse();
      }
   }

   @ApiResponses(
      {
            @ApiResponse(responseCode = "200", description = "The password was changed"),
            @ApiResponse(
               responseCode = "501",
               description = "The active configuration does not support the requested operation.",
               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(extensions = @Extension(properties = @ExtensionProperty(name = "x-internal", value = "true")))
   @Path("/password/check-and-change")
   @Consumes("application/json")
   @Produces({"application/vnd.api+json", "application/json;qs=0.9"})
   public Response checkAndChangePassword(@NotNull @Valid PasswordAuthenticationAndChangeRequest request) {
      String providedUsername = request.getUsername();
      AdminappAuthenticationService.PasswordCheckAndChangeResult result = ((AdminappAuthenticationService)this.authService.get())
         .checkAndChangePassword(providedUsername, PasswordImpl.passwordOf(request.getOldPassword()), PasswordImpl.passwordOf(request.getNewPassword()));
      UserId adminId = this.getAdminId();
      ValidationToErrorMapper mapper = new ValidationToErrorMapper(this.uriInfo);
      Optional<AdminappAuthenticationService.PasswordCheckResult> failure = result.getCheckFailure();
      if (failure.isPresent()) {
         this.reporting.report(ReportingDataFactory.dataForAction(Action.AUTHN_LOGIN).status(ActionStatus.FAILURE).build());
         this.helpdeskLogger.log(adminId, "Failed to authenticate: " + failure.get() + ". Password not changed.");
         return failure.get().equals(AdminappAuthenticationService.PasswordCheckResult.USERNAME_PASSWORD_WRONG)
            ? mapper.useErrorCode(() -> this.getErrorCode(failure.get()))
               .mapToResponse(ValidationResult.invalid(ValidatorError.validatorError("oldPassword")).getErrors())
            : ErrorResponse.badRequest().errorCode(failure.get()::name).build().toResponse();
      } else if (!result.getPolicyViolations().isEmpty()) {
         this.helpdeskLogger.log(adminId, "Password check succeeded, but new password violated password policies.");
         List<ValidatorError> errors = PasswordPolicyValidator.getValidationResultForViolations(result.getPolicyViolations(), "newPassword").getErrors();
         return mapper.useErrorCode(IamRestErrorCode.PASSWORD_POLICY_VIOLATED).mapToResponse(errors);
      } else {
         this.helpdeskLogger.log(adminId, "Successfully authenticated and changed password.");
         this.reporting.report(ReportingDataFactory.dataForAction(Action.AUTHN_LOGIN).status(ActionStatus.SUCCESS).build());
         return Response.ok().build();
      }
   }

   private UserId getAdminId() {
      return ((PrincipalInfo)this.principalInfo.get()).getKnownIdentity().<UserId>map(UserIdentity::id).orElse(null);
   }

   @DELETE
   @Operation(extensions = @Extension(properties = @ExtensionProperty(name = "x-internal", value = "true")))
   @Produces({"application/vnd.api+json", "application/json;qs=0.9"})
   public Response logout() {
      ((AdminappAuthenticationService)this.authService.get()).logout();
      return Response.noContent().build();
   }

   @GET
   @Operation(extensions = @Extension(properties = @ExtensionProperty(name = "x-internal", value = "true")))
   @Produces({"application/vnd.api+json", "application/json;qs=0.9"})
   public Response checkAuthentication() {
      return ((AdminappAuthenticationService)this.authService.get()).hasAuthenticatedSession()
         ? Response.noContent().build()
         : ErrorResponse.notFound().build().toResponse();
   }

   private String getErrorCode(AdminappAuthenticationService.PasswordCheckResult result) {
      return result == AdminappAuthenticationService.PasswordCheckResult.USERNAME_PASSWORD_WRONG
         ? IamRestErrorCode.EXISTING_PASSWORD_WRONG.name()
         : result.name();
   }
}
