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

import static com.iqser.red.service.persistence.management.v1.processor.roles.ActionRoles.ADD_DICTIONARY_ENTRY;
import static com.iqser.red.service.persistence.management.v1.processor.roles.ActionRoles.ADD_DOSSIER_DICTIONARY_ENTRY;
import static com.iqser.red.service.persistence.management.v1.processor.roles.ActionRoles.ADD_UPDATE_DICTIONARY_TYPE;
import static com.iqser.red.service.persistence.management.v1.processor.roles.ActionRoles.ADD_UPDATE_DOSSIER_DICTIONARY_TYPE;
import static com.iqser.red.service.persistence.management.v1.processor.roles.ActionRoles.DELETE_DICTIONARY_ENTRY;
import static com.iqser.red.service.persistence.management.v1.processor.roles.ActionRoles.DELETE_DICTIONARY_TYPE;
import static com.iqser.red.service.persistence.management.v1.processor.roles.ActionRoles.DELETE_DOSSIER_DICTIONARY_ENTRY;
import static com.iqser.red.service.persistence.management.v1.processor.roles.ActionRoles.DELETE_DOSSIER_DICTIONARY_TYPE;
import static com.iqser.red.service.persistence.management.v1.processor.roles.ActionRoles.READ_COLORS;
import static com.iqser.red.service.persistence.management.v1.processor.roles.ActionRoles.READ_DICTIONARY_TYPES;
import static com.iqser.red.service.persistence.management.v1.processor.roles.ActionRoles.WRITE_COLORS;
import static com.iqser.red.service.persistence.management.v1.processor.utils.TypeIdUtils.toTypeId;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import org.apache.commons.collections4.CollectionUtils;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;

import com.amazonaws.util.StringUtils;
import com.iqser.red.service.dictionarymerge.commons.CommonsDictionaryModel;
import com.iqser.red.service.dictionarymerge.commons.DictionaryEntry;
import com.iqser.red.service.dictionarymerge.commons.DictionaryEntryModel;
import com.iqser.red.service.dictionarymerge.commons.DictionaryMergeService;
import com.iqser.red.service.persistence.management.v1.processor.entity.configuration.ColorsEntity;
import com.iqser.red.service.persistence.management.v1.processor.entity.configuration.TypeEntity;
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.ColorUtils;
import com.knecon.fforesight.databasetenantcommons.providers.utils.MagicConverter;
import com.iqser.red.service.persistence.service.v1.api.shared.model.CreateTypeValue;
import com.iqser.red.service.persistence.service.v1.api.shared.model.Dictionary;
import com.iqser.red.service.persistence.service.v1.api.shared.model.TypeResponse;
import com.iqser.red.service.persistence.service.v1.api.shared.model.TypeValue;
import com.iqser.red.service.persistence.service.v1.api.shared.model.UpdateTypeValue;
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.configuration.Colors;
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 lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service
@RequiredArgsConstructor
public class DictionaryService {

    private final DictionaryManagementService dictionaryManagementService;

    private final ColorsService colorsService;
    private final DictionaryPersistenceService dictionaryPersistenceService;
    private final AccessControlService accessControlService;
    private final EntryPersistenceService entryPersistenceService;
    private final DictionaryMergeService dictionaryMergeService;
    private final DossierService dossierService;


    @PreAuthorize("hasAuthority('" + ADD_DICTIONARY_ENTRY + "')")
    public void addGlobalEntries(String type, String dossierTemplateId, List<String> entries, boolean removeCurrent, DictionaryEntryType dictionaryEntryType) {

        var typeId = toTypeId(type, dossierTemplateId);
        dictionaryManagementService.validateAddRemoveToDossierTemplateDictionary(typeId);
        addEntries(typeId, entries, removeCurrent, dictionaryEntryType);
    }


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

        dictionaryManagementService.addEntries(typeId, entries, removeCurrent, false, dictionaryEntryType);
    }


    @PreAuthorize("hasAuthority('" + ADD_DOSSIER_DICTIONARY_ENTRY + "')")
    public void addDossierEntries(String type, String dossierTemplateId, List<String> entries, boolean removeCurrent, String dossierId, DictionaryEntryType dictionaryEntryType) {

        addEntries(toTypeId(type, dossierTemplateId, dossierId), entries, removeCurrent, dictionaryEntryType);
    }


    @PreAuthorize("hasAuthority('" + DELETE_DICTIONARY_ENTRY + "')")
    public void deleteGlobalEntries(String type, String dossierTemplateId, List<String> entries, DictionaryEntryType dictionaryEntryType) {

        var typeId = toTypeId(type, dossierTemplateId);
        dictionaryManagementService.validateAddRemoveToDossierTemplateDictionary(typeId);
        deleteEntries(toTypeId(type, dossierTemplateId), entries, dictionaryEntryType);

    }


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

        dictionaryManagementService.deleteEntries(typeId, entries, dictionaryEntryType);
    }


    @PreAuthorize("hasAuthority('" + DELETE_DOSSIER_DICTIONARY_ENTRY + "')")
    public void deleteDossierEntries(String type, String dossierTemplateId, List<String> entries, String dossierId, DictionaryEntryType dictionaryEntryType) {

        try {
            accessControlService.verifyUserHasAccessPermissions(dossierId);
            accessControlService.verifyUserIsMemberOrApprover(dossierId);
        } catch (AccessDeniedException e) {
            throw new NotFoundException("Object not found");
        }
        deleteEntries(toTypeId(type, dossierTemplateId, dossierId), entries, dictionaryEntryType);
    }


    @PreAuthorize("hasAuthority('" + ADD_UPDATE_DICTIONARY_TYPE + "')")
    public void updateGlobalType(String type, String dossierTemplateId, UpdateTypeValue typeValue) {

        updateType(dossierTemplateId, toTypeId(type, dossierTemplateId), typeValue);
        var dossierTypes = dictionaryPersistenceService.getAllDossierTypesForDossierTemplateAndType(dossierTemplateId, type, false);
        dossierTypes.forEach(t -> this.updateDossierType(t.getType(), dossierTemplateId, typeValue, t.getDossierId()));
    }


    private void updateType(String dossierTemplateId, String typeId, UpdateTypeValue typeValue) {

        dictionaryManagementService.updateTypeValue(typeId,
                Type.builder()
                        .dossierTemplateId(dossierTemplateId)
                        .hexColor(typeValue.getHexColor())
                        .recommendationHexColor(typeValue.getRecommendationHexColor())
                        .skippedHexColor(typeValue.getSkippedHexColor())
                        .rank(typeValue.getRank())
                        .isHint(typeValue.isHint())
                        .isCaseInsensitive(typeValue.isCaseInsensitive())
                        .isRecommendation(typeValue.isRecommendation())
                        .description(typeValue.getDescription())
                        .addToDictionaryAction(typeValue.isAddToDictionaryAction())
                        .label(typeValue.getLabel())
                        .hasDictionary(typeValue.isHasDictionary())
                        .autoHideSkipped(typeValue.isAutoHideSkipped())
                        .build());
    }


    @PreAuthorize("hasAuthority('" + ADD_UPDATE_DOSSIER_DICTIONARY_TYPE + "')")
    public void updateDossierType(String type, String dossierTemplateId, UpdateTypeValue typeValue, String dossierId) {

        accessControlService.verifyUserIsDossierOwner(dossierId);
        updateType(dossierTemplateId, toTypeId(type, dossierTemplateId, dossierId), typeValue);
    }


    @PreAuthorize("hasAuthority('" + ADD_UPDATE_DICTIONARY_TYPE + "')")
    public Type addGlobalType(CreateTypeValue typeValue) {

        return addType(typeValue, null);
    }


    private Type addType(CreateTypeValue typeValue, String dossierId) {

        return dictionaryManagementService.addType(Type.builder()
                .dossierTemplateId(typeValue.getDossierTemplateId())
                .dossierId(dossierId)
                .type(typeValue.getType())
                .hexColor(typeValue.getHexColor())
                .recommendationHexColor(typeValue.getRecommendationHexColor())
                .skippedHexColor(typeValue.getSkippedHexColor())
                .rank(typeValue.getRank())
                .isHint(typeValue.isHint())
                .isCaseInsensitive(typeValue.isCaseInsensitive())
                .isRecommendation(typeValue.isRecommendation())
                .description(typeValue.getDescription())
                .addToDictionaryAction(typeValue.isAddToDictionaryAction())
                .label(typeValue.getLabel())
                .hasDictionary(typeValue.isHasDictionary())
                .systemManaged(false)
                .autoHideSkipped(typeValue.isAutoHideSkipped())
                .dossierDictionaryOnly(typeValue.isDossierDictionaryOnly())
                .build());
    }


    @PreAuthorize("hasAuthority('" + DELETE_DICTIONARY_TYPE + "')")
    public void deleteGlobalType(String type, String dossierTemplateId) {

        deleteType(toTypeId(type, dossierTemplateId));
        // delete the dossier types with the same type
        var dossierTypes = dictionaryPersistenceService.getAllDossierTypesForDossierTemplateAndType(dossierTemplateId, type, false);
        dossierTypes.forEach(t -> this.deleteDossierType(t.getType(), dossierTemplateId, t.getDossierId()));
    }


    public void deleteType(String typeId) {

        dictionaryManagementService.deleteType(typeId);
    }


    @PreAuthorize("hasAuthority('" + DELETE_DOSSIER_DICTIONARY_TYPE + "')")
    public void deleteDossierType(String type, String dossierTemplateId, String dossierId) {

        accessControlService.verifyUserHasAccessPermissions(dossierId);
        accessControlService.verifyUserIsMemberOrApprover(dossierId);
        deleteType(toTypeId(type, dossierTemplateId, dossierId));
    }


    @PreAuthorize("hasAuthority('" + READ_DICTIONARY_TYPES + "')")
    public TypeResponse getAllTypes(String dossierTemplateId, String dossierId, boolean includeDeleted) {

        List<Type> types = MagicConverter.convert(dictionaryPersistenceService.getAllTypesForDossierTemplate(dossierTemplateId, includeDeleted), Type.class);
        if (dossierId != null) {
            try {
                accessControlService.verifyUserHasViewPermissions(dossierId);
                dictionaryManagementService.checkDossierMatchesDossierTemplate(dossierId, dossierTemplateId);
                // for every dossier template type check if a dossier type exists
                types.forEach(t -> dictionaryManagementService.checkForDossierTypeExistenceAndCreate(toTypeId(t.getType(), t.getDossierTemplateId(), dossierId)));
                types.addAll(MagicConverter.convert(dictionaryPersistenceService.getAllTypesForDossier(dossierId, includeDeleted), Type.class));
            } catch (AccessDeniedException e) {
                log.debug(" Don't include the types for the dossier id");
            }
        }

        List<TypeValue> typeValues = types.stream()
                .map(typeResult -> TypeValue.builder()
                        .type(typeResult.getType())
                        .typeId(typeResult.getId())
                        .hexColor(typeResult.getHexColor())
                        .recommendationHexColor(typeResult.getRecommendationHexColor())
                        .skippedHexColor(typeResult.getSkippedHexColor())
                        .rank(typeResult.getRank())
                        .dossierTemplateId(typeResult.getDossierTemplateId())
                        .hint(typeResult.isHint())
                        .caseInsensitive(typeResult.isCaseInsensitive())
                        .recommendation(typeResult.isRecommendation())
                        .description(typeResult.getDescription())
                        .addToDictionaryAction(typeResult.isAddToDictionaryAction())
                        .label(typeResult.getLabel())
                        .hasDictionary(typeResult.isHasDictionary())
                        .systemManaged(typeResult.isSystemManaged())
                        .autoHideSkipped(typeResult.isAutoHideSkipped())
                        .dossierDictionaryOnly(typeResult.isDossierDictionaryOnly())
                        .build())
                .collect(Collectors.toList());
        return new TypeResponse(typeValues);
    }


    @PreAuthorize("hasAuthority('" + READ_DICTIONARY_TYPES + "')")
    public Dictionary getDictionaryForType(String type, String dossierTemplateId, String dossierId) {

        try {
            if (dossierId != null) {
                accessControlService.verifyUserHasViewPermissions(dossierId);
            }
            var typeId = toTypeId(type, dossierTemplateId, dossierId);
            // create dossier level type if it does not exist
            dictionaryManagementService.checkForDossierTypeExistenceAndCreate(typeId);
            var entity = dictionaryPersistenceService.getType(typeId);
            var dictionaryForType = MagicConverter.convert(entity, Type.class);
            dictionaryForType.setEntries(MagicConverter.convert(entryPersistenceService.getEntries(typeId, DictionaryEntryType.ENTRY, null), DictionaryEntry.class));
            dictionaryForType.setFalsePositiveEntries(MagicConverter.convert(entryPersistenceService.getEntries(typeId, DictionaryEntryType.FALSE_POSITIVE, null),
                    DictionaryEntry.class));
            dictionaryForType.setFalseRecommendationEntries(MagicConverter.convert(entryPersistenceService.getEntries(typeId, DictionaryEntryType.FALSE_RECOMMENDATION, null),
                    DictionaryEntry.class));

            return Dictionary.builder()
                    .entries(dictionaryForType.getEntries()
                            .stream()
                            .filter(e -> !e.isDeleted())
                            .map(DictionaryEntry::getValue)
                            .sorted(Comparator.comparing(StringUtils::lowerCase))
                            .collect(Collectors.toList()))
                    .falsePositiveEntries(dictionaryForType.getFalsePositiveEntries()
                            .stream()
                            .filter(e -> !e.isDeleted())
                            .map(DictionaryEntry::getValue)
                            .sorted(Comparator.comparing(StringUtils::lowerCase))
                            .collect(Collectors.toList()))
                    .falseRecommendationEntries(dictionaryForType.getFalseRecommendationEntries()
                            .stream()
                            .filter(e -> !e.isDeleted())
                            .map(DictionaryEntry::getValue)
                            .sorted(Comparator.comparing(StringUtils::lowerCase))
                            .collect(Collectors.toList()))
                    .hexColor(dictionaryForType.getHexColor())
                    .recommendationHexColor(dictionaryForType.getRecommendationHexColor())
                    .skippedHexColor(dictionaryForType.getSkippedHexColor())
                    .dossierTemplateId(dossierTemplateId)
                    .rank(dictionaryForType.getRank())
                    .hint(dictionaryForType.isHint())
                    .caseInsensitive(dictionaryForType.isCaseInsensitive())
                    .recommendation(dictionaryForType.isRecommendation())
                    .description(dictionaryForType.getDescription())
                    .addToDictionaryAction(dictionaryForType.isAddToDictionaryAction())
                    .label(dictionaryForType.getLabel())
                    .hasDictionary(dictionaryForType.isHasDictionary())
                    .systemManaged(dictionaryForType.isSystemManaged())
                    .autoHideSkipped(dictionaryForType.isAutoHideSkipped())
                    .dossierDictionaryOnly(dictionaryForType.isDossierDictionaryOnly())
                    .build();
        } catch (AccessDeniedException e) {
            throw new NotFoundException("Object not found");
        }
    }

    @PreAuthorize("hasAuthority('" + READ_DICTIONARY_TYPES + "')")
    public Dictionary getMergedDictionaryForType(String type, String dossierTemplateId, String dossierId) {

        try {
            if (dossierId != null) {
                accessControlService.verifyUserHasViewPermissions(dossierId);
            }
            var dossierTemplateDictionary = dictionaryPersistenceService.getType(toTypeId(type, dossierTemplateId));
            var typeId = toTypeId(type, dossierTemplateId, dossierId);
            // create dossier level type if it does not exist
            dictionaryManagementService.checkForDossierTypeExistenceAndCreate(typeId);
            var dossierDictionary = dictionaryPersistenceService.getType(typeId);

            var mergedDictionaries = dictionaryMergeService.getMergedDictionary(convertType(dossierTemplateDictionary), convertType(dossierDictionary));
            return convertMergedDictionaries(mergedDictionaries, dossierTemplateId, dossierId);
        } catch (AccessDeniedException e) {
            throw new NotFoundException("Object not found");
        }
    }

    private Dictionary convertMergedDictionaries (List<CommonsDictionaryModel> commonsDictionaries, String dossierTemplateId, String dossierId) {
        List<Dictionary> dictionaries = new ArrayList<>();
        commonsDictionaries.forEach(cdm -> {
            var typeId = toTypeId(cdm.getType(), dossierTemplateId, dossierId);
            var entity = dictionaryPersistenceService.getType(typeId);
            Dictionary dict = Dictionary.builder()
                    .entries(cdm.getEntries()
                            .stream()
                            .filter(e -> !e.isDeleted())
                            .map(DictionaryEntry::getValue)
                            .sorted(Comparator.comparing(StringUtils::lowerCase))
                            .collect(Collectors.toList()))
                    .falsePositiveEntries(cdm.getFalsePositives()
                            .stream()
                            .filter(e -> !e.isDeleted())
                            .map(DictionaryEntry::getValue)
                            .sorted(Comparator.comparing(StringUtils::lowerCase))
                            .collect(Collectors.toList()))
                    .falseRecommendationEntries(cdm.getFalseRecommendations()
                            .stream()
                            .filter(e -> !e.isDeleted())
                            .map(DictionaryEntry::getValue)
                            .sorted(Comparator.comparing(StringUtils::lowerCase))
                            .collect(Collectors.toList()))
                    .hexColor(entity.getHexColor())
                    .recommendationHexColor(entity.getRecommendationHexColor())
                    .skippedHexColor(entity.getSkippedHexColor())
                    .dossierTemplateId(dossierTemplateId)
                    .rank(cdm.getRank())
                    .hint(cdm.isHint())
                    .caseInsensitive(cdm.isCaseInsensitive())
                    .recommendation(entity.isRecommendation())
                    .description(entity.getDescription())
                    .addToDictionaryAction(entity.isAddToDictionaryAction())
                    .label(entity.getLabel())
                    .hasDictionary(entity.isHasDictionary())
                    .systemManaged(entity.isSystemManaged())
                    .autoHideSkipped(entity.isAutoHideSkipped())
                    .dossierDictionaryOnly(entity.isDossierDictionaryOnly())
                    .build();
            dictionaries.add(dict);
        });

        return dictionaries.get(0);
    }

    private List<CommonsDictionaryModel> convertType(TypeEntity typeEntity) {

        return List.of(CommonsDictionaryModel.builder()
                    .type(typeEntity.getType())
                    .rank(typeEntity.getRank())
                    .color(ColorUtils.convertColor(typeEntity.getHexColor()))
                    .caseInsensitive(typeEntity.isCaseInsensitive())
                    .hint(typeEntity.isHint())
                    .entries(getCommonsEntries(typeEntity.getId(), DictionaryEntryType.ENTRY))
                    .falsePositives(getCommonsEntries(typeEntity.getId(), DictionaryEntryType.FALSE_POSITIVE))
                    .falseRecommendations(getCommonsEntries(typeEntity.getId(), DictionaryEntryType.FALSE_RECOMMENDATION))
                    .build());
    }

    private Set<DictionaryEntryModel> getCommonsEntries(String typeId, DictionaryEntryType entryType) {
        var entries = MagicConverter.convert(entryPersistenceService.getEntries(typeId, entryType, null), DictionaryEntry.class);
        return CollectionUtils.isNotEmpty(entries) ? new HashSet<>(entries.stream()
                .map(DictionaryEntryModel::new)
                .collect(Collectors.toSet())) : new HashSet<>();
    }

    @PreAuthorize("hasAuthority('" + WRITE_COLORS + "')")
    public void setColors(String dossierTemplateId, Colors colors) {
        //validate all colors before setting them
        ColorUtils.validateColor(colors.getRequestAddColor());
        ColorUtils.validateColor(colors.getRequestRemoveColor());
        ColorUtils.validateColor(colors.getDictionaryRequestColor());
        ColorUtils.validateColor(colors.getPreviewColor());
        ColorUtils.validateColor(colors.getAnalysisColor());
        ColorUtils.validateColor(colors.getUpdatedColor());
        ColorUtils.validateColor(colors.getRecommendationColor());
        ColorUtils.validateColor(colors.getHintColor());
        ColorUtils.validateColor(colors.getRedactionColor());
        ColorUtils.validateColor(colors.getIgnoredHintColor());
        ColorUtils.validateColor(colors.getSkippedColor());
        ColorUtils.validateColor(colors.getAppliedRedactionColor());

        var colorsEntity = MagicConverter.convert(colors, ColorsEntity.class);
        colorsEntity.setDossierTemplateId(dossierTemplateId);
        colorsService.saveColors(colorsEntity);
    }


    @PreAuthorize("hasAuthority('" + READ_COLORS + "')")
    public Colors getColors(String dossierTemplateId) {

        return MagicConverter.convert(colorsService.getColors(dossierTemplateId), Colors.class);
    }


}
