
package de.ivu.elect.dokumente.ergebnis;

import static java.util.stream.Collectors.toCollection;
import static java.util.stream.Collectors.toList;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.SortedMap;
import java.util.function.Predicate;
import java.util.function.ToLongBiFunction;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.ivu.document.generator.template.BaseDocument;
import de.ivu.document.generator.template.annotations.DocumentComplexField;
import de.ivu.document.generator.template.annotations.DocumentQueue;
import de.ivu.document.generator.template.annotations.DocumentRepeat;
import de.ivu.document.generator.template.annotations.DocumentSimpleField;
import de.ivu.elect.business.ergebnis.control.NlErgebnisDaten;
import de.ivu.elect.business.ergebnis.entity.AbstractGewaehlt;
import de.ivu.elect.business.ergebnis.entity.Gewaehlt;
import de.ivu.elect.business.ergebnis.entity.GewaehltStatus;
import de.ivu.elect.business.ergebnis.entity.Sitze;
import de.ivu.elect.business.ergebnis.entity.Stimmabgabeart;
import de.ivu.elect.business.gebietsbaum.entity.Wahldurchfuehrungsgebiet;
import de.ivu.elect.business.kandidatur.control.StimmzettelFormatter;
import de.ivu.elect.business.kandidatur.entity.Listenkandidatur;
import de.ivu.elect.business.person.entity.PersonendatenPK;
import de.ivu.elect.business.wahlvorschlag.entity.Liste;
import de.ivu.elect.dokumente.wahlvorschlag.GruppeDokument;
import de.ivu.elect.jee.entity.customdata.EmptyCustomData;

public abstract class AbstractNlKandidatenStimmenProListeDocument extends BaseDocument {

  private static final long serialVersionUID = 1L;

  private static final Logger LOG = LoggerFactory.getLogger(AbstractNlKandidatenStimmenProListeDocument.class);

  @DocumentComplexField("Gruppe")
  public final GruppeDokument gruppe;

  // Die Nummer des Stels wird anstelle der Gebietsnummer angezeigt.
  @DocumentSimpleField("Stelnummer")
  public final Integer stelnummer;

  // Gebietsnummer wird *nur für selbständige Listen* ganz oben, vor der Kandidatenstimmen Tabelle, in einer Zusammenfassung der Gruppe/Liste angezeigt.
  @DocumentSimpleField("Gebietsnummer")
  public final String gebietsnummer;

  @DocumentSimpleField("Gebietsname")
  public final String gebietsname;

  @DocumentSimpleField("IsEinzelneListe")
  public final Boolean isEinzelneListe;

  @DocumentSimpleField("IsStelOderGruppeIdentischerListe")
  public final Boolean isStelOderGruppeIdentischerListe;

  @DocumentSimpleField("AnzahlStimmenDerListe")
  public final Long anzahlStimmenDerListe;

  @DocumentSimpleField("AnzahlStimmenDerListeGewichtet")
  public final Long anzahlStimmenDerListeGewichtetMitVoteValues;

  @DocumentSimpleField("AnzahlSitzeDerListe")
  public final Long anzahlSitzeDerListe;

  @DocumentSimpleField("AnzahlBeteiligteWahlkreise")
  public final Long anzahlBeteiligtenWahlbezirke;

  @DocumentSimpleField("IsMitGeaenderterReihenfolge")
  public final Boolean isMitGeaenderterReihenfolge;

  @DocumentSimpleField("IsMitNichtUeberVorzugGewaehlte")
  public final Boolean isMitNichtUeberVorzugGewaehlte;

  @DocumentSimpleField("IsIdentischerListe")
  public final Boolean isIdentischerListe;

  @DocumentQueue(value = "gebietsnummerKopfzeile", elementClass = String.class)
  public final LinkedList<String> gebietsnummerKopfzeile;

  @DocumentQueue(value = "gebietsnameKopfzeile", elementClass = String.class)
  public final LinkedList<String> gebietsnameKopfzeile; // ELECT-13547

  @DocumentQueue(value = "gebietsnummerKopfzeile2", elementClass = String.class)
  public final LinkedList<String> gebietsnummerKopfzeile2; // ELECT-11985

  @DocumentQueue(value = "gebietsnameKopfzeile2", elementClass = String.class)
  public final LinkedList<String> gebietsnameKopfzeile2; // ELECT-13547

  @DocumentRepeat(value = "KandidatStimmenNachStimmzettelPosition", elementClass = NlKandidatStimmenDocument.class, itemName = "kandidat")
  public final List<NlKandidatStimmenDocument> kandidatenStimmenzahl;

  @DocumentQueue(value = "GesamtStimmenzahlProGebietFuerGruppe", elementClass = String.class)
  public final LinkedList<Long> gesamtStimmenzahlProGebiet;

  @DocumentQueue(value = "GesamtStimmenzahlProGebietFuerGruppeGewichtet", elementClass = String.class)
  public final LinkedList<Long> gesamtStimmenzahlProGebietGewichtetMitVoteValues;

  @DocumentRepeat(value = "KandidatStimmenNachRang", elementClass = NlKandidatStimmenDocument.class, itemName = "kandidat")
  public final List<NlKandidatStimmenDocument> kandidatenStimmenzahlNachRang;

  protected AbstractNlKandidatenStimmenProListeDocument(GruppeDokument gruppe, LinkedList<String> gebietsnummerKopfzeile, LinkedList<String> gebietsnameKopfzeile,
      List<NlKandidatStimmenDocument> kandidatenStimmenzahl, Long anzahlSitzeDerListe, Long anzahlStimmenDerListe, Long anzahlStimmenDerListeGewichtetMitVoteValues, Integer stelnummer,
      String gebietsnummer, String gebietsname, Boolean mitGeaenderterReihenfolge, Boolean isIdentischerListe) {
    super();

    this.gruppe = gruppe;

    this.isMitGeaenderterReihenfolge = mitGeaenderterReihenfolge;

    this.gebietsnummerKopfzeile = new LinkedList<>(gebietsnummerKopfzeile);
    this.gebietsnameKopfzeile = new LinkedList<>(gebietsnameKopfzeile);
    this.gebietsnummerKopfzeile2 = new LinkedList<>(gebietsnummerKopfzeile);
    this.gebietsnameKopfzeile2 = new LinkedList<>(gebietsnameKopfzeile);

    this.kandidatenStimmenzahl = kandidatenStimmenzahl;

    this.anzahlSitzeDerListe = anzahlSitzeDerListe;

    this.anzahlStimmenDerListe = anzahlStimmenDerListe;
    this.anzahlStimmenDerListeGewichtetMitVoteValues = anzahlStimmenDerListeGewichtetMitVoteValues;

    this.stelnummer = stelnummer;

    this.gebietsnummer = gebietsnummer;
    this.gebietsname = gebietsname;

    long anzahlGebiete = gebietsnummerKopfzeile.stream().filter(StringUtils::isNotBlank).count();

    this.isIdentischerListe = isIdentischerListe;
    this.isEinzelneListe = anzahlGebiete == 0;
    this.isStelOderGruppeIdentischerListe = !this.isEinzelneListe;

    if (anzahlGebiete == 0) {
      this.anzahlBeteiligtenWahlbezirke = 1L;
    } else {
      this.anzahlBeteiligtenWahlbezirke = anzahlGebiete;
    }

    if (this.isEinzelneListe) {
      this.gesamtStimmenzahlProGebiet = NlKandidatStimmenDocument.createEmptyAnzahlenStimmen(gebietsnummerKopfzeile);
      this.gesamtStimmenzahlProGebietGewichtetMitVoteValues = this.gesamtStimmenzahlProGebiet;
    } else {
      this.gesamtStimmenzahlProGebiet = erzeugeGesamtStimmenzahlProGebiet(anzahlGebiete, kandidatenStimmenzahl, (nlKandidatStimmenDocument, index) -> nlKandidatStimmenDocument.kandidatStimmenProGebiet.get(index));
      this.gesamtStimmenzahlProGebietGewichtetMitVoteValues = erzeugeGesamtStimmenzahlProGebiet(anzahlGebiete, kandidatenStimmenzahl, (nlKandidatStimmenDocument, index) -> nlKandidatStimmenDocument.kandidatStimmenProGebietGewichtetMitVoteValues.get(index));
    }

    this.kandidatenStimmenzahlNachRang = kandidatenStimmenzahl.stream().filter(c -> c.getListenkandidatur().isZugelassen()).sorted(Comparator.comparingLong(k -> k.rang)).collect(toList());

    this.isMitNichtUeberVorzugGewaehlte = kandidatenStimmenzahl.stream().anyMatch(d -> d.isEndgueltigGewaehlt && !d.isUeberVorzugGewaehlt);
  }

  private LinkedList<Long> erzeugeGesamtStimmenzahlProGebiet(long anzahlGebiete, List<NlKandidatStimmenDocument> kandidatenStimmenzahl, ToLongBiFunction<NlKandidatStimmenDocument, Integer> anzahlStimmenByIndexFct) {
    LinkedList<Long> result = new LinkedList<>();

    for (int index = 0; index < anzahlGebiete; index++) {
      long stimmenzahlFuerGebiet = 0;

      for (NlKandidatStimmenDocument nlKandidatStimmenDocument : kandidatenStimmenzahl) {
        stimmenzahlFuerGebiet += anzahlStimmenByIndexFct.applyAsLong(nlKandidatStimmenDocument, index);
      }

      result.add(stimmenzahlFuerGebiet);
    }

    return result;
  }

  protected static LinkedList<String> erzeugeKopfzeileGebietsnummer(SortedMap<Wahldurchfuehrungsgebiet, Liste> listePerWahlkreis) {
    return erzeugeKopfzeileGebietsnummer(new ArrayList(listePerWahlkreis.keySet()));
  }

  protected static LinkedList<String> erzeugeKopfzeileGebietsnummer(List<Wahldurchfuehrungsgebiet> wahlkreise) {
    return wahlkreise.stream().map(Wahldurchfuehrungsgebiet::getNummer).collect(toCollection(LinkedList::new));
  }

  protected static LinkedList<String> erzeugeKopfzeileGebietsname(SortedMap<Wahldurchfuehrungsgebiet, Liste> listePerWahlkreis) {
    return erzeugeKopfzeileGebietsname(new ArrayList(listePerWahlkreis.keySet()));
  }

  protected static LinkedList<String> erzeugeKopfzeileGebietsname(List<Wahldurchfuehrungsgebiet> wahlkreise) {
    return wahlkreise.stream().map(Wahldurchfuehrungsgebiet::getName).map(name -> {
      if ("Buitenland".equals(name)) { // ELECT-16289
        return "Kieskring buitenland";
      }

      return name;
    }).collect(toCollection(LinkedList::new));
  }

  protected static List<NlKandidatStimmenDocument> erzeugeKandidatenStimmenZeilen(NlErgebnisDatenCustomDataDocumentContext context, SortedMap<Wahldurchfuehrungsgebiet, Liste> listePerWahlkreis, Predicate<PersonendatenPK> isMitVorzugsstimmenGewaehltFunction) {
    NlErgebnisDaten nlErgebnisDaten = context.getNlErgebnisDaten();
    Stimmabgabeart stimmabgabeart = context.getStimmabgabeart();

    StimmzettelFormatter stimmzettelFormatter = context.getStimmzettelFormatter();

    int anzahlListenkandidaturen = listePerWahlkreis.values().stream().findAny().map(l -> l.getListenkandidaturen().size()).orElse(0);
    List<NlKandidatStimmenDocument> result = new ArrayList<>();

    for (int kandidaturPosition = 0; kandidaturPosition < anzahlListenkandidaturen; kandidaturPosition++) {
      LinkedList<Long> stimmenzahlProGebiet = new LinkedList<>();
      LinkedList<Long> stimmenzahlProGebietGewichtetMitVoteValues = new LinkedList<>();

      long anzahlStimmenDesKandidats = 0L;
      long anzahlStimmenDesKandidatsGewichtetMitVoteValues = 0L;

      boolean isUeberVorzugGewaehlt = false;

      for (Entry<Wahldurchfuehrungsgebiet, Liste> entry : listePerWahlkreis.entrySet()) {
        Wahldurchfuehrungsgebiet wahlkreis = entry.getKey();
        Liste liste = entry.getValue();

        Listenkandidatur listenkandidatur = liste.getListenkandidaturenSortiertNachRelativerPosition().get(kandidaturPosition);

        long summeListenkandidaturListenstimmen = nlErgebnisDaten.getSummeListenkandidaturListenstimmen(wahlkreis, stimmabgabeart, listenkandidatur);
        long summeListenkandidaturListenstimmenGewichtetMitVoteValues = nlErgebnisDaten.getSummeListenkandidaturListenstimmenGewichtetMitVoteValues(wahlkreis, stimmabgabeart, listenkandidatur);

        anzahlStimmenDesKandidats += summeListenkandidaturListenstimmen;
        anzahlStimmenDesKandidatsGewichtetMitVoteValues += summeListenkandidaturListenstimmenGewichtetMitVoteValues;

        stimmenzahlProGebiet.add(summeListenkandidaturListenstimmen);
        stimmenzahlProGebietGewichtetMitVoteValues.add(summeListenkandidaturListenstimmenGewichtetMitVoteValues);

        isUeberVorzugGewaehlt = isMitVorzugsstimmenGewaehltFunction.test(listenkandidatur.getPk());
      }

      result.add(new NlKandidatStimmenDocument(context, getGewaehltFuerKandidat(listePerWahlkreis, kandidaturPosition, nlErgebnisDaten), stimmzettelFormatter, anzahlStimmenDesKandidats, anzahlStimmenDesKandidatsGewichtetMitVoteValues, stimmenzahlProGebiet, stimmenzahlProGebietGewichtetMitVoteValues, isUeberVorzugGewaehlt));
    }

    return result;
  }

  private static Gewaehlt getGewaehltFuerKandidat(SortedMap<Wahldurchfuehrungsgebiet, Liste> listePerWahlkreis, int kandidaturPosition, NlErgebnisDaten nlErgebnisdaten) {
    Collection<Liste> listenDerGruppeSortedByWahlkreis = new ArrayList<>(listePerWahlkreis.values()); // ELECT-13638, Liste MUSS nach Wahlkreis sortiert sein, um das korrekte Gewaehlt zu finden!!!

    for (Liste liste : listenDerGruppeSortedByWahlkreis) {
      Listenkandidatur listenkandidatur = liste.getListenkandidaturenSortiertNachRelativerPosition().get(kandidaturPosition);

      if (listenkandidatur != null) {
        Gewaehlt gewaehlt = nlErgebnisdaten.getGewaehltFuerListenkandidatur(listenkandidatur);

        if (gewaehlt == null) { // ELECT-13521
          LOG.error("KEIN Gewaehlt für Listenkandidatur bestimmbar [kandidaturPosition = {}, kandidatur = {}, listen = {}]", kandidaturPosition, listenkandidatur, listenDerGruppeSortedByWahlkreis);

          gewaehlt = new Gewaehlt(EmptyCustomData.INSTANCE);
          gewaehlt.setKandidaturOhneUpdateBeziehung(listenkandidatur);
          gewaehlt.setGewaehltStatus(GewaehltStatus.NICHT_GEWAEHLT);
          gewaehlt.setRang(AbstractGewaehlt.KEIN_RANG);
        }

        return gewaehlt;
      }
    }

    throw new IllegalStateException("KEINE Listenkandidatur für Position bestimmbar [kandidaturPosition = " + kandidaturPosition + ", listen = " + listenDerGruppeSortedByWahlkreis + "]");
  }

  protected static long getAnzahlSitzeDerListe(NlErgebnisDatenCustomDataDocumentContext context, SortedMap<Wahldurchfuehrungsgebiet, Liste> listePerWahlkreis) {
    NlErgebnisDaten nlErgebnisDaten = context.getNlErgebnisDaten();

    return new HashSet<>(listePerWahlkreis.values()).stream().map(nlErgebnisDaten::getSitzeFuerListe).filter(Objects::nonNull).findAny().map(Sitze::getAnzahl).orElse(0L);
  }

  protected static long getAnzahlStimmenDerListe(NlErgebnisDatenCustomDataDocumentContext context, SortedMap<Wahldurchfuehrungsgebiet, Liste> listePerWahlkreis) {
    long result = 0L;

    NlErgebnisDaten nlErgebnisDaten = context.getNlErgebnisDaten();
    Stimmabgabeart stimmabgabeart = context.getStimmabgabeart();

    for (Entry<Wahldurchfuehrungsgebiet, Liste> entry : listePerWahlkreis.entrySet()) {
      result += nlErgebnisDaten.getSummeListeListenstimmen(entry.getKey(), stimmabgabeart, entry.getValue());
    }

    return result;
  }

  protected static long getAnzahlStimmenDerListeGewichtetMitVoteValues(NlErgebnisDatenCustomDataDocumentContext context, SortedMap<Wahldurchfuehrungsgebiet, Liste> listePerWahlkreis) {
    long result = 0L;

    NlErgebnisDaten nlErgebnisDaten = context.getNlErgebnisDaten();
    Stimmabgabeart stimmabgabeart = context.getStimmabgabeart();

    for (Entry<Wahldurchfuehrungsgebiet, Liste> entry : listePerWahlkreis.entrySet()) {
      result += nlErgebnisDaten.getSummeListeListenstimmenGewichtetMitVoteValues(entry.getKey(), stimmabgabeart, entry.getValue());
    }

    return result;
  }
}
