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

import com.iqser.red.service.dictionarymerge.commons.DictionaryEntry;
import com.iqser.red.service.persistence.management.v1.processor.entity.configuration.BaseDictionaryEntry;
import com.iqser.red.service.persistence.management.v1.processor.entity.configuration.TypeEntity;
import com.iqser.red.service.persistence.management.v1.processor.exception.BadRequestException;
import com.iqser.red.service.persistence.management.v1.processor.exception.ConflictException;
import com.iqser.red.service.persistence.management.v1.processor.exception.NotAllowedException;
import com.iqser.red.service.persistence.management.v1.processor.exception.NotFoundException;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.DictionaryPersistenceService;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.EntryPersistenceService;
import com.iqser.red.service.persistence.management.v1.processor.utils.TextNormalizationUtilities;
import com.iqser.red.service.persistence.management.v1.processor.utils.TypeMapper;
import com.iqser.red.service.persistence.management.v1.processor.validation.DictionaryValidator;
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.dossiertemplate.type.Type;
import com.knecon.fforesight.databasetenantcommons.providers.utils.MagicConverter;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;

import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static com.iqser.red.service.persistence.management.v1.processor.utils.TypeIdUtils.*;
import static java.util.stream.Collectors.toSet;

@Slf4j
@Service
@RequiredArgsConstructor
public class DictionaryManagementService {

    private final EntryPersistenceService entryPersistenceService;
    private final DictionaryPersistenceService dictionaryPersistenceService;
    private final ColorsService colorsService;
    private final StopwordService stopwordService;
    private final DossierService dossierService;


    public List<DictionaryEntry> getAllEntriesInDossierTemplate(String dossierTemplateTypeId, String value, DictionaryEntryType dictionaryEntryType){
        return MagicConverter.convert(entryPersistenceService.getAllEntriesInDossierTemplate(dossierTemplateTypeId, value, dictionaryEntryType),DictionaryEntry.class);
    }


    public Type addType(Type typeRequest) {

        if (typeRequest.getDossierTemplateId() == null) {
            throw new BadRequestException("Dossier template id does not exist.");
        }

        if (typeRequest.getLabel() == null || typeRequest.getLabel().isEmpty()) {
            String label = humanizedDictionaryType(typeRequest.getType());
            typeRequest.setLabel(label);
        }

        checkForDuplicateLabels(typeRequest.getDossierTemplateId(), typeRequest.getDossierId(), typeRequest.getType(), typeRequest.getLabel());

        if (this.checkForExistingType(typeRequest)) {
            throw new ConflictException("The type already exists, could not be added again.");
        }
        String color = typeRequest.getHexColor();
        validateColor(color);

        String skippedHexColor = typeRequest.getSkippedHexColor();
        if (StringUtils.isBlank(skippedHexColor)) { //use the default value
            skippedHexColor = colorsService.getColors(typeRequest.getDossierTemplateId()).getSkippedColor();
        } else {
            validateColor(typeRequest.getSkippedHexColor());
        }

        String recommendationHexColor = typeRequest.getRecommendationHexColor();
        if (StringUtils.isBlank(recommendationHexColor)) { //use the default value
            recommendationHexColor = colorsService.getColors(typeRequest.getDossierTemplateId()).getRecommendationColor();
        } else {
            validateColor(typeRequest.getRecommendationHexColor());
        }

        return MagicConverter.convert(dictionaryPersistenceService.addType(typeRequest.getType(),
                typeRequest.getDossierTemplateId(),
                color,
                recommendationHexColor,
                skippedHexColor,
                typeRequest.getRank(),
                typeRequest.isHint(),
                typeRequest.isCaseInsensitive(),
                typeRequest.isRecommendation(),
                typeRequest.getDescription(),
                typeRequest.isAddToDictionaryAction(),
                typeRequest.getLabel(),
                typeRequest.getDossierId(),
                typeRequest.isHasDictionary(),
                typeRequest.isSystemManaged(),
                typeRequest.isAutoHideSkipped(),
                typeRequest.isDossierDictionaryOnly()), Type.class, new TypeMapper());
    }


    private String humanizedDictionaryType(String label) {

        String str = label;
        str = str.replaceAll("-+?", " ");
        str = str.replaceAll("_+?", " ");
        str = str.replaceAll(" +", " ");

        StringBuilder strbf = new StringBuilder();
        Matcher match = Pattern.compile("([a-z])([a-z]*)", Pattern.CASE_INSENSITIVE).matcher(str);
        while (match.find()) {
            match.appendReplacement(strbf, match.group(1).toUpperCase() + match.group(2));
        }
        return match.appendTail(strbf).toString();

    }


    private void checkForDuplicateLabels(String dossierTemplateId, String dossierId, String type, String labelToCheck) {

        List<TypeEntity> typeResponse = dictionaryPersistenceService.getCumulatedTypes(dossierTemplateId, dossierId, false);
        for (TypeEntity res : typeResponse) {
            var dossierTemplateResponse = res.getDossierTemplateId() == null ? res.getDossierTemplate().getId() : res.getDossierTemplateId();
            if (res.getDossierId() != null && res.getDossierId()
                    .equals(dossierId) && !type.equals(res.getType()) && dossierTemplateResponse.equals(dossierTemplateId) && labelToCheck.equals(res.getLabel()) || !type.equals(
                    res.getType()) && dossierTemplateResponse.equals(dossierTemplateId) && labelToCheck.equals(res.getLabel())) {
                throw new ConflictException("Label must be unique.");
            }
        }
    }


    public boolean checkForExistingType(Type typeRequest) {

        return dictionaryPersistenceService.getCumulatedTypes(typeRequest.getDossierTemplateId(), typeRequest.getDossierId(), false)
                .stream()
                .anyMatch(typeResult -> typeRequest.getDossierId() != null && typeResult.getDossierId() != null && typeRequest.getDossierId()
                        .equals(typeResult.getDossierId()) && typeRequest.getType().equals(typeResult.getType()) && typeRequest.getDossierTemplateId()
                        .equals(typeResult.getDossierTemplateId()) || typeRequest.getDossierId() == null && typeRequest.getType()
                        .equals(typeResult.getType()) && typeRequest.getDossierTemplateId().equals(typeResult.getDossierTemplateId()));
    }


    private void validateColor(String hexColor) {

        Optional<String> errorMessage = DictionaryValidator.validateColor(hexColor);
        if (errorMessage.isPresent()) {
            throw new BadRequestException(errorMessage.get());
        }

    }


    public void updateTypeValue(String typeId, Type typeRequest) {

        validateColor(typeRequest.getHexColor());
        validateBoolean(typeRequest.isHint(), "isHint");
        validateBoolean(typeRequest.isCaseInsensitive(), "isCaseInsensitive");
        String skippedHexColor = typeRequest.getSkippedHexColor();

        if (StringUtils.isBlank(skippedHexColor)) { //use the default value
            skippedHexColor = colorsService.getColors(typeRequest.getDossierTemplateId()).getSkippedColor();
            typeRequest.setSkippedHexColor(skippedHexColor);
        } else {
            validateColor(skippedHexColor);
        }

        String recommendationHexColor = typeRequest.getRecommendationHexColor();
        if (StringUtils.isBlank(recommendationHexColor)) { //use the default value
            recommendationHexColor = colorsService.getColors(typeRequest.getDossierTemplateId()).getRecommendationColor();
            typeRequest.setRecommendationHexColor(recommendationHexColor);
        } else {
            validateColor(typeRequest.getRecommendationHexColor());
        }

        // To check whether the type exists and it is not deleted
        Type typeResult = MagicConverter.convert(dictionaryPersistenceService.getType(typeId), Type.class);

        if (typeRequest.getLabel() != null) {
            checkForDuplicateLabels(typeResult.getDossierTemplateId(), typeResult.getDossierId(), typeResult.getType(), typeRequest.getLabel());
        } else {
            typeRequest.setLabel(typeResult.getLabel());
        }

        dictionaryPersistenceService.updateType(typeId, MagicConverter.convert(typeRequest, TypeEntity.class));

        if (typeResult.isHint() != typeRequest.isHint() || typeResult.isCaseInsensitive() != typeRequest.isCaseInsensitive() || typeResult.getRank() != typeRequest.getRank()) {

            var currentVersion = getCurrentVersion(typeResult);

            entryPersistenceService.setVersion(typeId, currentVersion + 1, DictionaryEntryType.ENTRY);
            entryPersistenceService.setVersion(typeId, currentVersion + 1, DictionaryEntryType.FALSE_POSITIVE);
            entryPersistenceService.setVersion(typeId, currentVersion + 1, DictionaryEntryType.FALSE_RECOMMENDATION);

        }

    }


    private void validateBoolean(Boolean bool, String name) {

        Optional<String> errorMessage = DictionaryValidator.validateBoolean(bool, name);
        if (errorMessage.isPresent()) {
            throw new BadRequestException(errorMessage.get());
        }
    }


    private long getCurrentVersion(Type typeResult) {

        long currentVersion;
        if (typeResult.getDossierId() != null) {
            currentVersion = dictionaryPersistenceService.getVersionForDossier(typeResult.getDossierId());
        } else {
            currentVersion = dictionaryPersistenceService.getVersion(typeResult.getDossierTemplateId());
        }
        return currentVersion;
    }


    @Transactional
    public void addEntries(String typeId, List<String> entries, boolean removeCurrent, boolean ignoreInvalidEntries, DictionaryEntryType dictionaryEntryType) {

        checkForDossierTypeExistenceAndCreate(typeId);

        Set<String> cleanEntries = entries.stream().map(this::cleanDictionaryEntry).collect(toSet());

        if (CollectionUtils.isEmpty(entries)) {
            throw new BadRequestException("Entry list is empty.");
        }

        var invalidEntries = getInvalidEntries(cleanEntries);

        if (!ignoreInvalidEntries && CollectionUtils.isNotEmpty(invalidEntries)) {
            throw new BadRequestException("Error(s) validating dictionary entries:\\n" + String.join("\\n", invalidEntries));
        } else {
            cleanEntries.removeAll(invalidEntries);
        }

        // To check whether the type exists, type should not be added into database implicitly by addEntry.
        Type typeResult = MagicConverter.convert(dictionaryPersistenceService.getType(typeId), Type.class);
        if (!typeResult.isHasDictionary()) {
            throw new BadRequestException("Entity type does not have a dictionary");
        }

        var currentVersion = getCurrentVersion(typeResult);

        if (removeCurrent) {
            entryPersistenceService.deleteAllEntriesForTypeId(typeId, currentVersion + 1, dictionaryEntryType);
            entryPersistenceService.addEntries(typeId, cleanEntries, currentVersion + 1, dictionaryEntryType);
        } else {
            entryPersistenceService.addEntries(typeId, cleanEntries, currentVersion + 1, dictionaryEntryType);
        }

        dictionaryPersistenceService.incrementVersion(typeId);
    }


    public void checkForDossierTypeExistenceAndCreate(String typeId) {
        // check for the existence of dossier type and create in case it does not exist
        if (isDossierTypeId(typeId)) {
            try {
                dictionaryPersistenceService.getType(typeId);
            } catch (NotFoundException e) {
                // type not found check first dossier is matching the specified dossier template
                var dossierId = getDossierIdFromTypeId(typeId);
                var dossierTemplateId = getDossierTemplateIdFromTypeId(typeId);
                log.info("--> " + dossierId + " - " + dossierTemplateId);
                checkDossierMatchesDossierTemplate(dossierId, dossierTemplateId);
                // type not found, it should be created based on the info from the dossier template type
                var dossierTemplateType = dictionaryPersistenceService.getType(getDosssierTemplateTypeIdFromTypeId(typeId));
                Type dossierDictionaryType = MagicConverter.convert(dossierTemplateType, Type.class);
                dossierDictionaryType.setVersion(0);
                dossierDictionaryType.setDossierId(dossierId);
                var returnedType = this.addType(dossierDictionaryType);
                log.info("Type added: " + returnedType.toString());
            }
        }
    }

    public void checkDossierMatchesDossierTemplate(String dossierId, String dossierTemplateId) {
        var dossier = dossierService.getDossierById(dossierId);
        if (!dossier.getDossierTemplateId().equals(dossierTemplateId)) {
            throw new BadRequestException(String.format("Dossier %s and dossier template %s mismatched", dossierId, dossierTemplateId));
        }
    }


    private String cleanDictionaryEntry(String entry) {

        return TextNormalizationUtilities.removeHyphenLineBreaks(entry).replaceAll("\\n", " ");
    }


    private Set<String> getInvalidEntries(Set<String> entries) {

        Predicate<String> isDictionaryEntryNotValid = entry -> DictionaryValidator.validateDictionaryEntry(entry).isPresent();
        Predicate<String> isStopword = stopwordService::isStopword;

        return entries.stream().filter(isDictionaryEntryNotValid.or(isStopword)).collect(toSet());
    }


    public void deleteEntries(String typeId, List<String> entries, DictionaryEntryType dictionaryEntryType) {
        checkForDossierTypeExistenceAndCreate(typeId);

        // To check whether the type exists
        Type typeResult = MagicConverter.convert(dictionaryPersistenceService.getType(typeId), Type.class);

        var currentVersion = getCurrentVersion(typeResult);

        List<String> allExistingEntiesFromType = entryPersistenceService.getEntries(typeId, dictionaryEntryType, null).stream().map(BaseDictionaryEntry::getValue).toList();

        if (typeResult.isCaseInsensitive()) {

            // Set existing entries to deleted.
            var caseInsensitiveExistingEntries = allExistingEntiesFromType.stream().filter(e -> entries.stream().anyMatch(e::equalsIgnoreCase)).collect(toSet());
            entryPersistenceService.deleteEntries(typeId,
                    caseInsensitiveExistingEntries,
                    currentVersion + 1,
                    dictionaryEntryType);

            // Create new deleted entries for not existing.
            var caseInsensitiveNonExistingEntries = entries.stream().filter(e -> caseInsensitiveExistingEntries.stream().noneMatch(e::equalsIgnoreCase)).collect(toSet());
            entryPersistenceService.addDeleteEntries(typeId, caseInsensitiveNonExistingEntries, currentVersion + 1, dictionaryEntryType);
        } else {

            // Set existing entries to deleted.
            var caseSensitiveExistingEntries = allExistingEntiesFromType.stream().filter(e -> entries.stream().anyMatch(e::equals)).collect(toSet());
            entryPersistenceService.deleteEntries(typeId, new HashSet<>(caseSensitiveExistingEntries), currentVersion + 1, dictionaryEntryType);

            // Create new deleted entries for not existing.
            var nonExistingEntries = entries.stream().filter(e -> caseSensitiveExistingEntries.stream().noneMatch(e::equals)).collect(toSet());
            entryPersistenceService.addDeleteEntries(typeId, nonExistingEntries, currentVersion + 1, dictionaryEntryType);
        }

        dictionaryPersistenceService.incrementVersion(typeId);
    }


    public void deleteType(String typeId) {

        // NotFoundException would be thrown if the type not found in database.
        Type typeResult = MagicConverter.convert(dictionaryPersistenceService.getType(typeId), Type.class);
        if (typeResult.isSystemManaged()) {
            throw new BadRequestException("Can not delete system managed entity type");
        }

        var currentVersion = getCurrentVersion(typeResult);

        dictionaryPersistenceService.deleteType(typeId);

        entryPersistenceService.deleteAllEntriesForTypeId(typeId, currentVersion + 1, DictionaryEntryType.ENTRY);
        entryPersistenceService.deleteAllEntriesForTypeId(typeId, currentVersion + 1, DictionaryEntryType.FALSE_POSITIVE);
        entryPersistenceService.deleteAllEntriesForTypeId(typeId, currentVersion + 1, DictionaryEntryType.FALSE_RECOMMENDATION);

        dictionaryPersistenceService.incrementVersion(typeId);
    }


    public long getCurrentVersion(String typeId) {

        Type typeResult = MagicConverter.convert(dictionaryPersistenceService.getType(typeId), Type.class);
        long currentVersion;
        if (typeResult.getDossierId() != null) {
            currentVersion = dictionaryPersistenceService.getVersionForDossier(typeResult.getDossierId());
        } else {
            currentVersion = dictionaryPersistenceService.getVersion(typeResult.getDossierTemplateId());
        }
        return currentVersion;
    }

    public void validateAddRemoveToDossierTemplateDictionary(String dossierTemplateTypeId) {
        this.validateAddRemoveToDossierTemplateDictionary(dossierTemplateTypeId, true, true);
    }

    public void validateAddRemoveToDossierTemplateDictionary(String dossierTemplateTypeId, boolean isAddRemoveToDictionary, boolean isAddRemoveToAllDossiers) {

        var type = dictionaryPersistenceService.getType(dossierTemplateTypeId);

        if (isAddRemoveToDictionary && isAddRemoveToAllDossiers && type.isDossierDictionaryOnly()) {
            throw new NotAllowedException("Adding / removing entry to dossier template dictionary is not allowed for this type: " + dossierTemplateTypeId);
        }

    }

}
