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

import static de.ivu.elect.business.wahlvorschlag.entity.AbstractWahlvorschlag.TRANSIENT_CONTEXT_KEY_WAHLVORSCHLAGS_PERSONEN;
import static de.ivu.elect.business.wahlvorschlag.entity.AbstractWahlvorschlag.TRANSIENT_CONTEXT_KEY_WVP_WARNUNGEN;
import static de.ivu.elect.business.wahlvorschlag.entity.AbstractWahlvorschlag.extractAllKandidaturen;
import static java.util.stream.Collectors.toSet;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

import javax.annotation.Nullable;
import javax.ejb.Stateless;
import javax.enterprise.inject.Instance;
import javax.inject.Inject;

import de.ivu.elect.business.datei.entity.DateiImport;
import de.ivu.elect.business.ergebnis.boundary.ErgebnisDAO;
import de.ivu.elect.business.ergebnis.entity.Ergebnis;
import de.ivu.elect.business.ergebnis.entity.Ergebnisart;
import de.ivu.elect.business.ergebnis.entity.Gewaehlt;
import de.ivu.elect.business.exchange.control.AbstractEntityImporter;
import de.ivu.elect.business.exchange.control.ImportExchange;
import de.ivu.elect.business.exchange.control.MappingException;
import de.ivu.elect.business.gebietsbaum.entity.Gebiet;
import de.ivu.elect.business.gruppe.boundary.GruppeDAO;
import de.ivu.elect.business.gruppe.boundary.PolitischeGruppeFactory;
import de.ivu.elect.business.gruppe.control.ImporterFindVorhandeneGruppeVerhalten;
import de.ivu.elect.business.gruppe.control.PolitischeGruppeCreate;
import de.ivu.elect.business.gruppe.entity.Gruppe;
import de.ivu.elect.business.gruppe.entity.GruppeTK;
import de.ivu.elect.business.gruppe.entity.PolitischeGruppe;
import de.ivu.elect.business.gruppeimgebiet.control.GruppeImGebietCache;
import de.ivu.elect.business.gruppeimgebiet.control.GruppeImGebietCacheFactory;
import de.ivu.elect.business.gruppeimgebiet.control.GruppeImGebietEinzelbewerberValidator;
import de.ivu.elect.business.gruppeimgebiet.control.GruppeImGebietGruppenGebieteValidator;
import de.ivu.elect.business.gruppeimgebiet.entity.GruppeImGebiet;
import de.ivu.elect.business.kandidatenportal.boundary.WvpWarnungDAO;
import de.ivu.elect.business.kandidatenportal.control.WvpWarnungCreate;
import de.ivu.elect.business.kandidatenportal.entity.WvpWarnung;
import de.ivu.elect.business.kandidatur.control.*;
import de.ivu.elect.business.kandidatur.entity.Direktkandidatur;
import de.ivu.elect.business.kandidatur.entity.Ersatzkandidatur;
import de.ivu.elect.business.kandidatur.entity.Kandidatur;
import de.ivu.elect.business.kandidatur.entity.KandidaturExtendedTK;
import de.ivu.elect.business.kandidatur.entity.KandidaturStatus;
import de.ivu.elect.business.kandidatur.entity.Listenkandidatur;
import de.ivu.elect.business.kandidatur.entity.ListenkandidaturMeta;
import de.ivu.elect.business.person.boundary.PersonenVerknuepfungDAO;
import de.ivu.elect.business.person.entity.PersonStatus;
import de.ivu.elect.business.person.entity.PersonenVerknuepfung;
import de.ivu.elect.business.person.entity.Personendaten;
import de.ivu.elect.business.person.entity.PersonendatenPairKey;
import de.ivu.elect.business.person.entity.PersonenregisterAbgleichStatus;
import de.ivu.elect.business.staat.boundary.StaatDAO;
import de.ivu.elect.business.staat.boundary.StaatFinder;
import de.ivu.elect.business.staat.entity.Staat;
import de.ivu.elect.business.wahl.boundary.WahlDAO;
import de.ivu.elect.business.wahl.entity.Wahl;
import de.ivu.elect.business.wahlvorschlag.boundary.DirektkandidatEdit;
import de.ivu.elect.business.wahlvorschlag.boundary.EinzelbewerberWahlvoschlagBoundaryCreate;
import de.ivu.elect.business.wahlvorschlag.boundary.ListeEdit;
import de.ivu.elect.business.wahlvorschlag.boundary.WahlvorschlagDAO;
import de.ivu.elect.business.wahlvorschlag.control.DirektkandidatCreate;
import de.ivu.elect.business.wahlvorschlag.control.ListeCreate;
import de.ivu.elect.business.wahlvorschlag.control.UnattachedWahlvorschlagByTiefeImGebietsbaumComparator;
import de.ivu.elect.business.wahlvorschlag.control.WahlvorschlagByGebietComparator;
import de.ivu.elect.business.wahlvorschlag.control.WahlvorschlagsdokumentCreate;
import de.ivu.elect.business.wahlvorschlag.entity.Direktkandidat;
import de.ivu.elect.business.wahlvorschlag.entity.Liste;
import de.ivu.elect.business.wahlvorschlag.entity.Wahlvorschlag;
import de.ivu.elect.business.wahlvorschlag.entity.WahlvorschlagMeta;
import de.ivu.elect.business.wahlvorschlag.entity.WahlvorschlagStatus;
import de.ivu.elect.business.wahlvorschlag.entity.WahlvorschlagTK;
import de.ivu.elect.business.wahlvorschlag.entity.Wahlvorschlagsdokument;
import de.ivu.elect.business.wahlvorschlagsperson.boundary.WahlvorschlagsPersonDAO;
import de.ivu.elect.business.wahlvorschlagsperson.boundary.WahlvorschlagsPersonEdit;
import de.ivu.elect.business.wahlvorschlagsperson.control.WahlvorschlagsPersonCreate;
import de.ivu.elect.business.wahlvorschlagsperson.entity.WahlvorschlagsPerson;
import de.ivu.elect.jee.entity.Meta;
import de.ivu.elect.jee.exchange.ImportOptions;
import de.ivu.elect.jee.exchange.Violations;
import de.ivu.elect.presentation.wahlvorschlag.WahlvorschlagsdokumentFactory;
import de.ivu.elect.util.CollectionSeparator;

@Stateless
public class WahlvorschlagImporter extends AbstractEntityImporter<Wahlvorschlag, WahlvorschlagTK, WahlvorschlagImportContext, WahlvorschlagDateiImportBericht> {

  @Inject
  protected transient WahlvorschlagImportExchange wahlvorschlagImportExchange;

  @Inject
  protected transient WahlvorschlagMeta wahlvorschlagMeta;

  @Inject
  protected transient GruppeImGebietGruppenGebieteValidator gruppeImGebietGruppenGebieteValidator;

  @Inject
  protected transient GruppeImGebietEinzelbewerberValidator gruppeImGebietEinzelbewerberValidator;

  @Inject
  protected transient KandidaturEdit kandidaturEdit;

  @Inject
  protected transient DirektkandidatEdit direktkandidatEdit;

  @Inject
  protected transient ListeEdit listeEdit;

  @Inject
  protected transient WahlDAO wahlDAO;

  @Inject
  protected transient GruppeDAO gruppeDAO;

  @Inject
  protected transient GruppeImGebietCacheFactory gruppeImGebietCacheFactory;

  @Inject
  protected transient ListeCreate listeCreate;

  @Inject
  protected transient DirektkandidatCreate direktkandidatCreate;

  @Inject
  protected transient EinzelbewerberWahlvoschlagBoundaryCreate einzelbewerberWahlvoschlagBoundaryCreate;

  @Inject
  protected transient ListenkandidaturMeta listenkandidaturMeta;

  @Inject
  protected transient WahlvorschlagDAO wahlvorschlagDAO;

  @Inject
  protected transient KandidaturDAO kandidaturDAO;

  @Inject
  protected transient PersonenVerknuepfungDAO personenVerknuepfungDao;

  @Inject
  protected transient ListenkandidaturCreate listenkkandidaturCreate;

  @Inject
  protected transient DirektkandidaturCreate direktkandidaturCreate;

  @Inject
  protected transient ErsatzkandidaturCreate ersatzkandidaturCreate;

  @Inject
  protected transient ErgebnisDAO ergebnisDAO;

  @Inject
  protected transient WahlvorschlagsPersonDAO wahlvorschlagsPersonDAO;

  @Inject
  protected transient WahlvorschlagsPersonEdit wahlvorschlagsPersonEdit;

  @Inject
  protected transient WahlvorschlagsPersonCreate wahlvorschlagsPersonCreate;

  @Inject
  protected transient WvpWarnungDAO wvpWarnungDAO;

  @Inject
  protected transient WvpWarnungCreate wvpWarnungCreate;

  @Inject
  protected transient WahlvorschlagsdokumentFactory wahlvorschlagsdokumentFactory;

  @Inject
  protected transient WahlvorschlagsdokumentCreate wahlvorschlagsdokumentCreate;

  @Inject
  protected transient Instance<WahlvorschlagImportFilter> wahlvorschlagFilters;

  @Inject
  private transient PolitischeGruppeFactory politischeGruppeFactory;

  @Inject
  private transient PolitischeGruppeCreate politischeGruppeCreate;

  @Inject
  private transient StaatDAO staatDAO;

  @Inject
  private transient StaatFinder staatFinder;

  public WahlvorschlagImporter() {
    super();
  }

  @Override
  protected List<Wahlvorschlag> readImportObject(DateiImport dateiImport, WahlvorschlagImportContext context) throws MappingException {
    List<Wahlvorschlag> result = super.readImportObject(dateiImport, context);

    // Wahlvorschläge filtern, falls eine Datei mit zu vielen Informationen importiert wird
    for (WahlvorschlagImportFilter wahlvorschlagFilter : wahlvorschlagFilters) {
      if (wahlvorschlagFilter.isFilterApplicable(dateiImport, context)) {
        result = wahlvorschlagFilter.filtereWahlvorschlaege(result, context);
      }
    }

    return result;
  }

  @Override
  protected boolean isUnterstuetztAnlegenWennNichtVorhanden() {
    return true;
  }

  @Override
  protected boolean isUnterstuetztAktualisiereBestehendenDaten() {
    return true;
  }

  @Override
  protected boolean isUnterstuetztLoescheBestehendenDatenWennNichtInImportDatenVorhanden() {
    return true;
  }

  @Override
  protected void importiereImportObject(CollectionSeparator<Wahlvorschlag, WahlvorschlagTK> separator, WahlvorschlagImportContext context, ImportOptions importOptions,
      WahlvorschlagDateiImportBericht importBericht, StringBuilder bericht) {

    Wahl wahl = attach(context.getWahl());

    // ELECTMUC-983
    if (importOptions.isImportDatenUebernehmenWennNichtBereitsVorhanden()) {
      addUnveraenderteWahlvorschlaegeToImportBericht(separator, importBericht);
    }

    importDerListenkandidaturenBehandeln(separator, importOptions, wahl, context, importBericht);

    super.importiereImportObject(separator, context, importOptions, importBericht, bericht);

    // Flush aufrufen, damit nachfolgende Finder (hier: beim Persistieren der Verknüpfungen) die frisch angelegten Kandidaturen finden
    kandidaturDAO.flush();

    Set<PersonenVerknuepfung> verknuepfungen = extractVerknuepfungen(extractAllKandidaturen(getPersistierteNichtAktualisierteNichtGeloeschteEntities(separator)), context);
    persistPersonenVerknuepfungenIfNotInDb(wahl, verknuepfungen, context, importBericht);
  }

  /**
   * Ermittelt genau die importierten Entitäten -- nicht die aktualisierten -- nicht die gelöschten.
   * <p/>
   * Grund: für das Finden von Dubletten mit Bestandsdaten ist die Dublettensuche die bessere Wahl.
   * <p/>
   * cf. ELECT-3363
   *
   * @param separator
   *          mit First == importierte Entitäten und Second = bestehende Datenbank-Entitäten
   */
  private Iterable<Wahlvorschlag> getPersistierteNichtAktualisierteNichtGeloeschteEntities(CollectionSeparator<Wahlvorschlag, WahlvorschlagTK> separator) {
    return separator.getInFirstNotInSecond();
  }

  @Override
  protected void loescheAusDatenbank(Collection<Wahlvorschlag> entitiesInDb, WahlvorschlagImportContext context, WahlvorschlagDateiImportBericht customDateiImportBerichtCustomData,
      StringBuilder bericht) {
    Set<Liste> listen = new HashSet<>();
    Set<Direktkandidat> direktkandidaten = new HashSet<>();

    for (Wahlvorschlag wahlvorschlagInDb : entitiesInDb) {
      if (wahlvorschlagInDb.isListe()) {
        listen.add(wahlvorschlagInDb.castTo(Liste.class).getEntity());
      } else if (wahlvorschlagInDb.isDirektkandidat()) {
        direktkandidaten.add(wahlvorschlagInDb.castTo(Direktkandidat.class).getEntity());
      } else {
        throw new IllegalStateException("Wahlvorschlag [" + wahlvorschlagInDb + "] hat nicht unterstützte Wahlvorschlags-Art!");
      }
    }

    deleteListen(listen);
    deleteDirektkandidaten(direktkandidaten);

    kandidaturDAO.flush(); // notwendig, da in persistiere() ein Finder des DAOs aufgerufen wird
  }

  /**
   * NOTE, dass die Aktualisierung zum Import EINES konsistenten Datenstandes aus MEHREREN Dateien dient; nicht zur nachträglichen Aktualisierung auf einen neueren Datenstand (dazu müssen zuerst alle
   * Wahlvorschläge gelöscht werden).
   */
  @Override
  protected void aktualisiere(Wahlvorschlag entityInDatei, Wahlvorschlag entityInDatenbank, WahlvorschlagImportContext context, WahlvorschlagDateiImportBericht importBericht, StringBuilder bericht) {

    aktualisiereGruppeImGebiet(entityInDatei.getGruppeImGebiet(), entityInDatenbank.getGruppeImGebiet());

    if (entityInDatei.isListe()) {
      Wahl wahl = attach(context.getWahl());
      Ergebnis ergebnisVorperiode = findOrCreateErgebnisVorperiode(wahl);
      Ergebnis ergebnisEndgueltig = findOrCreateErgebnisEndgueltig(wahl);
      aktualisiereListe(entityInDatei.castTo(Liste.class).getEntity(), entityInDatenbank.castTo(Liste.class).getEntity(), wahl, ergebnisVorperiode, ergebnisEndgueltig,
          context.getZulassungsVerhalten(), context, importBericht);
    } else if (entityInDatei.isDirektkandidat()) {
      aktualisiereDirektkandidat(entityInDatei.castTo(Direktkandidat.class).getEntity(), entityInDatenbank.castTo(Direktkandidat.class).getEntity());
    }

    aktualisiereWahlvorschlagsPersonen(entityInDatei, entityInDatenbank);
    aktualisiereWvpWarnungen(entityInDatei, entityInDatenbank);
  }

  private void aktualisiereGruppeImGebiet(GruppeImGebiet zuImportierendeGruppeImGebiet, GruppeImGebiet bestehendeGruppeImGebiet) {
    bestehendeGruppeImGebiet.setName(zuImportierendeGruppeImGebiet.getName());
    bestehendeGruppeImGebiet.setKurzname(zuImportierendeGruppeImGebiet.getKurzname());
    bestehendeGruppeImGebiet.setZusatzbezeichnung(zuImportierendeGruppeImGebiet.getZusatzbezeichnung());
    if (zuImportierendeGruppeImGebiet.getRelativePosition() != null) {
      bestehendeGruppeImGebiet.setRelativePosition(zuImportierendeGruppeImGebiet.getRelativePosition());
    }
    if (zuImportierendeGruppeImGebiet.getEingereichtePosition() != null) {
      bestehendeGruppeImGebiet.setEingereichtePosition(zuImportierendeGruppeImGebiet.getEingereichtePosition());
    }
    if (zuImportierendeGruppeImGebiet.getVorgegebenePosition() != null) {
      bestehendeGruppeImGebiet.setVorgegebenePosition(zuImportierendeGruppeImGebiet.getVorgegebenePosition());
    }
  }

  private void aktualisiereDirektkandidat(Direktkandidat zuImportierenderDirektkandidat, Direktkandidat bestehenderDirektkandidat) {
    PolitischeGruppe gruppe = bestehenderDirektkandidat.getGruppe();
    String bestehenderGruppeName = gruppe.getName();
    String bestehenderGruppeKurzname = gruppe.getKurzname();

    copyDirektkandidatAttributesOhneZeitpunktDerEinreichung(zuImportierenderDirektkandidat, bestehenderDirektkandidat);

    Direktkandidatur zuImportierendeDirektkandidatur = zuImportierenderDirektkandidat.getDirektkandidatur();
    Direktkandidatur bestehendeDirektkandidatur = bestehenderDirektkandidat.getDirektkandidatur();

    zuImportierendeDirektkandidatur.copyAttributesUsingSetterMitAnschrift(bestehendeDirektkandidatur);

    // copyAttributesUsingSetter überschreibt bei Direktkanditaten auch Name und Kurzname der Gruppe. Falls das gewollt ist,
    // wird dies bereits im Mapper gemacht. Hier wollen wir das nicht, damit die Mapper entscheiden können.
    gruppe.setName(bestehenderGruppeName);
    gruppe.setKurzname(bestehenderGruppeKurzname);

    aktualisiereZugeordneteErsatzkandidatur(zuImportierendeDirektkandidatur, bestehendeDirektkandidatur);
  }

  private void copyDirektkandidatAttributesOhneZeitpunktDerEinreichung(Direktkandidat zuImportierenderDirektkandidat, Direktkandidat bestehenderDirektkandidat) {
    Date zeitpunktDerEinreichung = bestehenderDirektkandidat.getZeitpunktDerEinreichung();
    zuImportierenderDirektkandidat.copyEntityAttributesUsingSetter(bestehenderDirektkandidat);
    bestehenderDirektkandidat.setZeitpunktDerEinreichung(zeitpunktDerEinreichung);
  }

  private void aktualisiereZugeordneteErsatzkandidatur(Kandidatur zuImportierendeKandidatur, Kandidatur bestehendeKandidatur) {
    Ersatzkandidatur zuImportierendeErsatzkandidatur = zuImportierendeKandidatur.getZugeordneteErsatzkandidatur();
    Ersatzkandidatur bestehendeErsatzkandidatur = bestehendeKandidatur.getZugeordneteErsatzkandidatur();
    if (zuImportierendeErsatzkandidatur != null) {
      if (bestehendeErsatzkandidatur == null) {
        // anlegen
        zuImportierendeErsatzkandidatur.setUebergeordneteKandidatur(bestehendeKandidatur);
        persistiereErsatzkandidatur(zuImportierendeErsatzkandidatur, bestehendeKandidatur.getWahl());
      } else {
        // aktualisieren
        zuImportierendeErsatzkandidatur.copyAttributesUsingSetterMitAnschrift(bestehendeErsatzkandidatur);
      }
    } else if (bestehendeErsatzkandidatur != null) {
      // löschen
      bestehendeKandidatur.setZugeordneteErsatzkandidatur(null);
      kandidaturEdit.delete(bestehendeErsatzkandidatur);
    }
  }

  /**
   * NOTE: Bei einer Aktualisierung müssen wir davon ausgehen, dass die bestehendeListe schon Listenkandidaturen hat; diese sind zu behalten!
   */
  private void aktualisiereListe(Liste zuImportierendeListe, Liste bestehendeListe, Wahl wahl, Ergebnis ergebnisVorperiode, Ergebnis ergebnisEndgueltig,
      WahlvorschlagImporterZulassungsVerhalten zulassungsVerhalten, WahlvorschlagImportContext context, WahlvorschlagDateiImportBericht importBericht) {

    ImportExchange<List<Wahlvorschlag>, WahlvorschlagImportContext> exchange = getExchange();

    Map<KandidaturExtendedTK, Kandidatur> bestehendeKandidaturTkMap = context.getKandidaturenNachTkFunction().apply(bestehendeListe.extractAllKandidaturen());

    copyListeAttributesOhneZeitpunktDerEinreichung(zuImportierendeListe, bestehendeListe);

    WahlvorschlagStatus wahlvorschlagStatusImportiert = bestehendeListe.getWahlvorschlagStatus();

    for (Listenkandidatur zuImportierendeListenkandidatur : zuImportierendeListe.getListenkandidaturen()) {
      Optional<Kandidatur> optionalBestehendeKandidatur = Optional.ofNullable(bestehendeKandidaturTkMap.getOrDefault(context.getKandidaturExtendedTK(zuImportierendeListenkandidatur), null));

      if (optionalBestehendeKandidatur.isPresent()) {
        // Immerhin vergleicht der Wahlvorschlagsimport die Kandidaturen nur anhand der Listenposition (cf. KandidaturExtendedTK)
        optionalBestehendeKandidatur.ifPresent(zuImportierendeListenkandidatur::copyAttributesUsingSetterMitAnschrift);

        aktualisiereZugeordneteErsatzkandidatur(zuImportierendeListenkandidatur, optionalBestehendeKandidatur.get());

      } else {
        zuImportierendeListenkandidatur.setListe(bestehendeListe);

        setzeZulassungsverhalten(zuImportierendeListenkandidatur, zulassungsVerhalten);

        persistiereListenkandidatur(zuImportierendeListenkandidatur, wahl, ergebnisVorperiode, ergebnisEndgueltig, importBericht);
      }

    }

    // NOTE: WahlvorschlagStatus hinterher setzen; er wird bei jedem ListenkandidaturCreate zurückgesetzt ...
    if (zulassungsVerhalten.isZulassungsstatusImportieren()) {
      bestehendeListe.setWahlvorschlagStatus(wahlvorschlagStatusImportiert);
    } else {
      bestehendeListe.setWahlvorschlagStatus(zulassungsVerhalten.getListeWahlvorschlagStatus());
    }

  }

  private void copyListeAttributesOhneZeitpunktDerEinreichung(Liste zuImportierendeListe, Liste bestehendeListe) {
    Date zeitpunktDerEinreichung = bestehendeListe.getZeitpunktDerEinreichung();
    zuImportierendeListe.copyEntityAttributesUsingSetter(bestehendeListe);
    bestehendeListe.setZeitpunktDerEinreichung(zeitpunktDerEinreichung);
  }

  private void setzeZulassungsverhalten(Listenkandidatur zuImportierendeListenkandidatur, WahlvorschlagImporterZulassungsVerhalten zulassungsVerhalten) {
    if (zulassungsVerhalten.isZulassungsstatusImportieren()) {
      return;
    }
    setzeZulassungsVerhalten(zuImportierendeListenkandidatur, zulassungsVerhalten.getListenkandidaturPersonStatus(), zulassungsVerhalten.getListenkandidaturPersonenregisterAbgleichStatus(),
        zulassungsVerhalten.getListenkandidaturKandidaturStatus());
  }

  private void setzeZulassungsVerhalten(Direktkandidatur direktkandidatur, WahlvorschlagImporterZulassungsVerhalten zulassungsVerhalten) {
    if (zulassungsVerhalten.isZulassungsstatusImportieren()) {
      return;
    }
    setzeZulassungsVerhalten(direktkandidatur, zulassungsVerhalten.getDirektkandidaturPersonStatus(), zulassungsVerhalten.getDirektkandidaturPersonenregisterAbgleichStatus(),
        zulassungsVerhalten.getDirektkandidaturKandidaturStatus());
  }

  private void setzeZulassungsVerhalten(Kandidatur kandidatur, PersonStatus personStatus, PersonenregisterAbgleichStatus personenregisterAbgleichStatus, KandidaturStatus kandidaturStatus) {
    kandidatur.setPersonStatus(personStatus);
    kandidatur.setPersonenregisterAbgleichStatus(personenregisterAbgleichStatus);
    kandidatur.setKandidaturStatus(kandidaturStatus);
    for (Ersatzkandidatur ersatzkandidatur : kandidatur.getErsatzkandidatursByUebergeordneteKandidatur()) {
      setzeZulassungsVerhalten(ersatzkandidatur, personStatus, personenregisterAbgleichStatus, kandidaturStatus);
    }
  }

  private void aktualisiereWahlvorschlagsPersonen(Wahlvorschlag wahlvorschlagInDatei, Wahlvorschlag wahlvorschlagInDb) {
    List<WahlvorschlagsPerson> wahlvorschlagsPersonenInDatei = (List<WahlvorschlagsPerson>) wahlvorschlagInDatei.getTransientContext().get(TRANSIENT_CONTEXT_KEY_WAHLVORSCHLAGS_PERSONEN);

    if (wahlvorschlagsPersonenInDatei != null) {
      // Vorhandene löschen...
      wahlvorschlagsPersonEdit.deleteAll(wahlvorschlagsPersonDAO.findByWahlvorschlag(wahlvorschlagInDb));

      // ...und neue anlegen...
      wahlvorschlagsPersonenInDatei.forEach(wahlvorschlagsPerson -> {
        wahlvorschlagsPerson.setWahlvorschlag(wahlvorschlagInDb);
        tryToAttachStaatsangehoerigkeiten(wahlvorschlagsPerson);
        wahlvorschlagsPersonCreate.save(wahlvorschlagsPerson);
      });
    }
  }

  private void aktualisiereWvpWarnungen(Wahlvorschlag wahlvorschlagInDatei, Wahlvorschlag wahlvorschlagInDb) {
    List<WvpWarnung> wvpWarnungenInDatei = (List<WvpWarnung>) wahlvorschlagInDatei.getTransientContext().get(TRANSIENT_CONTEXT_KEY_WVP_WARNUNGEN);
    if (wvpWarnungenInDatei != null) {
      // Vorhandene löschen...
      wvpWarnungDAO.findByWahlvorschlag(wahlvorschlagInDb).forEach(wvpWarnungDAO::remove);
      // ...und neue anlegen...
      wvpWarnungenInDatei.forEach(wvpWarnung -> {
        wvpWarnung.setWahlvorschlag(wahlvorschlagInDb);
        Kandidatur kandidaturInDatei = wvpWarnung.getKandidatur();
        if (kandidaturInDatei != null) {
          if (kandidaturInDatei.isDirektkandidatur() && wahlvorschlagInDb.isDirektkandidat()) {
            wvpWarnung.setKandidatur(wahlvorschlagInDb.castTo(Direktkandidat.class).getEntity().getDirektkandidatur());
          } else if (kandidaturInDatei.isListenkandidatur() && wahlvorschlagInDb.isListe()) {
            Listenkandidatur listenkandidaturInDatei = kandidaturInDatei.castTo(Listenkandidatur.class).getEntity();
            Long eingereichtePosition = listenkandidaturInDatei.getEingereichtePosition();
            if (eingereichtePosition != null) {
              Liste listeInDb = wahlvorschlagInDb.castTo(Liste.class).getEntity();
              Optional<Listenkandidatur> listenkandidaturInDb = listeInDb.getListenkandidatursByListe().stream().filter(k -> eingereichtePosition.equals(k.getEingereichtePosition())).findAny();
              if (listenkandidaturInDb.isPresent()) {
                wvpWarnung.setKandidatur(listenkandidaturInDb.get());
              }
            }
          } else if (kandidaturInDatei.isErsatzkandidatur()) {
            // TODO Wie wollen wir zukünftig WVP-Warnungen für Ersatzkandidaturen behandeln?
            throw new UnsupportedOperationException("Implementierung für die Aktualisierung von WVP-Warnungen für Ersatzkandidaturen fehlt.");
          }
        }
        wvpWarnungCreate.save(wvpWarnung);
      });
    }
  }

  @Override
  protected void persistiere(Collection<Wahlvorschlag> neueWahlvorschlaege, WahlvorschlagImportContext context, WahlvorschlagDateiImportBericht importBericht, StringBuilder bericht) {

    Wahl wahl = attach(context.getWahl());

    legeGruppenAnWennNichtVorhanden(wahl, neueWahlvorschlaege, bericht);

    Ergebnis ergebnisVorperiode = findOrCreateErgebnisVorperiode(wahl);
    Ergebnis ergebnisEndgueltig = findOrCreateErgebnisEndgueltig(wahl);

    /*
     * NOTE, dass der Cache nicht nur aus Performancegründen sinnvoll ist, sondern auch notwendig um häufige Flushes zu vermeiden
     */
    GruppeImGebietCache gruppeImGebietCache = gruppeImGebietCacheFactory.createNewGruppeImGebietCompleteHierarchyCache(wahl);

    /*
     * HINT: Wahlvorschläge, die näher am Wurzelgebiet sind, zuerst persistieren -- sonst legt der GruppeImGebietCompleteHierarchyCache die GruppeImGebiet-Hierarchie nicht richtig an.
     */
    List<Wahlvorschlag> persistierteWahlvorschlaege = sortByTiefeImGebietsbaum(neueWahlvorschlaege, gruppeImGebietCache).stream()
        .map(wahlvorschlag -> persistiereWahlvorschlag(wahlvorschlag, wahl, ergebnisVorperiode, ergebnisEndgueltig, gruppeImGebietCache, context.getZulassungsVerhalten(), importBericht, context))
        .collect(Collectors.toList());

    // WilldFly 11 - erst flush dann Dokumente anlegen, sonst MySQLIntegrityConstraintViolationException  Cannot add or update a child row: a foreign key constraint fails
    if (!persistierteWahlvorschlaege.isEmpty()) {
      wahlvorschlagDAO.flush();
      for (Wahlvorschlag wahlvorschlag : persistierteWahlvorschlaege) {
        persistiereWahlvorschlagsPersonen(wahlvorschlag);
        persistiereWvpWarnungen(wahlvorschlag);
        createAndPersistWahlvorschlagsdokumente(wahlvorschlag);
      }
    }
  }

  // ELECTKOM-13

  protected void legeGruppenAnWennNichtVorhanden(Wahl wahl, Collection<Wahlvorschlag> neueWahlvorschlaege, StringBuilder bericht) {
    Set<GruppeTK> vorhandeneGruppen = gruppeDAO.findByWahl(wahl).stream().map(Gruppe::getGruppeTK).collect(toSet());

    for (Wahlvorschlag wahlvorschlag : neueWahlvorschlaege) {
      PolitischeGruppe politischeGruppeInDatei = wahlvorschlag.getGruppe();
      if (!vorhandeneGruppen.contains(politischeGruppeInDatei.getGruppeTK())) {
        PolitischeGruppe politischeGruppeNeu = politischeGruppeFactory.create();
        politischeGruppeInDatei.copyEntityAttributesUsingSetter(politischeGruppeNeu);

        politischeGruppeNeu.setWahl(wahl);
        politischeGruppeNeu.setLogo(politischeGruppeInDatei.getLogo());

        politischeGruppeCreate.save(politischeGruppeNeu);
        politischeGruppeInDatei.setSchluessel(politischeGruppeNeu.getSchluessel()); //wurde ggf. in save automatisch vergeben.
        vorhandeneGruppen.add(politischeGruppeNeu.getGruppeTK());
        gruppeDAO.flush(); // notwendig für Relative Position in politischeGruppeCreate
        bericht.append(getMessage("wahlvorschlag.import.gruppe.angelegt", politischeGruppeNeu.getSchluessel()));
        bericht.append("\n");
      }
    }

  }

  protected List<Wahlvorschlag> sortByTiefeImGebietsbaum(Collection<Wahlvorschlag> wahlvorschlaegeAusDatei, GruppeImGebietCache gruppeImGebietCache) {
    List<Wahlvorschlag> result = new ArrayList<>(wahlvorschlaegeAusDatei);

    result.sort(new UnattachedWahlvorschlagByTiefeImGebietsbaumComparator(gruppeImGebietCache));

    return result;
  }

  protected Wahlvorschlag persistiereWahlvorschlag(Wahlvorschlag wahlvorschlag, Wahl wahl, Ergebnis ergebnisVorperiode, Ergebnis ergebnisEndgueltig, GruppeImGebietCache gruppeImGebietCache,
      WahlvorschlagImporterZulassungsVerhalten zulassungsVerhalten, WahlvorschlagDateiImportBericht importBericht, WahlvorschlagImportContext context) {
    Set<GruppeImGebiet> gruppenImGebietAusDatei = wahlvorschlag.getGruppe().getGruppenImGebiet();

    tryToAttachGruppeAndGebiet(gruppeImGebietCache, wahlvorschlag.getGruppeImGebiet());

    if (wahlvorschlag.isListe()) {
      Liste liste = wahlvorschlag.castTo(Liste.class).getEntity();
      WahlvorschlagStatus wahlvorschlagStatusImportiert = liste.getWahlvorschlagStatus();

      persistiereListe(liste, wahl, ergebnisVorperiode, ergebnisEndgueltig, gruppeImGebietCache, zulassungsVerhalten, importBericht);

      // NOTE: WahlvorschlagStatus hinterher setzen; er wird bei jedem ListenkandidaturCreate zurückgesetzt ...
      if (zulassungsVerhalten.isZulassungsstatusImportieren()) {
        liste.setWahlvorschlagStatus(wahlvorschlagStatusImportiert);
      } else {
        liste.setWahlvorschlagStatus(zulassungsVerhalten.getListeWahlvorschlagStatus());
      }
    } else if (wahlvorschlag.isDirektkandidat()) {
      Direktkandidat direktkandidat = wahlvorschlag.castTo(Direktkandidat.class).getEntity();
      Direktkandidatur direktkandidatur = direktkandidat.getDirektkandidatur();

      persistiereDirektkandidat(direktkandidat, wahl, ergebnisVorperiode, ergebnisEndgueltig, gruppeImGebietCache, importBericht);

      setzeZulassungsVerhalten(direktkandidatur, zulassungsVerhalten);
    } else {
      throw new IllegalStateException("Wahlvorschlag [" + wahlvorschlag + "] hat nicht unterstützte Wahlvorschlags-Art!");
    }

    if (wahlvorschlag.getZeitpunktDerEinreichung() == null) {
      wahlvorschlag.setZeitpunktDerEinreichung(new Date());
    }

    // HINT: Kennwort & Kurzname hinterher setzen, um Änderungen durch Create-Beans mitzubekommen (z.B. Einzelbewerber!)
    if (isKennwortAutomatischUebernehmen(context, wahlvorschlag)) {
      wahlvorschlag.setKennwort(getNameAufGebiet(wahlvorschlag));
    }

    if (isKurznameAutomatischUebernehmen(context, wahlvorschlag)) {
      wahlvorschlag.setKurzname(getKurznameAufGebiet(wahlvorschlag));
    }

    aktualisierePositionen(gruppeImGebietCache, gruppenImGebietAusDatei);
    return wahlvorschlag;
  }

  private void persistiereWahlvorschlagsPersonen(Wahlvorschlag wahlvorschlag) {
    List<WahlvorschlagsPerson> wahlvorschlagsPersonen = (List<WahlvorschlagsPerson>) wahlvorschlag.getTransientContext().get(TRANSIENT_CONTEXT_KEY_WAHLVORSCHLAGS_PERSONEN);
    if (wahlvorschlagsPersonen != null) {
      wahlvorschlagsPersonen.forEach(wahlvorschlagsPerson -> {
        tryToAttachStaatsangehoerigkeiten(wahlvorschlagsPerson);
        wahlvorschlagsPersonCreate.save(wahlvorschlagsPerson);
      });
    }
  }

  private void persistiereWvpWarnungen(Wahlvorschlag wahlvorschlag) {
    List<WvpWarnung> wvpWarnungen = (List<WvpWarnung>) wahlvorschlag.getTransientContext().get(TRANSIENT_CONTEXT_KEY_WVP_WARNUNGEN);
    if (wvpWarnungen != null) {
      wvpWarnungen.forEach(wvpWarnungCreate::save);
    }
  }

  private void createAndPersistWahlvorschlagsdokumente(Wahlvorschlag wahlvorschlag) {
    Collection<Wahlvorschlagsdokument> wahlvorschlagsdokumente = wahlvorschlagsdokumentFactory.createAllWahlvorschlagsdokumenteForEntity(wahlvorschlag);

    wahlvorschlagsdokumentCreate.save(wahlvorschlagsdokumente);
  }

  private String getKurznameAufGebiet(Wahlvorschlag wahlvorschlag) {
    return attach(wahlvorschlag.getGruppe()).getKurznameAufGebiet(wahlvorschlag.getGebiet());
  }

  private String getNameAufGebiet(Wahlvorschlag wahlvorschlag) {
    return attach(wahlvorschlag.getGruppe()).getNameAufGebiet(wahlvorschlag.getGebiet());
  }

  protected boolean isKennwortAutomatischUebernehmen(WahlvorschlagImportContext context, Wahlvorschlag wahlvorschlag) {
    return !wahlvorschlag.hasKennwort() && context.getKennwortVonGruppeUebernehmenWennNichtGesetzt(); // ELECT-5265
  }

  protected boolean isKurznameAutomatischUebernehmen(WahlvorschlagImportContext context, Wahlvorschlag wahlvorschlag) {
    return !wahlvorschlag.hasKurzname() && context.getKurznameVonGruppeUebernehmenWennNichtGesetzt(); // ELECT-5265
  }

  protected void aktualisierePositionen(GruppeImGebietCache gruppeImGebietCache, Set<GruppeImGebiet> gruppenImGebietAusDatei) {
    gruppeImGebietCache.aktualisierePositionen(gruppenImGebietAusDatei);
  }

  private void tryToAttachGruppeAndGebiet(GruppeImGebietCache gruppeImGebietCache, GruppeImGebiet gruppeImGebiet) {
    gruppeImGebiet.setGebiet(findGebiet(gruppeImGebietCache, gruppeImGebiet.getGebiet()));
    gruppeImGebiet.setGruppe(tryToFindPolitischeGruppe(gruppeImGebietCache, gruppeImGebiet.getGruppe()));
  }

  protected Gebiet findGebiet(GruppeImGebietCache gruppeImGebietCache, Gebiet gebiet) {
    return gruppeImGebietCache.getCachedGebiet(gebiet.getGebietTK());
  }

  protected Gruppe tryToFindPolitischeGruppe(GruppeImGebietCache gruppeImGebietCache, Gruppe gruppe) {
    if (gruppeImGebietCache.isGruppeVorhanden(gruppe.getGruppeTK())) {
      return gruppeImGebietCache.getCachedGruppe(gruppe.getGruppeTK());
    }

    // NOTE, dass das Erstellen der Gruppe später im EinzelkandidatCreate übernommen wird!
    return gruppe;
  }

  protected void tryToAttachStaatsangehoerigkeiten(Personendaten personendaten) {
    List<Staat> staatsangehoerigkeitenSorted = personendaten.getStaatsangehoerigkeitenSorted();
    staatsangehoerigkeitenSorted.replaceAll(staat -> staatFinder.tryFindUniqueStaat(staatDAO, staat));

    personendaten.setStaatsangehoerigkeitenSorted(staatsangehoerigkeitenSorted);
  }

  protected void persistiereListe(Liste liste, Wahl wahl, Ergebnis ergebnisVorperiode, Ergebnis ergebnisEndgueltig, GruppeImGebietCache gruppeImGebietCache,
      WahlvorschlagImporterZulassungsVerhalten zulassungsVerhalten, WahlvorschlagDateiImportBericht importBericht) {
    for (Listenkandidatur listenkandidatur : liste.getListenkandidaturen()) {
      setzeZulassungsverhalten(listenkandidatur, zulassungsVerhalten);
    }

    listeCreate.save(liste.getGruppeImGebiet(), liste, gruppeImGebietCache);

    importBericht.increaseListen(1L);

    for (Listenkandidatur listenkandidatur : liste.getListenkandidaturen()) {
      persistiereListenkandidatur(listenkandidatur, wahl, ergebnisVorperiode, ergebnisEndgueltig, importBericht);
    }
  }

  protected void persistiereDirektkandidat(Direktkandidat direktkandidat, Wahl wahl, Ergebnis ergebnisVorperiode, Ergebnis ergebnisEndgueltig, GruppeImGebietCache gruppeImGebietCache,
      WahlvorschlagDateiImportBericht importBericht) {
    if (direktkandidat.isEinzelbewerber()) {
      direktkandidat.getGruppe().setWahl(wahl);

      einzelbewerberWahlvoschlagBoundaryCreate.save(direktkandidat, gruppeImGebietCache);
    } else if (isGruppeVorhanden(gruppeImGebietCache, direktkandidat.getGruppeImGebiet())) {
      direktkandidatCreate.save(direktkandidat.getGruppeImGebiet(), direktkandidat, gruppeImGebietCache);
    } else {
      throw new IllegalStateException("Versuch, einen Direktkandidaten für eine unbekannte Gruppe zu persistieren! [direktkandidat = " + direktkandidat + "]");
    }

    persistiereDirektkandidatur(direktkandidat.getDirektkandidatur(), wahl, ergebnisVorperiode, ergebnisEndgueltig, importBericht);
  }

  protected boolean isGruppeVorhanden(GruppeImGebietCache gruppeImGebietCache, GruppeImGebiet gruppeImGebiet) {
    return gruppeImGebietCache.isGruppeVorhanden(gruppeImGebiet.getGruppe().getGruppeTK());
  }

  private void importDerListenkandidaturenBehandeln(CollectionSeparator<Wahlvorschlag, WahlvorschlagTK> separator, ImportOptions importOptions, Wahl wahl, WahlvorschlagImportContext context,
      WahlvorschlagDateiImportBericht importBericht) {
    if (!importOptions.isLoescheBestehendenDatenWennNichtInImportDatenVorhanden()
        && !(importOptions.isImportDatenUebernehmenWennNichtBereitsVorhanden() && !importOptions.isAktualisiereBestehendenDaten())) {
      return;
    }

    Ergebnis ergebnisVorperiode = findOrCreateErgebnisVorperiode(wahl);
    Ergebnis ergebnisEndgueltig = findOrCreateErgebnisEndgueltig(wahl);
    for (WahlvorschlagTK wahlvorschlagTK : separator.getInBoth()) {
      if (!separator.getFirstMap().get(wahlvorschlagTK).isListe() || !separator.getSecondMap().get(wahlvorschlagTK).isListe()) {
        continue;
      }
      Liste listeInDatei = separator.getFirstMap().get(wahlvorschlagTK).castTo(Liste.class).getEntity();
      Liste listeInDb = separator.getSecondMap().get(wahlvorschlagTK).castTo(Liste.class).getEntity();

      Map<KandidaturExtendedTK, Kandidatur> bestehendeKandidaturTks = context.getKandidaturenNachTkFunction().apply(listeInDb.extractAllKandidaturen());
      Set<Kandidatur> kandidaturenInDatei = listeInDatei.extractAllKandidaturen();

      if (importOptions.isLoescheBestehendenDatenWennNichtInImportDatenVorhanden()) {
        kandidaturenInDbUndNichtInDateiLoeschen(bestehendeKandidaturTks, kandidaturenInDatei, context);
      }

      if (importOptions.isImportDatenUebernehmenWennNichtBereitsVorhanden() && !importOptions.isAktualisiereBestehendenDaten()) {
        listenkandidaturenInDateiUndNichtInDbAnlegen(wahl, listeInDb, bestehendeKandidaturTks, kandidaturenInDatei, ergebnisVorperiode, ergebnisEndgueltig, context, importBericht);
      }
    }
  }

  private void listenkandidaturenInDateiUndNichtInDbAnlegen(Wahl wahl, Liste listeInDb, Map<KandidaturExtendedTK, Kandidatur> bestehendeKandidaturTks, Set<Kandidatur> kandidaturenInDatei,
      Ergebnis ergebnisVorperiode, Ergebnis ergebnisEndgueltig, WahlvorschlagImportContext context, WahlvorschlagDateiImportBericht importBericht) {
    kandidaturenInDatei.stream().filter(k -> k.isListenkandidatur() && !bestehendeKandidaturTks.containsKey(context.getKandidaturExtendedTK(k))).forEach(k -> {
      Listenkandidatur listenkandidatur = k.castTo(Listenkandidatur.class).getEntity();
      listenkandidatur.setListe(listeInDb);
      persistiereListenkandidatur(listenkandidatur, wahl, ergebnisVorperiode, ergebnisEndgueltig, importBericht);
    });
  }

  private void kandidaturenInDbUndNichtInDateiLoeschen(Map<KandidaturExtendedTK, Kandidatur> bestehendeKandidaturTks, Set<Kandidatur> kandidaturenInDatei, WahlvorschlagImportContext context) {
    bestehendeKandidaturTks.keySet().stream().filter(dbTk -> isDbKandidaturNichtInDatei(dbTk, kandidaturenInDatei, context)).map(bestehendeKandidaturTks::get).forEach(kandidaturEdit::delete);
  }

  private boolean isDbKandidaturNichtInDatei(KandidaturExtendedTK kandidaturInDbExtendedTK, Set<Kandidatur> kandidaturenInDatei, WahlvorschlagImportContext context) {
    return kandidaturenInDatei.stream().noneMatch(lk -> kandidaturInDbExtendedTK.equals(context.getKandidaturExtendedTK(lk)));
  }

  private void persistiereDirektkandidatur(Direktkandidatur direktkandidatur, Wahl wahl, Ergebnis ergebnisVorperiode, Ergebnis ergebnisEndgueltig, WahlvorschlagDateiImportBericht importBericht) {
    setzeGewaehltErgebnis(direktkandidatur, ergebnisVorperiode, ergebnisEndgueltig);

    setzeWahl(direktkandidatur.getDirektkandidatur(), wahl);

    tryToAttachStaatsangehoerigkeiten(direktkandidatur);

    direktkandidaturCreate.save(direktkandidatur);

    importBericht.increaseDirektkandidaturen(1L);

    // Ersatzkandidaturen werden automatisch persistiert
    importBericht.increaseErsatzkandidaturen(direktkandidatur.getUntergeordneteErsatzkandidaturen().size());
  }

  private void persistiereErsatzkandidatur(Ersatzkandidatur ersatzkandidatur, Wahl wahl) {
    setzeWahl(ersatzkandidatur, wahl);

    tryToAttachStaatsangehoerigkeiten(ersatzkandidatur);

    ersatzkandidaturCreate.save(ersatzkandidatur);
  }

  private void setzeGewaehltErgebnis(Kandidatur kandidatur, Ergebnis ergebnisVorperiode, Ergebnis ergebnisEndgueltig) {
    for (Gewaehlt gewaehlt : kandidatur.getGewaehlte()) {
      if (Ergebnisart.ENDGUELTIG.equals(gewaehlt.getErgebnis().getErgebnisart())) {
        gewaehlt.setErgebnis(ergebnisEndgueltig);
      } else if (Ergebnisart.VORPERIODE.equals(gewaehlt.getErgebnis().getErgebnisart())) {
        gewaehlt.setErgebnis(ergebnisVorperiode);
      } else {
        throw new IllegalStateException("Unbekannte Ergebnisart: " + gewaehlt.getErgebnis().getErgebnisart());
      }
    }

    if (kandidatur.getZugeordneteErsatzkandidatur() != null) {
      setzeGewaehltErgebnis(kandidatur.getZugeordneteErsatzkandidatur(), ergebnisVorperiode, ergebnisEndgueltig);
    }
  }

  private void setzeWahl(Kandidatur kandidatur, Wahl wahl) {
    kandidatur.setWahl(wahl);

    if (kandidatur.getZugeordneteErsatzkandidatur() != null) {
      setzeWahl(kandidatur.getZugeordneteErsatzkandidatur(), wahl);
    }
  }

  private void persistiereListenkandidatur(Listenkandidatur listenkandidatur, Wahl wahl, Ergebnis ergebnisVorperiode, Ergebnis ergebnisEndgueltig, WahlvorschlagDateiImportBericht importBericht) {
    setzeGewaehltErgebnis(listenkandidatur, ergebnisVorperiode, ergebnisEndgueltig);

    setzeWahl(listenkandidatur, wahl);

    tryToAttachStaatsangehoerigkeiten(listenkandidatur);

    listenkkandidaturCreate.save(listenkandidatur);

    importBericht.increaseListenkandidaturen(1L);

    // Ersatzkandidaturen werden automatisch persistiert
    importBericht.increaseErsatzkandidaturen(listenkandidatur.getUntergeordneteErsatzkandidaturen().size());
  }

  private void persistPersonenVerknuepfungenIfNotInDb(Wahl wahl, Set<PersonenVerknuepfung> verknuepfungen, WahlvorschlagImportContext context, WahlvorschlagDateiImportBericht importBericht) {
    Map<KandidaturExtendedTK, Kandidatur> kandidaturenInDbPerTK = context.getKandidaturenNachTkFunction().apply(getKandidaturenDerWahl(wahl));

    Map<PersonendatenPairKey, PersonenVerknuepfung> personenVerknuepfungen = PersonenVerknuepfung.mappePersonenVerknuepfungenNachPersonendatenPairKey(personenVerknuepfungDao.findByWahl(wahl));

    for (PersonenVerknuepfung verknuepfung : verknuepfungen) {
      Kandidatur kandidaturVerknuepfung1 = verknuepfung.getPerson1().castTo(Kandidatur.class).getEntity();
      Personendaten person1 = waehlePersistiertePerson(kandidaturVerknuepfung1, kandidaturenInDbPerTK, context);
      Kandidatur kandidaturVerknuepfung2 = verknuepfung.getPerson2().castTo(Kandidatur.class).getEntity();
      Personendaten person2 = waehlePersistiertePerson(kandidaturVerknuepfung2, kandidaturenInDbPerTK, context);

      if (person1 == null) {
        fehlerPersonNichtGefunden(kandidaturVerknuepfung1, kandidaturenInDbPerTK, context);
      }

      if (person2 == null) {
        fehlerPersonNichtGefunden(kandidaturVerknuepfung2, kandidaturenInDbPerTK, context);
      }

      if (!personenVerknuepfungen.containsKey(new PersonendatenPairKey(person1, person2))) {
        verknuepfung.setPersonen(person1, person2);

        persistPersonenVerknuepfung(verknuepfung);

        personenVerknuepfungen.put(verknuepfung.getPersonendatenPairKey(), verknuepfung);
        importBericht.increaseVerknuepfungen(1L);
      }
    }
  }

  private List<Kandidatur> getKandidaturenDerWahl(Wahl wahl) {
    return kandidaturDAO.findByWahl(wahl)/* ELECTKOM-117 */.stream().filter(k -> k.getWahlvorschlag() != null).collect(Collectors.toList());
  }

  private void fehlerPersonNichtGefunden(Kandidatur kandidatur, Map<KandidaturExtendedTK, Kandidatur> kandidaturenInDbPerTK, WahlvorschlagImportContext context) {
    throw new RuntimeException("Personenverknüpfung kann nicht persistiert werden, da eine zu verknüpfende Person nicht persistent ist.  ZuVerknüpfen=[" + kandidatur + " -- "
        + kandidatur.getDiscriminator() + " --- " + context.getKandidaturExtendedTK(kandidatur) + "]" + " - Habe: " + kandidaturenInDbPerTK);
  }

  /**
   * Bei additivem Import, kann es sein, dass eine der beiden verknüpften Kandidaturen bereits in der Datenbank war und deshalb nicht durch den Import persistiert wurde.
   * <p>
   * Gibt NULL zurück, wenn die Person weder vom Import persistiert wurde, noch in der Datenbank vorliegt (oder dort vom Import gelöscht wurde).
   */
  @Nullable
  private Personendaten waehlePersistiertePerson(Kandidatur kandidatur, Map<KandidaturExtendedTK, Kandidatur> kandidaturenAusDbPerTK, WahlvorschlagImportContext context) {
    if (!kandidatur.isPersistent()) {
      return kandidaturenAusDbPerTK.get(context.getKandidaturExtendedTK(kandidatur));
    }

    return kandidatur;
  }

  private void persistPersonenVerknuepfung(PersonenVerknuepfung verknuepfung) {
    personenVerknuepfungDao.persist(verknuepfung);
  }

  private Ergebnis findOrCreateErgebnisVorperiode(Wahl wahl) {
    return findOrCreateErgebnis(wahl, Ergebnisart.VORPERIODE);
  }

  private Ergebnis findOrCreateErgebnisEndgueltig(Wahl wahl) {
    return findOrCreateErgebnis(wahl, Ergebnisart.ENDGUELTIG);
  }

  private Ergebnis findOrCreateErgebnis(Wahl wahl, Ergebnisart ergebnisart) {
    Ergebnis ergebnis;

    List<Ergebnis> vorperiodenergebnisse = ergebnisDAO.findByWahlAndErgebnisart(wahl, ergebnisart);

    if (vorperiodenergebnisse.isEmpty()) {
      ergebnis = new Ergebnis();
      ergebnis.setWahl(wahl);
      ergebnis.setErgebnisart(ergebnisart);
      ergebnisDAO.persist(ergebnis);
    } else {
      ergebnis = vorperiodenergebnisse.get(0);
    }

    return ergebnis;
  }

  private Set<PersonenVerknuepfung> extractVerknuepfungen(Collection<Kandidatur> kandidaturen, WahlvorschlagImportContext context) {
    Set<PersonenVerknuepfung> verknuepfungen = new HashSet<>();

    final Set<KandidaturExtendedTK> kandidaturExtendedTKs = context.getKandidaturenNachTkFunction().apply(kandidaturen).keySet();

    for (Kandidatur kandidatur : kandidaturen) {
      verknuepfungen.addAll(behalteNurVerknuepfungenInnerhalbDerKandidaturen(kandidatur.getPersonenVerknuepfungsByPerson1(), kandidaturExtendedTKs, context));
      verknuepfungen.addAll(behalteNurVerknuepfungenInnerhalbDerKandidaturen(kandidatur.getPersonenVerknuepfungsByPerson2(), kandidaturExtendedTKs, context));
    }

    return verknuepfungen;
  }

  private Collection<PersonenVerknuepfung> behalteNurVerknuepfungenInnerhalbDerKandidaturen(Collection<PersonenVerknuepfung> verknuepfungen, Set<KandidaturExtendedTK> kandidaturExtendedTKs,
      WahlvorschlagImportContext context) {

    return verknuepfungen.stream().filter(input -> {
      Kandidatur kandidatur1 = input.getPerson1().castTo(Kandidatur.class).getEntity();
      Kandidatur kandidatur2 = input.getPerson2().castTo(Kandidatur.class).getEntity();

      return kandidaturExtendedTKs.contains(context.getKandidaturExtendedTK(kandidatur1)) && kandidaturExtendedTKs.contains(context.getKandidaturExtendedTK(kandidatur2));
    }).collect(toSet());
  }

  @Override
  protected ImportExchange<List<Wahlvorschlag>, WahlvorschlagImportContext> getExchange() {
    return wahlvorschlagImportExchange;
  }

  @Override
  public Meta getMetaForEntityInternal() {
    return wahlvorschlagMeta;
  }

  private void deleteListen(Collection<Liste> listen) {
    listeEdit.delete(listen);
  }

  private void deleteDirektkandidaten(Collection<Direktkandidat> direktkandidaten) {
    direktkandidatEdit.delete(direktkandidaten);
  }

  @Override
  protected List<Wahlvorschlag> getAllEntitiesInDB(WahlvorschlagImportContext context) {
    return wahlvorschlagDAO.findByWahl(context.getWahl());
  }

  @Override
  protected Function<Wahlvorschlag, WahlvorschlagTK> getEntityToEntityTKFunction() {
    return Wahlvorschlag::getWahlvorschlagTK;
  }

  @Override
  protected void validiere(Violations violations, List<Wahlvorschlag> wahlvorschlaege, WahlvorschlagImportContext context, ImportOptions importOptions) {
    List<GruppeImGebiet> gruppenImGebiet = new ArrayList<>();

    for (Wahlvorschlag wahlvorschlag : wahlvorschlaege) {
      // Deaktiviert, da extrem lahm und wenig bis keinen Nutzen, if (validateEntity(violations, kandidatur, kandidatur.getKandidaturTK())) {
      GruppeImGebiet gruppeImGebiet = wahlvorschlag.getGruppeImGebiet();
      if (gruppeImGebiet != null) {
        gruppenImGebiet.add(gruppeImGebiet);
      }
      // }

      for (Kandidatur kandidatur : wahlvorschlag.extractAllKandidaturen()) {
        pruefePersonenVerknuepfung(kandidatur, violations);
      }
    }

    pruefeGruppenImGebiet(context.getWahl(), gruppenImGebiet, violations, context.getFindVorhandeneGruppeVerhalten());

    // ELECT-4052
    for (Wahlvorschlag wahlvorschlag : wahlvorschlaege) {
      validateEntity(violations, wahlvorschlag, wahlvorschlag.getUk(), wahlvorschlagMeta);
      if (wahlvorschlag.isListe()) {
        Liste liste = wahlvorschlag.castTo(Liste.class).getEntity();
        for (Listenkandidatur listenkandidatur : liste.getListenkandidaturen()) {
          validateEntity(violations, listenkandidatur, listenkandidatur.getTk(), listenkandidaturMeta);
        }
      }
    }
  }

  private void pruefePersonenVerknuepfung(Kandidatur kandidatur, Violations violations) {
    // NOTE, dass Person1 nicht null sein kann, da die Verknüpfung immer für diese angelegt wird!
    if (kandidatur.getPersonenVerknuepfungsByPerson1().stream().anyMatch(verknuepfung -> verknuepfung.getPerson2() == null)) {
      violations.addError("wahlvorschlag.import.verknuepfung.inkonsistent", kandidatur.getNamePraesentation());
    }
  }

  protected void pruefeGruppenImGebiet(Wahl wahl, List<GruppeImGebiet> gruppenImGebiet, Violations violations, ImporterFindVorhandeneGruppeVerhalten findVorhandeneGruppeVerhalten) {
    gruppeImGebietGruppenGebieteValidator.pruefeExistenzAlleGruppenUndGebiete(gruppenImGebiet, wahl, violations, findVorhandeneGruppeVerhalten);
    gruppeImGebietEinzelbewerberValidator.pruefeEinzelbewerberAufKeinemAnderemWahlgebietInDB(gruppenImGebiet, wahl, violations);
  }

  @Override
  protected void postImportActions(List<Wahlvorschlag> wahlvorschlaege, WahlvorschlagImportContext context, ImportOptions importOptions, WahlvorschlagDateiImportBericht importBericht,
      StringBuilder bericht) {
    super.postImportActions(wahlvorschlaege, context, importOptions, importBericht, bericht);

    boolean vorab = false;
    erstelleBericht(importBericht, bericht, vorab);
  }

  @Override
  protected void erstelleBericht(List<Wahlvorschlag> importObject, WahlvorschlagImportContext context, ImportOptions importOptions, StringBuilder berichtBuilder, boolean vorab) {
    if (vorab) {
      // TODO: Genau genommen müssten wir erst in die Datenbank schauen, was es dort schon gibt, und ob das aktualisiert wird...
      WahlvorschlagDateiImportBericht importBericht = new WahlvorschlagDateiImportBericht();

      for (Wahlvorschlag wahlvorschlag : importObject) {
        if (wahlvorschlag.isListe()) {
          importBericht.increaseListen(1L);
        }

        for (Kandidatur kandidatur : wahlvorschlag.extractAllKandidaturen()) {
          if (kandidatur.isDirektkandidatur()) {
            importBericht.increaseDirektkandidaturen(1L);
          }
          if (kandidatur.isListenkandidatur()) {
            importBericht.increaseListenkandidaturen(1L);
          }
          if (kandidatur.isErsatzkandidatur()) {
            importBericht.increaseErsatzkandidaturen(1L);
          }

          if (!kandidatur.getPersonenVerknuepfungsByPerson1().isEmpty() || !kandidatur.getPersonenVerknuepfungsByPerson2().isEmpty()) {
            importBericht.increaseVerknuepfungen(1L);
          }
        }
      }

      // TODO Ist das noch sinnvoll, jetzt wo wir beim Importieren auch aktualisieren können?
      //      addUnveraenderteWahlvorschlaegeToImportBericht(importObject, context, importBericht);

      erstelleBericht(importBericht, berichtBuilder, vorab);
    }
  }

  private void erstelleBericht(WahlvorschlagDateiImportBericht importBericht, StringBuilder berichtBuilder, boolean vorab) {
    List<String> ignorierteWahlvorschlaege = importBericht.getIgnorierteWahlvorschlaegeMessages();
    if (ignorierteWahlvorschlaege.size() > 0) {
      berichtBuilder.append(ignorierteWahlvorschlaege.stream().map(wahlvorschlagMessage -> wahlvorschlagMessage + "\n").collect(Collectors.joining("", "", "\n")));
    }

    if (vorab) {
      // HINT: Benutzer informieren, dass dies nicht die tatsächlich importierte Anzahl an Entitäten ist
      berichtBuilder.append(getMessage("wahlvorschlag.import.anzahl.entitaeten.in.datei")).append('\n');
    }

    berichtBuilder.append(getMessage("wahlvorschlag.import.anzahl.direktkandidaturen", importBericht.getImportierteDirektkandidaturen())).append('\n');
    berichtBuilder.append(getMessage("wahlvorschlag.import.anzahl.listen", importBericht.getImportierteListen())).append('\n');
    berichtBuilder.append(getMessage("wahlvorschlag.import.anzahl.listenkandidaturen", importBericht.getImportierteListenkandidaturen())).append('\n');
    berichtBuilder.append(getMessage("wahlvorschlag.import.anzahl.ersatzkandidaturen", importBericht.getImportierteErsatzkandidaturen())).append('\n');
    berichtBuilder.append(getMessage("wahlvorschlag.import.anzahl.doppelkandidaturen", importBericht.getImportierteVerknuepfungen() / 2));
  }

  protected void addUnveraenderteWahlvorschlaegeToImportBericht(List<Wahlvorschlag> entitiesInDatei, WahlvorschlagImportContext context, WahlvorschlagDateiImportBericht importBericht) {
    addUnveraenderteWahlvorschlaegeToImportBericht(getSeparator(entitiesInDatei, context), importBericht);
  }

  /**
   * @param separator
   *          mit First == importierte Entitäten und Second = bestehende Datenbank-Entitäten
   */
  protected void addUnveraenderteWahlvorschlaegeToImportBericht(CollectionSeparator<Wahlvorschlag, WahlvorschlagTK> separator, WahlvorschlagDateiImportBericht importBericht) {
    Map<WahlvorschlagTK, Wahlvorschlag> wahlvorschlagMap = separator.getSecondMap();

    separator.getInBoth().stream().map(wahlvorschlagMap::get).filter(Objects::nonNull).sorted(new WahlvorschlagByGebietComparator()).forEach(wahlvorschlag -> {
      Gebiet wahlvorschlagGebiet = wahlvorschlag.getGebiet();
      importBericht.addUnveraenderterWahlvorschlag(getMessage("wahlvorschlag.import.wahlvorschlag.unveraendert", wahlvorschlag.getNameAufGebiet(),
          wahlvorschlagGebiet.getGebietsart().lokalisiere(getMessages()), wahlvorschlagGebiet.getNummer()));
    });
  }

  protected Wahl attach(Wahl wahl) {
    return wahlDAO.attach(wahl);
  }

  protected Gruppe attach(PolitischeGruppe gruppe) {
    return gruppeDAO.attach(gruppe);
  }
}
