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

import static java.util.Collections.emptyMap;
import static org.apache.commons.lang3.StringUtils.isNoneBlank;

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.Supplier;

import javax.annotation.Nonnull;
import javax.inject.Inject;

import de.ivu.elect.business.ergebnis.control.AbstractKumulierteStimmenSearch;
import de.ivu.elect.business.ergebnis.control.AbstractKumulierteStimmenSearchParameters;
import de.ivu.elect.business.ergebnis.control.StimmenPool;
import de.ivu.elect.business.ergebnis.entity.Ergebnis;
import de.ivu.elect.business.ergebnis.entity.Ergebnisart;
import de.ivu.elect.business.gebietsbaum.entity.Gebiet;
import de.ivu.elect.business.gruppe.entity.Gruppe;
import de.ivu.elect.business.gruppe.entity.GruppeKumulierteStimmen;
import de.ivu.elect.business.gruppeimgebiet.entity.GruppeImGebiet;
import de.ivu.elect.business.stimmeingabe.control.StimmeingabeGruppeStimmenErfassenLabelGeneratorFinder;
import de.ivu.elect.business.stimmen.boundary.GruppeStimmeLabelGenerator;
import de.ivu.elect.business.wahl.entity.UmgangGueltigeWerte;
import de.ivu.elect.business.wahl.entity.Wahl;
import de.ivu.elect.business.wahl.entity.WahlartErgebnisart;
import de.ivu.elect.business.wahlvorschlag.entity.Wahlvorschlag;

public abstract class AbstractGruppeStimmenSearch<SP extends AbstractKumulierteStimmenSearchParameters> extends AbstractKumulierteStimmenSearch<SP, GruppeKumulierteStimmen> {

  @Inject
  protected transient GruppeStimmeLabelGenerator gruppeStimmeLabelGenerator;

  @Inject
  protected transient StimmeingabeGruppeStimmenErfassenLabelGeneratorFinder stimmeingabeGruppeStimmenErfassenLabelGeneratorFinder;

  protected AbstractGruppeStimmenSearch() {
    super();
  }

  @Override
  protected List<GruppeKumulierteStimmen> suche(Wahl wahl, Gebiet gebietSelected, Ergebnis ergebnisSelected, SP searchParameters) {
    List<Gruppe> gruppenInWahl = gruppeExtendedDAO.findByWahl(wahl);
    List<Wahlvorschlag> wahlvorschlaegeInWahl = wahlvorschlagDAO.findByWahl(wahl);
    Set<Gebiet> erfassteGebiete = collectErfassteGebiete(ergebnisSelected);
    Gruppe gruppeGueltige = bestimmeGueltige(gruppenInWahl);

    Ergebnisart ergebnisart = ergebnisSelected.getErgebnisart();
    Ergebnisart ergebnisartFuerDifferenz = searchParameters.getErgebnisartFuerDifferenz();

    StimmenPool stimmenPool = getStimmenPool(gebietSelected, ergebnisSelected, gruppeGueltige, searchParameters, gruppenInWahl, () -> erfassteGebiete);
    StimmenPool stimmenPoolFuerDifferenz = StimmenPool.createStimmenPoolLeer();

    List<Ergebnis> ergebnisseFuerDifferenz = ergebnisDao.findByWahlAndErgebnisart(wahl, ergebnisartFuerDifferenz);

    if (!ergebnisseFuerDifferenz.isEmpty()) {
      stimmenPoolFuerDifferenz = getStimmenPool(gebietSelected, ergebnisseFuerDifferenz.get(0), gruppeGueltige, searchParameters, gruppenInWahl, () -> erfassteGebiete);
    }

    Map<Gruppe, Set<GruppeImGebiet>> gigNachGruppen = ladeGigInGebietshierachieUebergeordnetNachGruppen(gebietSelected);
    List<GruppeKumulierteStimmen> result = getGruppeKumulierteStimmenInUntergeordneterGebietshierarchie(wahl, ergebnisart, gebietSelected, stimmenPool, stimmenPoolFuerDifferenz, wahlvorschlaegeInWahl,
        gruppeGueltige, searchParameters, () -> erfassteGebiete, gigNachGruppen);

    postProcessGruppeKumulierteStimmen(result, wahl, ergebnisart, gebietSelected, stimmenPool, stimmenPoolFuerDifferenz, gigNachGruppen);

    if (searchParameters.getErgebnisSelected() != null) {
      searchParameters.getErgebnisSelected().getTransientContext().clear(); // ELECT-12102
    }

    return result;
  }

  protected Map<Gruppe, Set<GruppeImGebiet>> ladeGigInGebietshierachieUebergeordnetNachGruppen(Gebiet gebietSelected) {
    if (gebietSelected == null) {
      return emptyMap();
    }

    return (Map<Gruppe, Set<GruppeImGebiet>>) gebietSelected.getTransientContext().computeIfAbsent("GigInGebietshierachieUebergeordnetNachGruppen", x -> {
      Gebiet gebiet = gebietSelected;
      Map<Gruppe, Set<GruppeImGebiet>> result = new HashMap<>();
      while (gebiet != null) {
        addGigNachGruppe(gebiet, result);
        gebiet = gebiet.getUebergeordnetesGebiet();
      }
      return result;
    });

  }

  protected void addGigNachGruppe(Gebiet gebiet, Map<Gruppe, Set<GruppeImGebiet>> result) {
    for (GruppeImGebiet gruppeImGebiet : gruppeImGebietDAO.findByGebiet(gebiet)) {
      result.computeIfAbsent(gruppeImGebiet.getGruppe(), ignore -> new HashSet<>()).add(gruppeImGebiet);
    }
  }

  private void postProcessGruppeKumulierteStimmen(List<GruppeKumulierteStimmen> result, Wahl wahl, Ergebnisart ergebnisart, Gebiet gebiet, StimmenPool stimmenPool,
      StimmenPool stimmenPoolFuerDifferenz, Map<Gruppe, Set<GruppeImGebiet>> gigNachGruppen) {
    boolean vorperiodenStimmenVorhanden = stimmenPoolFuerDifferenz.isDirektOderListenstimmenVorhanden();

    for (GruppeKumulierteStimmen gruppeKumulierteStimmen : result) {
      gruppeKumulierteStimmen.setVorperiodenStimmenVorhanden(vorperiodenStimmenVorhanden);
    }

    postProcessGruppeKumulierteStimmzettel(result, wahl, ergebnisart, gebiet, stimmenPool, stimmenPoolFuerDifferenz, gigNachGruppen);
  }

  // ELECT-8887
  private void postProcessGruppeKumulierteStimmzettel(List<GruppeKumulierteStimmen> result, Wahl wahl, Ergebnisart ergebnisart, Gebiet gebiet, StimmenPool stimmenPool,
      StimmenPool stimmenPoolFuerDifferenz, Map<Gruppe, Set<GruppeImGebiet>> gigNachGruppen) {
    UmgangGueltigeWerte umgangGueltige = UmgangGueltigeWerte.valueOfGueltige(wahl, ergebnisart);
    UmgangGueltigeWerte umgangUngueltige = UmgangGueltigeWerte.valueOfUngueltige(wahl, ergebnisart);

    if (!Objects.equals(umgangGueltige, umgangUngueltige)) {
      String anzahlStimmzettelGueltigeKennung = gruppeStimmeLabelGenerator.formatStimmzettelAnzahlGueltigeGesamtKennung(gebiet, ergebnisart);
      String anzahlStimmzettelGueltigeBezeichnung = gruppeStimmeLabelGenerator.formatStimmzettelAnzahlGueltigeGesamtBezeichnung(gebiet, ergebnisart);

      String anzahlStimmzettelUngueltigeKennung = gruppeStimmeLabelGenerator.formatStimmzettelAnzahlUngueltigeGesamtKennung(gebiet, ergebnisart);
      String anzahlStimmzettelUngueltigeBezeichnung = gruppeStimmeLabelGenerator.formatStimmzettelAnzahlUngueltigeGesamtBezeichnung(gebiet, ergebnisart);

      long anzahlWaehlerGesamt = getStimmenKumulierer().getSummeWaehlerGesamtStimmenAufGebiet(stimmenPool, gebiet);
      long anzahlWaehlerGesamtFuerDifferenz = getStimmenKumulierer().getSummeWaehlerGesamtStimmenAufGebiet(stimmenPoolFuerDifferenz, gebiet);

      GruppeKumulierteStimmen systemGruppeKumulierteStimmenGueltige = null;
      GruppeKumulierteStimmen systemGruppeKumulierteStimmenUngueltige = null;

      for (GruppeKumulierteStimmen gruppeKumulierteStimmen : result) {
        Gruppe gruppe = gruppeKumulierteStimmen.getGruppe();

        if (isSystemGruppeGueltige(gruppe)) {
          systemGruppeKumulierteStimmenGueltige = gruppeKumulierteStimmen;
        } else if (isSystemGruppeUngueltige(gruppe)) {
          systemGruppeKumulierteStimmenUngueltige = gruppeKumulierteStimmen;
        }
      }

      if (systemGruppeKumulierteStimmenGueltige != null) {
        handleGruppeKumulierteStimmzettel(systemGruppeKumulierteStimmenGueltige, umgangGueltige, anzahlWaehlerGesamt, anzahlWaehlerGesamtFuerDifferenz);

        if (umgangGueltige.isErfassenAlsStimmzettel()) { // "Ungültige" erfasst als Stimmen -> berechne ungültige Stimmzettel aus Anzahl Wähler gesamt...
          handleGruppeKumulierteStimmzettelBerechnet(result, systemGruppeKumulierteStimmenGueltige, anzahlStimmzettelGueltigeKennung, anzahlStimmzettelGueltigeBezeichnung,
              systemGruppeKumulierteStimmenUngueltige, anzahlStimmzettelUngueltigeKennung, anzahlStimmzettelUngueltigeBezeichnung, anzahlWaehlerGesamt, anzahlWaehlerGesamtFuerDifferenz,
              gigNachGruppen);
        }
      }

      if (systemGruppeKumulierteStimmenUngueltige != null) {
        handleGruppeKumulierteStimmzettel(systemGruppeKumulierteStimmenUngueltige, umgangUngueltige, anzahlWaehlerGesamt, anzahlWaehlerGesamtFuerDifferenz);

        if (umgangUngueltige.isErfassenAlsStimmzettel()) { // "Gültige" erfasst als Stimmen -> berechne gültige Stimmzettel aus Anzahl Wähler gesamt...
          handleGruppeKumulierteStimmzettelBerechnet(result, systemGruppeKumulierteStimmenUngueltige, anzahlStimmzettelUngueltigeKennung, anzahlStimmzettelUngueltigeBezeichnung,
              systemGruppeKumulierteStimmenGueltige, anzahlStimmzettelGueltigeKennung, anzahlStimmzettelGueltigeBezeichnung, anzahlWaehlerGesamt, anzahlWaehlerGesamtFuerDifferenz, gigNachGruppen);
        }
      }
    }
  }

  private void handleGruppeKumulierteStimmzettelBerechnet(List<GruppeKumulierteStimmen> result, GruppeKumulierteStimmen systemGruppeKumulierteStimmzettel, String anzahlStimmzettelKumuliertKennung,
      String anzahlStimmzettelKumuliertBezeichnung, GruppeKumulierteStimmen systemGruppeKumulierteStimmen, String anzahlStimmzettelBerechnetKennung, String anzahlStimmzettelBerechnetBezeichnung,
      long anzahlWaehlerGesamt, long anzahlWaehlerGesamtFuerDifferenz, Map<Gruppe, Set<GruppeImGebiet>> gigNachGruppen) {
    if (isNoneBlank(anzahlStimmzettelBerechnetKennung, anzahlStimmzettelBerechnetBezeichnung) && systemGruppeKumulierteStimmen != null) {
      systemGruppeKumulierteStimmzettel.setKennung(anzahlStimmzettelKumuliertKennung);
      systemGruppeKumulierteStimmzettel.setName(anzahlStimmzettelKumuliertBezeichnung);
      systemGruppeKumulierteStimmzettel.setKurzname(anzahlStimmzettelKumuliertBezeichnung);

      long anzahlStimmzettelBerechnetDirektstimmen = Math.max(0, anzahlWaehlerGesamt - fixNullToZero(systemGruppeKumulierteStimmzettel.getAnzahlDirektstimmen()));
      long anzahlStimmzettelBerechnetListenstimmen = Math.max(0, anzahlWaehlerGesamt - fixNullToZero(systemGruppeKumulierteStimmzettel.getAnzahlListenstimmen()));

      GruppeKumulierteStimmen systemGruppeKumulierteStimmzettelBerechnet = createGruppeKumulierteStimmen(systemGruppeKumulierteStimmen.getGruppe(), systemGruppeKumulierteStimmen.getGebiet(),
          anzahlStimmzettelBerechnetDirektstimmen, anzahlStimmzettelBerechnetListenstimmen, null, null, null, gigNachGruppen);

      systemGruppeKumulierteStimmzettelBerechnet.setPosition(systemGruppeKumulierteStimmzettel.getPosition());
      systemGruppeKumulierteStimmzettelBerechnet.setKennung(anzahlStimmzettelBerechnetKennung);
      systemGruppeKumulierteStimmzettelBerechnet.setKurzname(anzahlStimmzettelBerechnetBezeichnung);
      systemGruppeKumulierteStimmzettelBerechnet.setName(anzahlStimmzettelBerechnetBezeichnung);

      handleGruppeKumulierteStimmzettel(systemGruppeKumulierteStimmzettelBerechnet, UmgangGueltigeWerte.ERFASSEN_ALS_STIMMZETTEL, anzahlWaehlerGesamt, anzahlWaehlerGesamtFuerDifferenz);

      result.add(result.indexOf(systemGruppeKumulierteStimmzettel) + 1, systemGruppeKumulierteStimmzettelBerechnet);
    }
  }

  private void handleGruppeKumulierteStimmzettel(GruppeKumulierteStimmen gruppeKumulierteStimmen, UmgangGueltigeWerte umgangGueltigeWerte, long anzahlWaehlerGesamt,
      long anzahlWaehlerGesamtFuerDifferenz) {
    if (umgangGueltigeWerte.isErfassenAlsStimmen()) {
      gruppeKumulierteStimmen.setAnzahlDirektstimmenAllerGruppen(null);
      gruppeKumulierteStimmen.setAnzahlListenstimmenAllerGruppen(null);

      gruppeKumulierteStimmen.setAnzahlDirektstimmenVorperiodeAllerGruppen(null);
      gruppeKumulierteStimmen.setAnzahlDirektstimmenVorperiodeAllerGruppenVorhanden(false);
      gruppeKumulierteStimmen.setAnzahlListenstimmenVorperiodeAllerGruppen(null);
      gruppeKumulierteStimmen.setAnzahlListenstimmenVorperiodeAllerGruppenVorhanden(false);
    } else if (umgangGueltigeWerte.isErfassenAlsStimmzettel()) {
      gruppeKumulierteStimmen.setAnzahlDirektstimmenAllerGruppen(anzahlWaehlerGesamt);
      gruppeKumulierteStimmen.setAnzahlListenstimmenAllerGruppen(anzahlWaehlerGesamt);

      boolean anzahlWaehlerGesamtFuerDifferenzVorhanden = anzahlWaehlerGesamtFuerDifferenz > 0L;

      gruppeKumulierteStimmen.setAnzahlDirektstimmenVorperiodeAllerGruppen(anzahlWaehlerGesamtFuerDifferenz);
      gruppeKumulierteStimmen.setAnzahlDirektstimmenVorperiodeAllerGruppenVorhanden(anzahlWaehlerGesamtFuerDifferenzVorhanden);
      gruppeKumulierteStimmen.setAnzahlListenstimmenVorperiodeAllerGruppen(anzahlWaehlerGesamtFuerDifferenz);
      gruppeKumulierteStimmen.setAnzahlListenstimmenVorperiodeAllerGruppenVorhanden(anzahlWaehlerGesamtFuerDifferenzVorhanden);
    }
  }

  protected abstract List<GruppeKumulierteStimmen> getGruppeKumulierteStimmenInUntergeordneterGebietshierarchie(Wahl wahl, WahlartErgebnisart ergebnisart, Gebiet gebietSelected,
      StimmenPool stimmenPool, StimmenPool stimmenPoolFuerDifferenz, List<Wahlvorschlag> wahlvorschlaegeInWahl, Gruppe gruppeGueltige, SP searchParameters, Supplier<Set<Gebiet>> erfassteGebiete,
      Map<Gruppe, Set<GruppeImGebiet>> gigNachGruppen);

  @Nonnull
  protected abstract StimmenPool getStimmenPool(Gebiet gebietSelected, Ergebnis ergebnisFuerDifferenz, Gruppe gruppeGueltige, SP searchParameters, List<Gruppe> alleGruppen,
      Supplier<Set<Gebiet>> erfassteGebiete);

  protected Map<Gruppe, Set<Wahlvorschlag>> mappeWahlvorschlaegeNachGruppe(List<Wahlvorschlag> alleWahlvorschlaege) {
    Map<Gruppe, Set<Wahlvorschlag>> result = new HashMap<>();

    for (Wahlvorschlag wahlvorschlag : alleWahlvorschlaege) {
      result.computeIfAbsent(wahlvorschlag.getGruppe(), x -> new HashSet<>()).add(wahlvorschlag);
    }

    return result;
  }

  private Gruppe bestimmeGueltige(List<Gruppe> alleGruppen) {
    for (Gruppe gruppe : alleGruppen) {
      if (isSystemGruppeGueltige(gruppe)) {
        return gruppe;
      }
    }

    throw new IllegalStateException("Gruppe 'Gültige' nicht gefunden, habe:" + alleGruppen);
  }

  protected GruppeKumulierteStimmen createGruppeKumulierteStimmen(Gruppe gruppe, Gebiet gebiet, Long anzahl, Map<Gruppe, Set<GruppeImGebiet>> gigNachGruppen) {
    return createGruppeKumulierteStimmen(gruppe, gebiet, anzahl, anzahl, null, null, null, gigNachGruppen);
  }

  protected GruppeKumulierteStimmen createGruppeKumulierteStimmen(Gruppe gruppe, Gebiet gebiet, Long anzahlDirektstimmen, Long anzahlListenstimmen,
      Long anzahlStimmzettelNurEinzelnerWahlvorschlagUnveraendert, Long anzahlStimmzettelNurEinzelnerWahlvorschlagVeraendert, Long anzahlStimmzettelNurEinzelnerWahlvorschlagPanaschiert,
      Map<Gruppe, Set<GruppeImGebiet>> gigNachGruppen) {
    return new GruppeKumulierteStimmen(getEntityAufStimmzettelDaten(), getSystemGruppeEigenschaften(), gruppe, gebiet, anzahlDirektstimmen, anzahlListenstimmen,
        anzahlStimmzettelNurEinzelnerWahlvorschlagUnveraendert, anzahlStimmzettelNurEinzelnerWahlvorschlagVeraendert, anzahlStimmzettelNurEinzelnerWahlvorschlagPanaschiert, gigNachGruppen);
  }

  protected GruppeKumulierteStimmen createGruppeKumulierteStimmen(GruppeImGebiet gruppeImGebiet, Long anzahlDirektstimmen, Long anzahlListenstimmen,
      Long anzahlStimmzettelNurEinzelnerWahlvorschlagUnveraendert, Long anzahlStimmzettelNurEinzelnerWahlvorschlagVeraendert, Long anzahlStimmzettelNurEinzelnerWahlvorschlagPanaschiert,
      Map<Gruppe, Set<GruppeImGebiet>> gigNachGruppen) {
    return new GruppeKumulierteStimmen(getEntityAufStimmzettelDaten(), getSystemGruppeEigenschaften(), gruppeImGebiet, anzahlDirektstimmen, anzahlListenstimmen,
        anzahlStimmzettelNurEinzelnerWahlvorschlagUnveraendert, anzahlStimmzettelNurEinzelnerWahlvorschlagVeraendert, anzahlStimmzettelNurEinzelnerWahlvorschlagPanaschiert, gigNachGruppen);
  }

  protected long fixNullToZero(Long value) {
    if (value == null) {
      return 0L;
    }

    return value;
  }
}
