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

import ch.ergon.restal.jsonapi.document.data.CollectionDocument;
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.ValidationResult;
import com.airlock.iam.base.infrastructure.mapper.ValidationToErrorMapper;
import com.airlock.iam.common.api.domain.model.device.DeviceTokenId;
import com.airlock.iam.common.api.infrastructure.annotation.RequiresWritableSession;
import com.airlock.iam.common.application.exception.RequestDocumentAttributesMissingException;
import com.airlock.iam.flow.shared.domain.model.devicetoken.DeviceTokenInfo;
import com.airlock.iam.flow.shared.domain.model.devicetoken.RegisterDeviceTokenRequest;
import com.airlock.iam.login.rest.infrastructure.generated.transfer.shared.response.my.MyDeviceTokenDataCollectionDocument;
import com.airlock.iam.login.rest.infrastructure.generated.transfer.shared.response.my.MyDeviceTokenDataResourceDocument;
import com.airlock.iam.login.rest.infrastructure.logger.ProtectedHelpdeskLogger;
import com.airlock.iam.login.rest.infrastructure.transfer.shared.request.device.DeviceRegistrationRequest;
import com.airlock.iam.login.rest.infrastructure.transfer.shared.response.my.MyDeviceTokenData;
import com.airlock.iam.selfservice.domain.service.devicetoken.DeviceTokenSelfManagementService;
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.DELETE;
import jakarta.ws.rs.GET;
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.security.Principal;
import java.util.Optional;
import ma.glasnost.orika.MapperFacade;

@Path("/my/tokens/device-tokens")
public class UserDeviceTokenResource {
   private final Provider<DeviceTokenSelfManagementService> deviceTokenSelfManagementService;
   private final ProtectedHelpdeskLogger helpdeskLogger;
   private final Provider<Principal> principalProvider;
   private final MapperFacade mapper;
   private final ValidationToErrorMapper errorMapper;

   @Inject
   public UserDeviceTokenResource(
      Provider<DeviceTokenSelfManagementService> deviceTokenSelfManagementService,
      ProtectedHelpdeskLogger logger,
      Provider<Principal> principalProvider,
      MapperFacade mapper,
      ValidationToErrorMapper errorMapper
   ) {
      this.deviceTokenSelfManagementService = deviceTokenSelfManagementService;
      this.helpdeskLogger = logger;
      this.principalProvider = principalProvider;
      this.mapper = mapper;
      this.errorMapper = errorMapper;
   }

   @ApiResponses(
      {
            @ApiResponse(
               responseCode = "200",
               description = "Available device tokens retrieved.",
               content = {
                     @Content(mediaType = "application/vnd.api+json", schema = @Schema(implementation = MyDeviceTokenDataCollectionDocument.class)),
                     @Content(mediaType = "application/json", schema = @Schema(implementation = MyDeviceTokenDataCollectionDocument.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"))
               }
            )
      }
   )
   @Operation(
      description = "Retrieves the set of enabled and valid device tokens.\n<div class=\"iam-resource-return\">The (possibly empty) collection of enabled and valid device tokens.</div>",
      tags = "Device Tokens",
      summary = "List tokens"
   )
   @GET
   @Produces({"application/vnd.api+json", "application/json;qs=0.9"})
   @RequiresWritableSession
   public Response getEnabledAndValidDeviceTokens() {
      CollectionDocument<MyDeviceTokenData> document = new CollectionDocument();

      for (DeviceTokenInfo deviceToken : ((DeviceTokenSelfManagementService)this.deviceTokenSelfManagementService.get())
         .getEnabledAndValidDeviceTokens(this.authenticatedUser())) {
         document.add(this.toResourceObjectBuilder(deviceToken).build());
      }

      return Response.ok(document).build();
   }

   @ApiResponses(
      {
            @ApiResponse(
               responseCode = "200",
               description = "Device token successfully registered.",
               content = {
                     @Content(mediaType = "application/vnd.api+json", schema = @Schema(implementation = MyDeviceTokenDataResourceDocument.class)),
                     @Content(mediaType = "application/json", schema = @Schema(implementation = MyDeviceTokenDataResourceDocument.class))
               }
            ),
            @ApiResponse(
               responseCode = "400",
               description = "The supplied attributes could not be validated (wrong key type, wrong key length, or missing attribute).",
               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 = "Register device token.\n<p>\nRegister a new device token by providing a JSON Web Key (JWK) public key and optionally a label and/or a serial ID of the device.\n</p>\n<p>\nThe public key is represented as specified in JWK (RFC7517). Currently elliptic curve signatures (keys of type \"EC\") are supported.<br/>\nThe following attributes are expected in the public key attribute (according to the JWK specification):\n<ul>\n<li>\"crv\" - the elliptic curve. Supported values are:\n<ul>\n<li>\"P-256\": for ES256 signatures</li>\n<li>\"P-256K\": for ES256K signatures</li>\n<li>\"P-384\": for ES384 signatures</li>\n<li>\"P-521\": for ES512 signatures</li>\n</ul>\nPlease note: During registration all four values are always allowed, but for the other use cases of device tokens (e.g. authentication)\nit is possible to restrict (in the IAM configuration) which curves are allowed.</li>\n<li>\"x\" - the x coordinate of the base point.</li>\n<li>\"y\" - the y coordinate of the base point.</li>\n<li>\"kty\" - the key type - must be \"EC\"</li>\n<li>\"kid\" - a key identifier (optional)</li>\n</ul>\n</p>\n<div class=\"iam-resource-return\">A device token to be used during authentication.</div>",
      tags = "Device Tokens",
      summary = "Register token"
   )
   @POST
   @Consumes({"application/vnd.api+json", "application/json"})
   @Produces({"application/vnd.api+json", "application/json;qs=0.9"})
   public Response registerDeviceToken(
      @Parameter(description = "A request to register a new device token (public key, optionally label and serial).") @NotNull @Valid RequestDocument<DeviceRegistrationRequest> requestDocument
   ) {
      DeviceRegistrationRequest attributes = RequestDocumentAttributesMissingException.checkAttributesPresent(requestDocument);
      RegisterDeviceTokenRequest registerDeviceTokenParams = RegisterDeviceTokenRequest.builder(this.authenticatedUser(), attributes.getPublicJwk())
         .deviceLabel(Optional.ofNullable(attributes.getLabel()))
         .deviceSerial(Optional.ofNullable(attributes.getSerial()))
         .build();
      ValidationResult validationResult = registerDeviceTokenParams.validate();
      if (!validationResult.isValid()) {
         return this.errorMapper.withJsonApiAttributesMapper().mapToResponse(validationResult.getErrors());
      }

      DeviceTokenInfo deviceToken = ((DeviceTokenSelfManagementService)this.deviceTokenSelfManagementService.get())
         .registerDeviceToken(registerDeviceTokenParams);
      this.helpdeskLogger.log("User ${authentee} registered device token with id \"%s\" valid until %s.", deviceToken.getId(), deviceToken.getValidTo());
      return Response.ok(this.toResourceObjectBuilder(deviceToken).document()).build();
   }

   @ApiResponses(
      {
            @ApiResponse(responseCode = "204", description = "Device token successfully deleted."),
            @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 = "The specified device token 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"))
               }
            )
      }
   )
   @Operation(
      description = "Remove device token.\n<p>\nRemoves an existing device token given its id.\n</p>\n<div class=\"iam-resource-return\">Device token information (ID, expiry date):</div>",
      tags = "Device Tokens",
      summary = "Remove token"
   )
   @DELETE
   @Consumes("application/json")
   @Produces({"application/vnd.api+json", "application/json;qs=0.9"})
   @Path("/{deviceTokenId}/")
   public Response deleteDeviceToken(@Parameter(description = "The ID of the device token to be deleted.") @PathParam("deviceTokenId") String deviceTokenId) {
      if (((DeviceTokenSelfManagementService)this.deviceTokenSelfManagementService.get())
         .deleteDeviceToken(UserId.userId(this.authenticatedUser()), new DeviceTokenId(deviceTokenId))) {
         this.helpdeskLogger.log("User ${authentee} deleted device token with id \"%s\".", deviceTokenId);
         return Response.noContent().build();
      } else {
         return Response.status(Status.NOT_FOUND).build();
      }
   }

   private Builder<MyDeviceTokenData> toResourceObjectBuilder(DeviceTokenInfo deviceToken) {
      MyDeviceTokenData attributes = (MyDeviceTokenData)this.mapper.map(deviceToken, MyDeviceTokenData.class);
      return ResourceObject.builder(deviceToken.getId(), attributes);
   }

   private String authenticatedUser() {
      return ((Principal)this.principalProvider.get()).getName();
   }
}
