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

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 com.airlock.iam.admin.api.domain.model.administrator.CurrentAdministrator;
import com.airlock.iam.admin.infrastructure.generated.transfer.response.techclient.ApiKeyDataCollectionDocument;
import com.airlock.iam.admin.infrastructure.generated.transfer.response.techclient.PlainTechClientDataResourceDocument;
import com.airlock.iam.admin.infrastructure.transfer.request.techclient.PlainTechClientUpdateRequest;
import com.airlock.iam.admin.infrastructure.transfer.response.techclient.ApiKeyData;
import com.airlock.iam.admin.infrastructure.transfer.response.techclient.PlainTechClientData;
import com.airlock.iam.admin.infrastructure.transfer.response.techclient.SubscriptionData;
import com.airlock.iam.base.api.domain.model.identity.Identity;
import com.airlock.iam.base.infrastructure.error.ErrorResponse;
import com.airlock.iam.base.infrastructure.mapper.entity.RestMapper;
import com.airlock.iam.base.infrastructure.mapper.jsonapi.JsonApiMapper;
import com.airlock.iam.base.infrastructure.mapper.jsonapi.JsonApiResourceMapper;
import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.common.application.exception.RequestDocumentAttributesMissingException;
import com.airlock.iam.common.domain.model.identity.DefaultStringIdentity;
import com.airlock.iam.common.infrastructure.exception.IamRestErrorCode;
import com.airlock.iam.common.infrastructure.mapper.OrikaMapperUtils;
import com.airlock.iam.common.infrastructure.validation.JsonApiUpdateResourceIdValidator;
import com.airlock.iam.log.api.signing.SigningLogger;
import com.airlock.iam.techclient.admin.application.service.ApiKeyService;
import com.airlock.iam.techclient.admin.application.service.PlainTechClientService;
import com.airlock.iam.techclient.admin.application.service.SubscriptionService;
import com.airlock.iam.techclient.admin.domain.model.ApiKey;
import com.airlock.iam.techclient.admin.domain.model.ApiKeyTechClient;
import com.airlock.iam.techclient.admin.domain.model.PlainTechClient;
import com.airlock.iam.techclient.admin.domain.model.Subscription;
import com.airlock.iam.techclient.api.domain.model.TechnicalClient;
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.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.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.PATCH;
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.Optional;
import java.util.Set;
import ma.glasnost.orika.MappingContext;

@Path("/tech-clients/{id}")
public class TechClientResource {
   private static final String TECH_CLIENT_RESP_EXAMPLE = "{\n\t \"meta\":{\n\t  \"type\":\"jsonapi.metadata.document\",\n\t  \"timestamp\":\"2020-02-18T10:21:26.825+01:00\"\n\t},\n\t\"data\":{\n\t  \"type\":\"tech-client\",\n\t  \"id\":\"4e7932a5-1a7f-43b9-ab3b-ecb3ea76635c\",\n\t  \"attributes\":{\n\t\t\"displayName\":\"myClient\",\n\t\t\"creationDate\":\"2020-02-18T10:19:36.106+01:00\"\n\t  },\n\t  \"relationships\":{\n\t\t\"subscription\":{\n\t\t  \"data\":{\n\t\t\t\"type\":\"subscription\",\n\t\t\t\"id\":\"8d0d2b59-2155-4eb2-a523-3c90b67df35f\"\n\t\t  }\n\t\t},\n\t\t\"api-keys\":{\n\t\t  \"data\":[\n\t\t\t{\n\t\t\t  \"type\":\"api-key\",\n\t\t\t  \"id\":\"c017b120-1a0d-4aa5-9667-51ac1c951383\"\n\t\t\t},\n\t\t\t{\n\t\t\t  \"type\":\"api-key\",\n\t\t\t  \"id\":\"50066ef1-30f8-40b7-a6ed-b5a56d7c255a\"\n\t\t\t}\n\t\t  ]\n\t\t}\n\t  }\n\t}\n}\n";
   private final RestMapper mapper;
   private final Provider<PlainTechClientService> plainTechClientService;
   private final Provider<ApiKeyService> apiKeyService;
   private final Provider<SubscriptionService> subscriptionService;
   private final Provider<CurrentAdministrator> currentAdministrator;
   private final JsonApiUpdateResourceIdValidator idValidator;
   private final SigningLogger signingLogger;

   @Inject
   public TechClientResource(
      RestMapper mapper,
      Provider<PlainTechClientService> plainTechClientService,
      Provider<ApiKeyService> apiKeyService,
      Provider<SubscriptionService> subscriptionService,
      Provider<CurrentAdministrator> currentAdministrator,
      JsonApiUpdateResourceIdValidator idValidator,
      SigningLogger signingLogger
   ) {
      this.mapper = mapper;
      this.plainTechClientService = plainTechClientService;
      this.apiKeyService = apiKeyService;
      this.subscriptionService = subscriptionService;
      this.currentAdministrator = currentAdministrator;
      this.idValidator = idValidator;
      this.signingLogger = signingLogger;
   }

   @ApiResponses(
      {
            @ApiResponse(
               responseCode = "200",
               description = "The response contains the specified technical client.",
               content = {
                     @Content(
                        mediaType = "application/vnd.api+json",
                        schema = @Schema(implementation = PlainTechClientDataResourceDocument.class),
                        examples = @ExampleObject(
                           name = "Technical client",
                           value = "{\n\t \"meta\":{\n\t  \"type\":\"jsonapi.metadata.document\",\n\t  \"timestamp\":\"2020-02-18T10:21:26.825+01:00\"\n\t},\n\t\"data\":{\n\t  \"type\":\"tech-client\",\n\t  \"id\":\"4e7932a5-1a7f-43b9-ab3b-ecb3ea76635c\",\n\t  \"attributes\":{\n\t\t\"displayName\":\"myClient\",\n\t\t\"creationDate\":\"2020-02-18T10:19:36.106+01:00\"\n\t  },\n\t  \"relationships\":{\n\t\t\"subscription\":{\n\t\t  \"data\":{\n\t\t\t\"type\":\"subscription\",\n\t\t\t\"id\":\"8d0d2b59-2155-4eb2-a523-3c90b67df35f\"\n\t\t  }\n\t\t},\n\t\t\"api-keys\":{\n\t\t  \"data\":[\n\t\t\t{\n\t\t\t  \"type\":\"api-key\",\n\t\t\t  \"id\":\"c017b120-1a0d-4aa5-9667-51ac1c951383\"\n\t\t\t},\n\t\t\t{\n\t\t\t  \"type\":\"api-key\",\n\t\t\t  \"id\":\"50066ef1-30f8-40b7-a6ed-b5a56d7c255a\"\n\t\t\t}\n\t\t  ]\n\t\t}\n\t  }\n\t}\n}\n"
                        )
                     ),
                     @Content(
                        mediaType = "application/json",
                        schema = @Schema(implementation = PlainTechClientDataResourceDocument.class),
                        examples = @ExampleObject(
                           name = "Technical client",
                           value = "{\n\t \"meta\":{\n\t  \"type\":\"jsonapi.metadata.document\",\n\t  \"timestamp\":\"2020-02-18T10:21:26.825+01:00\"\n\t},\n\t\"data\":{\n\t  \"type\":\"tech-client\",\n\t  \"id\":\"4e7932a5-1a7f-43b9-ab3b-ecb3ea76635c\",\n\t  \"attributes\":{\n\t\t\"displayName\":\"myClient\",\n\t\t\"creationDate\":\"2020-02-18T10:19:36.106+01:00\"\n\t  },\n\t  \"relationships\":{\n\t\t\"subscription\":{\n\t\t  \"data\":{\n\t\t\t\"type\":\"subscription\",\n\t\t\t\"id\":\"8d0d2b59-2155-4eb2-a523-3c90b67df35f\"\n\t\t  }\n\t\t},\n\t\t\"api-keys\":{\n\t\t  \"data\":[\n\t\t\t{\n\t\t\t  \"type\":\"api-key\",\n\t\t\t  \"id\":\"c017b120-1a0d-4aa5-9667-51ac1c951383\"\n\t\t\t},\n\t\t\t{\n\t\t\t  \"type\":\"api-key\",\n\t\t\t  \"id\":\"50066ef1-30f8-40b7-a6ed-b5a56d7c255a\"\n\t\t\t}\n\t\t  ]\n\t\t}\n\t  }\n\t}\n}\n"
                        )
                     )
               }
            ),
            @ApiResponse(
               responseCode = "404",
               description = "The technical client 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
   @Operation(
      description = "Retrieves the specified technical client. See example response for more details.\n<div class=\"iam-resource-return\">The available information of the technical client.</div>",
      tags = "Technical Clients",
      summary = "Get specific client"
   )
   @Produces({"application/vnd.api+json", "application/json;qs=0.9"})
   public Response retrieveClient(@Parameter(description = "The identification of the technical client.") @PathParam("id") String id) {
      Optional<PlainTechClient> client = this.plainTechClientService().findTechClient(techClientIdentity(id));
      if (client.isEmpty()) {
         return techClientNotFound();
      }

      ResourceDocument<PlainTechClientData> document = toTechClientDocument(client.get(), this.mapper);
      return Response.ok(document).build();
   }

   @ApiResponses(
      {
            @ApiResponse(responseCode = "204", description = "The technical client has been deleted."),
            @ApiResponse(
               responseCode = "404",
               description = "The technical client 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 the specified technical client.\n<div class=\"iam-resource-return\">response</div>",
      tags = "Technical Clients",
      summary = "Delete technical client"
   )
   @Produces({"application/vnd.api+json", "application/json;qs=0.9"})
   public Response deleteClient(@Parameter(description = "The identification of the technical client.") @PathParam("id") String id) {
      Optional<PlainTechClient> client = this.plainTechClientService().findTechClient(techClientIdentity(id));
      if (client.isEmpty()) {
         return techClientNotFound();
      }

      this.plainTechClientService().delete(client.get().getId());
      this.signingLogger.log("Administrator %s deleted the technical client with ID %s", new Object[]{LogUtil.quote(this.currentAdmin()), LogUtil.quote(id)});
      return Response.noContent().build();
   }

   @ApiResponses(
      {
            @ApiResponse(
               responseCode = "200",
               description = "The technical client was updated successfully. The complete updated technical client is returned.",
               content = {
                     @Content(mediaType = "application/vnd.api+json", schema = @Schema(implementation = PlainTechClientDataResourceDocument.class)),
                     @Content(mediaType = "application/json", schema = @Schema(implementation = PlainTechClientDataResourceDocument.class))
               }
            ),
            @ApiResponse(
               responseCode = "404",
               description = "The requested technical client 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
   @Operation(
      description = "Updates the specified technical client.\n<p>Separate endpoints are available to lock or unlock a technical client.</p>\n<div class=\"iam-resource-return\">The updated technical client or an error document.</div>",
      tags = "Technical Clients",
      summary = "Update technical client"
   )
   @Consumes({"application/vnd.api+json", "application/json"})
   @Produces({"application/vnd.api+json", "application/json;qs=0.9"})
   public Response updateClient(
      @Parameter(description = "The identification of the technical client.") @PathParam("id") String id,
      @Parameter(description = "The document containing the attributes that should be updated. Omitted attributes are left unchanged.") @NotNull @Valid RequestDocument<PlainTechClientUpdateRequest> requestDocument
   ) {
      Optional<Response> errorResponse = this.idValidator.validate(id, requestDocument);
      if (errorResponse.isPresent()) {
         return errorResponse.get();
      }

      Optional<PlainTechClient> client = this.plainTechClientService().findTechClient(techClientIdentity(id));
      if (client.isEmpty()) {
         return techClientNotFound();
      }

      PlainTechClientUpdateRequest request = (PlainTechClientUpdateRequest)RequestDocumentAttributesMissingException.checkAttributesPresent(requestDocument);
      PlainTechClient clientUpdate = this.applyChangedValues(request, client.get());
      PlainTechClient updatedTechClient = this.plainTechClientService().update(clientUpdate);
      this.signingLogger
         .log(
            "Administrator %s updated a technical client (ID: %s)", new Object[]{LogUtil.quote(this.currentAdmin()), LogUtil.quote(updatedTechClient.getId())}
         );
      ResourceDocument<PlainTechClientData> document = toTechClientDocument(updatedTechClient, this.mapper);
      return Response.status(Status.OK).entity(document).build();
   }

   private PlainTechClient applyChangedValues(PlainTechClientUpdateRequest request, PlainTechClient client) {
      request.getDisplayName().ifPresent(client::setDisplayName);
      request.getLabel().ifPresent(client::setLabel);
      request.getDescription().ifPresent(client::setDescription);
      return client;
   }

   @ApiResponses(
      {
            @ApiResponse(
               responseCode = "200",
               description = "The response contains all API keys.",
               content = {
                     @Content(mediaType = "application/vnd.api+json", schema = @Schema(implementation = ApiKeyDataCollectionDocument.class)),
                     @Content(mediaType = "application/json", schema = @Schema(implementation = ApiKeyDataCollectionDocument.class))
               }
            ),
            @ApiResponse(
               responseCode = "404",
               description = "The technical client 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("/api-keys")
   @Produces({"application/vnd.api+json", "application/json;qs=0.9"})
   @Operation(
      description = "Retrieves all API keys for the specified technical client.\n<div class=\"iam-resource-return\">The collection of API keys.</div>",
      tags = "Technical Clients",
      summary = "List API keys"
   )
   public Response retrieveApiKeys(@Parameter(description = "The identification of the technical client.") @PathParam("id") String id) {
      Optional<ApiKeyTechClient> client = this.apiKeyService().findApiKeyTechClient(techClientIdentity(id));
      if (client.isEmpty()) {
         return techClientNotFound();
      }

      Set<ApiKey> apiKeys = client.get().getApiKeys();
      CollectionDocument<ApiKeyData> document = this.mapper.forEntities(apiKeys, ApiKeyData.class).toCollection();
      return Response.ok(document).build();
   }

   @ApiResponse(
      responseCode = "404",
      description = "The technical client 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("/subscription")
   @Produces({"application/vnd.api+json", "application/json;qs=0.9"})
   @Operation(
      description = "Retrieves the subscription for the specified technical client.\n<div class=\"iam-resource-return\">The subscription of this technical client if present.</div>",
      tags = "Technical Clients",
      summary = "Get subscription"
   )
   public Response retrieveSubscription(@Parameter(description = "The identification of the technical client.") @PathParam("id") String id) {
      Optional<PlainTechClient> client = ((PlainTechClientService)this.plainTechClientService.get()).findTechClient(techClientIdentity(id));
      if (client.isEmpty()) {
         return techClientNotFound();
      }

      Optional<Identity<Subscription>> subscriptionId = client.get().getSubscriptionId();
      if (subscriptionId.isEmpty()) {
         return Response.ok(new ResourceDocument(null)).build();
      }

      Optional<Subscription> subscription = ((SubscriptionService)this.subscriptionService.get()).findSubscription(subscriptionId.get());
      if (subscription.isEmpty()) {
         return ErrorResponse.notFound().build().toResponse();
      }

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

   private String currentAdmin() {
      return ((CurrentAdministrator)this.currentAdministrator.get()).getName();
   }

   private PlainTechClientService plainTechClientService() {
      return (PlainTechClientService)this.plainTechClientService.get();
   }

   private ApiKeyService apiKeyService() {
      return (ApiKeyService)this.apiKeyService.get();
   }

   static Identity<TechnicalClient> techClientIdentity(String id) {
      return DefaultStringIdentity.identity(id, TechnicalClient.class);
   }

   private static Response techClientNotFound() {
      return ErrorResponse.notFound().errorCode(IamRestErrorCode.TECH_CLIENT_NOT_FOUND).build().toResponse();
   }

   static <T extends TechnicalClient> ResourceDocument<PlainTechClientData> toTechClientDocument(T technicalClient, RestMapper mapper) {
      JsonApiResourceMapper<TechnicalClient, PlainTechClientData> jsonApiMapper = mapper.forEntity(technicalClient, PlainTechClientData.class);
      addCustomMappingContext((JsonApiMapper<T, PlainTechClientData>)jsonApiMapper);
      return jsonApiMapper.toResourceDocument();
   }

   static <T extends TechnicalClient> void addCustomMappingContext(JsonApiMapper<T, PlainTechClientData> mapper) {
      MappingContext mappingContext = OrikaMapperUtils.initMappingContext();
      mapper.withMappingContext(mappingContext);
   }
}
