package com.iqser.red.service.persistence.service.v1.api.shared.mongo.service;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import org.springframework.stereotype.Service;

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.Position;
import com.iqser.red.service.persistence.service.v1.api.shared.mongo.document.EntityLogDocument;
import com.iqser.red.service.persistence.service.v1.api.shared.mongo.document.EntityLogEntryDocument;
import com.iqser.red.service.persistence.service.v1.api.shared.mongo.exception.DocumentNotFoundException;
import com.iqser.red.service.persistence.service.v1.api.shared.mongo.mapper.EntityLogDocumentMapper;
import com.iqser.red.service.persistence.service.v1.api.shared.mongo.repository.EntityLogDocumentRepository;
import com.iqser.red.service.persistence.service.v1.api.shared.mongo.repository.EntityLogEntryDocumentCustomRepository;
import com.iqser.red.service.persistence.service.v1.api.shared.mongo.repository.EntityLogEntryDocumentRepository;
import com.knecon.fforesight.tenantcommons.TenantContext;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class EntityLogMongoService {

    private final EntityLogDocumentRepository entityLogDocumentRepository;
    private final EntityLogEntryDocumentRepository entityLogEntryDocumentRepository;
    private final EntityLogDocumentUpdateService entityLogDocumentUpdateService;
    private final EntityLogEntryDocumentCustomRepository entityLogEntryDocumentCustomRepository;

    private final EntityLogDocumentMapper mapper = EntityLogDocumentMapper.INSTANCE;


    public void insertEntityLog(String dossierId, String fileId, EntityLog entityLog) {

        EntityLogDocument entityLogDocument = entityLogDocumentRepository.insert(mapper.toLogDocument(dossierId, fileId, entityLog));

        entityLogEntryDocumentRepository.insert(entityLog.getEntityLogEntry()
                                                        .stream()
                                                        .map(entityLogEntry -> mapper.toLogEntryDocument(entityLogDocument.getId(), entityLogEntry))
                                                        .toList());
    }


    public void saveEntityLog(String dossierId, String fileId, EntityLog entityLog) {

        EntityLogDocument entityLogDocument = entityLogDocumentRepository.save(mapper.toLogDocument(dossierId, fileId, entityLog));

        String tenantId = TenantContext.getTenantId();
        entityLog.getEntityLogEntry().parallelStream()
                .map(entityLogEntry -> mapper.toLogEntryDocument(entityLogDocument.getId(), entityLogEntry))
                .forEach(entity -> {
                    TenantContext.setTenantId(tenantId);
                    entityLogEntryDocumentRepository.save(entity);
                });
    }


    // this does everything : insert when not found and update if found
    public void upsertEntityLog(String dossierId, String fileId, EntityLog entityLog) {

        Optional<EntityLogDocument> optionalEntityLogDocument = entityLogDocumentRepository.findById(mapper.getLogId(dossierId, fileId));
        if (optionalEntityLogDocument.isEmpty()) {
            insertEntityLog(dossierId, fileId, entityLog);
            return;
        }

        EntityLogDocument oldEntityLogDocument = optionalEntityLogDocument.get();
        List<EntityLogEntryDocument> oldEntityLogEntryDocuments = oldEntityLogDocument.getEntityLogEntryDocuments();

        EntityLogDocument newEntityLogDocument = mapper.toLogDocument(dossierId, fileId, entityLog);
        List<EntityLogEntryDocument> newEntityLogEntryDocuments = newEntityLogDocument.getEntityLogEntryDocuments();

        List<EntityLogEntryDocument> toUpdate = new ArrayList<>(newEntityLogEntryDocuments);
        toUpdate.retainAll(oldEntityLogEntryDocuments);

        List<EntityLogEntryDocument> toRemove = new ArrayList<>(oldEntityLogEntryDocuments);
        toRemove.removeAll(toUpdate);

        List<EntityLogEntryDocument> toInsert = new ArrayList<>(newEntityLogEntryDocuments);
        toInsert.removeAll(toUpdate);

        entityLogEntryDocumentRepository.saveAll(toUpdate);
        entityLogEntryDocumentRepository.deleteAll(toRemove);
        entityLogEntryDocumentRepository.insert(toInsert);

        entityLogDocumentRepository.save(newEntityLogDocument);
    }


    public void deleteEntityLog(String dossierId, String fileId) {

        String entityLogId = mapper.getLogId(dossierId, fileId);

        entityLogDocumentRepository.deleteById(entityLogId);

        entityLogEntryDocumentRepository.deleteByEntityLogId(entityLogId);
    }


    public void insertEntityLogEntries(String dossierId, String fileId, List<EntityLogEntry> entityLogEntries) {

        String entityLogId = mapper.getLogId(dossierId, fileId);

        List<EntityLogEntryDocument> entityLogEntryDocuments = entityLogEntries.stream()
                .map(entityLogEntry -> mapper.toLogEntryDocument(entityLogId, entityLogEntry))
                .toList();

        entityLogEntryDocumentRepository.insert(entityLogEntryDocuments);

        entityLogDocumentUpdateService.addEntityLogEntryReferences(entityLogId, entityLogEntryDocuments);
    }


    public void saveEntityLogEntries(String dossierId, String fileId, List<EntityLogEntry> entityLogEntries) {

        String entityLogId = mapper.getLogId(dossierId, fileId);

        String tenantId = TenantContext.getTenantId();
        List<EntityLogEntryDocument> entityLogEntryDocuments = entityLogEntries.parallelStream()
                .map(entityLogEntry -> mapper.toLogEntryDocument(entityLogId, entityLogEntry))
                .peek(entity -> {
                    TenantContext.setTenantId(tenantId);
                    entityLogEntryDocumentRepository.save(entity);
                })
                .toList();

        entityLogDocumentUpdateService.addEntityLogEntryReferences(entityLogId, entityLogEntryDocuments);
    }


    public void updateEntityLogEntries(String dossierId, String fileId, List<EntityLogEntry> entityLogEntries) {

        String entityLogId = mapper.getLogId(dossierId, fileId);

        String tenantId = TenantContext.getTenantId();
        entityLogEntries.parallelStream()
                .map(entityLogEntry -> mapper.toLogEntryDocument(entityLogId, entityLogEntry))
                .forEach(entity -> {
                    TenantContext.setTenantId(tenantId);
                    entityLogEntryDocumentRepository.save(entity);
                });
    }


    public void deleteEntityLogEntries(String dossierId, String fileId, List<EntityLogEntry> entityLogEntries) {

        String entityLogId = mapper.getLogId(dossierId, fileId);

        String tenantId = TenantContext.getTenantId();
        List<EntityLogEntryDocument> entityLogEntryDocuments = entityLogEntries.parallelStream()
                .map(entityLogEntry -> mapper.toLogEntryDocument(entityLogId, entityLogEntry))
                .peek(entity -> {
                    TenantContext.setTenantId(tenantId);
                    entityLogEntryDocumentRepository.delete(entity);
                })
                .toList();

        entityLogDocumentUpdateService.removeEntityLogEntryReferences(entityLogId, entityLogEntryDocuments);
    }


    private EntityLogDocument getEntityLogDocument(String entityLogId) {

        Optional<EntityLogDocument> optionalEntityLogDocument = entityLogDocumentRepository.findById(entityLogId);

        if (optionalEntityLogDocument.isEmpty()) {
            throw new DocumentNotFoundException(String.format("Entity log not found for %s", entityLogId));
        }

        return optionalEntityLogDocument.get();
    }


    public Optional<EntityLog> findEntityLogByDossierIdAndFileId(String dossierId, String fileId) {

        return entityLogDocumentRepository.findById(mapper.getLogId(dossierId, fileId))
                .map(mapper::fromLogDocument);
    }


    public Optional<EntityLogEntry> findEntityLogEntryById(String dossierId, String fileId, String annotationId) {

        return entityLogEntryDocumentRepository.findEntityLogEntryDocumentByEntryId(mapper.getLogId(dossierId, fileId), annotationId)
                .map(mapper::fromLogEntryDocument);
    }


    public List<EntityLogEntry> findEntityLogEntriesByIds(String dossierId, String fileId, List<String> entryIds) {

        return entityLogEntryDocumentRepository.findEntityLogEntryDocumentsByEntryIds(mapper.getLogId(dossierId, fileId), entryIds)
                .stream()
                .map(mapper::fromLogEntryDocument)
                .collect(Collectors.toList());
    }


    public boolean entityLogDocumentExists(String dossierId, String fileId) {

        return entityLogDocumentRepository.existsById(mapper.getLogId(dossierId, fileId));
    }


    public Optional<Integer> findLatestAnalysisNumber(String dossierId, String fileId) {

        return entityLogDocumentRepository.findAnalysisNumberById(mapper.getLogId(dossierId, fileId))
                .map(EntityLogDocument::getAnalysisNumber);
    }


    public List<EntityLogEntry> findEntityLogEntriesWithManualChanges(String dossierId, String fileId) {

        return entityLogEntryDocumentRepository.findByEntityLogIdAndManualChangesNotEmpty(mapper.getLogId(dossierId, fileId))
                .stream()
                .map(mapper::fromLogEntryDocument)
                .toList();
    }


    public Set<Integer> findFirstContainingNodeIdForEachEntry(String dossierId, String fileId, Collection<String> entryIds) {

        String entityLogId = mapper.getLogId(dossierId, fileId);
        List<String> ids = entryIds.stream()
                .map(entryId -> mapper.getLogEntryId(entityLogId, entryId))
                .toList();

        return entityLogEntryDocumentRepository.findContainingNodeIdForAllByIdAndContainingNodeIdNotEmpty(ids)
                .stream()
                .filter(entityLogEntryDocument -> !entityLogEntryDocument.getContainingNodeId().isEmpty())
                .map((EntityLogEntryDocument entityLogEntryDocument) -> entityLogEntryDocument.getContainingNodeId()
                        .get(0))
                .collect(Collectors.toSet());
    }


    public List<EntityLogEntry> findEntityLogEntriesNotContainedOrFirstContainedByElementInList(String dossierId, String fileId, Collection<Integer> nodeIds) {

        String entityLogId = mapper.getLogId(dossierId, fileId);

        return entityLogEntryDocumentRepository.findByEntityLogIdAndNotContainedOrFirstContainedByElementInList(entityLogId, nodeIds)
                .stream()
                .map(mapper::fromLogEntryDocument)
                .toList();
    }


    public List<EntityLogEntry> findEntityLogEntriesWithEntryIdsIn(String dossierId, String fileId, Collection<String> entryIds) {

        String entityLogId = mapper.getLogId(dossierId, fileId);
        List<String> ids = entryIds.stream()
                .map(entryId -> mapper.getLogEntryId(entityLogId, entryId))
                .toList();

        return entityLogEntryDocumentRepository.findAllById(ids)
                .stream()
                .map(mapper::fromLogEntryDocument)
                .toList();
    }


    public List<EntityLogEntry> findEntityLogEntriesWithContainingNodeIds(String dossierId, String fileId) {

        return entityLogEntryDocumentRepository.findByEntityLogIdAndContainingNodeIdNotEmpty(mapper.getLogId(dossierId, fileId))
                .stream()
                .map(mapper::fromLogEntryDocument)
                .toList();
    }


    public Optional<EntityLog> findEntityLogWithoutEntries(String dossierId, String fileId) {

        return entityLogDocumentRepository.findEntityLogDocumentWithoutEntriesById(mapper.getLogId(dossierId, fileId))
                .map(mapper::fromLogDocument);
    }


    public Optional<EntityLog> findEntityLogWithoutExcludedEntryTypes(String dossierId, String fileId, List<String> excludedTypes) {

        var excludedTypesNonNull = excludedTypes == null ? new ArrayList<String>() : excludedTypes;
        String entityLogId = mapper.getLogId(dossierId, fileId);
        Optional<EntityLog> optionalEntityLog = entityLogDocumentRepository.findEntityLogDocumentWithoutEntriesById(entityLogId)
                .map(mapper::fromLogDocument);
        optionalEntityLog.ifPresent(entityLog -> entityLog.getEntityLogEntry()
                .addAll(entityLogEntryDocumentRepository.findEntityLogDocumentByIdAndExcludedTypes(entityLogId, excludedTypesNonNull)
                                .stream()
                                .map(mapper::fromLogEntryDocument)
                                .toList()));
        return optionalEntityLog;
    }


    public Optional<EntityLog> findEntityLogWithEntriesOnPages(String dossierId, String fileId, List<Integer> pageNumbers) {

        String entityLogId = mapper.getLogId(dossierId, fileId);
        Optional<EntityLog> optionalEntityLog = entityLogDocumentRepository.findEntityLogDocumentWithoutEntriesById(entityLogId)
                .map(mapper::fromLogDocument);
        optionalEntityLog.ifPresent(entityLog -> entityLog.getEntityLogEntry()
                .addAll(entityLogEntryDocumentRepository.findByEntityLogIdAndPositionsPageNumberIn(entityLogId, pageNumbers)
                                .stream()
                                .map(mapper::fromLogEntryDocument)
                                .toList()));
        return optionalEntityLog;
    }


    public List<EntityLogEntry> findEntityLogEntriesByAnalysisNumber(String dossierId, String fileId, int analysisNumber) {

        return entityLogEntryDocumentRepository.findByEntityLogIdAndChangesAnalysisNumberEquals(mapper.getLogId(dossierId, fileId), analysisNumber)
                .stream()
                .map(mapper::fromLogEntryDocument)
                .toList();
    }


    public Optional<EntityLog> findEntityLogWithEntriesSinceAnalysisNumber(String dossierId, String fileId, Integer analysisNumber) {

        String entityLogId = mapper.getLogId(dossierId, fileId);
        Optional<EntityLog> optionalEntityLog = entityLogDocumentRepository.findEntityLogDocumentWithoutEntriesById(entityLogId)
                .map(mapper::fromLogDocument);
        optionalEntityLog.ifPresent(entityLog -> entityLog.getEntityLogEntry()
                .addAll(entityLogEntryDocumentRepository.findByEntityLogIdAndChangesAnalysisNumberBetween(entityLogId, analysisNumber, entityLog.getAnalysisNumber())
                                .stream()
                                .map(mapper::fromLogEntryDocument)
                                .toList()));
        return optionalEntityLog;
    }


    public void saveEntityLogWithoutEntries(String dossierId, String fileId, EntityLog entityLog) {

        entityLogDocumentUpdateService.updateEntityLogDocumentWithoutEntries(dossierId, fileId, entityLog);
    }


    public boolean entityLogContainsImportedEntries(String dossierId, String fileId) {

        String entityLogId = mapper.getLogId(dossierId, fileId);
        return entityLogEntryDocumentRepository.existsByEntityLogIdAndEngineImported(entityLogId);
    }


    public Set<EntityLogEntry> findEntriesByValueWithFilters(String dossierId,
                                                             String fileId,
                                                             String value,
                                                             boolean caseSensitive,
                                                             Set<String> originTypes,
                                                             Set<String> originLegalBases,
                                                             Set<Integer> pageNumbers) {

        return new HashSet<>(entityLogEntryDocumentCustomRepository.findEntryIdsByValueWithFilters(mapper.getLogId(dossierId, fileId),
                                                                                                   value,
                                                                                                   caseSensitive,
                                                                                                   originTypes,
                                                                                                   originLegalBases,
                                                                                                   pageNumbers)
                                     .stream()
                                     .map(mapper::fromLogEntryDocument)
                                     .toList());
    }


    public Set<EntityLogEntry> findEntriesByMatchingFullPositionWithFilters(String dossierId,
                                                                            String fileId,
                                                                            float[] rectangle,
                                                                            Set<String> originTypes,
                                                                            Set<String> originLegalBases,
                                                                            Set<Integer> pageNumbers) {

        return new HashSet<>(entityLogEntryDocumentCustomRepository.findEntryIdsByMatchingPositionWithFilters(mapper.getLogId(dossierId, fileId),
                                                                                                              rectangle,
                                                                                                              originTypes,
                                                                                                              originLegalBases,
                                                                                                              pageNumbers)
                                     .stream()
                                     .map(mapper::fromLogEntryDocument)
                                     .toList());
    }


    public Set<String> findEntryIdsByMatchingFullPositionWithFilters(String dossierId,
                                                                     String fileId,
                                                                     float[] rectangle,
                                                                     Set<String> originTypes,
                                                                     Set<String> originalLegalBases,
                                                                     Set<Integer> pageNumbers) {

        return findEntriesByMatchingFullPositionWithFilters(dossierId, fileId, rectangle, originTypes, originalLegalBases, pageNumbers).stream()
                .map(EntityLogEntry::getId)
                .collect(Collectors.toSet());
    }


    public Set<String> findEntryIdsByValueWithFilters(String dossierId,
                                                      String fileId,
                                                      String value,
                                                      boolean caseSensitive,
                                                      Set<String> originTypes,
                                                      Set<String> originLegalBases,
                                                      Set<Integer> pageNumbers) {

        return findEntriesByValueWithFilters(dossierId, fileId, value, caseSensitive, originTypes, originLegalBases, pageNumbers).stream()
                .map(EntityLogEntry::getId)
                .collect(Collectors.toSet());
    }

    public Set<EntityLogEntry> findDictionaryEntityLogEntriesByValueAndPositions(String dossierId,
                                                                                 String fileId,
                                                                                 String value,
                                                                                 List<List<Position>> positionsList) {

        return new HashSet<>(entityLogEntryDocumentRepository.findDictionaryEntityLogEntriesByValueAndPositions(mapper.getLogId(dossierId, fileId),
                                                                                                                value,
                                                                                                                positionsList)
                                            .stream()
                                            .map(mapper::fromLogEntryDocument)
                                            .toList());
    }

}
