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

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

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Optional;

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

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.dataexchange.models.DateFormatsExportModel;
import com.iqser.red.service.persistence.management.v1.processor.entity.configuration.ColorsEntity;
import com.iqser.red.service.persistence.management.v1.processor.entity.configuration.DateFormatsEntity;
import com.iqser.red.service.persistence.management.v1.processor.entity.configuration.FileAttributesGeneralConfigurationEntity;
import com.iqser.red.service.persistence.management.v1.processor.entity.configuration.LegalBasisEntity;
import com.iqser.red.service.persistence.management.v1.processor.entity.configuration.RuleSetEntity;
import com.iqser.red.service.persistence.management.v1.processor.entity.configuration.TypeEntity;
import com.iqser.red.service.persistence.management.v1.processor.entity.configuration.WatermarkEntity;
import com.iqser.red.service.persistence.management.v1.processor.entity.dossier.ComponentDefinitionEntity;
import com.iqser.red.service.persistence.management.v1.processor.entity.dossier.DossierAttributeConfigEntity;
import com.iqser.red.service.persistence.management.v1.processor.entity.dossier.DossierTemplateEntity;
import com.iqser.red.service.persistence.management.v1.processor.entity.dossier.FileAttributeConfigEntity;
import com.iqser.red.service.persistence.management.v1.processor.entity.dossier.ReportTemplateEntity;
import com.iqser.red.service.persistence.management.v1.processor.entity.download.DownloadStatusEntity;
import com.iqser.red.service.persistence.management.v1.processor.exception.NotFoundException;
import com.iqser.red.service.persistence.management.v1.processor.dataexchange.ExportDownloadMessage;
import com.iqser.red.service.persistence.management.v1.processor.dataexchange.models.ExportDownloadRequest;
import com.iqser.red.service.persistence.management.v1.processor.dataexchange.ExportFilename;
import com.iqser.red.service.persistence.management.v1.processor.dataexchange.models.RulesExportModel;
import com.iqser.red.service.persistence.management.v1.processor.model.ComponentMapping;
import com.iqser.red.service.persistence.management.v1.processor.service.ColorsService;
import com.iqser.red.service.persistence.management.v1.processor.service.ComponentMappingService;
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.ComponentDefinitionPersistenceService;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.DateFormatsPersistenceService;
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.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.DossierAttributeConfigMapper;
import com.iqser.red.service.persistence.management.v1.processor.utils.FileAttributeConfigMapper;
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.component.ComponentMappingMetadata;
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.legalbasis.LegalBasis;
import com.knecon.fforesight.tenantcommons.TenantContext;

import io.micrometer.observation.annotation.Observed;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.experimental.FieldDefaults;
import lombok.extern.slf4j.Slf4j;

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

    static String JSON_EXT = ".json";
    static String TXT_EXT = ".txt";
    static String RULE_EXT = ".drl";
    DossierTemplatePersistenceService dossierTemplatePersistenceService;
    DownloadStatusPersistenceService downloadStatusPersistenceService;
    DossierAttributeConfigPersistenceService dossierAttributeConfigPersistenceService;
    DossierStatusPersistenceService dossierStatusPersistenceService;
    DictionaryPersistenceService dictionaryPersistenceService;
    FileAttributeConfigPersistenceService fileAttributeConfigPersistenceService;
    LegalBasisMappingPersistenceService legalBasisMappingPersistenceService;
    RulesPersistenceService rulesPersistenceService;
    DateFormatsPersistenceService dateFormatsPersistenceService;
    FileManagementStorageService fileManagementStorageService;
    ReportTemplatePersistenceService reportTemplatePersistenceService;
    ComponentMappingService componentMappingService;
    ColorsService colorsService;
    ObjectMapper objectMapper;
    EntityTypeExportService entityTypeExportService;
    ComponentDefinitionPersistenceService componentDefinitionPersistenceService;

    WatermarkService watermarkService;

    RabbitTemplate rabbitTemplate;


    public JSONPrimitive<String> prepareExportDownload(ExportDownloadRequest request) {

        var mimeType = "application/zip";

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

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

        return new JSONPrimitive<>(storageId);
    }


    private void addToExportDownloadQueue(ExportDownloadMessage downloadJob, int priority) {

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


    @Observed(name = "DossierTemplateExportService", contextualName = "export-dossier-template")
    public long createDownloadArchive(ExportDownloadMessage downloadJob) throws IOException {

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

        var dossierTemplate = dossierTemplatePersistenceService.getDossierTemplate(downloadJob.getDossierTemplateId());

        try (FileSystemBackedArchiver fileSystemBackedArchiver = new FileSystemBackedArchiver()) {

            addDossierTemplateToArchive(fileSystemBackedArchiver, null, dossierTemplate);

            storeZipFile(downloadStatus.getStorageId(), fileSystemBackedArchiver);

            return fileSystemBackedArchiver.getContentLength();
        }

    }


    @SneakyThrows
    @Observed(name = "DossierTemplateExportService", contextualName = "write-dossier-template-to-archive")
    public void addDossierTemplateToArchive(FileSystemBackedArchiver fileSystemBackedArchiver, String folder, DossierTemplateEntity dossierTemplate) {

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

        //add watermark json file
        List<WatermarkEntity> watermarkList = watermarkService.getWatermarksForDossierTemplateId(dossierTemplate.getId());
        fileSystemBackedArchiver.addEntry(new FileSystemBackedArchiver.ArchiveModel(folder,
                                                                                    getFilename(ExportFilename.WATERMARK, JSON_EXT),
                                                                                    objectMapper.writeValueAsBytes(convert(watermarkList, WatermarkModel.class))));

        //add colors json file
        ColorsEntity colors = colorsService.getColors(dossierTemplate.getId());
        fileSystemBackedArchiver.addEntries(new FileSystemBackedArchiver.ArchiveModel(folder,
                                                                                      getFilename(ExportFilename.COLORS, JSON_EXT),
                                                                                      objectMapper.writeValueAsBytes(convert(colors, Colors.class))));

        // add dossier statuses
        List<DossierStatusInfo> dossierStatusList = dossierStatusPersistenceService.getAllDossierStatusForTemplateWithoutSecurity(dossierTemplate.getId());
        fileSystemBackedArchiver.addEntries(new FileSystemBackedArchiver.ArchiveModel(folder,
                                                                                      getFilename(ExportFilename.DOSSIER_STATUS, JSON_EXT),
                                                                                      objectMapper.writeValueAsBytes(convert(dossierStatusList, DossierStatusInfo.class))));

        // add dossier attributes config json
        List<DossierAttributeConfigEntity> dossierAttributesConfig = dossierAttributeConfigPersistenceService.getDossierAttributes(dossierTemplate.getId());
        fileSystemBackedArchiver.addEntries(new FileSystemBackedArchiver.ArchiveModel(folder,
                                                                                      getFilename(ExportFilename.DOSSIER_ATTRIBUTES_CONFIG, JSON_EXT),
                                                                                      objectMapper.writeValueAsBytes(convert(dossierAttributesConfig,
                                                                                                                             DossierAttributeConfig.class,
                                                                                                                             new DossierAttributeConfigMapper()))));

        // add file attribute configs
        List<FileAttributeConfigEntity> fileAttributeConfigList = fileAttributeConfigPersistenceService.getFileAttributes(dossierTemplate.getId());
        fileSystemBackedArchiver.addEntries(new FileSystemBackedArchiver.ArchiveModel(folder,
                                                                                      getFilename(ExportFilename.FILE_ATTRIBUTE_CONFIG, JSON_EXT),
                                                                                      objectMapper.writeValueAsBytes(convert(fileAttributeConfigList,
                                                                                                                             FileAttributeConfig.class,
                                                                                                                             new FileAttributeConfigMapper()))));

        // add legal basis mapping
        List<LegalBasisEntity> legalBasisMappingList = legalBasisMappingPersistenceService.getLegalBasisMapping(dossierTemplate.getId());
        fileSystemBackedArchiver.addEntries(new FileSystemBackedArchiver.ArchiveModel(folder,
                                                                                      getFilename(ExportFilename.LEGAL_BASIS, JSON_EXT),
                                                                                      objectMapper.writeValueAsBytes(convert(legalBasisMappingList, LegalBasis.class))));

        // add file attributes general configuration
        try {
            FileAttributesGeneralConfigurationEntity fileAttributesGeneralConfig = fileAttributeConfigPersistenceService.getFileAttributesGeneralConfiguration(dossierTemplate.getId());
            fileSystemBackedArchiver.addEntries(new FileSystemBackedArchiver.ArchiveModel(folder,
                                                                                          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
        Optional<RuleSetEntity> ruleSetOptional = rulesPersistenceService.getRules(dossierTemplate.getId(), RuleFileType.ENTITY);
        if (ruleSetOptional.isEmpty()) {
            throw new NotFoundException(String.format("No rule file of type %s found for dossierTemplateId %s", RuleFileType.ENTITY, dossierTemplate.getId()));
        }
        RuleSetEntity ruleSet = ruleSetOptional.get();
        fileSystemBackedArchiver.addEntries(new FileSystemBackedArchiver.ArchiveModel(folder,
                                                                                      getFilename(ExportFilename.RULES, RULE_EXT),
                                                                                      ruleSet.getValue().getBytes(StandardCharsets.UTF_8)));
        RulesExportModel rulesExportModel = RulesExportModel.builder().version(ruleSet.getVersion()).build();
        fileSystemBackedArchiver.addEntries(new FileSystemBackedArchiver.ArchiveModel(folder,
                                                                                      getFilename(ExportFilename.RULES, JSON_EXT),
                                                                                      objectMapper.writeValueAsBytes(rulesExportModel)));

        // add component rule set
        try {
            Optional<RuleSetEntity> componentRuleSetOptional = rulesPersistenceService.getRules(dossierTemplate.getId(), RuleFileType.COMPONENT);
            if (componentRuleSetOptional.isEmpty()) {
                throw new NotFoundException(String.format("No rule file of type %s found for dossierTemplateId %s", RuleFileType.COMPONENT, dossierTemplate.getId()));
            }
            RuleSetEntity componentRuleSet = componentRuleSetOptional.get();
            fileSystemBackedArchiver.addEntries(new FileSystemBackedArchiver.ArchiveModel(folder,
                                                                                          getFilename(ExportFilename.COMPONENT_RULES, RULE_EXT),
                                                                                          componentRuleSet.getValue().getBytes(StandardCharsets.UTF_8)));
            RulesExportModel componentRuleExportModel = RulesExportModel.builder().version(componentRuleSet.getVersion()).build();
            fileSystemBackedArchiver.addEntries(new FileSystemBackedArchiver.ArchiveModel(folder,
                                                                                          getFilename(ExportFilename.COMPONENT_RULES, JSON_EXT),
                                                                                          objectMapper.writeValueAsBytes(componentRuleExportModel)));
        } catch (NotFoundException e) {
            log.debug("No component rules present.");
        }

        // add date formats
        try {
            Optional<DateFormatsEntity> dateFormatsOptional = dateFormatsPersistenceService.getDateFormats(dossierTemplate.getId());
            if (dateFormatsOptional.isEmpty()) {
                throw new NotFoundException(String.format("No date formats found for dossierTemplateId %s", dossierTemplate.getId()));
            }
            DateFormatsEntity dateFormats = dateFormatsOptional.get();
            fileSystemBackedArchiver.addEntries(new FileSystemBackedArchiver.ArchiveModel(folder,
                                                                                          getFilename(ExportFilename.DATE_FORMATS, TXT_EXT),
                                                                                          dateFormats.getValue().getBytes(StandardCharsets.UTF_8)));
            DateFormatsExportModel dateFormatsExportModel = DateFormatsExportModel.builder().version(dateFormats.getVersion()).build();
            fileSystemBackedArchiver.addEntries(new FileSystemBackedArchiver.ArchiveModel(folder,
                                                                                          getFilename(ExportFilename.DATE_FORMATS, JSON_EXT),
                                                                                          objectMapper.writeValueAsBytes(dateFormatsExportModel)));
        } catch (NotFoundException e) {
            log.debug("No date formats present.");
        }

        //N files with the related report templates
        List<ReportTemplateEntity> reportTemplateList = reportTemplatePersistenceService.findByDossierTemplateId(dossierTemplate.getId());
        fileSystemBackedArchiver.addEntries(new FileSystemBackedArchiver.ArchiveModel(folder,
                                                                                      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(folder,
                                                                                          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
        List<TypeEntity> dossierTypes = dictionaryPersistenceService.getAllTypesForDossierTemplate(dossierTemplate.getId(), false);
        for (TypeEntity typeEntity : dossierTypes) {
//                log.info("type: " + typeEntity.getType() + " " + typeEntity.getDossierId() + " " + typeEntity.isDeleted());
            entityTypeExportService.addEntityTypeToArchive(fileSystemBackedArchiver, typeEntity, folder);
        }

        Path mappingDir = Files.createTempDirectory("mappings");

        List<ComponentMapping> componentMappings = componentMappingService.getMappingFilesByDossierTemplateId(dossierTemplate.getId(), mappingDir);

        List<ComponentMappingMetadata> metadata = componentMappings.stream()
                .map(ComponentMapping::metaData)
                .toList();
        fileSystemBackedArchiver.addEntries(new FileSystemBackedArchiver.ArchiveModel(resolveFolder(folder, ExportFilename.COMPONENT_MAPPINGS_FOLDER.getFilename()),
                                                                                      ExportFilename.COMPONENT_MAPPINGS_FILE.getFilename() + JSON_EXT,
                                                                                      objectMapper.writeValueAsBytes(metadata)));
        for (ComponentMapping componentMapping : componentMappings) {
            try (var in = new FileInputStream(componentMapping.file())) {
                fileSystemBackedArchiver.addEntries(new FileSystemBackedArchiver.ArchiveModel(resolveFolder(folder, ExportFilename.COMPONENT_MAPPINGS_FOLDER.getFilename()),
                                                                                              componentMapping.metaData().getName() + ".csv",
                                                                                              in.readAllBytes()));
            }
        }

        FileSystemUtils.deleteRecursively(mappingDir);

        // components
        List<ComponentDefinitionEntity> componentDefinitions = componentDefinitionPersistenceService.findByDossierTemplateIdAndNotSoftDeleted(dossierTemplate.getId());
        for (ComponentDefinitionEntity componentDefinition : componentDefinitions) {
            fileSystemBackedArchiver.addEntries(new FileSystemBackedArchiver.ArchiveModel(resolveFolder(folder, componentDefinition.getTechnicalName()),
                                                                                          getFilename(ExportFilename.COMPONENTS, JSON_EXT),
                                                                                          objectMapper.writeValueAsBytes(componentDefinition)));
        }
    }


    private static String resolveFolder(String folder, String name) {

        if (folder == null) {
            return name;
        }
        return folder + "/" + name;
    }


    private String getFilename(ExportFilename exportFilename, String extension) {

        return exportFilename.getFilename() + extension;
    }


    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);
    }

}
