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

import ch.ergon.restal.jsonapi.document.data.CollectionDocument;
import ch.ergon.restal.jsonapi.document.data.ResourceDocument;
import ch.ergon.restal.jsonapi.document.data.ResourceObject;
import ch.ergon.restal.jsonapi.document.request.RequestDocument;
import com.airlock.iam.admin.application.service.role.RoleService;
import com.airlock.iam.admin.domain.model.user.UserSpecificSettings;
import com.airlock.iam.admin.infrastructure.generated.transfer.response.user.UserRoleDataCollectionDocument;
import com.airlock.iam.admin.infrastructure.generated.transfer.response.user.UserRoleDataResourceDocument;
import com.airlock.iam.admin.infrastructure.service.user.UserIdResolver;
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.error.ErrorResponse;
import com.airlock.iam.base.infrastructure.mapper.ValidationToErrorMapper;
import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.common.api.domain.model.role.Role;
import com.airlock.iam.common.domain.model.role.RolesUtil;
import com.airlock.iam.common.infrastructure.resource.Resources;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
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.parameters.RequestBody;
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.PUT;
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.Link.Builder;
import jakarta.ws.rs.core.Response.Status;
import java.net.URI;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;

@Path("/users/{userId}/roles")
public class UserRolesResource {
   private static final ELogger LOG = ELoggerFactory.getLogger(UserRolesResource.class);
   private static final String ADD_ROLE_EXAMPLE = "{\n\t\"data\": {\n\t\t\"id\": \"roleName\",\n\t\t\"type\": \"user.role\"\n\t}\n}\n";
   private final Provider<ExternalUriInfo> uriInfo;
   private final Provider<RoleService> roleService;
   private final Provider<UserSpecificSettings> settings;
   private final HelpdeskLogger helpdeskLogger;
   private final ValidationToErrorMapper errorMapper;
   private final Provider<UserIdResolver> userIdResolver;

   @Inject
   public UserRolesResource(
      Provider<ExternalUriInfo> uriInfo,
      Provider<RoleService> roleServiceProvider,
      Provider<UserSpecificSettings> settings,
      HelpdeskLogger logger,
      ValidationToErrorMapper errorMapper,
      Provider<UserIdResolver> userIdResolver
   ) {
      this.uriInfo = uriInfo;
      this.roleService = roleServiceProvider;
      this.settings = settings;
      this.helpdeskLogger = logger;
      this.errorMapper = errorMapper;
      this.userIdResolver = userIdResolver;
   }

   @ApiResponses(
      {
            @ApiResponse(
               responseCode = "200",
               description = "User roles retrieved.",
               content = {
                     @Content(mediaType = "application/vnd.api+json", schema = @Schema(implementation = UserRoleDataCollectionDocument.class)),
                     @Content(mediaType = "application/json", schema = @Schema(implementation = UserRoleDataCollectionDocument.class))
               }
            ),
            @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"))
               }
            )
      }
   )
   @GET
   @Produces({"application/vnd.api+json", "application/json;qs=0.9"})
   @Operation(
      description = "Retrieves the user roles.\n<div class=\"iam-resource-return\">The roles of the user.</div>",
      tags = "User Roles",
      summary = "List user roles"
   )
   public Response retrieveRoles(@PathParam("userId") @Parameter(schema = @Schema(type = "string")) String userIdParam) {
      UserId userId = ((UserIdResolver)this.userIdResolver.get()).resolve(userIdParam);
      Set<Role> userRoles = ((RoleService)this.roleService.get()).getUserRoles(userId);
      CollectionDocument<Void> document = new CollectionDocument();

      for (Role role : userRoles) {
         Builder roleSelfLinkBuilder = Resources.link((ExternalUriInfo)this.uriInfo.get(), UserRolesResource.class, role.formatRole());
         ResourceObject<Void> roleResource = ResourceObject.builder("user.role", role.formatRole())
            .linkSelf(roleSelfLinkBuilder.build(new Object[]{userId}).getUri())
            .build();
         document.add(roleResource);
      }

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

   @ApiResponses(
      {
            @ApiResponse(responseCode = "201", description = "The specified role was added to the user."),
            @ApiResponse(responseCode = "204", description = "The user already had the specified role."),
            @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"))
               }
            )
      }
   )
   @POST
   @Consumes({"application/vnd.api+json", "application/json"})
   @Operation(
      description = "Adds the specified role to the user.\n<div class=\"iam-resource-return\">Response that the role was added or already existed.</div>",
      tags = "User Roles",
      summary = "Add specified role"
   )
   @Produces({"application/vnd.api+json", "application/json;qs=0.9"})
   public Response addRole(
      @PathParam("userId") @Parameter(schema = @Schema(type = "string")) String userIdParam,
      @NotNull @Valid @RequestBody(
   description = "A document containing a resource object where the 'id' is the name of the role to add. Note that 'type' must always be 'user.role'.",
   required = true,
   content = {
         @Content(
            mediaType = "application/vnd.api+json",
            schema = @Schema(ref = "json-api.yaml#/components/schemas/ResourceDocumentIdentifierRequest"),
            examples = @ExampleObject(name = "Add role", value = "{\n\t\"data\": {\n\t\t\"id\": \"roleName\",\n\t\t\"type\": \"user.role\"\n\t}\n}\n")
         ),
         @Content(
            mediaType = "application/json",
            schema = @Schema(ref = "json-api.yaml#/components/schemas/ResourceDocumentIdentifierRequest"),
            examples = @ExampleObject(name = "Add role", value = "{\n\t\"data\": {\n\t\t\"id\": \"roleName\",\n\t\t\"type\": \"user.role\"\n\t}\n}\n")
         )
   }
) RequestDocument<Void> roleRequestDocument
   ) {
      UserId userId = ((UserIdResolver)this.userIdResolver.get()).resolve(userIdParam);
      ((UserSpecificSettings)this.settings.get()).failIfUserNotEditable();
      Role role = Role.parseRole(roleRequestDocument.getData().getId());
      ValidationResult result = ((RoleService)this.roleService.get()).validateAddRole(role);
      if (!result.isValid()) {
         return this.errorMapper.withJsonApiAttributesMapper("role").mapToResponse(result.getErrors());
      } else if (((RoleService)this.roleService.get()).addRoleToUser(userId, role)) {
         URI link = Resources.link((ExternalUriInfo)this.uriInfo.get(), UserRolesResource.class, role.formatRole()).build(new Object[]{userId}).getUri();
         this.helpdeskLogger.log(userId, "Administrator ${authentee} added role %s to user ${affectedUser}.", new Object[]{LogUtil.quote(role)});
         return Response.created(link).build();
      } else {
         this.helpdeskLogger
            .log(
               userId,
               "Administrator ${authentee} tried to add role %s to user ${affectedUser} but user already had the role.",
               new Object[]{LogUtil.quote(role)}
            );
         return Response.noContent().build();
      }
   }

   @ApiResponses(
      {
            @ApiResponse(
               responseCode = "200",
               description = "The user has the specified role.",
               content = {
                     @Content(mediaType = "application/vnd.api+json", schema = @Schema(implementation = UserRoleDataResourceDocument.class)),
                     @Content(mediaType = "application/json", schema = @Schema(implementation = UserRoleDataResourceDocument.class))
               }
            ),
            @ApiResponse(
               responseCode = "404",
               description = "The user does not exist or does not have the specified role.",
               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"})
   @Path("{rolename}")
   @Operation(
      description = "Checks if a user has the specific role.\n<div class=\"iam-resource-return\">Response if the user has the role or not.</div>",
      tags = "User Roles",
      summary = "Check user role"
   )
   public Response retrieveRole(
      @PathParam("userId") @Parameter(schema = @Schema(type = "string")) String userIdParam,
      @Parameter(description = "The role to check.") @PathParam("rolename") String roleName
   ) {
      UserId userId = ((UserIdResolver)this.userIdResolver.get()).resolve(userIdParam);
      if (((RoleService)this.roleService.get()).hasUserRole(userId, Role.parseRole(roleName))) {
         ResourceDocument<Void> document = ResourceObject.builder("user.role", roleName)
            .linkSelf(Resources.link((ExternalUriInfo)this.uriInfo.get(), UserRolesResource.class, roleName).build(new Object[]{userId}).getUri())
            .document();
         return Response.ok(document).build();
      } else {
         LOG.info("The user does not have the requested role: " + LogUtil.logSafeQuote(roleName));
         return ErrorResponse.notFound().build().toResponse();
      }
   }

   @ApiResponses(
      {
            @ApiResponse(responseCode = "204", description = "Removed the specified role from the user."),
            @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 or does not have the\nrole to be removed.\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"))
               }
            )
      }
   )
   @DELETE
   @Operation(
      description = "Removes the specified role from the user.\n<div class=\"iam-resource-return\">Response that role was deleted.</div>",
      tags = "User Roles",
      summary = "Remove specified role"
   )
   @Consumes("application/json")
   @Produces({"application/vnd.api+json", "application/json;qs=0.9"})
   @Path("{rolename}")
   public Response removeRole(
      @PathParam("userId") @Parameter(schema = @Schema(type = "string")) String userIdParam,
      @Parameter(description = "The role to remove.") @PathParam("rolename") String roleName
   ) {
      UserId userId = ((UserIdResolver)this.userIdResolver.get()).resolve(userIdParam);
      if (!((UserSpecificSettings)this.settings.get()).isEditable()) {
         LOG.info("User " + LogUtil.logSafeQuote(userId) + " is not editable");
         return ErrorResponse.forbidden().build().toResponse();
      } else {
         Role role = Role.parseRole(roleName);
         ValidationResult result = ((RoleService)this.roleService.get()).validateRemoveRole(role);
         if (!result.isValid()) {
            return this.errorMapper.withJsonApiAttributesMapper("role").mapToResponse(result.getErrors());
         } else {
            boolean roleRemoved = ((RoleService)this.roleService.get()).removeRoleFromUser(userId, role);
            if (roleRemoved) {
               this.helpdeskLogger.log(userId, "Administrator ${authentee} removed role %s from user ${affectedUser}.", new Object[]{LogUtil.quote(role)});
               return Response.noContent().build();
            } else {
               this.helpdeskLogger
                  .log(
                     userId,
                     "Administrator ${authentee} tried to remove role %s from user ${affectedUser} but user did not have the role.",
                     new Object[]{LogUtil.quote(role)}
                  );
               return Response.status(Status.NOT_FOUND).build();
            }
         }
      }
   }

   @ApiResponses(
      {
            @ApiResponse(responseCode = "200", description = "The specified role was added to the user."),
            @ApiResponse(responseCode = "204", description = "The user already had the specified role."),
            @ApiResponse(
               responseCode = "400",
               description = "Some roles are not modifiable by the current admin.",
               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 = "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"))
               }
            )
      }
   )
   @PUT
   @Operation(
      description = "Replaces all roles with the new ones.\n<div class=\"iam-resource-return\">The updated set of roles of this user.</div>",
      extensions = @Extension(properties = @ExtensionProperty(name = "x-internal", value = "true")),
      tags = "User Roles",
      summary = "Replace all roles"
   )
   @Consumes({"application/vnd.api+json", "application/json"})
   @Produces({"application/vnd.api+json", "application/json;qs=0.9"})
   public Response updateRoles(
      @PathParam("userId") @Parameter(schema = @Schema(type = "string")) String userIdParam,
      @Parameter(description = "Collection of new roles.") @NotNull @Valid CollectionDocument<Role> request
   ) {
      UserId userId = ((UserIdResolver)this.userIdResolver.get()).resolve(userIdParam);
      if (!((UserSpecificSettings)this.settings.get()).isEditable()) {
         LOG.info("User " + LogUtil.logSafeQuote(userId) + " is not editable");
         return ErrorResponse.forbidden().build().toResponse();
      }

      Set<Role> newRoles = this.extractRoles(request);
      ValidationResult result = ((RoleService)this.roleService.get()).validateSetRoles(userId, newRoles);
      if (!result.isValid()) {
         return this.errorMapper.withJsonApiArrayParameterMapper(request.getData()).mapToResponse(result.getErrors());
      }

      Set<Role> updatedRoles = ((RoleService)this.roleService.get()).setUserRoles(userId, newRoles);
      this.logUserRolesChange(userId, updatedRoles);
      return Response.ok().build();
   }

   private void logUserRolesChange(UserId userId, Set<Role> updatedRoles) {
      String resultingRoles = RolesUtil.rolesToCommaSeparatedString(updatedRoles);
      if (StringUtils.isBlank(resultingRoles)) {
         this.helpdeskLogger.log(userId, "Administrator ${authentee} removed all roles on user ${affectedUser}.");
      } else {
         this.helpdeskLogger
            .log(userId, "Administrator ${authentee} updated roles on user ${affectedUser}. User has roles: %s.", new Object[]{LogUtil.quote(resultingRoles)});
      }
   }

   private Set<Role> extractRoles(CollectionDocument<Role> request) {
      return request.getData().stream().<String>map(ResourceObject::getId).filter(Objects::nonNull).<Role>map(Role::parseRole).collect(Collectors.toSet());
   }
}
