package com.airlock.iam.admin.infrastructure.resource.prot.token;

import ch.ergon.restal.jsonapi.document.data.CollectionDocument;
import ch.ergon.restal.jsonapi.document.data.ResourceDocument;
import ch.ergon.restal.jsonapi.document.request.RequestDocument;
import ch.ergon.restal.jsonapi.parameter.FilterParameter.Condition;
import ch.ergon.restal.jsonapi.parameter.FilterParameter.Disjunction;
import com.airlock.iam.admin.application.service.token.vasco.VascoOtpDeviceFilter;
import com.airlock.iam.admin.application.service.token.vasco.VascoOtpDeviceId;
import com.airlock.iam.admin.application.service.token.vasco.VascoOtpDeviceManagementService;
import com.airlock.iam.admin.domain.model.token.vasco.VascoOtpDevice;
import com.airlock.iam.admin.infrastructure.generated.transfer.response.user.tokens.VascoDeviceDataCollectionDocument;
import com.airlock.iam.admin.infrastructure.generated.transfer.response.user.tokens.VascoDeviceDataResourceDocument;
import com.airlock.iam.admin.infrastructure.transfer.request.SynchronizeVascoTokenRequest;
import com.airlock.iam.admin.infrastructure.transfer.request.VascoDevicePatchRequest;
import com.airlock.iam.admin.infrastructure.transfer.response.user.tokens.VascoDeviceData;
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.infrastructure.mapper.entity.RestMapper;
import com.airlock.iam.base.infrastructure.validation.DocumentedFilterParameter;
import com.airlock.iam.base.infrastructure.validation.SanitizedPageParameter;
import com.airlock.iam.base.infrastructure.validation.ValidPageParameter;
import com.airlock.iam.common.application.exception.RequestDocumentAttributesMissingException;
import com.airlock.iam.log.api.helpdesk.HelpdeskLogger;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
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.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.inject.Provider;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import jakarta.ws.rs.BeanParam;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
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.QueryParam;
import jakarta.ws.rs.core.Response;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

@Path("tokens/vasco-otp/devices")
public class VascoOtpDeviceResource {
   private static final String VASCO_DEVICE_DATA_RESP_EXAMPLE = "{\n\t\"meta\": {\n\t\t\"type\": \"jsonapi.metadata.document\",\n\t\t\"timestamp\": \"2017-04-05T09:52:50.638+02:00\"\n\t},\n\t\"data\": {\n\t\t\"type\": \"token.vasco.device\",\n\t\t\"id\": \"2\",\n\t\t\"attributes\": {\n\t\t\t\"activationDate\": \"2017-04-05T09:51:27.636+02:00\",\n\t\t\t\"numberOfUsages\": 12,\n\t\t\t\"enabled\": true\n\t\t},\n\t\t\"relationships\": {\n\t\t\t\"assignees\": {\n\t\t\t\t\"data\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"type\": \"user\",\n\t\t\t\t\t\t\"id\": \"233\"\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"type\": \"user\",\n\t\t\t\t\t\t\"id\": \"19\"\n\t\t\t\t\t}\n\t\t\t\t]\n\t\t\t}\n\t\t}\n\t}\n}\n";
   private final Provider<VascoOtpDeviceManagementService> service;
   private final HelpdeskLogger helpdeskLogger;
   private final RestMapper mapper;
   private final ValidationToErrorMapper errorMapper;

   @Inject
   public VascoOtpDeviceResource(
      Provider<VascoOtpDeviceManagementService> service, HelpdeskLogger helpdeskLogger, RestMapper mapper, ValidationToErrorMapper errorMapper
   ) {
      this.service = service;
      this.helpdeskLogger = helpdeskLogger;
      this.mapper = mapper;
      this.errorMapper = errorMapper;
   }

   @ApiResponses(
      {
            @ApiResponse(responseCode = "204", description = "The device has been enabled."),
            @ApiResponse(
               responseCode = "400",
               description = "The request was invalid.",
               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 device 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"))
               }
            )
      }
   )
   @Operation(
      description = "Enables a Vasco device.\n<div class=\"iam-resource-return\">An empty response.</div>",
      tags = "Vasco OTP",
      summary = "Enable Vasco device"
   )
   @POST
   @Path("{deviceId}/enable")
   @Produces({"application/vnd.api+json", "application/json;qs=0.9"})
   public Response enableDevice(@Parameter(description = "The ID of the requested device.") @PathParam("deviceId") String deviceId) {
      if (((VascoOtpDeviceManagementService)this.service.get()).findDeviceById(VascoOtpDeviceId.vascoOtpDeviceId(deviceId)).isEmpty()) {
         return ErrorResponse.notFound().build().toResponse();
      }

      ((VascoOtpDeviceManagementService)this.service.get()).enableDevice(VascoOtpDeviceId.vascoOtpDeviceId(deviceId));
      this.logForEachAssignedUser(
         VascoOtpDeviceId.vascoOtpDeviceId(deviceId), "Administrator ${authentee} enabled Vasco-OTP device %s which affects user ${affectedUser}.", deviceId
      );
      return Response.noContent().build();
   }

   @ApiResponses(
      {
            @ApiResponse(responseCode = "204", description = "The device has been disabled."),
            @ApiResponse(
               responseCode = "404",
               description = "The device 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"))
               }
            )
      }
   )
   @DELETE
   @Operation(
      description = "Disables a Vasco device.\n<div class=\"iam-resource-return\">An empty response.</div>",
      tags = "Vasco OTP",
      summary = "Disable Vasco device"
   )
   @Path("{deviceId}/enable")
   @Produces({"application/vnd.api+json", "application/json;qs=0.9"})
   public Response disableDevice(@Parameter(description = "The ID of the requested device.") @PathParam("deviceId") String deviceId) {
      if (((VascoOtpDeviceManagementService)this.service.get()).findDeviceById(VascoOtpDeviceId.vascoOtpDeviceId(deviceId)).isEmpty()) {
         return ErrorResponse.notFound().build().toResponse();
      }

      ((VascoOtpDeviceManagementService)this.service.get()).disableDevice(VascoOtpDeviceId.vascoOtpDeviceId(deviceId));
      this.logForEachAssignedUser(
         VascoOtpDeviceId.vascoOtpDeviceId(deviceId), "Administrator ${authentee} disabled Vasco-OTP device %s which affects user ${affectedUser}.", deviceId
      );
      return Response.noContent().build();
   }

   @ApiResponses(
      {
            @ApiResponse(responseCode = "204", description = "The device has been deleted."),
            @ApiResponse(
               responseCode = "400",
               description = "The request was invalid or the device cannot be deleted.\nbecause it is still assigned to at least one user.\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 = "404",
               description = "The device 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"))
               }
            )
      }
   )
   @DELETE
   @Operation(
      description = "Deletes a Vasco device.\n<div class=\"iam-resource-return\">An empty response</div>",
      tags = "Vasco OTP",
      summary = "Delete Vasco device"
   )
   @Path("{deviceId}")
   @Produces({"application/vnd.api+json", "application/json;qs=0.9"})
   public Response deleteDevice(@Parameter(description = "The ID of the device to be deleted.") @PathParam("deviceId") String deviceId) {
      if (((VascoOtpDeviceManagementService)this.service.get()).findDeviceById(VascoOtpDeviceId.vascoOtpDeviceId(deviceId)).isEmpty()) {
         return ErrorResponse.notFound().build().toResponse();
      }

      ((VascoOtpDeviceManagementService)this.service.get()).deleteDevice(VascoOtpDeviceId.vascoOtpDeviceId(deviceId));
      return Response.noContent().build();
   }

   @ApiResponses(
      {
            @ApiResponse(
               responseCode = "200",
               description = "Retrieved Vasco device.",
               content = {
                     @Content(
                        mediaType = "application/vnd.api+json",
                        schema = @Schema(implementation = VascoDeviceDataResourceDocument.class),
                        examples = @ExampleObject(
                           name = "Vasco device with user assignment",
                           value = "{\n\t\"meta\": {\n\t\t\"type\": \"jsonapi.metadata.document\",\n\t\t\"timestamp\": \"2017-04-05T09:52:50.638+02:00\"\n\t},\n\t\"data\": {\n\t\t\"type\": \"token.vasco.device\",\n\t\t\"id\": \"2\",\n\t\t\"attributes\": {\n\t\t\t\"activationDate\": \"2017-04-05T09:51:27.636+02:00\",\n\t\t\t\"numberOfUsages\": 12,\n\t\t\t\"enabled\": true\n\t\t},\n\t\t\"relationships\": {\n\t\t\t\"assignees\": {\n\t\t\t\t\"data\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"type\": \"user\",\n\t\t\t\t\t\t\"id\": \"233\"\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"type\": \"user\",\n\t\t\t\t\t\t\"id\": \"19\"\n\t\t\t\t\t}\n\t\t\t\t]\n\t\t\t}\n\t\t}\n\t}\n}\n"
                        )
                     ),
                     @Content(
                        mediaType = "application/json",
                        schema = @Schema(implementation = VascoDeviceDataResourceDocument.class),
                        examples = @ExampleObject(
                           name = "Vasco device with user assignment",
                           value = "{\n\t\"meta\": {\n\t\t\"type\": \"jsonapi.metadata.document\",\n\t\t\"timestamp\": \"2017-04-05T09:52:50.638+02:00\"\n\t},\n\t\"data\": {\n\t\t\"type\": \"token.vasco.device\",\n\t\t\"id\": \"2\",\n\t\t\"attributes\": {\n\t\t\t\"activationDate\": \"2017-04-05T09:51:27.636+02:00\",\n\t\t\t\"numberOfUsages\": 12,\n\t\t\t\"enabled\": true\n\t\t},\n\t\t\"relationships\": {\n\t\t\t\"assignees\": {\n\t\t\t\t\"data\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"type\": \"user\",\n\t\t\t\t\t\t\"id\": \"233\"\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"type\": \"user\",\n\t\t\t\t\t\t\"id\": \"19\"\n\t\t\t\t\t}\n\t\t\t\t]\n\t\t\t}\n\t\t}\n\t}\n}\n"
                        )
                     )
               }
            ),
            @ApiResponse(
               responseCode = "404",
               description = "The device 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"))
               }
            )
      }
   )
   @GET
   @Path("{deviceId}")
   @Produces({"application/vnd.api+json", "application/json;qs=0.9"})
   @Operation(
      description = "Retrieves a specific Vasco device by ID.\n<p>\nDevices can be related to other objects by means of relationships.\nSuch relationships (if available) are expressed by a \"relationships\" element within \"data\".\nA device can be either unassigned or assigned to a number of different users. This\nrelationship is modeled in the \"assignees\" element.\n</p>\n<p>\n<tt>previous-device</tt> and <tt>next-device</tt> relationships are available on user-specific Vasco device\nendpoints only.\n</p>\n<p>\nSee example response for more details.\n</p>\n<div class=\"iam-resource-return\">The requested Vasco device</div>",
      tags = "Vasco OTP",
      summary = "Get device by ID"
   )
   public Response retrieveDevice(@Parameter(description = "The ID of the requested device.") @PathParam("deviceId") String deviceId) {
      Optional<VascoOtpDevice> device = ((VascoOtpDeviceManagementService)this.service.get()).findDeviceById(VascoOtpDeviceId.vascoOtpDeviceId(deviceId));
      if (device.isEmpty()) {
         return ErrorResponse.notFound().build().toResponse();
      }

      ResourceDocument<VascoDeviceData> document = this.mapper.forEntity(device.get(), VascoDeviceData.class).toResourceDocument();
      return Response.ok(document).build();
   }

   @ApiResponses(
      {
            @ApiResponse(responseCode = "204", description = "The device has been updated accordingly."),
            @ApiResponse(
               responseCode = "404",
               description = "The device 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"))
               }
            )
      }
   )
   @PATCH
   @Path("{deviceId}")
   @Consumes({"application/vnd.api+json", "application/json"})
   @Produces({"application/vnd.api+json", "application/json;qs=0.9"})
   @Operation(description = "Updates a Vasco device.\n<div class=\"iam-resource-return\">nothing.</div>", tags = "Vasco OTP", summary = "Update Vasco device")
   public Response updateDevice(
      @Parameter(description = "The ID of the device.") @PathParam("deviceId") String deviceId,
      @Parameter(description = "The attributes to be updated.") @NotNull @Valid RequestDocument<VascoDevicePatchRequest> request
   ) {
      RequestDocumentAttributesMissingException.checkAttributesPresent(request);
      if (((VascoOtpDeviceManagementService)this.service.get()).findDeviceById(VascoOtpDeviceId.vascoOtpDeviceId(deviceId)).isEmpty()) {
         return ErrorResponse.notFound().build().toResponse();
      }

      ValidationResult result = ((VascoOtpDeviceManagementService)this.service.get())
         .validateDeviceValidityUpdate(
            VascoOtpDeviceId.vascoOtpDeviceId(deviceId),
            ((VascoDevicePatchRequest)Objects.requireNonNull((VascoDevicePatchRequest)request.getData().getAttributes())).getValidFrom(),
            ((VascoDevicePatchRequest)request.getData().getAttributes()).getValidTo()
         );
      if (!result.isValid()) {
         return this.errorMapper.withJsonApiAttributesMapper().mapToResponse(result.getErrors());
      }

      ((VascoOtpDeviceManagementService)this.service.get())
         .updateDeviceValidity(
            VascoOtpDeviceId.vascoOtpDeviceId(deviceId),
            ((VascoDevicePatchRequest)request.getData().getAttributes()).getValidFrom(),
            ((VascoDevicePatchRequest)request.getData().getAttributes()).getValidTo()
         );
      this.logForEachAssignedUser(
         VascoOtpDeviceId.vascoOtpDeviceId(deviceId),
         "Administrator ${authentee} updated validity of Vasco-OTP device %s which affects user ${affectedUser}.",
         deviceId
      );
      return Response.noContent().build();
   }

   @ApiResponse(
      responseCode = "200",
      description = "Retrieved Vasco devices.",
      content = {
            @Content(mediaType = "application/vnd.api+json", schema = @Schema(implementation = VascoDeviceDataCollectionDocument.class)),
            @Content(mediaType = "application/json", schema = @Schema(implementation = VascoDeviceDataCollectionDocument.class))
      }
   )
   @GET
   @Produces({"application/vnd.api+json", "application/json;qs=0.9"})
   @Operation(
      description = "Retrieves all Vasco devices.\n<ul>\n<li><b>filter</b>: Restricts the set of devices to those matching the filter criteria.\nAllowed filter parameters are: <tt>enabled</tt> and <tt>assignedToUser</tt>.\nThey are boolean parameters allowing <tt>true</tt> / <tt>false</tt> as values.\nAND filtering is supported, OR filtering is currently not supported.</li>\n</ul>\n<p>\nAll devices returned by this endpoint also contain the <tt>assignees</tt>\nrelationship if available on the specific device. An example can be found\non the endpoint that retrieves a single device.\n</p>\n<div class=\"iam-resource-return\">A collection document with all Vasco devices.</div>",
      tags = "Vasco OTP",
      summary = "List devices"
   )
   public Response retrieveDevices(
      @Parameter(description = "Restricts the set of devices to those on the requested page.") @ValidPageParameter @BeanParam SanitizedPageParameter pageParameter,
      @Parameter(description = "See above") @BeanParam DocumentedFilterParameter filterParameter,
      @Parameter(extensions = @Extension(properties = @ExtensionProperty(name = "x-internal", value = "true"))) @QueryParam("noLimit") boolean noLimit
   ) {
      VascoOtpDeviceFilter deviceFilter = new VascoOtpDeviceFilter();

      for (Disjunction filter : filterParameter.parse()) {
         for (Condition condition : filter.getConditions()) {
            if (condition.getProperty().equals("enabled")) {
               deviceFilter.filterEnabled(Boolean.parseBoolean(condition.getValue()));
            }

            if (condition.getProperty().equals("assignedToUser")) {
               deviceFilter.filterAssigned(Boolean.parseBoolean(condition.getValue()));
            }
         }
      }

      deviceFilter.startIndex(Math.max(0, pageParameter.getOffset()));
      if (!noLimit) {
         deviceFilter.maxCount(Math.max(0, pageParameter.getLimit()));
      }

      List<VascoOtpDevice> devices = ((VascoOtpDeviceManagementService)this.service.get()).getFilteredDevices(deviceFilter);
      CollectionDocument<VascoDeviceData> document = this.mapper.forEntities(devices, VascoDeviceData.class).toCollection();
      return Response.ok(document).build();
   }

   @ApiResponses(
      {
            @ApiResponse(responseCode = "204", description = "Synchronization successful."),
            @ApiResponse(
               responseCode = "400",
               description = "The request was invalid or the OTPs were incorrect.",
               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 device 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"))
               }
            )
      }
   )
   @POST
   @Path("{deviceId}/synchronize")
   @Consumes("application/json")
   @Operation(
      description = "Synchronizes a Vasco device.\n<div class=\"iam-resource-return\">nothing.</div>",
      tags = "Vasco OTP",
      summary = "Sync Vasco device"
   )
   @Produces({"application/vnd.api+json", "application/json;qs=0.9"})
   public Response synchronize(
      @Parameter(description = "The ID of the device.") @PathParam("deviceId") String deviceId,
      @Parameter(description = "Contains the OTPs for synchronization.") @NotNull @Valid SynchronizeVascoTokenRequest request
   ) {
      if (((VascoOtpDeviceManagementService)this.service.get()).findDeviceById(VascoOtpDeviceId.vascoOtpDeviceId(deviceId)).isEmpty()) {
         return ErrorResponse.notFound().build().toResponse();
      } else {
         boolean success = ((VascoOtpDeviceManagementService)this.service.get())
            .synchronizeDevice(VascoOtpDeviceId.vascoOtpDeviceId(deviceId), request.getOtp1(), request.getOtp2());
         if (success) {
            this.logForEachAssignedUser(
               VascoOtpDeviceId.vascoOtpDeviceId(deviceId),
               "Administrator ${authentee} synchronized Vasco-OTP device %s which affects user ${affectedUser}.",
               deviceId
            );
            return Response.noContent().build();
         } else {
            this.logForEachAssignedUser(
               VascoOtpDeviceId.vascoOtpDeviceId(deviceId),
               "Administrator ${authentee} failed to synchronize Vasco-OTP device %s which affects user ${affectedUser}.",
               deviceId
            );
            return ErrorResponse.badRequest().build().toResponse();
         }
      }
   }

   private void logForEachAssignedUser(VascoOtpDeviceId deviceId, String message, Object... params) {
      ((VascoOtpDeviceManagementService)this.service.get())
         .findDeviceById(deviceId)
         .stream()
         .flatMap(dev -> dev.getAssignedUsers().stream())
         .forEach(assignment -> this.helpdeskLogger.log(assignment.getUserId(), message, params));
   }
}
