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

import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;

import org.springframework.stereotype.Service;

import com.iqser.red.service.persistence.management.v1.processor.entity.dossier.DossierEntity;
import com.iqser.red.service.persistence.management.v1.processor.entity.dossier.FileEntity;
import com.iqser.red.service.persistence.management.v1.processor.service.ComponentLogService;
import com.iqser.red.service.persistence.management.v1.processor.service.FileManagementStorageService;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.repository.DossierRepository;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.repository.FileRepository;
import com.iqser.red.service.persistence.management.v1.processor.utils.StorageIdUtils;
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.componentlog.ComponentLog;
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.componentlog.ComponentLogEntry;
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.componentlog.ComponentLogEntryValue;
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.componentlog.OldComponentLog;
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.componentlog.OldComponentLogEntry;
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.component.ComponentsOverrides;
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.dossier.file.FileType;
import com.iqser.red.service.persistence.service.v1.api.shared.mongo.document.ComponentLogEntryDocument;
import com.iqser.red.service.persistence.service.v1.api.shared.mongo.document.OldComponentLogEntryDocument;
import com.iqser.red.service.persistence.service.v1.api.shared.mongo.mapper.ComponentLogDocumentMapper;
import com.iqser.red.service.persistence.service.v1.api.shared.mongo.repository.OldComponentLogEntryDocumentRepository;
import com.iqser.red.storage.commons.exception.StorageException;
import com.iqser.red.storage.commons.exception.StorageObjectDoesNotExist;
import com.iqser.red.storage.commons.service.StorageService;
import com.knecon.fforesight.tenantcommons.TenantContext;

import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.experimental.FieldDefaults;
import lombok.extern.slf4j.Slf4j;

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

    FileManagementStorageService fileManagementStorageService;
    StorageService storageService;

    DossierRepository dossierRepository;
    FileRepository fileRepository;
    ComponentLogService componentLogService;
    OldComponentLogEntryDocumentRepository oldComponentLogEntryDocumentRepository;
    ComponentLogDocumentMapper mapper = ComponentLogDocumentMapper.INSTANCE;


    @SneakyThrows
    public void copyEntityLogs() {

        forEachFile(this::copyEntityLogs);
    }
    @SneakyThrows
    public void copyComponentLogs() {

        forEachFile(this::copyComponentLogs);
    }


    public void copyComponentOverrides() {

        forEachFile(this::copyComponentOverrides);
    }


    private void forEachFile(Consumer<DossierFile> consumer) {

        List<DossierEntity> dossierEntities = dossierRepository.findAll();
        List<DossierFile> files = new ArrayList<>();
        dossierEntities.forEach(dossierEntity -> {
            List<FileEntity> byDossierId = fileRepository.findByDossierId(dossierEntity.getId());
            byDossierId.forEach(file -> files.add(new DossierFile(dossierEntity.getId(), file.getId(), file.getDeleted(), file.getHardDeletedTime())));
        });

        for (DossierFile dossierFile : files) {
            consumer.accept(dossierFile);
        }
    }


    private void copyEntityLogs(DossierFile dossierFile) {

        log.info("Reading dossier {} file {} from storage", dossierFile.dossierId, dossierFile.fileId);
        Optional<EntityLog> entityLogFromStorage = getEntityLogFromStorageForMigration(dossierFile.dossierId, dossierFile.fileId);
        if (entityLogFromStorage.isPresent()) {
            EntityLog entityLog = removeDuplicates(entityLogFromStorage.get());
            log.info("File found, now saving in mongodb");
            fileManagementStorageService.deleteEntityLog(dossierFile.dossierId, dossierFile.fileId);
            fileManagementStorageService.insertEntityLog(dossierFile.dossierId, dossierFile.fileId, entityLog);
            log.info("Deleting old file from storage");
            fileManagementStorageService.storeJSONObject(dossierFile.dossierId, dossierFile.fileId, FileType.ENTITY_LOG_BAK, entityLogFromStorage.get());
            fileManagementStorageService.deleteObject(dossierFile.dossierId, dossierFile.fileId, FileType.ENTITY_LOG);
        }
    }


    private void copyComponentLogs(DossierFile dossierFile) {

        log.info("Reading dossier {} file {} from storage for component log copying", dossierFile.dossierId, dossierFile.fileId);
        Optional<OldComponentLog> componentLogFromStorage = getComponentLogFromStorageForMigration(dossierFile.dossierId, dossierFile.fileId);
        if (componentLogFromStorage.isPresent()) {
            List<OldComponentLogEntryDocument> oldOverrides = oldComponentLogEntryDocumentRepository.findByComponentLogId(mapper.getLogId(dossierFile.dossierId,
                                                                                                                                          dossierFile.fileId));
            OldComponentLog oldComponentLog = componentLogFromStorage.get();
            ComponentLog newComponentLog = createNewComponentLog(oldComponentLog, oldOverrides);

            log.info("Component log found, now saving in mongodb");
            fileManagementStorageService.deleteComponentLog(dossierFile.dossierId, dossierFile.fileId);
            fileManagementStorageService.insertComponentLog(dossierFile.dossierId, dossierFile.fileId, newComponentLog);
            log.info("Deleting old file from storage");
            fileManagementStorageService.storeJSONObject(dossierFile.dossierId, dossierFile.fileId, FileType.COMPONENT_LOG_BAK, oldComponentLog);
            fileManagementStorageService.deleteObject(dossierFile.dossierId, dossierFile.fileId, FileType.COMPONENT_LOG);
        }
    }


    private static ComponentLog createNewComponentLog(OldComponentLog oldComponentLog, List<OldComponentLogEntryDocument> oldOverrides) {

        ComponentLog componentLog = new ComponentLog();
        componentLog.setComponentRulesVersion(oldComponentLog.getComponentRulesVersion());
        componentLog.setDateFormatsVersion(-1);
        componentLog.setAnalysisNumber(oldComponentLog.getAnalysisNumber());

        Map<String, OldComponentLogEntryDocument> overrideMap = oldOverrides.stream()
                .collect(Collectors.toMap(OldComponentLogEntryDocument::getName, Function.identity()));

        List<ComponentLogEntry> newComponentLogEntries = new ArrayList<>();

        for (OldComponentLogEntry oldEntry : oldComponentLog.getComponentLogEntries()) {
            ComponentLogEntry newEntry = new ComponentLogEntry();
            newEntry.setName(oldEntry.getName());

            List<ComponentLogEntryValue> originalValues = oldEntry.getComponentValues()
                    .stream()
                    .map(oldValue -> {
                        ComponentLogEntryValue originalValue = new ComponentLogEntryValue();
                        originalValue.setValue(oldValue.getOriginalValue());
                        originalValue.setValueDescription(oldValue.getValueDescription());
                        originalValue.setComponentRuleId(oldValue.getComponentRuleId());
                        originalValue.setComponentLogEntityReferences(oldValue.getComponentLogEntityReferences());
                        return originalValue;
                    })
                    .collect(Collectors.toList());
            newEntry.setOriginalValues(originalValues);

            OldComponentLogEntryDocument overrideDoc = overrideMap.get(oldEntry.getName());
            if (overrideDoc != null) {
                newEntry.setOverridden(true);
                newEntry.setValues(overrideDoc.getOverrideValues());
            } else {
                newEntry.setOverridden(oldEntry.isOverridden());
                List<ComponentLogEntryValue> values = oldEntry.getComponentValues()
                        .stream()
                        .map(oldValue -> {
                            ComponentLogEntryValue value = new ComponentLogEntryValue();
                            value.setValue(oldValue.getValue());
                            value.setValueDescription(oldValue.getValueDescription());
                            value.setComponentRuleId(oldValue.getComponentRuleId());
                            value.setComponentLogEntityReferences(oldValue.getComponentLogEntityReferences());
                            return value;
                        })
                        .collect(Collectors.toList());
                newEntry.setValues(values);
            }

            newComponentLogEntries.add(newEntry);
        }

        componentLog.setComponentLogEntries(newComponentLogEntries);
        return componentLog;
    }


    // shouldn't have happened, but it seems there are a few rare cases at knoell prod stack. We can't get any example files though.
    protected static EntityLog removeDuplicates(EntityLog entityLog) {

        HashMap<String, EntityLogEntry> existingAnnotationIds = new HashMap<>();

        int size = entityLog.getEntityLogEntry().size();
        List<EntityLogEntry> cleanedEntries = new ArrayList<>(size);
        for (int i = 0; i < size; i++) {
            EntityLogEntry entry = entityLog.getEntityLogEntry()
                    .get(i);
            if (!existingAnnotationIds.containsKey(entry.getId())) {
                cleanedEntries.add(entry);
                existingAnnotationIds.put(entry.getId(), entry);
            } else {
                log.warn("Duplicate entry found for id {} \nExisting: {}\n Duplicate: {}", entry.getId(), existingAnnotationIds.get(entry.getId()), entry);
            }
        }
        entityLog.setEntityLogEntry(cleanedEntries);
        return entityLog;
    }


    private Optional<EntityLog> getEntityLogFromStorageForMigration(String dossierId, String fileId) {

        try {
            return Optional.ofNullable(storageService.readJSONObject(TenantContext.getTenantId(),
                                                                     StorageIdUtils.getStorageId(dossierId, fileId, FileType.ENTITY_LOG),
                                                                     EntityLog.class));
        } catch (StorageObjectDoesNotExist e) {
            log.debug("EntityLog does not exist");
        } catch (StorageException e) {
            log.debug(e.getMessage());
        }
        return Optional.empty();
    }


    private Optional<OldComponentLog> getComponentLogFromStorageForMigration(String dossierId, String fileId) {

        try {
            return Optional.ofNullable(storageService.readJSONObject(TenantContext.getTenantId(),
                                                                     StorageIdUtils.getStorageId(dossierId, fileId, FileType.COMPONENT_LOG),
                                                                     OldComponentLog.class));
        } catch (StorageObjectDoesNotExist e) {
            log.debug("ComponentLog does not exist");
        } catch (StorageException e) {
            log.debug(e.getMessage());
        }
        return Optional.empty();
    }


    private void copyComponentOverrides(DossierFile file) {

        if (file.hardDeleted != null) {
            return;
        }

        Optional<ComponentsOverrides> optionalComponentsOverrides = getComponentOverridesFromStorageForMigration(file.dossierId(), file.fileId());
        if (optionalComponentsOverrides.isEmpty()) {
            return;
        }
        ComponentsOverrides oldComponentsOverrides = optionalComponentsOverrides.get();
        oldComponentsOverrides.getComponentOverrides()
                .forEach((componentName, componentValue) -> {
                    ComponentLogEntry overrideEntry = buildOverride(componentName, componentValue);
                    componentLogService.overrideComponent(file.dossierId(), file.fileId(), overrideEntry);
                });

//        if (file.softDeleted != null) {
//            componentLogService.softDeleteAllOverrides(file.dossierId(), file.fileId(), file.softDeleted);
//        }

        fileManagementStorageService.deleteObject(file.dossierId(), file.fileId(), FileType.COMPONENTS);
    }


    private Optional<ComponentsOverrides> getComponentOverridesFromStorageForMigration(String dossierId, String fileId) {

        try {
            return Optional.ofNullable(storageService.readJSONObject(TenantContext.getTenantId(),
                                                                     StorageIdUtils.getStorageId(dossierId, fileId, FileType.COMPONENTS),
                                                                     ComponentsOverrides.class));
        } catch (StorageObjectDoesNotExist e) {
            log.debug("Component overrides do not exist");
        } catch (StorageException e) {
            log.debug(e.getMessage());
        }
        return Optional.empty();
    }


    private ComponentLogEntry buildOverride(String componentName, String componentValue) {

        return ComponentLogEntry.builder()
                .name(componentName)
                .values(List.of(ComponentLogEntryValue.builder()
                                        .value(componentValue)
                                        .valueDescription("no description")
                                        .componentRuleId("OLD.0.0")
                                        .componentLogEntityReferences(Collections.emptyList())
                                        .build()))
                .overridden(true)
                .build();
    }


    public record DossierFile(String dossierId, String fileId, OffsetDateTime softDeleted, OffsetDateTime hardDeleted) {

    }

}
