
package de.ivu.elect.business.sitzberechnung.control.absolutemehrheitswahl;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import de.ivu.elect.business.ergebnis.entity.Ergebnis;
import de.ivu.elect.business.ergebnis.entity.ErgebnisKonfliktArt;
import de.ivu.elect.business.ergebnis.entity.ErgebnisLogEintragArt;
import de.ivu.elect.business.ergebnis.entity.GewaehltStatus;
import de.ivu.elect.business.ergebniskonflikt.control.util.ErgebnisKonfliktLoeser;
import de.ivu.elect.business.ergebniskonflikt.entity.ErgebnisKonflikt;
import de.ivu.elect.business.gebietsbaum.control.GebietByNummerComparator;
import de.ivu.elect.business.gebietsbaum.entity.Wahldurchfuehrungsgebiet;
import de.ivu.elect.business.kandidatur.boundary.WasWaehlerBenannteKandidaturKonfiguration;
import de.ivu.elect.business.kandidatur.entity.Direktkandidatur;
import de.ivu.elect.business.person.entity.PersonendatenPK;
import de.ivu.elect.business.sitzberechnung.control.util.SitzberechnungProGebietHelper;
import de.ivu.elect.business.sitzberechnung.entity.SitzberechnungsalgorithmusKonfiguration;
import de.ivu.elect.business.sitzberechnung.entity.Sitzberechnungseingabe;
import de.ivu.elect.business.sitzberechnung.entity.Sitzberechnungsergebnis;
import de.ivu.elect.util.NotNull;

/**
 * Bei der absoluten Mehrheitswahl ist der Vorschlag gewählt, der mehr als die Hälfte der Stimmen erhält. Um in allen Fällen die erforderliche Mehrheit zu erreichen, wird eine Stichwahl durchgeführt,
 * bei der nur die beiden besten Kandidaten des ersten Durchgangs zugelassen werden. Dies findet z.B. Anwendung bei den meisten Bürgermeister- und Landratswahlen in Deutschland. (frei nach
 * https://de.wikipedia.org/wiki/Mehrheitswahl#Absolute_Mehrheitswahl)
 * <p/>
 * 1 Gewählt ist, wer mehr als die Hälfte der abgegebenen gültigen Stimmen erhält. 2 Erhält niemand diese Mehrheit, findet eine Stichwahl unter den zwei Personen statt, die bei der ersten Wahl die
 * höchsten Stimmenzahlen erhalten haben. 3 Erhalten mehr als zwei Personen die höchste Stimmenzahl, ist die Wahl zu wiederholen. 4 Erhalten mehr als eine Person die zweithöchste Stimmenzahl,
 * entscheidet das Los, wer von ihnen in die Stichwahl kommt.
 * <p/>
 * Zu unterscheiden sind also die folgenden Fälle:
 * <ul>
 * <li>Erste Wahl, absolute Mehrheit für einen Kandidaten - fertig</li>
 * <li>Erste Wahl, keine absolute Mehrheit, zwei Kandidaten mit der höchsten Stimmenzahl liegen eindeutig fest - Stichwahl zwischen diesen beiden</li>
 * <li>Erste Wahl, mehr als 2 Kandidaten haben gleiche höchste Stimmenzahl - Wahl wiederholen</li>
 * <li>Erste Wahl, mehr als 1 Kandidat hat gleiche zweithöchste Stimmenzahl - Stichwahl nötig, Gewählte losen</li>
 * <li>Stichwahl, (absolute) Mehrheit für einen Kandidaten - fertig</li>
 * <li>Stichwahl, Stimmengleichheit - Stichwahl wiederholen</li>
 * </ul>
 * <p/>
 * Eine Stichwahl wird genau wie eine "erste" Wahl mit nur zwei Kandidaten behandelt. Im Algorithmus wäre also keine Unterscheidung zwischen erster Wahl und Stichwahl nötig. Auch eine
 * Wiederholungswahl ist wie eine "erste" Wahl zu behandeln.
 * <p/>
 * Das Ergebnis der Wahl sind keine {@link Sitze}, sondern nur {@link Gewaehlt}e und maximal einen {@link ErgebnisKonflikt}. Je nach Anzahl ist das Ergebnis zu interpretieren:
 * <ul>
 * <li>kein Gewählter = keine Kandidaten vorhanden oder noch keine Stimmen eingegangen</li>
 * <li>1 Gewählter = Wahlsieger, nur in diesem Fall ist <code>Gewaehlt.getIstGewaehlt() == Boolean.TRUE</code></li>
 * <li>2 Gewählte = Teilnehmer an Stichwahl</li>
 * <li>mehr als 2 Gewählte = Teilnehmer an Wiederholungswahl</li>
 * <li>1 Gewählter, 1 Konflikt = unentschiedener Losentscheid über zweiten Teilnehmer an der Stichwahl</li>
 * <li>2 Gewählte, 1 Konflikt = entschiedener Losentscheid über zweiten Teilnehmer an der Stichwahl</li>
 * </ul>
 */
public class AbsoluteMehrheitswahlSitzberechnungsalgorithmusInternal {

  private static final Long ERSTER = 1L;
  private static final Long ZWEITER = 2L;

  public static final String KANDIDAT_MIT_ABSOLUTER_MEHRHEIT_KEY = "sitzberechnung.algorithmus.bemerkung.absolute.mehrheitswahl.kandidat.gewinnt.mit.absoluter.mehrheit";
  public static final String STICHWAHL_NOTWENDIG_KEY = "sitzberechnung.algorithmus.bemerkung.absolute.mehrheitswahl.stichwahl.notwendig";
  public static final String STICHWAHL_NACH_LOSENTSCHEID_NOTWENDIG_KEY = "sitzberechnung.algorithmus.bemerkung.absolute.mehrheitswahl.stichwahl.nach.losentscheid.notwendig";
  public static final String WIEDERHOLUNGSWAHL_NOTWENDIG_KEY = "sitzberechnung.algorithmus.bemerkung.absolute.mehrheitswahl.wiederholungswahl.notwendig";

  private final Sitzberechnungseingabe< ? > sitzberechnungseingabe;

  private final WasWaehlerBenannteKandidaturKonfiguration waehlerBenannteKandidaturKonfiguration;

  private final Sitzberechnungsergebnis sitzberechnungsergebnis;
  private final AbsoluteMehrheitswahlSitzberechnungsalgorithmus sitzberechnungsalgorithmus;

  AbsoluteMehrheitswahlSitzberechnungsalgorithmusInternal(AbsoluteMehrheitswahlSitzberechnungsalgorithmus sitzberechnungsalgorithmus,
      Sitzberechnungseingabe<SitzberechnungsalgorithmusKonfiguration> sitzberechnungseingabe, WasWaehlerBenannteKandidaturKonfiguration waehlerBenannteKandidaturKonfiguration) {
    super();

    this.sitzberechnungsalgorithmus = sitzberechnungsalgorithmus;

    this.sitzberechnungseingabe = sitzberechnungseingabe;

    this.waehlerBenannteKandidaturKonfiguration = waehlerBenannteKandidaturKonfiguration;

    this.sitzberechnungsergebnis = Sitzberechnungsergebnis.createLeeresErgebnis();
  }

  Sitzberechnungsergebnis berechneSitzverteilung(Collection<Wahldurchfuehrungsgebiet> gebiete) {
    List<Wahldurchfuehrungsgebiet> gebieteSortiert = new ArrayList<>(gebiete);
    gebieteSortiert.sort(GebietByNummerComparator.ASCENDING);

    for (Wahldurchfuehrungsgebiet gebiet : gebieteSortiert) {
      sitzberechnungsergebnis.addAll(berechneSitzverteilungFuerGebiet(gebiet));
    }

    return sitzberechnungsergebnis;
  }

  private Sitzberechnungsergebnis berechneSitzverteilungFuerGebiet(Wahldurchfuehrungsgebiet gebiet) {
    SitzberechnungProGebietHelper helper = new SitzberechnungProGebietHelper(sitzberechnungseingabe, gebiet);

    AbsoluteMehrheitswahlKandidaturenNachStimmenSortierer sortiererDirektkandidaturenImGebiet = new AbsoluteMehrheitswahlKandidaturenNachStimmenSortierer();

    long anzahlStimmenWaehlerBenannteDirektkandidaturSonstige = 0L;

    for (Direktkandidatur direktkandidatur : helper.getDirektkandidaturen()) {
      if (direktkandidatur.isZugelassen()) {
        long anzahlStimmen = getAnzahlStimmen(helper, direktkandidatur);

        if (waehlerBenannteKandidaturKonfiguration.isWaehlerBenannteDirektkandidaturSonstige(helper.getMessages(), direktkandidatur)) {
          anzahlStimmenWaehlerBenannteDirektkandidaturSonstige += anzahlStimmen;
        } else {
          sortiererDirektkandidaturenImGebiet.add(anzahlStimmen, direktkandidatur);
        }
      }
    }

    Ergebnis ergebnis = sitzberechnungseingabe.getErgebnis();

    helper.log(ErgebnisLogEintragArt.TITEL, "sitzberechnung.algorithmus.protokoll.titel", gebiet.getName(), ergebnis.getWahl().getWahlart().getNameGui(),
        helper.getMessages().lokalisiere(ergebnis.getErgebnisart()));

    if (anzahlStimmenWaehlerBenannteDirektkandidaturSonstige > 0 && !sitzberechnungsalgorithmus.isBerechnetErgebnisFallsWaehlerBenannteWahlvorschlaegeSonstigeErfasst()) { // ELECT-9453
      helper.log(ErgebnisLogEintragArt.WARNUNG, "sitzberechnung.algorithmus.bemerkung.absolute.mehrheitswahl.fehler.waehler.benannte.direktkandidatur.sonstige", gebiet.getName(),
          anzahlStimmenWaehlerBenannteDirektkandidaturSonstige);

      return helper.getSitzberechnungsergebnis();
    }

    if (sortiererDirektkandidaturenImGebiet.isAlleStimmenNull()) {
      // Tue nichts - Ergebnis bleibt leer
    } else if (sortiererDirektkandidaturenImGebiet.hatWahlsiegerAbsoluteMehrheit()) {
      siegerMitAbsoluterMehrheit(helper, sortiererDirektkandidaturenImGebiet);
      helper.log(ErgebnisLogEintragArt.INFO, KANDIDAT_MIT_ABSOLUTER_MEHRHEIT_KEY, gebiet.getName());
    } else if (sortiererDirektkandidaturenImGebiet.sindTeilnehmerAnStichwahlEindeutig()) {
      teilnehmerStichwahlEindeutig(helper, sortiererDirektkandidaturenImGebiet);
      helper.log(ErgebnisLogEintragArt.WARNUNG, STICHWAHL_NOTWENDIG_KEY, gebiet.getName());
    } else if (sortiererDirektkandidaturenImGebiet.isWiederholungswahlNoetig()) {
      // alle nehmen an der Wiederholungswahl teil => keine Gewählten
      helper.log(ErgebnisLogEintragArt.WARNUNG, WIEDERHOLUNGSWAHL_NOTWENDIG_KEY, gebiet.getName());
    } else {
      losentscheidFuerWiederholungswahlNotwendig(helper, sortiererDirektkandidaturenImGebiet);
      helper.log(ErgebnisLogEintragArt.WARNUNG, STICHWAHL_NACH_LOSENTSCHEID_NOTWENDIG_KEY, gebiet.getName());
    }

    return helper.getSitzberechnungsergebnis();
  }

  private long getAnzahlStimmen(SitzberechnungProGebietHelper helper, Direktkandidatur direktkandidatur) {
    Long result = helper.getSitzberechnungseingabe().getAnzahlStimmenFuerDirektkandidaturKumuliertBisGebietAufDemKandidaturAntritt(direktkandidatur);

    return NotNull.notNull(result);
  }

  /**
   * Der Sieger der Wahl steht fest. Erzeuge ein Gewaehlt-Objekt.
   */
  private void siegerMitAbsoluterMehrheit(SitzberechnungProGebietHelper helper, AbsoluteMehrheitswahlKandidaturenNachStimmenSortierer sortierer) {
    List<Direktkandidatur> kandidaturenIstGewaehlt = new ArrayList<>();
    kandidaturenIstGewaehlt.add(sortierer.getErstplatzierte().iterator().next());

    long rangIstGewaehlt = ERSTER;
    long rangIstNichtGewaehlt = ERSTER + 1L;

    for (Direktkandidatur kandidaturIstGewaehlt : kandidaturenIstGewaehlt) {
      helper.createGewaehlt(kandidaturIstGewaehlt, GewaehltStatus.GEWAEHLT, rangIstGewaehlt);
    }

    for (Direktkandidatur kandidatur : sortierer.getAlle()) { // ELECT-9466
      if (!kandidaturenIstGewaehlt.contains(kandidatur)) {
        helper.createGewaehlt(kandidatur, GewaehltStatus.NICHT_GEWAEHLT, rangIstNichtGewaehlt);
      }
    }
  }

  /**
   * Die Teilnehmer an der Stichwahl stehen fest. Erzeuge zwei Gewaehlt-Objekte.
   */
  private void teilnehmerStichwahlEindeutig(SitzberechnungProGebietHelper helper, AbsoluteMehrheitswahlKandidaturenNachStimmenSortierer sortierer) {
    List<Direktkandidatur> kandidaturenIstInStichwahl = new ArrayList<>();
    kandidaturenIstInStichwahl.addAll(sortierer.getErstplatzierte());

    long rangIstInStichwahl = 1L;
    boolean rangIstInStichwahlErhoehen = false;

    if (kandidaturenIstInStichwahl.size() < 2) {
      rangIstInStichwahlErhoehen = true;

      kandidaturenIstInStichwahl.addAll(sortierer.getZweitplatzierte());
    }

    for (Direktkandidatur kandidaturIstInStichwahl : kandidaturenIstInStichwahl) {
      helper.createGewaehlt(kandidaturIstInStichwahl, GewaehltStatus.STICHWAHL, rangIstInStichwahl);

      if (rangIstInStichwahlErhoehen) {
        rangIstInStichwahl++;
      }
    }

    Long rangIstNichtGewaehlt = kandidaturenIstInStichwahl.size() + 1L;

    for (Direktkandidatur kandidatur : sortierer.getAlle()) { // ELECT-9466
      if (!kandidaturenIstInStichwahl.contains(kandidatur)) {
        helper.createGewaehlt(kandidatur, GewaehltStatus.NICHT_GEWAEHLT, rangIstNichtGewaehlt);
      }
    }
  }

  /**
   * Die Teilnehmer an der Stichwahl müssen durch Losentscheid festgestellt werden. Erzeuge einen Konflikt und mindestens drei Gewaehlt-Objekte.
   */
  private void losentscheidFuerWiederholungswahlNotwendig(SitzberechnungProGebietHelper helper, AbsoluteMehrheitswahlKandidaturenNachStimmenSortierer sortierer) {
    // Es bleibt nur der Fall eines Losentscheids um die Teilnahme an der Stichwahl unter den Zweitplatzierten
    List<Direktkandidatur> erstplatzierte = sortierer.getErstplatzierte();
    List<Direktkandidatur> zweitplatzierte = sortierer.getZweitplatzierte();

    checkErstUndZweitplatzierte(sortierer, erstplatzierte, zweitplatzierte);

    List<Direktkandidatur> kandidaturenIstInStichwahl = new ArrayList<>();
    kandidaturenIstInStichwahl.addAll(erstplatzierte);
    kandidaturenIstInStichwahl.addAll(zweitplatzierte);

    long rangIstInStichwahl = ERSTER;
    long rangIstInStichwahlFallsGelost = ZWEITER;
    long rangIstNichtGewaehlt = ZWEITER + 1L;

    for (Direktkandidatur kandidaturIstInStichwahl : erstplatzierte) {
      helper.createGewaehlt(kandidaturIstInStichwahl, GewaehltStatus.STICHWAHL, rangIstInStichwahl);
    }

    Set<PersonendatenPK> kandidaturenPKsZurAuswahl = new HashSet<>();

    for (Direktkandidatur kandidaturIstInStichwahlFallsGelost : zweitplatzierte) {
      kandidaturenPKsZurAuswahl.add(kandidaturIstInStichwahlFallsGelost.getPk());
    }

    ErgebnisKonflikt konflikt = entscheideOderErzeugeKonflikt(helper, kandidaturenPKsZurAuswahl, zweitplatzierte);

    if (konflikt.isGeloest()) {
      for (Direktkandidatur kandidaturIstInStichwahlFallsGelost : zweitplatzierte) {
        AbsoluteMehrheitswahlErgebnisKonfliktCustomData customData = konflikt.getCustomData(AbsoluteMehrheitswahlErgebnisKonfliktCustomData.class);

        if (kandidaturIstInStichwahlFallsGelost.getPk().equals(customData.getGelosteKandidaturPK())) {
          helper.createGewaehlt(kandidaturIstInStichwahlFallsGelost, GewaehltStatus.STICHWAHL, rangIstInStichwahlFallsGelost);
        } else {
          helper.createGewaehlt(kandidaturIstInStichwahlFallsGelost, GewaehltStatus.NICHT_GEWAEHLT, rangIstNichtGewaehlt);
        }
      }
    }

    for (Direktkandidatur kandidatur : sortierer.getAlle()) { // ELECT-9466
      if (!kandidaturenIstInStichwahl.contains(kandidatur)) {
        helper.createGewaehlt(kandidatur, GewaehltStatus.NICHT_GEWAEHLT, rangIstNichtGewaehlt);
      }
    }
  }

  private ErgebnisKonflikt entscheideOderErzeugeKonflikt(SitzberechnungProGebietHelper helper, Set<PersonendatenPK> kandidaturenPKsZurAuswahl, List<Direktkandidatur> zweitplatzierte) {
    ErgebnisKonflikt konflikt = createErgebnisKonflikt(helper, kandidaturenPKsZurAuswahl, zweitplatzierte);
    ErgebnisKonfliktLoeser konfliktLoeser = new ErgebnisKonfliktLoeser();
    return helper.loeseKonflikt(konflikt, konfliktLoeser, false);
  }

  protected ErgebnisKonflikt createErgebnisKonflikt(SitzberechnungProGebietHelper helper, Set<PersonendatenPK> kandidaturenPKsZurAuswahl, List<Direktkandidatur> zweitplatzierte) {
    AbsoluteMehrheitswahlErgebnisKonfliktCustomData customData = new AbsoluteMehrheitswahlErgebnisKonfliktCustomData();
    customData.setBeteiligteKandidaturenPKs(kandidaturenPKsZurAuswahl);

    String konfliktBeschreibung = getBeschreibungFuerKonflikt(helper, zweitplatzierte);
    ErgebnisKonflikt neuerKonflikt = helper.createKonflikt(customData, ErgebnisKonfliktArt.LOSENTSCHEID_DIREKTKANDIDAT, konfliktBeschreibung);
    return neuerKonflikt;
  }

  private void checkErstUndZweitplatzierte(AbsoluteMehrheitswahlKandidaturenNachStimmenSortierer sortierer, List<Direktkandidatur> erstplatzierte, List<Direktkandidatur> zweitplatzierte) {
    if (erstplatzierte.size() != 1) {
      throw new IllegalStateException("Unerwarteter Zustand beim AbsoluteMehrheitswahlSitzberechnungsalgorithmus: Losentscheid aber kein eindeutiger Erstplatzierter: " + sortierer);
    }
    if (zweitplatzierte.size() < 2) {
      throw new IllegalStateException("Unerwarteter Zustand beim AbsoluteMehrheitswahlSitzberechnungsalgorithmus: Losentscheid aber weniger als zwei Zweitplatzierte: " + sortierer);
    }
  }

  public String getBeschreibungFuerKonflikt(SitzberechnungProGebietHelper helper, List<Direktkandidatur> kandidaturen) {
    return helper.getMessages().lokalisiere(AbsoluteMehrheitswahlLosentscheidTyp.STIMMENGLEICHHEIT_DER_ZWEITPLATZIERTEN);
  }
}
