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

import static de.ivu.elect.jee.entity.EntityUtil.castTo;
import static java.util.Collections.singleton;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.toList;

import java.util.ArrayList;
import java.util.Collection;
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 javax.annotation.Nullable;

import de.ivu.elect.business.gebietsbaum.entity.Gebiet;
import de.ivu.elect.business.gebietsbaum.entity.Wahldurchfuehrungsgebiet;
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;

  protected final DynamicStimmenExchangeErgebnisDaten ergebnisDaten;

  private final ListenwahlStimmeingabeKonfiguration listenwahlStimmeingabeKonfiguration;

  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;

    var 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 (var wahlvorschlag : zugelasseneWahlvorschlaege) {
      if (wahlvorschlag.getGebiet().isInGebietshierarchieUntergeordnetesGebietVon(wurzelGebiet)) {
        var 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(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(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(toList());
  }

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

  public List<List<String>> createValues(List<Gebiet> erfassungsgebiete) {
    List<List<String>> zeilen = new ArrayList<>();

    for (Gebiet gebiet : erfassungsgebiete) {
      zeilen.add(schreibeDaten(castTo(gebiet, Wahldurchfuehrungsgebiet.class)));
    }

    return zeilen;
  }

  protected 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> anzahlFct) {
    return fixNullToEmpty(sumAnzahlWahlberechtigte(singleton(wahldurchfuehrungsgebiet), anzahlFct));
  }

  protected long sumAnzahlWahlberechtigte(Collection<Wahldurchfuehrungsgebiet> wahldurchfuehrungsgebiete, Function<Wahldurchfuehrungsgebiet, Long> anzahlFct) {
    long result = 0L;

    for (Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet : wahldurchfuehrungsgebiete) {
      if (wahldurchfuehrungsgebiet != null && /* ELECT-9532 */ !wahldurchfuehrungsgebiet.isBriefwahlgebiet()) {
        Long value = anzahlFct.apply(wahldurchfuehrungsgebiet);

        if (value != null) {
          result += value;
        }
      }
    }

    return result;
  }

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

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

  protected String getWaehlerOhneWahlschein(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet) {
    return getAnzahlWaehler(wahldurchfuehrungsgebiet, ergebnisDaten::getWaehlerOhneWahlschein);
  }

  protected String getWaehlerMitWahlschein(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet) {
    return getAnzahlWaehler(wahldurchfuehrungsgebiet, ergebnisDaten::getWaehlerMitWahlschein);
  }

  protected String getUrnenwaehlerMitWahlscheinInUrnenwahllokal(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet) {
    return getAnzahlWaehler(wahldurchfuehrungsgebiet, ergebnisDaten::getUrnenwaehlerMitWahlscheinInUrnenwahllokal);
  }

  protected String getBriefwaehlerMitWahlscheinInUrnenwahllokal(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet) {
    return getAnzahlWaehler(wahldurchfuehrungsgebiet, ergebnisDaten::getBriefwaehlerMitWahlscheinInUrnenwahllokal);
  }

  protected String getWaehlerGesamt(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet) {
    return getAnzahlWaehler(wahldurchfuehrungsgebiet, ergebnisDaten::getWaehlerGesamt);
  }

  protected String getAnzahlWaehler(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet, Function<Wahldurchfuehrungsgebiet, Long> anzahlFct) {
    return fixNullToEmpty(sumAnzahlWaehler(singleton(wahldurchfuehrungsgebiet), anzahlFct));
  }

  protected long sumAnzahlWaehler(Collection<Wahldurchfuehrungsgebiet> wahldurchfuehrungsgebiete, Function<Wahldurchfuehrungsgebiet, Long> anzahlFct) {
    return sumAnzahl(wahldurchfuehrungsgebiete, anzahlFct);
  }

  protected String getWahlbriefeZurueckgewiesen(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet) {
    return getAnzahlWahlbriefe(wahldurchfuehrungsgebiet, ergebnisDaten::getWahlbriefeZurueckgewiesen);
  }

  protected String getAnzahlWahlbriefe(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet, Function<Wahldurchfuehrungsgebiet, Long> anzahlFct) {
    return fixNullToEmpty(sumAnzahlWahlbriefe(singleton(wahldurchfuehrungsgebiet), anzahlFct));
  }

  protected long sumAnzahlWahlbriefe(Collection<Wahldurchfuehrungsgebiet> wahldurchfuehrungsgebiete, Function<Wahldurchfuehrungsgebiet, Long> anzahlFct) {
    return sumAnzahl(wahldurchfuehrungsgebiete, anzahlFct);
  }

  protected Wahldurchfuehrungsgebiet getWurzelgebiet() {
    return wurzelgebiet;
  }

  protected DynamicStimmenExchangeErgebnisDaten getErgebnisDaten() {
    return ergebnisDaten;
  }

  protected EntityAufStimmzettelDaten getEntityAufStimmzettelDaten() {
    return getErgebnisDaten().getEntityAufStimmzettelDaten();
  }

  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<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(Wahldurchfuehrungsgebiet wurzelgebiet) {
    var zugelasseneListen = new ArrayList<>(ergebnisDaten.getZugelasseneListen());
    if (wurzelgebiet == null) {
      return zugelasseneListen;
    }

    // ELECTKOM-2786
    zugelasseneListen.removeIf(l -> !l.getGebiet().isInGebietshierarchieUntergeordnetesGebietVon(wurzelgebiet));

    return zugelasseneListen;
  }

  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(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();
  }

  protected long sumAnzahl(Collection<Wahldurchfuehrungsgebiet> wahldurchfuehrungsgebiete, Function<Wahldurchfuehrungsgebiet, Long> anzahlFct) {
    long result = 0L;

    for (Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet : wahldurchfuehrungsgebiete) {
      if (wahldurchfuehrungsgebiet != null) {
        Long value = anzahlFct.apply(wahldurchfuehrungsgebiet);

        if (value != null) {
          result += value;
        }
      }
    }

    return result;
  }
}
