package com.iqser.red.service.persistence.management.v1.processor.service;

import static com.iqser.red.service.persistence.management.v1.processor.utils.TypeIdUtils.toTypeId;

import java.nio.charset.StandardCharsets;
import java.time.OffsetDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;

import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.hash.HashFunction;
import com.google.common.hash.Hashing;
import com.iqser.red.service.persistence.management.v1.processor.configuration.MessagingConfiguration;
import com.iqser.red.service.persistence.management.v1.processor.entity.annotations.AnnotationEntityId;
import com.iqser.red.service.persistence.management.v1.processor.entity.annotations.CommentEntity;
import com.iqser.red.service.persistence.management.v1.processor.entity.annotations.IdRemovalEntity;
import com.iqser.red.service.persistence.management.v1.processor.entity.annotations.ManualImageRecategorizationEntity;
import com.iqser.red.service.persistence.management.v1.processor.entity.annotations.ManualRedactionEntryEntity;
import com.iqser.red.service.persistence.management.v1.processor.entity.annotations.ManualResizeRedactionEntity;
import com.iqser.red.service.persistence.management.v1.processor.entity.dossier.DossierEntity;
import com.iqser.red.service.persistence.management.v1.processor.exception.BadRequestException;
import com.iqser.red.service.persistence.management.v1.processor.exception.ConflictException;
import com.iqser.red.service.persistence.management.v1.processor.exception.NotFoundException;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.DictionaryPersistenceService;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.DossierPersistenceService;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.DossierTemplatePersistenceService;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.FileStatusPersistenceService;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.annotations.AddRedactionPersistenceService;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.annotations.CommentPersistenceService;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.annotations.ForceRedactionPersistenceService;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.annotations.ImageRecategorizationPersistenceService;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.annotations.LegalBasisChangePersistenceService;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.annotations.RemoveRedactionPersistenceService;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.annotations.ResizeRedactionPersistenceService;
import com.iqser.red.service.persistence.service.v1.api.shared.model.AnalyzeRequest;
import com.iqser.red.service.persistence.service.v1.api.shared.model.MessageType;
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.AddRedactionRequest;
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.AnnotationStatus;
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.CommentRequest;
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.ForceRedactionRequest;
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.ImageRecategorizationRequest;
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.LegalBasisChangeRequest;
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.ManualAddResponse;
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.ManualRedactions;
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.RemoveRedactionRequest;
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.ResizeRedactionRequest;
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.IdRemoval;
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.dossier.file.ProcessingStatus;
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.dossier.file.WorkflowStatus;
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.type.DictionaryEntryType;
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.RedactionLog;
import com.iqser.red.service.persistence.service.v1.api.shared.model.redactionlog.RedactionLogEntry;
import com.knecon.fforesight.databasetenantcommons.providers.utils.MagicConverter;

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

@Slf4j
@Service
@RequiredArgsConstructor
public class ManualRedactionService {

    private final DossierPersistenceService dossierPersistenceService;
    private final AddRedactionPersistenceService addRedactionPersistenceService;
    private final RemoveRedactionPersistenceService removeRedactionPersistenceService;
    private final ForceRedactionPersistenceService forceRedactionPersistenceService;
    private final CommentPersistenceService commentPersistenceService;
    private final FileStatusPersistenceService fileStatusPersistenceService;
    private final DictionaryPersistenceService dictionaryPersistenceService;
    private final FileManagementStorageService fileManagementStorageService;
    private final ImageRecategorizationPersistenceService recategorizationPersistenceService;
    private final LegalBasisChangePersistenceService legalBasisChangePersistenceService;
    private final ResizeRedactionPersistenceService resizeRedactionPersistenceService;
    private final FileStatusService fileStatusService;
    private final ManualRedactionProviderService manualRedactionProviderService;
    private final AnalysisFlagsCalculationService analysisFlagsCalculationService;
    private final StopwordService stopwordService;
    private final RedactionLogService redactionLogService;
    private final DictionaryManagementService dictionaryManagementService;
    private final HashFunction hashFunction = Hashing.murmur3_128();


    @Transactional
    public List<ManualAddResponse> addAddRedaction(String dossierId, String fileId, List<AddRedactionRequest> addRedactionRequests) {

        var response = new ArrayList<ManualAddResponse>();

        dossierPersistenceService.getAndValidateDossier(dossierId);

        // validate add to dossier template dictionaries
        addRedactionRequests.forEach(request -> dictionaryManagementService.validateAddRemoveToDossierTemplateDictionary(request.getDossierTemplateTypeId(),
                request.isAddToDictionary(),
                request.isAddToAllDossiers()));

        boolean actionPerformed = false;
        for (var addRedactionRequest : addRedactionRequests) {
            if (addRedactionRequest.isAddToDictionary()) {
                validateDictionary(addRedactionRequest);
            }

            validatePositions(fileId, addRedactionRequest);
            String annotationId = hashFunction.hashString(fileId + addRedactionRequest, StandardCharsets.UTF_8).toString();
            addRedactionPersistenceService.insert(fileId, annotationId, addRedactionRequest);

            Long commentId = null;
            if (addRedactionRequest.getComment() != null) {
                commentId = addComment(fileId, annotationId, addRedactionRequest.getComment(), addRedactionRequest.getUser()).getId();
            }

            handleAddToDictionary(fileId,
                    annotationId,
                    addRedactionRequest.getDossierTemplateTypeId(),
                    addRedactionRequest.getValue(),
                    addRedactionRequest.getStatus(),
                    addRedactionRequest.isAddToDictionary(),
                    addRedactionRequest.isAddToAllDossiers(),
                    false,
                    dossierId,
                    addRedactionRequest.getDictionaryEntryType());

            response.add(ManualAddResponse.builder().annotationId(annotationId).commentId(commentId).build());
            actionPerformed = actionPerformed || addRedactionRequest.getStatus().equals(AnnotationStatus.APPROVED);
        }

        analysisFlagsCalculationService.calculateFlags(dossierId, fileId);

        if (actionPerformed) {
            reprocess(dossierId, fileId);
        }

        return response;
    }


    private void validateDictionary(AddRedactionRequest addRedactionRequest) {

        try {
            if (!addRedactionRequest.isForceAddToDictionary() && stopwordService.isStopword(addRedactionRequest.getValue())) {
                throw new ConflictException("The entry you are trying to add is a stopword");
            }
            dictionaryPersistenceService.getType(addRedactionRequest.getDossierTemplateTypeId());
        } catch (NotFoundException e) {
            throw new BadRequestException("Invalid type: " + addRedactionRequest.getDossierTemplateTypeId());
        }
    }


    private void validatePositions(String fileId, AddRedactionRequest addRedactionRequest) {

        var numberOfPages = fileStatusService.getStatus(fileId).getNumberOfPages();
        addRedactionRequest.getPositions().stream().filter(p -> p.getPage() > numberOfPages).findAny().ifPresent(p -> new BadRequestException("Invalid page found in the request"));
    }


    private CommentEntity addComment(String fileId, String annotationId, String comment, String user) {

        return commentPersistenceService.insert(CommentEntity.builder()
                .text(comment)
                .fileId(fileId)
                .annotationId(annotationId)
                .user(user)
                .date(OffsetDateTime.now().truncatedTo(ChronoUnit.MILLIS))
                .build());
    }


    private boolean handleAddToDictionary(String fileId,
                                          String annotationId,
                                          String dossierTemplateTypeId,
                                          String value,
                                          AnnotationStatus status,
                                          boolean addToDictionary,
                                          boolean addToAllDossiers,
                                          boolean revert,
                                          String dossierId,
                                          DictionaryEntryType dictionaryEntryType) {

        if (status == AnnotationStatus.APPROVED) {

            if (addToDictionary) {
                Set<String> typeIdsOfModifiedDictionaries = new HashSet<>();
                if (revert) {
                    var addRedactionToRevert = addRedactionPersistenceService.findAddRedaction(fileId, annotationId);
                    addRedactionToRevert.getTypeIdsOfModifiedDictionaries().forEach(typeId -> {
                        removeFromDictionary(typeId, value, dossierId, fileId, dictionaryEntryType);
                    });
                } else {
                    if (addToAllDossiers) {
                        var dictionaryEntriesToUnDelete = dictionaryManagementService.getAllEntriesInDossierTemplate(dossierTemplateTypeId, value);
                        dictionaryEntriesToUnDelete.forEach(entry -> {
                            typeIdsOfModifiedDictionaries.add(entry.getTypeId());
                            addToDictionary(entry.getTypeId(), value, dossierId, fileId, dictionaryEntryType);
                        });
                        addToDictionary(dossierTemplateTypeId, value, dossierId, fileId, dictionaryEntryType);
                        typeIdsOfModifiedDictionaries.add(dossierTemplateTypeId);
                    } else {
                        addToDictionary(dossierTemplateTypeId + ":" + dossierId, value, dossierId, fileId, dictionaryEntryType);
                        typeIdsOfModifiedDictionaries.add(dossierTemplateTypeId + ":" + dossierId);
                    }
                    addRedactionPersistenceService.updateStatus(fileId, annotationId, status, true, typeIdsOfModifiedDictionaries);
                }
                return true;
            }
            addRedactionPersistenceService.updateStatus(fileId, annotationId, status, false, null);
        }
        return false;
    }


    private void reprocess(String dossierId, String fileId) {

        fileStatusService.setStatusReprocessForManual(dossierId, fileId, true);
    }


    public ManualRedactionEntryEntity getAddRedaction(String fileId, String annotationId) {

        return addRedactionPersistenceService.findAddRedaction(fileId, annotationId);
    }


    private void removeFromDictionary(String typeId, String value, String dossierId, String fileId, DictionaryEntryType dictionaryEntryType) {

        try {
            log.debug("Deleting entries to {} for {} / {}", typeId, dossierId, fileId);
            dictionaryManagementService.deleteEntries(typeId, List.of(value), dictionaryEntryType != null ? dictionaryEntryType : DictionaryEntryType.ENTRY);
        } catch (FeignException e) {
            throw new BadRequestException(e.getMessage());
        }
    }


    private void addToDictionary(String typeId, String value, String dossierId, String fileId, DictionaryEntryType dictionaryEntryType) {

        try {
            log.debug("Adding entry: {} to {} for {} / {}", value, typeId, dossierId, fileId);
            dictionaryManagementService.addEntries(typeId, List.of(value), false, false, dictionaryEntryType != null ? dictionaryEntryType : DictionaryEntryType.ENTRY);
        } catch (Exception e) {
            throw new BadRequestException(e.getMessage());
        }
    }


    public List<ManualAddResponse> addRemoveRedaction(String dossierId, String fileId, List<RemoveRedactionRequest> removeRedactionRequests) {

        var response = new ArrayList<ManualAddResponse>();
        var dossier = dossierPersistenceService.getAndValidateDossier(dossierId);
        RedactionLog redactionLog = fileManagementStorageService.getRedactionLog(dossier.getId(), fileId);

        var requiresReAnalysis = false;
        var manualRedactions = manualRedactionProviderService.getManualRedactions(fileId);

        //validate removing from dossier template dictionary
        removeRedactionRequests.forEach(request -> {
            if (request.isRemoveFromDictionary()) {
                RedactionLogEntry redactionLogEntry = getRedactionLogEntry(redactionLog, request.getAnnotationId());
                var dossierTemplateTypeId = toTypeId(redactionLogEntry.getType(), dossier.getDossierTemplateId());
                dictionaryManagementService.validateAddRemoveToDossierTemplateDictionary(dossierTemplateTypeId,
                        request.isRemoveFromDictionary(),
                        request.isRemoveFromAllDossiers());
            }
        });

        for (var removeRedactionRequest : removeRedactionRequests) {

            if (manualAddRedactionsContains(manualRedactions, removeRedactionRequest.getAnnotationId()) && AnnotationStatus.APPROVED.equals(removeRedactionRequest.getStatus())) {
                log.info("hard delete ManualRedactions for file {} and annotation {}", fileId, removeRedactionRequest.getAnnotationId());
                manualRedactionProviderService.hardDeleteManualRedactions(fileId, removeRedactionRequest.getAnnotationId());
            } else {

                log.info("add removeRedaction for file {} and annotation {}", fileId, removeRedactionRequest.getAnnotationId());
                var idRemoval = MagicConverter.convert(removeRedactionPersistenceService.insert(fileId, removeRedactionRequest), IdRemoval.class);

                Long commentId = null;
                if (removeRedactionRequest.getComment() != null) {
                    commentId = addComment(fileId, removeRedactionRequest.getAnnotationId(), removeRedactionRequest.getComment(), removeRedactionRequest.getUser()).getId();
                }
                if (!removeRedactionRequest.isRemoveFromDictionary() && AnnotationStatus.APPROVED.equals(removeRedactionRequest.getStatus())) {
                    boolean matchingEntryFound;
                    try {
                        getRedactionLogEntry(redactionLog, removeRedactionRequest.getAnnotationId());
                        matchingEntryFound = true;
                    } catch (NotFoundException e) {
                        matchingEntryFound = false;
                    }
                    requiresReAnalysis = requiresReAnalysis || matchingEntryFound;
                    if (!matchingEntryFound && idRemoval.isApproved()) {
                        removeRedactionPersistenceService.markAsProcessed(idRemoval);
                    }
                }

                var removedFromDictionary = handleRemoveFromDictionary(redactionLog,
                        dossier,
                        fileId,
                        removeRedactionRequest.getAnnotationId(),
                        removeRedactionRequest.getStatus(),
                        removeRedactionRequest.isRemoveFromDictionary(),
                        removeRedactionRequest.isRemoveFromAllDossiers(),
                        false);

                if (!removedFromDictionary && idRemoval.isApproved()) {
                    removeRedactionPersistenceService.markAsProcessed(idRemoval);
                }

                requiresReAnalysis = requiresReAnalysis || removedFromDictionary;

                response.add(ManualAddResponse.builder().annotationId(removeRedactionRequest.getAnnotationId()).commentId(commentId).build());
            }
        }

        if (requiresReAnalysis) {
            reprocess(dossierId, fileId);
        }

        analysisFlagsCalculationService.calculateFlags(dossierId, fileId);

        return response;
    }


    private boolean manualAddRedactionsContains(ManualRedactions manualRedactions, String annotationId) {

        return manualRedactions.getEntriesToAdd().stream().anyMatch(m -> annotationId.equals(m.getAnnotationId()));
    }


    private boolean handleRemoveFromDictionary(RedactionLog redactionLog,
                                               DossierEntity dossier,
                                               String fileId,
                                               String annotationId,
                                               AnnotationStatus status,
                                               boolean removeFromDictionary,
                                               boolean removeFromAllDossiers,
                                               boolean revert) {

        if (status == AnnotationStatus.APPROVED) {

            Set<String> typeIdsOfModifiedDictionaries = new HashSet<>();

            if (removeFromDictionary) {

                RedactionLogEntry redactionLogEntry = getRedactionLogEntry(redactionLog, annotationId);

                if (revert) {
                    var idRemovalEntity = removeRedactionPersistenceService.findRemoveRedaction(fileId, annotationId);
                    idRemovalEntity.getTypeIdsOfModifiedDictionaries().forEach(changedTypeId -> {
                        addToDictionary(changedTypeId, redactionLogEntry.getValue(), dossier.getId(), fileId, DictionaryEntryType.ENTRY);
                    });
                } else {
                    if (removeFromAllDossiers) {
                        var dictionaryEntriesToRemove = dictionaryManagementService.getAllEntriesInDossierTemplate(toTypeId(redactionLogEntry.getType(),
                                dossier.getDossierTemplateId()), redactionLogEntry.getValue());
                        dictionaryEntriesToRemove.forEach(entry -> {
                            typeIdsOfModifiedDictionaries.add(entry.getTypeId());
                            removeFromDictionary(entry.getTypeId(), entry.getValue(), dossier.getId(), fileId, DictionaryEntryType.ENTRY);
                        });
                    } else {
                        typeIdsOfModifiedDictionaries.add(toTypeId(redactionLogEntry.getType(), dossier.getDossierTemplateId(), dossier.getId()));
                        removeFromDictionary(toTypeId(redactionLogEntry.getType(), dossier.getDossierTemplateId(), dossier.getId()),
                                redactionLogEntry.getValue(),
                                dossier.getId(),
                                fileId,
                                DictionaryEntryType.ENTRY);
                    }

                    // This is needed to remove resizeRedactions with addToDictionary.
                    removeResizeRedactionsWithAddToDictionary(dossier.getDossierTemplateId(), redactionLogEntry.getValue());
                }

                removeRedactionPersistenceService.updateStatus(fileId, annotationId, status, true, typeIdsOfModifiedDictionaries);
                return true;
            }
            removeRedactionPersistenceService.updateStatus(fileId, annotationId, status, false, typeIdsOfModifiedDictionaries);
        }
        return false;
    }


    private RedactionLogEntry getRedactionLogEntry(RedactionLog redactionLog, String annotationId) {

        Optional<RedactionLogEntry> redactionLogEntryOptional = redactionLog.getRedactionLogEntry().stream().filter(entry -> entry.getId().equals(annotationId)).findFirst();

        if (redactionLogEntryOptional.isEmpty()) {
            throw new NotFoundException("Annotation does not exist in redaction log.");
        }

        var redactionLogEntry = redactionLogEntryOptional.get();
        return redactionLogEntry;
    }


    private void removeResizeRedactionsWithAddToDictionary(String dossierTemplateId, String redactionLogEntryValue) {

        var resizeRedactionsWithSameValue = resizeRedactionPersistenceService.findByAnnotationStatusAndValue(AnnotationStatus.APPROVED, redactionLogEntryValue);
        resizeRedactionsWithSameValue.forEach(resizeRedaction -> {
            var file = fileStatusPersistenceService.getStatus(resizeRedaction.getId().getFileId());
            var dossierForResizeRedaction = dossierPersistenceService.findByDossierId(file.getDossierId());
            if (!file.getWorkflowStatus().equals(WorkflowStatus.APPROVED) && dossierTemplateId.equals(dossierForResizeRedaction.getDossierTemplateId())) {
                resizeRedactionPersistenceService.hardDelete(resizeRedaction.getId().getFileId(), resizeRedaction.getId().getAnnotationId());
            }
        });
    }


    @Transactional
    public List<ManualAddResponse> addForceRedaction(String dossierId, String fileId, List<ForceRedactionRequest> forceRedactionRequests) {

        var response = new ArrayList<ManualAddResponse>();
        dossierPersistenceService.getAndValidateDossier(dossierId);
        var actionPerformed = false;

        for (var forceRedactionRequest : forceRedactionRequests) {
            forceRedactionPersistenceService.insert(fileId, forceRedactionRequest);

            Long commentId = null;
            if (forceRedactionRequest.getComment() != null) {

                commentId = addComment(fileId, forceRedactionRequest.getAnnotationId(), forceRedactionRequest.getComment(), forceRedactionRequest.getUser()).getId();
            }
            actionPerformed = actionPerformed || forceRedactionRequest.getStatus().equals(AnnotationStatus.APPROVED);
            response.add(ManualAddResponse.builder().annotationId(forceRedactionRequest.getAnnotationId()).commentId(commentId).build());
        }

        if (actionPerformed) {
            reprocess(dossierId, fileId);
        }

        analysisFlagsCalculationService.calculateFlags(dossierId, fileId);

        return response;
    }


    @Transactional
    public List<ManualAddResponse> addLegalBasisChange(String dossierId, String fileId, List<LegalBasisChangeRequest> legalBasisChangeRequests) {

        var response = new ArrayList<ManualAddResponse>();
        dossierPersistenceService.getAndValidateDossier(dossierId);

        for (var legalBasisChangeRequest : legalBasisChangeRequests) {
            legalBasisChangePersistenceService.insert(fileId, legalBasisChangeRequest);

            Long commentId = null;
            if (legalBasisChangeRequest.getComment() != null) {

                commentId = addComment(fileId, legalBasisChangeRequest.getAnnotationId(), legalBasisChangeRequest.getComment(), legalBasisChangeRequest.getUser()).getId();
            }

            response.add(ManualAddResponse.builder().annotationId(legalBasisChangeRequest.getAnnotationId()).commentId(commentId).build());
        }
        analysisFlagsCalculationService.calculateFlags(dossierId, fileId);

        return response;
    }


    @Transactional
    public List<ManualAddResponse> addImageRecategorization(String dossierId, String fileId, List<ImageRecategorizationRequest> imageRecategorizationRequests) {

        var response = new ArrayList<ManualAddResponse>();
        var actionPerformed = false;
        dossierPersistenceService.getAndValidateDossier(dossierId);

        for (var imageRecategorizationRequest : imageRecategorizationRequests) {
            recategorizationPersistenceService.insert(fileId, imageRecategorizationRequest);

            Long commentId = null;
            if (imageRecategorizationRequest.getComment() != null) {

                commentId = addComment(fileId,
                        imageRecategorizationRequest.getAnnotationId(),
                        imageRecategorizationRequest.getComment(),
                        imageRecategorizationRequest.getUser()).getId();
            }

            actionPerformed = actionPerformed || imageRecategorizationRequest.getStatus().equals(AnnotationStatus.APPROVED);

            response.add(ManualAddResponse.builder().annotationId(imageRecategorizationRequest.getAnnotationId()).commentId(commentId).build());
        }
        if (actionPerformed) {
            reprocess(dossierId, fileId);
        }

        analysisFlagsCalculationService.calculateFlags(dossierId, fileId);

        return response;
    }


    public CommentEntity addComment(String fileId, String annotationId, CommentRequest commentRequest) {

        var createdComment = addComment(fileId, annotationId, commentRequest.getText(), commentRequest.getUser());

        fileStatusPersistenceService.updateHasComments(fileId, true);

        return createdComment;
    }


    @Transactional
    public void deleteAddRedaction(String dossierId, String fileId, List<String> annotationIds) {

        var dossier = dossierPersistenceService.getAndValidateDossier(dossierId);
        var actionPerformed = false;

        for (var annotationId : annotationIds) {
            var addRedaction = getAddRedaction(fileId, annotationId);
            actionPerformed = actionPerformed || handleAddToDictionary(fileId,
                    annotationId,
                    addRedaction.getTypeId(),
                    addRedaction.getValue(),
                    addRedaction.getStatus(),
                    addRedaction.isAddToDictionary(),
                    addRedaction.isAddToAllDossiers(),
                    true,
                    dossier.getId(),
                    addRedaction.getDictionaryEntryType());
            addRedactionPersistenceService.softDelete(fileId, annotationId, OffsetDateTime.now());
        }

        if (actionPerformed) {
            reprocess(dossierId, fileId);
        }

        analysisFlagsCalculationService.calculateFlags(dossierId, fileId);
    }


    @Transactional
    public void deleteRemoveRedaction(String dossierId, String fileId, List<String> annotationIds) {

        var dossier = dossierPersistenceService.getAndValidateDossier(dossierId);
        var actionPerformed = false;
        RedactionLog redactionLog = redactionLogService.getRedactionLog(dossierId, fileId, true, true);

        for (var annotationId : annotationIds) {

            var removeRedaction = getRemoveRedaction(fileId, annotationId);

            var removedFromDictionary = handleRemoveFromDictionary(redactionLog,
                    dossier,
                    fileId,
                    annotationId,
                    removeRedaction.getStatus(),
                    removeRedaction.isRemoveFromDictionary(),
                    removeRedaction.isRemoveFromAllDossiers(),
                    true);

            actionPerformed = actionPerformed || removedFromDictionary;
            removeRedactionPersistenceService.softDelete(fileId, annotationId, OffsetDateTime.now());
        }

        if (actionPerformed) {
            reprocess(dossierId, fileId);
        }

        analysisFlagsCalculationService.calculateFlags(dossierId, fileId);
    }


    private IdRemovalEntity getRemoveRedaction(String fileId, String annotationId) {

        return removeRedactionPersistenceService.findRemoveRedaction(fileId, annotationId);
    }


    @Transactional
    public void deleteForceRedaction(String dossierId, String fileId, List<String> annotationIds) {

        var now = OffsetDateTime.now();

        for (var annotationId : annotationIds) {
            forceRedactionPersistenceService.softDelete(fileId, annotationId, now);
        }

        analysisFlagsCalculationService.calculateFlags(dossierId, fileId);
    }


    @Transactional
    public void deleteLegalBasisChange(String dossierId, String fileId, List<String> annotationIds) {

        for (var annotationId : annotationIds) {
            legalBasisChangePersistenceService.softDelete(fileId, annotationId, OffsetDateTime.now());
        }

        analysisFlagsCalculationService.calculateFlags(dossierId, fileId);
    }


    @Transactional
    public void deleteImageRecategorization(String dossierId, String fileId, List<String> annotationIds) {

        var actionPerformed = false;
        for (var annotationId : annotationIds) {
            var imageRecategorization = getImageRecategorization(fileId, annotationId);

            recategorizationPersistenceService.softDelete(fileId, annotationId, OffsetDateTime.now());

            actionPerformed = actionPerformed || !AnnotationStatus.REQUESTED.equals(imageRecategorization.getStatus());
        }

        if (actionPerformed) {
            reprocess(dossierId, fileId);
        }
        analysisFlagsCalculationService.calculateFlags(dossierId, fileId);
    }


    private ManualImageRecategorizationEntity getImageRecategorization(String fileId, String annotationId) {

        return recategorizationPersistenceService.findRecategorization(fileId, annotationId);
    }


    @Transactional
    public void deleteComment(String fileId, List<Long> commentIds) {

        for (var commentId : commentIds) {
            commentPersistenceService.softDelete(commentId, OffsetDateTime.now());
        }
        // update indicator
        fileStatusPersistenceService.updateHasComments(fileId, commentPersistenceService.fileHasComments(fileId));
    }


    @Transactional
    public void deleteResizeRedaction(String dossierId, String fileId, List<String> annotationIds) {

        OffsetDateTime now = OffsetDateTime.now();
        for (var annotationId : annotationIds) {
            resizeRedactionPersistenceService.softDelete(fileId, annotationId, now);
        }
        analysisFlagsCalculationService.calculateFlags(dossierId, fileId);
    }


    @Transactional
    public List<ManualAddResponse> addResizeRedaction(String dossierId, String fileId, List<ResizeRedactionRequest> resizeRedactionRequests) {

        var response = new ArrayList<ManualAddResponse>();

        RedactionLog redactionLog = redactionLogService.getRedactionLog(dossierId, fileId, true, true);

        for (ResizeRedactionRequest resizeRedactionRequest : resizeRedactionRequests) {

            var resizeRedaction = resizeRedactionPersistenceService.insert(fileId, resizeRedactionRequest);

            if (resizeRedactionRequest.getComment() != null) {
                var commentId = addComment(fileId, resizeRedactionRequest.getAnnotationId(), resizeRedactionRequest.getComment(), resizeRedactionRequest.getUser()).getId();
                response.add(ManualAddResponse.builder().annotationId(resizeRedactionRequest.getAnnotationId()).commentId(commentId).build());
            }

            updateDictionaryForResizeRedactions(dossierId, fileId, resizeRedaction, redactionLog);
        }

        if (resizeRedactionRequests.stream().anyMatch(resize -> AnnotationStatus.APPROVED.equals(resize.getStatus()))) {
            reprocess(dossierId, fileId);
        }

        analysisFlagsCalculationService.calculateFlags(dossierId, fileId);

        return response;
    }


    private void updateDictionaryForResizeRedactions(String dossierId, String fileId, ManualResizeRedactionEntity resizeRedaction, RedactionLog redactionLog) {

        RedactionLogEntry redactionLogEntry = getRedactionLogEntry(redactionLog, resizeRedaction.getId().getAnnotationId());

        if (resizeRedaction.getUpdateDictionary() != null && resizeRedaction.getUpdateDictionary() && resizeRedaction.getStatus()
                .equals(AnnotationStatus.APPROVED) && (redactionLogEntry.isDictionaryEntry() || redactionLogEntry.isDossierDictionaryEntry())) {

            var dossier = dossierPersistenceService.findByDossierId(dossierId);

            var typeId = buildTypeId(redactionLogEntry, resizeRedaction, dossier);
            var newValue = resizeRedaction.getValue();
            var oldValue = redactionLogEntry.getValue();
            var dictionaryEntryType = getDictionaryEntryType(redactionLogEntry);

            boolean isShrinking = oldValue != null && oldValue.length() > newValue.length();

            Set<String> typeIdsOfModifiedDictionaries = new HashSet<>();

            if (isShrinking) {
                log.info("Remove old value '{}' from dictionary", oldValue);
                removeFromDictionary(typeId, oldValue, dossierId, fileId, dictionaryEntryType);
                typeIdsOfModifiedDictionaries.add(typeId);

                if (resizeRedaction.isAddToAllDossiers() && redactionLogEntry.isDictionaryEntry()) {
                    String dossierTemplateId = dossier.getDossierTemplateId();
                    var dossiersOfThisDossierTemplate = dossierPersistenceService.findAllDossiersForDossierTemplateId(dossierTemplateId);
                    var type = redactionLogEntry.getType();
                    dossiersOfThisDossierTemplate.forEach(dossierEntity -> {
                        var typeIdOfDossierEntity = toTypeId(type, dossierTemplateId, dossierEntity.getId());
                        removeFromDictionary(typeIdOfDossierEntity, oldValue, dossierId, fileId, dictionaryEntryType);
                        typeIdsOfModifiedDictionaries.add(typeIdOfDossierEntity);
                    });
                }

            }

            log.info("Add new value '{}' to dictionary", newValue);
            addToDictionary(typeId, newValue, dossierId, fileId, dictionaryEntryType);
            typeIdsOfModifiedDictionaries.add(typeId);

            resizeRedactionPersistenceService.updateStatus(resizeRedaction.getId().getFileId(),
                    resizeRedaction.getId().getAnnotationId(),
                    resizeRedaction.getStatus(),
                    typeIdsOfModifiedDictionaries);
        }
    }


    private String buildTypeId(RedactionLogEntry redactionLogEntry, ManualResizeRedactionEntity resizeRedaction, DossierEntity dossier) {

        if (resizeRedaction.isAddToAllDossiers()) {
            return toTypeId(redactionLogEntry.getType(), dossier.getDossierTemplateId());
        } else {
            return toTypeId(redactionLogEntry.getType(), dossier.getDossierTemplateId(), dossier.getId());
        }

    }


    private DictionaryEntryType getDictionaryEntryType(RedactionLogEntry redactionLogEntry) {

        if (redactionLogEntry.isRecommendation() && redactionLogEntry.isFalsePositive()) {
            return DictionaryEntryType.FALSE_RECOMMENDATION;
        } else if (redactionLogEntry.isFalsePositive()) {
            return DictionaryEntryType.FALSE_POSITIVE;
        } else {
            return DictionaryEntryType.ENTRY;
        }
    }


    public ManualRedactions getManualRedactions(String fileId) {

        return manualRedactionProviderService.getManualRedactions(fileId);
    }


    @Transactional
    public void updateProcessedDate(String fileId, ManualRedactions manualRedactions) {

        if (manualRedactions != null) {

            if (manualRedactions.getEntriesToAdd() != null) {
                manualRedactions.getEntriesToAdd().forEach(e -> {
                    if (!e.getStatus().equals(AnnotationStatus.REQUESTED) && e.getProcessedDate() == null) {
                        addRedactionPersistenceService.markAsProcessed(e);
                    }
                });
            }
            if (manualRedactions.getIdsToRemove() != null) {
                manualRedactions.getIdsToRemove().forEach(e -> {
                    if (!e.getStatus().equals(AnnotationStatus.REQUESTED) && e.getProcessedDate() == null) {
                        removeRedactionPersistenceService.markAsProcessed(e);
                    }
                });
            }
            if (manualRedactions.getForceRedactions() != null) {
                manualRedactions.getForceRedactions().forEach(e -> {
                    if (!e.getStatus().equals(AnnotationStatus.REQUESTED) && e.getProcessedDate() == null) {
                        forceRedactionPersistenceService.markAsProcessed(e.getAnnotationId(), e.getFileId());
                    }
                });
            }
            if (manualRedactions.getImageRecategorization() != null) {
                manualRedactions.getImageRecategorization().forEach(e -> {
                    if (!e.getStatus().equals(AnnotationStatus.REQUESTED) && e.getProcessedDate() == null) {
                        recategorizationPersistenceService.markAsProcessed(e.getAnnotationId(), e.getFileId());
                    }
                });
            }
            if (manualRedactions.getResizeRedactions() != null) {
                manualRedactions.getResizeRedactions().forEach(e -> {
                    if (!e.getStatus().equals(AnnotationStatus.REQUESTED) && e.getProcessedDate() == null) {
                        resizeRedactionPersistenceService.markAsProcessed(e.getAnnotationId(), e.getFileId());
                    }
                });
            }
        }
    }

}
