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

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.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;

import java.util.ArrayList;
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;

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();
  }

  private void verifyNlGruppen() {
    List<Gruppe> ungueltigeGruppen = sitzberechnungsdaten.getGruppen().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 verifyAnzahlSitzeUnabhaengigVomGebiet() {
    Set<Long> anzahlSitzeProGebiet = sitzberechnungsdaten.getSitzberechnungeingabe().getGebiete().stream()
            .map(gebiet -> sitzberechnungsdaten.getSitzberechnungeingabe().getAnzahlSitzeFuerGebiet(gebiet))
            .collect(Collectors.toSet());
    if (anzahlSitzeProGebiet.size() > 1) {
      throw new IllegalStateException("Anzahl Sitze sollte im OSV unabhängig vom Gebiet sein");
    }
    Long anzahlSitze = anzahlSitzeProGebiet.iterator().next();

    if (anzahlSitze < 1) {
      throw new IllegalStateException(String.format("Die Anzahl der zu verteilenden Sitze ist %s, aber es muss höher als 1 sein!", anzahlSitze));
    }
  }

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

  public long getAnzahlSitze() {
    //Anzahl der Sitze im OSV ist unabhängig vom Gebiet
    return sitzberechnungsdaten.getSitzberechnungeingabe().getAnzahlSitzeFuerGebiet(sitzberechnungsdaten.
            getSitzberechnungeingabe().
            getGebiete().iterator().next());
  }
}
