GroupController.java

package ntnu.idatt2016.v233.SmartMat.controller.group;

import lombok.AllArgsConstructor;
import ntnu.idatt2016.v233.SmartMat.dto.request.group.ChangeAuthorityRequest;
import ntnu.idatt2016.v233.SmartMat.dto.request.group.GroupConnectionRequest;
import ntnu.idatt2016.v233.SmartMat.dto.request.group.GroupRequest;
import ntnu.idatt2016.v233.SmartMat.dto.response.group.GroupDetailsResponse;
import ntnu.idatt2016.v233.SmartMat.dto.response.group.UserAuthorityInfo;
import ntnu.idatt2016.v233.SmartMat.dto.response.group.GroupResponse;
import ntnu.idatt2016.v233.SmartMat.entity.group.Group;
import ntnu.idatt2016.v233.SmartMat.entity.group.UserGroupAsso;
import ntnu.idatt2016.v233.SmartMat.entity.group.UserGroupId;
import ntnu.idatt2016.v233.SmartMat.entity.user.User;
import ntnu.idatt2016.v233.SmartMat.service.group.GroupService;
import ntnu.idatt2016.v233.SmartMat.service.user.UserService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;

import java.util.*;

/**
 * Controller for groups API, providing endpoints for group management
 *
 * @author Anders Austlid, Pedro Cardona, Birk
 * @version 2.0
 */
@AllArgsConstructor
@RestController
@RequestMapping("/api/groups")
public class GroupController {
    private final GroupService groupService;

    private final UserService userService;

    /**
     * Gets a group by its id
     * @param groupId the id of the group
     * @param auth the authentication of the user
     * @return a ResponseEntity containing the group if it exists, or a 404 if it doesn't
     */
    @GetMapping("/{groupId}")
    public ResponseEntity<?> getGroupById(@PathVariable("groupId") long groupId, Authentication auth) {
        if (auth.getAuthorities().stream().noneMatch(role -> role.getAuthority().equals("ADMIN"))){
            if (!groupService.isUserAssociatedWithGroup(auth.getName(), groupId)) {
                return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
            }
        }

        Optional<Group> group = groupService.getGroupById(groupId);
        if (group.isPresent()) {
            List<UserGroupAsso> users = group.get().getUser();
            List<UserAuthorityInfo> userAuthorityInfoList = new ArrayList<>();

            for (UserGroupAsso user : users) {
                Optional<UserGroupAsso> userGroupAsso = groupService.getUserGroupAsso(user.getUser().getUsername(), groupId);
                if (userGroupAsso.isPresent()) {
                    String userAuthority = userGroupAsso.get().getGroupAuthority();
                    userAuthorityInfoList.add(new UserAuthorityInfo(user.getUser().getUsername(), userAuthority));
                }
            }

            GroupDetailsResponse groupDetailsResponse = new GroupDetailsResponse(group.get(), userAuthorityInfoList);

            return ResponseEntity.ok(groupDetailsResponse);
        }
        return ResponseEntity.badRequest().body("Group not found.");
    }


    /**
     * Creates a new group
     *
     * @param groupRequest the group to create
     * @param auth the authentication of the user
     * @return a ResponseEntity containing the created group if it was created successfully,
     * or a 400 if the group name is invalid or already exists, or if the username is invalid
     */
    @PostMapping("/group")
    public ResponseEntity<?> createGroup(@RequestBody GroupRequest groupRequest, Authentication auth) {
        if (groupRequest.groupName() == null || groupRequest.groupName().trim().length() < 3) {
            return ResponseEntity.badRequest().body("Group name must be at least 3 characters long.");
        }

        if (groupService.getGroupByName(groupRequest.groupName()).isPresent()) {
            return ResponseEntity.badRequest().body("Group name already exists.");
        }

        Optional<User> optionalUser = userService.getUserFromUsername(auth.getName());
        if (optionalUser.isEmpty()) {
            return ResponseEntity.badRequest().body("Invalid username.");
        }

        Group group = new Group();
        group.setGroupName(groupRequest.groupName());
        group.setPoints(10);
        group.setLevel(0);

        Group createdGroup = groupService.createGroup(group);

        User user = optionalUser.get();

        UserGroupId userGroupId = UserGroupId.builder()
                .username(user.getUsername())
                .groupId(createdGroup.getGroupId())
                .build();

        Optional<UserGroupAsso> oldPrimaryOptional = groupService.findPrimaryUserGroupAssoForUser(user.getUsername());
        if(oldPrimaryOptional.isPresent()){
            oldPrimaryOptional.get().setPrimaryGroup(false);
            groupService.updateUserGroupAsso(oldPrimaryOptional.get());
        }

        createdGroup.addUser(UserGroupAsso.builder()
                .id(userGroupId)
                .primaryGroup(true)
                .groupAuthority("ADMIN")
                .group(createdGroup)
                .user(user)
                .build());

        GroupResponse groupResponse = new GroupResponse(createdGroup.getGroupId(), createdGroup.getLinkCode());

        groupService.updateGroup(createdGroup);

        return ResponseEntity.ok(groupResponse);
    }


    /**
     * Gets the level of a group
     *
     * @param groupId the id of the group
     * @param auth the authentication of the user
     * @return a ResponseEntity containing the level of the group if it exists, or a 404 if it doesn't
     */
    @GetMapping("/{groupId}/level")
    public ResponseEntity<Long> getGroupLevel(@PathVariable("groupId") long groupId, Authentication auth) {
        if (auth.getAuthorities().stream().noneMatch(role -> role.getAuthority().equals("ADMIN"))){
            if (!groupService.isUserAssociatedWithGroup(auth.getName(), groupId)) {
                return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
            }
        }

        return groupService.getGroupById(groupId)
                .map(group -> ResponseEntity.ok(group.getLevel()))
                .orElseGet(() -> ResponseEntity.notFound().build());
    }


    /**
     * Returns the progress of the level for the group identified by the given ID.
     * Returns a ResponseEntity containing the progress of the current level as a percentage, or a 404 Not Found response if no Group with the given ID was found.
     *
     * @param groupId the ID of the group to query
     * @param auth    the Authentication object containing the user's credentials
     * @return a ResponseEntity containing the progress of the current level as a percentage, or a 404 Not Found response if no Group with the given ID was found
     */
    @GetMapping("/{groupId}/progress")
    public ResponseEntity<Integer> getProgressOfLevel(@PathVariable("groupId") long groupId, Authentication auth) {
        if(auth.getAuthorities().stream().noneMatch(role -> role.getAuthority().equals("ADMIN"))){
            if (!groupService.isUserAssociatedWithGroup(auth.getName(), groupId))
            {
                return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
            }
        }



        return groupService.getProgressOfLevel(groupId)
                .map(ResponseEntity::ok)
                .orElseGet(() -> ResponseEntity.notFound().build());
    }

    /**
     * Updates the open/closed status of the group with the specified ID.
     *
     * @param groupId the ID of the group to update
     * @param auth the authentication of the user
     * @return a ResponseEntity with a Boolean value indicating whether the operation was successful
     */
    @PutMapping("/{groupId}/changeOpen")
    public ResponseEntity<Boolean> changeOpenValue(@PathVariable("groupId") long groupId, Authentication auth) {
        if (auth.getAuthorities().stream().noneMatch(role -> role.getAuthority().equals("ADMIN"))){
            if (!groupService.isUserAssociatedWithGroup(auth.getName(), groupId)) {
                return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
            }
            if (!(groupService.getUserGroupAssoAuthority(auth.getName(), groupId).equals("ADMIN")))
                return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
        }


        return groupService.OpenOrCloseGroup(groupId)
                .map(ResponseEntity::ok)
                .orElseGet(() -> ResponseEntity.notFound().build());
    }

    /**
     * Handles the HTTP PUT request to change the primary group of a user.
     *
     * @param newPrimaryGroupId the ID of the new primary group
     * @param auth the authentication of the user
     * @return a ResponseEntity object containing an HTTP status code and the updated UserGroupAsso object,
     *         or a ResponseEntity object with an HTTP status code indicating that the request was not successful
     */
    @PutMapping("/markNewPrimary/{newPrimaryGroupId}")
    public ResponseEntity<?> markNewPrimaryGroup(@PathVariable("newPrimaryGroupId") long newPrimaryGroupId, Authentication auth) {
        if (!groupService.isUserAssociatedWithGroup(auth.getName(), newPrimaryGroupId)) {
            return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
        }

        Optional<User> optionalUser = userService.getUserFromUsername(auth.getName());

        if (optionalUser.isEmpty()) {
            return ResponseEntity.badRequest().body("Invalid username.");
        }

        User user = optionalUser.get();

        Optional<UserGroupAsso> oldPrimaryOpt = groupService.findPrimaryUserGroupAssoForUser(user.getUsername());
        if (oldPrimaryOpt.isEmpty()) {
            return ResponseEntity.badRequest().body("No primary group found for the user.");
        }

        UserGroupAsso oldPrimary = oldPrimaryOpt.get();
        oldPrimary.setPrimaryGroup(false);
        groupService.updateUserGroupAsso(oldPrimary);

        Optional<UserGroupAsso> newPrimaryOpt = groupService.getUserGroupAsso(user.getUsername(), newPrimaryGroupId);
        if (newPrimaryOpt.isEmpty()) {
            return ResponseEntity.badRequest().body("Invalid new primary group ID.");
        }

        UserGroupAsso newPrimary = newPrimaryOpt.get();
        newPrimary.setPrimaryGroup(true);
        groupService.updateUserGroupAsso(newPrimary);

        userService.updateUser(user);

        Map<String, Object> responseBody = new HashMap<>();
        responseBody.put("username", user.getUsername());
        responseBody.put("oldPrimaryGroupId", oldPrimary.getGroup().getGroupId());
        responseBody.put("newPrimaryGroupId", newPrimary.getGroup().getGroupId());
        return ResponseEntity.ok(responseBody);
    }

    /**
     * Handles the HTTP POST request to add a new connection between a user and a group.
     *
     * @param groupConnectionRequest the request object containing the username and link code of the user and group to be connected
     * @param auth the authentication of the user
     * @return a ResponseEntity object containing an HTTP status code and the newly created UserGroupAsso object,
     *         or a ResponseEntity object with an HTTP status code indicating that the request was not successful
     */
    @PostMapping("/connection")
    public ResponseEntity<?> addConnection(@RequestBody GroupConnectionRequest groupConnectionRequest, Authentication auth) {
        Optional<Group> optionalGroup = groupService.getGroupByLinkCode(groupConnectionRequest.linkCode());

        if (optionalGroup.isEmpty()) {
            return ResponseEntity.badRequest().body("Invalid link code.");
        }

        Optional<User> optionalUser = userService.getUserFromUsername(auth.getName());

        if (optionalUser.isEmpty()) {
            return ResponseEntity.badRequest().body("Invalid username.");
        }

        Group group = optionalGroup.get();
        User user = optionalUser.get();

        if (groupService.isUserAssociatedWithGroup(user.getUsername(), group.getGroupId())) {
            return ResponseEntity.badRequest().body("User is already associated with the group.");
        }

        UserGroupId userGroupId = UserGroupId.builder()
                .username(user.getUsername())
                .groupId(group.getGroupId())
                .build();

        UserGroupAsso userGroupAsso = UserGroupAsso.builder()
                .id(userGroupId)
                .group(group)
                .user(user)
                .build();

        user.addGroup(userGroupAsso);
        userService.updateUser(user);

        Map<String, Object> responseBody = new HashMap<>();
        responseBody.put("groupId", group.getGroupId());
        responseBody.put("username", user.getUsername());

        return ResponseEntity.ok(responseBody);
    }


    /**
     * Changes the authority level of a user in a group.
     *
     * @param authorityRequest the request object containing the username and group ID of the user to change the authority of
     * @param auth the authentication of the user
     * @return a ResponseEntity object containing the updated UserGroupAsso object and an HTTP status code of 200,
     *        or a ResponseEntity object with an HTTP status code of 403 if the user is not authorized to change the authority,
     *         or a ResponseEntity object with an HTTP status code of 404 if the group or user does not exist
     */
    @PutMapping("/groupAuthority")
    public ResponseEntity<?> changeAuthority(@RequestBody ChangeAuthorityRequest authorityRequest,
                                             Authentication auth) {
        Optional<User> groupAdminOpt = userService.getUserFromUsername(auth.getName());
        if (groupAdminOpt.isPresent()) {
            if (auth.getAuthorities().stream().noneMatch(role -> role.getAuthority().equals("ADMIN"))){
                if (!groupService.isUserAssociatedWithGroup(auth.getName(), authorityRequest.groupId())) {
                    return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
                }
                boolean isUserAdmin = groupService.getUserGroupAssoAuthority(auth.getName(), authorityRequest.groupId()).equals("ADMIN");
                System.out.println(auth.getName() + ((isUserAdmin)  ? " is admin" : " is not admin"));
                if (!isUserAdmin)
                    return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
            }

            Optional<Group> groupOpt = groupService.getGroupById(authorityRequest.groupId());
            Optional<User> userOpt = userService.getUserFromUsername(authorityRequest.username());

            if (groupOpt.isEmpty() || userOpt.isEmpty()) {
                return ResponseEntity.notFound().build();
            }

            User user = userOpt.get();
            UserGroupAsso userGroupAsso = user.getGroup().stream()
                    .filter(asso -> asso.getGroup().getGroupId() == authorityRequest.groupId())
                    .findFirst()
                    .orElse(null);

            if (userGroupAsso != null) {
                userGroupAsso.setGroupAuthority(authorityRequest.authority());
                userService.updateUser(user);
                return ResponseEntity.ok("Authority changed successfully.");
            } else {
                return ResponseEntity.notFound().build();
            }
        }
        return ResponseEntity.status(HttpStatus.FORBIDDEN)
                .body("Cant find user");
    }

    /**
     * Handles the HTTP DELETE request to remove a user from a group.
     * @param auth the authentication object containing the username of the user
     * @return a ResponseEntity object containing the list of groups the user is associated with and an HTTP status code of 200,
     */
    @GetMapping("/")
    public ResponseEntity<List<UserGroupAsso>> getAllGroupsByUser(Authentication auth) {
        return ResponseEntity.ok(groupService.getUserGroupAssoByUserName(auth.getName()));
    }


    /**
     * Handles the HTTP DELETE request to remove a user from a group.
     * @param groupId the ID of the group to get the members of
     * @param username the username of the user to remove from the group
     * @param auth the authentication object containing the username of the user
     * @return a ResponseEntity object containing the list of groups the
     * user is associated with and an HTTP status code of 200,
     */
    @DeleteMapping("/removeUser/{groupId}/{username}")
    public ResponseEntity<?> removeUserFromGroup(@PathVariable("groupId") long groupId,
                                                 @PathVariable("username") String username,
                                                 Authentication auth) {
        Optional<User> groupAdminOpt = userService.getUserFromUsername(auth.getName());
        if (groupAdminOpt.isPresent()) {
            User groupAdmin = groupAdminOpt.get();
            if (auth.getAuthorities().stream().noneMatch(role -> role.getAuthority().equals("ADMIN"))){
                if (!groupService.isUserAssociatedWithGroup(groupAdmin.getUsername(), groupId)) {
                    return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
                }
                try {
                    if (!username.equals(auth.getName()))
                        if (!(groupService.getUserGroupAssoAuthority(groupAdmin.getUsername(), groupId).equals("ADMIN")))
                            return ResponseEntity.status(HttpStatus.FORBIDDEN).build();

                } catch (IllegalArgumentException e) {
                    return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
                }
            }

            Optional<Group> groupOpt = groupService.getGroupById(groupId);
            Optional<User> userOpt = userService.getUserFromUsername(username);

            if (groupOpt.isEmpty() || userOpt.isEmpty()) {
                return ResponseEntity.notFound().build();
            }

            User user = userOpt.get();
            UserGroupAsso userGroupAsso = user.getGroup().stream()
                    .filter(asso -> asso.getGroup().getGroupId() == groupId)
                    .findFirst()
                    .orElse(null);

            if (userGroupAsso != null) {
                groupService.removeUserFromGroup(userGroupAsso);
                return ResponseEntity.ok("User removed successfully.");
            } else {
                return ResponseEntity.notFound().build();
            }
        }

        return ResponseEntity.status(HttpStatus.FORBIDDEN)
                .body("Cant find user");
    }
}