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

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.ToLongFunction;
import java.util.stream.Collectors;

import javax.enterprise.inject.Specializes;
import javax.inject.Inject;

import de.ivu.elect.business.CurrentCustomer;
import de.ivu.elect.business.ergebnis.control.NlStimmenGewichtetKumulierer;
import de.ivu.elect.business.ergebnis.entity.Ergebnis;
import de.ivu.elect.business.ergebnis.entity.Gewaehlt;
import de.ivu.elect.business.ergebnis.entity.Stimmabgabeart;
import de.ivu.elect.business.ergebnis.entity.Stimmen;
import de.ivu.elect.business.gebietsbaum.boundary.GebietExtendedDAO;
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.internet.control.GebietUndStimmabgabeart;
import de.ivu.elect.business.kandidatur.entity.Kandidatur;
import de.ivu.elect.business.kandidatur.entity.KandidaturGewaehlt;
import de.ivu.elect.business.kandidatur.entity.Listenkandidatur;
import de.ivu.elect.business.kandidatur.entity.NlKandidaturGewaehlt;
import de.ivu.elect.business.kandidatur.entity.NlWasListenkandidatur;
import de.ivu.elect.business.person.entity.PersonendatenPK;
import de.ivu.elect.business.stimmzettel.entity.EntityAufStimmzettelDaten;
import de.ivu.elect.business.wahl.entity.Wahl;

@Specializes
public class NlKandidaturGewaehltSearch extends KandidaturGewaehltSearch {

  public static final String NL_POSITION = "NL_POSITION";

  @Inject
  private transient CurrentCustomer currentCustomer;

  @Inject
  private transient GebietExtendedDAO gebietExtendedDAO;

  @Override
  protected List<KandidaturGewaehlt> suche(Wahl wahl, Gebiet gebietSelected, Ergebnis ergebnisSelected, KandidaturGewaehltSearchParameters searchParameters) {
    /*ELECTVAPP-921 immer erstmal alle finden*/
    List<KandidaturGewaehlt> alleKandidaturenGewaehlt = super
        .suche(wahl, findeOberstesGebiet(gebietSelected) /*ELECTVAPP-921 immer erstmal alle finden*/, ergebnisSelected, searchParameters);

    alleKandidaturenGewaehlt = erzeugeNlKandidaturGewaehlte(wahl, gebietSelected, alleKandidaturenGewaehlt);

    if (!gebietSelected.hasUebergeordnetesGebiet()) {
      return sortiereUndMarkierePosition(getEineEinzigeKandidaturMitKumuliertenStimmenanzahlAllerKandidaturenWennGewaehlt(alleKandidaturenGewaehlt));
    }
    return getKandidaturenAufGebietWennUeberhauptGewaehlt(alleKandidaturenGewaehlt, gebietSelected);
  }

  private List<KandidaturGewaehlt> erzeugeNlKandidaturGewaehlte(Wahl wahl, Gebiet gebietSelected, List<KandidaturGewaehlt> kandidaturenGewaehlt) {
    List<KandidaturGewaehlt> nlKandidaturenGewaehlt = new ArrayList<>(kandidaturenGewaehlt.size());

    NlStimmenGewichtetKumulierer nlStimmenGewichtetKumulierer = new NlStimmenGewichtetKumulierer(wahl, () -> gebietExtendedDAO.findByWahl(wahl));
    GebietUndStimmabgabeart gebietUndStimmabgabeart = new GebietUndStimmabgabeart(gebietSelected.castTo(Wahldurchfuehrungsgebiet.class).getEntity(), Stimmabgabeart.GESAMT); // Stimmabgabeart wird hier nicht verwendet und ist deshalb egal

    for (KandidaturGewaehlt kandidaturGewaehlt : kandidaturenGewaehlt) {
      long summeListenstimmenGewichtet = nlStimmenGewichtetKumulierer.getSummeListenstimmenGewichtetMitVoteValues(gebietUndStimmabgabeart, (voteValueGebietUndStimmabgabeart, voteValueFct) -> wichteStimmen(kandidaturGewaehlt, voteValueGebietUndStimmabgabeart, voteValueFct));
      nlKandidaturenGewaehlt.add(new NlKandidaturGewaehlt(kandidaturGewaehlt, summeListenstimmenGewichtet));
    }

    return nlKandidaturenGewaehlt;
  }

  private long wichteStimmen(KandidaturGewaehlt kandidaturGewaehlt, GebietUndStimmabgabeart voteValueGebietUndStimmabgabeart, ToLongFunction<Wahldurchfuehrungsgebiet> voteValueFct) {
    Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet = voteValueGebietUndStimmabgabeart.getWahldurchfuehrungsgebiet();

    Long summeListenstimmen = kandidaturGewaehlt.getStimmen().stream().filter(stimmen -> kandidaturGewaehlt.getKandidatur().equals(stimmen.getListenkandidatur()) && stimmen.getGebiet().equals(wahldurchfuehrungsgebiet)).mapToLong(Stimmen::getAnzahl).sum();

    long wichtung = voteValueFct.applyAsLong(wahldurchfuehrungsgebiet);

    return wichtung * summeListenstimmen;
  }

  private List<KandidaturGewaehlt> sortiereUndMarkierePosition(List<KandidaturGewaehlt> gewaehlte) {
    if (gewaehlte.size() < 2) {
      return gewaehlte;
    }
    // ELECTVAPP-1354
    Map<Listenkandidatur, KandidaturGewaehlt> gewaehltNachListenkandidatur = gewaehlte.stream().collect(Collectors
        .toMap(kandidaturGewaehlt -> kandidaturGewaehlt.getKandidatur().castTo(Listenkandidatur.class).getEntity(), kandidaturGewaehlt -> kandidaturGewaehlt,
            (a, b) -> b));
    EntityAufStimmzettelDaten entityAufStimmzettelDaten = entityAufStimmzettelDatenExtendedDAOFactory.getEntityAufStimmzettelDaten();

    Predicate<Listenkandidatur> isUberVorzugtGewaehlt = l -> new NlWasListenkandidatur(l).isUeberVorzugGewaehlt();
    Function<Listenkandidatur, Long> numberOfVotesFct = l -> gewaehltNachListenkandidatur.get(l).getStimmenAnzahl();

    Function<Listenkandidatur, String> shortCodeFct = k -> new NlWasListenkandidatur(k)
        .getShortCode(Collections.emptyList(), Collections.emptyMap(), currentCustomer.getCustomer());

    Function<Listenkandidatur, Long> listPositionFct = l -> getStimmzettelPosition(l, entityAufStimmzettelDaten);

    Function<Listenkandidatur, Long> kieskringNumberFct = l -> new NlWasListenkandidatur(l).getKleinsteWahlkreisNummer();

    Eml520CandidateComparator eml520CandidateComparator = new Eml520CandidateComparator(isUberVorzugtGewaehlt, numberOfVotesFct, shortCodeFct, listPositionFct,
        kieskringNumberFct);

    Map<Gruppe, List<KandidaturGewaehlt>> nachGruppe = gewaehlte.stream()
        .collect(Collectors.groupingBy(kandidaturGewaehlt -> kandidaturGewaehlt.getKandidatur().getGruppe()));
    List<KandidaturGewaehlt> result = new ArrayList<>(gewaehlte.size());

    for (List<KandidaturGewaehlt> value : nachGruppe.values()) {
      value.sort((u,v)->eml520CandidateComparator.compare(u.getKandidatur().castTo(Listenkandidatur.class).getEntity(),
          v.getKandidatur().castTo(Listenkandidatur.class).getEntity()));
      long pos = 0;
      for (KandidaturGewaehlt kandidaturGewaehlt : value) {
        pos++;
        kandidaturGewaehlt.getKandidatur().getTransientContext().put(NL_POSITION, pos);
      }
      result.addAll(value);
    }

    return result;
  }

  private Long getStimmzettelPosition(Listenkandidatur k, EntityAufStimmzettelDaten entityAufStimmzettelDaten) {
    Long result = k.getStimmzettelPosition(entityAufStimmzettelDaten).getPosition();
    if (result == null) {
      return 0L;
    }
    return result;
  }

  private boolean isTrittInUntergeordnetemGebietAn(Kandidatur kandidatur, Gebiet gebietSelected) {
    return kandidatur.getGebiet().isInGebietshierarchieUntergeordnetesGebietVon(gebietSelected);
  }

  private Gebiet findeOberstesGebiet(Gebiet gebietSelected) {
    if (gebietSelected.hasUebergeordnetesGebiet()) {
      return findeOberstesGebiet(gebietSelected.getUebergeordnetesGebiet());
    }
    return gebietSelected;
  }

  protected List<KandidaturGewaehlt> getEineEinzigeKandidaturMitKumuliertenStimmenanzahlAllerKandidaturenWennGewaehlt(List<KandidaturGewaehlt> gewaehlteFuerAlleKandidaturen) {
    List<KandidaturGewaehlt> einGewaehltProKandidat = new ArrayList<>();
    List<PersonendatenPK> geseheneKandidaturen = new ArrayList<>();

    for (KandidaturGewaehlt kandidaturGewaehlt : gewaehlteFuerAlleKandidaturen) {
      if (kandidaturGewaehlt.getGewaehlt().getGewaehltStatus().isGewaehlt() && !geseheneKandidaturen.contains(kandidaturGewaehlt.getPk())) {
        Set<KandidaturGewaehlt> kandidaturenDesKandidats = mappeKandidaturGewaehlt(gewaehlteFuerAlleKandidaturen,
            getKandidaturenDesKandidats(kandidaturGewaehlt.getKandidatur()));

        einGewaehltProKandidat.add(stimmenVonKandidaturenKombinieren(kandidaturenDesKandidats, kandidaturGewaehlt.getGewaehlt()));
        updateGeseheneKandidaturen(geseheneKandidaturen, kandidaturenDesKandidats);
      }
    }
    return einGewaehltProKandidat;
  }

  protected List<KandidaturGewaehlt> getKandidaturenAufGebietWennUeberhauptGewaehlt(List<KandidaturGewaehlt> gewaehlteFuerAlleKandidaturen, Gebiet gebietSelected) {
    return gewaehlteFuerAlleKandidaturen.stream()
        .filter(g -> isTrittInUntergeordnetemGebietAn(g.getKandidatur(), gebietSelected))
        .filter(g -> isKandidatGewaehlt(gewaehlteFuerAlleKandidaturen, g.getKandidatur()))
        .collect(Collectors.toList());
  }

  private boolean isKandidatGewaehlt(List<KandidaturGewaehlt> gewaehlteFuerAlleKandidaturen, Kandidatur kandidatur) {
    return mappeKandidaturGewaehlt(gewaehlteFuerAlleKandidaturen, getKandidaturenDesKandidats(kandidatur)).stream()
        .anyMatch(kg -> kg.getGewaehlt().getGewaehltStatus().isGewaehlt());
  }

  private void updateGeseheneKandidaturen(List<PersonendatenPK> geseheneKandidaturen, Set<KandidaturGewaehlt> kandidaturenDesKandidats) {
    geseheneKandidaturen.addAll(kandidaturenDesKandidats.stream().map(KandidaturGewaehlt::getPk).collect(Collectors.toList()));
  }

  private Set<Kandidatur> getKandidaturenDesKandidats(Kandidatur kandidatur) {
    Set<Kandidatur> result = new HashSet<>(kandidatur.getIdentischVerknuepfteKandidaturen());
    result.add(kandidatur);
    return result;
  }

  private KandidaturGewaehlt stimmenVonKandidaturenKombinieren(Set<KandidaturGewaehlt> kandidaturenDesKandidats, Gewaehlt gewaehlteKandidatur) {
    if (kandidaturenDesKandidats.size() == 1) {
      return kandidaturenDesKandidats.iterator().next();
    }

    Long gesamteStimmenAnzahl = kandidaturenDesKandidats.stream().mapToLong(KandidaturGewaehlt::getStimmenAnzahl).sum();
    Stimmen gesamteStimmen = new Stimmen();
    gesamteStimmen.setAnzahl(gesamteStimmenAnzahl);
    gesamteStimmen.setListenkandidatur(gewaehlteKandidatur.getKandidatur().castTo(Listenkandidatur.class).getEntity());
    Long gesamteStimmenAnzahlGewichtet = kandidaturenDesKandidats.stream().map(NlKandidaturGewaehlt.class::cast).mapToLong(nlKandiaturGewaehlt -> Optional.ofNullable(nlKandiaturGewaehlt.getStimmenAnzahlGewichtet()).orElse(0L)).sum();

    return new NlKandidaturGewaehlt(getEntityAufStimmzettelDaten(),
        gewaehlteKandidatur.getKandidatur(),
        gewaehlteKandidatur,
        Collections.singleton(gesamteStimmen),
        gesamteStimmenAnzahlGewichtet);
  }

  private Set<KandidaturGewaehlt> mappeKandidaturGewaehlt(List<KandidaturGewaehlt> gewaehlteFuerAlleKandidaturen, Collection<Kandidatur> identischVerknuepfteKandidaturen) {
    return identischVerknuepfteKandidaturen.stream()
        .map(k -> gewaehlteFuerAlleKandidaturen.stream()
            .filter(g -> g.getKandidatur().equals(k)).findAny()
            .orElseThrow(NullPointerException::new))
        .collect(Collectors.toSet());
  }
}
