
package de.ivu.elect.core.exchange.wahl;

import static java.util.Collections.emptyList;
import static java.util.stream.Collectors.toList;
import static org.apache.commons.lang3.StringUtils.isNotBlank;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;

import javax.annotation.Nullable;
import javax.inject.Inject;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.ivu.elect.business.CurrentCustomer;
import de.ivu.elect.business.datei.entity.Datei;
import de.ivu.elect.business.datei.entity.DateiFileNames;
import de.ivu.elect.business.datei.entity.FileType;
import de.ivu.elect.business.exchange.control.FileFormat;
import de.ivu.elect.business.exchange.control.FileKonfiguration;
import de.ivu.elect.business.exchange.control.MappingException;
import de.ivu.elect.business.exchange.entity.DateiExportArt;
import de.ivu.elect.business.gebietsbaum.boundary.GebietDAO;
import de.ivu.elect.business.gebietsbaum.boundary.GebietsgruppierungDAO;
import de.ivu.elect.business.gebietsbaum.entity.Gebiet;
import de.ivu.elect.business.gebietsbaum.entity.Gebietsgruppierung;
import de.ivu.elect.business.gebietsbaum.entity.NlGebietCustomData;
import de.ivu.elect.business.security.Customer;
import de.ivu.elect.business.wahl.boundary.NlExportArtefaktNachSystemExportstatus;
import de.ivu.elect.business.wahl.boundary.WahlDAO;
import de.ivu.elect.business.wahl.control.AbstractNlExportArtefaktNachSystemExportstatusExportContext;
import de.ivu.elect.business.wahl.control.NlExportArtefaktNachSystemExportstatusExportObject;
import de.ivu.elect.business.wahl.entity.Wahl;
import de.ivu.elect.business.wahlvorschlag.boundary.WahlvorschlagDAO;
import de.ivu.elect.business.wahlvorschlag.entity.Wahlvorschlag;
import de.ivu.elect.jee.faces.Messages;
import de.ivu.elect.jee.presentation.dynamicform.FormBuilder;
import de.ivu.elect.util.FileUtil;

public abstract class AbstractNlExportArtefaktNachSystemExportstatusZipExportMapper {

  @Inject
  private transient WahlDAO wahlDAO;

  @Inject
  private transient GebietDAO gebietDAO;

  @Inject
  private transient GebietsgruppierungDAO gebietsgruppierungDAO;

  @Inject
  private transient WahlvorschlagDAO wahlvorschlagDAO;

  @Inject
  private transient NlExportArtefaktNachSystemExportstatusExportMapperResolver nlExportArtefaktNachSystemExportstatusExportMapperResolver;

  @Inject
  private transient NlExportArtefaktNachSystemExportstatusExportFormBuilder nlExportArtefaktNachSystemExportstatusExportFormBuilder;

  @Inject
  private transient CurrentCustomer currentCustomer;

  @Inject
  private transient Messages messages;

  private final Logger log;

  protected AbstractNlExportArtefaktNachSystemExportstatusZipExportMapper() {
    super();

    this.log = LoggerFactory.getLogger(getClass());
  }

  public boolean isAcceptContext(AbstractNlExportArtefaktNachSystemExportstatusExportContext context) {
    return !collectSimpleExportMappersIgnoringSystemExportstatus(context).isEmpty(); // Die überhaupt unterstützten Export-Mapper auswerten, auch wenn der Status des Systems (Gebietstatus, Wahlphase) noch nicht korrekt ist!
  }

  public FileFormat getFileFormat() {
    return new FileFormat(getFileFormatName(), getFileType());
  }

  public abstract String getFileFormatName();

  public FileType getFileType() {
    return FileType.ZIP;
  }

  // ELECTVAPP-225
  public DateiFileNames getDateiExportFileNames(DateiExportArt dateiExportArt, AbstractNlExportArtefaktNachSystemExportstatusExportContext context) {
    List< ? extends NlExportArtefaktNachSystemExportstatusExportMapper> simpleFileFormatNames = collectSimpleExportMappersIgnoringSystemExportstatus(context).stream() //
        .filter(simpleExportMapper -> !simpleExportMapper.requiresTargetFilesInSeparateDateiExport(context)) //
        .collect(toList());

    return createDateiExportFileNames(context, simpleFileFormatNames);
  }

  private DateiFileNames createDateiExportFileNames(AbstractNlExportArtefaktNachSystemExportstatusExportContext context,
      List< ? extends NlExportArtefaktNachSystemExportstatusExportMapper> simpleExportMappers) {
    return new NlExportArtefaktNachSystemExportstatusFileNames(context, getGebietDAO(),
        simpleExportMappers.stream().map(NlExportArtefaktNachSystemExportstatusExportMapper::getFileFormat).collect(toList()));
  }

  protected NlExportArtefaktNachSystemExportstatusExportMapperResolver getExportMapperResolver() {
    return nlExportArtefaktNachSystemExportstatusExportMapperResolver;
  }

  protected NlExportArtefaktNachSystemExportstatusExportFormBuilder getExportFormBuilder() {
    return nlExportArtefaktNachSystemExportstatusExportFormBuilder;
  }

  public void addProperties(FormBuilder formBuilder, AbstractNlExportArtefaktNachSystemExportstatusExportContext context, @Nullable Datei datei) {
    Supplier<Collection<Wahlvorschlag>> wahlvorschlaegeInWahlLoader = () -> {
      Wahl wahl = context.getWahl();

      if (wahl == null) {
        return emptyList();
      }

      List<Wahlvorschlag> result = wahlvorschlagDAO.findByWahl(wahl);

      for (Wahlvorschlag wahlvorschlag : result) { // Eager-Loading...
        wahlvorschlag.getGruppe().getName();
        wahlvorschlag.getGebiet().getGebietsart().getName();
      }

      return result;
    };

    Supplier<Collection<Gebietsgruppierung>> gebietsgruppierungenInWahlLoader = () -> {
      Wahl wahl = context.getWahl();

      if (wahl == null) {
        return emptyList();
      }

      List<Gebietsgruppierung> result = gebietsgruppierungDAO.findByWahl(wahl);

      for (Gebietsgruppierung gebietsgruppierung : result) {
        for (Gebiet gebiet : gebietsgruppierung.getGebiete()) { // Eager-Loading...
          gebiet.getCustomData(NlGebietCustomData.class).isRomanNumerals(); // NlGebietNummerFormatter
          gebiet.getGebietsart().getName(); // GebietsgruppierungGebietPickListProperty#getItemTooltip
        }
      }

      return result;
    };

    getExportFormBuilder().addProperties(formBuilder, context, wahlvorschlaegeInWahlLoader, gebietsgruppierungenInWahlLoader, collectSimpleExportMappersAcceptingSystemExportstatus(context));
  }

  public void serialize(NlExportArtefaktNachSystemExportstatusExportObject exportObject, OutputStream os, AbstractNlExportArtefaktNachSystemExportstatusExportContext context,
      FileKonfiguration fileKonfiguration) throws MappingException {
    List<NlExportArtefaktNachSystemExportstatusExportMapper> simpleExportMappers = new ArrayList<>();

    for (NlExportArtefaktNachSystemExportstatusExportMapper simpleExportMapper : collectSimpleExportMappersAcceptingSystemExportstatus(context)) {
      if (simpleExportMapper.supportsTargetFilesHashCode(context)) {
        simpleExportMappers.add(0, simpleExportMapper); // Mapper, die Hash-Codes erzeugen, IMMER nach vorn, um diese als Erstes aufzurufen!!!
      } else {
        simpleExportMappers.add(simpleExportMapper);
      }
    }

    try {
      ZipOutputStream zos = new ZipOutputStream(os);

      Map<String, Long> zipEntryFileNames = new HashMap<>();

      for (NlExportArtefaktNachSystemExportstatusExportMapper simpleExportMapper : simpleExportMappers) {
        List<FileType> targetFileTypes = NlExportArtefaktNachSystemExportstatusExportMapper.sortFileTypesByOdtFirst(simpleExportMapper.getTargetFileTypes(context)); // ELECT-10670, ODT IMMER nach vorn, um diese als Erstes zu generieren!!!

        for (int i = 0; i < targetFileTypes.size(); i++) {
          boolean fileFormatAlreadyExported = i > 0; // ELECT-10752

          FileType targetFileType = targetFileTypes.get(i);
          String targetFileName = simpleExportMapper.createTargetFileName(context, targetFileType);

          File tempFile = serializeToTempFile(simpleExportMapper, exportObject, context, fileKonfiguration, targetFileType, !fileFormatAlreadyExported); // ELECT-10687

          if (tempFile != null) {
            if (simpleExportMapper.requiresTargetFilesInSeparateDateiExport(context)) { // ELECTVAPP-794, ELECTVAPP-795
              FileFormat artefaktFileFormat = new FileFormat(simpleExportMapper.getFileFormatName(), targetFileType);
              String artefaktFileName = targetFileName;

              context.addArtefaktInDateiExport(new NlExportArtefaktNachSystemExportstatus(artefaktFileFormat, artefaktFileName, tempFile));
            } else {
              if (targetFileType.isZipFile() && !simpleExportMapper.requiresTargetFilesInSeparateBulkExportEntry(context)) {
                unzipToZipOutputStream(zos, tempFile);
              } else {
                String zipEntryFileName = targetFileName;

                zipEntryFileNames.put(zipEntryFileName, zipEntryFileNames.getOrDefault(zipEntryFileName, 0L) + 1L);
                zipEntryFileName = createZipEntryFileNameOrDefaultByIndex(zipEntryFileName, zipEntryFileNames.get(zipEntryFileName));

                copyToZipOutputStream(zos, zipEntryFileName, tempFile);
              }

              try {
                FileUtils.forceDeleteOnExit(tempFile);
              } catch (Exception ex) {
                warn("FAILED to delete file in the default temporary-file directory [file = " + tempFile + "]", ex);
              }
            }
          }
        }
      }

      zos.finish();
    } catch (Exception ex) {
      throw new MappingException(ex.getMessage(), ex);
    }
  }

  private File serializeToTempFile(NlExportArtefaktNachSystemExportstatusExportMapper simpleExportMapper, NlExportArtefaktNachSystemExportstatusExportObject exportObject,
      AbstractNlExportArtefaktNachSystemExportstatusExportContext context, FileKonfiguration fileKonfiguration, FileType targetFileType, boolean failOnException) throws Exception {
    context.setTargetFileType(targetFileType); // ELECTVAPP-465

    info("Erzeuge Artefakt [" //
        + "wahl = " + context.getWahl().getName() + ", " //
        + "hash-codes unterstuetzt = " + simpleExportMapper.supportsTargetFilesHashCode(context) + ", " //
        + "signierung unterstuetzt = " + simpleExportMapper.supportsTargetFilesSigning(context) + ", " //
        + "fileFormat = " + simpleExportMapper.getFileFormatName() + ", " //
        + "fileType = " + targetFileType + ", " //
        + "failOnException = " + failOnException //
        + "]");

    try {
      File tempFile = Files.createTempFile("tempFile-" + System.nanoTime(), "." + targetFileType.getFileExtension()).toFile();

      try (BufferedOutputStream fos = new BufferedOutputStream(new FileOutputStream(tempFile))) {
        simpleExportMapper.serialize(exportObject, fos, context, fileKonfiguration, true);
      }

      return tempFile;
    } catch (Exception ex) {
      if (failOnException) {
        throw ex;
      }

      error("Erzeuge Artefakt FEHLGESCHLAGEN [" //
          + "wahl = " + context.getWahl().getName() + ", " //
          + "hash-codes unterstuetzt = " + simpleExportMapper.supportsTargetFilesHashCode(context) + ", " //
          + "signierung unterstuetzt = " + simpleExportMapper.supportsTargetFilesSigning(context) + ", " //
          + "fileFormat = " + simpleExportMapper.getFileFormatName() + ", " //
          + "fileType = " + targetFileType //
          + "]", ex);
    }

    return null;
  }

  private void unzipToZipOutputStream(ZipOutputStream zos, File tempFile) throws IOException {
    try (ZipFile zipFile = new ZipFile(tempFile)) {
      Enumeration< ? extends ZipEntry> entries = zipFile.entries();

      while (entries.hasMoreElements()) {
        ZipEntry zipEntry = entries.nextElement();

        if (!zipEntry.isDirectory()) {
          String zipEntryFileName = zipEntry.getName();

          try (InputStream is = zipFile.getInputStream(zipEntry)) {
            copyToZipOutputStream(zos, zipEntryFileName, is);
          }
        }
      }
    }
  }

  private void copyToZipOutputStream(ZipOutputStream zos, String zipEntryFileName, File tempFile) throws IOException {
    try (InputStream is = new BufferedInputStream(new FileInputStream(tempFile))) {
      copyToZipOutputStream(zos, zipEntryFileName, is);
    }
  }

  private void copyToZipOutputStream(ZipOutputStream zos, String zipEntryFileName, InputStream is) throws IOException {
    info("Packe Artefakt [name = " + zipEntryFileName + "]");

    zos.putNextEntry(new ZipEntry(zipEntryFileName));
    IOUtils.copy(is, zos);
    zos.closeEntry();
  }

  protected String createZipEntryFileNameOrDefaultByIndex(String fileName, long fileNameCount) {
    String result = FilenameUtils.removeExtension(fileName);

    if (fileNameCount > 1L) {
      result += "_" + (fileNameCount - 1);
    }

    if (isNotBlank(result)) {
      result = FileUtil.normalizeZipEntryNamePreserveCase(result);
    }

    String fileExtension = FilenameUtils.getExtension(fileName);

    if (isNotBlank(fileExtension)) {
      result += "." + fileExtension;
    }

    return result;
  }

  private Collection< ? extends NlExportArtefaktNachSystemExportstatusExportMapper> collectSimpleExportMappersAcceptingSystemExportstatus(
      AbstractNlExportArtefaktNachSystemExportstatusExportContext context) {
    Map<FileFormat, NlExportArtefaktNachSystemExportstatusExportMapper> simpleExportMapperPerFileFormatUnique = new HashMap<>();

    for (NlExportArtefaktNachSystemExportstatusExportMapper simpleExportMapper : collectSimpleExportMappersIgnoringSystemExportstatus(context)) {
      if (simpleExportMapper.isAcceptSystemExportstatus(context)) {
        simpleExportMapperPerFileFormatUnique.put(simpleExportMapper.getFileFormat(), simpleExportMapper);
      }
    }

    return new HashSet<>(simpleExportMapperPerFileFormatUnique.values());
  }

  protected abstract Collection< ? extends NlExportArtefaktNachSystemExportstatusExportMapper> collectSimpleExportMappersIgnoringSystemExportstatus(
      AbstractNlExportArtefaktNachSystemExportstatusExportContext context);

  protected WahlDAO getWahlDAO() {
    return wahlDAO;
  }

  protected GebietDAO getGebietDAO() {
    return gebietDAO;
  }

  protected Customer getCustomer() {
    return currentCustomer.getCustomer();
  }

  protected boolean existsMessage(String msgKey) {
    return getMessages().isMessageKeyVorhanden(msgKey);
  }

  protected String getMessage(String msgKey, Object... params) {
    return getMessages().getMessageForMessageKey(msgKey, params);
  }

  protected String getMessage(Enum< ? > enumValue, Object... params) {
    return getMessages().lokalisiere(enumValue, params);
  }

  protected Messages getMessages() {
    return messages;
  }

  protected void info(String format, Object... params) {
    log.info(format, params);
  }

  protected void warn(String format, Throwable cause) {
    log.warn(format, cause);
  }

  protected void warn(String format, Object... params) {
    log.warn(format, params);
  }

  protected void error(String format, Object... params) {
    log.error(format, params);
  }

  protected void error(String format, Throwable cause) {
    log.error(format, cause);
  }
}
