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

import ch.ergon.restal.jsonapi.document.data.ResourceDocument;
import ch.ergon.restal.jsonapi.document.data.ResourceObject;
import com.airlock.iam.admin.application.service.authmethod.AuthenticationMethodService;
import com.airlock.iam.admin.application.service.user.UserAdminService;
import com.airlock.iam.admin.domain.model.user.UserSpecificSettings;
import com.airlock.iam.admin.infrastructure.generated.transfer.response.user.UserDataResourceDocument;
import com.airlock.iam.admin.infrastructure.service.user.UserIdResolver;
import com.airlock.iam.admin.infrastructure.transfer.request.user.tokens.ChangeUsernameRequest;
import com.airlock.iam.admin.infrastructure.transfer.response.user.UserData;
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.api.infrastructure.uri.ExternalUriInfo;
import com.airlock.iam.base.infrastructure.mapper.FieldMapper;
import com.airlock.iam.base.infrastructure.mapper.ValidationToErrorMapper;
import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.common.infrastructure.mapper.MapBasedFieldMapper;
import com.airlock.iam.common.infrastructure.mapper.OrikaMapperUtils;
import com.airlock.iam.common.infrastructure.resource.Resources;
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.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.util.Map.Entry;
import ma.glasnost.orika.MapperFacade;
import ma.glasnost.orika.MappingContext;

@Path("/users/{userId}")
public class UserResource {
   private final MapperFacade mapper;
   private final Provider<ExternalUriInfo> uriInfo;
   private final HelpdeskLogger helpdeskLogger;
   private final Provider<UserAdminService> userServiceProvider;
   private final Provider<UserSpecificSettings> settings;
   private final Provider<AuthenticationMethodService> tokenService;
   private final ValidationToErrorMapper errorMapper;
   private final Provider<UserIdResolver> userIdResolver;

   @Inject
   public UserResource(
      Provider<ExternalUriInfo> uriInfo,
      MapperFacade mapper,
      HelpdeskLogger logger,
      Provider<UserAdminService> userServiceProvider,
      Provider<UserSpecificSettings> settings,
      Provider<AuthenticationMethodService> tokenService,
      ValidationToErrorMapper errorMapper,
      Provider<UserIdResolver> userIdResolver
   ) {
      this.uriInfo = uriInfo;
      this.mapper = mapper;
      this.userServiceProvider = userServiceProvider;
      this.helpdeskLogger = logger;
      this.settings = settings;
      this.tokenService = tokenService;
      this.errorMapper = errorMapper;
      this.userIdResolver = userIdResolver;
   }

   @ApiResponses(
      {
            @ApiResponse(
               responseCode = "200",
               description = "Requested user found.",
               content = {
                     @Content(mediaType = "application/vnd.api+json", schema = @Schema(implementation = UserDataResourceDocument.class)),
                     @Content(mediaType = "application/json", schema = @Schema(implementation = UserDataResourceDocument.class))
               }
            ),
            @ApiResponse(
               responseCode = "404",
               description = "The user does not exist or cannot be retrieved with the current configuration.",
               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
   @Produces({"application/vnd.api+json", "application/json;qs=0.9"})
   @Operation(
      description = "Retrieves the available information about a user.\n<div class=\"iam-resource-return\">The available information about the user.</div>",
      tags = "Users",
      summary = "Get user info"
   )
   public Response retrieve(@PathParam("userId") @Parameter(schema = @Schema(type = "string")) String userIdParam) {
      UserId userId = ((UserIdResolver)this.userIdResolver.get()).resolve(userIdParam);
      UserData data = this.retrieveUserData(userId);
      ResourceDocument<UserData> document = ResourceObject.builder(userId.getValue(), data)
         .linkSelf(Resources.uri((ExternalUriInfo)this.uriInfo.get(), UserResource.class).build(new Object[]{userId}))
         .document();
      return Response.ok(document).build();
   }

   private UserData retrieveUserData(UserId userId) {
      UserAdminService.AdminappUser user = ((UserAdminService)this.userServiceProvider.get()).loadUser(userId.getValue());
      MappingContext mappingContext = OrikaMapperUtils.initMappingContext();
      mappingContext.setProperty("additionalActiveAuthMethodKeys", ((AuthenticationMethodService)this.tokenService.get()).getAdditionalActiveAuthMethodKeys());
      UserData userData = (UserData)this.mapper.map(user.persistentUser(), UserData.class, mappingContext);

      for (Entry<String, Object> entry : user.filteredContextData().entrySet()) {
         userData.addContextData(entry.getKey(), entry.getValue());
      }

      return userData;
   }

   @ApiResponses(
      {
            @ApiResponse(responseCode = "204", description = "The user has been deleted."),
            @ApiResponse(
               responseCode = "403",
               description = "The user is not editable.",
               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 user 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 an existing user.\n<div class=\"iam-resource-return\">A Response indicating whether the user was successfully deleted or not.</div>",
      tags = "Users",
      summary = "Delete existing user"
   )
   @Consumes("application/json")
   @Produces({"application/vnd.api+json", "application/json;qs=0.9"})
   public Response delete(@PathParam("userId") @Parameter(schema = @Schema(type = "string")) String userIdParam) {
      UserId userId = ((UserIdResolver)this.userIdResolver.get()).resolve(userIdParam);
      ((UserSpecificSettings)this.settings.get()).failIfUserNotEditable();
      ((UserAdminService)this.userServiceProvider.get()).deleteUser(userId);
      this.helpdeskLogger.log(userId, "Administrator ${authentee} deleted user ${affectedUser}.");
      return Response.status(Status.NO_CONTENT).build();
   }

   @ApiResponses(
      {
            @ApiResponse(responseCode = "200", description = "Username was successfully changed."),
            @ApiResponse(
               responseCode = "403",
               description = "The user is not editable.",
               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 user 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"))
               }
            )
      }
   )
   @Path("/username/change")
   @POST
   @Consumes("application/json")
   @Operation(
      description = "Changes the username (identity) of the user.\n<div class=\"iam-resource-return\">response indicating whether or not the username was changed.</div>",
      tags = "Users",
      summary = "Change username"
   )
   @Produces({"application/vnd.api+json", "application/json;qs=0.9"})
   public Response changeUsername(
      @PathParam("userId") @Parameter(schema = @Schema(type = "string")) String userIdParam,
      @Parameter(description = "Contains the new desired username.") @NotNull @Valid ChangeUsernameRequest request
   ) {
      UserId userId = ((UserIdResolver)this.userIdResolver.get()).resolve(userIdParam);
      ((UserSpecificSettings)this.settings.get()).failIfUserNotEditable();
      String newUsername = request.getNewUsername();
      ValidationResult validationResult = ((UserAdminService)this.userServiceProvider.get()).validate(newUsername);
      if (!validationResult.isValid()) {
         return this.errorMapper
            .withFieldToSourcePointerMapper(this.usernameChangeFieldMapper())
            .withStatus(Status.BAD_REQUEST)
            .mapToResponse(validationResult.getErrors());
      }

      ((UserAdminService)this.userServiceProvider.get()).changeUsername(userId.getValue(), newUsername);
      this.helpdeskLogger
         .log(userId, "Administrator ${authentee} changed the username of user ${affectedUser} to %s.", new Object[]{LogUtil.quote(newUsername)});
      return Response.ok().build();
   }

   private FieldMapper usernameChangeFieldMapper() {
      return MapBasedFieldMapper.builder().mapping("username", "newUsername").build();
   }
}
