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

import static com.knecon.fforesight.databasetenantcommons.providers.utils.MagicConverter.convert;

import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.stream.Collectors;

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

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.iqser.red.service.persistence.management.v1.processor.configuration.MessagingConfiguration;
import com.iqser.red.service.persistence.management.v1.processor.entity.configuration.BaseDictionaryEntry;
import com.iqser.red.service.persistence.management.v1.processor.entity.configuration.TypeEntity;
import com.iqser.red.service.persistence.management.v1.processor.entity.download.DownloadStatusEntity;
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.DownloadJob;
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.WatermarkService;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.DictionaryPersistenceService;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.DossierAttributeConfigPersistenceService;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.DossierStatusPersistenceService;
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.EntryPersistenceService;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.FileAttributeConfigPersistenceService;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.LegalBasisMappingPersistenceService;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.ReportTemplatePersistenceService;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.RulesPersistenceService;
import com.iqser.red.service.persistence.management.v1.processor.utils.FileSystemBackedArchiver;
import com.iqser.red.service.persistence.management.v1.processor.utils.StorageIdUtils;
import com.iqser.red.service.persistence.service.v1.api.shared.model.RuleFileType;
import com.iqser.red.service.persistence.service.v1.api.shared.model.WatermarkModel;
import com.iqser.red.service.persistence.service.v1.api.shared.model.common.JSONPrimitive;
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.DossierAttributeConfig;
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.DossierTemplate;
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.FileAttributesGeneralConfiguration;
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.ReportTemplate;
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.configuration.Colors;
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.dossier.DossierStatusInfo;
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.dossier.file.FileAttributeConfig;
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.importexport.ExportDownloadRequest;
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.importexport.ExportFilename;
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.legalbasis.LegalBasis;
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.type.DictionaryEntryType;
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.type.Type;
import com.iqser.red.service.persistence.service.v1.api.shared.model.download.DownloadStatusValue;

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

@Slf4j
@Service
@RequiredArgsConstructor
public class DossierTemplateExportService {

    private static final String JSON_EXT = ".json";
    private static final String TXT_EXT = ".txt";
    private final DossierTemplatePersistenceService dossierTemplatePersistenceService;
    private final DownloadStatusPersistenceService downloadStatusPersistenceService;
    private final DossierAttributeConfigPersistenceService dossierAttributeConfigPersistenceService;
    private final DossierStatusPersistenceService dossierStatusPersistenceService;
    private final DictionaryPersistenceService dictionaryPersistenceService;
    private final FileAttributeConfigPersistenceService fileAttributeConfigPersistenceService;
    private final LegalBasisMappingPersistenceService legalBasisMappingPersistenceService;
    private final RulesPersistenceService rulesPersistenceService;
    private final FileManagementStorageService fileManagementStorageService;
    private final ReportTemplatePersistenceService reportTemplatePersistenceService;
    private final ColorsService colorsService;
    private final EntryPersistenceService entryPersistenceService;
    private final ObjectMapper objectMapper = new ObjectMapper();

    private final WatermarkService watermarkService;

    private final RabbitTemplate rabbitTemplate;


    public JSONPrimitive<String> prepareExportDownload(ExportDownloadRequest request) {

        var mimeType = "application/zip";

        String downloadFilename = request.getDossierTemplateId() + ".zip";
        String storageId = StorageIdUtils.getStorageId(request.getUserId(), request.getDossierTemplateId());

        downloadStatusPersistenceService.createStatus(request.getUserId(), storageId, downloadFilename, mimeType);
        addToExportDownloadQueue(DownloadJob.builder().storageId(storageId).userId(request.getUserId()).build(), 1);

        return new JSONPrimitive<>(storageId);
    }


    private void addToExportDownloadQueue(DownloadJob downloadJob, int priority) {

        rabbitTemplate.convertAndSend(MessagingConfiguration.EXPORT_DOWNLOAD_QUEUE, downloadJob, message -> {
            message.getMessageProperties().setPriority(priority);
            return message;
        });
    }


    @SneakyThrows
    @Transactional
    public void createDownloadArchive(DownloadJob downloadJob) {

        objectMapper.registerModule(new JavaTimeModule());
        DownloadStatusEntity downloadStatus = downloadStatusPersistenceService.getStatus(downloadJob.getStorageId());

        String dossierTemplateId = extractDossierTemplateId(downloadStatus.getFilename());
        var dossierTemplate = dossierTemplatePersistenceService.getDossierTemplate(dossierTemplateId);

        try (FileSystemBackedArchiver fileSystemBackedArchiver = new FileSystemBackedArchiver()) {
            // add dossier template name and meta data
            fileSystemBackedArchiver.addEntries(new FileSystemBackedArchiver.ArchiveModel(null,
                    getFilename(ExportFilename.DOSSIER_TEMPLATE_META, JSON_EXT),
                    objectMapper.writeValueAsBytes(convert(dossierTemplate, DossierTemplate.class))));

            //add watermark json file
            var watermarkList = watermarkService.getWatermarksForDossierTemplateId(dossierTemplateId);
            fileSystemBackedArchiver.addEntry(new FileSystemBackedArchiver.ArchiveModel(null,
                    getFilename(ExportFilename.WATERMARK, JSON_EXT),
                    objectMapper.writeValueAsBytes(convert(watermarkList, WatermarkModel.class))));

            //add colors json file
            var colors = colorsService.getColors(dossierTemplateId);
            fileSystemBackedArchiver.addEntries(new FileSystemBackedArchiver.ArchiveModel(null,
                    getFilename(ExportFilename.COLORS, JSON_EXT),
                    objectMapper.writeValueAsBytes(convert(colors, Colors.class))));

            // add dossier statuses
            var dossierStatusList = dossierStatusPersistenceService.getAllDossierStatusForTemplate(dossierTemplateId);
            fileSystemBackedArchiver.addEntries(new FileSystemBackedArchiver.ArchiveModel(null,
                    getFilename(ExportFilename.DOSSIER_STATUS, JSON_EXT),
                    objectMapper.writeValueAsBytes(convert(dossierStatusList, DossierStatusInfo.class))));

            // add dossier attributes config json
            var dossierAttributesConfig = dossierAttributeConfigPersistenceService.getDossierAttributes(dossierTemplateId);
            fileSystemBackedArchiver.addEntries(new FileSystemBackedArchiver.ArchiveModel(null,
                    getFilename(ExportFilename.DOSSIER_ATTRIBUTES_CONFIG, JSON_EXT),
                    objectMapper.writeValueAsBytes(convert(dossierAttributesConfig, DossierAttributeConfig.class))));

            // add file attribute configs
            var fileAttributeConfigList = fileAttributeConfigPersistenceService.getFileAttributes(dossierTemplateId);
            fileSystemBackedArchiver.addEntries(new FileSystemBackedArchiver.ArchiveModel(null,
                    getFilename(ExportFilename.FILE_ATTRIBUTE_CONFIG, JSON_EXT),
                    objectMapper.writeValueAsBytes(convert(fileAttributeConfigList, FileAttributeConfig.class))));

            // add legal basis mapping
            var legalBasisMappingList = legalBasisMappingPersistenceService.getLegalBasisMapping(dossierTemplateId);
            fileSystemBackedArchiver.addEntries(new FileSystemBackedArchiver.ArchiveModel(null,
                    getFilename(ExportFilename.LEGAL_BASIS, JSON_EXT),
                    objectMapper.writeValueAsBytes(convert(legalBasisMappingList, LegalBasis.class))));

            // add file attributes general configuration
            try {
                var fileAttributesGeneralConfig = fileAttributeConfigPersistenceService.getFileAttributesGeneralConfiguration(dossierTemplateId);
                fileSystemBackedArchiver.addEntries(new FileSystemBackedArchiver.ArchiveModel(null,
                        getFilename(ExportFilename.FILE_ATTRIBUTE_GENERAL_CONFIG, JSON_EXT),
                        objectMapper.writeValueAsBytes(convert(fileAttributesGeneralConfig, FileAttributesGeneralConfiguration.class))));
            } catch (NotFoundException e) {
                log.debug("file attributes general config not found");
            }

            // add rule set
            var ruleSet = rulesPersistenceService.getRules(dossierTemplateId, RuleFileType.ENTITY);
            fileSystemBackedArchiver.addEntries(new FileSystemBackedArchiver.ArchiveModel(null,
                    getFilename(ExportFilename.RULES, TXT_EXT),
                    objectMapper.writeValueAsBytes(ruleSet.getValue())));

            // add component rule set
            try {
                var componentRuleSet = rulesPersistenceService.getRules(dossierTemplateId, RuleFileType.COMPONENT);
                fileSystemBackedArchiver.addEntries(new FileSystemBackedArchiver.ArchiveModel(null,
                        getFilename(ExportFilename.COMPONENT_RULES, TXT_EXT),
                        objectMapper.writeValueAsBytes(componentRuleSet.getValue())));
            } catch (NotFoundException e) {
                log.debug("No component rules present.");
            }

            //N files with the related report templates
            var reportTemplateList = reportTemplatePersistenceService.findByDossierTemplateId(dossierTemplate.getId());
            fileSystemBackedArchiver.addEntries(new FileSystemBackedArchiver.ArchiveModel(null,
                    getFilename(ExportFilename.REPORT_TEMPLATE, JSON_EXT),
                    objectMapper.writeValueAsBytes(convert(reportTemplateList, ReportTemplate.class))));
            reportTemplateList.forEach(reportTemplate -> {
                var report = fileManagementStorageService.getStoredObjectBytes(reportTemplate.getStorageId());
                fileSystemBackedArchiver.addEntries(new FileSystemBackedArchiver.ArchiveModel(null,
                        reportTemplate.isMultiFileReport() ? reportTemplate.getFileName() + ExportFilename.REPORT_TEMPLATE_MULTI_FILE.getFilename() : reportTemplate.getFileName(),
                        report));
            });
            // for every type 1 folder with:
            // 1 json file containing meta info of the type
            // and 1 txt file for every type: entries, false positives and false recommendation

            // remove the types related to dossiers and also the ones that are deleted
            var dossierTypes = dictionaryPersistenceService.getAllTypesForDossierTemplate(dossierTemplateId, false);
            for (TypeEntity typeEntity : dossierTypes) {
//                log.info("type: " + typeEntity.getType() + " " + typeEntity.getDossierId() + " " + typeEntity.isDeleted());
                fileSystemBackedArchiver.addEntries(new FileSystemBackedArchiver.ArchiveModel(typeEntity.getType(),
                        getFilename(ExportFilename.DOSSIER_TYPE, JSON_EXT),
                        objectMapper.writeValueAsBytes(convert(typeEntity, Type.class))));

                var entriesValuesList = entryPersistenceService.getEntries(typeEntity.getId(), DictionaryEntryType.ENTRY, null)
                        .stream()
                        .map(BaseDictionaryEntry::getValue)
                        .collect(Collectors.toList());
                var falsePositiveValuesList = entryPersistenceService.getEntries(typeEntity.getId(), DictionaryEntryType.FALSE_POSITIVE, null)
                        .stream()
                        .map(BaseDictionaryEntry::getValue)
                        .collect(Collectors.toList());
                var falseRecommendationValuesList = entryPersistenceService.getEntries(typeEntity.getId(), DictionaryEntryType.FALSE_RECOMMENDATION, null)
                        .stream()
                        .map(BaseDictionaryEntry::getValue)
                        .collect(Collectors.toList());

                writeEntriesListToFile(fileSystemBackedArchiver, entriesValuesList, typeEntity.getType(), getFilename(ExportFilename.ENTRIES, TXT_EXT));
                writeEntriesListToFile(fileSystemBackedArchiver, falsePositiveValuesList, typeEntity.getType(), getFilename(ExportFilename.FALSE_POSITIVES, TXT_EXT));
                writeEntriesListToFile(fileSystemBackedArchiver, falseRecommendationValuesList, typeEntity.getType(), getFilename(ExportFilename.FALSE_RECOMMENDATION, TXT_EXT));
            }

            storeZipFile(downloadStatus.getStorageId(), fileSystemBackedArchiver);
            downloadStatusPersistenceService.updateStatus(downloadStatus, DownloadStatusValue.READY, fileSystemBackedArchiver.getContentLength());

        } catch (JsonProcessingException e) {
            log.debug("fail ", e);
            throw new BadRequestException("Error creating the archive"); // update error handling
        }

    }


    private String extractDossierTemplateId(String fileName) {

        var lastIndexOfDot = fileName.lastIndexOf(".");
        return fileName.substring(0, lastIndexOfDot);
    }


    private String getFilename(ExportFilename exportFilename, String extension) {

        return exportFilename.getFilename() + extension;
    }


    private void writeEntriesListToFile(FileSystemBackedArchiver fileSystemBackedArchiver, List<String> entriesValuesList, String folderName, String filename) {

        try {
            ByteArrayOutputStream bt = new ByteArrayOutputStream();
            DataOutputStream dt = new DataOutputStream(bt);
            for (String entry : entriesValuesList) {
                dt.writeUTF(entry);
            }
            fileSystemBackedArchiver.addEntries(new FileSystemBackedArchiver.ArchiveModel(folderName, filename, bt.toByteArray()));
            dt.close();
            bt.close();
        } catch (IOException e) {
            log.debug("Error writing values to files");
        }
    }


    private void storeZipFile(String storageId, FileSystemBackedArchiver fileSystemBackedArchiver) {

        long start = System.currentTimeMillis();
        try (InputStream data = fileSystemBackedArchiver.toInputStream()) {
            fileManagementStorageService.storeObject(storageId, data);
        } catch (IOException ex) {
            log.warn("IO error when sending file to storage", ex);
        }
        log.info("Successfully stored zip for downloadId {}, took {}", storageId, System.currentTimeMillis() - start);
    }

}
