package com.airlock.iam.admin.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.error.ErrorDocument;
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 ch.ergon.restal.jsonapi.parameter.FilterParameter.Operator;
import ch.ergon.restal.jsonapi.parameter.SortParameter.Criterion;
import com.airlock.iam.admin.application.service.maintenancemessage.MaintenanceMessageService;
import com.airlock.iam.admin.domain.model.maintenancemessage.MaintenanceMessage;
import com.airlock.iam.admin.infrastructure.generated.transfer.response.MaintenanceMessageDataCollectionDocument;
import com.airlock.iam.admin.infrastructure.generated.transfer.response.MaintenanceMessageDataResourceDocument;
import com.airlock.iam.admin.infrastructure.transfer.request.MaintenanceMessageRequest;
import com.airlock.iam.admin.infrastructure.transfer.response.MaintenanceMessageData;
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.api.infrastructure.uri.ExternalUriInfo;
import com.airlock.iam.base.infrastructure.error.ErrorResponse;
import com.airlock.iam.base.infrastructure.error.GenericRestErrorCode;
import com.airlock.iam.base.infrastructure.mapper.ValidationToErrorMapper;
import com.airlock.iam.base.infrastructure.validation.DocumentedFilterParameter;
import com.airlock.iam.base.infrastructure.validation.DocumentedSortParameter;
import com.airlock.iam.common.api.infrastructure.annotation.RequiresWritableSession;
import com.airlock.iam.common.application.exception.RequestDocumentAttributesMissingException;
import com.airlock.iam.common.infrastructure.resource.Resources;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.google.common.collect.Lists;
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.BeanParam;
import jakarta.ws.rs.Consumes;
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 jakarta.ws.rs.core.Response.Status;
import java.net.URI;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import ma.glasnost.orika.MapperFacade;

@Path("/maintenance-messages")
public class MaintenanceMessagesResource {
   private static final ELogger LOG = ELoggerFactory.getLogger(MaintenanceMessagesResource.class);
   private static final String VALID_TO = "validTo";
   private static final String VALID_FROM = "validFrom";
   private static final String ACTIVE = "active";
   private static final String SYSTEM_AVAILABLE = "systemAvailable";
   private static final String EXPIRED = "expired";
   private final Provider<Optional<MaintenanceMessageService>> maintenanceMessageService;
   private final MapperFacade mapper;
   private final Provider<ExternalUriInfo> uriInfo;
   private final ValidationToErrorMapper errorMapper;

   @Inject
   public MaintenanceMessagesResource(
      Provider<Optional<MaintenanceMessageService>> maintenanceMessageService,
      MapperFacade mapper,
      Provider<ExternalUriInfo> uriInfo,
      ValidationToErrorMapper errorMapper
   ) {
      this.maintenanceMessageService = maintenanceMessageService;
      this.mapper = mapper;
      this.uriInfo = uriInfo;
      this.errorMapper = errorMapper;
   }

   @ApiResponse(
      responseCode = "200",
      description = "The response contains a list of maintenance messages.",
      content = {
            @Content(mediaType = "application/vnd.api+json", schema = @Schema(implementation = MaintenanceMessageDataCollectionDocument.class)),
            @Content(mediaType = "application/json", schema = @Schema(implementation = MaintenanceMessageDataCollectionDocument.class))
      }
   )
   @GET
   @Operation(
      tags = "Maintenance Messages",
      description = "Returns all maintenance messages.\n<p>\n<ul>\n<li><b>filter</b>: Restricts the set of all messages to those matching the filter condition.\nAllowed filter parameters are: <tt>active</tt>, <tt>systemAvailable</tt>, <tt>expired</tt>. They are boolean parameters allowing <tt>true</tt> / <tt>false</tt> as values.\nAND filtering is supported, OR filtering is currently not supported.</li>\n<li><b>sort</b>: Allows to sort by <tt>validFrom</tt> and <tt>validTo</tt>.</li>\n</ul>\n<div class=\"iam-resource-return\">A collection document containing the matching maintenance messages, ordered ascending by start date.</div>",
      summary = "Get maintenance messages"
   )
   @Produces({"application/vnd.api+json", "application/json;qs=0.9"})
   @RequiresWritableSession
   public Response retrieve(
      @Parameter(description = "see above") @BeanParam DocumentedFilterParameter filter,
      @Parameter(description = "see above") @BeanParam DocumentedSortParameter sort
   ) {
      List<Disjunction> disjunctionOfConditions = filter.parse();
      boolean hasUnsupportedFormat = disjunctionOfConditions.stream()
         .anyMatch(d -> d.getConditions().size() != 1 || ((Condition)d.getConditions().getFirst()).getOperator() != Operator.EQUALS);
      if (hasUnsupportedFormat) {
         ErrorDocument error = ValidationToErrorMapper.error(GenericRestErrorCode.VALIDATION_FAILED, GenericError.WRONG_FORMAT, "filter");
         LOG.info("OR terms or operators other than == are not supported for maintenance messages.");
         return Response.status(Status.BAD_REQUEST).entity(error).build();
      }

      Map<String, String> params;
      try {
         params = disjunctionOfConditions.stream()
            .collect(Collectors.toMap(d -> ((Condition)d.getConditions().getFirst()).getProperty(), d -> ((Condition)d.getConditions().getFirst()).getValue()));
      } catch (IllegalStateException e) {
         ErrorDocument error = ValidationToErrorMapper.error(GenericRestErrorCode.VALIDATION_FAILED, GenericError.WRONG_FORMAT, "filter");
         LOG.info("Duplicate filter keys.");
         return Response.status(Status.BAD_REQUEST).entity(error).build();
      }

      if (params.keySet().stream().anyMatch(key -> !Lists.newArrayList(new String[]{"active", "systemAvailable", "expired"}).contains(key))) {
         ErrorDocument error = ValidationToErrorMapper.error(GenericRestErrorCode.VALIDATION_FAILED, GenericError.INVALID_VALUE, "filter");
         LOG.info(String.format("Filtering is allowed for '%s', '%s' and '%s' only.", "active", "systemAvailable", "expired"));
         return Response.status(Status.BAD_REQUEST).entity(error).build();
      }

      List<Criterion> sortCriteria = sort.parse();
      if (sortCriteria.size() > 1) {
         ErrorDocument error = ValidationToErrorMapper.error(GenericRestErrorCode.VALIDATION_FAILED, GenericError.WRONG_FORMAT, "sort");
         LOG.info("Only one sort criterion supported.");
         return Response.status(Status.BAD_REQUEST).entity(error).build();
      }

      Optional<MaintenanceMessageService.SortOption> sortOption = Optional.empty();
      if (!sortCriteria.isEmpty()) {
         if (!Lists.newArrayList(new String[]{"validFrom", "validTo"}).contains(sortCriteria.getFirst().getName())) {
            ErrorDocument error = ValidationToErrorMapper.error(GenericRestErrorCode.VALIDATION_FAILED, GenericError.INVALID_VALUE, "sort");
            LOG.info(String.format("Sorting is only supported for the attributes '%s' and '%s'.", "validFrom", "validTo"));
            return Response.status(Status.BAD_REQUEST).entity(error).build();
         }

         sortOption = Optional.of(new MaintenanceMessageService.SortOption(sortCriteria.getFirst().isAscending(), sortCriteria.getFirst().getName()));
      }

      if (((Optional)this.maintenanceMessageService.get()).isEmpty()) {
         return ErrorResponse.notImplemented().build().toResponse();
      }

      Boolean filterExpired = params.containsKey("expired") ? Boolean.valueOf(params.get("expired")) : null;
      Boolean filterSystemAvailable = params.containsKey("systemAvailable") ? Boolean.valueOf(params.get("systemAvailable")) : null;
      Boolean filterActive = params.containsKey("active") ? Boolean.valueOf(params.get("active")) : null;
      MaintenanceMessageService.RetrieveOptions options = MaintenanceMessageService.RetrieveOptions.builder()
         .filterExpired(Optional.ofNullable(filterExpired))
         .filterSystemAvailable(Optional.ofNullable(filterSystemAvailable))
         .filterActive(Optional.ofNullable(filterActive))
         .sortOption(sortOption)
         .build();
      List<MaintenanceMessage> messages = ((MaintenanceMessageService)((Optional)this.maintenanceMessageService.get()).orElseThrow()).retrieve(options);
      CollectionDocument<MaintenanceMessageData> document = new CollectionDocument();

      for (MaintenanceMessage mm : messages) {
         MaintenanceMessageData to = (MaintenanceMessageData)this.mapper.map(mm, MaintenanceMessageData.class);
         ResourceObject<MaintenanceMessageData> resource = ResourceObject.builder(mm.getName(), to)
            .linkSelf(Resources.link((ExternalUriInfo)this.uriInfo.get(), MaintenanceMessagesResource.class, mm.getName()).build(new Object[0]).getUri())
            .build();
         document.add(resource);
      }

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

   @ApiResponses(
      {
            @ApiResponse(
               responseCode = "201",
               description = "A new maintenance message was created.",
               content = {
                     @Content(mediaType = "application/vnd.api+json", schema = @Schema(implementation = MaintenanceMessageDataResourceDocument.class)),
                     @Content(mediaType = "application/json", schema = @Schema(implementation = MaintenanceMessageDataResourceDocument.class))
               }
            ),
            @ApiResponse(
               responseCode = "409",
               description = "A maintenance message with the supplied identifier already exists.",
               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(
      tags = "Maintenance Messages",
      description = "Creates a new maintenance message.\n<div class=\"iam-resource-return\">A response containing the type and id attribute of the created message.\n The response contains a Location header referring to the created maintenance message.</div>",
      summary = "Create maintenance message"
   )
   @Consumes({"application/vnd.api+json", "application/json"})
   @Produces({"application/vnd.api+json", "application/json;qs=0.9"})
   public Response create(
      @Parameter(description = "A request object with the maintenance message details.") @NotNull @Valid RequestDocument<MaintenanceMessageRequest> requestDocument
   ) {
      MaintenanceMessageRequest attributes = (MaintenanceMessageRequest)RequestDocumentAttributesMissingException.checkAttributesPresent(requestDocument);
      if (((Optional)this.maintenanceMessageService.get()).isEmpty()) {
         return ErrorResponse.notImplemented().build().toResponse();
      }

      MaintenanceMessage maintenanceMessage = (MaintenanceMessage)this.mapper.map(attributes, MaintenanceMessage.class);
      maintenanceMessage.setName(requestDocument.getData().getId());
      ValidationResult validation = ((MaintenanceMessageService)((Optional)this.maintenanceMessageService.get()).orElseThrow())
         .validateCreate(maintenanceMessage);
      if (!validation.isValid()) {
         return this.errorMapper
            .withStatus(validation.hasError("name", GenericError.NOT_UNIQUE) ? Status.CONFLICT : Status.BAD_REQUEST)
            .withFieldToSourcePointerMapper(MaintenanceMessageResource.MAINTENANCE_MESSAGE_SOURCE_POINTER_MAPPER)
            .mapToResponse(validation.getErrors());
      }

      ((MaintenanceMessageService)((Optional)this.maintenanceMessageService.get()).orElseThrow()).create(maintenanceMessage);
      URI resourceUri = Resources.uri((ExternalUriInfo)this.uriInfo.get(), MaintenanceMessagesResource.class)
         .path(maintenanceMessage.getName())
         .build(new Object[0]);
      return Response.created(resourceUri).entity(ResourceObject.builder("maintenance-message", maintenanceMessage.getName()).document()).build();
   }
}
