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

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Supplier;

import javax.inject.Inject;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.ivu.elect.business.ergebnis.boundary.ErgebnisDAO;
import de.ivu.elect.business.ergebnis.control.ErgebnisDaten;
import de.ivu.elect.business.ergebnis.control.ErgebnisDatenCreator;
import de.ivu.elect.business.ergebnis.control.ErgebnisDatenHolder;
import de.ivu.elect.business.gebietsbaum.boundary.WahldurchfuehrungsgebietDAO;
import de.ivu.elect.business.gebietsbaum.entity.Gebiet;
import de.ivu.elect.business.gebietsbaum.entity.Wahldurchfuehrungsgebiet;
import de.ivu.elect.business.gruppe.entity.Gruppe;
import de.ivu.elect.business.gruppeimgebiet.boundary.GruppeImGebietDAO;
import de.ivu.elect.business.gruppeimgebiet.control.GruppeImGebietWahlvorschlaegeInHierarchiePreloader;
import de.ivu.elect.business.gruppeimgebiet.entity.GruppeImGebiet;
import de.ivu.elect.business.hochrechnung.boundary.Hochrechnungsalgorithm;
import de.ivu.elect.business.hochrechnung.boundary.HochrechnungsalgorithmusKonfiguration;
import de.ivu.elect.business.hochrechnung.control.KoordinatenInformationenVonEingabedatenErzeuger;
import de.ivu.elect.business.hochrechnung.control.StimmenHochrechnerEingabedatenVonErgebnisdaten;
import de.ivu.elect.business.hochrechnung.entity.AnzahlStimmenProWaehlerNachStimmenartBestimmer;
import de.ivu.elect.business.hochrechnung.entity.EinfacheWahlAnzahlStimmenProWaehlerNachStimmenartBestimmer;
import de.ivu.elect.business.hochrechnung.entity.GebietDaten;
import de.ivu.elect.business.hochrechnung.entity.HochrechneralgorithmusAusfuehrbarkeitStatus;
import de.ivu.elect.business.hochrechnung.entity.HochrechnungsalgorithmusArt;
import de.ivu.elect.business.hochrechnung.entity.KoordinatenInformationen;
import de.ivu.elect.business.hochrechnung.entity.StimmenHochrechnerEingabedaten;
import de.ivu.elect.business.hochrechnung.entity.StimmenhochrechnerAusgabedaten;
import de.ivu.elect.business.hochrechnung.entity.StimmenhochrechnerGebietAusgabedaten;
import de.ivu.elect.business.stimmeingabe.control.StimmeingabeCreate;
import de.ivu.elect.business.stimmeingabe.control.StimmeingabeErfassenContext;
import de.ivu.elect.business.stimmeingabe.control.StimmeingabeErfassenModelBuilderResolver;
import de.ivu.elect.business.stimmeingabe.control.StimmeingabePlausiBuilder;
import de.ivu.elect.business.stimmeingabe.control.fiktive.ErgebniseingangPlausiFehlerBeheber;
import de.ivu.elect.business.stimmeingabe.control.fiktive.GruppenStimmeingabenAusFiktivwertenSetzer;
import de.ivu.elect.business.stimmeingabe.control.fiktive.ListenkandidaturStimmeingabenAusFiktivwertenSetzer;
import de.ivu.elect.business.stimmeingabe.control.fiktive.ReferendumOptionStimmeingabenAusFiktivwertenSetzer;
import de.ivu.elect.business.stimmeingabe.control.fiktive.WahlberechtigteUndWaehlerStimmeingabenAusFiktivwertenSetzer;
import de.ivu.elect.business.stimmeingabe.control.plausi.WahlvorschlaegeSupplier;
import de.ivu.elect.business.stimmeingabe.entity.Ergebniseingang;
import de.ivu.elect.business.stimmeingabe.entity.ErgebniseingangErfasser;
import de.ivu.elect.business.stimmeingabe.entity.Ergebniseingangsart;
import de.ivu.elect.business.stimmeingabe.entity.Ergebniseingangsherkunft;
import de.ivu.elect.business.stimmeingabe.entity.Stimmeingabe;
import de.ivu.elect.business.stimmeingabe.entity.StimmeingabeKopfzahlErfassenModel;
import de.ivu.elect.business.stimmeingabe.entity.StimmeingabeListenkandidaturStimmenErfassenModel;
import de.ivu.elect.business.stimmzettel.boundary.AnzahlStimmenProWaehlerBestimmer;
import de.ivu.elect.business.stimmzettel.entity.EntityAufStimmzettelDaten;
import de.ivu.elect.business.wahl.entity.Wahl;
import de.ivu.elect.business.wahlvorschlag.boundary.DirektkandidatDAO;
import de.ivu.elect.business.wahlvorschlag.boundary.ListeDAO;
import de.ivu.elect.business.wahlvorschlag.boundary.WahlvorschlagDAO;
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.jee.entity.EntityUtil;
import de.ivu.elect.jee.entity.tk.EntityTK;


import static java.util.Arrays.asList;

/**
 * cf. https://support.elect-it.com/wiki/display/ELECT/Fiktive+Stimmeingaben
 */
@javax.ejb.Stateless
public class StimmeingabeFiktivewerteCreate {

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

  @Inject
  private transient GruppeImGebietDAO gruppeImGebietDao;

  @Inject
  private transient GruppeImGebietWahlvorschlaegeInHierarchiePreloader gruppeImGebietWahlvorschlaegeInHierarchiePreloader;

  @Inject
  private transient WahlvorschlagDAO wahlvorschlagDao;

  @Inject
  private transient StimmeingabeCreate stimmeingabeCreate;

  @Inject
  private transient StimmeingabeErfassenModelBuilderResolver stimmeingabeErfassenModelBuilderResolver;

  @Inject
  private transient WahlberechtigteUndWaehlerStimmeingabenAusFiktivwertenSetzer wahlberechtigteUndWaehlerStimmeingabenAusFiktivwertenSetzer;

  @Inject
  private transient GruppenStimmeingabenAusFiktivwertenSetzer gruppenStimmeingabenAusFiktivwertenSetzer;

  @Inject
  private transient ListenkandidaturStimmeingabenAusFiktivwertenSetzer listenkandidaturStimmeingabenAusFiktivwertenSetzer;

  @Inject
  private transient ReferendumOptionStimmeingabenAusFiktivwertenSetzer referendumOptionStimmeingabenAusFiktivwertenSetzer;

  @Inject
  private transient ErgebniseingangPlausiFehlerBeheber ergebniseingangPlausiFehlerBeheber;

  @Inject
  protected transient ErgebnisDAO ergebnisDao;

  @Inject
  protected transient ErgebnisDatenCreator ergebnisDatenCreator;

  @Inject
  protected transient WahldurchfuehrungsgebietDAO wahldurchfuehrungsgebietDao;

  @Inject
  protected transient ErgebniseingangDAO ergebniseingangDao;

  @Inject
  private transient AnzahlStimmenProWaehlerBestimmer anzahlStimmenProWaehlerBestimmer;

  @Inject
  protected transient DirektkandidatDAO direktkandidatDAO;

  @Inject
  protected transient ListeDAO listeDAO;

  public StimmeingabeFiktivewerteCreate() {
    super();
  }

  public StimmenhochrechnerAusgabedaten berechneStimmenhochrechnerAusgabedatenNachDefaultKonfigurationen(Wahldurchfuehrungsgebiet gebietNotAttached, Ergebniseingangsart ergebniseingangsart) {
    Wahldurchfuehrungsgebiet gebiet = wahldurchfuehrungsgebietDao.attach(gebietNotAttached);
    if (gebiet == null) {
      return new StimmenhochrechnerAusgabedaten(
          HochrechneralgorithmusAusfuehrbarkeitStatus.NICHT_AUSFUEHRBAR_WEIL_WAHLDURCHFUEHRUNGSGEBIET_NICHT_VORHANDEN_IST);
    }
    StimmenHochrechnerEingabedaten<Long> stimmenhochrechnerEingabedaten = getStimmenhochrechnerEingabedaten(ergebniseingangsart, gebiet);
    HochrechnungsalgorithmusArt defaultMoeglicheHochrechnerAlgorithmusArt = getDefaultMoeglicheHochrechnerAlgorithmusArt(stimmenhochrechnerEingabedaten,gebiet);

    if (defaultMoeglicheHochrechnerAlgorithmusArt == null) {
      return new StimmenhochrechnerAusgabedaten(HochrechneralgorithmusAusfuehrbarkeitStatus.NICHT_AUSFUEHRBAR_WEGEN_AUSWAHL_HOCHRECHNUNGSALGORITHMUS);
    }

    HochrechneralgorithmusAusfuehrbarkeitStatus algorithmusAusfuehrbarkeitStatus = defaultMoeglicheHochrechnerAlgorithmusArt
        .getAlgorithmusAusfuehrbarkeitStatus(stimmenhochrechnerEingabedaten, Collections.singleton(gebiet));
    if (!algorithmusAusfuehrbarkeitStatus.isAlgorithmusAusfuehrbar()) {
      return new StimmenhochrechnerAusgabedaten(algorithmusAusfuehrbarkeitStatus);
    }

    LOG.info("Ermittle hochgerechnete Stimmen");
    KoordinatenInformationen koordinatenInformationen = new KoordinatenInformationenVonEingabedatenErzeuger(stimmenhochrechnerEingabedaten);
    HochrechnungsalgorithmusKonfiguration konfiguration = getGeeigneteKonfiguration(defaultMoeglicheHochrechnerAlgorithmusArt, koordinatenInformationen);

    Hochrechnungsalgorithm hochrechnungsalgorithmus = defaultMoeglicheHochrechnerAlgorithmusArt.getHochrechnungsalgorithm(konfiguration);
    if (hochrechnungsalgorithmus == null) {
      return new StimmenhochrechnerAusgabedaten(
          HochrechneralgorithmusAusfuehrbarkeitStatus.NICHT_AUSFUEHRBAR_WEGEN_AUSWAHL_HOCHRECHNUNGSALGORITHMUSKONFIGURATION);
    }

    StimmenhochrechnerAusgabedaten result = hochrechnungsalgorithmus.ermittleHochgerechneteStimmen(stimmenhochrechnerEingabedaten);
    LOG.info("Ermittle hochgerechnete Stimmen - done");
    return result;
  }

  private ErgebnisDaten getErgebnisDaten(Ergebniseingangsart ergebniseingangsart, Wahldurchfuehrungsgebiet gebietNotAttached) {
    Wahldurchfuehrungsgebiet gebiet = wahldurchfuehrungsgebietDao.attach(gebietNotAttached);
    if (gebiet == null) {
      return null;
    }
    LOG.info("Erzeuge ErgebnisDaten");
    ErgebnisDatenHolder ergebnisDatenHolder = new ErgebnisDatenHolder(ergebnisDatenCreator);
    ErgebnisDaten ergebnisDaten = ergebnisDatenHolder.getErgebnisDaten(gebiet.getWahldurchfuehrung(), ergebniseingangsart.korrespondierendeErgebnisart());
    LOG.info("Erzeuge ErgebnisDaten - done");
    return ergebnisDaten;
  }

  public void createAndBehebePlausiFehler(Wahldurchfuehrungsgebiet gebietNotAttached, Ergebniseingangsart ergebniseingangsart, ErgebniseingangErfasser erfasser,
                                          Ergebniseingang erwarteterLetzterErgebniseingang, boolean overrideErgebniseingangsart, StimmenhochrechnerAusgabedaten stimmenhochrechnerAusgabedaten) {
    createAndBehebePlausiFehler(gebietNotAttached, ergebniseingangsart, getErgebnisDaten(ergebniseingangsart, gebietNotAttached), erfasser,
        erwarteterLetzterErgebniseingang, overrideErgebniseingangsart,
        stimmenhochrechnerAusgabedaten);
  }

  public void createAndBehebePlausiFehler(Wahldurchfuehrungsgebiet gebietNotAttached, Ergebniseingangsart ergebniseingangsart, ErgebnisDaten ergebnisDaten, ErgebniseingangErfasser erfasser,
                                          Ergebniseingang erwarteterLetzterErgebniseingang, boolean overrideErgebniseingangsart, StimmenhochrechnerAusgabedaten stimmenhochrechnerAusgabedaten) {
    Wahldurchfuehrungsgebiet gebiet = wahldurchfuehrungsgebietDao.attach(gebietNotAttached);
    if (gebiet == null) {
      return;
    }

    HochrechneralgorithmusAusfuehrbarkeitStatus ausfuehrbarkeitStatus = stimmenhochrechnerAusgabedaten.getAusfuehrbarkeitStatus();
    if (!ausfuehrbarkeitStatus.isAlgorithmusAusfuehrbar()) {
      return;
    }

    Ergebniseingang ergebniseingang = createErgebniseingang(gebiet, ergebniseingangsart, erfasser, overrideErgebniseingangsart);

    createStimmeingabenUndSetzeAnzahlen(ergebniseingang, new WahlvorschlaegeSupplier(gebiet.getWahl(), wahlvorschlagDao), ergebnisDaten,
        stimmenhochrechnerAusgabedaten);

    ergebniseingangPlausiFehlerBeheber.behebePlausiFehler(ergebniseingang);

    stimmeingabeCreate.saveMitLetzterErgebniseingangInGebietPruefungMitStimmeingabePlausibilisierungKannFehlerBestaetigen(ergebniseingang,
        ergebniseingangDao.attach(erwarteterLetzterErgebniseingang),
        StimmeingabePlausiBuilder.NO_OP_BUILDER);

    // Im Zuge von ELECT-7009 notwendig
    if (ergebniseingang.isGueltigKannFehler()) {
      ergebniseingang.setGueltigKeinFehler();
    }

  }

  private HochrechnungsalgorithmusKonfiguration getGeeigneteKonfiguration(HochrechnungsalgorithmusArt hochrechnungsalgorithmusArt, KoordinatenInformationen koordinatenInformationen) {
    HochrechnungsalgorithmusKonfiguration konfiguration = hochrechnungsalgorithmusArt.getKonfiguration();
    konfiguration.adjustDefaultSelectionNachKoordinatenInformationen(koordinatenInformationen);
    return konfiguration;
  }

  private HochrechnungsalgorithmusArt getDefaultMoeglicheHochrechnerAlgorithmusArt(StimmenHochrechnerEingabedaten<Long> eingabeDaten, Wahldurchfuehrungsgebiet gebiet) {
    if (gebiet == null) {
      return null;
    }

    Set<Wahldurchfuehrungsgebiet> singletonGebieteSet = Collections.singleton(gebiet);
    List<HochrechnungsalgorithmusArt> hochrechnungsalgorithmusArtenInBevorzugteReihenfolge = HochrechnungsalgorithmusArt
        .getHochrechnungsalgorithmusArtenInBevorzugteReihenfolge();
    for (HochrechnungsalgorithmusArt hochrechnungsalgorithmusArt : hochrechnungsalgorithmusArtenInBevorzugteReihenfolge) {
      if (hochrechnungsalgorithmusArt.getAlgorithmusAusfuehrbarkeitStatus(eingabeDaten, singletonGebieteSet).isAlgorithmusAusfuehrbar()) {
        return hochrechnungsalgorithmusArt;
      }
    }
    return null;
  }

  private StimmenHochrechnerEingabedaten<Long> getStimmenhochrechnerEingabedaten(Ergebniseingangsart ergebniseingangsart, Wahldurchfuehrungsgebiet gebiet) {
    LOG.info("Erzeuge StimmenHochrechnerEingabedaten");

    LOG.info("Erzeuge ErgebnisDaten");
    ErgebnisDatenHolder ergebnisDatenHolder = new ErgebnisDatenHolder(ergebnisDatenCreator);
    ErgebnisDaten ergebnisDaten = ergebnisDatenHolder.getErgebnisDaten(gebiet.getWahldurchfuehrung(), ergebniseingangsart.korrespondierendeErgebnisart());
    LOG.info("Erzeuge ErgebnisDaten - done");

    if (ergebnisDaten == null) {
      LOG.info("Erzeuge leere StimmenHochrechnerEingabedaten, weil ErgebnisDaten == null ist.");
      return getEmptyEingabeDaten();
    }

    StimmenHochrechnerEingabedaten<Long> eingabeDaten = getStimmenhochrechnerEingabedaten(ergebnisDaten);
    LOG.info("Erzeuge StimmenHochrechnerEingabedaten - done");
    return eingabeDaten;
  }

  private StimmenHochrechnerEingabedaten<Long> getStimmenhochrechnerEingabedaten(ErgebnisDaten ergebnisDatenAktuell) {
    LOG.info("Erzeuge StimmenHochrechnerEingabedaten von ErgebnnisDaten");
    if (ergebnisDatenAktuell == null) {
      LOG.info("Erzeuge leere StimmenHochrechnerEingabedaten, weil ErgebnisDaten == null ist.");
      return getEmptyEingabeDaten();
    }
    StimmenHochrechnerEingabedaten<Long> eingabeDaten =  new StimmenHochrechnerEingabedatenVonErgebnisdaten(ergebnisDatenAktuell);
    LOG.info("Erzeuge StimmenHochrechnerEingabedaten von ErgebnnisDaten - done");
    return eingabeDaten;
  }

  public static StimmenHochrechnerEingabedaten<Long> getEmptyEingabeDaten() {
    return new StimmenHochrechnerEingabedaten<Long>() {
      @Override
      public Map<Gebiet, GebietDaten<Long>> getVorperiode() {
        return new HashMap<>();
      }

      @Override
      public Map<Gebiet, GebietDaten<Long>> getAktuelleWahl() {
        return new HashMap<>();
      }
    };
  }

  private boolean isHochgerechnungNichtVorhnaden(StimmenhochrechnerAusgabedaten stimmenhochrechnerAusgabedaten) {
    return stimmenhochrechnerAusgabedaten == null || stimmenhochrechnerAusgabedaten.getAusgabedatenProGebiet() == null || stimmenhochrechnerAusgabedaten
        .getAusgabedatenProGebiet().isEmpty();
  }

  private Ergebniseingang createErgebniseingang(Wahldurchfuehrungsgebiet gebiet, Ergebniseingangsart ergebniseingangsart, ErgebniseingangErfasser erfasser, boolean overrideErgebniseingangsart) {
    Ergebniseingang ergebniseingang = new Ergebniseingang();
    ergebniseingang.setWahldurchfuehrungsgebiet(gebiet);
    ergebniseingang.setErfasser(erfasser.getKennung());
    ergebniseingang.setErgebniseingangsherkunft(Ergebniseingangsherkunft.FIKTIVE_WERTE);
    ergebniseingang.setErgebniseingangsart(ergebniseingangsart);
    ergebniseingang.setOverrideErgebniseingangsart(overrideErgebniseingangsart); // ELECTKOM-27 und  ELECT-8174

    return ergebniseingang;
  }

  private void createStimmeingabenUndSetzeAnzahlen(Ergebniseingang ergebniseingang,
                                                   Supplier<Map<Gruppe, Collection<Wahlvorschlag>>> wahlvorschlaege, ErgebnisDaten ergebnisDaten,
                                                   StimmenhochrechnerAusgabedaten stimmenhochrechnerAusgabedaten) {

    createStimmeingabenOhneAnzahl(ergebniseingang, wahlvorschlaege, ergebnisDaten.getEntityAufStimmzettelDaten());

    ubernehmeVorlaeufigeStimmeingabenPerTKWennAktuellerErgebniseingangIstEndgueltig(ergebniseingang);

    Ergebniseingangsart ergebniseingangsart = ergebniseingang.getErgebniseingangsart();
    Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet = ergebniseingang.getWahldurchfuehrungsgebiet();

    log("Erzeuge fiktive Stimmeingaben [gebiet = {}, ergebniseingangsart = {}] - START", wahldurchfuehrungsgebiet.getNummer(), ergebniseingangsart);
    if (isHochgerechnungNichtVorhnaden(stimmenhochrechnerAusgabedaten)) {
      log("Keine hochgerechnete Stimmen sind vorhanden.");
      return;
    }
    setzeStimmeingabenAnzahlenAusHochrechnungsalgorithmus(ergebniseingang,
        stimmenhochrechnerAusgabedaten.getAusgabedatenVomGebiet(wahldurchfuehrungsgebiet));
    log("Erzeuge fiktive Stimmeingaben [gebiet = {}, ergebniseingangsart = {}] - DONE", wahldurchfuehrungsgebiet.getNummer(), ergebniseingangsart);

    if (wahlberechtigteUndWaehlerStimmeingabenAusFiktivwertenSetzer.hasAnzahlenWahlberechtigteFuerFiktiveWerte(ergebniseingang)
        && ergebniseingang.isStimmeingabeKopfzahlErfassenModelSet()) {
      assertStimmeingabeKopfzahlEntsprichtAnzahlWahlberechtigteAnGebiet(ergebniseingang, wahldurchfuehrungsgebiet, ergebniseingangsart);
    }
  }

  private void ubernehmeVorlaeufigeStimmeingabenPerTKWennAktuellerErgebniseingangIstEndgueltig(Ergebniseingang ergebniseingang) {
    if (ergebniseingang.getErgebniseingangsart().isEndgueltig()) {
      ubernehmeVorlaeufigeStimmeingabenPerTKWennErgebniseingangIstGueltig(ergebniseingang);
    }
  }

  private void ubernehmeVorlaeufigeStimmeingabenPerTKWennErgebniseingangIstGueltig(Ergebniseingang ergebniseingangAktuell) {
    Ergebniseingang vorlaeufigerErgebniseingang = ergebniseingangAktuell.getWahldurchfuehrungsgebiet()
        .getLetzterErgebniseingangAktuellInErgebnisUebernehmbar(ergebniseingangDao,
            Ergebniseingangsart.VORLAEUFIG);

    if (vorlaeufigerErgebniseingang == null || !vorlaeufigerErgebniseingang.isGueltig()) {
      return;
    }

    log("Fiktive Stimmeingaben vom letzten vorlaeufigen Ergebniseingang uebernehmen - START");
    Map<EntityTK, Stimmeingabe> stimmeingabeVorlaeufigPerTK = EntityUtil.mappeNachKonkretestemTk(vorlaeufigerErgebniseingang.getStimmeingaben());
    Map<EntityTK, Stimmeingabe> stimmeingabeAktuellPerTK = EntityUtil.mappeNachKonkretestemTk(ergebniseingangAktuell.getStimmeingaben());
    stimmeingabeAktuellPerTK.forEach(((entityTK, zuFuellendenStimmeingabe) -> {
      fillStimmeingabeWennStimmenanzahlVorhanden(stimmeingabeVorlaeufigPerTK.get(entityTK), zuFuellendenStimmeingabe);
    }));
    log("Fiktive Stimmeingaben vom letzten vorlaeufigen Ergebniseingang uebernehmen - DONE");

  }

  private void fillStimmeingabeWennStimmenanzahlVorhanden(Stimmeingabe vorlaeufigeStimmeingabe, Stimmeingabe zuFuellendenStimmeingabe) {
    if (vorlaeufigeStimmeingabe == null || zuFuellendenStimmeingabe == null) {
      return;
    }
    if (hasStimmeingabeEchteStimmenanzahl(vorlaeufigeStimmeingabe)) {
      zuFuellendenStimmeingabe.setAnzahl(vorlaeufigeStimmeingabe.getAnzahl());
    }
  }

  private boolean hasStimmeingabeEchteStimmenanzahl(Stimmeingabe stimmeingabe) {
    return stimmeingabe.hasAnzahlGroesserGleichNull();
  }

  private void setzeStimmeingabenAnzahlenAusHochrechnungsalgorithmus(Ergebniseingang ergebniseingang, StimmenhochrechnerGebietAusgabedaten hochrechnerAusgabedaten) {
    if (hochrechnerAusgabedaten == null) {
      return;
    }

    AnzahlStimmenProWaehlerNachStimmenartBestimmer anzahlStimmenProWaehlerNachStimmenartBestimmer = getAnzahlStimmenProWaehlerNachStimmenartBestimmer();

    wahlberechtigteUndWaehlerStimmeingabenAusFiktivwertenSetzer
        .setzeErgebniseingangStimmeingabenAnzahlAusHochgerechnetenWerten(ergebniseingang, hochrechnerAusgabedaten,
            anzahlStimmenProWaehlerNachStimmenartBestimmer);

    if (isGruppenStimmeingabenErfassen(ergebniseingang)) {
      gruppenStimmeingabenAusFiktivwertenSetzer.setzeErgebniseingangStimmeingabenAnzahlAusHochgerechnetenWerten(ergebniseingang, hochrechnerAusgabedaten,
          anzahlStimmenProWaehlerNachStimmenartBestimmer);
    }

    if (isReferendumOptionStimmeingabenErfassen(ergebniseingang)) {
      referendumOptionStimmeingabenAusFiktivwertenSetzer.setzeErgebniseingangStimmeingabenAnzahlAusHochgerechnetenWerten(ergebniseingang, hochrechnerAusgabedaten,
          anzahlStimmenProWaehlerNachStimmenartBestimmer);
    }

    if (isListenkandidatenStimmeingabenErfassen(ergebniseingang)) {
      listenkandidaturStimmeingabenAusFiktivwertenSetzer
          .setzeErgebniseingangStimmeingabenAnzahlAusHochgerechnetenWerten(ergebniseingang, hochrechnerAusgabedaten,
              anzahlStimmenProWaehlerNachStimmenartBestimmer);
    }
  }

  private AnzahlStimmenProWaehlerNachStimmenartBestimmer getAnzahlStimmenProWaehlerNachStimmenartBestimmer() {
    EinfacheWahlAnzahlStimmenProWaehlerNachStimmenartBestimmer einfacheWahlAnzahlStimmenProWaehlerNachStimmenartBestimmer = new EinfacheWahlAnzahlStimmenProWaehlerNachStimmenartBestimmer(
        anzahlStimmenProWaehlerBestimmer, getWahlvorschlaegeInDirektwahlgebietFct(), getWahlvorschlaegeInListenwahlgebietFct());
    return einfacheWahlAnzahlStimmenProWaehlerNachStimmenartBestimmer;
  }

  public Function<Gebiet, Collection<Direktkandidat>> getWahlvorschlaegeInDirektwahlgebietFct() {
    if (direktkandidatDAO == null) {
      return gebiet -> new ArrayList<>();
    }
    return direktkandidatDAO::findByGebiet;
  }

  public Function<Gebiet, Collection<Liste>> getWahlvorschlaegeInListenwahlgebietFct() {
    if (listeDAO == null) {
      return gebiet -> new ArrayList<>();
    }
    return listeDAO::findByGebiet;
  }

  private void assertStimmeingabeKopfzahlEntsprichtAnzahlWahlberechtigteAnGebiet(Ergebniseingang ergebniseingang, Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet,
                                                                                 Ergebniseingangsart ergebniseingangsart) {
    StimmeingabeKopfzahlErfassenModel stimmeingabeKopfzahlErfassenModel = ergebniseingang.getStimmeingabeKopfzahlErfassenModel();

    asList( //
        stimmeingabeKopfzahlErfassenModel.getWahlberechtigteOhneWahlschein(), //
        stimmeingabeKopfzahlErfassenModel.getWahlberechtigteMitWahlschein(), //
        stimmeingabeKopfzahlErfassenModel.getWahlberechtigteNichtImWaehlerverzeichnis(), //
        stimmeingabeKopfzahlErfassenModel.getWahlberechtigteGesamt() //
    ).stream() //
        .filter(Objects::nonNull) //
        .filter(stimmeingabeWahlberechtigte -> !Objects
            .equals(stimmeingabeWahlberechtigte.getAnzahlEingabe(), stimmeingabeWahlberechtigte.getAnzahlEingabeExpected())) //
        .forEach(
            stimmeingabeWahlberechtigte -> LOG.error(
                "FAILED to find UNMODIFIED StimmeingabeKopfzahl [gebiet = {}, ergebniseingangsart = {}, gruppe = {}, stimmenart = {}, anzahl = {}, expected = {}]",
                wahldurchfuehrungsgebiet.getNummer(), ergebniseingangsart, stimmeingabeWahlberechtigte.getGruppeImGebiet(),
                stimmeingabeWahlberechtigte.getStimmenart(),
                stimmeingabeWahlberechtigte.getAnzahlEingabe(), stimmeingabeWahlberechtigte.getAnzahlEingabeExpected()));
  }

  protected boolean isUebernahmeAusVorperiodenDatenMoeglich(Wahl wahl) {
    return true;
  }

  private Collection<Stimmeingabe> createStimmeingabenOhneAnzahl(Ergebniseingang ergebniseingang, Supplier<Map<Gruppe, Collection<Wahlvorschlag>>> wahlvorschlaege,
                                                                 EntityAufStimmzettelDaten entityAufStimmzettelDaten) {
    Collection<Stimmeingabe> result = new ArrayList<>();
    Collection<GruppeImGebiet> gruppenImGebiet = findGruppenImGebiet(ergebniseingang);
    StimmeingabeErfassenContext context = createStimmeingabeErfassenContext(ergebniseingang, gruppenImGebiet, wahlvorschlaege, entityAufStimmzettelDaten);

    result.addAll(getStimmeingabeKopfzahlErfassenModelBuilder(ergebniseingang).createKopfzahlErfassenModel(context).getAllStimmeingaben());

    if (hasStimmeingabeGruppeStimmenErfassenModelBuilder(ergebniseingang)) {
      result.addAll(getStimmeingabeGruppeStimmenErfassenModelBuilder(ergebniseingang).createGruppeStimmenErfassenModel(context).getAllStimmeingaben());
    }

    if (hasStimmeingabeReferendumOptionStimmenErfassenModelBuilder(ergebniseingang)) {
      result.addAll(getStimmeingabeReferendumOptionStimmenErfassenModelBuilder(ergebniseingang).createReferendumVorlageStimmenErfassenModel(context)
          .getAllStimmeingaben());
    }

    if (isListenkandidatenStimmeingabenErfassen(ergebniseingang)) {
      List<StimmeingabeListenkandidaturStimmenErfassenModel> models = getStimmeingabeListenkandidaturStimmenErfassenModelBuilder(ergebniseingang)
          .createListenkandidaturStimmenErfassenModelForAllApplicableGruppeImGebiet(context);

      for (StimmeingabeListenkandidaturStimmenErfassenModel model : models) {
        result.addAll(model.getAllStimmeingaben());
      }
    }

    return result;
  }

  protected boolean isListenkandidatenStimmeingabenErfassen(Ergebniseingang ergebniseingang) {
    return ergebniseingang.getWahldurchfuehrungsgebiet().getWahldurchfuehrung().isWahlMitListenkandidatenStimmen();
  }

  private boolean isReferendumOptionStimmeingabenErfassen(Ergebniseingang ergebniseingang) {
    return ergebniseingang.getWahldurchfuehrungsgebiet().getWahl().isReferendum();
  }

  private boolean isGruppenStimmeingabenErfassen(Ergebniseingang ergebniseingang) {
    return ergebniseingang.getWahldurchfuehrungsgebiet().getWahl().isWahlMitListengebieten() || ergebniseingang.getWahldurchfuehrungsgebiet().getWahl()
        .isWahlMitDirektkandidatengebieten();
  }

  private Collection<GruppeImGebiet> findGruppenImGebiet(Ergebniseingang ergebniseingang) {
    List<GruppeImGebiet> gruppenImGebiet = gruppeImGebietDao.findByGebiet(ergebniseingang.getWahldurchfuehrungsgebiet());

    gruppeImGebietWahlvorschlaegeInHierarchiePreloader
        .preloadGruppeImGebietWahlvorschlaegeInHierarchie(gruppenImGebiet, ergebniseingang.getWahldurchfuehrungsgebiet(), wahlvorschlagDao);

    return gruppenImGebiet;
  }

  private StimmeingabeListenkandidaturStimmenErfassenModelBuilder getStimmeingabeListenkandidaturStimmenErfassenModelBuilder(Ergebniseingang ergebniseingang) {
    return stimmeingabeErfassenModelBuilderResolver
        .getStimmeingabeListenkandidaturStimmenErfassenModelBuilder(ergebniseingang.getWahldurchfuehrungsgebiet().getWahl());
  }

  private StimmeingabeKopfzahlErfassenModelBuilder getStimmeingabeKopfzahlErfassenModelBuilder(Ergebniseingang ergebniseingang) {
    return stimmeingabeErfassenModelBuilderResolver.getStimmeingabeKopfzahlErfassenModelBuilder(ergebniseingang.getWahldurchfuehrungsgebiet().getWahl());
  }

  private StimmeingabeGruppeStimmenErfassenModelBuilder getStimmeingabeGruppeStimmenErfassenModelBuilder(Ergebniseingang ergebniseingang) {
    return stimmeingabeErfassenModelBuilderResolver.getStimmeingabeGruppeStimmenErfassenModelBuilder(ergebniseingang.getWahldurchfuehrungsgebiet().getWahl());
  }

  private boolean hasStimmeingabeGruppeStimmenErfassenModelBuilder(Ergebniseingang ergebniseingang) {
    return stimmeingabeErfassenModelBuilderResolver.hasStimmeingabeGruppeStimmenErfassenModelBuilder(ergebniseingang.getWahldurchfuehrungsgebiet().getWahl());
  }

  private StimmeingabeReferendumOptionStimmenErfassenModelBuilder getStimmeingabeReferendumOptionStimmenErfassenModelBuilder(Ergebniseingang ergebniseingang) {
    return stimmeingabeErfassenModelBuilderResolver
        .getStimmeingabeReferendumOptionStimmenErfassenModelBuilder(ergebniseingang.getWahldurchfuehrungsgebiet().getWahl());
  }

  private boolean hasStimmeingabeReferendumOptionStimmenErfassenModelBuilder(Ergebniseingang ergebniseingang) {
    return stimmeingabeErfassenModelBuilderResolver
        .hasStimmeingabeReferendumOptionStimmenErfassenModelBuilder(ergebniseingang.getWahldurchfuehrungsgebiet().getWahl());
  }

  private StimmeingabeErfassenContext createStimmeingabeErfassenContext(Ergebniseingang ergebniseingang, Collection<GruppeImGebiet> gruppenImGebiet,
                                                                        Supplier<Map<Gruppe, Collection<Wahlvorschlag>>> wahlvorschlaege, EntityAufStimmzettelDaten entityAufStimmzettelDaten) {
    return new DefaultStimmeingabeErfassenContext(ergebniseingang, gruppenImGebiet, wahlvorschlaege, entityAufStimmzettelDaten,
        wahldurchfuehrungsgebietDao::findByErfasstFuerGebiete);
  }

  private static void log(String message, Object... params) {
    LOG.debug(message, params);
  }
}
