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

import com.google.common.collect.Sets;
import com.iqser.red.service.pdftron.redaction.v1.api.model.ByteContentDocument;
import com.iqser.red.service.pdftron.redaction.v1.api.model.highlights.TextHighlightConversionOperation;
import com.iqser.red.service.pdftron.redaction.v1.api.model.highlights.TextHighlightConversionRequest;
import com.iqser.red.service.persistence.management.v1.processor.client.pdftronredactionservice.PDFTronClient;
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.NotFoundException;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.DossierPersistenceService;
import com.iqser.red.service.persistence.service.v1.api.shared.model.ReanalysisSettings;
import com.iqser.red.service.persistence.service.v1.api.shared.model.annotations.DeleteImportedRedactionsRequest;
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.dossier.file.FileModel;
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.ProcessingStatus;
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.dossier.file.WorkflowStatus;

import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;

import org.springframework.stereotype.Service;

import java.time.OffsetDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;

@Slf4j
@Service
@RequiredArgsConstructor
public class ReanalysisService {

    private final FileStatusService fileStatusService;
    private final DossierPersistenceService dossierPersistenceService;
    private final IndexingService indexingService;
    private final PDFTronClient pDFTronRedactionClient;
    private final FileManagementStorageService fileManagementStorageService;
    private final DossierIdFileIdRequestValidator requestValidator;

    private final Predicate<FileModel> validFilesFilter = fileStatus -> !fileStatus.isSoftOrHardDeleted() && !fileStatus.getWorkflowStatus().equals(WorkflowStatus.APPROVED);
    private final Predicate<FileModel> errorFilesFilter = fileStatus -> fileStatus.getProcessingStatus().equals(ProcessingStatus.ERROR);


    public void reanalyzeAllRelevantErrorFiles(boolean repeatStructureAnalysis, boolean runOcr) {

        var errorFiles = fileStatusService.getAllRelevantErrorFiles();
        analyseFiles(repeatStructureAnalysis, errorFiles, runOcr);
    }


    public void reanalyzeDossier(String dossierId, boolean force) {

        List<FileModel> relevantFiles = getAllFilesForDossier(dossierId, validFilesFilter);
        reanalyseFiles(force, relevantFiles);
    }


    private List<FileModel> getAllFilesForDossier(String dossierId, Predicate<FileModel> filter) {

        var dossier = dossierPersistenceService.getAndValidateDossier(dossierId);

        return fileStatusService.getDossierStatus(dossier.getId())
                .stream()
                .filter(filter)
                .collect(Collectors.toList());

    }


    private List<FileModel> getAllFilesForDossier(String dossierId) {

        var dossier = dossierPersistenceService.getAndValidateDossier(dossierId);

        return fileStatusService.getDossierStatus(dossier.getId());

    }


    private void reanalyseFiles(boolean force, List<FileModel> filesToReanalyse) {

        if (force) {
            filesToReanalyse.forEach(file -> {
                if (file.isFullAnalysisRequired()) {
                    fileStatusService.setStatusFullReprocess(file.getDossierId(), file.getId(), filesToReanalyse.size() == 1, false, false);
                } else {
                    fileStatusService.setStatusReprocess(file.getDossierId(), file.getId(), filesToReanalyse.size() == 1, true);
                }
            });
        } else {
            filesToReanalyse.stream()
                    .filter(FileModel::isReanalysisRequired)
                    .forEach(file -> {
                        if (file.isFullAnalysisRequired()) {
                            fileStatusService.setStatusFullReprocess(file.getDossierId(), file.getId(), filesToReanalyse.size() == 1, false, false);
                        } else {
                            fileStatusService.setStatusReprocess(file.getDossierId(), file.getId(), filesToReanalyse.size() == 1, true);
                        }
                    });
        }
    }


    private void analyseFiles(boolean repeatStructureAnalysis, List<FileModel> filesToReanalyse, boolean runOcr) {

        filesToReanalyse.forEach(file -> fileStatusService.setStatusFullReprocess(file.getDossierId(),
                                                                                  file.getId(),
                                                                                  filesToReanalyse.size() == 1,
                                                                                  repeatStructureAnalysis,
                                                                                  runOcr));
    }


    public void reanalyzeFiles(String dossierId, Set<String> fileIds, boolean force) {

        var relevantFiles = getRelevantFiles(dossierId, fileIds);
        reanalyseFiles(force, relevantFiles);
    }


    public void reanalyzeGivenErrorFilesInDossier(String dossierId, Set<String> fileIds, boolean repeatStructureAnalysis, boolean runOcr) {

        analyseFiles(repeatStructureAnalysis, fileIds.isEmpty() ? getAllFilesForDossier(dossierId, errorFilesFilter) : getRelevantErrorFiles(dossierId, fileIds), runOcr);
    }


    private List<FileModel> getRelevantFiles(String dossierId, Collection<String> fileIds) {

        var dossierFiles = getAllFilesForDossier(dossierId);

        var relevantFiles = new ArrayList<FileModel>();
        for (var fileId : fileIds) {
            var dossierFileOptional = dossierFiles.stream()
                    .filter(f -> f.getId().equals(fileId))
                    .findAny();
            if (dossierFileOptional.isEmpty()) {
                throw new BadRequestException("Cannot reanalyse file from different dossier!");
            }

            var dossierFile = dossierFileOptional.get();
            if (dossierFile.isSoftOrHardDeleted()) {
                throw new BadRequestException("Cannot reanalyse deleted file!");
            }
            if (dossierFile.getWorkflowStatus() == WorkflowStatus.APPROVED) {
                throw new BadRequestException("Cannot reanalyse approved file!");
            }

            relevantFiles.add(dossierFile);

        }
        return relevantFiles;
    }


    private List<FileModel> getRelevantErrorFiles(String dossierId, Collection<String> fileIds) {

        List<FileModel> relevantFiles = getRelevantFiles(dossierId, fileIds);
        relevantFiles.forEach(fileModel -> {
            if (!fileModel.getProcessingStatus().equals(ProcessingStatus.ERROR)) {
                throw new ConflictException("Only files in error state are allowed to be analyzed here!");
            }
        });
        return relevantFiles;
    }


    public void ocrDossier(String dossierId) {

        var relevantFiles = getAllFilesForDossier(dossierId, validFilesFilter);

        relevantFiles.stream()
                .filter(fileStatus -> fileStatus.getOcrStartTime() == null)
                .filter(fileStatus -> fileStatus.getProcessingStatus().equals(ProcessingStatus.PROCESSED))
                .forEach(fileStatus -> fileStatusService.setStatusOcrQueued(dossierId, fileStatus.getId(), false));
    }


    public void ocrFile(String dossierId, String fileId, boolean force, boolean allPages) {

        dossierPersistenceService.getAndValidateDossier(dossierId);
        FileModel dossierFile = fileStatusService.getStatus(fileId);

        if (!dossierFile.getProcessingStatus().equals(ProcessingStatus.PROCESSED)
            && !dossierFile.getProcessingStatus().equals(ProcessingStatus.OCR_PROCESSING_QUEUED)
            && !dossierFile.getProcessingStatus().equals(ProcessingStatus.OCR_PROCESSING)) {
            throw new ConflictException("File is not processed");
        }

        if (dossierFile.isSoftOrHardDeleted()) {
            throw new NotFoundException("File does not exist");
        }

        if (dossierFile.getWorkflowStatus().equals(WorkflowStatus.APPROVED)) {
            throw new ConflictException("Cannot analyse a approved file");
        }

        if (force) {
            fileStatusService.setStatusOcrQueued(dossierId, fileId, allPages);
        } else {
            if (dossierFile.getOcrStartTime() != null) {
                throw new ConflictException("File already has been OCR processed");
            }

            ocrFiles(dossierId, Sets.newHashSet(fileId), allPages);
        }
    }


    public void ocrFiles(String dossierId, Set<String> fileIds, boolean allPages) {

        var relevantFiles = getRelevantFiles(dossierId, fileIds);

        if (relevantFiles.stream()
                .anyMatch(fileStatus -> !fileStatus.getProcessingStatus().equals(ProcessingStatus.PROCESSED) && !fileStatus.getProcessingStatus()
                        .equals(ProcessingStatus.OCR_PROCESSING_QUEUED) && !fileStatus.getProcessingStatus().equals(ProcessingStatus.OCR_PROCESSING))) {
            throw new ConflictException("File is not processed");
        }

        relevantFiles.stream()
                .filter(fileStatus -> fileStatus.getOcrStartTime() == null)
                .forEach(fileStatus -> fileStatusService.setStatusOcrQueued(dossierId, fileStatus.getId(), allPages));
    }


    public void importRedactions(ByteContentDocument documentRequest) {

        try {
            pDFTronRedactionClient.importRedactions(documentRequest);
        } catch (Exception e) {
            throw new BadRequestException("File Not Valid PDF or number of pages do not match");
        }
        fileStatusService.setStatusFullReprocess(documentRequest.getDossierId(), documentRequest.getFileId(), true, false, false);
    }


    @SneakyThrows
    public void deleteImportedRedactions(DeleteImportedRedactionsRequest deleteImportedRedactionsRequest) {

        var importedRedactions = fileManagementStorageService.getImportedRedactions(deleteImportedRedactionsRequest.getDossierId(), deleteImportedRedactionsRequest.getFileId());
        importedRedactions.getImportedRedactions()
                .forEach((key, value) -> value.removeIf(v -> deleteImportedRedactionsRequest.getAnnotationIds().contains(v.getId())));
        fileManagementStorageService.storeJSONObject(deleteImportedRedactionsRequest.getDossierId(),
                                                     deleteImportedRedactionsRequest.getFileId(),
                                                     FileType.IMPORTED_REDACTIONS,
                                                     importedRedactions);
        fileStatusService.setStatusFullReprocess(deleteImportedRedactionsRequest.getDossierId(), deleteImportedRedactionsRequest.getFileId(), true, false, false);
    }


    public void reindex(String dossierId, boolean dropIndex, Set<String> fileIds) {

        indexingService.reindex(dossierId, fileIds, dropIndex);
    }


    public void convertTextHighlights(TextHighlightConversionRequest textHighlightRequest) {

        boolean hasTextHighlights = pDFTronRedactionClient.convertTextHighlights(textHighlightRequest);

        fileStatusService.updateHasHighlights(textHighlightRequest.getFileId(), hasTextHighlights);
        fileStatusService.updateFileModificationDate(textHighlightRequest.getFileId(), OffsetDateTime.now().truncatedTo(ChronoUnit.MILLIS));

        if (textHighlightRequest.getOperation().equals(TextHighlightConversionOperation.CONVERT)) {
            fileStatusService.setStatusFullReprocess(textHighlightRequest.getDossierId(), textHighlightRequest.getFileId(), true, false, false);
        }
    }


    public List<FileModel> reanalyzeTemplate(String dossierTemplateId, ReanalysisSettings reanalysisSettings) {

        requestValidator.validateRequestOrThrow404(dossierTemplateId, reanalysisSettings.dossierIds(), reanalysisSettings.fileIds());

        var files = fileStatusService.getDossierTemplateStatus(dossierTemplateId)
                .stream()
                .filter(file -> isInList(file, reanalysisSettings))
                .filter(reanalysisSettings.fileStatusFilter().asPredicate())
                .peek(file -> log.info("Reanalyzing file {}", file.getId()))
                .collect(Collectors.toList());

        List<FileModel> rejectedFiles = filterInvalidFiles(files);

        files.forEach(file -> fileStatusService.setStatusFullReprocess(file.getDossierId(),
                                                                       file.getId(),
                                                                       false,
                                                                       reanalysisSettings.repeatStructureAnalysis(),
                                                                       reanalysisSettings.runOcr()));

        return rejectedFiles;
    }


    private List<FileModel> filterInvalidFiles(List<FileModel> files) {

        List<FileModel> rejectedFiles = new ArrayList<>();

        Iterator<FileModel> iterator = files.iterator();
        while (iterator.hasNext()) {
            FileModel file = iterator.next();
            if (file.isDossierDeleted() || file.getWorkflowStatus() == WorkflowStatus.APPROVED || file.isDossierArchived()) {
                rejectedFiles.add(file);
                iterator.remove();
            }
        }
        return rejectedFiles;
    }


    private boolean isInList(FileModel file, ReanalysisSettings reAnalysisSettings) {

        return (reAnalysisSettings.fileIds().isEmpty() || reAnalysisSettings.fileIds().contains(file.getId())) //
               && (reAnalysisSettings.dossierIds().isEmpty() || reAnalysisSettings.dossierIds().contains(file.getDossierId()));
    }

}
