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

import static com.iqser.red.service.persistence.service.v1.api.external.resource.ManualRedactionResource.ANNOTATION_ID;
import static com.iqser.red.service.persistence.service.v1.api.external.resource.ManualRedactionResource.DOSSIER_ID;
import static com.iqser.red.service.persistence.service.v1.api.external.resource.ManualRedactionResource.FILE_ID;

import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.iqser.red.service.persistence.management.v1.processor.entity.annotations.IdRemovalEntity;
import com.iqser.red.service.persistence.management.v1.processor.entity.annotations.ManualRecategorizationEntity;
import com.iqser.red.service.persistence.management.v1.processor.entity.annotations.ManualRedactionEntryEntity;
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.model.ManualChangesQueryOptions;
import com.iqser.red.service.persistence.management.v1.processor.service.EntityLogService;
import com.iqser.red.service.persistence.management.v1.processor.service.FileStatusService;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.AuditPersistenceService;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.DossierPersistenceService;
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.ForceRedactionPersistenceService;
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.RecategorizationPersistenceService;
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.AuditCategory;
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.annotations.ManualRedactions;
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.audit.AuditRequest;
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.manual.ManualRedactionWrapperModel;
import com.knecon.fforesight.keycloakcommons.security.KeycloakSecurity;

import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.experimental.FieldDefaults;

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

    ManualRedactionProviderService manualRedactionProviderService;
    AuditPersistenceService auditPersistenceService;
    FileStatusService fileStatusService;
    FileStatusPersistenceService fileStatusPersistenceService;
    RecategorizationPersistenceService recategorizationPersistenceService;
    DossierPersistenceService dossierPersistenceService;
    AddRedactionPersistenceService addRedactionPersistenceService;
    RemoveRedactionPersistenceService removeRedactionPersistenceService;
    ForceRedactionPersistenceService forceRedactionPersistenceService;
    LegalBasisChangePersistenceService legalBasisChangePersistenceService;
    ResizeRedactionPersistenceService resizeRedactionPersistenceService;
    EntityLogService entityLogService;
    ManualRedactionDictionaryUpdateHandler manualRedactionDictionaryUpdateHandler;


    @Transactional
    public void undo(String dossierId, String fileId, Set<String> annotationIds, boolean includeOnlyUnprocessed, boolean includeOnlyLocal) {

        // undo the manual redaction for each annotationId
        ManualRedactions manualRedactions = getManualRedactions(fileId, includeOnlyUnprocessed, includeOnlyLocal, new ArrayList<>(annotationIds));

        Map<String, List<ManualRedactionWrapperModel>> manualRedactionWrappers = groupAndSortManualRedactionsForAnnotationIds(manualRedactions, annotationIds);

        if (manualRedactionWrappers.isEmpty()) {
            throw new NotFoundException(String.format("ManualRedaction with annotationIds %s could not be found.", annotationIds));
        }

        for (String annotationId : manualRedactionWrappers.keySet()) {
            manualRedactionWrappers.get(annotationId)
                    .stream()
                    .forEach(manualChange -> {
                        if (manualChange.getItem() instanceof ManualRedactionEntry) {
                            undoManualRedactionEntry(dossierId, fileId, annotationId);
                        } else if (manualChange.getItem() instanceof IdRemoval) {
                            undoIdRemoval(dossierId, fileId, annotationId);
                        } else if (manualChange.getItem() instanceof ManualResizeRedaction) {
                            undoResizeEntry(dossierId, fileId, annotationId);
                        } else if (manualChange.getItem() instanceof ManualLegalBasisChange) {
                            undoLegalBasisChangeEntry(dossierId, fileId, annotationId);
                        } else if (manualChange.getItem() instanceof ManualRecategorization) {
                            undoRecategorizationEntry(dossierId, fileId, annotationId, includeOnlyUnprocessed);
                        } else if (manualChange.getItem() instanceof ManualForceRedaction) {
                            undoForceRedactionEntry(dossierId, fileId, annotationId);
                        }
                    });
        }

        reprocess(dossierId, fileId);
        fileStatusPersistenceService.setLastManualChangeDate(fileId, OffsetDateTime.now());
    }


    private ManualRedactions getManualRedactions(String fileId, boolean includeOnlyUnprocessed, boolean includeOnlyLocal, List<String> annotationIds) {

        var fileStatus = fileStatusService.getStatus(fileId);
        if (fileStatus.isExcludedFromAutomaticAnalysis() && !includeOnlyUnprocessed) { // analysis is disabled, only unprocessed changes can be reverted
            throw new BadRequestException("Only unprocessed manual redactions can be reverted. includeOnlyUnprocessed must be set to true");
        }

        ManualChangesQueryOptions options = ManualChangesQueryOptions.builder()
                .includeDeletions(false)
                .includeDictChanges(!includeOnlyLocal)
                .includeOnlyUnprocessed(includeOnlyUnprocessed)
                .annotationIds(annotationIds)
                .build();
        return manualRedactionProviderService.getManualRedactions(fileId, options);
    }


    private void reprocess(String dossierId, String fileId) {

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


    private void undoResizeEntry(String dossierId, String fileId, String annotationId) {

        OffsetDateTime now = OffsetDateTime.now();
        resizeRedactionPersistenceService.softDelete(fileId, annotationId, now);
        auditPersistenceService.audit(AuditRequest.builder()
                                              .userId(KeycloakSecurity.getUserId())
                                              .objectId(fileId)
                                              .category(AuditCategory.DOCUMENT.name())
                                              .message("Undo of manual resize redaction was done.")
                                              .details(Map.of(DOSSIER_ID, dossierId, FILE_ID, fileId, ANNOTATION_ID, annotationId))
                                              .build());

    }


    private void undoLegalBasisChangeEntry(String dossierId, String fileId, String annotationId) {

        legalBasisChangePersistenceService.softDelete(fileId, annotationId, OffsetDateTime.now());
        auditPersistenceService.audit(AuditRequest.builder()
                                              .userId(KeycloakSecurity.getUserId())
                                              .objectId(fileId)
                                              .category(AuditCategory.DOCUMENT.name())
                                              .message("Undo of legal basis change was done.")
                                              .details(Map.of(DOSSIER_ID, dossierId, FILE_ID, fileId, ANNOTATION_ID, annotationId))
                                              .build());
    }


    private void undoRecategorizationEntry(String dossierId, String fileId, String annotationId, boolean includeUnprocessed) {

        EntityLog entityLog = entityLogService.getEntityLog(dossierId, fileId, includeUnprocessed);

        ManualRecategorizationEntity recategorizationEntity = recategorizationPersistenceService.findRecategorization(fileId, annotationId);
        String originalValue = getEntityLogEntry(entityLog, annotationId).getValue();
        manualRedactionDictionaryUpdateHandler.revertRemoveFromDictionary(originalValue, dossierId, fileId, recategorizationEntity.getTypeIdsOfDictionariesWithDelete());
        manualRedactionDictionaryUpdateHandler.revertAddToDictionary(originalValue,
                                                                     DictionaryEntryType.ENTRY,
                                                                     fileId,
                                                                     dossierId,
                                                                     recategorizationEntity.getTypeIdsOfDictionariesWithAdd());
        recategorizationEntity.setTypeIdsOfDictionariesWithAdd(Collections.emptySet());
        recategorizationEntity.setTypeIdsOfDictionariesWithDelete(Collections.emptySet());
        recategorizationEntity.setSoftDeletedTime(OffsetDateTime.now());
        recategorizationPersistenceService.saveAndFlush(recategorizationEntity);

        auditPersistenceService.audit(AuditRequest.builder()
                                              .userId(KeycloakSecurity.getUserId())
                                              .objectId(fileId)
                                              .category(AuditCategory.DOCUMENT.name())
                                              .message("Undo of manual recategorization was done.")
                                              .details(Map.of(DOSSIER_ID, dossierId, FILE_ID, fileId, ANNOTATION_ID, annotationId))
                                              .build());
    }


    private void undoForceRedactionEntry(String dossierId, String fileId, String annotationId) {

        var now = OffsetDateTime.now();
        forceRedactionPersistenceService.softDelete(fileId, annotationId, now);
        auditPersistenceService.audit(AuditRequest.builder()
                                              .userId(KeycloakSecurity.getUserId())
                                              .objectId(fileId)
                                              .category(AuditCategory.DOCUMENT.name())
                                              .message("Undo of manual force redaction was done.")
                                              .details(Map.of(DOSSIER_ID, dossierId, FILE_ID, fileId, ANNOTATION_ID, annotationId))
                                              .build());

    }


    private void undoIdRemoval(String dossierId, String fileId, String annotationId) {

        EntityLog entityLog = entityLogService.getEntityLog(dossierId, fileId, true);

        IdRemovalEntity removeRedaction = removeRedactionPersistenceService.findRemoveRedaction(fileId, annotationId);
        String originalValue = getEntityLogEntry(entityLog, annotationId).getValue();
        boolean dictionaryChanged = manualRedactionDictionaryUpdateHandler.revertRemoveFromDictionary(originalValue, dossierId, fileId, removeRedaction);
        removeRedactionPersistenceService.updateModifiedDictionaries(fileId, annotationId, dictionaryChanged, Collections.emptySet());
        removeRedactionPersistenceService.softDelete(fileId, annotationId, OffsetDateTime.now());

        auditPersistenceService.audit(AuditRequest.builder()
                                              .userId(KeycloakSecurity.getUserId())
                                              .objectId(fileId)
                                              .category(AuditCategory.DOCUMENT.name())
                                              .message("Undo of manual remove redaction was done.")
                                              .details(Map.of(DOSSIER_ID, dossierId, FILE_ID, fileId, ANNOTATION_ID, annotationId))
                                              .build());

    }


    private void undoManualRedactionEntry(String dossierId, String fileId, String annotationId) {

        ManualRedactionEntryEntity addRedaction = addRedactionPersistenceService.findAddRedaction(fileId, annotationId);

        boolean dictionaryChanged = manualRedactionDictionaryUpdateHandler.revertAddToDictionary(fileId, dossierId, addRedaction);
        addRedactionPersistenceService.updateModifiedDictionaries(fileId, annotationId, dictionaryChanged, null);
        addRedactionPersistenceService.softDelete(fileId, annotationId, OffsetDateTime.now());

        auditPersistenceService.audit(AuditRequest.builder()
                                              .userId(KeycloakSecurity.getUserId())
                                              .objectId(fileId)
                                              .category(AuditCategory.DOCUMENT.name())
                                              .message("Undo of manual add redaction was done.")
                                              .details(Map.of(DOSSIER_ID, dossierId, FILE_ID, fileId, ANNOTATION_ID, annotationId))
                                              .build());

    }


    private EntityLogEntry getEntityLogEntry(EntityLog entityLog, String annotationId) {

        return entityLog.getEntityLogEntry()
                .stream()
                .filter(entry -> entry.getId().equals(annotationId))
                .findFirst()
                .orElseThrow(() -> new NotFoundException("Annotation does not exist in entity log."));
    }


    private Map<String, List<ManualRedactionWrapperModel>> groupAndSortManualRedactionsForAnnotationIds(ManualRedactions manualRedactions, Set<String> annotationIds) {

        Map<String, List<ManualRedactionWrapperModel>> result = new HashMap<>();
        annotationIds.forEach(annotationId -> {
            var manualRedactionsForId = getLatestManualRedactionForAnnotationId(manualRedactions, annotationId);
            if (manualRedactionsForId != null) {
                result.put(annotationId, manualRedactionsForId);
            }
        });

        return result;
    }


    private Map<String, ManualRedactionWrapperModel> getLatestManualRedactionsForAnnotationIds(ManualRedactions manualRedactions, Set<String> annotationIds) {

        Map<String, ManualRedactionWrapperModel> result = new HashMap<>();
        annotationIds.forEach(annotationId -> {
            var last = getLatestManualRedactionForAnnotationId(manualRedactions, annotationId);
            if (last != null) {
                result.put(annotationId, last.get(0));
            }
        });

        return result;
    }


    private List<ManualRedactionWrapperModel> getLatestManualRedactionForAnnotationId(ManualRedactions manualRedactions, String annotationId) {

        final List<ManualRedactionWrapperModel> manualRedactionWrappers = new ArrayList<>();

        manualRedactions.getEntriesToAdd()
                .stream()
                .filter(item -> item.getSoftDeletedTime() == null && item.getAnnotationId().equals(annotationId))
                .forEach(item -> manualRedactionWrappers.add(new ManualRedactionWrapperModel(item.getAnnotationId(), item.getRequestDate(), item)));

        manualRedactions.getRecategorizations()
                .stream()
                .filter(item -> item.getSoftDeletedTime() == null && item.getAnnotationId().equals(annotationId))
                .forEach(item -> manualRedactionWrappers.add(new ManualRedactionWrapperModel(item.getAnnotationId(), item.getRequestDate(), item)));

        manualRedactions.getIdsToRemove()
                .stream()
                .filter(item -> item.getSoftDeletedTime() == null && item.getAnnotationId().equals(annotationId))
                .forEach(item -> manualRedactionWrappers.add(new ManualRedactionWrapperModel(item.getAnnotationId(), item.getRequestDate(), item)));

        manualRedactions.getForceRedactions()
                .stream()
                .filter(item -> item.getSoftDeletedTime() == null && item.getAnnotationId().equals(annotationId))
                .forEach(item -> manualRedactionWrappers.add(new ManualRedactionWrapperModel(item.getAnnotationId(), item.getRequestDate(), item)));

        manualRedactions.getLegalBasisChanges()
                .stream()
                .filter(item -> item.getSoftDeletedTime() == null && item.getAnnotationId().equals(annotationId))
                .forEach(item -> manualRedactionWrappers.add(new ManualRedactionWrapperModel(item.getAnnotationId(), item.getRequestDate(), item)));

        manualRedactions.getResizeRedactions()
                .stream()
                .filter(item -> item.getSoftDeletedTime() == null && item.getAnnotationId().equals(annotationId))
                .forEach(item -> manualRedactionWrappers.add(new ManualRedactionWrapperModel(item.getAnnotationId(), item.getRequestDate(), item)));

        var sortedManualRedactionWrappers = manualRedactionWrappers.stream()
                .sorted(Comparator.comparing(ManualRedactionWrapperModel::getDate, Comparator.nullsLast(Comparator.reverseOrder())))
                .toList();

        return sortedManualRedactionWrappers.isEmpty() ? null : sortedManualRedactionWrappers;
    }

}
