package com.iqser.red.persistence.service.v2.external.api.impl.controller;

import static com.iqser.red.service.persistence.management.v1.processor.roles.ActionRoles.GET_REPORT_TEMPLATES;
import static com.iqser.red.service.persistence.management.v1.processor.roles.ActionRoles.READ_DATA_FORMATS;
import static com.iqser.red.service.persistence.management.v1.processor.roles.ActionRoles.READ_DOSSIER_ATTRIBUTES_CONFIG;
import static com.iqser.red.service.persistence.management.v1.processor.roles.ActionRoles.READ_DOSSIER_STATUS;
import static com.iqser.red.service.persistence.management.v1.processor.roles.ActionRoles.READ_FILE_ATTRIBUTES_CONFIG;
import static com.iqser.red.service.persistence.management.v1.processor.roles.ActionRoles.READ_RULES;
import static com.iqser.red.service.persistence.management.v1.processor.roles.ActionRoles.WRITE_DATA_FORMATS;
import static com.iqser.red.service.persistence.management.v1.processor.roles.ActionRoles.WRITE_RULES;
import static java.lang.String.format;

import java.io.ByteArrayInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;

import org.springframework.core.io.InputStreamResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import com.google.common.base.Strings;
import com.iqser.red.persistence.service.v1.external.api.impl.controller.DossierTemplateController;
import com.iqser.red.persistence.service.v1.external.api.impl.controller.FileAttributesController;
import com.iqser.red.persistence.service.v2.external.api.impl.mapper.ComponentMappingMapper;
import com.iqser.red.service.persistence.management.v1.processor.entity.configuration.DateFormatsEntity;
import com.iqser.red.service.persistence.management.v1.processor.entity.configuration.RuleSetEntity;
import com.iqser.red.service.persistence.management.v1.processor.entity.dossier.ComponentDefinitionEntity;
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.ComponentMappingDownloadModel;
import com.iqser.red.service.persistence.management.v1.processor.service.ComponentDefinitionService;
import com.iqser.red.service.persistence.management.v1.processor.service.ComponentMappingService;
import com.iqser.red.service.persistence.management.v1.processor.service.DateFormatsValidationService;
import com.iqser.red.service.persistence.management.v1.processor.service.RulesValidationService;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.AuditPersistenceService;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.DateFormatsPersistenceService;
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.ReportTemplatePersistenceService;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.RulesPersistenceService;
import com.iqser.red.service.persistence.management.v1.processor.utils.RulesValidationMapper;
import com.iqser.red.service.persistence.management.v1.processor.utils.StringEncodingUtils;
import com.iqser.red.service.persistence.service.v1.api.shared.model.AuditCategory;
import com.iqser.red.service.persistence.service.v1.api.shared.model.DossierTemplateModel;
import com.iqser.red.service.persistence.service.v1.api.shared.model.RuleFileType;
import com.iqser.red.service.persistence.service.v1.api.shared.model.audit.AuditRequest;
import com.iqser.red.service.persistence.service.v1.api.shared.model.component.ComponentDefinition;
import com.iqser.red.service.persistence.service.v1.api.shared.model.component.ComponentDefinitionAddRequest;
import com.iqser.red.service.persistence.service.v1.api.shared.model.component.ComponentDefinitionUpdateRequest;
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.DateFormatPatternErrorMessage;
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.rules.DroolsValidationResponse;
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.rules.RulesUploadRequest;
import com.iqser.red.service.persistence.service.v2.api.external.model.ComponentMappingMetadataModel;
import com.iqser.red.service.persistence.service.v2.api.external.model.ComponentMappingSummary;
import com.iqser.red.service.persistence.service.v2.api.external.model.DossierAttributeDefinition;
import com.iqser.red.service.persistence.service.v2.api.external.model.DossierAttributeDefinitionList;
import com.iqser.red.service.persistence.service.v2.api.external.model.DossierStatusDefinition;
import com.iqser.red.service.persistence.service.v2.api.external.model.DossierStatusDefinitionList;
import com.iqser.red.service.persistence.service.v2.api.external.model.FileAttributeDefinition;
import com.iqser.red.service.persistence.service.v2.api.external.model.FileAttributeDefinitionList;
import com.iqser.red.service.persistence.service.v2.api.external.model.ReportTemplate;
import com.iqser.red.service.persistence.service.v2.api.external.model.ReportTemplateList;
import com.iqser.red.service.persistence.service.v2.api.external.resource.DossierTemplateResource;
import com.knecon.fforesight.keycloakcommons.security.KeycloakSecurity;

import feign.FeignException;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.experimental.FieldDefaults;

@RestController
@RequiredArgsConstructor
@Tag(name = "1. Dossier templates endpoints", description = "Provides operations related to dossier templates")
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
public class DossierTemplateControllerV2 implements DossierTemplateResource {

    private static final String RULES_DOWNLOAD_FILE_NAME_SUFFIX = "-rules.drl";
    private static final String DATE_FORMAT_FILE_NAME = "date_formats.txt";

    DossierTemplateController dossierTemplateController;
    RulesPersistenceService rulesPersistenceService;
    DateFormatsPersistenceService dateFormatsPersistenceService;
    RulesValidationService rulesValidationService;
    DateFormatsValidationService dateFormatsValidationService;
    AuditPersistenceService auditPersistenceService;
    FileAttributesController fileAttributesController;
    ComponentMappingService componentMappingService;
    ComponentMappingMapper componentMappingMapper = ComponentMappingMapper.INSTANCE;
    DossierTemplatePersistenceService dossierTemplatePersistenceService;
    DossierStatusPersistenceService dossierStatusPersistenceService;
    DossierAttributeConfigPersistenceService dossierAttributeConfigPersistenceService;
    ComponentDefinitionService componentDefinitionService;
    ReportTemplatePersistenceService reportTemplatePersistenceService;


    public List<DossierTemplateModel> getAllDossierTemplates() {

        return dossierTemplateController.getAllDossierTemplates();
    }


    public DossierTemplateModel getDossierTemplate(@PathVariable(DOSSIER_TEMPLATE_ID_PARAM) String dossierTemplateId) {

        return dossierTemplateController.getDossierTemplate(dossierTemplateId);
    }


    @PreAuthorize("hasAuthority('" + WRITE_RULES + "')")
    public ResponseEntity<DroolsValidationResponse> uploadEntityRules(@PathVariable(DOSSIER_TEMPLATE_ID_PARAM) String dossierTemplateId,
                                                                      @Schema(type = "string", format = "binary", name = "file") @RequestPart(name = "file") MultipartFile file,
                                                                      @Parameter(name = DRY_RUN_PARAM, description = "If true rules will be only validated not stored.") @RequestParam(value = DRY_RUN_PARAM, required = false, defaultValue = "false") boolean dryRun) {

        dossierTemplatePersistenceService.checkDossierTemplateExistsOrElseThrow404(dossierTemplateId);

        return uploadRules(dossierTemplateId, RuleFileType.ENTITY, file, dryRun);
    }


    @PreAuthorize("hasAuthority('" + READ_RULES + "')")
    public ResponseEntity<?> downloadEntityRules(@PathVariable(DOSSIER_TEMPLATE_ID_PARAM) String dossierTemplateId) {

        dossierTemplatePersistenceService.checkDossierTemplateExistsOrElseThrow404(dossierTemplateId);

        return downloadRules(dossierTemplateId, RuleFileType.ENTITY);
    }


    @PreAuthorize("hasAuthority('" + WRITE_RULES + "')")
    public ResponseEntity<DroolsValidationResponse> uploadComponentRules(@PathVariable(DOSSIER_TEMPLATE_ID_PARAM) String dossierTemplateId,
                                                                         @Schema(type = "string", format = "binary", name = "file") @RequestPart(name = "file") MultipartFile file,
                                                                         @Parameter(name = DRY_RUN_PARAM, description = "If true rules will be only validated not stored.") @RequestParam(value = DRY_RUN_PARAM, required = false, defaultValue = "false") boolean dryRun) {

        dossierTemplatePersistenceService.checkDossierTemplateExistsOrElseThrow404(dossierTemplateId);

        return uploadRules(dossierTemplateId, RuleFileType.COMPONENT, file, dryRun);
    }


    @PreAuthorize("hasAuthority('" + READ_RULES + "')")
    public ResponseEntity<?> downloadComponentRules(@PathVariable(DOSSIER_TEMPLATE_ID_PARAM) String dossierTemplateId) {

        dossierTemplatePersistenceService.checkDossierTemplateExistsOrElseThrow404(dossierTemplateId);

        return downloadRules(dossierTemplateId, RuleFileType.COMPONENT);
    }


    @SneakyThrows
    @PreAuthorize("hasAuthority('" + WRITE_DATA_FORMATS + "')")
    public ResponseEntity<?> uploadDateFormats(String dossierTemplateId, MultipartFile file) {

        dossierTemplatePersistenceService.checkDossierTemplateExistsOrElseThrow404(dossierTemplateId);

        String originalFilename = file.getOriginalFilename();
        if (originalFilename == null || !originalFilename.endsWith(".txt")) {
            throw new BadRequestException("Only .txt files can be parsed.");
        }

        String dateFormats = new String(file.getBytes(), StandardCharsets.UTF_8);

        List<DateFormatPatternErrorMessage> dateFormatPatternErrorMessages = dateFormatsValidationService.validateDateFormats(dateFormats);
        if (!dateFormatPatternErrorMessages.isEmpty()) {
            return new ResponseEntity<>(dateFormatPatternErrorMessages, HttpStatus.UNPROCESSABLE_ENTITY);
        }

        dateFormatsPersistenceService.setDateFormats(dateFormats, dossierTemplateId);
        auditPersistenceService.audit(AuditRequest.builder()
                                              .userId(KeycloakSecurity.getUserId())
                                              .objectId(dossierTemplateId)
                                              .category(AuditCategory.DOSSIER_TEMPLATE.name())
                                              .message("date formats have been updated")
                                              .build());

        return new ResponseEntity<>(HttpStatus.NO_CONTENT);
    }


    @SneakyThrows
    @PreAuthorize("hasAuthority('" + READ_DATA_FORMATS + "')")
    public ResponseEntity<?> downloadDateFormats(String dossierTemplateId) {

        dossierTemplatePersistenceService.checkDossierTemplateExistsOrElseThrow404(dossierTemplateId);

        Optional<DateFormatsEntity> dateFormatsOptional = dateFormatsPersistenceService.getDateFormats(dossierTemplateId);
        if (dateFormatsOptional.isEmpty()) {
            throw new NotFoundException(String.format("No date formats found for dossierTemplateId %s", dossierTemplateId));
        }
        var data = dateFormatsOptional.get().getValue().getBytes(StandardCharsets.UTF_8);

        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.setContentType(MediaType.TEXT_PLAIN);

        httpHeaders.add("Content-Disposition", "attachment" + "; filename*=utf-8''" + StringEncodingUtils.urlEncode(DATE_FORMAT_FILE_NAME));

        InputStream is = new ByteArrayInputStream(data);

        return new ResponseEntity<>(new InputStreamResource(is), httpHeaders, HttpStatus.OK);
    }


    @PreAuthorize("hasAuthority('" + READ_FILE_ATTRIBUTES_CONFIG + "')")
    public FileAttributeDefinitionList getFileAttributeDefinitions(@PathVariable(DOSSIER_TEMPLATE_ID_PARAM) String dossierTemplateId) {

        dossierTemplatePersistenceService.checkDossierTemplateExistsOrElseThrow404(dossierTemplateId);

        var fileAttributeConfigs = fileAttributesController.getFileAttributesConfiguration(dossierTemplateId);

        var csvImportSettings = FileAttributeDefinitionList.CsvImportSettings.builder()
                .encoding(fileAttributeConfigs.getEncoding())
                .delimiter(fileAttributeConfigs.getDelimiter())
                .filenameMappingCsvColumnHeader(fileAttributeConfigs.getFilenameMappingColumnHeaderName())
                .build();

        var fileAttributeDefinitions = fileAttributeConfigs.getFileAttributeConfigs()
                .stream()
                .map(fileAttributeConfig -> FileAttributeDefinition.builder()
                        .id(fileAttributeConfig.getId())
                        .name(fileAttributeConfig.getLabel())
                        .type(fileAttributeConfig.getType())
                        .mappedCsvColumnHeader(fileAttributeConfig.getCsvColumnHeader())
                        .reportingPlaceholder(fileAttributeConfig.getPlaceholder())
                        .displaySettings(FileAttributeDefinition.DisplaySettings.builder()
                                                 .primaryAttribute(fileAttributeConfig.isPrimaryAttribute())
                                                 .editable(fileAttributeConfig.isEditable())
                                                 .filterable(fileAttributeConfig.isFilterable())
                                                 .displayedInFileList(fileAttributeConfig.isDisplayedInFileList())
                                                 .build())
                        .includeInCsvExport(fileAttributeConfig.isIncludeInCsvExport())
                        .build())
                .toList();

        return new FileAttributeDefinitionList(csvImportSettings, fileAttributeDefinitions);
    }


    @Override
    @PreAuthorize("hasAuthority('" + READ_RULES + "')")
    public ComponentMappingSummary getComponentMappingSummaries(@PathVariable(DOSSIER_TEMPLATE_ID_PARAM) String dossierTemplateId) {

        dossierTemplatePersistenceService.checkDossierTemplateExistsOrElseThrow404(dossierTemplateId);
        List<com.iqser.red.service.persistence.service.v1.api.shared.model.component.ComponentMappingMetadata> summaries = componentMappingService.getMetaDataByDossierTemplateId(
                dossierTemplateId);
        List<ComponentMappingMetadataModel> componentMappingMetadataModelList = componentMappingMapper.toModelList(summaries);
        return new ComponentMappingSummary(dossierTemplateId, componentMappingMetadataModelList);
    }


    @Override
    @SneakyThrows
    @PreAuthorize("hasAuthority('" + WRITE_RULES + "')")
    public ComponentMappingMetadataModel uploadMapping(String dossierTemplateId, MultipartFile file, String name, String encoding, String delimiter, String quoteChar) {

        dossierTemplatePersistenceService.checkDossierTemplateExistsOrElseThrow404(dossierTemplateId);

        if (Strings.isNullOrEmpty(file.getOriginalFilename()) || !file.getOriginalFilename().endsWith(".csv")) {
            throw new BadRequestException(format("File name \"%s\" does not end with .csv", file.getOriginalFilename()));
        }

        String fileName = file.getOriginalFilename();

        String nameToUse = Strings.isNullOrEmpty(name) ? fileName.replaceAll(".csv$", "") : name;

        if (Strings.isNullOrEmpty(nameToUse)) {
            throw new BadRequestException(format("The provided file name \"%s\" is not valid!", nameToUse));
        }

        char cleanDelimiter = getDelimiter(delimiter);
        char cleanQuoteChar = getQuoteChar(quoteChar);

        Path mappingFile = saveToFile(file);

        try {

            ComponentMappingMetadata metaData = componentMappingService.create(dossierTemplateId,
                                                                               nameToUse,
                                                                               fileName,
                                                                               cleanDelimiter,
                                                                               encoding,
                                                                               mappingFile.toFile(),
                                                                               cleanQuoteChar);

            return componentMappingMapper.toModel(metaData);
        } finally {
            Files.deleteIfExists(mappingFile);
        }

    }


    @Override
    @SneakyThrows
    @PreAuthorize("hasAuthority('" + WRITE_RULES + "')")
    public ComponentMappingMetadataModel updateMapping(String dossierTemplateId,
                                                       String componentMappingId,
                                                       MultipartFile file,
                                                       String name,
                                                       String encoding,
                                                       String delimiter,
                                                       String quoteChar) {

        dossierTemplatePersistenceService.checkDossierTemplateExistsOrElseThrow404(dossierTemplateId);

        String nameToUse = validateFileName(file, name);

        char cleanDelimiter = getDelimiter(delimiter);
        char cleanQuoteChar = getQuoteChar(quoteChar);

        Path mappingFile = saveToFile(file);

        try {
            ComponentMappingMetadata resultMetaData = componentMappingService.update(dossierTemplateId,
                                                                                     componentMappingId,
                                                                                     nameToUse,
                                                                                     encoding,
                                                                                     cleanDelimiter,
                                                                                     mappingFile.toFile(),
                                                                                     file.getOriginalFilename(),
                                                                                     cleanQuoteChar);

            return componentMappingMapper.toModel(resultMetaData);
        } finally {
            Files.deleteIfExists(mappingFile);
        }
    }


    private static char getDelimiter(String delimiter) {

        if (Strings.isNullOrEmpty(delimiter)) {
            throw new BadRequestException("The provided delimiter is not valid! Can't be null or empty.");
        } else if (delimiter.length() != 1) {
            throw new BadRequestException(format("The provided delimiter %s is not valid! Only a single character is allowed.", delimiter));
        }
        return delimiter.charAt(0);
    }


    private static char getQuoteChar(String quoteChar) {

        if (Strings.isNullOrEmpty(quoteChar)) {
            throw new BadRequestException("The provided quoteChar is not valid! Can't be null or empty.");
        } else if (quoteChar.length() != 1) {
            throw new BadRequestException(format("The provided quoteChar %s is not valid! Only a single character is allowed.", quoteChar));
        }
        return quoteChar.charAt(0);
    }


    private static String validateFileName(MultipartFile file, String name) {

        if (Strings.isNullOrEmpty(file.getOriginalFilename()) || !file.getOriginalFilename().endsWith(".csv")) {
            throw new BadRequestException(format("File name \"%s\" does not end with .csv", file.getOriginalFilename()));
        }

        String fileName = file.getOriginalFilename();

        String nameToUse = Strings.isNullOrEmpty(name) ? fileName.replaceAll(".csv$", "") : name;

        if (Strings.isNullOrEmpty(nameToUse)) {
            throw new BadRequestException(format("The provided file name \"%s\" is not valid!", nameToUse));
        }
        return nameToUse;
    }


    @Override
    @PreAuthorize("hasAuthority('" + READ_RULES + "')")
    public ResponseEntity<?> downloadMapping(String dossierTemplateId, String componentMappingId) {

        dossierTemplatePersistenceService.checkDossierTemplateExistsOrElseThrow404(dossierTemplateId);

        ComponentMappingDownloadModel mappingDownloadModel = componentMappingService.getMappingForDownload(dossierTemplateId, componentMappingId);

        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.setContentType(MediaType.TEXT_PLAIN);

        httpHeaders.add("Content-Disposition",
                        "attachment"
                        + "; filename*="
                        + mappingDownloadModel.encoding().toLowerCase(Locale.US)
                        + "''"
                        + StringEncodingUtils.urlEncode(mappingDownloadModel.fileName()));

        return new ResponseEntity<>(mappingDownloadModel.mappingFileResource(), httpHeaders, HttpStatus.OK);
    }


    @Override
    @PreAuthorize("hasAuthority('" + WRITE_RULES + "')")
    public ResponseEntity<?> deleteMapping(String dossierTemplateId, String componentMappingId) {

        dossierTemplatePersistenceService.checkDossierTemplateExistsOrElseThrow404(dossierTemplateId);

        try {
            componentMappingService.delete(dossierTemplateId, componentMappingId);
        } catch (Exception ignored) {

        }

        return new ResponseEntity<>(HttpStatus.NO_CONTENT);
    }


    @PreAuthorize("hasAuthority('" + READ_DOSSIER_STATUS + "')")
    public DossierStatusDefinitionList getDossierStatusDefinitions(@PathVariable(DOSSIER_TEMPLATE_ID_PARAM) String dossierTemplateId) {

        getDossierTemplate(dossierTemplateId);

        return new DossierStatusDefinitionList(dossierStatusPersistenceService.getAllDossierStatusForTemplate(dossierTemplateId)
                                                       .stream()
                                                       .map(dossierStatusInfo -> DossierStatusDefinition.builder()
                                                               .id(dossierStatusInfo.getId())
                                                               .name(dossierStatusInfo.getName())
                                                               .description(dossierStatusInfo.getDescription())
                                                               .rank(dossierStatusInfo.getRank())
                                                               .color(dossierStatusInfo.getColor())
                                                               .dossierCount(dossierStatusInfo.getDossierCount() != null ? dossierStatusInfo.getDossierCount() : 0)
                                                               .build())
                                                       .toList());
    }


    @PreAuthorize("hasAuthority('" + READ_DOSSIER_ATTRIBUTES_CONFIG + "')")
    public DossierAttributeDefinitionList getDossierAttributeDefinitions(@PathVariable(DOSSIER_TEMPLATE_ID_PARAM) String dossierTemplateId) {

        getDossierTemplate(dossierTemplateId);

        return new DossierAttributeDefinitionList(dossierAttributeConfigPersistenceService.getDossierAttributes(dossierTemplateId)
                                                          .stream()
                                                          .map(config -> DossierAttributeDefinition.builder()
                                                                  .id(config.getId())
                                                                  .name(config.getLabel())
                                                                  .type(config.getType())
                                                                  .reportingPlaceholder(config.getPlaceholder())
                                                                  .displaySettings(DossierAttributeDefinition.DossierDisplaySettings.builder()
                                                                                           .editable(config.isEditable())
                                                                                           .filterable(config.isFilterable())
                                                                                           .displayedInDossierList(config.isDisplayedInDossierList())
                                                                                           .build())
                                                                  .build())
                                                          .toList());
    }


    @PreAuthorize("hasAuthority('" + GET_REPORT_TEMPLATES + "')")
    public ReportTemplateList getReportTemplates(@PathVariable(DOSSIER_TEMPLATE_ID_PARAM) String dossierTemplateId) {

        getDossierTemplate(dossierTemplateId);
        var templates = reportTemplatePersistenceService.findByDossierTemplateId(dossierTemplateId);
        return new ReportTemplateList(templates.stream()
                                              .map(t -> ReportTemplate.builder()
                                                      .id(t.getTemplateId())
                                                      .name(t.getFileName())
                                                      .createdOn(t.getUploadDate())
                                                      .multiFile(t.isMultiFileReport())
                                                      .preSelect(t.isActiveByDefault())
                                                      .build())
                                              .toList());
    }


    @Override
    public List<ComponentDefinition> createComponents(String dossierTemplateId, List<ComponentDefinitionAddRequest> componentDefinitionAddRequests) {

        if (componentDefinitionAddRequests.isEmpty()) {
            return Collections.emptyList();
        }

        List<ComponentDefinition> componentDefinitions = componentDefinitionService.createComponents(dossierTemplateId, componentDefinitionAddRequests);
        auditPersistenceService.audit(AuditRequest.builder()
                                              .userId(KeycloakSecurity.getUserId())
                                              .objectId(dossierTemplateId)
                                              .category(AuditCategory.DOSSIER_TEMPLATE.name())
                                              .message("Components added.")
                                              .details(Map.of("Number of added components", componentDefinitions.size()))
                                              .build());
        return componentDefinitions;
    }


    @Override
    public List<ComponentDefinition> getComponents(String dossierTemplateId, boolean includeSoftDeleted) {

        return componentDefinitionService.getComponentsByDossierTemplateId(dossierTemplateId, includeSoftDeleted);
    }


    @Override
    public ComponentDefinition getComponent(String dossierTemplateId, String componentId) {

        return componentDefinitionService.getComponentByDossierTemplateIdAndComponentId(dossierTemplateId, componentId);
    }


    @Override
    public List<ComponentDefinition> updateComponents(String dossierTemplateId, List<ComponentDefinitionUpdateRequest> componentDefinitionUpdateRequests) {

        if (componentDefinitionUpdateRequests.isEmpty()) {
            return Collections.emptyList();
        }

        List<ComponentDefinition> componentDefinitions = componentDefinitionService.updateComponents(dossierTemplateId, componentDefinitionUpdateRequests);
        auditPersistenceService.audit(AuditRequest.builder()
                                              .userId(KeycloakSecurity.getUserId())
                                              .objectId(dossierTemplateId)
                                              .category(AuditCategory.DOSSIER_TEMPLATE.name())
                                              .message("Components updated.")
                                              .details(Map.of("Number of updated components", componentDefinitions.size()))
                                              .build());
        return componentDefinitions;
    }


    @Override
    public void deleteComponents(String dossierTemplateId, List<String> componentIds) {

        List<ComponentDefinitionEntity> components = componentDefinitionService.deleteComponents(dossierTemplateId, componentIds);
        if (!components.isEmpty()) {
            auditPersistenceService.audit(AuditRequest.builder()
                                                  .userId(KeycloakSecurity.getUserId())
                                                  .objectId(dossierTemplateId)
                                                  .category(AuditCategory.DOSSIER_TEMPLATE.name())
                                                  .message("Components deleted.")
                                                  .details(Map.of("Number of deleted components", components.size()))
                                                  .build());
        }
    }


    @Override
    public List<ComponentDefinition> restoreComponents(String dossierTemplateId, List<String> componentIds) {

        List<ComponentDefinition> components = componentDefinitionService.restoreComponents(dossierTemplateId, componentIds);
        if (!components.isEmpty()) {
            auditPersistenceService.audit(AuditRequest.builder()
                                                  .userId(KeycloakSecurity.getUserId())
                                                  .objectId(dossierTemplateId)
                                                  .category(AuditCategory.DOSSIER_TEMPLATE.name())
                                                  .message("Components restored.")
                                                  .details(Map.of("Number of restored components", components.size()))
                                                  .build());
        }
        return components;
    }


    @Override
    public List<ComponentDefinition> reorderComponents(String dossierTemplateId, List<String> componentIds) {

        List<ComponentDefinition> orderedComponents = componentDefinitionService.reorderComponents(dossierTemplateId, componentIds);
        if (!orderedComponents.isEmpty()) {
            auditPersistenceService.audit(AuditRequest.builder()
                                                  .userId(KeycloakSecurity.getUserId())
                                                  .objectId(dossierTemplateId)
                                                  .category(AuditCategory.DOSSIER_TEMPLATE.name())
                                                  .message("Components reordered.")
                                                  .details(Map.of("Number of reordered components", orderedComponents.size()))
                                                  .build());
        }
        return orderedComponents;
    }


    @SneakyThrows
    private static Path saveToFile(MultipartFile file) {

        Path mappingFile = Files.createTempFile(file.getName(), ".csv");

        try (var out = new FileOutputStream(mappingFile.toFile())) {
            out.write(file.getBytes());
        }
        return mappingFile;
    }


    private ResponseEntity<?> downloadRules(String dossierTemplateId, RuleFileType ruleFileType) {

        Optional<RuleSetEntity> ruleEntityOptional = rulesPersistenceService.getRules(dossierTemplateId, ruleFileType);
        if (ruleEntityOptional.isEmpty()) {
            throw new NotFoundException(String.format("No rule file of type %s found for dossierTemplateId %s", ruleFileType, dossierTemplateId));
        }
        var data = ruleEntityOptional.get().getValue().getBytes(StandardCharsets.UTF_8);

        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.setContentType(MediaType.TEXT_PLAIN);

        httpHeaders.add("Content-Disposition",
                        "attachment" + "; filename*=utf-8''" + StringEncodingUtils.urlEncode(ruleFileType.name().toLowerCase(Locale.ROOT) + RULES_DOWNLOAD_FILE_NAME_SUFFIX));

        InputStream is = new ByteArrayInputStream(data);

        return new ResponseEntity<>(new InputStreamResource(is), httpHeaders, HttpStatus.OK);
    }


    @SneakyThrows
    private ResponseEntity<DroolsValidationResponse> uploadRules(String dossierTemplateId, RuleFileType ruleFileType, MultipartFile file, boolean dryRun) {

        var rulesUploadRequest = RulesUploadRequest.builder()
                .rules(new String(file.getBytes(), StandardCharsets.UTF_8))
                .dossierTemplateId(dossierTemplateId)
                .ruleFileType(ruleFileType)
                .build();

        DroolsValidationResponse rulesValidationResponse = new DroolsValidationResponse();

        try {
            var droolsValidation = rulesValidationService.validateRules(rulesUploadRequest.getRuleFileType(), rulesUploadRequest.getRules());
            rulesValidationResponse = RulesValidationMapper.createFromDroolsValidation(droolsValidation);
            if (!droolsValidation.isCompiled()) {

                return new ResponseEntity<>(rulesValidationResponse, !dryRun ? HttpStatus.UNPROCESSABLE_ENTITY : HttpStatus.OK);
            }
        } catch (FeignException e) {
            if (e.status() == HttpStatus.BAD_REQUEST.value()) {
                throw new BadRequestException("The provided rule string is not a valid drools rule file!");
            }
        }

        if (!dryRun) {
            rulesPersistenceService.setRules(rulesUploadRequest.getRules(), rulesUploadRequest.getDossierTemplateId(), rulesUploadRequest.getRuleFileType());
        }

        auditPersistenceService.audit(AuditRequest.builder()
                                              .userId(KeycloakSecurity.getUserId())
                                              .objectId(rulesUploadRequest.getDossierTemplateId())
                                              .category(AuditCategory.DOSSIER_TEMPLATE.name())
                                              .message(format("%s rules have been %s", rulesUploadRequest.getRuleFileType(), dryRun ? "validated" : "updated"))
                                              .build());

        return new ResponseEntity<>(rulesValidationResponse, HttpStatus.OK);
    }

}
