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

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.iqser.red.service.pdftron.redaction.v1.api.model.RedactionMessage;
import com.iqser.red.service.pdftron.redaction.v1.api.model.RedactionResultDetail;
import com.iqser.red.service.pdftron.redaction.v1.api.model.RedactionResultMessage;
import com.iqser.red.service.pdftron.redaction.v1.api.model.RedactionType;
import com.iqser.red.service.persistence.management.v1.processor.configuration.MessagingConfiguration;
import com.iqser.red.service.persistence.management.v1.processor.dataexchange.FileExchangeNames;
import com.iqser.red.service.persistence.management.v1.processor.dataexchange.service.DossierTemplateExportService;
import com.iqser.red.service.persistence.management.v1.processor.entity.dossier.DossierEntity;
import com.iqser.red.service.persistence.management.v1.processor.entity.dossier.DossierTemplateEntity;
import com.iqser.red.service.persistence.management.v1.processor.entity.dossier.FileEntity;
import com.iqser.red.service.persistence.management.v1.processor.entity.dossier.ReportTemplateEntity;
import com.iqser.red.service.persistence.management.v1.processor.entity.download.DownloadRedactionFileStatusEntity;
import com.iqser.red.service.persistence.management.v1.processor.entity.download.DownloadStatusEntity;
import com.iqser.red.service.persistence.management.v1.processor.service.ColorsService;
import com.iqser.red.service.persistence.management.v1.processor.service.FileManagementStorageService;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.DossierPersistenceService;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.DossierTemplatePersistenceService;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.DownloadStatusPersistenceService;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.NotificationPersistenceService;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.ReportTemplatePersistenceService;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.repository.DownloadRedactionFileStatusRepository;
import com.iqser.red.service.persistence.management.v1.processor.service.websocket.WebsocketService;
import com.iqser.red.service.persistence.management.v1.processor.settings.FileManagementServiceSettings;
import com.iqser.red.service.persistence.management.v1.processor.utils.FileSystemBackedArchiver;
import com.iqser.red.service.persistence.service.v1.api.shared.model.audit.AddNotificationRequest;
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.DownloadFileType;
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.WorkflowStatus;
import com.iqser.red.service.persistence.service.v1.api.shared.model.download.DownloadStatusValue;
import com.iqser.red.service.redaction.report.v1.api.model.ReportResultMessage;
import com.iqser.red.service.redaction.report.v1.api.model.StoredFileInformation;
import com.knecon.fforesight.tenantcommons.TenantContext;

import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service
@RequiredArgsConstructor
public class DownloadPreparationService {

    private final DownloadStatusPersistenceService downloadStatusPersistenceService;
    private final FileManagementStorageService fileManagementStorageService;
    private final ReportTemplatePersistenceService reportTemplatePersistenceService;
    private final NotificationPersistenceService notificationPersistenceService;
    private final RabbitTemplate rabbitTemplate;
    private final ObjectMapper objectMapper;
    private final DownloadReportCleanupService downloadReportCleanupService;
    private final ColorsService colorsService;
    private final FileManagementServiceSettings settings;
    private final DossierTemplatePersistenceService dossierTemplatePersistenceService;
    private final DownloadRedactionFileStatusRepository downloadRedactionFileStatusRepository;
    private final WebsocketService websocketService;
    private final DossierPersistenceService dossierPersistenceService;
    private final DossierTemplateExportService dossierTemplateExportService;

    @Value("${storage.backend}")
    private String storageBackend;

    private static final String REPORT_INFO = "/REPORT_INFO.json";


    @Transactional
    public void createDownload(ReportResultMessage reportResultMessage) {

        var downloadId = reportResultMessage.getDownloadId();
        var downloadStatus = downloadStatusPersistenceService.getStatus(downloadId);
        RedactionMessage.RedactionMessageBuilder messageBuilder = this.generateGeneralRedactionMessage(downloadId, downloadStatus);

        downloadStatus.getFiles()
                .forEach(fileEntity -> {
                    RedactionMessage message = messageBuilder.fileId(fileEntity.getId()).unapprovedFile(fileEntity.getWorkflowStatus() != WorkflowStatus.APPROVED).build();
                    log.info("Sending redaction request for downloadId:{} fileId:{} to pdftron-redaction-queue", downloadId, fileEntity.getId());
                    rabbitTemplate.convertAndSend(MessagingConfiguration.PDFTRON_REQUEST_EXCHANGE, TenantContext.getTenantId(), message);
                });
    }


    private RedactionMessage.RedactionMessageBuilder generateGeneralRedactionMessage(String downloadId, DownloadStatusEntity downloadStatus) {

        var dossier = downloadStatus.getDossier();
        var storedPreviewColor = downloadStatus.getRedactionPreviewColor();
        var dossierTemplate = dossierTemplatePersistenceService.getDossierTemplate(dossier.getDossierTemplateId());

        final String previewColor;
        if (storedPreviewColor == null || storedPreviewColor.isBlank()) {
            var colors = colorsService.getColors(dossier.getDossierTemplateId());
            previewColor = colors.getPreviewColor();
        } else {
            previewColor = storedPreviewColor;
        }
        String appliedRedactionColor = colorsService.getColors(dossier.getDossierTemplateId()).getAppliedRedactionColor();
        return buildRedactionMessage(downloadId, downloadStatus, dossier, dossierTemplate, previewColor, appliedRedactionColor);
    }


    private RedactionMessage.RedactionMessageBuilder buildRedactionMessage(String downloadId,
                                                                           DownloadStatusEntity downloadStatus,
                                                                           DossierEntity dossier,
                                                                           DossierTemplateEntity dossierTemplate,
                                                                           String previewColor,
                                                                           String appliedRedactionColor) {

        return RedactionMessage.builder()
                .dossierId(dossier.getId())
                .downloadId(downloadId)
                .redactionTypes(toPdfTronRedactionTypes(downloadStatus.getDownloadFileTypes()))
                .redactionPreviewColor(previewColor)
                .keepImageMetaData(dossierTemplate.isKeepImageMetadata())
                .keepHiddenText(dossierTemplate.isKeepHiddenText())
                .keepOverlappingObjects(dossierTemplate.isKeepOverlappingObjects())
                .appliedRedactionColor(appliedRedactionColor);
    }


    private List<RedactionType> toPdfTronRedactionTypes(Collection<DownloadFileType> downloadFileTypes) {

        var result = new LinkedList<RedactionType>();
        for (var downloadFileType : downloadFileTypes) {
            switch (downloadFileType) {
                case REDACTED -> result.add(RedactionType.REDACTED);
                case PREVIEW, DATASET_EXPORT -> result.add(RedactionType.PREVIEW);
                case OPTIMIZED_PREVIEW -> result.add(RedactionType.OPTIMIZED_PREVIEW);
                case DELTA_PREVIEW -> result.add(RedactionType.DELTA);
                default -> {
                }
                // Other types don't need to be passed to pdf-tron
            }
        }

        return result;
    }


    @Transactional
    public void markFileAsProcessed(RedactionResultMessage redactionResultMessage) {

        downloadRedactionFileStatusRepository.save(DownloadRedactionFileStatusEntity.builder()
                                                           .id(UUID.randomUUID().toString())
                                                           .downloadStorageId(redactionResultMessage.getDownloadId())
                                                           .fileId(redactionResultMessage.getFileId())
                                                           .processingErrorCounter(0)
                                                           .details(redactionResultMessage.getRedactionResultDetails())
                                                           .build());
    }


    @Transactional
    public void markFileAsProcessingError(RedactionMessage redactionMessage) {

        downloadRedactionFileStatusRepository.save(DownloadRedactionFileStatusEntity.builder()
                                                           .id(UUID.randomUUID().toString())
                                                           .downloadStorageId(redactionMessage.getDownloadId())
                                                           .fileId(redactionMessage.getFileId())
                                                           .processingErrorCounter(1)
                                                           .build());
    }


    @Transactional
    public void increaseProcessingErrorCounter(RedactionMessage redactionMessage, Integer numberOfErrors) {

        downloadRedactionFileStatusRepository.updateStatusErrorInfo(redactionMessage.getDownloadId(), redactionMessage.getFileId(), numberOfErrors);
    }


    @Transactional
    public Optional<DownloadRedactionFileStatusEntity> getRedactionFileStatusEntry(String downloadStorageId, String fileId) {

        return downloadRedactionFileStatusRepository.findByDownloadStorageIdAndFileId(downloadStorageId, fileId);
    }


    @Transactional
    public void clearRedactionStatusEntries(String downloadStorageId) {

        downloadRedactionFileStatusRepository.deleteIfPresentByDownloadStorageId(downloadStorageId);
    }


    public void createDownload(String downloadId) {

        var redactionFileResults = downloadRedactionFileStatusRepository.findAllByDownloadStorageId(downloadId);
        DownloadStatusEntity downloadStatus = downloadStatusPersistenceService.getStatus(downloadId);

        List<StoredFileInformation> storedFileInformations = getStoredFileInformation(downloadId);

        try (FileSystemBackedArchiver fileSystemBackedArchiver = new FileSystemBackedArchiver()) {

            generateAndAddFiles(downloadStatus, redactionFileResults, fileSystemBackedArchiver);
            addReports(downloadId, downloadStatus, storedFileInformations, fileSystemBackedArchiver);
            storeZipFile(downloadStatus, fileSystemBackedArchiver);

            updateStatusToReady(downloadStatus, fileSystemBackedArchiver);

            notificationPersistenceService.insertNotification(AddNotificationRequest.builder()
                                                                      .userId(downloadStatus.getUserId())
                                                                      .issuerId(downloadStatus.getUserId())
                                                                      .notificationType("DOWNLOAD_READY")
                                                                      .target(Map.of("userId", downloadStatus.getUserId(), //
                                                                                     "dossierId", downloadStatus.getDossier().getId(), //
                                                                                     "downloadId", downloadStatus.getStatus()))
                                                                      .build());
        }

        downloadReportCleanupService.deleteTmpReportFiles(Stream.concat(storedFileInformations.stream()
                                                                                .map(StoredFileInformation::getStorageId), //
                                                                        redactionFileResults.stream()
                                                                                .map(DownloadRedactionFileStatusEntity::getDetails)
                                                                                .flatMap(List::stream)
                                                                                .map(RedactionResultDetail::getStorageId)) //
                                                                  .collect(Collectors.toSet()));
    }


    private void updateStatusToReady(DownloadStatusEntity downloadStatus, FileSystemBackedArchiver fileSystemBackedArchiver) {

        downloadStatusPersistenceService.updateStatus(downloadStatus, DownloadStatusValue.READY, fileSystemBackedArchiver.getContentLength());
        websocketService.sendDownloadEvent(downloadStatus.getStorageId(), downloadStatus.getUserId(), DownloadStatusValue.READY);
    }


    private void generateAndAddFiles(DownloadStatusEntity downloadStatus,
                                     List<DownloadRedactionFileStatusEntity> redactionFileResults,
                                     FileSystemBackedArchiver fileSystemBackedArchiver) {

        int i = 1;
        long fileGenerationStart = System.currentTimeMillis();
        for (FileEntity file : downloadStatus.getFiles()) {

            long start = System.currentTimeMillis();
            byte[] original = fileManagementStorageService.getStoredObjectBytes(file.getDossierId(), file.getId(), FileType.ORIGIN);

            var isFileApproved = WorkflowStatus.APPROVED.equals(file.getWorkflowStatus());
            String filename = isFileApproved ? file.getFilename() : "UNAPPROVED_" + file.getFilename();
            for (DownloadFileType downloadFileType : downloadStatus.getDownloadFileTypes()) {
                Optional<DownloadRedactionFileStatusEntity> redactionFileResult = redactionFileResults.stream()
                        .filter(rfr -> rfr.getFileId().equals(file.getId()))
                        .findFirst();

                if (redactionFileResult.isEmpty()) {
                    throw new RuntimeException();
                }

                if (downloadFileType.name().equals(DownloadFileType.ORIGINAL.name())) {
                    fileSystemBackedArchiver.addEntry(new FileSystemBackedArchiver.ArchiveModel("Original", filename, original));
                }
                if (downloadFileType.name().equals(DownloadFileType.PREVIEW.name())) {
                    fileSystemBackedArchiver.addEntry(new FileSystemBackedArchiver.ArchiveModel("Preview", addSuffix(filename, "highlighted"), //
                                                                                                getPreview(file.getId(), redactionFileResult.get().getDetails())));
                }
                if (downloadFileType.name().equals(DownloadFileType.OPTIMIZED_PREVIEW.name())) {
                    fileSystemBackedArchiver.addEntry(new FileSystemBackedArchiver.ArchiveModel("Optimized Preview", addSuffix(filename, "optimized_highlighted"), //
                                                                                                getOptimizedPreview(file.getId(), redactionFileResult.get().getDetails())));
                }
                if (downloadFileType.name().equals(DownloadFileType.DELTA_PREVIEW.name())) {
                    fileSystemBackedArchiver.addEntry(new FileSystemBackedArchiver.ArchiveModel("Delta Preview", addSuffix(filename, "delta_highlighted"), //
                                                                                                getDeltaPreview(file.getId(), redactionFileResult.get().getDetails())));
                }
                if (downloadFileType.name().equals(DownloadFileType.REDACTED.name()) && isFileApproved) {
                    fileSystemBackedArchiver.addEntry(new FileSystemBackedArchiver.ArchiveModel("Redacted", addSuffix(file.getFilename(), "redacted"), //
                                                                                                getRedacted(file.getId(), redactionFileResult.get().getDetails())));
                }
                if (downloadFileType.equals(DownloadFileType.DATASET_EXPORT)) {
                    String dossierName = dossierPersistenceService.getDossierNameById(file.getDossierId());
                    fileSystemBackedArchiver.addEntry(new FileSystemBackedArchiver.ArchiveModel(dossierName, file.getFilename(),//
                                                                                                getPreview(file.getId(), redactionFileResult.get().getDetails())));
                }

            }
            log.info("Successfully added file {}/{} for downloadId {}, took {}",
                     i,
                     downloadStatus.getFiles().size(),
                     downloadStatus.getStorageId(),
                     System.currentTimeMillis() - start);
            i++;
        }
        if (downloadStatus.getDownloadFileTypes().contains(DownloadFileType.DATASET_EXPORT)) {
            DossierTemplateEntity dossierTemplate = dossierTemplatePersistenceService.getDossierTemplate(downloadStatus.getDossier().getDossierTemplateId());
            dossierTemplateExportService.addDossierTemplateToArchive(fileSystemBackedArchiver, FileExchangeNames.TEMPLATE_FOLDER, dossierTemplate);
        }
        log.info("Successfully added {} files for downloadId {}, took {}",
                 downloadStatus.getFiles()
                         .stream()
                         .map(FileEntity::getId)
                         .toList(),
                 downloadStatus.getStorageId(),
                 System.currentTimeMillis() - fileGenerationStart);
    }


    private byte[] getRedacted(String fileId, List<RedactionResultDetail> redactionResultDetails) {

        return getStoredFileBytes(fileId, redactionResultDetails, RedactionType.REDACTED);
    }


    private byte[] getStoredFileBytes(String fileId, List<RedactionResultDetail> redactionResultDetails, RedactionType redactionType) {

        var redactionResultDetail = redactionResultDetails.stream()
                .filter(rrd -> rrd.getRedactionType() == redactionType)
                .findFirst();
        if (redactionResultDetail.isPresent()) {
            try (var inputStream = fileManagementStorageService.getObject(TenantContext.getTenantId(), redactionResultDetail.get().getStorageId())) {
                byte[] storedFileBytes = inputStream.readAllBytes();
                inputStream.close();
                return storedFileBytes;
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }

        throw new RuntimeException(String.format("Result for file %s of type %s not found", fileId, redactionType));
    }


    private byte[] getDeltaPreview(String fileId, List<RedactionResultDetail> redactionResultDetails) {

        return getStoredFileBytes(fileId, redactionResultDetails, RedactionType.DELTA);
    }


    private byte[] getPreview(String fileId, List<RedactionResultDetail> redactionResultDetails) {

        return getStoredFileBytes(fileId, redactionResultDetails, RedactionType.PREVIEW);
    }


    private byte[] getOptimizedPreview(String fileId, List<RedactionResultDetail> redactionResultDetails) {

        return getStoredFileBytes(fileId, redactionResultDetails, RedactionType.OPTIMIZED_PREVIEW);
    }


    private void addReports(String downloadId,
                            DownloadStatusEntity downloadStatus,
                            List<StoredFileInformation> storedFileInformations,
                            FileSystemBackedArchiver fileSystemBackedArchiver) {

        long addReportsStart = System.currentTimeMillis();

        Map<String, FileEntity> fileById = downloadStatus.getFiles()
                .stream()
                .collect(Collectors.toMap(FileEntity::getId, a -> a));

        for (StoredFileInformation storedFileInformation : storedFileInformations) {

            FileEntity fileStatus = null;

            if (storedFileInformation.getFileId() != null) {
                fileStatus = fileById.get(storedFileInformation.getFileId());
            }

            ReportTemplateEntity reportTemplate = reportTemplatePersistenceService.find(storedFileInformation.getTemplateId());
            byte[] report = fileManagementStorageService.getStoredObjectBytes(storedFileInformation.getStorageId());
            fileSystemBackedArchiver.addEntry(new FileSystemBackedArchiver.ArchiveModel(removeExtension(reportTemplate.getFileName())
                                                                                        + (reportTemplate.isMultiFileReport() ? " (multifile)" : ""),
                                                                                        settings.isScmEnabled() ? createFileName(fileStatus, reportTemplate) : addSuffix(
                                                                                                createFileName(fileStatus, reportTemplate),
                                                                                                "justification" + (storedFileInformation.getDocumentPartNr() != 0 ? "_"
                                                                                                                                                                    + storedFileInformation.getDocumentPartNr() : "")),
                                                                                        report));

        }
        log.info("Successfully added {} reports for downloadId {}, took {}", storedFileInformations.size(), downloadId, System.currentTimeMillis() - addReportsStart);
    }


    @SneakyThrows
    private List<StoredFileInformation> getStoredFileInformation(String downloadId) {

        String storageId;
        if (storageBackend.equals("s3")) {
            storageId = generateReportJsonStorageIdForS3(downloadId);
        } else {
            storageId = generateReportJsonStorageIdForAzure(downloadId);
        }
        if (!fileManagementStorageService.objectExists(storageId)) {
            log.warn("Could not find stored file information with id {}", storageId);
            return new ArrayList<>();
        }
        return objectMapper.readValue(fileManagementStorageService.getStoredObjectBytes(storageId), new TypeReference<>() {
        });
    }


    private String generateReportJsonStorageIdForS3(String storageId) {

        return storageId.substring(0, storageId.length() - 3) + REPORT_INFO;
    }


    private String generateReportJsonStorageIdForAzure(String storageId) {

        return storageId.substring(0, storageId.length() - 4) + REPORT_INFO;
    }


    private String createFileName(FileEntity fileStatus, ReportTemplateEntity reportTemplate) {

        if (fileStatus != null) {
            var isFileApproved = WorkflowStatus.APPROVED.equals(fileStatus.getWorkflowStatus());
            String filename = isFileApproved ? fileStatus.getFilename() : "UNAPPROVED_" + fileStatus.getFilename();
            return removeExtension(filename) + getExtension(reportTemplate.getFileName());
        } else {
            return reportTemplate.getFileName();
        }
    }


    private String getExtension(String fileName) {

        var index = fileName.lastIndexOf(".");
        if (index > 0) {
            return fileName.substring(index);
        } else {
            return "";
        }
    }


    private String removeExtension(String fileName) {

        var index = fileName.lastIndexOf(".");
        if (index > 0) {
            return fileName.substring(0, index);
        } else {
            return fileName;
        }
    }


    private String addSuffix(String filename, String suffix) {

        var oldExtension = getExtension(filename);
        return removeExtension(filename) + "_" + suffix + oldExtension;

    }


    @SneakyThrows
    private void storeZipFile(DownloadStatusEntity downloadStatus, FileSystemBackedArchiver fileSystemBackedArchiver) {

        long start = System.currentTimeMillis();
        try (var in = fileSystemBackedArchiver.toInputStream()) {
            fileManagementStorageService.storeObject(downloadStatus.getStorageId(), in);
        }
        log.info("Successfully stored zip for downloadId {}, took {}", downloadStatus.getStorageId(), System.currentTimeMillis() - start);
    }

}
