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

import java.time.OffsetDateTime;
import java.util.Map;
import java.util.stream.Collectors;

import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;

import com.iqser.red.service.persistence.management.v1.processor.entity.annotations.ViewedPageEntity;
import com.iqser.red.service.persistence.management.v1.processor.entity.dossier.FileEntity;
import com.iqser.red.service.persistence.management.v1.processor.exception.NotFoundException;
import com.iqser.red.service.persistence.management.v1.processor.model.AnalysisFlags;
import com.iqser.red.service.persistence.management.v1.processor.model.websocket.FileEventType;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.FileStatusPersistenceService;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.ViewedPagesPersistenceService;
import com.iqser.red.service.persistence.management.v1.processor.service.websocket.WebsocketService;
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.dossiertemplate.dossier.file.WorkflowStatus;

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

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

    FileStatusPersistenceService fileStatusPersistenceService;
    EntityLogService entityLogService;
    ViewedPagesPersistenceService viewedPagesPersistenceService;
    ObservationRegistry observationRegistry;
    WebsocketService websocketService;


    @Timed("redactmanager_calculateFlags")
    @Observed(name = "AnalysisFlagsCalculationService", contextualName = "calculate-flags")
    public void calculateFlags(String dossierId, String fileId) {

        addIdsToTrace(dossierId, fileId);

        long startTime = System.currentTimeMillis();

        FlagCalculationData data;
        try {
            data = getFlagCalculationData(dossierId, fileId);
        } catch (NotFoundException e) {
            log.warn("EntityLog does not exist, skipping flag calculation.");
            fileStatusPersistenceService.setLastFlagCalculation(fileId, OffsetDateTime.now());
            fileStatusPersistenceService.updateFlags(fileId, false, false, false, false, false, false);
            return;
        }

        fileStatusPersistenceService.setLastFlagCalculation(fileId, OffsetDateTime.now());

        AnalysisFlags flags = calculateFlags(data);

        log.debug("Flag Calculations for file: {} took: {}ms", fileId, System.currentTimeMillis() - startTime);
        if (data.file().isHasRedactions() == flags.hasRedactions()
            && data.file().isHasHints() == flags.hasHints()
            && data.file().isHasImages() == flags.hasImages()
            && data.file().isHasSuggestions() == flags.hasSuggestions()
            && data.file().isHasAnnotationComments() == flags.hasComments()
            && data.file().isHasUpdates() == flags.hasUpdates()) {
            log.debug("Nothing Changed for file: {}", fileId);
        } else {
            fileStatusPersistenceService.updateFlags(fileId,
                                                     flags.hasRedactions(),
                                                     flags.hasHints(),
                                                     flags.hasImages(),
                                                     flags.hasSuggestions(),
                                                     flags.hasComments(),
                                                     flags.hasUpdates());
        }

        if (flags.lastRedactionModification() != null && (data.file().getRedactionModificationDate() == null || data.file()
                .getRedactionModificationDate()
                .isBefore(flags.lastRedactionModification()))) {
            fileStatusPersistenceService.setLastRedactionModificationDateForFile(fileId, flags.lastRedactionModification());
        }
        if (flags.lastManualChangeDate() != null && (data.file().getLastManualChangeDate() == null || data.file()
                .getLastManualChangeDate()
                .isBefore(flags.lastManualChangeDate()))) {
            fileStatusPersistenceService.setLastManualChangeDate(fileId, flags.lastManualChangeDate());
        }

        fileStatusPersistenceService.setLastFlagCalculation(fileId, OffsetDateTime.now());
        websocketService.sendFileEvent(dossierId, fileId, FileEventType.UPDATE);
    }


    public AnalysisFlags calculateFlags(FlagCalculationData data) {

        boolean hasRedactions = false;
        boolean hasHints = false;
        boolean hasSuggestions = false;
        boolean hasImages = false;
        boolean hasUpdates = false;
        boolean hasComments = false;

        OffsetDateTime lastRedactionModification = null;
        OffsetDateTime lastManualChangeDate = null;

        for (EntityLogEntry entry : data.entityLog().getEntityLogEntry()) {

            String type = getType(entry.getType());

            var lastChange = entry.getChanges().isEmpty() ? null : entry.getChanges()
                    .get(entry.getChanges().size() - 1);
            var entryType = entry.getEntryType();
            if (entry.getManualChanges() != null && !entry.getManualChanges().isEmpty()) {

                for (var manualChange : entry.getManualChanges()) {
                    if (entryTypeIsNotRecommendationOrHintAndManualChangeIsNotRemoved(entry, manualChange, entryType, lastRedactionModification)) {
                        lastRedactionModification = manualChange.getProcessedDate();
                    }

                    if (manualChange.getProcessedDate() != null && (lastManualChangeDate == null || manualChange.getProcessedDate().isAfter(lastManualChangeDate))) {
                        lastManualChangeDate = manualChange.getProcessedDate();
                    }

                    if (manualChange.getRequestedDate() != null && (lastManualChangeDate == null || manualChange.getRequestedDate().isAfter(lastManualChangeDate))) {
                        lastManualChangeDate = manualChange.getRequestedDate();
                    }
                }
            }

            if (lastChange != null && //
                (lastRedactionModification == null || lastChange.getDateTime().isAfter(lastRedactionModification)) && //
                !entryType.equals(EntryType.HINT) &&//
                !entryType.equals(EntryType.RECOMMENDATION)) {
                lastRedactionModification = lastChange.getDateTime();
            }

            if (!hasRedactions && entry.getState().equals(EntryState.APPLIED) && //
                (entryType.equals(EntryType.ENTITY)
                 || entryType.equals(EntryType.IMAGE)
                 || entryType.equals(EntryType.HINT)
                 || entryType.equals(EntryType.IMAGE_HINT)
                 || entryType.equals(EntryType.AREA))) {
                hasRedactions = true;
            }

            if (!hasHints && (entryType.equals(EntryType.HINT) || entryType.equals(EntryType.IMAGE_HINT)) && entry.getState().equals(EntryState.SKIPPED)) {
                hasHints = true;
            }

            if (!hasImages && (entryType.equals(EntryType.IMAGE) || entryType.equals(EntryType.IMAGE_HINT))) {
                hasImages = true;
            }

            OffsetDateTime viewedPage = entry.getPositions().isEmpty() ? null : data.viewedPages.get(entry.getPositions()
                                                                                                             .get(0).getPageNumber());

            if (data.file().getWorkflowStatus() != WorkflowStatus.APPROVED && lastChange != null && lastChange.getDateTime() != null && viewedPage != null && //
                viewedPage.isBefore(lastChange.getDateTime())) {
                hasUpdates = true;
            }

            if (entry.getNumberOfComments() > 0 && !entry.getState().equals(EntryState.REMOVED)) {
                hasComments = true;
            }

        }
        return new AnalysisFlags(hasRedactions, hasHints, hasSuggestions, hasImages, hasUpdates, hasComments, lastRedactionModification, lastManualChangeDate);
    }


    private FlagCalculationData getFlagCalculationData(String dossierId, String fileId) {

        var file = fileStatusPersistenceService.getStatus(fileId);
        var entityLog = entityLogService.getEntityLog(dossierId, fileId, true);
        var viewedPagesForCurrentAssignee = viewedPagesPersistenceService.findViewedPages(fileId, file.getAssignee())
                .stream()
                .collect(Collectors.toMap(ViewedPageEntity::getPage, ViewedPageEntity::getViewedTime));

        return new FlagCalculationData(file, entityLog, viewedPagesForCurrentAssignee);
    }


    public record FlagCalculationData(
            FileEntity file, EntityLog entityLog, Map<Integer, OffsetDateTime> viewedPages
    ) {

    }


    private void addIdsToTrace(String dossierId, String fileId) {

        if (observationRegistry.getCurrentObservation() != null) {
            observationRegistry.getCurrentObservation().highCardinalityKeyValue("dossierId", dossierId).highCardinalityKeyValue("fileId", fileId);
        }
    }


    private static boolean entryTypeIsNotRecommendationOrHintAndManualChangeIsNotRemoved(EntityLogEntry entry,
                                                                                         ManualChange manualChange,
                                                                                         EntryType entryType,
                                                                                         OffsetDateTime lastRedactionModification) {

        return !entryType.equals(EntryType.HINT) && //
               !entryType.equals(EntryType.RECOMMENDATION) && //
               StringUtils.isNotEmpty(entry.getReason()) && //
               (manualChange.getManualRedactionType().equals(ManualRedactionType.ADD) || //
                manualChange.getManualRedactionType().equals(ManualRedactionType.RECATEGORIZE) || //
                manualChange.getManualRedactionType().equals(ManualRedactionType.REMOVE) || //
                manualChange.getManualRedactionType().equals(ManualRedactionType.FORCE) || //
                manualChange.getManualRedactionType().equals(ManualRedactionType.LEGAL_BASIS_CHANGE) || //
                manualChange.getManualRedactionType().equals(ManualRedactionType.RESIZE)) && //
               manualChange.getProcessedDate() != null && //
               (lastRedactionModification == null || manualChange.getProcessedDate().isAfter(lastRedactionModification));
    }


    private String getType(String typeId) {

        return typeId.split(":")[0];
    }

}
