
package de.ivu.elect.business.dokumente.boundary;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Predicate;

import javax.annotation.Nullable;
import javax.ejb.AsyncResult;
import javax.ejb.Asynchronous;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import javax.inject.Inject;

import org.primefaces.model.file.UploadedFile;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.ivu.elect.business.benutzerverwaltung.boundary.BenutzerDAO;
import de.ivu.elect.business.benutzerverwaltung.entity.Benutzer;
import de.ivu.elect.business.datei.boundary.DateiExportDAO;
import de.ivu.elect.business.datei.entity.Datei;
import de.ivu.elect.business.datei.entity.DateiExport;
import de.ivu.elect.business.datei.entity.DateiExportPK;
import de.ivu.elect.business.datei.entity.DateiFileNames;
import de.ivu.elect.business.datei.entity.FileType;
import de.ivu.elect.business.dokumente.control.AbstractDokumentMerger;
import de.ivu.elect.business.dokumente.control.DokumentExtraFileOutput;
import de.ivu.elect.business.dokumente.control.DokumentGenerieren;
import de.ivu.elect.business.dokumente.control.DokumentKonverter;
import de.ivu.elect.business.dokumente.control.DokumentKonverterLookup;
import de.ivu.elect.business.dokumente.control.ElectDokumentContext;
import de.ivu.elect.business.dokumente.control.ProgressMonitor;
import de.ivu.elect.business.dokumente.entity.DokumentFileNames;
import de.ivu.elect.business.dokumente.entity.DokumentGenerierenProgress;
import de.ivu.elect.business.dokumente.entity.DokumentKonverterResult;
import de.ivu.elect.business.dokumente.entity.DokumentVorlage;
import de.ivu.elect.business.dokumente.entity.DokumentVorlageArt;
import de.ivu.elect.business.dokumente.entity.InMemoryDokumentExtraFileOutput;
import de.ivu.elect.business.dokumente.entity.NoDokumentExtraFileOutput;
import de.ivu.elect.business.exchange.entity.DateiExportArt;
import de.ivu.elect.business.person.control.PersonendatenDateiExportCreate;
import de.ivu.elect.business.person.entity.Personendaten;
import de.ivu.elect.business.wahl.boundary.WahlterminDAO;
import de.ivu.elect.business.wahl.control.WahlAttacher;
import de.ivu.elect.business.wahl.entity.Wahl;
import de.ivu.elect.business.wahl.entity.Wahltermin;
import de.ivu.elect.jee.faces.Messages;
import de.ivu.elect.jee.performance.PerformanceLog;
import de.ivu.elect.jee.performance.PerformanceLogTransactionHandling;
import de.ivu.elect.jee.performance.PerformanceStore;
import de.ivu.elect.jee.performance.PhaseIdEnum;
import de.ivu.elect.jee.performance.ViewAccessTarget;
import de.ivu.elect.jee.performance.ViewAccessType;
import de.ivu.elect.jee.performance.ViewType;
import de.ivu.elect.jee.protokollierung.ProtokollThreadLocal;
import de.ivu.elect.presentation.dokumente.DokumentErzeugenResult;


import static java.util.Collections.emptyList;
import static org.apache.commons.lang3.StringUtils.equalsIgnoreCase;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.apache.commons.lang3.StringUtils.startsWithIgnoreCase;

// abgeleitete Klasse mit @Stateless annotieren
public abstract class AbstractDokumentGenerator<E, C extends ElectDokumentContext> {

  private static final Logger LOG = LoggerFactory.getLogger(AbstractDokumentGenerator.class);

  private static final DateiExportArt DATEI_EXPORT_ART = AbstractDokumentMerger.DATEI_EXPORT_ART;

  private static final String FILE_FORMAT_NAME = AbstractDokumentMerger.FILE_FORMAT_NAME;

  @Inject
  protected transient BenutzerDAO benutzerDAO;

  @Inject
  protected transient WahlAttacher wahlAttacher;

  @Inject
  protected transient WahlterminDAO wahlterminDAO;

  @Inject
  protected transient DateiExportDAO dateiExportDAO;

  @Inject
  protected transient DokumentVorlageDAO dokumentVorlageDAO;

  @Inject
  protected transient DokumentGenerieren dokumentGenerieren;

  @Inject
  protected transient DokumentKonverterLookup dokumentKonverterLookup;

  @Inject
  protected transient PersonendatenDateiExportCreate personendatenDateiExportCreate;

  @Inject
  protected transient Messages messages;

  protected AbstractDokumentGenerator() {
    super();
  }

  public DateiExportArt getDateiExportArt() {
    return DATEI_EXPORT_ART;
  }

  // ELECTMUC-2216
  public Predicate<DateiExport> getDateiExportPredicate() {
    return dateiExport -> {
      Datei datei = dateiExport.getDatei();
      String dateinamePrefix = DokumentFileNames.generateDateinamePrefix(getMessages(), getDokumentVorlageArt());

      return datei != null && equalsIgnoreCase(datei.getFormat(), FILE_FORMAT_NAME) && startsWithIgnoreCase(datei.getDateiname(), dateinamePrefix);
    };
  }

  @Asynchronous
  public Future<DokumentErzeugenResult> generiereDokumentAsynchron(List<E> entitiesNotAttached, @Nullable DokumentVorlage dokumentVorlageNotAttached, @Nullable UploadedFile uploadedFile,
                                                                   FileType zieldateiTyp, Wahl wahlNotAttached, Wahltermin wahlterminNotAttached, C context, ProgressMonitor monitor) {
    long start = System.currentTimeMillis();

    DokumentErzeugenResult result;

    try {
      result = generiereDokument(entitiesNotAttached, dokumentVorlageNotAttached, uploadedFile, zieldateiTyp, wahlNotAttached, wahlterminNotAttached, context,
          monitor);
    } finally {
      try {
        if (PerformanceStore.isStatistikAktiviert()) {
          PerformanceStore.addDuration(
              new ViewAccessTarget(ViewType.EXPORT, ViewAccessType.GESAMT, PhaseIdEnum.INVOKE_APPLICATION, ViewAccessTarget.SESSION_ID_OHNE,
                  getClass().getSimpleName() + ":generiereDokumentAsynchron"), System.currentTimeMillis() - start);
        }
      } catch (Exception ex) {
        LOG.warn("Fehler beim Loggen der Statistiken: {}, {}", ex.getMessage(), getClass().getName());
      }
    }

    return new AsyncResult<>(result);
  }

  @PerformanceLog(viewType = ViewType.EXPORT, transactionHandling = PerformanceLogTransactionHandling.IMMEDIATELY)
  public Future<DokumentErzeugenResult> generiereDokumentSynchron(List<E> entitiesNotAttached, @Nullable DokumentVorlage dokumentVorlageNotAttached, @Nullable UploadedFile uploadedFile,
                                                                  FileType zieldateiTyp, Wahl wahlNotAttached, Wahltermin wahlterminNotAttached, C context, ProgressMonitor monitor) {
    DokumentErzeugenResult result = generiereDokument(entitiesNotAttached, dokumentVorlageNotAttached, uploadedFile, zieldateiTyp, wahlNotAttached,
        wahlterminNotAttached, context, monitor);

    // NOTE, dass das Ergebnis VOR Rückgabe des Future erzeugt werden muss!
    return new Future<DokumentErzeugenResult>() {

      @Override
      public boolean isDone() {
        return true;
      }

      @Override
      public DokumentErzeugenResult get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
        return get();
      }

      @Override
      public DokumentErzeugenResult get() throws InterruptedException, ExecutionException {
        return result;
      }

      @Override
      public boolean isCancelled() {
        return false;
      }

      @Override
      public boolean cancel(boolean mayInterruptIfRunning) {
        return false;
      }
    };
  }

  @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW) // ELECTVAPP-723 und beachte ELECT-10910
  @PerformanceLog(viewType = ViewType.EXPORT, transactionHandling = PerformanceLogTransactionHandling.IMMEDIATELY)
  public DokumentErzeugenResult generiereDokument(List<E> entitiesNotAttached, @Nullable DokumentVorlage dokumentVorlageNotAttached, @Nullable UploadedFile uploadedFile, FileType zieldateiTyp,
                                                  Wahl wahlNotAttached, Wahltermin wahlterminNotAttached, C context, ProgressMonitor monitor) {
    try {
      return DokumentErzeugenResult.success(
          createDateiExport(entitiesNotAttached, dokumentVorlageNotAttached, uploadedFile, zieldateiTyp, wahlNotAttached, wahlterminNotAttached, context,
              monitor));
    } catch (Exception ex) {
      // NOTE ELECT-8206, dass die Exception HIER (noch) NICHT weitergeworfen werden darf!
      LOG.error(ex.getLocalizedMessage(), ex);
      return DokumentErzeugenResult.error(ex);
    }
  }

  protected DateiExportPK createDateiExport(List<E> entitiesNotAttached, @Nullable DokumentVorlage dokumentVorlageNotAttached, @Nullable UploadedFile uploadedFile, FileType zieldateiTyp,
                                            Wahl wahlNotAttached, Wahltermin wahlterminNotAttached, C context, ProgressMonitor monitor) {
    DokumentKonverter<E, C> dokumentKonverter = getDokumentKonverter();

    if (dokumentKonverter == null || !isDokumentErzeugungMoeglich()) {
      throw new IllegalStateException(
          "Kein Konverter für Dokumententyp \"" + getMessage(getDokumentVorlageArt()) + "\" (" + getDokumentVorlageArt() + ") gefunden!");
    }

    List<E> entities;

    if (entitiesNotAttached == null || entitiesNotAttached.isEmpty()) {
      entities = ladeDaten(monitor, context);
    } else {
      entities = attach(entitiesNotAttached, monitor, context);
    }

    DokumentVorlage dokumentVorlage = dokumentVorlageDAO.attach(dokumentVorlageNotAttached);
    DokumentExtraFileOutput dokumentExtraFileOutput = erzeugeDokumentExtraFileOutput(dokumentVorlage, uploadedFile, monitor);
    DokumentKonverterResult konvertierteEingabe = dokumentKonverter.konvertiereInDokumentEingabe(entities, context, dokumentExtraFileOutput, monitor);

    if (monitor.isCanceled()) {
      throw new RuntimeException("canceled");
    }

    Wahl wahl = wahlAttacher.attach(wahlNotAttached);
    Wahltermin wahltermin = wahlterminDAO.attach(wahlterminNotAttached);

    if (context.getBenutzer() != null && isNotBlank(context.getBenutzer().getBenutzerkennung()) && context.getBenutzer().isPersistent()/*ELECT-14186*/) {
      Benutzer benutzer = benutzerDAO.attach(context.getBenutzer());
      ProtokollThreadLocal.setBenutzerKennung(
          benutzer.getBenutzerkennung()); // Benutzerkennung setzen, ansonsten wird (bei asynchroner Ausführung?) "***SYSTEM***" gesetzt...
    }

    DateiExport dateiExport = dokumentGenerieren.generiereDokument(dokumentVorlage, uploadedFile, konvertierteEingabe, dokumentExtraFileOutput, wahl,
        wahltermin, zieldateiTyp,
        getDokumentFileNames(dokumentVorlage, uploadedFile, context), monitor);

    if (isPostProcessNotwendig(entities, context)) {
      monitor.progress(DokumentGenerierenProgress.ABGESCHLOSSEN);

      postProcess(entities, context);

      monitor.progress(DokumentGenerierenProgress.ENTITIES_ANGEPASST);
    } else {
      monitor.progress(DokumentGenerierenProgress.ABGESCHLOSSEN_DOWNLOAD);
    }

    postGeneriereDokument(dateiExport, entities, dokumentVorlage, zieldateiTyp, wahl, wahltermin, context, monitor);

    return dateiExport.getPk();
  }

  private DokumentExtraFileOutput erzeugeDokumentExtraFileOutput(DokumentVorlage dokumentVorlage, UploadedFile uploadedFile, ProgressMonitor monitor) {
    if (isDokumentVorlageArchive(dokumentVorlage, uploadedFile)) {
      return new InMemoryDokumentExtraFileOutput(monitor);
    }

    return NoDokumentExtraFileOutput.INSTANCE;
  }

  private boolean isDokumentVorlageArchive(DokumentVorlage dokumentVorlage, UploadedFile uploadedFile) {
    if (uploadedFile != null) {
      return dokumentGenerieren.isDokumentVorlageArchive(uploadedFile);
    }

    return isDokumentVorlageArchive(dokumentVorlage);
  }

  private boolean isDokumentVorlageArchive(DokumentVorlage dokumentVorlage) {
    return dokumentVorlage != null && dokumentVorlage.hasFileTypeDokumentVorlageArchive();
  }

  protected void postGeneriereDokument(DateiExport dateiExport, List<E> entities, @Nullable DokumentVorlage dokumentVorlage, FileType zieldateiTyp, Wahl wahl, Wahltermin wahltermin, C context,
                                       ProgressMonitor monitor) {
    // Do nothing
  }

  protected boolean isPostProcessNotwendig(List<E> entities, C context) {
    return false;
  }

  protected void postProcess(List<E> entities, C context) {
    // Do nothing
  }

  protected List<E> attach(List<E> entitiesNotAttached, ProgressMonitor monitor, C context) {
    int count = 0;
    int size = entitiesNotAttached.size();

    List<E> result = new ArrayList<>(size);

    for (E entity : entitiesNotAttached) {
      count++;
      result.add(attach(entity, context));
      monitor.progress(DokumentGenerierenProgress.LADE_DATEN, count, size);
    }

    return result;
  }

  protected abstract E attach(E entity, C context);

  /**
   * Überschreiben, wenn die Daten nicht aus einer Liste von Entitäten kommen
   */
  protected List<E> ladeDaten(ProgressMonitor monitor, C context) {
    return Collections.emptyList();
  }

  protected DateiFileNames getDokumentFileNames(@Nullable DokumentVorlage dokumentVorlage, @Nullable UploadedFile uploadedFile, C context) {
    if (dokumentVorlage == null) {
      if (uploadedFile == null) {
        throw new IllegalStateException();
      }

      return getDokumentFileNames(uploadedFile);
    }

    return getDokumentFileNames(dokumentVorlage);
  }

  protected DateiFileNames getDokumentFileNames(DokumentVorlage dokumentVorlage) {
    return new DokumentFileNames(getMessages(), dokumentVorlage);
  }

  protected DateiFileNames getDokumentFileNames(UploadedFile uploadedFile) {
    return new DokumentFileNames(getMessages(), getDokumentVorlageArt(), uploadedFile.getFileName());
  }

  public abstract DokumentVorlageArt getDokumentVorlageArt();

  @Nullable
  @SuppressWarnings("unchecked")
  private DokumentKonverter<E, C> getDokumentKonverter() {
    return (DokumentKonverter<E, C>) dokumentKonverterLookup.getAlleDokumentKonverter().stream() //
        .filter(konverter -> konverter.getDokumentVorlageArt().equals(getDokumentVorlageArt())) //
        .findFirst().orElse(null);
  }

  public boolean isDokumentErzeugungMoeglich() {
    return getDokumentKonverter() != null;
  }

  protected void protokollierePersonendatenDateiExport(Collection<? extends Personendaten> personendaten, DateiExport dateiExport, DokumentVorlage dokumentVorlage, Benutzer benutzer) {
    protokollierePersonendatenDateiExport(personendaten, dateiExport, dokumentVorlage, benutzer, emptyList());
  }

  // Cf. AbstractExporter#protokollierePersonendatenDateiExport!
  protected void protokollierePersonendatenDateiExport(Collection<? extends Personendaten> personendaten, DateiExport dateiExport, DokumentVorlage dokumentVorlage, Benutzer benutzer,
                                                       List<String> bemerkungen) {
    personendatenDateiExportCreate.save(personendaten, dateiExport, dokumentVorlage, benutzer, bemerkungen);
  }

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

  protected String getMessage(Boolean value) {
    return getMessages().lokalisiere(value);
  }

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

  protected Messages getMessages() {
    return messages;
  }
}
