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

import static com.iqser.red.service.persistence.management.v1.processor.configuration.MessagingConfiguration.MIGRATION_REQUEST_QUEUE;

import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;

import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Service;

import com.iqser.red.service.persistence.management.v1.processor.entity.annotations.AnnotationEntityId;
import com.iqser.red.service.persistence.management.v1.processor.exception.InternalServerErrorException;
import com.iqser.red.service.persistence.management.v1.processor.exception.NotFoundException;
import com.iqser.red.service.persistence.management.v1.processor.model.ManualChangesQueryOptions;
import com.iqser.red.service.persistence.management.v1.processor.service.CommentService;
import com.iqser.red.service.persistence.management.v1.processor.service.DossierService;
import com.iqser.red.service.persistence.management.v1.processor.service.IndexingService;
import com.iqser.red.service.persistence.management.v1.processor.service.job.AutomaticAnalysisJob;
import com.iqser.red.service.persistence.management.v1.processor.service.layoutparsing.LayoutParsingRequestFactory;
import com.iqser.red.service.persistence.management.v1.processor.service.manualredactions.ManualRedactionProviderService;
import com.iqser.red.service.persistence.management.v1.processor.service.manualredactions.ManualRedactionService;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.FileStatusPersistenceService;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.SaasMigrationStatusPersistenceService;
import com.iqser.red.service.persistence.management.v1.processor.settings.FileManagementServiceSettings;
import com.iqser.red.service.persistence.management.v1.processor.utils.StorageIdUtils;
import com.iqser.red.service.persistence.service.v1.api.shared.model.analysislog.migration.MigratedIds;
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.entitymapped.ManualRedactionEntry;
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.model.dossiertemplate.dossier.file.SaasMigrationStatus;
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.dossier.file.WorkflowStatus;
import com.iqser.red.service.redaction.v1.model.MigrationRequest;
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.databasetenantcommons.providers.TenantSyncService;
import com.knecon.fforesight.service.layoutparser.internal.api.queue.LayoutParsingQueueNames;
import com.knecon.fforesight.tenantcommons.TenantContext;
import com.knecon.fforesight.tenantcommons.TenantProvider;
import com.knecon.fforesight.tenantcommons.model.TenantSyncEvent;
import com.knecon.fforesight.tenantcommons.model.UpdateDetailsRequest;

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

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

    AutomaticAnalysisJob automaticAnalysisJob;
    FileStatusPersistenceService fileStatusPersistenceService;
    SaasMigrationStatusPersistenceService saasMigrationStatusPersistenceService;
    DossierService dossierService;
    ManualRedactionProviderService manualRedactionProviderService;
    TenantProvider tenantProvider;
    IndexingService indexingService;
    LayoutParsingRequestFactory layoutParsingRequestFactory;
    RabbitTemplate rabbitTemplate;
    FileManagementServiceSettings settings;
    StorageService storageService;
    SaasAnnotationIdMigrationService saasAnnotationIdMigrationService;
    UncompressedFilesMigrationService uncompressedFilesMigrationService;
    ManualRedactionService manualRedactionService;
    CommentService commentService;
    RankDeDuplicationService rankDeDuplicationService;
    SaasMigrationManualChangesUpdateService saasMigrationManualChangesUpdateService;


    @Override
    public synchronized void syncTenant(TenantSyncEvent tenantSyncEvent) {

        startMigrationForTenant(tenantSyncEvent.getTenantId());
    }

    // Persistence-Service needs to be scaled to 1.


    public void startMigrationForTenant(String tenantId) {

        // TODO migrate rules.
        automaticAnalysisJob.stopForTenant(tenantId);

        log.info("Starting uncompressed files migration ...");
        uncompressedFilesMigrationService.migrateUncompressedFiles(tenantId);
        log.info("Finished uncompressed files migration ...");

        rankDeDuplicationService.deduplicate();
        int numberOfFiles = 0;

        var files = saasMigrationStatusPersistenceService.findAll();

        for (var file : files) {

            var dossier = dossierService.getDossierById(file.getDossierId());

            if (dossier.getHardDeletedTime() != null) {
                if (fileStatusPersistenceService.getStatus(file.getFileId()).getHardDeletedTime() != null) {
                    saasMigrationStatusPersistenceService.updateStatus(file.getFileId(), SaasMigrationStatus.FINISHED);
                    continue;
                } else {
                    fileStatusPersistenceService.hardDelete(file.getFileId(), dossier.getHardDeletedTime());
                    saasMigrationStatusPersistenceService.updateStatus(file.getFileId(), SaasMigrationStatus.FINISHED);
                    continue;
                }
            }
            if (fileStatusPersistenceService.getStatus(file.getFileId()).getHardDeletedTime() != null) {
                saasMigrationStatusPersistenceService.updateStatus(file.getFileId(), SaasMigrationStatus.FINISHED);
                continue;
            }

            if (!file.getStatus().equals(SaasMigrationStatus.MIGRATION_REQUIRED)) {
                log.info("Skipping {} for tenant {} since migration status is {}", file.getFileId(), TenantContext.getTenantId(), file.getStatus());
                continue;
            }

            // delete NER_ENTITIES since offsets depend on old document structure.
            storageService.deleteObject(TenantContext.getTenantId(), StorageIdUtils.getStorageId(file.getDossierId(), file.getFileId(), FileType.NER_ENTITIES));

            var layoutParsingRequest = layoutParsingRequestFactory.build(dossier.getDossierTemplate().getId(), file.getDossierId(), file.getFileId(), false);

            rabbitTemplate.convertAndSend(LayoutParsingQueueNames.LAYOUT_PARSING_REQUEST_EXCHANGE, TenantContext.getTenantId(), layoutParsingRequest);

            numberOfFiles++;

        }

        log.info("Added {} documents for tenant {} to Layout-Parsing queue for saas migration", numberOfFiles, TenantContext.getTenantId());
        if (numberOfFiles == 0) {
            finalizeMigration();
        }
    }


    public void startMigrationForFile(String dossierId, String fileId) {

        var dossier = dossierService.getDossierById(dossierId);

        if (dossier.getHardDeletedTime() != null) {
            if (fileStatusPersistenceService.getStatus(fileId).getHardDeletedTime() != null) {
                saasMigrationStatusPersistenceService.updateStatus(fileId, SaasMigrationStatus.FINISHED);
                return;
            } else {
                fileStatusPersistenceService.hardDelete(fileId, dossier.getHardDeletedTime());
                saasMigrationStatusPersistenceService.updateStatus(fileId, SaasMigrationStatus.FINISHED);
                return;
            }
        }
        if (fileStatusPersistenceService.getStatus(fileId).getHardDeletedTime() != null) {
            saasMigrationStatusPersistenceService.updateStatus(fileId, SaasMigrationStatus.FINISHED);
            return;
        }

        log.info("Starting Migration for dossierId {} and fileId {}", dossierId, fileId);
        saasMigrationStatusPersistenceService.createMigrationRequiredStatus(dossierId, fileId);
        var layoutParsingRequest = layoutParsingRequestFactory.build(dossier.getDossierTemplate().getId(), dossierId, fileId, false);
        rabbitTemplate.convertAndSend(LayoutParsingQueueNames.LAYOUT_PARSING_REQUEST_EXCHANGE, TenantContext.getTenantId(), layoutParsingRequest);
    }


    public void handleLayoutParsingFinished(String dossierId, String fileId) {

        if (!layoutParsingFilesExist(dossierId, fileId)) {
            saasMigrationStatusPersistenceService.updateErrorStatus(fileId, "Layout parsing files not written!");
            return;
        }

        log.info("Layout Parsing finished for saas migration for tenant {} dossier {} and file {}", TenantContext.getTenantId(), dossierId, fileId);
        saasMigrationStatusPersistenceService.updateStatus(fileId, SaasMigrationStatus.DOCUMENT_FILES_MIGRATED);

        if (fileStatusPersistenceService.getStatus(fileId).getWorkflowStatus().equals(WorkflowStatus.APPROVED)) {
            saasMigrationManualChangesUpdateService.convertUnprocessedAddToDictionariesToLocalChanges(fileId);
        }

        try {
            indexingService.reindex(dossierId, Set.of(fileId), false);

            String dossierTemplateId = dossierService.getDossierById(dossierId).getDossierTemplateId();

            rabbitTemplate.convertAndSend(MIGRATION_REQUEST_QUEUE,
                                          MigrationRequest.builder()
                                                  .dossierTemplateId(dossierTemplateId)
                                                  .dossierId(dossierId)
                                                  .fileId(fileId)
                                                  .fileIsApproved(fileStatusPersistenceService.getStatus(fileId).getWorkflowStatus().equals(WorkflowStatus.APPROVED))
                                                  .manualRedactions(manualRedactionProviderService.getManualRedactions(fileId, ManualChangesQueryOptions.allWithoutDeleted()))
                                                  .entitiesWithComments(commentService.getCommentCounts(fileId).keySet())
                                                  .build());
        } catch (Exception e) {
            log.error("Queuing of entityLog migration failed with {}", e.getMessage());
            saasMigrationStatusPersistenceService.updateErrorStatus(fileId, String.format("Queuing of entityLog migration failed with %s", e.getMessage()));
        }

    }


    private boolean layoutParsingFilesExist(String dossierId, String fileId) {

        return storageService.objectExists(TenantContext.getTenantId(), StorageIdUtils.getStorageId(dossierId, fileId, FileType.DOCUMENT_STRUCTURE)) //
               && storageService.objectExists(TenantContext.getTenantId(), StorageIdUtils.getStorageId(dossierId, fileId, FileType.DOCUMENT_TEXT)) //
               && storageService.objectExists(TenantContext.getTenantId(), StorageIdUtils.getStorageId(dossierId, fileId, FileType.DOCUMENT_PAGES)) //
               && storageService.objectExists(TenantContext.getTenantId(), StorageIdUtils.getStorageId(dossierId, fileId, FileType.DOCUMENT_POSITION));
    }


    public void handleEntityLogMigrationFinished(String dossierId, String fileId) {

        if (!entityLogMigrationFilesExist(dossierId, fileId)) {
            saasMigrationStatusPersistenceService.updateErrorStatus(fileId, "Migration Files not written!");
            return;
        }
        saasMigrationStatusPersistenceService.updateStatus(fileId, SaasMigrationStatus.REDACTION_LOGS_MIGRATED);

        log.info("EntityLog migration finished for saas migration for tenant {} dossier {} and file {}", TenantContext.getTenantId(), dossierId, fileId);
        migrateAnnotationIdsAndAddManualAddRedactionsAndDeleteSectionGrid(dossierId, fileId);
    }


    private boolean entityLogMigrationFilesExist(String dossierId, String fileId) {

        return storageService.objectExists(TenantContext.getTenantId(), StorageIdUtils.getStorageId(dossierId, fileId, FileType.ENTITY_LOG)) && storageService.objectExists(
                TenantContext.getTenantId(),
                StorageIdUtils.getStorageId(dossierId, fileId, FileType.MIGRATED_IDS));
    }


    public void handleError(String dossierId, String fileId, String errorCause, String retryExchange) {

        var migrationEntry = saasMigrationStatusPersistenceService.findById(fileId);
        Integer numErrors = migrationEntry.getProcessingErrorCounter();
        if (numErrors != null && numErrors <= settings.getMaxErrorRetries()) {
            saasMigrationStatusPersistenceService.updateErrorCounter(fileId, numErrors + 1, errorCause);
            rabbitTemplate.convertAndSend(retryExchange, TenantContext.getTenantId(), MigrationRequest.builder().dossierId(dossierId).fileId(fileId).build());
            log.error("Retrying error during saas migration for tenant {} dossier {} and file {}, cause {}", TenantContext.getTenantId(), dossierId, fileId, errorCause);
        } else {
            saasMigrationStatusPersistenceService.updateErrorStatus(fileId, errorCause);
            log.error("Error during saas migration for tenant {} dossier {} and file {}, cause {}", TenantContext.getTenantId(), dossierId, fileId, errorCause);
        }
    }


    public void requeueErrorFiles() {

        automaticAnalysisJob.stopForTenant(TenantContext.getTenantId());
        saasMigrationStatusPersistenceService.findAllByStatus(SaasMigrationStatus.ERROR)
                .forEach(migrationStatus -> startMigrationForFile(migrationStatus.getDossierId(), migrationStatus.getFileId()));

    }


    private void migrateAnnotationIdsAndAddManualAddRedactionsAndDeleteSectionGrid(String dossierId, String fileId) {

        MigratedIds migratedIds = getMigratedIds(dossierId, fileId);
        Map<String, String> oldToNewMapping = migratedIds.buildOldToNewMapping();
        updateAnnotationIds(dossierId, fileId, oldToNewMapping);
        List<String> forceRedactionIdsToDelete = migratedIds.getForceRedactionIdsToDelete();
        softDeleteForceRedactions(fileId, forceRedactionIdsToDelete);
        log.info("Soft-deleted force redactions.");
        List<ManualRedactionEntry> manualRedactionEntriesToAdd = migratedIds.getManualRedactionEntriesToAdd();
        int count = addManualRedactionEntries(manualRedactionEntriesToAdd);
        log.info("Added {} additional manual entries.", count);
        deleteSectionGridAndNerEntitiesFiles(dossierId, fileId);
        saasMigrationStatusPersistenceService.updateStatus(fileId, SaasMigrationStatus.FINISHED);

        log.info("AnnotationIds migration finished for saas migration for tenant {} dossier {} and file {}", TenantContext.getTenantId(), dossierId, fileId);
        finalizeMigration(); // AutomaticAnalysisJob should be re-enabled by re-starting the persistence service pod after a rule change
    }


    private void deleteSectionGridAndNerEntitiesFiles(String dossierId, String fileId) {

        try {
            storageService.deleteObject(TenantContext.getTenantId(), StorageIdUtils.getStorageId(dossierId, fileId, FileType.SECTION_GRID));
        } catch (StorageObjectDoesNotExist e) {
            log.info("No sectiongrid found for {}, {}, ignoring....", dossierId, fileId);
        }

        try {
            storageService.deleteObject(TenantContext.getTenantId(), StorageIdUtils.getStorageId(dossierId, fileId, FileType.NER_ENTITIES));
        } catch (StorageObjectDoesNotExist e) {
            log.info("No ner entities file found for {}, {}, ignoring....", dossierId, fileId);
        }
    }


    private void softDeleteForceRedactions(String fileId, List<String> forceRedactionIdsToDelete) {

        manualRedactionService.softDeleteForceRedactions(fileId, forceRedactionIdsToDelete);
    }


    private int addManualRedactionEntries(List<ManualRedactionEntry> manualRedactionEntriesToAdd) {

        manualRedactionEntriesToAdd.forEach(add -> {
            if (add.getSection() != null && add.getSection().length() > 254) {
                add.setSection(add.getSection().substring(0, 254));
            }
        });

        return manualRedactionService.addManualRedactionEntries(manualRedactionEntriesToAdd, true);
    }


    public void revertMigrationForFile(String dossierId, String fileId) {

        log.info("Reverting Migration for dossierId {} and fileId {}", dossierId, fileId);
        MigratedIds migratedIds = getMigratedIds(dossierId, fileId);
        Map<String, String> newToOldMapping = migratedIds.buildNewToOldMapping();
        updateAnnotationIds(dossierId, fileId, newToOldMapping);
        deleteManualRedactionEntries(migratedIds.getManualRedactionEntriesToAdd());
        undeleteForceRedactions(fileId, migratedIds.getForceRedactionIdsToDelete());
        saasMigrationStatusPersistenceService.createMigrationRequiredStatus(dossierId, fileId);
    }


    private void undeleteForceRedactions(String fileId, List<String> forceRedactionIdsToDelete) {

        manualRedactionService.undeleteForceRedactions(fileId, forceRedactionIdsToDelete);

    }


    private void deleteManualRedactionEntries(List<ManualRedactionEntry> manualRedactionEntriesToAdd) {

        manualRedactionService.deleteManualRedactionEntries(manualRedactionEntriesToAdd);
    }


    private void updateAnnotationIds(String dossierId, String fileId, Map<String, String> idMapping) {

        try {
            updateAnnotationIds(fileId, idMapping);
        } catch (Exception e) {
            String message = String.format("Error during annotation id migration for tenant %s dossier %s and file %s, cause %s",
                                           TenantContext.getTenantId(),
                                           dossierId,
                                           fileId,
                                           e.getMessage());
            saasMigrationStatusPersistenceService.updateErrorStatus(fileId, message);
            log.error(message);
            throw e;
        }
    }


    private void finalizeMigration() {

        if (saasMigrationStatusPersistenceService.countByStatus(SaasMigrationStatus.FINISHED) == saasMigrationStatusPersistenceService.countAll()) {
//            automaticAnalysisJob.startForTenant(TenantContext.getTenantId()); // AutomaticAnalysisJob should be re-enabled by re-starting the persistence service pod after a rule change
            tenantProvider.updateDetails(TenantContext.getTenantId(), UpdateDetailsRequest.builder().key("persistence-service-ready").value(true).build());
            log.info("Saas migration finished for tenantId {}, re-enabled scheduler", TenantContext.getTenantId());
        }
    }


    public void updateAnnotationIds(String fileId, Map<String, String> idMapping) {

        AtomicInteger numUpdates = new AtomicInteger(0);
        AtomicInteger numCommentUpdates = new AtomicInteger(0);
        idMapping.forEach((key, value) -> {
            AnnotationEntityId oldAnnotationEntityId = buildAnnotationId(fileId, key);
            AnnotationEntityId newAnnotationEntityId = buildAnnotationId(fileId, value);
            numUpdates.getAndAdd(saasAnnotationIdMigrationService.updateManualAddRedaction(oldAnnotationEntityId, newAnnotationEntityId));
            numUpdates.getAndAdd(saasAnnotationIdMigrationService.updateRemoveRedaction(oldAnnotationEntityId, newAnnotationEntityId));
            numUpdates.getAndAdd(saasAnnotationIdMigrationService.updateForceRedaction(oldAnnotationEntityId, newAnnotationEntityId));
            numUpdates.getAndAdd(saasAnnotationIdMigrationService.updateResizeRedaction(oldAnnotationEntityId, newAnnotationEntityId));
            numUpdates.getAndAdd(saasAnnotationIdMigrationService.updateRecategorizationRedaction(oldAnnotationEntityId, newAnnotationEntityId));
            numUpdates.getAndAdd(saasAnnotationIdMigrationService.updateLegalBasisChangeRedaction(oldAnnotationEntityId, newAnnotationEntityId));
            numCommentUpdates.getAndAdd(saasAnnotationIdMigrationService.updateCommentIds(fileId, key, value));
        });
        log.info("Migrated {} annotationIds and {} comments for file {}", numUpdates.get(), numCommentUpdates, fileId);
    }


    private AnnotationEntityId buildAnnotationId(String fileId, String annotationId) {

        return new AnnotationEntityId(annotationId, fileId);
    }


    private MigratedIds getMigratedIds(String dossierId, String fileId) {

        try {
            return storageService.readJSONObject(TenantContext.getTenantId(), StorageIdUtils.getStorageId(dossierId, fileId, FileType.MIGRATED_IDS), MigratedIds.class);
        } catch (StorageObjectDoesNotExist e) {
            throw new NotFoundException(String.format("MigratedIds does not exist for Dossier ID \"%s\" and File ID \"%s\"!", dossierId, fileId));
        } catch (StorageException e) {
            throw new InternalServerErrorException(e.getMessage());
        }
    }

}
