package org.mte.numecoeval.referentiel.domain.ports.output;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVPrinter;
import org.apache.commons.lang3.StringUtils;

import java.io.IOException;
import java.io.Writer;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.List;
import java.util.Locale;
import java.util.Objects;

@Slf4j
public abstract class ReferentielCsvExportService<T> {
    static final String DELIMITER = ";";

    final Class<T> typeParameterClass;

    protected ReferentielCsvExportService(Class<T> typeParameterClass) {
        this.typeParameterClass = typeParameterClass;
    }

    /**
     * Headers du fichier CSV.
     * @return Tableau des headers du fichier CSV généré.
     */
    protected abstract String[] getHeaders();

    /**
     * Ecriture d'une ligne dans le fichier CSV.
     * @param csvPrinter {@link CSVPrinter} à utiliser
     * @param objectToWrite objet à traiter
     * @throws IOException levée en cas d'erreur de la ligne viw le {@link CSVPrinter}
     */
    protected abstract void printRecord(CSVPrinter csvPrinter, T objectToWrite) throws IOException;

    /**
     * Renvoie la liste des objets à traiter pour le CSV.
     * @return Liste des objets à traiter, peut être {@code null}
     */
    protected abstract List<T> getObjectsToWrite();

    /**
     * Méthode appelé pour identifier un objet à traiter dans les logs d'erreur spécifique.
     * @param object objet à traiter, ne peut être {@code null}
     * @return {@link String} perettant d'identifier l'objet
     */
    protected abstract String getObjectId(T object);

    /**
     * Renvoie la chaine de caractères correctement formatté pour un {@link Double}
     * @param doubleValue valeur à traiter
     * @return {@link String} du représentant le double au bon format
     */
    protected String formatDouble(Double doubleValue) {
        if(Objects.isNull(doubleValue)) {
            return StringUtils.EMPTY;
        }
        DecimalFormat df = new DecimalFormat("0", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
        df.setMaximumFractionDigits(340);
        return df.format(doubleValue);
    }

    /**
     * Log d'une erreur sur l'écriture d'une ligne du fichier CSV.
     * @param objectToWrite objet à traiter
     * @param exception {@link Exception} levée lors de l'erreur sur l'écriture de la ligne
     */
    public void logRecordError(T objectToWrite, Exception exception) {
        var logMessage = "Erreur d'écriture d'une ligne d'objet "+ typeParameterClass.getName();
        if(Objects.nonNull(objectToWrite)) {
            logMessage = "Erreur d'écriture d'une ligne d'objet %s : %s".formatted(typeParameterClass.getName(), getObjectId(objectToWrite));
        }
        log.error(logMessage, exception);
    }

    /**
     * Logs d'une erreur en cas de problème à l'écriture via {@link Writer}
     * @param exception {@link Exception} levée lors de l'erreur sur l'écriture dans le Writer
     */
    public void logWriterError(Exception exception) {
        log.error("Erreur d'écriture du fichier CSV des objets {}", typeParameterClass.getName(), exception);
    }

    /**
     * Ecriture du fichier CSV via {@link Writer}.
     * @see #printRecord(CSVPrinter, Object)
     * @see #logRecordError(Object, Exception)
     * @see #logWriterError(Exception)
     * @param writer {@link Writer} à utiliser
     */
    public void writeToCsv(Writer writer) {
        List<T> objects = getObjectsToWrite();
        CSVFormat csvFormat = CSVFormat.Builder.create().setHeader(getHeaders()).setDelimiter(DELIMITER).build();
        try (CSVPrinter csvPrinter = new CSVPrinter(writer, csvFormat)) {
            CollectionUtils.emptyIfNull(objects).forEach(objectToWrite -> {
                try {
                    printRecord(csvPrinter, objectToWrite);
                } catch (Exception e) {
                    logRecordError(objectToWrite, e);
                }
            });
        } catch (Exception e) {
            logWriterError(e);
        }
    }
}
