package de.ivu.elect.was.sitzberechnung.control;

import de.ivu.elect.business.ergebnis.entity.ErgebnisLogEintrag;
import de.ivu.elect.business.ergebnis.entity.Gewaehlt;
import de.ivu.elect.business.ergebnis.entity.Sitze;
import de.ivu.elect.business.gebietsbaum.control.GebietByBreadthFirstComparator;
import de.ivu.elect.business.gruppe.boundary.GruppeDAO;
import de.ivu.elect.business.gruppe.entity.AbstractPolitischeGruppe;
import de.ivu.elect.business.gruppe.entity.Gruppe;
import de.ivu.elect.business.gruppe.entity.PolitischeGruppe;
import de.ivu.elect.business.person.boundary.PersonendatenDAO;
import de.ivu.elect.business.person.entity.Personendaten;
import de.ivu.elect.business.wahlvorschlag.boundary.WahlvorschlagDAO;
import de.ivu.elect.business.wahlvorschlag.entity.Liste;
import de.ivu.elect.business.wahlvorschlag.entity.NlListe;
import de.ivu.elect.business.wahlvorschlag.entity.Wahlvorschlag;
import de.ivu.elect.sitzberechnung.GewaehltStatus;
import de.ivu.elect.sitzberechnung.Kandidat;
import de.ivu.elect.sitzberechnung.SitzberechnungLogeintrag;
import de.ivu.elect.sitzberechnung.entity.DefaultGewaehlt;
import de.ivu.elect.was.sitzberechnung.entity.SitzberechnungGebiet;
import de.ivu.elect.was.sitzberechnung.entity.SitzberechnungGruppe;
import de.ivu.elect.was.sitzberechnung.entity.SitzberechnungMappingContext;

import javax.enterprise.inject.Specializes;
import javax.inject.Inject;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;

@Specializes
public class NlSitzberechnungsergebnisMapper extends SitzberechnungsergebnisMapper {

  @Inject
  transient PersonendatenDAO personendatenDAO;

  @Inject
  transient GruppeDAO gruppeDAO;

  @Inject
  transient WahlvorschlagDAO wahlvorschlagDAO;

  @Override
  protected List<ErgebnisLogEintrag> mappeErgebnisLogEintraege(List<SitzberechnungLogeintrag> logEintraege, SitzberechnungMappingContext sitzberechnungMappingContext) {
    List<ErgebnisLogEintrag> result = new ArrayList<>();
    if (logEintraege.isEmpty()) {
      return result;
    }

    List<Gruppe> gruppen = gruppeDAO.findByWahl(sitzberechnungMappingContext.getWahl());
    List<Wahlvorschlag> wahlvorschlaege = wahlvorschlagDAO.findByWahl(sitzberechnungMappingContext.getWahl());
    List<Personendaten> personendaten = personendatenDAO.findByWahl(sitzberechnungMappingContext.getWahl());
    Map<String, Object> stelnummerMap = createStellnummerMap(wahlvorschlaege);

    for (SitzberechnungLogeintrag eintrag : logEintraege) {
      ErgebnisLogEintrag ergebnisLogEintrag = new ErgebnisLogEintrag();
      ergebnisLogEintrag.setErgebnisLogEintragArt(mappeErgebnisLogEintragArt(eintrag.getErgebnisLogEintragArt()));
      ergebnisLogEintrag.setText(messages.getMessageForMessageKey(eintrag.getMessageKey(), konvertiereParameter(eintrag.getObjects(), gruppen, wahlvorschlaege, personendaten, stelnummerMap)));
      result.add(ergebnisLogEintrag);
    }
    return result;
  }

  private Map<String, Object> createStellnummerMap(List<Wahlvorschlag> wahlvorschlaege) {
    //noinspection OptionalGetWithoutIsPresent
    return wahlvorschlaege.stream()
            .map(w -> w.castTo(Liste.class).getEntity())
            .filter(NlListe::isListeEinesStelWas)
            .map(w -> new AbstractMap.SimpleImmutableEntry<>(NlListe.createStelKey(w.getGruppe().getPk(), NlListe.getStelNummerWas(w).get()), NlListe.getStelNummerWas(w).get()))
            .distinct()
            .collect(Collectors.toMap(AbstractMap.SimpleImmutableEntry::getKey, AbstractMap.SimpleImmutableEntry::getValue));
  }

  private Object[] konvertiereParameter(Object[] parameter, Collection<Gruppe> gruppen, Collection<Wahlvorschlag> wahlvorschlaege,
                                        Collection<Personendaten> personendaten, Map<String, Object> stelnummerMap) {
    // Params sind Strings der PKs der Entitäten (oder die Stelnummer).
    // Wenn bspw. GruppePk:123 vorkommt, wird dieser Parameter in den Kurzname der Gruppe konvertiert.
    List<Object> result = new ArrayList<>();
    for (Object param : parameter) {
      if (isParameterEineGruppe(param, gruppen)) {
        result.add(getGruppeKurzname(gruppen, param));
      } else if (isParameterEineListe(wahlvorschlaege, param)) {
        result.add(getGebietsnummerDerListe(wahlvorschlaege, param));
      } else if (isParameterEinKandidat(personendaten, param)) {
        result.add(getNameDesKandidaten(personendaten, param));
      } else {
        result.add(stelnummerMap.getOrDefault(param.toString(), param));
      }
    }

    return result.toArray();
  }

  @Override
  protected List<Sitze> mappeSitze(List<de.ivu.elect.sitzberechnung.Sitze> sitze) {
    //ELECT-10089. Nur das Sitze-Objekt des ersten Gebiets wird angenommen.

    Map<Liste, de.ivu.elect.sitzberechnung.Sitze> listeFuerSitzeMap = createListeFuerSitzeMap(sitze);
    Map<PolitischeGruppe, Collection<Liste>> listenProGruppeMap = createListenProGruppeMap(sitze);

    List<de.ivu.elect.sitzberechnung.Sitze> result = new ArrayList<>();

    for (Map.Entry<PolitischeGruppe, Collection<Liste>> gruppeListEntry : listenProGruppeMap.entrySet()) {
      Map<Integer, List<Liste>> stelMap = new HashMap<>();
      for (Liste liste : gruppeListEntry.getValue()) {
        Optional<Integer> stelNummer = NlListe.getStelNummerWas(liste);
        if (stelNummer.isPresent()) {
          stelMap.computeIfAbsent(stelNummer.get(), a -> new ArrayList<>()).add(liste);
        } else {
          result.add(listeFuerSitzeMap.get(liste));
        }
      }
      //für jedes Stel der Gruppe, nur die Sitze des ersten Gebiets hinzufügen.
      stelMap.values().forEach(e -> result.add(listeFuerSitzeMap.get(getListeFuerErstesGebiet(e))));
    }


    return super.mappeSitze(result);
  }

  private Map<PolitischeGruppe, Collection<Liste>> createListenProGruppeMap(List<de.ivu.elect.sitzberechnung.Sitze> sitze) {
    return sitze.stream()
            .map(s -> ((SitzberechnungGruppe) s.getGruppe()).getEntity())
            .distinct()
            .collect(Collectors.toMap(Function.identity(), AbstractPolitischeGruppe::getListen));
  }

  protected Map<Liste, de.ivu.elect.sitzberechnung.Sitze> createListeFuerSitzeMap(List<de.ivu.elect.sitzberechnung.Sitze> sitze) {
    Map<Liste, de.ivu.elect.sitzberechnung.Sitze> result = new HashMap<>();
    List<Liste> listen = sitze.stream()
            .map(de.ivu.elect.sitzberechnung.Sitze::getGruppe)
            .distinct()
            .flatMap(g -> ((SitzberechnungGruppe) g).getEntity().getListen().stream()).collect(Collectors.toList());

    for (Liste liste : listen) {
      sitze.stream()
              .filter(s -> ((SitzberechnungGruppe) s.getGruppe()).getEntity().equals(liste.getGruppe()) && ((SitzberechnungGebiet) s.getGebiet()).getEntity().equals(liste.getGebiet()))
              .findAny().ifPresent(entsprechendeSitze -> result.put(liste, entsprechendeSitze));
    }


    return result;
  }

  @Override
  protected List<Gewaehlt> mappeGewaehlte(List<de.ivu.elect.sitzberechnung.Gewaehlt> gewaehlte) {
    // ELECT-10081 Nur eine Kandidatur eines Kandidaten darf als "Gewählt" markiert werden.
    // Nicht-Gewählte bleiben immer nicht-Gewählt, aber wenn mehreren Gewählte für einen Kandidat vorhanden sind,
    // werden stattdessen neue, nicht Gewählte Objekte erstellt, mit Ausnahme des Gewähltes des ersten Gebiets

    List<de.ivu.elect.sitzberechnung.Gewaehlt> tatsaechlichGewaehlte = gewaehlte.stream().filter(gewaehlt -> gewaehlt.getGewaehltStatus().isGewaehlt()).collect(Collectors.toList());
    List<de.ivu.elect.sitzberechnung.Gewaehlt> tatsaechlichNichtGewaehlte = gewaehlte.stream().filter(gewaehlt -> !gewaehlt.getGewaehltStatus().isGewaehlt()).collect(Collectors.toList());

    List<de.ivu.elect.sitzberechnung.Gewaehlt> einGewaehltProKandidat = new ArrayList<>(tatsaechlichNichtGewaehlte);

    Map<Kandidat, List<de.ivu.elect.sitzberechnung.Gewaehlt>> kandidatGewaehltMap = tatsaechlichGewaehlte.stream()
            .collect(Collectors.groupingBy(g -> g.getKandidatur().getKandidat()));

    for (List<de.ivu.elect.sitzberechnung.Gewaehlt> gewaehlteFuerKandidat : kandidatGewaehltMap.values()) {
      de.ivu.elect.sitzberechnung.Gewaehlt gewaehltFuerErstesGebiet = getGewaehltFuerErstesGebiet(gewaehlteFuerKandidat);
      einGewaehltProKandidat.add(gewaehltFuerErstesGebiet);
      einGewaehltProKandidat.addAll(nichtGewaehlteInAnderenGebieten(gewaehltFuerErstesGebiet, gewaehlteFuerKandidat));
    }

    return super.mappeGewaehlte(einGewaehltProKandidat);
  }

  private Collection<DefaultGewaehlt> nichtGewaehlteInAnderenGebieten(de.ivu.elect.sitzberechnung.Gewaehlt gewaehltFuerErstesGebiet, List<de.ivu.elect.sitzberechnung.Gewaehlt> gewaehlteFuerKandidat) {
    Collection<DefaultGewaehlt> result = new ArrayList<>();

    for (de.ivu.elect.sitzberechnung.Gewaehlt gewaehlt : gewaehlteFuerKandidat) {
      if (!gewaehlt.equals(gewaehltFuerErstesGebiet)) {
        result.add(new DefaultGewaehlt(gewaehlt.getKandidatur(), GewaehltStatus.NICHT_GEWAEHLT, gewaehlt.getRang()));
      }
    }
    return result;
  }


  private de.ivu.elect.sitzberechnung.Gewaehlt getGewaehltFuerErstesGebiet(List<de.ivu.elect.sitzberechnung.Gewaehlt> gewaehlteFuerKandidat) {
    GebietByBreadthFirstComparator instance = GebietByBreadthFirstComparator.INSTANCE;
    return gewaehlteFuerKandidat.stream().min((u, v) -> instance.compare(((SitzberechnungGebiet) u.getKandidatur().getGebiet()).getEntity(),
            ((SitzberechnungGebiet) v.getKandidatur().getGebiet()).getEntity())).orElseThrow(RuntimeException::new);
  }

  private Liste getListeFuerErstesGebiet(List<Liste> sitzeFuerGruppe) {
    GebietByBreadthFirstComparator instance = GebietByBreadthFirstComparator.INSTANCE;
    return sitzeFuerGruppe.stream().min((u, v) -> instance.compare(u.getGebiet(), v.getGebiet())).orElseThrow(RuntimeException::new);
  }


  private String getNameDesKandidaten(Collection<Personendaten> personendaten, Object param) {
    return personendaten.stream().filter(p -> param.toString().contains(p.getPk().toString()))
            .findAny().map(pd -> pd.getFamilienname() + ", " + pd.getVornamen())
            .orElse(null);
  }

  private boolean isParameterEinKandidat(Collection<Personendaten> personendaten, Object param) {
    return personendaten.stream().anyMatch(p -> param.toString().contains(p.getPk().toString()));
  }

  private String getGebietsnummerDerListe(Collection<Wahlvorschlag> wahlvorschlaege, Object param) {
    return wahlvorschlaege.stream().filter(g -> param.toString().equals(g.getPk().toString())).findAny().map(l -> l.getGebiet().getNummer()).orElse(null);
  }

  private boolean isParameterEineListe(Collection<Wahlvorschlag> wahlvorschlaege, Object param) {
    return wahlvorschlaege.stream().anyMatch(l -> param.equals(l.getPk().toString()));
  }

  private String getGruppeKurzname(Collection<Gruppe> gruppen, Object param) {
    return gruppen.stream().filter(g -> param.toString().equals(g.getPk().toString())).findAny().map(Gruppe::getKurzname).orElse(null);
  }

  private boolean isParameterEineGruppe(Object param, Collection<Gruppe> gruppen) {
    return gruppen.stream().anyMatch(g -> param.equals(g.getPk().toString()));
  }
}
