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

import java.time.OffsetDateTime;
import java.time.temporal.ChronoUnit;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;

import com.iqser.red.service.persistence.management.v1.processor.entity.dossier.FileAttributeConfigEntity;
import com.iqser.red.service.persistence.management.v1.processor.entity.dossier.FileAttributeEntity;
import com.iqser.red.service.persistence.management.v1.processor.entity.dossier.FileEntity;
import com.iqser.red.service.persistence.management.v1.processor.exception.BadRequestException;
import com.iqser.red.service.persistence.management.v1.processor.exception.NotFoundException;
import com.iqser.red.service.persistence.management.v1.processor.model.OCRStatusUpdateResponse;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.repository.FileAttributesRepository;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.repository.FileRepository;
import com.iqser.red.service.persistence.service.v1.api.shared.model.FileAttribute;
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.dossier.file.FileErrorInfo;
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 jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service
@RequiredArgsConstructor
public class FileStatusPersistenceService {

    private final FileRepository fileRepository;
    private final FileAttributesRepository fileAttributesRepository;
    private final FileAttributeConfigPersistenceService fileAttributeConfigPersistenceService;
    private final DossierPersistenceService dossierService;


    public void createStatus(String dossierId, String fileId, String filename, String uploader, long size) {

        OffsetDateTime now = OffsetDateTime.now().truncatedTo(ChronoUnit.MILLIS);
        FileEntity file = new FileEntity();
        file.setId(fileId);
        file.setDossierId(dossierId);
        file.setFilename(filename);
        file.setProcessingStatus(ProcessingStatus.UNPROCESSED);
        file.setWorkflowStatus(WorkflowStatus.NEW);
        file.setNumberOfPages(0);
        file.setAdded(now);
        file.setUploader(uploader);
        file.setLastUploaded(now);
        file.setLastUpdated(now);
        file.setFileManipulationDate(now);
        file.setProcessingErrorCounter(0);
        file.setFileSize(size);
        fileRepository.save(file);
    }


    @Transactional
    public void updateProcessingStatusPreprocessed(String fileId, boolean hasHighlights, long fileSize) {

        if (isFileDeleted(fileId)) {
            return;
        }

        log.info("File " + fileId + " has been optimized with file size " + fileSize);
        fileRepository.updateProcessingStatus(fileId,
                ProcessingStatus.PRE_PROCESSED,
                OffsetDateTime.now().truncatedTo(ChronoUnit.MILLIS),
                hasHighlights,
                calculateProcessingErrorCounter(fileId, ProcessingStatus.PRE_PROCESSED));
    }


    private boolean isFileDeleted(String fileId) {

        return fileRepository.findById(fileId).map(FileEntity::isSoftOrHardDeleted).orElse(false);
    }


    private int calculateProcessingErrorCounter(String fileId, ProcessingStatus processingStatus) {

        switch (processingStatus) {
            case ERROR:
                return fileRepository.findById(fileId).map(FileEntity::getProcessingErrorCounter).orElse(0) + 1;

            case PROCESSED:
            case REPROCESS:
                return 0;

            default:
                return fileRepository.findById(fileId).map(FileEntity::getProcessingErrorCounter).orElse(0);
        }

    }


    @Transactional
    public void updateProcessingStatus(String fileId,
                                       int numberOfPages,
                                       long dictionaryVersion,
                                       long rulesVersion,
                                       long componentRulesVersion,
                                       long legalBasisVersion,
                                       long duration,
                                       long dossierDictionaryVersion,
                                       int analysisVersion,
                                       int analysisNumber) {

        if (isFileDeleted(fileId)) {
            return;
        }
        fileRepository.updateProcessingStatus(fileId,
                numberOfPages,
                ProcessingStatus.PROCESSED,
                dictionaryVersion,
                rulesVersion,
                componentRulesVersion,
                legalBasisVersion,
                duration,
                dossierDictionaryVersion,
                analysisVersion,
                OffsetDateTime.now().truncatedTo(ChronoUnit.MILLIS),
                OffsetDateTime.now().truncatedTo(ChronoUnit.MILLIS),
                analysisNumber,
                calculateProcessingErrorCounter(fileId, ProcessingStatus.PROCESSED));
    }


    @Transactional
    public void updateFlags(String fileId, boolean hasRedactions, boolean hasHints, boolean hasImages, boolean hasSuggestions, boolean hasComments, boolean hasUpdates) {

        if (isFileDeleted(fileId)) {
            return;
        }
        fileRepository.updateFlags(fileId, OffsetDateTime.now(), hasRedactions, hasHints, hasImages, hasSuggestions, hasComments, hasUpdates);
    }


    @Transactional
    public void updateWorkflowStatus(String fileId, WorkflowStatus workflowStatus, boolean approval) {

        if (isFileDeleted(fileId)) {
            return;
        }
        if (approval) {
            fileRepository.updateWorkflowStatus(fileId,
                    workflowStatus,
                    OffsetDateTime.now().truncatedTo(ChronoUnit.MILLIS),
                    OffsetDateTime.now().truncatedTo(ChronoUnit.MILLIS),
                    true);
        } else {
            fileRepository.updateWorkflowStatus(fileId, workflowStatus, OffsetDateTime.now().truncatedTo(ChronoUnit.MILLIS), null);
        }

    }


    @Transactional
    public void updateProcessingStatus(String fileId, ProcessingStatus processingStatus, int processingErrorCount) {

        fileRepository.updateProcessingStatus(fileId, processingStatus, OffsetDateTime.now().truncatedTo(ChronoUnit.MILLIS), processingErrorCount);
    }


    @Transactional
    public void updateStatusErrorInfo(String fileId, FileErrorInfo fileErrorInfo) {

        if (fileErrorInfo == null) {
            fileRepository.updateStatusErrorInfo(fileId, null, null, null, null);
        } else {
            fileRepository.updateStatusErrorInfo(fileId,
                    fileErrorInfo.getCause().substring(0, Math.min(fileErrorInfo.getCause().length(), 255)),
                    fileErrorInfo.getQueue(),
                    fileErrorInfo.getService(),
                    fileErrorInfo.getTimestamp());
        }

    }


    @Transactional
    public void updateProcessingStatus(String fileId, ProcessingStatus processingStatus) {

        if (isFileDeleted(fileId)) {
            return;
        }
        if (processingStatus == ProcessingStatus.PROCESSED) {
            // reset the error info
            fileRepository.updateStatusErrorInfo(fileId, null, null, null, null);
            // In case the file is updated to "processed", "lastProcessed" date should be updated to "now"
            fileRepository.updateProcessingStatus(fileId,
                    processingStatus,
                    OffsetDateTime.now().truncatedTo(ChronoUnit.MILLIS),
                    OffsetDateTime.now().truncatedTo(ChronoUnit.MILLIS),
                    calculateProcessingErrorCounter(fileId, ProcessingStatus.PROCESSED));
        } else {
            fileRepository.updateProcessingStatus(fileId,
                    processingStatus,
                    OffsetDateTime.now().truncatedTo(ChronoUnit.MILLIS),
                    calculateProcessingErrorCounter(fileId, processingStatus));
        }
    }


    @Transactional
    public void updateOCRStartTime(String fileId, OffsetDateTime time) {

        if (isFileDeleted(fileId)) {
            return;
        }
        fileRepository.updateOCRStartTime(fileId, OffsetDateTime.now().truncatedTo(ChronoUnit.MILLIS), time.truncatedTo(ChronoUnit.MILLIS));
        fileRepository.updateFileModificationDate(fileId, OffsetDateTime.now().truncatedTo(ChronoUnit.MILLIS));
    }


    @Transactional
    public void updateHasComments(String fileId, boolean hasComments) {

        if (isFileDeleted(fileId)) {
            return;
        }
        fileRepository.updateHasComments(fileId, OffsetDateTime.now().truncatedTo(ChronoUnit.MILLIS), hasComments);
    }


    @Transactional
    public void updateFileSize(String fileId, long fileSize) {

        fileRepository.updateFileSize(fileId, fileSize);
    }


    @Transactional
    public void setUpdateStatusIndexingSuccessful(String fileId) {

        if (isFileDeleted(fileId)) {
            return;
        }
        fileRepository.setUpdateStatusIndexingSuccessful(fileId,
                ProcessingStatus.PROCESSED,
                OffsetDateTime.now().truncatedTo(ChronoUnit.MILLIS),
                OffsetDateTime.now().truncatedTo(ChronoUnit.MILLIS),
                calculateProcessingErrorCounter(fileId, ProcessingStatus.PROCESSED));
    }


    @Transactional
    public void setFileAttributes(String dossierId, String fileId, Map<String, String> fileAttributes) {

        fileAttributesRepository.deleteByFileId(fileId);

        fileRepository.findById(fileId).ifPresentOrElse((file) -> {
            if (file.isSoftOrHardDeleted()) {
                return;
            }

            var fileAttributeEntities = convertFileAttributes(dossierId, file, fileAttributes);
            fileAttributesRepository.saveAllAndFlush(fileAttributeEntities);

            file.setLastFileAttributeChange(OffsetDateTime.now().truncatedTo(ChronoUnit.MILLIS));
        }, () -> {
            throw new NotFoundException(String.format("File with ID \"%s\" not found!", fileId));
        });

    }


    private List<FileAttributeEntity> convertFileAttributes(String dossierId, FileEntity file, Map<String, String> fileAttributesMap) {

        var dossier = dossierService.getAndValidateDossier(dossierId);
        List<FileAttributeConfigEntity> configuration = fileAttributeConfigPersistenceService.getFileAttributes(dossier.getDossierTemplateId());

        return fileAttributesMap.entrySet().stream().map(entry -> {
            var fa = new FileAttributeEntity();
            var id = new FileAttributeEntity.FileAttributeEntityId();
            id.setFileId(file.getId());
            id.setFileAttributeConfigId(configuration.stream()
                    .filter(c -> c.getId().equals(entry.getKey()))
                    .findAny()
                    .map(FileAttributeConfigEntity::getId)
                    .orElseThrow(() -> new BadRequestException("Invalid File Attribute Id")));
            fa.setFileAttributeId(id);
            fa.setValue(StringUtils.isBlank(entry.getValue()) ? null : entry.getValue());
            return fa;
        }).collect(Collectors.toList());
    }


    @Transactional(value = Transactional.TxType.REQUIRES_NEW)
    public void addFileAttributes(String dossierId, String fileId, Set<FileAttribute> fileAttributes) {

        fileRepository.findById(fileId).ifPresentOrElse((file) -> {
            if (file.isSoftOrHardDeleted()) {
                return;
            }

            var dossier = dossierService.getAndValidateDossier(dossierId);
            List<FileAttributeConfigEntity> configuration = fileAttributeConfigPersistenceService.getFileAttributes(dossier.getDossierTemplateId());

            fileAttributes.forEach(attributeToAdd -> {
                var configId = configuration.stream().filter(c -> c.getLabel().equals(attributeToAdd.getLabel())).map(c -> c.getId()).findFirst();
                if (configId.isPresent()) {
                    var fileAttributeEntity = new FileAttributeEntity(new FileAttributeEntity.FileAttributeEntityId(fileId, configId.get()), attributeToAdd.getValue());
                    fileAttributesRepository.saveAndFlush(fileAttributeEntity);
                }
            });

            fileRepository.updateLastAttributeChangeDate(fileId, file.getLastProcessed());

        }, () -> {
            throw new NotFoundException(String.format("File with ID \"%s\" not found!", fileId));
        });
    }


    public FileEntity getStatus(String fileId) {

        return fileRepository.findById(fileId).orElseThrow(() -> new NotFoundException(String.format("File with ID \"%s\" not found!", fileId)));
    }


    @Transactional
    public void setExcludedPages(String fileId, Set<Integer> excludedPages) {

        fileRepository.findById(fileId).ifPresentOrElse((file) -> {
            if (file.isSoftOrHardDeleted()) {
                return;
            }

            file.setLastUpdated(OffsetDateTime.now().truncatedTo(ChronoUnit.MILLIS));
            file.setExcludedPages(excludedPages);
        }, () -> {
            throw new NotFoundException(String.format("File with ID \"%s\" not found!", fileId));
        });

    }


    public List<FileEntity> getStatusesForDossier(String dossierId) {

        return fileRepository.findByDossierId(dossierId);
    }


    public List<FileEntity> getStatusesAddedBefore(OffsetDateTime end) {

        return fileRepository.findByAddedBefore(end);
    }


    public List<FileEntity> getActiveFiles(String dossierId) {

        return fileRepository.findByDossierId(dossierId).stream().filter(f -> !f.isSoftOrHardDeleted()).collect(Collectors.toList());
    }


    public List<FileEntity> getAllFiles() {

        return fileRepository.findAll();
    }


    public List<FileEntity> getSoftDeletedFiles(List<String> dossierIds) {

        return fileRepository.getSoftDeletedFiles(dossierIds);
    }


    @Transactional
    public void hardDelete(String fileId) {

        var now = OffsetDateTime.now().truncatedTo(ChronoUnit.MILLIS);

        int countUpdate = fileRepository.setHardDelete(fileId, ProcessingStatus.PROCESSED, now, now, now);
        if (countUpdate == 0) {
            throw new NotFoundException(String.format("File with ID \"%s\" not found!", fileId));
        }
        fileAttributesRepository.deleteByFileId(fileId);
    }


    @Transactional
    public void softDelete(String fileId, OffsetDateTime softDeletedTime) {

        int countUpdate = fileRepository.setSoftDelete(fileId, ProcessingStatus.PROCESSED, OffsetDateTime.now().truncatedTo(ChronoUnit.MILLIS), softDeletedTime);
        if (countUpdate == 0) {
            throw new NotFoundException(String.format("File with ID \"%s\" not found!", fileId));
        }
    }


    @Transactional
    public void undelete(String fileId) {

        fileRepository.findById(fileId).ifPresentOrElse((file) -> {
            if (file.getHardDeletedTime() != null) {
                throw new BadRequestException("Cannot undelete a hard-deleted dossier file!");
            }
        }, () -> {
            throw new NotFoundException(String.format("File with ID \"%s\" not found!", fileId));
        });
        fileRepository.setSoftDelete(fileId, ProcessingStatus.PROCESSED, OffsetDateTime.now().truncatedTo(ChronoUnit.MILLIS), null);
    }


    @Transactional
    public void setAssignee(String fileId, String currentAssignee, String lastReviewer, String lastApprover) {

        if (isFileDeleted(fileId)) {
            return;
        }
        int updateCount = fileRepository.setAssignee(fileId, currentAssignee, lastReviewer, lastApprover, OffsetDateTime.now().truncatedTo(ChronoUnit.MILLIS));
        if (updateCount == 0) {
            throw new NotFoundException(String.format("File with ID \"%s\" not found!", fileId));
        }
    }


    @Transactional
    public void toggleExclusion(String fileId, boolean excluded) {

        if (isFileDeleted(fileId)) {
            return;
        }
        int countUpdate = fileRepository.toggleExclusion(fileId, excluded, OffsetDateTime.now().truncatedTo(ChronoUnit.MILLIS));
        if (countUpdate == 0) {
            throw new NotFoundException(String.format("File with ID \"%s\" not found!", fileId));
        }
    }


    @Transactional
    public void toggleAutomaticAnalysis(String fileId, boolean excludedFromAutomaticAnalysis) {

        if (isFileDeleted(fileId)) {
            return;
        }
        int countUpdate = fileRepository.toggleAutomaticAnalysis(fileId, excludedFromAutomaticAnalysis, OffsetDateTime.now().truncatedTo(ChronoUnit.MILLIS));
        if (countUpdate == 0) {
            throw new NotFoundException(String.format("File with ID \"%s\" not found!", fileId));
        }
    }


    @Transactional
    public void overwriteFile(String fileId, String uploader, String filename, boolean keepManualRedactions) {

        int countUpdate;

        if (keepManualRedactions) {
            countUpdate = fileRepository.overwriteFileAndKeepManualRedactions(fileId,
                    filename,
                    uploader,
                    ProcessingStatus.UNPROCESSED,
                    OffsetDateTime.now().truncatedTo(ChronoUnit.MILLIS),
                    OffsetDateTime.now().truncatedTo(ChronoUnit.MILLIS));
        } else {

            fileRepository.findById(fileId).ifPresent(file -> file.setExcludedPages(new HashSet<>()));

            countUpdate = fileRepository.overwriteFile(fileId,
                    filename,
                    uploader,
                    ProcessingStatus.UNPROCESSED,
                    WorkflowStatus.NEW,
                    OffsetDateTime.now().truncatedTo(ChronoUnit.MILLIS),
                    OffsetDateTime.now().truncatedTo(ChronoUnit.MILLIS));
            deleteFileAttributes(fileId);
        }

        if (countUpdate == 0) {
            throw new NotFoundException(String.format("File with ID \"%s\" not found!", fileId));
        }
    }


    @Transactional
    public void deleteFileAttributes(String fileId) {

        fileAttributesRepository.deleteByFileId(fileId);

    }


    @Transactional
    public void setLastRedactionModificationDateForFile(String fileId, OffsetDateTime changeDate) {

        fileRepository.setLastRedactionModificationDateForFile(fileId, changeDate, OffsetDateTime.now());
    }


    @Transactional
    public void setLastManualChangeDate(String fileId, OffsetDateTime lastManualChangeDate) {

        fileRepository.setLastManualChangeDate(fileId, lastManualChangeDate, OffsetDateTime.now());
    }


    public boolean hasChangesSince(String dossierId, OffsetDateTime since) {

        return fileRepository.existsByDossierIdAndLastUpdatedIsAfter(dossierId, since);
    }


    public int countSoftDeletedFilesPerDossierTemplateId(String dossierTemplateId) {

        return fileRepository.countSoftDeletedFilesPerDossierTemplateId(dossierTemplateId);
    }


    public int countSoftDeletedFilesPerDossierId(String dossierId) {

        return fileRepository.countSoftDeletedFilesPerDossierId(dossierId);
    }


    public List<FileEntity> getAllRelevantStatusesForReanalysisScheduler(int maxRetries) {

        return fileRepository.getAllRelevantStatusesForReanalysisScheduler(maxRetries);
    }


    @Transactional
    public void updateFileModificationDate(String fileId, OffsetDateTime fileManipulationDate) {

        fileRepository.updateFileModificationDate(fileId, fileManipulationDate);
    }


    @Transactional
    public void updateHasHighlights(String fileId, boolean hasHighlights) {

        fileRepository.updateHasHighlights(fileId, hasHighlights);
    }


    public void updateOCRStatus(OCRStatusUpdateResponse response) {

        fileRepository.updateOCRStatus(response.getFileId(),
                response.getNumberOfPagesToOCR(),
                response.getNumberOfOCRedPages(),
                response.isOcrFinished() ? OffsetDateTime.now().truncatedTo(ChronoUnit.MILLIS) : null,
                OffsetDateTime.now().truncatedTo(ChronoUnit.MILLIS));
    }


    public void updateLayoutProcessedTime(String fileId) {

        fileRepository.updateLayoutProcessedTime(fileId, OffsetDateTime.now().truncatedTo(ChronoUnit.MILLIS));
    }

}
