package com.airlock.iam.login.rest.infrastructure.resource.prot;

import ch.ergon.restal.jsonapi.document.data.CollectionDocument;
import ch.ergon.restal.jsonapi.document.data.ResourceDocument;
import ch.ergon.restal.jsonapi.document.data.ResourceObject;
import ch.ergon.restal.jsonapi.document.data.ResourceObject.Builder;
import ch.ergon.restal.jsonapi.document.request.RequestDocument;
import com.airlock.iam.base.api.domain.model.user.UserId;
import com.airlock.iam.base.api.domain.model.validation.GenericError;
import com.airlock.iam.base.api.domain.model.validation.ValidationResult;
import com.airlock.iam.base.infrastructure.error.ErrorResponse;
import com.airlock.iam.base.infrastructure.mapper.ValidationToErrorMapper;
import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.common.api.domain.model.otp.UnverifiedOtp;
import com.airlock.iam.common.api.infrastructure.annotation.RequiresWritableSession;
import com.airlock.iam.common.application.exception.RequestDocumentAttributesMissingException;
import com.airlock.iam.common.domain.model.auth.ExternalizationContext;
import com.airlock.iam.common.domain.model.user.PrincipalInfo;
import com.airlock.iam.core.misc.authen.result.AuthenticationFailed;
import com.airlock.iam.login.application.service.mtan.MtanSelfManagementService;
import com.airlock.iam.login.rest.infrastructure.generated.transfer.shared.response.my.MyMtanTokenDataCollectionDocument;
import com.airlock.iam.login.rest.infrastructure.generated.transfer.shared.response.my.MyMtanTokenDataResourceDocument;
import com.airlock.iam.login.rest.infrastructure.generated.transfer.shared.response.my.MyMtanTokenNumberChangeDataResourceDocument;
import com.airlock.iam.login.rest.infrastructure.logger.ProtectedHelpdeskLogger;
import com.airlock.iam.login.rest.infrastructure.transfer.shared.request.mtan.MtanNumberChangeRequest;
import com.airlock.iam.login.rest.infrastructure.transfer.shared.request.mtan.MtanNumberChangeVerificationRequest;
import com.airlock.iam.login.rest.infrastructure.transfer.shared.request.mtan.MtanTokenDataUpdateRequest;
import com.airlock.iam.login.rest.infrastructure.transfer.shared.response.my.MyMtanTokenData;
import com.airlock.iam.login.rest.infrastructure.transfer.shared.response.my.MyMtanTokenNumberChangeData;
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.GET;
import jakarta.ws.rs.PATCH;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import java.util.List;
import java.util.Optional;
import ma.glasnost.orika.MapperFacade;
import org.apache.commons.lang3.StringUtils;

@Path("/my/tokens/mtan")
public final class UserMtanSelfServiceResource {
   private final ProtectedHelpdeskLogger helpdeskLogger;
   private final Provider<MtanSelfManagementService> service;
   private final Provider<PrincipalInfo> principalInfo;
   private final MapperFacade mapper;
   private final ValidationToErrorMapper errorMapper;

   @Inject
   public UserMtanSelfServiceResource(
      ProtectedHelpdeskLogger helpdeskLogger,
      Provider<MtanSelfManagementService> service,
      Provider<PrincipalInfo> principalInfo,
      MapperFacade mapper,
      ValidationToErrorMapper errorMapper
   ) {
      this.helpdeskLogger = helpdeskLogger;
      this.service = service;
      this.principalInfo = principalInfo;
      this.mapper = mapper;
      this.errorMapper = errorMapper;
   }

   @ApiResponses(
      {
            @ApiResponse(
               responseCode = "200",
               description = "Retrieved available mTAN tokens.",
               content = {
                     @Content(mediaType = "application/vnd.api+json", schema = @Schema(implementation = MyMtanTokenDataCollectionDocument.class)),
                     @Content(mediaType = "application/json", schema = @Schema(implementation = MyMtanTokenDataCollectionDocument.class))
               }
            ),
            @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 = "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"))
               }
            )
      }
   )
   @Operation(
      description = "> [!caution]\n> This endpoint is deprecated and replaced by the protected self-service flow functionality. See\n<tt>/protected/self-service/mtan/tokens</tt>.\n<p>Fetching all mTAN tokens that the user has.</p>\n<div class=\"iam-resource-return\">All mTAN tokens belonging to the user.</div>",
      tags = "SMS/mTAN",
      summary = "List tokens"
   )
   @GET
   @Produces({"application/vnd.api+json", "application/json;qs=0.9"})
   @Deprecated
   @RequiresWritableSession
   public Response listMtanTokens() {
      List<AuthenticationFailed.MtanChoiceToken> mtanTokens = this.service().getMtanTokens(this.userId());
      CollectionDocument<MyMtanTokenData> document = new CollectionDocument();
      mtanTokens.forEach(token -> document.add(this.toResourceObject(token).build()));
      return Response.ok(document).build();
   }

   @ApiResponses(
      {
            @ApiResponse(
               responseCode = "200",
               description = "If the update was successful.",
               content = {
                     @Content(mediaType = "application/vnd.api+json", schema = @Schema(implementation = MyMtanTokenDataResourceDocument.class)),
                     @Content(mediaType = "application/json", schema = @Schema(implementation = MyMtanTokenDataResourceDocument.class))
               }
            ),
            @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 = "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"))
               }
            )
      }
   )
   @Operation(
      description = "> [!caution]\n> This endpoint is deprecated and replaced by the protected self-service flow functionality. See\n<tt>/protected/self-service/mtan/token/edit</tt>.\n<p>Updates mTAN token meta data. To change the phone number, use POST on '/start-number-change'.</p>\n<div class=\"iam-resource-return\">The updated mTAN token.</div>",
      tags = "SMS/mTAN",
      summary = "Update token data"
   )
   @PATCH
   @Path("/{mtanId}")
   @Consumes({"application/vnd.api+json", "application/json"})
   @Produces({"application/vnd.api+json", "application/json;qs=0.9"})
   @Deprecated
   public Response updateMtanToken(
      @Parameter(description = "The ID of the mTAN token.") @PathParam("mtanId") String mtanId,
      @Parameter(description = "The payload of the update.") @NotNull @Valid RequestDocument<MtanTokenDataUpdateRequest> requestDocument
   ) {
      MtanTokenDataUpdateRequest attributes = RequestDocumentAttributesMissingException.checkAttributesPresent(requestDocument);
      MtanSelfManagementService.MtanItemUpdate item = MtanSelfManagementService.MtanItemUpdate.builder()
         .userId(this.userId())
         .mtanTokenId(mtanId)
         .label(attributes.getLabel())
         .build();
      AuthenticationFailed.MtanChoiceToken updatedItem = this.service().updateMtanTokenData(item);
      this.helpdeskLogger.log("The label of the user's phone number %s was updated.", this.toDescription(mtanId));
      return Response.status(Status.OK).entity(this.toResourceObject(updatedItem).document()).build();
   }

   @ApiResponses(
      {
            @ApiResponse(
               responseCode = "200",
               description = "If the change was initiated and an OTP was sent to verify the change.",
               content = {
                     @Content(mediaType = "application/vnd.api+json", schema = @Schema(implementation = MyMtanTokenNumberChangeDataResourceDocument.class)),
                     @Content(mediaType = "application/json", schema = @Schema(implementation = MyMtanTokenNumberChangeDataResourceDocument.class))
               }
            ),
            @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 = "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"))
               }
            ),
            @ApiResponse(
               responseCode = "404",
               description = "If the mTAN token with the given ID does not exist.",
               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 = "409",
               description = "The phone number conflicts with an existing number.",
               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 = "> [!caution]\n> This endpoint is deprecated and replaced by the protected self-service flow functionality. See\n<tt>/protected/self-service/mtan/token/edit</tt>.\n<p>Initiates a phone number change for an mTAN token.</p>\n<p>If the server responds with 200 OK, an OTP is sent to the user's existing number.\nThe OTP together with the context that is returned within the response must be\nsent when calling '/verify-number-change' in order for the change to become effective.</p>\n<div class=\"iam-resource-return\">The response of the change.</div>",
      tags = "SMS/mTAN",
      summary = "Initiate number change"
   )
   @POST
   @Path("/{mtanId}/start-number-change")
   @Consumes("application/json")
   @Produces({"application/vnd.api+json", "application/json;qs=0.9"})
   @Deprecated
   public Response startMtanNumberChange(
      @Parameter(description = "The ID of the mTAN token that is about to change the phone number.") @PathParam("mtanId") String mtanId,
      @Parameter(description = "The payload of the phone number change.") @NotNull @Valid MtanNumberChangeRequest changeRequest
   ) {
      MtanSelfManagementService.MtanNumberChange numberChange = MtanSelfManagementService.MtanNumberChange.builder()
         .mtanId(mtanId)
         .userId(this.userId())
         .newPhoneNumber(StringUtils.remove(changeRequest.getNewPhoneNumber(), ' '))
         .build();
      ValidationResult validation = this.service().validate(numberChange);
      if (!validation.isValid()) {
         if (hasOnlyNotFoundError(validation)) {
            this.helpdeskLogger.log("Failed to change the user's phone number. The mTAN token with the given ID '%s' does not exist.", mtanId);
            return ErrorResponse.notFound().build().toResponse();
         } else {
            this.helpdeskLogger.log("The new phone number for %s is invalid.", this.toDescription(mtanId));
            return this.errorMapper.mapToResponse(validation.getErrors());
         }
      } else {
         ExternalizationContext context = this.service().startPhoneNumberChange(numberChange);
         this.helpdeskLogger
            .log(
               "A number change for number %s has been requested. An OTP has been sent and must be verified before the change becomes effective.",
               this.toDescription(mtanId)
            );
         ResourceDocument<MyMtanTokenNumberChangeData> document = ResourceObject.builder(mtanId, new MyMtanTokenNumberChangeData(context.getJwt())).document();
         return Response.ok().entity(document).build();
      }
   }

   private static boolean hasOnlyNotFoundError(ValidationResult result) {
      return result.getErrors().size() == 1 && result.getErrors().getFirst().getType().equals(GenericError.NOT_FOUND);
   }

   @ApiResponses(
      {
            @ApiResponse(responseCode = "200", description = "The verification was successful and the phone number was changed."),
            @ApiResponse(
               responseCode = "400",
               description = "The verification failed and the phone number was not changed.",
               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 = "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"))
               }
            )
      }
   )
   @Operation(
      description = "> [!caution]\n> This endpoint is deprecated and replaced by the protected self-service flow functionality. See\n<tt>/protected/self-service/approval/mtan/otp/check</tt>.\n<p>Verifies the mTAN number change.</p>\n<p>Verification is based on the OTP and the context received from '/start-number-change'.\nThe phone number only gets changed if the verification of the context and the OTP was successful.</p>\n<div class=\"iam-resource-return\">information whether the change was successful.</div>",
      tags = "SMS/mTAN",
      summary = "Verify number change"
   )
   @POST
   @Path("/{mtanId}/verify-number-change")
   @Consumes("application/json")
   @Produces({"application/vnd.api+json", "application/json;qs=0.9"})
   @Deprecated
   public Response verifyMtanNumberChange(
      @Parameter(description = "The ID of the mTAN token for which to verify the phone number change.") @PathParam("mtanId") String mtanId,
      @Parameter(description = "Contains additional request parameters.") @NotNull @Valid MtanNumberChangeVerificationRequest request
   ) {
      MtanSelfManagementService.MtanNumberChangeVerification verification = MtanSelfManagementService.MtanNumberChangeVerification.builder()
         .mtanId(mtanId)
         .userId(this.userId())
         .inputOtp(UnverifiedOtp.unverifiedOtp(request.getOtp()))
         .context(ExternalizationContext.builder().jwt(request.getContext()).build())
         .build();
      boolean validVerification = this.service().isValid(verification);
      if (!validVerification) {
         this.helpdeskLogger.log("Failed to change the user's phone number %s.", this.toDescription(mtanId));
         return ErrorResponse.badRequest().build().toResponse();
      } else {
         this.service().applyPhoneNumberChange(verification);
         this.helpdeskLogger.log("Successfully changed the user's phone number to %s.", this.toDescription(mtanId));
         return Response.ok().build();
      }
   }

   private String toDescription(String mtanId) {
      Optional<AuthenticationFailed.MtanChoiceToken> token = this.service()
         .getMtanTokens(this.userId())
         .stream()
         .filter(t -> t.getId().equals(mtanId))
         .findFirst();
      return token.isPresent() ? LogUtil.quote(token.get().getDesc()) : "(no matching number found for ID " + mtanId + ")";
   }

   private Builder<MyMtanTokenData> toResourceObject(AuthenticationFailed.MtanChoiceToken token) {
      return ResourceObject.builder(token.getId(), (MyMtanTokenData)this.mapper.map(token, MyMtanTokenData.class));
   }

   private MtanSelfManagementService service() {
      return (MtanSelfManagementService)this.service.get();
   }

   private UserId userId() {
      return ((PrincipalInfo)this.principalInfo.get()).tryGetAuthenticatedIdentity().id();
   }
}
