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.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.assertj.core.util.Strings;
import org.springframework.stereotype.Service;

import com.iqser.red.service.persistence.management.v1.processor.entity.configuration.TypeEntity;
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.NotFoundException;
import com.iqser.red.service.persistence.management.v1.processor.service.manualredactions.PendingEntryFactory;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.DictionaryPersistenceService;
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.Change;
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.ChangeType;
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.Engine;
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.EntityLog;
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.EntityLogEntry;
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.EntryState;
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.EntryType;
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.ManualChange;
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.ManualRedactionType;
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.Position;
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.entitylog.PropertyChange;
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.ChangeFactory;
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.ManualChangeFactory;
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.Rectangle;
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.BaseAnnotation;
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.annotations.entitymapped.ManualForceRedaction;
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.ManualLegalBasisChange;
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.ManualRecategorization;
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.ManualRedactionEntry;
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.ManualResizeRedaction;
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.utils.ChangeUtils;
import com.iqser.red.service.persistence.service.v1.api.shared.mongo.service.EntityLogMongoService;

import io.micrometer.observation.annotation.Observed;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.experimental.FieldDefaults;
import lombok.extern.slf4j.Slf4j;

@Service
@Slf4j
@RequiredArgsConstructor
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
public class EntityLogMergeService {

    DictionaryPersistenceService dictionaryPersistenceService;
    PendingEntryFactory pendingEntryFactory;
    EntityLogMongoService entityLogMongoService;


    @Observed(name = "EntityLogMergeService", contextualName = "merge-entity-log")
    public EntityLog mergeEntityLog(ManualRedactions unprocessedManualRedactions, EntityLog entityLog, DossierEntity dossier) {

        final int analysisNumber = entityLog.getAnalysisNumber();

        Map<String, List<BaseAnnotation>> allManualChanges = groupManualChanges(unprocessedManualRedactions);

        List<EntityLogEntry> mergedEntityLogEntries = merge(unprocessedManualRedactions, entityLog.getEntityLogEntry(), dossier, analysisNumber, allManualChanges);

        entityLog.setEntityLogEntry(mergedEntityLogEntries);
        return entityLog;
    }


    @Observed(name = "EntityLogMergeService", contextualName = "merge-entity-log-entries")
    public List<EntityLogEntry> mergeEntityLogEntries(ManualRedactions unprocessedManualRedactions, List<EntityLogEntry> entityLogEntries, DossierEntity dossier, String fileId) {

        final int analysisNumber = entityLogMongoService.findLatestAnalysisNumber(dossier.getId(), fileId)
                .orElseThrow(() -> new BadRequestException("Can't load analysis number"));

        Map<String, List<BaseAnnotation>> allManualChanges = groupManualChanges(unprocessedManualRedactions);

        return merge(unprocessedManualRedactions, entityLogEntries, dossier, analysisNumber, allManualChanges);
    }


    private List<EntityLogEntry> merge(ManualRedactions unprocessedManualRedactions,
                                       List<EntityLogEntry> entityLogEntries,
                                       DossierEntity dossier,
                                       int analysisNumber,
                                       Map<String, List<BaseAnnotation>> allManualChanges) {

        List<EntityLogEntry> mergedEntityLogEntries = new LinkedList<>(entityLogEntries);
        Map<String, EntityLogEntry> addedLocalManualEntries = buildUnprocessedLocalManualRedactions(unprocessedManualRedactions, entityLogEntries, dossier, analysisNumber)//
                .collect(Collectors.toMap(EntityLogEntry::getId, Function.identity()));
        mergedEntityLogEntries.addAll(addedLocalManualEntries.values());
        buildPendingDictionaryChanges(unprocessedManualRedactions).forEach(mergedEntityLogEntries::add);

        Map<String, String> trackLocalChangesBasedOnDictEntriesMap = unprocessedManualRedactions.getEntriesToAdd()
                .stream()
                .filter(ManualRedactionEntry::isLocal)
                .filter(entry -> entry.getSourceId() != null && !entry.getSourceId().isEmpty())
                .collect(Collectors.toMap(ManualRedactionEntry::getAnnotationId, ManualRedactionEntry::getSourceId));
        processEntityLogEntries(dossier, mergedEntityLogEntries, addedLocalManualEntries, analysisNumber, allManualChanges, trackLocalChangesBasedOnDictEntriesMap);

        adjustEntityLogEntriesAfterLocalChangesBasedOnDict(entityLogEntries, trackLocalChangesBasedOnDictEntriesMap, analysisNumber);
        return mergedEntityLogEntries;
    }


    private void adjustEntityLogEntriesAfterLocalChangesBasedOnDict(List<EntityLogEntry> entityLogEntries,
                                                                    Map<String, String> trackLocalChangesBasedOnDictEntriesMap,
                                                                    int analysisNumber) {

        Set<String> dictEntryIdsToUpdate = new HashSet<>(trackLocalChangesBasedOnDictEntriesMap.values());
        entityLogEntries.stream()
                .filter(entityLogEntry -> dictEntryIdsToUpdate.contains(entityLogEntry.getId()))
                .forEach(entityLogEntry -> {
                    List<Change> changes = new ArrayList<>();
                    changes.add(ChangeFactory.toChange(ChangeType.REMOVED,
                                                       OffsetDateTime.now(),
                                                       analysisNumber,
                                                       PropertyChange.builder()
                                                               .property("state")
                                                               .oldValue(entityLogEntry.getState().name())
                                                               .newValue(EntryState.REMOVED.name())
                                                               .build()));
                    addChanges(entityLogEntry, changes);
                    entityLogEntry.setState(EntryState.REMOVED);
                });
    }


    private void processEntityLogEntries(DossierEntity dossier,
                                         List<EntityLogEntry> entityLogEntries,
                                         Map<String, EntityLogEntry> addedLocalManualEntries,
                                         int analysisNumber,
                                         Map<String, List<BaseAnnotation>> allManualChanges,
                                         Map<String, String> trackLocalChangesBasedOnDictEntriesMap) {

        int numberOfAddedPendingEntries = 0; // since list is dynamically growing we need to keep track of the number of added pending entries to ignore them in the loop
        for (int i = 0; i + numberOfAddedPendingEntries < entityLogEntries.size(); i++) {
            EntityLogEntry entityLogEntry = entityLogEntries.get(i + numberOfAddedPendingEntries);

            if (isDuplicatedByAny(entityLogEntry, addedLocalManualEntries)) {
                mergeOriginalAndLocallyAddedEntries(addedLocalManualEntries, entityLogEntry, analysisNumber);
                continue;
            }

            if (allManualChanges.containsKey(entityLogEntry.getId())) {
                List<EntityLogEntry> pendingImageRecategorizations = mergeLocalManualChangesAndReturnNonMergeableAsPending(dossier,
                                                                                                                           allManualChanges,
                                                                                                                           entityLogEntry,
                                                                                                                           analysisNumber,
                                                                                                                           trackLocalChangesBasedOnDictEntriesMap);
                List<EntityLogEntry> pendingDictionaryEntries = buildPendingDictionaryEntries(allManualChanges, entityLogEntry);

                // insert pending entries directly after the associated entry to enable performant linking in UI
                for (EntityLogEntry pendingDictionaryEntry : concatLists(pendingDictionaryEntries, pendingImageRecategorizations)) {
                    numberOfAddedPendingEntries++;
                    entityLogEntries.add(i + numberOfAddedPendingEntries, pendingDictionaryEntry);
                }
            }
        }
    }


    private Map<String, List<BaseAnnotation>> groupManualChanges(ManualRedactions unprocessedManualRedactions) {

        return unprocessedManualRedactions.buildAll()
                .stream()
                .collect(Collectors.groupingBy(BaseAnnotation::getAnnotationId));
    }


    private static void mergeOriginalAndLocallyAddedEntries(Map<String, EntityLogEntry> addedLocalManualEntries, EntityLogEntry entityLogEntry, int analysisNumber) {

        EntityLogEntry duplicateManualEntry = addedLocalManualEntries.get(entityLogEntry.getId());
        duplicateManualEntry.getEngines().addAll(entityLogEntry.getEngines());

        // Mark existing entry REMOVED, will be replaced by the manual one.
        entityLogEntry.setState(EntryState.REMOVED);
        entityLogEntry.setReason("Removed due to duplicate locally added manual entity.");
        entityLogEntry.getChanges().add(Change.builder().type(ChangeType.REMOVED).dateTime(OffsetDateTime.now()).analysisNumber(analysisNumber).build());
    }


    private static List<EntityLogEntry> concatLists(List<EntityLogEntry> pendingDictionaryEntries, List<EntityLogEntry> pendingImageRecategorizations) {

        return Stream.of(pendingDictionaryEntries, pendingImageRecategorizations)
                .flatMap(Collection::stream)
                .toList();
    }


    private Stream<EntityLogEntry> buildPendingDictionaryChanges(ManualRedactions unprocessedManualRedactions) {

        return unprocessedManualRedactions.getEntriesToAdd()
                .stream()
                .filter(manualAdd -> !manualAdd.isLocal())
                .map(pendingEntryFactory::buildAddToDictionaryEntry);
    }


    private Stream<EntityLogEntry> buildUnprocessedLocalManualRedactions(ManualRedactions unprocessedManualRedactions,
                                                                         List<EntityLogEntry> entityLogEntries,
                                                                         DossierEntity dossier,
                                                                         int analysisNumber) {

        return unprocessedManualRedactions.getEntriesToAdd()
                .stream()
                .filter(ManualRedactionEntry::isLocal)
                .map(manualRedactionEntry -> mergeManualRedactionEntries(manualRedactionEntry, entityLogEntries, dossier, analysisNumber))
                .filter(Optional::isPresent)
                .map(Optional::get);
    }


    private List<EntityLogEntry> buildPendingDictionaryEntries(Map<String, List<BaseAnnotation>> allManualChanges, EntityLogEntry entityLogEntry) {

        return allManualChanges.getOrDefault(entityLogEntry.getId(), Collections.emptyList())
                .stream()
                .filter(baseAnnotation -> !baseAnnotation.isLocal())
                .sorted(Comparator.comparing(BaseAnnotation::getRequestDate))
                .map(dictionaryChange -> {
                    if (dictionaryChange instanceof ManualRedactionEntry) {
                        return null; // pending dictionaries are inserted before the manual changes loop
                    } else if (dictionaryChange instanceof IdRemoval idRemoval) {
                        return pendingEntryFactory.buildRemoveFromDictionary(idRemoval, entityLogEntry);
                    } else if (dictionaryChange instanceof ManualResizeRedaction manualResizeRedaction) {
                        return pendingEntryFactory.buildResizeWithDictionary(manualResizeRedaction, entityLogEntry);
                    } else if (dictionaryChange instanceof ManualRecategorization manualRecategorization) {
                        return pendingEntryFactory.buildRecategorizeWithDictionary(manualRecategorization, entityLogEntry);
                    } else {
                        throw new IllegalArgumentException(String.format("Manual change of type %s has no defined dictionary action!", dictionaryChange.getClass()));
                    }
                })
                .filter(Objects::nonNull)
                .toList();
    }


    private List<EntityLogEntry> mergeLocalManualChangesAndReturnNonMergeableAsPending(DossierEntity dossier,
                                                                                       Map<String, List<BaseAnnotation>> allManualChanges,
                                                                                       EntityLogEntry entityLogEntry,
                                                                                       int analysisNumber,
                                                                                       Map<String, String> trackLocalChangesBasedOnDictEntriesMap) {

        return allManualChanges.getOrDefault(entityLogEntry.getId(), Collections.emptyList())
                .stream()
                .filter(BaseAnnotation::isLocal)
                .sorted(Comparator.comparing(BaseAnnotation::getRequestDate))
                .map(localChange -> {
                    if (localChange instanceof IdRemoval idRemoval) {
                        mergeIdToRemove(idRemoval, entityLogEntry, analysisNumber);
                        if (trackLocalChangesBasedOnDictEntriesMap.containsKey(idRemoval.getAnnotationId())) {
                            trackLocalChangesBasedOnDictEntriesMap.remove(idRemoval.getAnnotationId());
                        }
                        return null;
                    } else if (localChange instanceof ManualResizeRedaction manualResizeRedaction) {
                        mergeResizeRedaction(manualResizeRedaction, entityLogEntry, analysisNumber);

                        return null;
                    } else if (localChange instanceof ManualLegalBasisChange manualLegalBasisChange) {
                        mergeLegalBasisChange(manualLegalBasisChange, entityLogEntry, analysisNumber);
                        return null;
                    } else if (localChange instanceof ManualRecategorization manualRecategorization) {
                        return mergeRecategorization(manualRecategorization, entityLogEntry, dossier, analysisNumber);
                    } else if (localChange instanceof ManualForceRedaction manualForceRedaction) {
                        mergeForceRedaction(manualForceRedaction, entityLogEntry, analysisNumber);
                        return null;
                    } else {
                        return null;
                    }
                })
                .filter(Objects::nonNull)
                .toList();
    }


    private Optional<EntityLogEntry> mergeManualRedactionEntries(ManualRedactionEntry manualRedactionEntry,
                                                                 List<EntityLogEntry> entityLogEntries,
                                                                 DossierEntity dossier,
                                                                 int analysisNumber) {

        if (manualRedactionEntry.getPositions() == null || manualRedactionEntry.getPositions().isEmpty()) {
            return Optional.empty();
        }

        if (isFalsePositive(manualRedactionEntry)) {
            var matchingEntities = entityLogEntries.stream()
                    .filter(entityLogEntry -> equalPosition(manualRedactionEntry.getPositions()
                                                                    .get(0),
                                                            entityLogEntry.getPositions()
                                                                    .get(0)))
                    .toList();
            matchingEntities.forEach(matchingEntity -> mergeFalsePositive(analysisNumber, matchingEntity));
            return Optional.empty();
        }

        return Optional.of(buildEntityLogEntry(manualRedactionEntry, analysisNumber, dossier));
    }


    public EntityLogEntry buildEntityLogEntry(ManualRedactionEntry manualRedactionEntry, int analysisNumber, DossierEntity dossier) {

        List<ManualChange> manualChanges = new ArrayList<>();
        manualChanges.add(ManualChangeFactory.toLocalManualChange(manualRedactionEntry, 0));

        List<Change> changes = new ArrayList<>();
        changes.add(ChangeFactory.toChange(ChangeType.ADDED, manualRedactionEntry.getRequestDate(), analysisNumber));

        boolean isHint = isHint(manualRedactionEntry.getType(), dossier);

        return EntityLogEntry.builder()
                .id(manualRedactionEntry.getAnnotationId())
                .type(manualRedactionEntry.getType())
                .value(manualRedactionEntry.getValue())
                .legalBasis(manualRedactionEntry.getLegalBasis())
                .reason(manualRedactionEntry.getReason())
                .entryType(isHint ? EntryType.HINT : manualRedactionEntry.isRectangle() ? EntryType.AREA : EntryType.ENTITY)
                .state(isHint ? EntryState.SKIPPED : EntryState.APPLIED)
                .imported(false)
                .matchedRule("")
                .section(manualRedactionEntry.getSection())
                .textBefore(manualRedactionEntry.getTextBefore())
                .textAfter(manualRedactionEntry.getTextAfter())
                .positions(convertPositions(manualRedactionEntry.getPositions()))
                .imageHasTransparency(false)
                .dictionaryEntry(manualRedactionEntry.isAddToDictionary())
                .dossierDictionaryEntry(manualRedactionEntry.isAddToDossierDictionary())
                .excluded(false)
                .changes(changes)
                .manualChanges(manualChanges)
                .engines(new HashSet<>(List.of(Engine.MANUAL)))
                .reference(new HashSet<>())
                .importedRedactionIntersections(new HashSet<>())
                .containingNodeId(Collections.emptyList())
                .closestHeadline("")
                .startOffset(-1)
                .endOffset(-1)
                .build();
    }


    private boolean isFalsePositive(ManualRedactionEntry manualRedactionEntry) {

        return manualRedactionEntry.getDictionaryEntryType() != null && manualRedactionEntry.getDictionaryEntryType().equals(DictionaryEntryType.FALSE_POSITIVE);
    }


    private void mergeFalsePositive(int analysisNumber, EntityLogEntry entityLogEntry) {

        List<Change> changes = new ArrayList<>();
        changes.add(ChangeFactory.toChange(ChangeType.REMOVED,
                                           OffsetDateTime.now(),
                                           analysisNumber,
                                           PropertyChange.builder().property("state").oldValue(entityLogEntry.getState().name()).newValue(EntryState.REMOVED.name()).build()));
        addChanges(entityLogEntry, changes);

        entityLogEntry.setState(EntryState.REMOVED);
    }


    public void mergeIdToRemove(IdRemoval idRemoval, EntityLogEntry entityLogEntry, int analysisNumber) {

        List<Change> changes = new ArrayList<>();
        Change change = ChangeFactory.toChange(ChangeType.IGNORED,
                                               OffsetDateTime.now(),
                                               analysisNumber,
                                               PropertyChange.builder().property("state").oldValue(entityLogEntry.getState().name()).newValue(EntryState.IGNORED.name()).build());

        entityLogEntry.setState(EntryState.IGNORED);
        //special case, only for add local and remove only
        if (entityLogEntry.getEngines().contains(Engine.MANUAL)) {
            entityLogEntry.setState(EntryState.REMOVED);
            change.setType(ChangeType.REMOVED);
        }
        entityLogEntry.getManualChanges().add(ManualChangeFactory.toLocalManualChange(idRemoval, 0));

        changes.add(change);
        addChanges(entityLogEntry, changes);
    }


    public void mergeResizeRedaction(ManualResizeRedaction manualResizeRedaction, EntityLogEntry entityLogEntry, int analysisNumber) {

        List<Change> changes = new ArrayList<>();
        List<Position> currentPositions = entityLogEntry.getPositions();
        List<Position> newPositions = convertPositions(manualResizeRedaction.getPositions());
        changes.add(ChangeFactory.toChange(ChangeType.RESIZED,
                                           manualResizeRedaction.getRequestDate(),
                                           analysisNumber,
                                           PropertyChange.builder()
                                                   .property("textAfter")
                                                   .oldValue(entityLogEntry.getTextAfter())
                                                   .newValue(manualResizeRedaction.getTextAfter())
                                                   .build(),
                                           PropertyChange.builder()
                                                   .property("textBefore")
                                                   .oldValue(entityLogEntry.getTextBefore())
                                                   .newValue(manualResizeRedaction.getTextBefore())
                                                   .build(),
                                           PropertyChange.builder().property("value").oldValue(entityLogEntry.getValue()).newValue(manualResizeRedaction.getValue()).build(),
                                           PropertyChange.builder()
                                                   .property("positions")
                                                   .oldValue(ChangeUtils.prettifyPositions(currentPositions))
                                                   .newValue(ChangeUtils.prettifyPositions(newPositions))
                                                   .build()));
        addChanges(entityLogEntry, changes);

        entityLogEntry.setTextAfter(manualResizeRedaction.getTextAfter());
        entityLogEntry.setTextBefore(manualResizeRedaction.getTextBefore());
        entityLogEntry.setPositions(newPositions);
        entityLogEntry.setValue(manualResizeRedaction.getValue());
        entityLogEntry.getManualChanges().add(ManualChangeFactory.toLocalManualChange(manualResizeRedaction, 0));
    }


    public boolean isDuplicatedByAny(EntityLogEntry entry, Map<String, EntityLogEntry> addedLocalManualEntryIds) {

        return !entry.getEngines().contains(Engine.MANUAL) && addedLocalManualEntryIds.containsKey(entry.getId()) && !addedLocalManualEntryIds.get(entry.getId()).equals(entry);
    }


    public void mergeLegalBasisChange(ManualLegalBasisChange manualLegalBasisChange, EntityLogEntry entityLogEntry, int analysisNumber) {

        List<Change> changes = new ArrayList<>();
        changes.add(ChangeFactory.toChange(ChangeType.LEGAL_BASIS_CHANGE,
                                           manualLegalBasisChange.getRequestDate(),
                                           analysisNumber,
                                           PropertyChange.builder()
                                                   .property("legalBasis")
                                                   .oldValue(entityLogEntry.getLegalBasis())
                                                   .newValue(manualLegalBasisChange.getLegalBasis())
                                                   .build(),
                                           PropertyChange.builder().property("section").oldValue(entityLogEntry.getSection()).newValue(manualLegalBasisChange.getSection()).build(),
                                           PropertyChange.builder().property("value").oldValue(entityLogEntry.getValue()).newValue(manualLegalBasisChange.getValue()).build()));
        addChanges(entityLogEntry, changes);

        entityLogEntry.setLegalBasis(manualLegalBasisChange.getLegalBasis());
        entityLogEntry.setSection(manualLegalBasisChange.getSection());
        entityLogEntry.setValue(manualLegalBasisChange.getValue());
        entityLogEntry.getManualChanges().add(ManualChangeFactory.toLocalManualChange(manualLegalBasisChange, 0));
    }


    public EntityLogEntry mergeRecategorization(ManualRecategorization recategorization, EntityLogEntry entityLogEntry, DossierEntity dossier, int analysisNumber) {

        List<Change> changes = new ArrayList<>();
        Change change = ChangeFactory.toChange(ChangeType.RECATEGORIZE,
                                               recategorization.getRequestDate(),
                                               analysisNumber,
                                               PropertyChange.builder().property("type").oldValue(entityLogEntry.getType()).newValue(recategorization.getType()).build());
        changes.add(change);
        EntryState entryState = entityLogEntry.getState();

        if ((recategorization.getType() != null && !Objects.equals(recategorization.getType(), entityLogEntry.getType()) && Strings.isNullOrEmpty(recategorization.getLegalBasis()))
            && (entityLogEntry.getEntryType().equals(EntryType.IMAGE) || entityLogEntry.getEntryType().equals(EntryType.IMAGE_HINT))) {

            addChanges(entityLogEntry, changes);
            return pendingEntryFactory.buildPendingImageRecategorizationEntry(recategorization, entityLogEntry);
        }

        if (recategorization.getType() != null && !recategorization.getType().equals(entityLogEntry.getType())) {
            boolean isHint = isHint(recategorization.getType(), dossier);
            entityLogEntry.setType(recategorization.getType());
            EntryType entryType = getEntryType(isHint, recategorization.getType());
            ChangeFactory.addPropertyChanges(change,
                                             PropertyChange.builder().property("entryType").oldValue(entityLogEntry.getEntryType().name()).newValue(entryType.name()).build());
            entityLogEntry.setEntryType(entryType);
            entityLogEntry.setState(isHint ? EntryState.SKIPPED : EntryState.APPLIED);
        }

        if (!Strings.isNullOrEmpty(recategorization.getLegalBasis())) {
            boolean isHint = isHint(entityLogEntry.getType(), dossier);
            if ((entityLogEntry.getEntryType().equals(EntryType.IMAGE) || entityLogEntry.getEntryType().equals(EntryType.IMAGE_HINT)) && !entityLogEntry.getManualChanges()
                    .isEmpty() && determineHintForImages(entityLogEntry.getManualChanges())) {
                isHint = false;
            }
            ChangeFactory.addPropertyChanges(change,
                                             PropertyChange.builder()
                                                     .property("legalBasis")
                                                     .oldValue(entityLogEntry.getLegalBasis())
                                                     .newValue(recategorization.getLegalBasis())
                                                     .build());
            entityLogEntry.setLegalBasis(recategorization.getLegalBasis());
            entityLogEntry.setState(isHint ? EntryState.SKIPPED : EntryState.APPLIED);
        }

        ChangeFactory.addPropertyChanges(change, PropertyChange.builder().property("state").oldValue(entryState.name()).newValue(entityLogEntry.getState().name()).build());

        if (recategorization.getSection() != null) {
            ChangeFactory.addPropertyChanges(change,
                                             PropertyChange.builder().property("section").oldValue(entityLogEntry.getSection()).newValue(recategorization.getSection()).build());
            entityLogEntry.setSection(recategorization.getSection());
        }

        if (recategorization.getValue() != null) {
            ChangeFactory.addPropertyChanges(change, PropertyChange.builder().property("value").oldValue(entityLogEntry.getValue()).newValue(recategorization.getValue()).build());
            entityLogEntry.setValue(recategorization.getValue());
        }

        addChanges(entityLogEntry, changes);
        entityLogEntry.getManualChanges().add(ManualChangeFactory.toLocalManualChange(recategorization, 0));
        return null;
    }


    public void mergeForceRedaction(ManualForceRedaction forceRedaction, EntityLogEntry entityLogEntry, int analysisNumber) {

        List<Change> changes = new ArrayList<>();
        EntryState oldState = entityLogEntry.getState();
        EntryState newState = entityLogEntry.getEntryType().equals(EntryType.HINT) ? EntryState.SKIPPED : EntryState.APPLIED;
        changes.add(ChangeFactory.toChange(ChangeType.FORCE_REDACT,
                                           forceRedaction.getRequestDate(),
                                           analysisNumber,
                                           PropertyChange.builder()
                                                   .property("legalBasis")
                                                   .oldValue(entityLogEntry.getLegalBasis())
                                                   .newValue(forceRedaction.getLegalBasis())
                                                   .build(),
                                           PropertyChange.builder().property("state").oldValue(oldState.name()).newValue(newState.name()).build()));
        entityLogEntry.setLegalBasis(forceRedaction.getLegalBasis());
        entityLogEntry.setState(newState);
        addChanges(entityLogEntry, changes);
        entityLogEntry.getManualChanges().add(ManualChangeFactory.toLocalManualChange(forceRedaction, 0));
    }


    private EntryType getEntryType(boolean isHint, String type) {

        if (type.equals("image") || type.equals("logo") || type.equals("signature") || type.equals("formula")) {
            return isHint ? EntryType.IMAGE_HINT : EntryType.IMAGE;
        } else {
            return isHint ? EntryType.HINT : EntryType.ENTITY;
        }
    }


    private void addChanges(EntityLogEntry entityLogEntry, List<Change> changes) {

        if (entityLogEntry.getChanges() != null && !entityLogEntry.getChanges().isEmpty()) {
            entityLogEntry.getChanges().addAll(changes);
        } else {
            entityLogEntry.setChanges(changes);
        }
    }


    private boolean determineHintForImages(List<ManualChange> manualChanges) {

        boolean hasForce = false;
        boolean hasRemoveAfterForce = false;

        for (ManualChange change : manualChanges) {
            if (change.getManualRedactionType() == ManualRedactionType.FORCE) {
                hasForce = true;
            }
            if (hasForce && change.getManualRedactionType() == ManualRedactionType.REMOVE) {
                hasRemoveAfterForce = true;
                break;
            }
        }

        return hasForce && !hasRemoveAfterForce;
    }


    public boolean isHint(String type, DossierEntity dossier) {

        String typeId = toTypeId(type, dossier.getDossierTemplateId());
        TypeEntity typeEntity = dictionaryPersistenceService.getType(typeId);

        if (typeEntity == null) {
            var optionalType = dictionaryPersistenceService.getAllTypes(false)
                    .stream()
                    .filter(typeEntity1 -> typeEntity1.getType().equals(type))
                    .findFirst();
            if (optionalType.isPresent()) {
                typeEntity = optionalType.get();
            } else {
                throw new NotFoundException("TypeEntity could not be found for typeId: " + typeId);
            }
        }

        return typeEntity.isHint();
    }


    private boolean equalPosition(Rectangle position1, Position position2) {

        return position1.getTopLeftX() == position2.x()
               && position1.getTopLeftY() == position2.y()
               && position1.getWidth() == position2.w()
               && position1.getHeight() == position2.h();
    }


    public List<Position> convertPositions(List<Rectangle> rectangles) {

        return rectangles.stream()
                .map(rectangle -> new Position(rectangle.getTopLeftX(), rectangle.getTopLeftY(), rectangle.getWidth(), rectangle.getHeight(), rectangle.getPage()))
                .collect(Collectors.toList());
    }

}
