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 java.util.stream.Collectors;

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.NotFoundException;
import com.iqser.red.service.persistence.management.v1.processor.service.AnalysisFlagsCalculationService;
import com.iqser.red.service.persistence.management.v1.processor.service.FileStatusService;
import com.iqser.red.service.persistence.management.v1.processor.service.RedactionLogService;
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.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.annotations.AnnotationStatus;
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.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.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;
    AnalysisFlagsCalculationService analysisFlagsCalculationService;
    RecategorizationPersistenceService recategorizationPersistenceService;
    DossierPersistenceService dossierPersistenceService;
    AddRedactionPersistenceService addRedactionPersistenceService;
    RemoveRedactionPersistenceService removeRedactionPersistenceService;
    ForceRedactionPersistenceService forceRedactionPersistenceService;
    LegalBasisChangePersistenceService legalBasisChangePersistenceService;
    ResizeRedactionPersistenceService resizeRedactionPersistenceService;
    RedactionLogService redactionLogService;
    ManualRedactionDictionaryUpdateHandler manualRedactionDictionaryUpdateHandler;


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

        // undo the latest manual redaction for each annotationId
        ManualRedactions manualRedactions = getManualRedactions(fileId);

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

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

        undoManualRedactionEntries(dossierId, fileId, manualRedactionWrappers);
        undoIdRemovals(dossierId, fileId, manualRedactionWrappers);
        undoForceRedactions(dossierId, fileId, manualRedactionWrappers);
        undoRecategorization(dossierId, fileId, manualRedactionWrappers);
        undoLegalBasisChange(dossierId, fileId, manualRedactionWrappers);
        undoResize(dossierId, fileId, manualRedactionWrappers);
        reprocess(dossierId, fileId);
    }


    private ManualRedactions getManualRedactions(String fileId) {

        return manualRedactionProviderService.getManualRedactions(fileId);
    }


    private void reprocess(String dossierId, String fileId) {

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


    private void undoResize(String dossierId, String fileId, Map<String, ManualRedactionWrapperModel> manualRedactionWrappers) {

        List<String> manualResizeRedactions = manualRedactionWrappers.values()
                .stream()
                .filter(manualRedactionWrapper -> manualRedactionWrapper.getItem() instanceof ManualResizeRedaction)
                .map(ManualRedactionWrapperModel::getId)
                .collect(Collectors.toList());
        if (!manualResizeRedactions.isEmpty()) {
            deleteResizeRedaction(dossierId, fileId, manualResizeRedactions);
            manualResizeRedactions.forEach(annotationId -> 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 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);
    }


    private void undoLegalBasisChange(String dossierId, String fileId, Map<String, ManualRedactionWrapperModel> manualRedactionWrappers) {

        List<String> manualLegalBasisChanges = manualRedactionWrappers.values()
                .stream()
                .filter(manualRedactionWrapper -> manualRedactionWrapper.getItem() instanceof ManualLegalBasisChange)
                .map(ManualRedactionWrapperModel::getId)
                .collect(Collectors.toList());
        if (!manualLegalBasisChanges.isEmpty()) {

            deleteLegalBasisChange(dossierId, fileId, manualLegalBasisChanges);
            manualLegalBasisChanges.forEach(annotationId -> 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 deleteLegalBasisChange(String dossierId, String fileId, List<String> annotationIds) {

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

        analysisFlagsCalculationService.calculateFlags(dossierId, fileId);
    }


    private void undoRecategorization(String dossierId, String fileId, Map<String, ManualRedactionWrapperModel> manualRedactionWrappers) {

        List<String> manualImageRecategorizations = manualRedactionWrappers.values()
                .stream()
                .filter(manualRedactionWrapper -> manualRedactionWrapper.getItem() instanceof ManualRecategorization)
                .map(ManualRedactionWrapperModel::getId)
                .collect(Collectors.toList());
        if (!manualImageRecategorizations.isEmpty()) {

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


    private void deleteRecategorization(String dossierId, String fileId, List<String> annotationIds) {

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

        for (var annotationId : annotationIds) {
            ManualRecategorizationEntity recategorizationEntity = recategorizationPersistenceService.findRecategorization(fileId, annotationId);
            String originalValue = getRedactionLogEntry(redactionLog, annotationId).getValue();
            manualRedactionDictionaryUpdateHandler.revertRemoveFromDictionary(originalValue, dossierId, fileId, recategorizationEntity.getTypeIdsOfDictionariesWithDelete());
            manualRedactionDictionaryUpdateHandler.revertAddToDictionary(originalValue,
                    DictionaryEntryType.ENTRY,
                    fileId,
                    dossierId,
                    recategorizationEntity.getTypeIdsOfDictionariesWithAdd());
            recategorizationPersistenceService.updateStatus(fileId, annotationId, AnnotationStatus.APPROVED, false, Collections.emptySet(), Collections.emptySet());
            recategorizationPersistenceService.softDelete(fileId, annotationId, OffsetDateTime.now());
        }
        analysisFlagsCalculationService.calculateFlags(dossierId, fileId);
    }


    private void undoForceRedactions(String dossierId, String fileId, Map<String, ManualRedactionWrapperModel> manualRedactionWrappers) {

        List<String> manualForceRedactions = manualRedactionWrappers.values()
                .stream()
                .filter(manualRedactionWrapper -> manualRedactionWrapper.getItem() instanceof ManualForceRedaction)
                .map(ManualRedactionWrapperModel::getId)
                .collect(Collectors.toList());
        if (!manualForceRedactions.isEmpty()) {

            deleteForceRedaction(dossierId, fileId, manualForceRedactions);
            manualForceRedactions.forEach(annotationId -> 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 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);
    }


    private void undoIdRemovals(String dossierId, String fileId, Map<String, ManualRedactionWrapperModel> manualRedactionWrappers) {

        List<String> idRemovals = manualRedactionWrappers.values()
                .stream()
                .filter(manualRedactionWrapper -> manualRedactionWrapper.getItem() instanceof IdRemoval)
                .map(ManualRedactionWrapperModel::getId)
                .collect(Collectors.toList());
        if (!idRemovals.isEmpty()) {
            deleteRemoveRedaction(dossierId, fileId, idRemovals);
            idRemovals.forEach(annotationId -> 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 deleteRemoveRedaction(String dossierId, String fileId, List<String> annotationIds) {

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

        for (String annotationId : annotationIds) {

            IdRemovalEntity removeRedaction = removeRedactionPersistenceService.findRemoveRedaction(fileId, annotationId);
            String originalValue = getRedactionLogEntry(redactionLog, annotationId).getValue();
            boolean dictionaryChanged = manualRedactionDictionaryUpdateHandler.revertRemoveFromDictionary(originalValue, dossierId, fileId, removeRedaction);
            removeRedactionPersistenceService.updateStatus(fileId, annotationId, removeRedaction.getStatus(), dictionaryChanged, Collections.emptySet());
            removeRedactionPersistenceService.softDelete(fileId, annotationId, OffsetDateTime.now());
        }

        analysisFlagsCalculationService.calculateFlags(dossierId, fileId);
    }


    private void undoManualRedactionEntries(String dossierId, String fileId, Map<String, ManualRedactionWrapperModel> manualRedactionWrappers) {

        List<String> manualRedactionEntries = manualRedactionWrappers.values()
                .stream()
                .filter(manualRedactionWrapper -> manualRedactionWrapper.getItem() instanceof ManualRedactionEntry)
                .map(ManualRedactionWrapperModel::getId)
                .collect(Collectors.toList());
        if (!manualRedactionEntries.isEmpty()) {
            deleteAddRedaction(dossierId, fileId, manualRedactionEntries);
            manualRedactionEntries.forEach(annotationId -> 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 void deleteAddRedaction(String dossierId, String fileId, List<String> annotationIds) {

        var dossier = dossierPersistenceService.getAndValidateDossier(dossierId);

        for (String annotationId : annotationIds) {

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

            boolean dictionaryChanged = manualRedactionDictionaryUpdateHandler.revertAddToDictionary(fileId, dossier.getId(), addRedaction);
            addRedactionPersistenceService.updateStatus(fileId, annotationId, AnnotationStatus.APPROVED, dictionaryChanged, null);
            addRedactionPersistenceService.softDelete(fileId, annotationId, OffsetDateTime.now());
        }

        analysisFlagsCalculationService.calculateFlags(dossierId, fileId);
    }


    private RedactionLogEntry getRedactionLogEntry(RedactionLog redactionLog, String annotationId) {

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


    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);
            }
        });

        return result;
    }


    private 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.get(0);
    }

}
