package com.iqser.red.persistence.service.v1.external.api.impl.controller;

import static com.iqser.red.service.persistence.management.v1.processor.roles.ActionRoles.ADD_UPDATE_DOSSIER;
import static com.iqser.red.service.persistence.management.v1.processor.roles.ActionRoles.ARCHIVE_DOSSIER;
import static com.iqser.red.service.persistence.management.v1.processor.roles.ActionRoles.DELETE_DOSSIER;
import static com.iqser.red.service.persistence.management.v1.processor.roles.ActionRoles.READ_DOSSIER;
import static com.iqser.red.service.persistence.management.v1.processor.roles.ActionRoles.UNARCHIVE_DOSSIER;

import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;

import com.iqser.red.service.persistence.management.v1.processor.entity.dossier.DossierAttributeEntity;
import com.iqser.red.service.persistence.management.v1.processor.model.websocket.DossierEventType;
import com.iqser.red.service.persistence.management.v1.processor.service.DossierCreatorService;

import com.iqser.red.service.persistence.management.v1.processor.service.FilterByPermissionsService;
import com.iqser.red.service.persistence.service.v1.api.shared.model.DossierChangeResponseV2;
import com.iqser.red.service.persistence.service.v1.api.shared.model.JsonNode;
import org.apache.commons.lang3.StringUtils;

import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.prepost.PostFilter;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.access.prepost.PreFilter;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.google.common.collect.Lists;
import com.iqser.red.service.persistence.management.v1.processor.acl.custom.dossier.DossierACLService;
import com.iqser.red.service.persistence.management.v1.processor.exception.BadRequestException;
import com.iqser.red.service.persistence.management.v1.processor.roles.ApplicationRoles;
import com.iqser.red.service.persistence.management.v1.processor.service.AccessControlService;
import com.iqser.red.service.persistence.management.v1.processor.service.DossierManagementService;
import com.iqser.red.service.persistence.management.v1.processor.service.FileStatusManagementService;
import com.iqser.red.service.persistence.management.v1.processor.service.websocket.WebsocketService;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.AuditPersistenceService;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.DossierAttributePersistenceService;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.NotificationPersistenceService;
import com.iqser.red.service.persistence.management.v1.processor.service.users.UserService;
import com.iqser.red.service.persistence.management.v1.processor.service.users.model.User;
import com.iqser.red.service.persistence.service.v1.api.external.resource.DossierResource;
import com.iqser.red.service.persistence.service.v1.api.shared.model.AuditCategory;
import com.iqser.red.service.persistence.service.v1.api.shared.model.DossierChangeEntry;
import com.iqser.red.service.persistence.service.v1.api.shared.model.DossierInformation;
import com.iqser.red.service.persistence.service.v1.api.shared.model.DossierRequest;
import com.iqser.red.service.persistence.service.v1.api.shared.model.audit.AddNotificationRequest;
import com.iqser.red.service.persistence.service.v1.api.shared.model.audit.AuditRequest;
import com.iqser.red.service.persistence.service.v1.api.shared.model.common.JSONPrimitive;
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.dossier.CreateOrUpdateDossierRequest;
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.dossier.Dossier;
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.dossier.DossierAttributes;
import com.iqser.red.service.persistence.service.v1.api.shared.model.notification.NotificationType;
import com.knecon.fforesight.keycloakcommons.security.KeycloakSecurity;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@RestController
@RequiredArgsConstructor
public class DossierController implements DossierResource {

    private static final Set<String> VALID_MEMBER_ROLES = Set.of(ApplicationRoles.RED_USER_ROLE, ApplicationRoles.RED_MANAGER_ROLE);

    private final DossierManagementService dossierManagementService;
    private final UserService userService;
    private final FilterByPermissionsService filterByPermissionsService;
    private final FileStatusManagementService fileStatusManagementService;
    private final AuditPersistenceService auditPersistenceService;
    private final NotificationPersistenceService notificationPersistenceService;
    private final AccessControlService accessControlService;
    private final DossierACLService dossierACLService;
    private final DossierCreatorService dossierCreatorService;
    private final WebsocketService websocketService;
    private final DossierAttributePersistenceService dossierAttributePersistenceService;


    @Override
    @PreAuthorize("hasAuthority('" + READ_DOSSIER + "')")
    public DossierInformation getDossierInformation() {

        return dossierManagementService.getDossierInformation(dossierACLService.getDossierIdsWithViewPermission());
    }


    @Override
    @PreAuthorize("hasAuthority('" + READ_DOSSIER + "')")
    @PostFilter("hasPermission(filterObject.dossierId, 'Dossier', 'VIEW_OBJECT')")
    public List<DossierChangeEntry> changesSince(@RequestBody JSONPrimitive<OffsetDateTime> since) {

        return dossierManagementService.changesSince(since)
                .stream()
                .map(d -> new DossierChangeEntry(d.getDossierId(), d.isDossierChanges(), d.isFileChanges()))
                .collect(Collectors.toList());
    }


    @Override
    @PreAuthorize("hasAuthority('" + READ_DOSSIER + "')")
    public DossierChangeResponseV2 changesSinceV2(@RequestBody JSONPrimitive<OffsetDateTime> since) {

        DossierChangeResponseV2 changes = dossierManagementService.changesSinceV2(since);

        // filter only viewables
        changes.setFileChanges(filterByPermissionsService.onlyViewableHavingDossierId(changes.getFileChanges()));
        changes.setDossierChanges(filterByPermissionsService.onlyViewableHavingDossierId(changes.getDossierChanges()));

        return changes;
    }


    @Override
    @PreAuthorize("hasAuthority('" + ADD_UPDATE_DOSSIER + "') && (#dossierRequest.dossierId == null || hasPermission(#dossierRequest.dossierId, 'Dossier', 'ACCESS_OBJECT') )")
    public ResponseEntity<Dossier> createDossierOrUpdateDossier(@RequestBody DossierRequest dossierRequest) {

        if (dossierRequest.getDossierId() != null && dossierRequest.getOwnerId() == null) {
            throw new BadRequestException("Owner must be set for any update");
        }

        // this code always executes - members, approvers and owner must be valid
        String ownerId = getAndValidateOwnerId(dossierRequest.getOwnerId());
        Set<String> members = getAndValidateMembers(ownerId, dossierRequest.getMemberIds());
        Set<String> approvers = getAndValidateMembers(ownerId, dossierRequest.getApproverIds());
        members.addAll(approvers);

        if ((dossierRequest.getDownloadFileTypes() == null || dossierRequest.getDownloadFileTypes().isEmpty()) && (dossierRequest.getReportTemplateIds() == null
                                                                                                                   || dossierRequest.getReportTemplateIds().isEmpty())) {
            throw new BadRequestException("Download and report types cannot both be empty");
        }

        // if dossierId is set - load dossier
        if (StringUtils.isNotEmpty(dossierRequest.getDossierId())) {

            Dossier existingDossier = dossierACLService.enhanceDossierWithACLData(dossierManagementService.getDossierById(dossierRequest.getDossierId(), true, false));

            if (existingDossier.getArchivedTime() != null) {
                checkValidityForArchivedDossierUpdateRequest(dossierRequest, existingDossier);
            }

            // update using data from request and computed owner/members/approvers
            Dossier updatedDossier = dossierManagementService.updateDossier(CreateOrUpdateDossierRequest.builder()
                                                                                    .dossierName(dossierRequest.getDossierName())
                                                                                    .description(dossierRequest.getDescription())
                                                                                    .dossierTemplateId(dossierRequest.getDossierTemplateId())
                                                                                    .downloadFileTypes(dossierRequest.getDownloadFileTypes())
                                                                                    .dueDate(dossierRequest.getDueDate())
                                                                                    .reportTemplateIds(new ArrayList<>(dossierRequest.getReportTemplateIds()))
                                                                                    .watermarkId(dossierRequest.getWatermarkId())
                                                                                    .previewWatermarkId(dossierRequest.getPreviewWatermarkId())
                                                                                    .dossierStatusId(dossierRequest.getDossierStatusId())
                                                                                    .build(), existingDossier.getId());

            dossierACLService.updateDossierACL(members, approvers, ownerId, updatedDossier.getId());
            dossierACLService.enhanceDossierWithACLData(updatedDossier);

            updateFileStatusForDossierFiles(updatedDossier.getId(), members);

            auditPersistenceService.audit(AuditRequest.builder()
                                                  .userId(KeycloakSecurity.getUserId())
                                                  .objectId(updatedDossier.getId())
                                                  .category(AuditCategory.DOSSIER.name())
                                                  .message("Dossier has been updated.")
                                                  .build());

            if (existingDossier.getOwnerId() == null || !existingDossier.getOwnerId().equals(ownerId)) {
                if (ownerId != null && !ownerId.equals(KeycloakSecurity.getUserId())) {
                    notificationPersistenceService.insertNotification(AddNotificationRequest.builder()
                                                                              .userId(ownerId)
                                                                              .issuerId(KeycloakSecurity.getUserId())
                                                                              .notificationType(NotificationType.DOSSIER_OWNER_SET.name())
                                                                              .target(Map.of("dossierId", dossierRequest.getDossierId()))
                                                                              .build());
                }
                if (existingDossier.getOwnerId() != null && !existingDossier.getOwnerId().equals(KeycloakSecurity.getUserId())) {
                    notificationPersistenceService.insertNotification(AddNotificationRequest.builder()
                                                                              .userId(existingDossier.getOwnerId())
                                                                              .issuerId(KeycloakSecurity.getUserId())
                                                                              .notificationType(NotificationType.DOSSIER_OWNER_REMOVED.name())
                                                                              .target(Map.of("dossierId", dossierRequest.getDossierId()))
                                                                              .build());
                }
            }

            Set<String> uniqueMembers = new LinkedHashSet<>(members);
            uniqueMembers.addAll(approvers);

            uniqueMembers.stream()
                    .filter(member -> !member.equals(ownerId) && !member.equals(KeycloakSecurity.getUserId()) && (existingDossier.getMemberIds() == null
                                                                                                                  || !existingDossier.getMemberIds().contains(member)))
                    .forEach(member -> notificationPersistenceService.insertNotification(AddNotificationRequest.builder()
                                                                                                 .userId(member)
                                                                                                 .issuerId(KeycloakSecurity.getUserId())
                                                                                                 .notificationType(NotificationType.USER_BECOMES_DOSSIER_MEMBER.name())
                                                                                                 .target(Map.of("dossierId", dossierRequest.getDossierId()))
                                                                                                 .build()));

            if (existingDossier.getMemberIds() != null) {
                existingDossier.getMemberIds()
                        .stream()
                        .filter(member -> !members.contains(member) && !approvers.contains(member) && !member.equals(KeycloakSecurity.getUserId()))
                        .forEach(member -> notificationPersistenceService.insertNotification(AddNotificationRequest.builder()
                                                                                                     .userId(member)
                                                                                                     .issuerId(KeycloakSecurity.getUserId())
                                                                                                     .notificationType(NotificationType.USER_REMOVED_AS_DOSSIER_MEMBER.name())
                                                                                                     .target(Map.of("dossierId", dossierRequest.getDossierId()))
                                                                                                     .build()));
            }

            approvers.stream()
                    .filter(approver -> !KeycloakSecurity.getUserId().equals(approver) && existingDossier.getMemberIds() != null && existingDossier.getMemberIds()
                            .contains(approver) && (existingDossier.getApproverIds() == null || !existingDossier.getApproverIds().contains(approver)))
                    .forEach(approver -> notificationPersistenceService.insertNotification(AddNotificationRequest.builder()
                                                                                                   .userId(approver)
                                                                                                   .issuerId(KeycloakSecurity.getUserId())
                                                                                                   .notificationType(NotificationType.USER_PROMOTED_TO_APPROVER.name())
                                                                                                   .target(Map.of("dossierId", dossierRequest.getDossierId()))
                                                                                                   .build()));

            members.stream()
                    .filter(member -> !member.equals(KeycloakSecurity.getUserId())
                                      && existingDossier.getApproverIds() != null
                                      && existingDossier.getApproverIds().contains(member)
                                      && !approvers.contains(member))
                    .forEach(member -> notificationPersistenceService.insertNotification(AddNotificationRequest.builder()
                                                                                                 .userId(member)
                                                                                                 .issuerId(KeycloakSecurity.getUserId())
                                                                                                 .notificationType(NotificationType.USER_DEGRADED_TO_REVIEWER.name())
                                                                                                 .target(Map.of("dossierId", dossierRequest.getDossierId()))
                                                                                                 .build()));

            websocketService.sendDossierEvent(dossierRequest.getDossierId(), DossierEventType.UPDATE);

            updatedDossier.setDossierAttributes(convertDossierAttributes(dossierAttributePersistenceService.getDossierAttributes(updatedDossier.getId())));

            HttpHeaders httpHeaders = new HttpHeaders();
            httpHeaders.setContentType(MediaType.APPLICATION_JSON);
            return new ResponseEntity<>(updatedDossier, httpHeaders, HttpStatus.OK);

        } else {
            Dossier created = createNewDossier(dossierRequest, ownerId, members, approvers);

            auditPersistenceService.audit(AuditRequest.builder()
                                                  .userId(KeycloakSecurity.getUserId())
                                                  .objectId(created.getId())
                                                  .category(AuditCategory.DOSSIER.name())
                                                  .message("Dossier has been created.")
                                                  .build());

            websocketService.sendDossierEvent(created.getId(), DossierEventType.CREATE);
            HttpHeaders httpHeaders = new HttpHeaders();
            httpHeaders.setContentType(MediaType.APPLICATION_JSON);
            return new ResponseEntity<>(created, httpHeaders, HttpStatus.CREATED);
        }

    }


    private String getAndValidateOwnerId(String ownerId) {

        String currentUserId = KeycloakSecurity.getUserId();
        // quick check if self, just proceed
        if (currentUserId.equals(ownerId)) {
            return currentUserId;
        }

        String actualOwnerId = ownerId == null ? currentUserId : ownerId;

        var user = userService.getUserById(actualOwnerId);
        if (user.isEmpty()) {
            userService.removeDeletedUsers(Collections.singleton(ownerId));
            actualOwnerId = null;
        }

        // check he has a manager role, thus he can be the owner
        if (user.isPresent() && user.get().getRoles()
                .stream()
                .noneMatch(ApplicationRoles.RED_MANAGER_ROLE::equals)) {
            throw new BadRequestException("Make sure provided user id has the manager role.");
        }

        return actualOwnerId;

    }


    private Set<String> getAndValidateMembers(String ownerId, Set<String> memberIds) {

        Set<String> actualMemberIds = memberIds == null ? new TreeSet<>() : memberIds;
        if (actualMemberIds.stream()
                .anyMatch(Objects::isNull)) {
            throw new BadRequestException("Member IDs cannot be null");
        }

        // add owner automatically
        if (ownerId != null) {
            actualMemberIds.add(ownerId);
        }

        List<User> users = userService.getUsersByIds(actualMemberIds);
        if (users.size() != actualMemberIds.size()) {
            Set<String> deletedUserIds = userService.removeDeletedUsers(actualMemberIds);
            actualMemberIds.removeAll(deletedUserIds);
        }
        if (users.stream()
                .anyMatch(u -> u.getRoles()
                        .stream()
                        .noneMatch(VALID_MEMBER_ROLES::contains))) {
            throw new BadRequestException("Make sure each provided member id has the permission to be a member of a dossier.");
        }
        return actualMemberIds;
    }


    private void checkValidityForArchivedDossierUpdateRequest(DossierRequest updatedDossier, Dossier existingDossier) {
        //these must not be editted for archived dossiers
        checkEquality(updatedDossier.getDossierTemplateId(), existingDossier.getDossierTemplateId(), "dossier template id");
        checkEquality(updatedDossier.getDossierName(), existingDossier.getDossierName(), "dossier name");
        checkEquality(updatedDossier.getDescription(), existingDossier.getDescription(), "the description");
        checkEquality(updatedDossier.getDueDate(), existingDossier.getDueDate(), "the due date");
        checkEquality(updatedDossier.getDossierStatusId(), existingDossier.getDossierStatusId(), "dossier status id");
    }


    private void updateFileStatusForDossierFiles(String dossierId, Collection<String> members) {

        fileStatusManagementService.getDossierStatus(dossierId)
                .stream()
                .filter(fileStatus -> !fileStatus.isSoftOrHardDeleted())
                .forEach(f -> {

                    if (f.getAssignee() != null && !members.contains(f.getAssignee())) {
                        fileStatusManagementService.setCurrentFileAssignee(dossierId, f.getId(), null);
                    }
                });

    }


    private Dossier createNewDossier(DossierRequest dossier, String ownerId, Set<String> members, Set<String> approvers) {

        Dossier newDossier = dossierCreatorService.addDossier(CreateOrUpdateDossierRequest.builder()
                                                                      .dossierName(dossier.getDossierName().trim())
                                                                      .description(dossier.getDescription())
                                                                      .dossierTemplateId(dossier.getDossierTemplateId())
                                                                      .downloadFileTypes(dossier.getDownloadFileTypes())
                                                                      .dueDate(dossier.getDueDate())
                                                                      .reportTemplateIds(dossier.getReportTemplateIds()
                                                                                         != null ? new ArrayList<>(dossier.getReportTemplateIds()) : Lists.newArrayList())
                                                                      .watermarkId(dossier.getWatermarkId())
                                                                      .previewWatermarkId(dossier.getPreviewWatermarkId())
                                                                      .dossierStatusId(dossier.getDossierStatusId())
                                                                      .build(), members, approvers, ownerId);

        dossierACLService.enhanceDossierWithACLData(newDossier);
        newDossier.setDossierAttributes(new DossierAttributes());

        return newDossier;

    }


    private <T> void checkEquality(T updatedValue, T existingValue, String message) {

        if (updatedValue == null && existingValue != null || updatedValue != null && existingValue == null) {
            throw new BadRequestException(String.format("You cannot edit %s in an archived dossier.", message));
        }
        if (updatedValue == null && existingValue == null) {
            return;
        }

        if (!updatedValue.equals(existingValue)) {
            throw new BadRequestException(String.format("You cannot edit %s in an archived dossier.", message));
        }
    }


    @PreAuthorize("hasAuthority('" + DELETE_DOSSIER + "')")
    public void deleteDossier(@PathVariable(DOSSIER_ID_PARAM) String dossierId) {

        Dossier dossier = dossierACLService.enhanceDossierWithACLData(dossierManagementService.getDossierById(dossierId, true, false));
        accessControlService.checkAccessPermissionsToDossier(dossierId);

        if (dossier.getOwnerId() != null && !dossier.getOwnerId().equals(KeycloakSecurity.getUserId())) {
            throw new AccessDeniedException("Can not delete dossier that is owned by a different user");
        }


        dossierManagementService.softDeleteDossier(dossierId);

        auditPersistenceService.audit(AuditRequest.builder()
                                              .userId(KeycloakSecurity.getUserId())
                                              .objectId(dossierId)
                                              .category(AuditCategory.DOSSIER.name())
                                              .message("Dossier moved to trash.")
                                              .build());

        dossier.getMemberIds()
                .stream()
                .filter(m -> !KeycloakSecurity.getUserId().equals(m))
                .forEach(member -> notificationPersistenceService.insertNotification(AddNotificationRequest.builder()
                                                                                             .userId(member)
                                                                                             .issuerId(KeycloakSecurity.getUserId())
                                                                                             .notificationType(NotificationType.DOSSIER_DELETED.name())
                                                                                             .target(Map.of("dossierId", dossierId, "dossierName", dossier.getDossierName()))
                                                                                             .build()));

        websocketService.sendDossierEvent(dossierId, DossierEventType.SOFT_DELETE);
    }


    @Override
    @PreAuthorize("hasAuthority('" + READ_DOSSIER + "')")
    public JSONPrimitive<Map<String, Dossier>> getDossiersByIds(@RequestBody JSONPrimitive<Set<String>> dossierIds) {

        // filter dossiers based on view
        var viewableDossierIds = filterByPermissionsService.onlyViewableDossierIds(dossierIds.getValue());
        // load dossiers
        var dossiers = dossierManagementService.getDossiersByIds(viewableDossierIds);

        // add attributes and ACL - already filtered before loading
        enhanceDossiersWithAttributeAndACLData(dossiers,false);

        // build response
        var responseMap = new LinkedHashMap<String, Dossier>();
        for (var dossier : dossiers) {
            responseMap.put(dossier.getId(), dossier);
        }

        return new JSONPrimitive<>(responseMap);
    }


    @PreAuthorize("hasAuthority('" + READ_DOSSIER + "')")
    public Dossier getDossier(@PathVariable(DOSSIER_ID_PARAM) String dossierId,
                              @RequestParam(name = INCLUDE_ARCHIVED_PARAM, defaultValue = "false", required = false) boolean includeArchived,
                              @RequestParam(name = INCLUDE_DELETED_PARAM, defaultValue = "false", required = false) boolean includeDeleted) {

        accessControlService.checkViewPermissionsToDossier(dossierId);
        var dossier = dossierManagementService.getDossierById(dossierId, includeArchived, includeDeleted);
        dossier.setDossierAttributes(convertDossierAttributes(dossierAttributePersistenceService.getDossierAttributes(dossierId)));
        return dossierACLService.enhanceDossierWithACLData(dossier);
    }


    @PreAuthorize("hasAuthority('" + READ_DOSSIER + "')")
    public List<Dossier> getDossiers(@RequestParam(name = INCLUDE_ARCHIVED_PARAM, defaultValue = "false", required = false) boolean includeArchived,
                                     @RequestParam(name = INCLUDE_DELETED_PARAM, defaultValue = "false", required = false) boolean includeDeleted) {

        var dossiers = dossierManagementService.getAllDossiers(includeArchived, includeDeleted);
        return enhanceDossiersWithAttributeAndACLData(dossiers);
    }


    @PreAuthorize("hasAuthority('" + READ_DOSSIER + "')")
    public List<Dossier> getDossiersForDossierTemplate(@PathVariable(DOSSIER_TEMPLATE_ID_PARAM) String dossierTemplateId,
                                                       @RequestParam(name = INCLUDE_ARCHIVED_PARAM, defaultValue = "false", required = false) boolean includeArchived,
                                                       @RequestParam(name = INCLUDE_DELETED_PARAM, defaultValue = "false", required = false) boolean includeDeleted) {

        var dossiers = dossierManagementService.getAllDossiersForDossierTemplateId(dossierTemplateId, includeArchived, includeDeleted);
        return enhanceDossiersWithAttributeAndACLData(dossiers);
    }


    @PreAuthorize("hasAuthority('" + READ_DOSSIER + "')")
    public List<Dossier> getSoftDeletedDossiers() {

        var dossiers = dossierManagementService.getSoftDeletedDossiers();
        return enhanceDossiersWithAttributeAndACLData(dossiers);
    }


    @PreAuthorize("hasAuthority('" + READ_DOSSIER + "')")
    public List<Dossier> getArchivedDossiers() {

        var dossiers = dossierManagementService.getArchivedDossiers();
        return enhanceDossiersWithAttributeAndACLData(dossiers);
    }


    @PreAuthorize("hasAuthority('" + READ_DOSSIER + "')")
    public List<Dossier> getArchivedDossiersForDossierTemplate(@PathVariable(DOSSIER_TEMPLATE_ID_PARAM) String dossierTemplateId) {

        var dossiers = dossierManagementService.getArchivedDossiersForDossierTemplateId(dossierTemplateId);
        return enhanceDossiersWithAttributeAndACLData(dossiers);
    }


    @PreAuthorize("hasAuthority('" + ARCHIVE_DOSSIER + "')")
    @PreFilter("hasPermission(filterObject, 'Dossier', 'ACCESS_OBJECT')")
    public void archiveDossiers(@RequestBody Set<String> dossierIds) {

        for (String dossierId : dossierIds) {
            accessControlService.verifyUserIsDossierOwner(dossierId);
        }
        dossierManagementService.archiveDossiers(dossierIds);

        for (String dossierId : dossierIds) {
            auditPersistenceService.audit(AuditRequest.builder()
                                                  .userId(KeycloakSecurity.getUserId())
                                                  .objectId(dossierId)
                                                  .category(AuditCategory.DOSSIER.name())
                                                  .message("Dossier archived.")
                                                  .build());
            websocketService.sendDossierEvent(dossierId, DossierEventType.ARCHIVE);
        }
    }


    @PreAuthorize("hasAuthority('" + UNARCHIVE_DOSSIER + "')")
    @PreFilter("hasPermission(filterObject, 'Dossier', 'ACCESS_OBJECT')")
    public void unarchiveDossiers(@RequestBody Set<String> dossierIds) {

        dossierManagementService.unarchiveDossiers(dossierIds);

        for (String dossierId : dossierIds) {
            auditPersistenceService.audit(AuditRequest.builder()
                                                  .userId(KeycloakSecurity.getUserId())
                                                  .objectId(dossierId)
                                                  .category(AuditCategory.DOSSIER.name())
                                                  .message("Dossier restored from archive.")
                                                  .build());
            websocketService.sendDossierEvent(dossierId, DossierEventType.UNARCHIVE);
        }
    }


    @PreAuthorize("hasAuthority('" + DELETE_DOSSIER + "')")
    @PreFilter("hasPermission(filterObject, 'Dossier', 'ACCESS_OBJECT')")
    public void hardDeleteDossiers(@RequestParam(DOSSIER_ID_PARAM) Set<String> dossierIds) {

        var filteredDossierIds = filterDossierIdsByOwnedKeepUnowned(dossierIds);

        dossierManagementService.hardDeleteDossiers(filteredDossierIds);

        for (String dossierId : filteredDossierIds) {

            auditPersistenceService.audit(AuditRequest.builder()
                                                  .userId(KeycloakSecurity.getUserId())
                                                  .objectId(dossierId)
                                                  .category(AuditCategory.DOSSIER.name())
                                                  .message("Dossier permanently deleted.")
                                                  .build());
            websocketService.sendDossierEvent(dossierId, DossierEventType.HARD_DELETE);
        }
    }


    @PreAuthorize("hasAuthority('" + DELETE_DOSSIER + "')")
    @PreFilter("hasPermission(filterObject, 'Dossier', 'ACCESS_OBJECT')")
    public void undeleteDossiers(@RequestBody Set<String> dossierIds) {

        var filteredDossierIds = filterDossierIdsByOwnedKeepUnowned(dossierIds);

        dossierManagementService.undeleteDossiers(filteredDossierIds);

        for (String dossierId : filteredDossierIds) {
            auditPersistenceService.audit(AuditRequest.builder()
                                                  .userId(KeycloakSecurity.getUserId())
                                                  .objectId(dossierId)
                                                  .category(AuditCategory.DOSSIER.name())
                                                  .message("Dossier restored from trash.")
                                                  .build());
            websocketService.sendDossierEvent(dossierId, DossierEventType.UNDELETE);

        }
    }


    private Set<String> filterDossierIdsByOwnedKeepUnowned(Set<String> dossierIds) {

        return dossierIds.stream()
                .map(id -> dossierManagementService.getDossierById(id, true, true))
                .map(dossierACLService::enhanceDossierWithACLData)
                .filter(dossier -> dossier.getOwnerId() == null || dossier.getOwnerId().equals(KeycloakSecurity.getUserId()))
                .map(Dossier::getId)
                .collect(Collectors.toSet());
    }


    private DossierAttributes convertDossierAttributes(List<DossierAttributeEntity> dossierAttributeEntities) {

        Map<String, String> attributeIdToValue = new HashMap<>();
        for (DossierAttributeEntity dossierAttributeEntity : dossierAttributeEntities) {
            attributeIdToValue.put(dossierAttributeEntity.getId().getDossierAttributeConfigId(), dossierAttributeEntity.getValue());
        }
        return new DossierAttributes(attributeIdToValue);
    }

    private List<Dossier> enhanceDossiersWithAttributeAndACLData(List<Dossier> dossiers) {

            return enhanceDossiersWithAttributeAndACLData(dossiers, true);
    }

    private List<Dossier> enhanceDossiersWithAttributeAndACLData(List<Dossier> dossiers, boolean filter) {

        // filter first, only load attributes and ACL for viewable dossiers
        List<Dossier> filteredDossiers = filter ? filterByPermissionsService.onlyViewableDossiers(dossiers) : dossiers;

        // load all attributes at once
        var attributes = dossierAttributePersistenceService.getDossierAttributes(filteredDossiers.stream().map(Dossier::getId).collect(Collectors.toSet()));
        var attributesMap = new HashMap<String, List<DossierAttributeEntity>>();
        for (DossierAttributeEntity attribute : attributes) {
            attributesMap.computeIfAbsent(attribute.getId().getDossierId(), k -> new ArrayList<>()).add(attribute);
        }

        for (var dossier : filteredDossiers) {
            // set attributes
            dossier.setDossierAttributes(convertDossierAttributes(attributesMap.getOrDefault(dossier.getId(), new ArrayList<>())));
            // set ACL data
            dossierACLService.enhanceDossierWithACLData(dossier);
        }
        return filteredDossiers;
    }

}

