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

import static java.util.Collections.emptyList;

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 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.UploadedFile;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.ivu.elect.business.benutzerverwaltung.entity.Benutzer;
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.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.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.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.presentation.dokumente.DokumentErzeugenResult;

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

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

  @Inject
  private transient WahlAttacher wahlDAO;

  @Inject
  private transient WahlterminDAO wahlterminDAO;

  @Inject
  private transient DokumentVorlageDAO dokumentVorlageDAO;

  @Inject
  private transient DokumentGenerieren dokumentGenerieren;

  @Inject
  private transient DokumentKonverterLookup dokumentKonverterLookup;

  @Inject
  private transient PersonendatenDateiExportCreate personendatenDateiExportCreate;

  @Inject
  private transient Messages messages;

  public AbstractDokumentGenerator() {
    super();
  }

  @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!
      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) {
    if (!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 = konvertiereInDokumentEingabe(entities, context, dokumentExtraFileOutput, monitor);

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

    DateiExport result = dokumentGenerieren.generiereDokument(dokumentVorlage, uploadedFile, konvertierteEingabe, dokumentExtraFileOutput, wahlDAO.attach(wahlNotAttached),
        wahlterminDAO.attach(wahlterminNotAttached), 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(result, entities, dokumentVorlage, zieldateiTyp, wahlNotAttached, wahlterminNotAttached, context, monitor);

    return result.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, @Nullable List<E> entities, @Nullable DokumentVorlage dokumentVorlage, FileType zieldateiTyp, Wahl wahlNotAttached,
      Wahltermin wahlterminNotAttached, 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();

  protected DokumentKonverterResult konvertiereInDokumentEingabe(List<E> entities, C context, DokumentExtraFileOutput dokumentExtraFileOutput, ProgressMonitor monitor) {
    return getDokumentKonverter().konvertiereInDokumentEingabe(entities, context, dokumentExtraFileOutput, monitor);
  }

  @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;
  }
}
