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

import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Reader;
import java.nio.charset.Charset;
import java.nio.charset.IllegalCharsetNameException;
import java.nio.charset.UnsupportedCharsetException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.UUID;

import org.springframework.stereotype.Service;

import com.iqser.red.service.persistence.management.v1.processor.entity.ComponentMappingEntity;
import com.iqser.red.service.persistence.management.v1.processor.exception.BadRequestException;
import com.iqser.red.service.persistence.management.v1.processor.mapper.ComponentMappingEntityMapper;
import com.iqser.red.service.persistence.management.v1.processor.model.ComponentMapping;
import com.iqser.red.service.persistence.management.v1.processor.model.ComponentMappingDownloadModel;
import com.iqser.red.service.persistence.management.v1.processor.service.persistence.DossierTemplatePersistenceService;
import com.iqser.red.service.persistence.management.v1.processor.utils.StringEncodingUtils;
import com.iqser.red.service.persistence.service.v1.api.shared.model.component.ComponentMappingMetadata;
import com.opencsv.CSVParserBuilder;
import com.opencsv.CSVReader;
import com.opencsv.CSVReaderBuilder;
import com.opencsv.CSVWriter;
import com.opencsv.exceptions.CsvException;

import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.experimental.FieldDefaults;

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

    ComponentMappingPersistenceService componentMappingPersistenceService;
    DossierTemplatePersistenceService dossierTemplatePersistenceService;
    ComponentMappingEntityMapper mappingEntityMapper = ComponentMappingEntityMapper.INSTANCE;

    static CSVSorter CSV_SORTER = new CSVSorter();


    public List<ComponentMappingMetadata> getMetaDataByDossierTemplateId(String dossierTemplateId) {

        List<ComponentMappingEntity> entities = componentMappingPersistenceService.getByDossierTemplateId(dossierTemplateId);
        return mappingEntityMapper.toComponentMappingMetaDataList(entities);
    }


    public ComponentMappingMetadata getMetaData(String dossierTemplateId, String mappingId) {

        return mappingEntityMapper.toComponentMappingMetaData(componentMappingPersistenceService.getEntityById(dossierTemplateId, mappingId));
    }


    @SneakyThrows
    public ComponentMappingMetadata update(String dossierTemplateId,
                                           String mappingId,
                                           String name,
                                           String encoding,
                                           char delimiter,
                                           File mappingFile,
                                           String fileName,
                                           char quoteChar) {

        ComponentMappingEntity entity = componentMappingPersistenceService.getEntityById(dossierTemplateId, mappingId);

        return updateOrCreate(entity, name, encoding, delimiter, mappingFile, fileName, quoteChar);
    }


    @SneakyThrows
    public ComponentMappingMetadata create(String dossierTemplateId, String name, String fileName, char delimiter, String encoding, File mappingFile, char quoteChar) {

        if (componentMappingPersistenceService.existsByNameAndDossierTemplateId(name, dossierTemplateId)) {
            throw new BadRequestException("A mapping with this name already exists in the dossier template!");
        }

        String id = UUID.randomUUID().toString();
        String storageId = buildStorageId(dossierTemplateId, id, name, fileName);
        ComponentMappingEntity entity = ComponentMappingEntity.builder()
                .id(id)
                .dossierTemplate(dossierTemplatePersistenceService.getDossierTemplate(dossierTemplateId))
                .storageId(storageId)
                .fileName(fileName)
                .build();

        return updateOrCreate(entity, name, encoding, delimiter, mappingFile, fileName, quoteChar);

    }


    @SneakyThrows
    private ComponentMappingMetadata updateOrCreate(ComponentMappingEntity entity,
                                                    String name,
                                                    String encoding,
                                                    char delimiter,
                                                    File mappingFile,
                                                    String fileName,
                                                    char quoteChar) {

        Charset charset = StringEncodingUtils.resolveCharset(encoding);

        CsvStats stats = sortCSVFile(delimiter, mappingFile, charset, quoteChar);

        entity.setName(name);
        entity.setDelimiter(delimiter);
        entity.setQuoteChar(quoteChar);
        entity.setEncoding(encoding);
        entity.setNumberOfLines(stats.numberOfLines());
        entity.setColumnLabels(stats.columnLabels());
        entity.setVersion(entity.getVersion() + 1);
        entity.setFileName(fileName);

        componentMappingPersistenceService.updateOrCreate(entity.getStorageId(), mappingFile, entity);

        return mappingEntityMapper.toComponentMappingMetaData(entity);
    }


    private static CsvStats sortCSVFile(char delimiter, File mappingFile, Charset charset, char quoteChar) throws BadRequestException, IOException {

        Path tempFile = Files.createTempFile("mapping", ".tmp");

        Files.move(mappingFile.toPath(), tempFile, StandardCopyOption.REPLACE_EXISTING);

        String[] columnLabels;
        int numberOfLines = 0;
        try (Reader fileReader = new FileReader(tempFile.toFile(), charset);//
             CSVReader reader = buildReader(fileReader, delimiter, quoteChar);//
             CSVWriter writer = new CSVWriter(new FileWriter(mappingFile, charset), delimiter, quoteChar, '\\', CSVWriter.DEFAULT_LINE_END)) {

            List<String[]> rows = reader.readAll();

            if (rows.isEmpty()) {
                throw new BadRequestException("CSV file can not be empty!");
            }

            columnLabels = rows.remove(0); // remove header row

            if (Arrays.stream(columnLabels)
                        .distinct()
                        .count() < columnLabels.length) {
                throw new BadRequestException("Column labels may not contain duplicates!");
            }

            if (rows.stream()
                    .anyMatch(row -> row.length != columnLabels.length)) {
                throw new BadRequestException("Data in csv must be rectangular!");
            }

            numberOfLines = (int) reader.getLinesRead() - 1; // subtract header row

            rows.sort(CSV_SORTER);

            writer.writeNext(columnLabels);

            writer.writeAll(rows);

        } catch (IOException e) {
            throw new BadRequestException("Error while sorting the csv file", e);
        } catch (CsvException e) {
            throw new BadRequestException("The file could not be parsed as a csv file", e);
        } finally {
            Files.deleteIfExists(tempFile);
        }

        return new CsvStats(Arrays.asList(columnLabels), numberOfLines);
    }


    private static CSVReader buildReader(Reader reader, char delimiter, char quoteChar) throws IOException {

        return new CSVReaderBuilder(reader).withCSVParser(new CSVParserBuilder().withSeparator(delimiter).withQuoteChar(quoteChar).build()).build();
    }


    public void delete(String dossierTemplateId, String componentMappingId) {

        componentMappingPersistenceService.deleteById(dossierTemplateId, componentMappingId);
    }


    public ComponentMappingDownloadModel getMappingForDownload(String dossierTemplateId, String componentMappingId) {

        return componentMappingPersistenceService.getMappingFileForDownload(dossierTemplateId, componentMappingId);
    }


    @SneakyThrows
    public List<ComponentMapping> getMappingFilesByDossierTemplateId(String dossierTemplateId, Path outputDir) {

        var entities = componentMappingPersistenceService.getByDossierTemplateId(dossierTemplateId);

        return entities.stream()
                .map(mappingEntityMapper::toComponentMappingMetaData)
                .map(metaData -> downloadFileAndCreateMapping(outputDir, metaData))
                .toList();

    }


    public void setVersion(String componentMappingId, Integer version) {

        componentMappingPersistenceService.setVersion(componentMappingId, version);
    }


    private ComponentMapping downloadFileAndCreateMapping(Path outputDir, ComponentMappingMetadata metaData) {

        File mappingFile = componentMappingPersistenceService.downloadMappingFileToFolder(metaData.getStorageId(), metaData.getFileName(), outputDir);
        return new ComponentMapping(metaData, mappingFile);
    }


    public static String buildStorageId(String dossierTemplateId, String id, String name, String fileName) {

        return dossierTemplateId + "/" + id + "_" + name + "_" + fileName;
    }


    private static class CSVSorter implements Comparator<String[]> {

        @Override
        public int compare(String[] line1, String[] line2) {

            for (int column = 0; column < line1.length; column++) {
                if (!line1[column].equals(line2[column])) {
                    return line1[column].compareTo(line2[column]);
                }
            }

            return 0;
        }

    }

    private record CsvStats(List<String> columnLabels, int numberOfLines) {

    }

}
