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

import static com.iqser.red.service.persistence.management.v1.processor.service.ReanalysisRequiredStatusService.VersionType.AI_CREATION;
import static com.iqser.red.service.persistence.management.v1.processor.service.ReanalysisRequiredStatusService.VersionType.COMPONENT_RULES;
import static com.iqser.red.service.persistence.management.v1.processor.service.ReanalysisRequiredStatusService.VersionType.DATE_FORMATS;
import static com.iqser.red.service.persistence.management.v1.processor.service.ReanalysisRequiredStatusService.VersionType.DICTIONARY;
import static com.iqser.red.service.persistence.management.v1.processor.service.ReanalysisRequiredStatusService.VersionType.LEGAL_BASIS;
import static com.iqser.red.service.persistence.management.v1.processor.service.ReanalysisRequiredStatusService.VersionType.RULES;

import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

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.DossierNotFoundException;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.DateFormatsPersistenceService;
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.LegalBasisMappingPersistenceService;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.RulesPersistenceService;
import com.iqser.red.service.persistence.management.v1.processor.settings.FileManagementServiceSettings;
import com.iqser.red.service.persistence.service.v1.api.shared.model.RuleFileType;
import com.iqser.red.service.persistence.service.v1.api.shared.model.component.ComponentMappingMetadata;
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.dossier.file.ErrorCode;
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.dossier.file.FileModel;
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.dossier.file.ProcessingStatus;

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.experimental.FieldDefaults;
import lombok.extern.slf4j.Slf4j;

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

    DictionaryPersistenceService dictionaryPersistenceService;
    RulesPersistenceService rulesPersistenceService;
    DateFormatsPersistenceService dateFormatsPersistenceService;
    DossierPersistenceService dossierPersistenceService;
    LegalBasisMappingPersistenceService legalBasisMappingPersistenceService;
    ComponentMappingService componentMappingService;
    FileManagementServiceSettings settings;


    public FileModel enhanceFileStatusWithAnalysisRequirements(FileModel fileModel) {

        return enhanceFileStatusWithAnalysisRequirements(Collections.singletonList(fileModel), false).iterator().next();
    }


    public List<FileModel> enhanceFileStatusWithAnalysisRequirements(List<FileModel> fileModels, boolean ignoreProcessingStates) {

        Map<String, Map<VersionType, Long>> dossierTemplateVersionMap = new HashMap<>();
        Map<String, Long> dossierVersionMap = new HashMap<>();
        Map<String, DossierEntity> dossierMap = new HashMap<>();
        Map<String, Map<String, Integer>> componentMappingVersionMap = new HashMap<>();
        fileModels.forEach(entry -> {
            var analysisRequiredResult = computeAnalysisRequired(entry,
                                                                 ignoreProcessingStates,
                                                                 dossierTemplateVersionMap,
                                                                 dossierVersionMap,
                                                                 dossierMap,
                                                                 componentMappingVersionMap);
            entry.setReanalysisRequired(analysisRequiredResult.isReanalysisRequired());
            entry.setFullAnalysisRequired(analysisRequiredResult.isFullAnalysisRequired());
            entry.setComponentReanalysisRequired(analysisRequiredResult.isComponentReanalysisRequired());
        });

        return fileModels;
    }


    private AnalysisRequiredResult computeAnalysisRequired(FileModel fileStatus,
                                                           boolean ignoreProcessingStates,
                                                           Map<String, Map<VersionType, Long>> dossierTemplateVersionMap,
                                                           Map<String, Long> dossierVersionMap,
                                                           Map<String, DossierEntity> dossierMap,
                                                           Map<String, Map<String, Integer>> componentMappingVersionMap) {

        // enhance with dossierTemplateId
        DossierEntity dossier;
        try {
            dossier = dossierMap.computeIfAbsent(fileStatus.getDossierId(), k -> dossierPersistenceService.findByDossierId(fileStatus.getDossierId()));
        } catch (DossierNotFoundException e) {
            log.info("Dossier {} was not found, thus analysis is not required", fileStatus.getDossierId());
            return new AnalysisRequiredResult(false, false);
        }

        fileStatus.setDossierArchived(dossier.getArchivedTime() != null);
        fileStatus.setDossierDeleted(dossier.getHardDeletedTime() != null || dossier.getSoftDeletedTime() != null);
        fileStatus.setDossierTemplateId(dossier.getDossierTemplateId());
        fileStatus.setDossierStatusId(dossier.getDossierStatusId());

        if (fileStatus.isSoftOrHardDeleted()) {
            log.debug("File {} is deleted, thus analysis is not required", fileStatus.getId());
            return new AnalysisRequiredResult(false, false);
        }
        if (dossier.getSoftDeletedTime() != null || dossier.getHardDeletedTime() != null || dossier.getArchivedTime() != null) {
            log.debug("Dossier {} is deleted, thus analysis is not required", fileStatus.getDossierId());
            return new AnalysisRequiredResult(false, false);
        }

        if (ProcessingStatus.ERROR.equals(fileStatus.getProcessingStatus()) && !ignoreProcessingStates) {
            return computeAnalysisRequiredForErrorState(dossier, fileStatus, dossierTemplateVersionMap);
        }

        if (ProcessingStatus.PROCESSED.equals(fileStatus.getProcessingStatus())
            || ProcessingStatus.PRE_PROCESSED.equals(fileStatus.getProcessingStatus())
            || ignoreProcessingStates) {

            switch (fileStatus.getWorkflowStatus()) {
                case NEW:
                case UNDER_REVIEW:
                case UNDER_APPROVAL:
                    if (fileStatus.getLastProcessed() == null) {
                        return new AnalysisRequiredResult(false, true);
                    }
                    if (fileStatus.getLastFileAttributeChange() != null && fileStatus.getLastProcessed().isBefore(fileStatus.getLastFileAttributeChange())) {
                        return new AnalysisRequiredResult(true, false);
                    } else {
                        return requiresReanalysisBasedOnVersionDifference(fileStatus, dossier, dossierTemplateVersionMap, dossierVersionMap, componentMappingVersionMap);
                    }
                default:
                    return new AnalysisRequiredResult(false, false);
            }
        }
        return new AnalysisRequiredResult(false, false);

    }


    private AnalysisRequiredResult computeAnalysisRequiredForErrorState(DossierEntity dossier,
                                                                        FileModel fileStatus,
                                                                        Map<String, Map<VersionType, Long>> dossierTemplateVersionMap) {

        Map<VersionType, Long> dossierTemplateVersions = dossierTemplateVersionMap.computeIfAbsent(dossier.getDossierTemplateId(),
                                                                                                   k -> buildVersionData(dossier.getDossierTemplateId()));
        var rulesVersionMatches = fileStatus.getRulesVersion() == dossierTemplateVersions.getOrDefault(RULES, -1L);
        var componentRulesVersionMatches = fileStatus.getComponentRulesVersion() == dossierTemplateVersions.getOrDefault(COMPONENT_RULES, -1L);

        var isRuleExecutionTimeout = fileStatus.getFileErrorInfo() != null && fileStatus.getFileErrorInfo().getErrorCode() != null && fileStatus.getFileErrorInfo()
                .getErrorCode()
                .equals(ErrorCode.RULES_EXECUTION_TIMEOUT);

        var fullAnalysisRequired = (!rulesVersionMatches || !componentRulesVersionMatches) && !isRuleExecutionTimeout;

        return new AnalysisRequiredResult(false, fullAnalysisRequired);
    }


    private AnalysisRequiredResult requiresReanalysisBasedOnVersionDifference(FileModel fileStatus,
                                                                              DossierEntity dossier,
                                                                              Map<String, Map<VersionType, Long>> dossierTemplateVersionMap,
                                                                              Map<String, Long> dossierVersionMap,
                                                                              Map<String, Map<String, Integer>> componentMappingVersionMap) {

        // get relevant versions
        Map<VersionType, Long> dossierTemplateVersions = dossierTemplateVersionMap.computeIfAbsent(dossier.getDossierTemplateId(),
                                                                                                   k -> buildVersionData(dossier.getDossierTemplateId()));

        Map<String, Integer> componentMappingVersions = componentMappingVersionMap.computeIfAbsent(dossier.getDossierTemplateId(),
                                                                                                   k -> componentMappingService.getMetaDataByDossierTemplateId(dossier.getDossierTemplateId())
                                                                                                           .stream()
                                                                                                           .collect(Collectors.toMap(ComponentMappingMetadata::getName,
                                                                                                                                     ComponentMappingMetadata::getVersion)));

        Long dossierDictionaryVersion = dossierVersionMap.computeIfAbsent(fileStatus.getDossierId(), k -> getDossierVersionData(fileStatus.getDossierId()));

        // compute matches
        var mappingVersionAllMatch = mappingVersionsEqual(fileStatus, componentMappingVersions);
        var rulesVersionMatches = fileStatus.getRulesVersion() == dossierTemplateVersions.getOrDefault(RULES, -1L);
        var componentRulesVersionMatches = fileStatus.getComponentRulesVersion() == dossierTemplateVersions.getOrDefault(COMPONENT_RULES, -1L);
        var dateFormatsVersionMatches = fileStatus.getDateFormatsVersion() == dossierTemplateVersions.getOrDefault(DATE_FORMATS, -1L);
        var dictionaryVersionMatches = fileStatus.getDictionaryVersion() == dossierTemplateVersions.getOrDefault(DICTIONARY, -1L);
        var legalBasisVersionMatches = fileStatus.getLegalBasisVersion() == dossierTemplateVersions.getOrDefault(LEGAL_BASIS, -1L);
        var aiVersionMatches = fileStatus.getAiCreationVersion() == dossierTemplateVersions.getOrDefault(AI_CREATION, 0L);
        var dossierDictionaryVersionMatches = Math.max(fileStatus.getDossierDictionaryVersion(), 0) == dossierDictionaryVersion;

        var reanalysisRequired = !dictionaryVersionMatches || !dossierDictionaryVersionMatches || !mappingVersionAllMatch;
        var fullAnalysisRequired = !rulesVersionMatches || !componentRulesVersionMatches || !legalBasisVersionMatches || (settings.isLlmNerServiceEnabled() && !aiVersionMatches);
        var componentReanalysisRequired = !dateFormatsVersionMatches;

        if (reanalysisRequired || fullAnalysisRequired || componentReanalysisRequired) {

            log.debug(buildMessage(fileStatus,
                                  rulesVersionMatches,
                                  dossierTemplateVersions,
                                  componentRulesVersionMatches,
                                  dateFormatsVersionMatches,
                                  dictionaryVersionMatches,
                                  legalBasisVersionMatches,
                                  dossierDictionaryVersionMatches,
                                  dossierDictionaryVersion,
                                  mappingVersionAllMatch,
                                  componentMappingVersions,
                                  aiVersionMatches));
        }
        return new AnalysisRequiredResult(reanalysisRequired, fullAnalysisRequired, componentReanalysisRequired);

    }


    private String buildMessage(FileModel fileStatus,
                                boolean rulesVersionMatches,
                                Map<VersionType, Long> dossierTemplateVersions,
                                boolean componentRulesVersionMatches,
                                boolean dateFormatsVersionMatches,
                                boolean dictionaryVersionMatches,
                                boolean legalBasisVersionMatches,
                                boolean dossierDictionaryVersionMatches,
                                Long dossierDictionaryVersion,
                                boolean mappingVersionAllMatch,
                                Map<String, Integer> componentMappingVersions,
                                boolean aiVersionMatches) {

        StringBuilder messageBuilder = new StringBuilder();
        messageBuilder.append("For file: ").append(fileStatus.getId()).append("-").append(fileStatus.getFilename()).append(" analysis is required because -> ");

        boolean needComma = false;

        if (!rulesVersionMatches) {
            messageBuilder.append("ruleVersions: ").append(fileStatus.getRulesVersion()).append("/").append(dossierTemplateVersions.getOrDefault(RULES, -1L));
            needComma = true;
        }

        if (!componentRulesVersionMatches) {
            if (needComma) {
                messageBuilder.append(", ");
            }
            messageBuilder.append("componentRuleVersions: ")
                    .append(fileStatus.getComponentRulesVersion())
                    .append("/")
                    .append(dossierTemplateVersions.getOrDefault(COMPONENT_RULES, -1L));
            needComma = true;
        }

        if (!dictionaryVersionMatches) {
            if (needComma) {
                messageBuilder.append(", ");
            }
            messageBuilder.append("dictionaryVersions: ").append(fileStatus.getDictionaryVersion()).append("/").append(dossierTemplateVersions.getOrDefault(DICTIONARY, -1L));
            needComma = true;
        }

        if (!legalBasisVersionMatches) {
            if (needComma) {
                messageBuilder.append(", ");
            }
            messageBuilder.append("legalBasisVersions: ").append(fileStatus.getLegalBasisVersion()).append("/").append(dossierTemplateVersions.getOrDefault(LEGAL_BASIS, -1L));
            needComma = true;
        }

        if (!dossierDictionaryVersionMatches) {
            if (needComma) {
                messageBuilder.append(", ");
            }
            messageBuilder.append("dossierDictionaryVersions: ").append(Math.max(fileStatus.getDossierDictionaryVersion(), 0)).append("/").append(dossierDictionaryVersion);
            needComma = true;
        }

        if (!mappingVersionAllMatch) {
            if (needComma) {
                messageBuilder.append(", ");
            }
            messageBuilder.append("componentMappingVersions: ").append(buildMappingVersionMatchesString(fileStatus.getComponentMappingVersions(), componentMappingVersions));
            needComma = true;
        }

        if (!dateFormatsVersionMatches) {
            if (needComma) {
                messageBuilder.append(", ");
            }
            messageBuilder.append("dateFormatsVersions: ").append(fileStatus.getDateFormatsVersion()).append("/").append(dossierTemplateVersions.getOrDefault(DATE_FORMATS, -1L));
            needComma = true;
        }

        if (settings.isLlmNerServiceEnabled() && !aiVersionMatches) {
            if (needComma) {
                messageBuilder.append(", ");
            }
            messageBuilder.append("aiVersions: ").append(fileStatus.getAiCreationVersion()).append("/").append(dossierTemplateVersions.getOrDefault(AI_CREATION, 0L));
        }

        return messageBuilder.toString();
    }


    private String buildMappingVersionMatchesString(Map<String, Integer> fileComponentMappingVersions, Map<String, Integer> dbComponentMappingVersions) {

        Set<String> allMappingNames = new HashSet<>();
        allMappingNames.addAll(fileComponentMappingVersions.keySet());
        allMappingNames.addAll(dbComponentMappingVersions.keySet());

        StringBuilder sb = new StringBuilder();
        allMappingNames.stream()
                .sorted()
                .forEach(mappingName -> {
                    long fileVersion = fileComponentMappingVersions.getOrDefault(mappingName, -1);
                    long dbVersion = dbComponentMappingVersions.getOrDefault(mappingName, -1);
                    if (fileVersion != dbVersion) {
                        sb.append(mappingName).append(": ").append(fileVersion).append("/").append(dbVersion);
                    }
                });
        return sb.toString();
    }


    private static boolean mappingVersionsEqual(FileModel fileStatus, Map<String, Integer> componentMappingVersions) {

        return fileStatus.getComponentMappingVersions().keySet().equals(componentMappingVersions.keySet()) && fileStatus.getComponentMappingVersions().keySet()
                .stream()
                .allMatch(name -> fileStatus.getComponentMappingVersions()
                        .get(name).equals(componentMappingVersions.get(name)));
    }


    private Map<VersionType, Long> buildVersionData(String dossierTemplateId) {

        var versions = new HashMap<VersionType, Long>();

        versions.put(RULES, rulesPersistenceService.getVersion(dossierTemplateId, RuleFileType.ENTITY));
        versions.put(COMPONENT_RULES, rulesPersistenceService.getVersion(dossierTemplateId, RuleFileType.COMPONENT));
        versions.put(DATE_FORMATS, dateFormatsPersistenceService.getVersion(dossierTemplateId));
        versions.put(DICTIONARY, dictionaryPersistenceService.getVersion(dossierTemplateId));
        versions.put(LEGAL_BASIS, legalBasisMappingPersistenceService.getVersion(dossierTemplateId));
        versions.put(AI_CREATION,
                     dictionaryPersistenceService.getTypesForAiCreation(dossierTemplateId)
                             .stream()
                             .mapToLong(TypeEntity::getAiCreationVersion).sum());

        return versions;
    }


    private Long getDossierVersionData(String dossierId) {

        return dictionaryPersistenceService.getVersionForDossier(dossierId);
    }


    public FileModel enhanceFileStatusWithAnalysisRequirements(FileModel fileModel, boolean ignoreProcessingStates) {

        return enhanceFileStatusWithAnalysisRequirements(Collections.singletonList(fileModel), ignoreProcessingStates).iterator().next();
    }


    public List<FileModel> enhanceFileStatusWithAnalysisRequirements(List<FileModel> fileModels) {

        return enhanceFileStatusWithAnalysisRequirements(fileModels, false);
    }


    public enum VersionType {
        RULES,
        COMPONENT_RULES,
        DATE_FORMATS,
        AI_CREATION,
        DICTIONARY,
        LEGAL_BASIS
    }

    @Data
    @AllArgsConstructor
    public static class AnalysisRequiredResult {

        private boolean reanalysisRequired;
        private boolean fullAnalysisRequired;
        private boolean componentReanalysisRequired;


        public AnalysisRequiredResult(boolean reanalysisRequired, boolean fullAnalysisRequired) {

            this.reanalysisRequired = reanalysisRequired;
            this.fullAnalysisRequired = fullAnalysisRequired;
            this.componentReanalysisRequired = false;
        }

    }

}
