
package de.ivu.elect.core.exchange.stimmen.csv.dynamic;

import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

import javax.annotation.Nullable;

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.kandidatur.entity.Listenkandidatur;
import de.ivu.elect.business.stimmeingabe.control.ListenwahlStimmeingabeKonfiguration;
import de.ivu.elect.business.stimmzettel.entity.EntityAufStimmzettelDaten;
import de.ivu.elect.business.wahl.entity.CommonWahlart;
import de.ivu.elect.business.wahl.entity.Wahl;
import de.ivu.elect.business.wahl.entity.Wahldurchfuehrung;
import de.ivu.elect.business.wahlvorschlag.entity.AbstractWahlvorschlag;
import de.ivu.elect.business.wahlvorschlag.entity.Direktkandidat;
import de.ivu.elect.business.wahlvorschlag.entity.Liste;
import de.ivu.elect.business.wahlvorschlag.entity.Wahlvorschlag;

public abstract class AbstractDynamicStimmenExportDTOFactory {

  private final Wahldurchfuehrungsgebiet wurzelgebiet;

  private final DynamicStimmenExchangeErgebnisDaten ergebnisDaten;

  private final ListenwahlStimmeingabeKonfiguration listenwahlStimmeingabeKonfiguration;

  private final List<Gruppe> gruppen;

  private final List<Liste> listen;

  private final List<Direktkandidat> direktkandidaten;

  private final List<Long> stimmzettelPositionenAllerDirektkandidaten;

  private final List<Long> stimmzettelPositionenAllerListen;

  protected AbstractDynamicStimmenExportDTOFactory(Wahldurchfuehrungsgebiet wurzelgebiet, DynamicStimmenExchangeErgebnisDaten ergebnisDaten,
      ListenwahlStimmeingabeKonfiguration listenwahlStimmeingabeKonfiguration) {
    super();

    this.wurzelgebiet = wurzelgebiet;

    this.ergebnisDaten = ergebnisDaten;

    this.listenwahlStimmeingabeKonfiguration = listenwahlStimmeingabeKonfiguration;

    this.gruppen = ergebnisDaten.getGruppenZugelassenSortiertNachPositionAufGebiet(wurzelgebiet);

    Map<Wahlvorschlag, Long> stimmzettelPositionPerWahlvorschlag = findeWahlvorschlaegeInGebietUndDarunterNachPosition(ergebnisDaten, wurzelgebiet);

    this.listen = collectListenSorted(stimmzettelPositionPerWahlvorschlag);

    if (this.listen.isEmpty()) {
      this.stimmzettelPositionenAllerListen = bestimmeStimmzettelPositionen(ergebnisDaten.getZugelasseneListen(), ergebnisDaten.getEntityAufStimmzettelDaten());
    } else {
      this.stimmzettelPositionenAllerListen = bestimmeStimmzettelPositionen(this.listen, ergebnisDaten.getEntityAufStimmzettelDaten());
    }

    this.direktkandidaten = collectWahlvorschlaegeSorted(stimmzettelPositionPerWahlvorschlag, ergebnisDaten.getZugelasseneDirektkandidatenZuGebiet(wurzelgebiet));

    if (this.direktkandidaten.isEmpty()) {
      this.stimmzettelPositionenAllerDirektkandidaten = bestimmeStimmzettelPositionen(ergebnisDaten.getZugelasseneDirektkandidaten(), ergebnisDaten.getEntityAufStimmzettelDaten());
    } else {
      this.stimmzettelPositionenAllerDirektkandidaten = bestimmeStimmzettelPositionen(direktkandidaten, ergebnisDaten.getEntityAufStimmzettelDaten());
    }

  }

  private Map<Wahlvorschlag, Long> findeWahlvorschlaegeInGebietUndDarunterNachPosition(DynamicStimmenExchangeErgebnisDaten ergebnisDaten, Gebiet wurzelGebiet) {

    Set<Wahlvorschlag> zugelasseneWahlvorschlaege = new HashSet<>();
    zugelasseneWahlvorschlaege.addAll(ergebnisDaten.getZugelasseneListen());
    zugelasseneWahlvorschlaege.addAll(ergebnisDaten.getZugelasseneDirektkandidaten());

    Map<Wahlvorschlag, Long> result = new HashMap<>();

    for (Wahlvorschlag wahlvorschlag : zugelasseneWahlvorschlaege) {
      if (wahlvorschlag.getGebiet().isInGebietshierarchieUntergeordnetesGebietVon(wurzelGebiet)) {
        Long position = wahlvorschlag.getStimmzettelPosition(ergebnisDaten.getEntityAufStimmzettelDaten()).getPosition();
        if (position != null) {
          result.put(wahlvorschlag, position);
        }
      }
    }

    return result;

  }

  private List<Long> bestimmeStimmzettelPositionen(List< ? extends Wahlvorschlag> wahlvorschlaege, EntityAufStimmzettelDaten entityAufStimmzettelDaten) {
    return wahlvorschlaege.stream().map(wahlvorschlag -> wahlvorschlag.getStimmzettelPosition(entityAufStimmzettelDaten).getPosition()).filter(Objects::nonNull).distinct().sorted()
        .collect(Collectors.toList());
  }

  private List<Liste> collectListenSorted(Map<Wahlvorschlag, Long> stimmzettelPositionPerWahlvorschlag) {
    return collectWahlvorschlaegeSorted(stimmzettelPositionPerWahlvorschlag,
        stimmzettelPositionPerWahlvorschlag.keySet().stream().filter(AbstractWahlvorschlag::isListe).map(l -> l.castTo(Liste.class).getEntity()).collect(Collectors.toList()));
  }

  private <T extends Wahlvorschlag> List<T> collectWahlvorschlaegeSorted(Map<Wahlvorschlag, Long> stimmzettelPositionPerWahlvorschlag, List<T> wahlvorschlaege) {
    return wahlvorschlaege.stream().sorted(Comparator.comparingLong(wahlvorschlag -> {
      Long stimmzettelPosition = stimmzettelPositionPerWahlvorschlag.get(wahlvorschlag);

      if (stimmzettelPosition != null) { // ELECT-10592
        return stimmzettelPosition;
      }

      return Long.MAX_VALUE;
    })).collect(Collectors.toList());
  }

  public abstract List<List<String>> createHeader();

  public abstract List<String> schreibeDaten(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet);

  protected String getGemeindeNummer(Gebiet gebiet) {
    if (gebiet == null) {
      return "";
    }

    if (gebiet.getGebietsart().getSystemGebietsebene().isGemeinde()) {
      return extractGemeindeNummerAusGemeinde(gebiet);
    }

    return getGemeindeNummer(gebiet.getUebergeordnetesGebiet());
  }

  protected String getGrossGeschriebeneWahlart(Gebiet gebiet) {
    Wahl wahl = gebiet.getWahl();

    if (wahl == null || wahl.getWahlart() == null) {
      return "";
    }

    return CommonWahlart.normalizeWahlartName(wahl.getWahlart().getKurzname());
  }

  @Nullable
  protected Date getWahlDatum(Gebiet gebiet) {
    if (gebiet == null || gebiet.getWahl() == null) {
      return null;
    }

    return gebiet.getWahl().getWahldatum();
  }

  protected Date getLetztenGueltigenEingangFuerGebiet(Wahldurchfuehrungsgebiet gebiet) {
    return ergebnisDaten.getLetztenGueltigenEingangFuerGebiet(gebiet);
  }

  protected String extractGemeindeNummerAusGemeinde(Gebiet gemeinde) {
    return gemeinde.getNummerBezirkFallsGesetzt();
  }

  protected String getWahlberechtigteNichtImWaehlerverzeichnis(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet) {
    return getAnzahlWahlberechtigte(wahldurchfuehrungsgebiet, ergebnisDaten::getWahlberechtigteNichtImWaehlerverzeichnis);
  }

  protected String getWahlberechtigteMitWahlschein(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet) {
    return getAnzahlWahlberechtigte(wahldurchfuehrungsgebiet, ergebnisDaten::getWahlberechtigteMitWahlschein);
  }

  protected String getWahlberechtigteOhneWahlschein(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet) {
    return getAnzahlWahlberechtigte(wahldurchfuehrungsgebiet, ergebnisDaten::getWahlberechtigteOhneWahlschein);
  }

  protected String getWahlberechtigteGesamt(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet) {
    return getAnzahlWahlberechtigte(wahldurchfuehrungsgebiet, ergebnisDaten::getWahlberechtigteGesamt);
  }

  protected String getAnzahlWahlberechtigte(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet, Function<Wahldurchfuehrungsgebiet, Long> anzahlWahlberechtigteFct) {
    Long value = 0L;

    if (wahldurchfuehrungsgebiet != null && /* ELECT-9532 */ !wahldurchfuehrungsgebiet.isBriefwahlgebiet()) {
      value = anzahlWahlberechtigteFct.apply(wahldurchfuehrungsgebiet);
    }

    return fixNullToEmpty(value);
  }

  protected String getStimmbezirksart(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet) {
    return wahldurchfuehrungsgebiet.getGebietsart().getBezirksart().name();
  }

  protected String getStimmbezirksnummer(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet) {
    return wahldurchfuehrungsgebiet.getNummerBezirkFallsGesetzt();
  }

  protected String getWaehlerMitWahlschein(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet) {
    return fixNullToEmpty(ergebnisDaten.getWaehlerMitWahlschein(wahldurchfuehrungsgebiet));
  }

  protected String getUrnenwaehlerMitWahlscheinInUrnenwahllokal(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet) {
    return fixNullToEmpty(ergebnisDaten.getUrnenwaehlerMitWahlscheinInUrnenwahllokal(wahldurchfuehrungsgebiet));
  }

  protected String getBriefwaehlerMitWahlscheinInUrnenwahllokal(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet) {
    return fixNullToEmpty(ergebnisDaten.getBriefwaehlerMitWahlscheinInUrnenwahllokal(wahldurchfuehrungsgebiet));
  }

  protected String getWaehlerOhneWahlschein(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet) {
    return fixNullToEmpty(ergebnisDaten.getWaehlerOhneWahlschein(wahldurchfuehrungsgebiet));
  }

  protected String getWaehlerGesamt(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet) {
    return fixNullToEmpty(ergebnisDaten.getWaehlerGesamt(wahldurchfuehrungsgebiet));
  }

  protected Wahldurchfuehrungsgebiet getWurzelgebiet() {
    return wurzelgebiet;
  }

  protected DynamicStimmenExchangeErgebnisDaten getErgebnisDaten() {
    return ergebnisDaten;
  }

  protected String getListenStimmenUnGueltig(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet) {
    return fixNullToEmpty(ergebnisDaten.getUngueltigeZweitstimmen(wahldurchfuehrungsgebiet));
  }

  protected String getListenStimmenGueltig(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet) {
    return fixNullToEmpty(ergebnisDaten.getGueltigeZweitstimmen(wahldurchfuehrungsgebiet));
  }

  protected String getListenStimmen(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet, Liste liste) {
    return fixNullToEmpty(ergebnisDaten.getZweitstimmen(liste, wahldurchfuehrungsgebiet));
  }

  protected String getListenStimmen(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet, List<Liste> listen) {
    return fixNullToEmpty(summiere(listen, liste -> ergebnisDaten.getZweitstimmen(liste, wahldurchfuehrungsgebiet), AbstractWahlvorschlag::getGruppe));
  }

  protected String getListenstimmenUnveraendert(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet, Liste liste) {
    return fixNullToEmpty(ergebnisDaten.getStimmzettelNurEinzelnerWahlvorschlagUnveraendert(liste, wahldurchfuehrungsgebiet));
  }

  protected String getListenstimmenUnveraendert(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet, List<Liste> listen) {
    return fixNullToEmpty(summiere(listen, liste -> ergebnisDaten.getStimmzettelNurEinzelnerWahlvorschlagUnveraendert(liste, wahldurchfuehrungsgebiet), AbstractWahlvorschlag::getGruppe));
  }

  protected String getListenstimmenVeraendert(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet, Liste liste) {
    return fixNullToEmpty(ergebnisDaten.getStimmzettelNurEinzelnerWahlvorschlagVeraendert(liste, wahldurchfuehrungsgebiet));
  }

  protected String getListenstimmenVeraendert(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet, List<Liste> listen) {
    return fixNullToEmpty(summiere(listen, liste -> ergebnisDaten.getStimmzettelNurEinzelnerWahlvorschlagVeraendert(liste, wahldurchfuehrungsgebiet), AbstractWahlvorschlag::getGruppe));
  }

  protected String getListenstimmenPanaschiert(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet, Liste liste) {
    return fixNullToEmpty(ergebnisDaten.getStimmzettelNurEinzelnerWahlvorschlagPanaschiert(liste, wahldurchfuehrungsgebiet));
  }

  protected String getListenstimmenPanaschiert(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet, List<Liste> listen) {
    return fixNullToEmpty(summiere(listen, liste -> ergebnisDaten.getStimmzettelNurEinzelnerWahlvorschlagPanaschiert(liste, wahldurchfuehrungsgebiet), AbstractWahlvorschlag::getGruppe));
  }

  protected String getListenkandidaturStimmen(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet, Listenkandidatur listenkandidatur) {
    return fixNullToEmpty(ergebnisDaten.getStimmenFuerListenkandidatur(listenkandidatur, wahldurchfuehrungsgebiet));
  }

  protected List<Gruppe> getGruppen() {
    return gruppen;
  }

  protected List<Liste> getListen() {
    return listen;
  }

  protected List<Listenkandidatur> getListenKandidaten(Liste liste) {
    return liste.getListenkandidaturenAufStimmzettelSortiertNachPosition(ergebnisDaten.getEntityAufStimmzettelDaten());
  }

  protected List<Direktkandidat> getDirektkandidaten() {
    return direktkandidaten;
  }

  protected List<Liste> getZugelasseneListen() {
    return ergebnisDaten.getZugelasseneListen();
  }

  public String getDirektStimmen(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet, Direktkandidat direktkandidat) {
    return fixNullToEmpty(ergebnisDaten.getErststimmen(direktkandidat.getGruppe(), wahldurchfuehrungsgebiet));
  }

  public String getDirektStimmen(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet, List<Direktkandidat> direktkandidaten) {
    return fixNullToEmpty(summiere(direktkandidaten, d -> ergebnisDaten.getErststimmen(d.getGruppe(), wahldurchfuehrungsgebiet), AbstractWahlvorschlag::getGruppe));
  }

  @Nullable
  public <E, G> Long summiere(List<E> dinge, Function<E, Long> fct, Function<E, G> groupingByFct) {

    if (dinge.isEmpty()) {
      return null;
    }

    Map<G, List<E>> groups = dinge.stream().collect(Collectors.groupingBy(groupingByFct));

    Long summe = null;
    for (List<E> value : groups.values()) {
      E ding = value.get(0);
      Long stimmen = fct.apply(ding); // ein Repräsentant // ELECT-12472
      if (stimmen != null) {
        if (summe == null) {
          summe = stimmen;
        } else {
          summe += stimmen;
        }
      }
    }
    return summe;
  }

  protected String getDirektStimmenUnGueltig(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet) {
    return fixNullToEmpty(ergebnisDaten.getUngueltigeErststimmen(wahldurchfuehrungsgebiet));
  }

  protected String getDirektStimmenGueltig(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet) {
    return fixNullToEmpty(ergebnisDaten.getGueltigeErststimmen(wahldurchfuehrungsgebiet));
  }

  protected String fixNullToEmpty(Long value) {
    if (value == null) {
      return "";
    }

    return value.toString();
  }

  protected boolean isNurDirektWahl() {
    Wahldurchfuehrung wahldurchfuehrung = getErgebnisDaten().getWahl();

    return wahldurchfuehrung.isWahlMitDirektkandidatengebieten() && !wahldurchfuehrung.isWahlMitListengebieten();
  }

  protected boolean isNurListenWahl() {
    Wahldurchfuehrung wahl = getErgebnisDaten().getWahl();

    return !wahl.isWahlMitDirektkandidatengebieten() && wahl.isWahlMitListengebieten();
  }

  protected boolean isWahlMitDirektstimmen() {
    return getErgebnisDaten().getWahl().isWahlMitDirektkandidatengebieten();
  }

  protected boolean isWahlMitListenstimmen() {
    return getErgebnisDaten().getWahl().isWahlMitListengebieten();
  }

  protected boolean isReferendum() {
    return getErgebnisDaten().getWahl().isReferendum();
  }

  protected boolean isWahlMitListenkandidatenstimmen() {
    return getErgebnisDaten().getWahl().isWahlMitListenkandidatenStimmen(); // ELECT-14617
  }

  protected boolean isWahlMitListenstimmenUnveraendert() {
    return listenwahlStimmeingabeKonfiguration.isStimmzetteleingabeGruppeStimmenNurEinzelnerWahlvorschlagUnveraendertRelevant(getErgebnisDaten().getWahlParameterLoader(), getErgebnisDaten().getWahl(),
        getErgebnisDaten().getErgebnisart());
  }

  protected boolean isWahlMitListenstimmenVeraendert() {
    return listenwahlStimmeingabeKonfiguration.isStimmzetteleingabeGruppeStimmenNurEinzelnerWahlvorschlagVeraendertRelevant(getErgebnisDaten().getWahlParameterLoader(), getErgebnisDaten().getWahl(),
        getErgebnisDaten().getErgebnisart());
  }

  protected boolean isWahlMitListenstimmenPanaschiert() {
    return listenwahlStimmeingabeKonfiguration.isStimmzetteleingabeGruppeStimmenNurEinzelnerWahlvorschlagPanaschiertRelevant(getErgebnisDaten().getWahlParameterLoader(), getErgebnisDaten().getWahl(),
        getErgebnisDaten().getErgebnisart());
  }

  public List<Long> getStimmzettelPositionenAllerDirektkandidaten() {
    return stimmzettelPositionenAllerDirektkandidaten;
  }

  public List<Long> getStimmzettelPositionenAllerListen() {
    return stimmzettelPositionenAllerListen;
  }

  protected boolean isAusbleibend(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet) {
    return wahldurchfuehrungsgebiet.getErgebniseingabestatus(getErgebnisDaten().getErgebnisart()).isAusbleibend();
  }
}
