
package de.ivu.elect.was.exchange.ergebnis.csv;

import static java.lang.Integer.MAX_VALUE;
import static org.apache.commons.lang3.StringUtils.trimToEmpty;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;

import org.apache.commons.lang3.math.NumberUtils;
import org.jsefa.csv.CsvSerializer;

import de.ivu.elect.business.anschrift.entity.Anschrift;
import de.ivu.elect.business.datei.entity.DateiFileNames;
import de.ivu.elect.business.ergebnis.control.ErgebnisDaten;
import de.ivu.elect.business.ergebnis.control.ErgebnisExportContext;
import de.ivu.elect.business.ergebnis.entity.Ergebnisart;
import de.ivu.elect.business.exchange.entity.DateiExportArt;
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.gruppe.entity.NlPolitischeGruppe;
import de.ivu.elect.business.gruppe.entity.NlPolitischeGruppeArt;
import de.ivu.elect.business.kandidatur.control.ListenkandidaturByStimmzettelPositionComparator;
import de.ivu.elect.business.kandidatur.entity.Kandidatur;
import de.ivu.elect.business.kandidatur.entity.Listenkandidatur;
import de.ivu.elect.business.kandidatur.entity.NlWasListenkandidatur;
import de.ivu.elect.business.person.entity.NlPersonendaten;
import de.ivu.elect.business.stimmzettel.entity.EntityAufStimmzettelDaten;
import de.ivu.elect.business.wahl.entity.NlWahlPhase;
import de.ivu.elect.business.wahlvorschlag.entity.Liste;
import de.ivu.elect.business.wahlvorschlag.entity.NlListe;

public class Osv56ErgebnisCsvExportMapper extends AbstractNlCsvErgebnisExportMapper {

  private static final long serialVersionUID = 1L;

  public static final String FILE_FORMAT_NAME = "file.format.ergebnisdokument.export.osv56.main";

  public Osv56ErgebnisCsvExportMapper() {
    super();
  }

  @Override
  public boolean isAcceptWahlPhase(NlWahlPhase nlWahlPhase) {
    return nlWahlPhase.isFreigabe();
  }

  @Override
  protected String getFileFormatName() {
    return FILE_FORMAT_NAME;
  }

  @Override
  public DateiFileNames getDateiExportFileNames(DateiExportArt dateiExportArt, ErgebnisExportContext context) {
    return new Osv56ErgebnisCsvExportFileNames(getMessages(), dateiExportArt, getCustomer()); // ELECTVAPP-933
  }

  @Override
  protected List<Gruppe> getGruppen(ErgebnisDaten ergebnisDaten) {
    List<Gruppe> gruppen = ergebnisDaten.getGruppen();
    gruppen.sort(Comparator.comparingLong(gruppe -> gruppe.getVorgegebenePosition() != null ? gruppe.getVorgegebenePosition() : MAX_VALUE));

    return gruppen;
  }

  @Override
  protected Map<Listenkandidatur, Long> getListenkandidaturSortiert(ErgebnisDaten ergebnisDaten, List<Kandidatur> kandidaturen, List<Gruppe> gruppenListe) {
    LinkedHashMap<Listenkandidatur, Long> sorierteListenKandidaturen = new LinkedHashMap<>();

    for (Gruppe gruppe : gruppenListe) {
      List<Kandidatur> kandidaturenZurGruppe = filterKandidaturenZurGruppe(kandidaturen, gruppe);

      Map<Boolean, List<Kandidatur>> map = kandidaturenZurGruppe.stream().collect(Collectors.partitioningBy(e -> NlListe.isListeEinesStelWas(e.castTo(Listenkandidatur.class).getEntity().getListe())));

      List<Kandidatur> kandidaturenInStel = map.get(true); // identische liste und listengruppen mit stelNr.isPresent()=true
      if (kandidaturenInStel.size() > 0) {
        sortiereKandidaturenInStel(ergebnisDaten, sorierteListenKandidaturen, kandidaturenInStel);
      }

      List<Kandidatur> kandidaturenNichtInStel = map.get(false);// einzelliste und listengruppe mit stelNr.isPresent()=false
      if (kandidaturenNichtInStel.size() > 0) {
        sortiereKandidaturenNichtInStel(ergebnisDaten, sorierteListenKandidaturen, kandidaturenNichtInStel);
      }

    }

    return sorierteListenKandidaturen;
  }

  private void sortiereKandidaturenNichtInStel(ErgebnisDaten ergebnisDaten, LinkedHashMap<Listenkandidatur, Long> sorierteListenKandidaturen, List<Kandidatur> kandidaturenNichtInStel) {
    Comparator<Listenkandidatur> listenkandidaturComparator = getListenkandidaturComparator(ergebnisDaten, false);

    List<Wahldurchfuehrungsgebiet> wahlgebieteSortiert = ergebnisDaten.getListenWahlGebiete().stream().sorted(Comparator.comparingLong(e -> NumberUtils.createInteger(e.getNummer())))
        .collect(Collectors.toList());

    for (Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet : wahlgebieteSortiert) {
      Map<Listenkandidatur, Long> nachRangschikkigSortierteMap = getStimmenVonSortierterListenkandidaturZumGebiet(ergebnisDaten, kandidaturenNichtInStel, listenkandidaturComparator,
          wahldurchfuehrungsgebiet);

      sorierteListenKandidaturen.putAll(nachRangschikkigSortierteMap);
    }
  }

  private void sortiereKandidaturenInStel(ErgebnisDaten ergebnisDaten, LinkedHashMap<Listenkandidatur, Long> sorierteListenKandidaturen, List<Kandidatur> kandidaturenInStel) {
    Comparator<Listenkandidatur> listenkandidaturComparator = getListenkandidaturComparator(ergebnisDaten, true);
    Map<Listenkandidatur, Long> map = getKummulierteStimmenVonListenkandidaturMap(ergebnisDaten, kandidaturenInStel);
    Map<Listenkandidatur, Long> nachStelUndRangschikkingSortierteMap = getStimmenVonSortierterListenkandidaturMap(listenkandidaturComparator, map);

    sorierteListenKandidaturen.putAll(nachStelUndRangschikkingSortierteMap);
  }

  private Map<Listenkandidatur, Long> getStimmenVonSortierterListenkandidaturZumGebiet(ErgebnisDaten ergebnisDaten, List<Kandidatur> kandidaturenZurGruppe,
      Comparator<Listenkandidatur> listenkandidaturComparator, Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet) {
    Map<Listenkandidatur, Long> map = new HashMap<>();

    for (Kandidatur kandidatur : kandidaturenZurGruppe) {
      if (kandidatur.isInstanceOf(Listenkandidatur.class)) {
        Listenkandidatur listenkandidatur = kandidatur.castTo(Listenkandidatur.class).getEntity();

        if (listenkandidatur.getListe().getGebiet().getNummer().equals(wahldurchfuehrungsgebiet.getNummer())) {
          long anzahlStimmen = ergebnisDaten.getStimmenFuerListenkandidatur(listenkandidatur, wahldurchfuehrungsgebiet);

          map.put(listenkandidatur, anzahlStimmen);
        }
      }
    }
    return getStimmenVonSortierterListenkandidaturMap(listenkandidaturComparator, map);
  }

  private Map<Listenkandidatur, Long> getKummulierteStimmenVonListenkandidaturMap(ErgebnisDaten ergebnisDaten, List<Kandidatur> kandidaturenInStel) {
    List<Kandidatur> removeKandidaturMitGleicherStel = new ArrayList<>();

    Map<Listenkandidatur, Long> map = new HashMap<>();

    for (Kandidatur kandidatur : kandidaturenInStel) {
      if (!removeKandidaturMitGleicherStel.contains(kandidatur) && kandidatur.isListenkandidatur()) {
        Listenkandidatur listenkandidatur = kandidatur.castTo(Listenkandidatur.class).getEntity();
        List<Kandidatur> identischeKandidaturenInGleicherStel = getIdentischVerkuepfteKandidaturenEinerStel(kandidatur, listenkandidatur);
        removeKandidaturMitGleicherStel.addAll(identischeKandidaturenInGleicherStel);

        long stimme = identischeKandidaturenInGleicherStel.stream().map(kandidatur1 -> kandidatur1.castTo(Listenkandidatur.class).getEntity())
            .mapToLong(listenkandidatur1 -> ergebnisDaten.getStimmenFuerListenkandidatur(listenkandidatur1, (Wahldurchfuehrungsgebiet) listenkandidatur1.getListe().getGebiet())).sum();
        map.put(listenkandidatur, stimme);
      }
    }

    return map;
  }

  private List<Kandidatur> getIdentischVerkuepfteKandidaturenEinerStel(Kandidatur kandidatur, Listenkandidatur listenkandidatur) {
    Collection<Kandidatur> identischVerknuepfteKandidaturen = listenkandidatur.getIdentischVerknuepfteKandidaturen();
    int stelNr = NlListe.getStelNummerWas(listenkandidatur.getListe()).orElse(MAX_VALUE);
    List<Kandidatur> identischeKandidaturenInGleicherStel = identischVerknuepfteKandidaturen.stream()
        .filter(k -> NlListe.getStelNummerWas(k.castTo(Listenkandidatur.class).getEntity().getListe()).orElse(MAX_VALUE).equals(stelNr)).collect(Collectors.toList());
    identischeKandidaturenInGleicherStel.add(kandidatur);
    return identischeKandidaturenInGleicherStel;
  }

  private Map<Listenkandidatur, Long> getStimmenVonSortierterListenkandidaturMap(Comparator<Listenkandidatur> listenkandidaturComparator, Map<Listenkandidatur, Long> map) {
    return map.entrySet().stream().sorted((one, another) -> listenkandidaturComparator.compare(one.getKey(), another.getKey()))
        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (oldValue, newValue) -> oldValue, LinkedHashMap::new));
  }

  private Comparator<Listenkandidatur> getListenkandidaturComparator(ErgebnisDaten ergebnisDaten, boolean inStel) {

    return (one, another) -> {
      if (inStel) {
        int compareStelNr = Integer.compare(NlListe.getStelNummerWas(one.getListe()).orElse(MAX_VALUE), NlListe.getStelNummerWas(another.getListe()).orElse(MAX_VALUE));

        if (compareStelNr == 0) {

          return rangschikkingComparator(ergebnisDaten, one, another);
        } else {
          return compareStelNr;
        }
      } else {
        return rangschikkingComparator(ergebnisDaten, one, another);
      }
    };
  }

  private int rangschikkingComparator(ErgebnisDaten ergebnisDaten, Listenkandidatur one, Listenkandidatur another) {
    // ELECTVAPP-936
    List<Gruppe> gruppenMitMindestensEinenSitzSortiertNachListenPosition = getGruppenMitMindestensEinenSitzSortiertNachListenPosition(ergebnisDaten);
    Comparator<Long> nullsLastLongComparator = Comparator.nullsLast(Comparator.naturalOrder());

    Long rangschikkingP19One = getRangschikkingP19(ergebnisDaten, one, new NlWasListenkandidatur(one), one.getGruppe(), gruppenMitMindestensEinenSitzSortiertNachListenPosition);
    Long rangschikkingP19Another = getRangschikkingP19(ergebnisDaten, another, new NlWasListenkandidatur(another), another.getGruppe(), gruppenMitMindestensEinenSitzSortiertNachListenPosition);

    if (rangschikkingP19One == null && rangschikkingP19Another == null || rangschikkingP19One == Long.MAX_VALUE && rangschikkingP19Another == Long.MAX_VALUE) {
      // ELECTVAPP-936 ... candidates that did not received a seat, has to be ordered by the number the candidates have on the list.
      return new ListenkandidaturByStimmzettelPositionComparator(ergebnisDaten.getEntityAufStimmzettelDaten()).compare(one, another);

    }

    return nullsLastLongComparator.compare(rangschikkingP19One, rangschikkingP19Another);
  }

  private List<Kandidatur> filterKandidaturenZurGruppe(List<Kandidatur> kandidaturen, Gruppe gruppe) {
    return kandidaturen.stream().filter(kandidatur -> kandidatur.isInstanceOf(Listenkandidatur.class))
        .filter(kandidatur -> kandidatur.castTo(Listenkandidatur.class).getEntity().getGruppe().equals(gruppe)).collect(Collectors.toList());
  }

  @Override
  protected void writeIntro(ErgebnisDaten ergebnisDaten, CsvSerializer serializer) {// ELECTVAPP-53
    writeField(serializer, "Verkiezing: " + ergebnisDaten.getWahl().getName());
    nextLine(serializer);
    nextLine(serializer);

    writeField(serializer, "Kandidaten in de volgorde van de rangschikking overeenkomstig artikel P 19 van de Kieswet:");
    nextLine(serializer);

    writeField(serializer, "Alleen de gegevens van lijsten die tenminste 1 zetel hebben behaald staan in het overzicht");
    nextLine(serializer);

    if (ergebnisDaten.getListenWahlGebiete().size() > 1) {
      writeField(serializer, "\"Benoemd = (Ja)\" betekent \"benoemd in een ander stel of kieskring van de lijstengroep\".");
      nextLine(serializer);
    }
  }

  @Override
  protected void writeHeader(CsvSerializer serializer, ErgebnisDaten ergebnisDaten) {
    nextLine(serializer);
    writeField(serializer, "Volgnummer"); // ELECTVAPP-936
    writeField(serializer, "Rangschikking P19");
    writeField(serializer, "Politieke Groepering");
    writeField(serializer, "Lijstnummer");

    if (ergebnisDaten.getListenWahlGebiete().size() > 1) {
      writeField(serializer, "Stel of kieskring");
      writeField(serializer, "Nummer van stel of kieskring");
    }

    writeField(serializer, "Stemmen");
    writeField(serializer, "Kandidaatnummer");

    writeField(serializer, "Gekozen");
    writeField(serializer, "Achternaam");
    writeField(serializer, "Voorletters");
    writeField(serializer, "Roepnaam");
    writeField(serializer, "Woonplaats");
    writeField(serializer, "Land");
    writeField(serializer, "Geslacht");
    writeField(serializer, "Publiceer geslacht");
    writeField(serializer, "(Corresp.)Adres");
    writeField(serializer, "Postcode");
    writeField(serializer, "Woonplaats");
    writeField(serializer, "Land");
    writeField(serializer, "Achternaam (gemachtigde)");
    writeField(serializer, "Voorletters (gemachtigde)");
    writeField(serializer, "Roepnaam (gemachtigde)");
    writeField(serializer, "Woonplaats (gemachtigde)");
    writeField(serializer, "Land (gemachtigde)");
    writeField(serializer, "(Corresp.)Adres (gemachtigde)");
    writeField(serializer, "Postcode (gemachtigde)");
    writeField(serializer, "Woonplaats (gemachtigde)");
    writeField(serializer, "Land (gemachtigde)");
  }

  @Override
  protected void writeData(CsvSerializer serializer, ErgebnisDaten ergebnisDaten, List<Gruppe> gruppenListe, Map<Listenkandidatur, Long> listenkandidaturenSortiert) {
    Ergebnisart ergebnisart = ergebnisDaten.getErgebnis().getErgebnisart();

    EntityAufStimmzettelDaten entityAufStimmzettelDaten = ergebnisDaten.getEntityAufStimmzettelDaten();
    List<Gruppe> gruppenMitMindestensEinenSitzSortiertNachListenPosition = getGruppenMitMindestensEinenSitzSortiertNachListenPosition(ergebnisDaten);// ELECTVAPP-311
    AtomicInteger volgnummer = new AtomicInteger(0);
    AtomicReference<Long> rangIterator = new AtomicReference<>(0L);

    listenkandidaturenSortiert.forEach((listenkandidatur, stimmen) -> {
      // Anyway, I discussed this with Jan-Jouke and we are OK to follow your suggestion to add a new column with a sequence number.
      // The new column should be added in front of the existing columns and should be named 'Volgnummer'.
      writeLong(serializer, (long) volgnummer.incrementAndGet());

      NlWasListenkandidatur nlListenkandidatur = new NlWasListenkandidatur(listenkandidatur);
      Liste liste = nlListenkandidatur.getListe();

      Gruppe gruppe = liste.getGruppe();
      wrietFieldRang(serializer, ergebnisDaten, gruppenMitMindestensEinenSitzSortiertNachListenPosition, rangIterator, listenkandidatur, nlListenkandidatur, gruppe);

      Gebiet gebiet = liste.getGebiet();
      writeField(serializer, gruppe.getName());

      //StimmzettelPosition Partei
      writeFieldStimmzettelPosition(serializer, liste.getStimmzettelPosition(gebiet, entityAufStimmzettelDaten).getPosition());
      writeFieldStelUndNr(serializer, ergebnisDaten, entityAufStimmzettelDaten, liste, gruppe, gebiet);
      writeLong(serializer, stimmen);
      writeFieldStimmzettelPosition(serializer, nlListenkandidatur.getStimmzettelPosition(gebiet, entityAufStimmzettelDaten).getPosition());

      writeFieldGewaehlt(serializer, ergebnisart, listenkandidatur);

      writeFieldsForPersonendaten(serializer, nlListenkandidatur);

      writeField(serializer, getAbkuerzungFuerGeschlecht(nlListenkandidatur.getGeschlecht()));
      writeField(serializer, istGeschlechtPubliziert(liste));// ELECTVAPP-935

      writeFieldsForErreichbarkeitsanschrift(serializer, nlListenkandidatur.getErreichbarkeitsanschrift());
      writeFieldsForBevollmaechtigter(serializer, nlListenkandidatur);

      nextLine(serializer);
    });
  }

  private void wrietFieldRang(CsvSerializer serializer, ErgebnisDaten ergebnisDaten, List<Gruppe> gruppenMitMindestensEinenSitzSortiertNachListenPosition, AtomicReference<Long> rangIterator,
      Listenkandidatur listenkandidatur, NlWasListenkandidatur nlListenkandidatur, Gruppe gruppe) {
    Long rang = getRangschikkingP19(ergebnisDaten, listenkandidatur, nlListenkandidatur, gruppe, gruppenMitMindestensEinenSitzSortiertNachListenPosition);// ELECTVAPP-935
    /*
     * The position of the deceased candidate can be at the end of the list for each party. Important though is that all candidates have a sequence nr. in a separate column, that can be used at a
     * later moment to sort them according this nr. The deceased candidate then becomes the highest nr. for the list.
     */
    rang = setRangByIterator(rangIterator, rang); // ELECTVAPP-936

    writeLong(serializer, rang);
  }

  private Long setRangByIterator(AtomicReference<Long> rangIterator, Long rang) {
    if (rang != null) {
      if (!rang.equals(Long.MAX_VALUE)) { // da rang fuer verstorbene auf LONG_MAX_Value gesetzt
        rangIterator.set(rang);
      } else {
        rangIterator.set(rangIterator.get() + 1);
        rang = rangIterator.get();
      }
    }

    return rang;
  }

  public Long getRangschikkingP19(ErgebnisDaten ergebnisDaten, Listenkandidatur key, NlWasListenkandidatur nlListenkandidatur, Gruppe gruppe,
      List<Gruppe> gruppenMitMindestensEinenSitzSortiertNachListenPosition) {
    if (!gruppenMitMindestensEinenSitzSortiertNachListenPosition.contains(gruppe)) {// ELECTVAPP-311
      return null;
    } else if (nlListenkandidatur.isVerstorben()) {
      return Long.MAX_VALUE;

    } else {
      return ergebnisDaten.getGewaehlteFuerKandidatur(key).getRang();
    }
  }

  private void writeFieldsForPersonendaten(CsvSerializer serializer, NlPersonendaten nlPersonendaten) {
    writeField(serializer, trimToEmpty(trimToEmpty(nlPersonendaten.getFamiliennameBestandteil()) + " " + trimToEmpty(nlPersonendaten.getFamilienname()))); // ELECTVAPP-1372
    writeField(serializer, nlPersonendaten.getInitialen());
    writeField(serializer, nlPersonendaten.getVorname());

    Anschrift wohnanschrift = nlPersonendaten.getWohnanschrift();
    writeField(serializer, wohnanschrift.getOrt());
    writeField(serializer, wohnanschrift.getStaat());
  }

  private void writeFieldsForErreichbarkeitsanschrift(CsvSerializer serializer, Anschrift erreichbarkeitsanschrift) {
    Anschrift nlListenkandidaturErreichbarkeitsanschrift = erreichbarkeitsanschrift;
    writeField(serializer, nlListenkandidaturErreichbarkeitsanschrift.getStrasse());
    writeField(serializer, nlListenkandidaturErreichbarkeitsanschrift.getPlz());
    writeField(serializer, nlListenkandidaturErreichbarkeitsanschrift.getOrt());
    writeField(serializer, nlListenkandidaturErreichbarkeitsanschrift.getStaat());
  }

  private void writeFieldsForBevollmaechtigter(CsvSerializer serializer, NlWasListenkandidatur nlListenkandidatur) {
    if (nlListenkandidatur.getBevollmaechtigter() != null) {
      NlPersonendaten nlBevollmaechtigter = new NlPersonendaten(nlListenkandidatur.getBevollmaechtigter());

      writeFieldsForPersonendaten(serializer, nlBevollmaechtigter);
      writeFieldsForErreichbarkeitsanschrift(serializer, nlBevollmaechtigter.getErreichbarkeitsanschrift());
    }
  }

  private void writeFieldStimmzettelPosition(CsvSerializer serializer, Long position) {
    Long listeStimmzettelPosition = position;
    writeLong(serializer, listeStimmzettelPosition);
  }

  private void writeFieldStelUndNr(CsvSerializer serializer, ErgebnisDaten ergebnisDaten, EntityAufStimmzettelDaten entityAufStimmzettelDaten, Liste liste, Gruppe gruppe, Gebiet gebiet) {
    if (ergebnisDaten.getListenWahlGebiete().size() > 1) {
      NlPolitischeGruppeArt nlPolitischeGruppeArt = NlPolitischeGruppe.bestimmeEinreichungsart(ergebnisDaten.getListenDerGruppe(gruppe), entityAufStimmzettelDaten);

      if (nlPolitischeGruppeArt.isListengruppe()) {
        Optional<Integer> stelNummerWas = NlListe.getStelNummerWas(liste);

        if (stelNummerWas.isPresent()) {
          writeField(serializer, "Stel");
          writeLong(serializer, stelNummerWas.get().longValue());
        } else {
          writeField(serializer, "Kieskring");
          writeField(serializer, gebiet.getNummer());
        }
      } else {// Einzelliste oder identische Liste
        writeField(serializer, "");
        writeField(serializer, "");
      }
    }
  }

  private void writeFieldGewaehlt(CsvSerializer serializer, Ergebnisart ergebnisart, Listenkandidatur key) {
    if (key.isGewaehlt(ergebnisart)) {
      writeField(serializer, "Ja");
    } else {
      long anzahlIdentischVerknuepfteSindGewaehlt = key.getIdentischVerknuepfteKandidaturen().stream().filter(l -> l.isGewaehlt(ergebnisart)).count();

      if (anzahlIdentischVerknuepfteSindGewaehlt == 0) {
        writeField(serializer, "Nee");
      } else {
        writeField(serializer, "(Ja)");
      }
    }
  }
}
