
package de.ivu.elect.sitzberechnung.osv.exchange.control;

import static java.util.stream.Collectors.toSet;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import org.apache.commons.lang3.StringUtils;

import de.ivu.elect.sitzberechnung.Gebiet;
import de.ivu.elect.sitzberechnung.Gruppe;
import de.ivu.elect.sitzberechnung.Kandidat;
import de.ivu.elect.sitzberechnung.Liste;
import de.ivu.elect.sitzberechnung.SitzberechnungEntity;
import de.ivu.elect.sitzberechnung.Sitzberechnungseingabe;
import de.ivu.elect.sitzberechnung.control.AbstractSitzberechnungsdaten;
import de.ivu.elect.sitzberechnung.control.Sitzberechnungsdaten;
import de.ivu.elect.sitzberechnung.osv.exchange.OSVLegacySitzberechnungseingabe;
import de.ivu.elect.sitzberechnung.osv.exchange.Stel;
import de.ivu.elect.sitzberechnung.osv.exchange.entity.NlGruppe;
import de.ivu.wahl.result.CandidateList;
import de.ivu.wahl.result.determination.P2List;

public class OSVLegacySitzberechnungsdaten extends AbstractSitzberechnungsdaten {

  private final OSVLegacySitzberechnungseingabe osvLegacySitzberechnungeingabe;
  private final Sitzberechnungsdaten sitzberechnungsdaten;
  private final Map<Stel, Set<Liste>> listenProStelMap;

  public OSVLegacySitzberechnungsdaten(OSVLegacySitzberechnungseingabe osvLegacySitzberechnungeingabe, Sitzberechnungsdaten sitzberechnungsdaten) {
    this.osvLegacySitzberechnungeingabe = osvLegacySitzberechnungeingabe;
    this.sitzberechnungsdaten = sitzberechnungsdaten;
    this.listenProStelMap = initListenProStelMap(sitzberechnungsdaten, osvLegacySitzberechnungeingabe);
  }

  private static Map<Stel, Set<Liste>> initListenProStelMap(Sitzberechnungsdaten sitzberechnungsdaten, OSVLegacySitzberechnungseingabe osvLegacySitzberechnungeingabe) {
    HashMap<Stel, Set<Liste>> result = new HashMap<>();
    for (Liste liste : sitzberechnungsdaten.getSitzberechnungeingabe().getListen()) {
      osvLegacySitzberechnungeingabe.getStelFuerListe(liste).ifPresent(stel -> result.computeIfAbsent(stel, a -> new HashSet<>()).add(liste));
    }
    return result;
  }

  public Sitzberechnungsdaten getSitzberechnungsdaten() {
    return sitzberechnungsdaten;
  }

  public Set<Stel> getStels() {
    return listenProStelMap.keySet();
  }

  public Stel findStelNachKey(String key) {
    return getStels().stream()
            .filter(stel -> stel.getKey().equals(key))
            .findAny().orElse(null);
  }

  public Set<Liste> getListenFuerStel(Stel stel) {
    return listenProStelMap.getOrDefault(stel, Collections.emptySet());
  }

  public Liste findListeEinesStelsAufGebiet(Stel stel, Gebiet gebiet) {
    return getListenFuerStel(stel).stream()
            .filter(liste -> liste.getGebiet().equals(gebiet))
            .findAny().orElse(null);
  }

  public OSVLegacySitzberechnungseingabe getOsvLegacySitzberechnungeingabe() {
    return osvLegacySitzberechnungeingabe;
  }

  public List<Liste> getElectListenEinerP2Liste(P2List p2List) {
    return p2List.getCandidateLists().stream()
            .map(this::getElectListeEinesOSVLegacyCandidateLists)
            .collect(Collectors.toList());
  }

  public Liste getElectListeEinesOSVLegacyCandidateLists(CandidateList candidateList) {
    String listeKey = candidateList.getP2List().getExternalKey().toString();
    Liste electListe = sitzberechnungsdaten.findListeNachKey(listeKey);
    if (electListe == null) {
      Stel stelNachKey = findStelNachKey(listeKey);
      if (stelNachKey != null) {
        Gebiet gebiet = sitzberechnungsdaten.findGebietNachKey(candidateList.getElectoralDistrict().getExternalKey().toString());
        electListe = findListeEinesStelsAufGebiet(stelNachKey, gebiet);
      }
    }
    return electListe;
  }

  @Override
  public void validate() {
    validateListenUndStelsKeys();
    verifyKandidatlistenInStels();
    verifyAnzahlSitzeUnabhaengigVomGebiet();
    verifyKandidatenInNurEineGruppeInJedemGebiet();
    verifyJedeKandidaturHatEineEindeutigePositionInListe();
    verifyAlleListenSindAufEinemListenwahlgebiet();
    verifyNlGruppen();
    verifyGebietsnummernSindNumerisch();
  }

  void verifyGebietsnummernSindNumerisch() {
    for (Gebiet gebiet : sitzberechnungsdaten.getSitzberechnungeingabe().getGebiete()) {
      String gebietsnummerFuerKey = osvLegacySitzberechnungeingabe.getGebietsnummerFuerKey(gebiet.getKey());
      if (StringUtils.isBlank(gebietsnummerFuerKey) || !StringUtils.isNumeric(gebietsnummerFuerKey)) {
        throw new IllegalStateException("Das Gebiet [" + gebiet.getKey() + "] hat eine ungültige Gebietsnummer: " + gebietsnummerFuerKey);
      }
    }
  }

  private void verifyNlGruppen() {
    List<Gruppe> ungueltigeGruppen = sitzberechnungsdaten.getGruppenSortiertNachKey().stream()
            .filter(g -> !(g instanceof NlGruppe))
            .collect(Collectors.toList());

    if (!ungueltigeGruppen.isEmpty()) {
      throw new IllegalStateException("Die folgenden Gruppen müssen " + NlGruppe.class.getName() + " implementieren: " +
              ungueltigeGruppen.stream().map(SitzberechnungEntity::getKey).collect(Collectors.joining(", ")));
    }
  }

  void verifyAlleListenSindAufEinemListenwahlgebiet() {
    List<Liste> fehlerhafteListen = sitzberechnungsdaten.getSitzberechnungeingabe().getListen().stream()
            .filter(liste -> !liste.getGebiet().isListenwahlgebiet())
            .collect(Collectors.toList());
    if (!fehlerhafteListen.isEmpty()) {
      throw new IllegalStateException("Die folgende Listen treten nicht in einem Listenwahlgebiet auf: " + fehlerhafteListen);
    }
  }

  void verifyJedeKandidaturHatEineEindeutigePositionInListe() {
    List<String> fehlerhafteListen = new ArrayList<>();
    for (Liste liste : sitzberechnungsdaten.getSitzberechnungeingabe().getListen()) {
      long eindeutigeListenpositionenDerKandidaturen = liste.getListenkandidaturen().stream()
              .map(liste::getPositionInListe)
              .filter(pos -> pos > 0)
              .distinct().count();

      if (eindeutigeListenpositionenDerKandidaturen != liste.getListenkandidaturen().size()) {
        fehlerhafteListen.add(liste.toString());
      }
    }
    if (!fehlerhafteListen.isEmpty()) {
      throw new IllegalStateException("Die folgende Liste enthält keine eindeutige Kandidaturenpositionen: " + String.join(", ", fehlerhafteListen));
    }
  }

  void verifyKandidatenInNurEineGruppeInJedemGebiet() {
    for (Kandidat kandidat : getSitzberechnungsdaten().getAlleKandidaten()) {
      Map<Gebiet, Long> anzahlListeAufGebiet = getSitzberechnungsdaten().getListenFuerKandidat(kandidat).stream()
              .collect(Collectors.groupingBy(Liste::getGebiet, Collectors.counting()))
              .entrySet().stream().filter(e -> e.getValue() > 1).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
      if (!anzahlListeAufGebiet.isEmpty()) {
        String anzahlListeAufGebietString = anzahlListeAufGebiet.entrySet().stream()
                .map(e -> String.format("%s (%sx)", e.getKey(), e.getValue())).collect(Collectors.joining(", "));
        throw new IllegalStateException(String.format("Kandidat %s tritt auf mehreren Listen an, die sich auf dem gleichen Gebiet befinden: " + anzahlListeAufGebietString, kandidat));
      }
    }
  }

  void verifyKandidatlistenInStels() {
    for (Map.Entry<Stel, Set<Liste>> stelListEntry : listenProStelMap.entrySet()) {
      Stel stel = stelListEntry.getKey();
      Set<Liste> listenDesStels = stelListEntry.getValue();
      Liste ersteListe = listenDesStels.iterator().next();
      boolean kandidatenGleichUeberAlleListenDesStels = listenDesStels.stream()
          .allMatch(liste -> sitzberechnungsdaten.getSortierteKandidaten(ersteListe).equals(sitzberechnungsdaten.getSortierteKandidaten(liste)));
      if (!kandidatenGleichUeberAlleListenDesStels) {
        throw new IllegalStateException("Die Listen des folgenden Stels haben unterschiedliche Kandidatlisten: " + stel);
      }

      boolean gruppeGleichUeberAlleListenDesStels = listenDesStels.stream().allMatch(liste -> liste.getGruppe().equals(ersteListe.getGruppe()));
      if (!gruppeGleichUeberAlleListenDesStels) {
        throw new IllegalStateException(String.format("Die Listen des Stels %s gehören zu unterschiedlichen Gruppen.", stel.getKey()));
      }
    }
  }

  void validateListenUndStelsKeys() {
    List<SitzberechnungEntity> entitaeten = new ArrayList<>(getStels());
    entitaeten.addAll(sitzberechnungsdaten.getSitzberechnungeingabe().getListen());
    verifyEindeutigeKeys(new ArrayList<>(entitaeten));
  }

  // Anzahl der Sitze im OSV ist unabhängig vom Gebiet
  void verifyAnzahlSitzeUnabhaengigVomGebiet() {
    Set<Long> anzahlenSitzeInGebietshierarchieGroesserNull = collectAnzahlenSitzeInGebietshierarchieGroesserNull();

    if (anzahlenSitzeInGebietshierarchieGroesserNull.size() > 1) {
      throw new IllegalStateException("Anzahl Sitze im Wahlgebiet nicht eindeutig (OSV erwartet nur eine Anzahl Sitze) [anzahlen = " + anzahlenSitzeInGebietshierarchieGroesserNull + "]");
    }

    Long anzahlSitze = getAnzahlSitze();

    if (anzahlSitze < 1L) {
      throw new IllegalStateException("Anzahl Sitze im Wahlgebiet nicht konfiguriert (OSV erwartet Anzahl Sitze größer 0) [anzahl = " + anzahlSitze + "]");
    }
  }

  public long getAnzahlSitze() {
    return collectAnzahlenSitzeInGebietshierarchieGroesserNull().stream().findFirst().orElse(0L);
  }

  private Set<Long> collectAnzahlenSitzeInGebietshierarchieGroesserNull() {
    Sitzberechnungseingabe sitzberechnungeingabe = sitzberechnungsdaten.getSitzberechnungeingabe();
    Collection<Gebiet> gebieteInGebietshierarchie = sitzberechnungeingabe.getGebiete();

    return gebieteInGebietshierarchie.stream() //
        .map(sitzberechnungeingabe::getAnzahlSitze) //
        .filter(anzahlSitze -> anzahlSitze > 0L) // ELECT-13897
        .collect(toSet());
  }
}
