
package de.ivu.elect.business.stimmeingabe.control.fiktive;

import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;
import static org.apache.commons.lang3.StringUtils.isNotBlank;

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.Map.Entry;
import java.util.Set;
import java.util.function.Predicate;

import javax.inject.Inject;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.ivu.elect.business.gebietsbaum.entity.Wahldurchfuehrungsgebiet;
import de.ivu.elect.business.stimmeingabe.control.PlausiBuilder;
import de.ivu.elect.business.stimmeingabe.control.StimmeingabePlausiBuilderResolver;
import de.ivu.elect.business.stimmeingabe.control.plausi.StimmeingabePlausiFiktiveWerteFehlerNichtBehebbarException;
import de.ivu.elect.business.stimmeingabe.control.plausi.StimmeingabePlausiResult;
import de.ivu.elect.business.stimmeingabe.entity.Ergebniseingang;
import de.ivu.elect.business.stimmeingabe.entity.Ergebniseingangsart;
import de.ivu.elect.business.stimmeingabe.entity.Stimmeingabe;
import de.ivu.elect.business.stimmeingabe.entity.ZwischensummeEingabe;
import de.ivu.elect.util.ThrowableUtil;

public class ErgebniseingangPlausiFehlerBeheber {

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

  // HINT: Anzahl Durchläufe beschränken, um Endlosschleifen zu vermeiden
  private static final int ANZAHL_DURCHLAEUFE_MAX_AUFFUELLEN_MIT_SOLL_WERTEN = 10; // HINT: Plausis sind derzeit höchstens 5-stufig voneinander abhängig.

  private static final int ANZAHL_DURCHLAEUFE_MAX_STIMMEINGABE_PLAUSI_FIKTIVE_WERTE_FEHLER_BEHEBER = 10; // HINT: Je komplexer die Wahl, desto komplexer die Zusammenhänge zwischen den Plausis -- das braucht, um alle Fehler auszubügeln

  private static final Predicate<Stimmeingabe> STIMMEINGABE_IS_ZWISCHENSUMME = Stimmeingabe::isZwischensummeEingabe;

  private static final Predicate<Stimmeingabe> STIMMEINGABE_IS_INITIAL_LEER = stimmeingabe -> !stimmeingabe.hasAnzahlGroesserGleichNull();

  @Inject
  private transient StimmeingabePlausiBuilderResolver stimmeingabePlausiBuilderResolver;

  public ErgebniseingangPlausiFehlerBeheber() {
    super();
  }

  public void behebePlausiFehler(Ergebniseingang ergebniseingang) {
    PlausiBuilder plausiBuilder = getStimmeingabePlausiBuilder(ergebniseingang);
    plausiBuilder.fillStimmeingabePlausis(ergebniseingang);

    Predicate<Stimmeingabe> stimmeingabeKorrigierbarPredicate = STIMMEINGABE_IS_ZWISCHENSUMME.or(STIMMEINGABE_IS_INITIAL_LEER); // ELECT-7537

    behebePlausiFehlerByAuffuellenMitSollWerten(ergebniseingang, stimmeingabeKorrigierbarPredicate);
    behebePlausiFehlerByStimmeingabePlausiFiktiveWerteFehlerBeheber(ergebniseingang);
  }

  public void behebePlausiFehlerByAuffuellenLeererStimmeingabenMitSollWerten(Ergebniseingang ergebniseingang) {
    behebePlausiFehlerByAuffuellenLeererStimmeingabenMitSollWerten(ergebniseingang, ignored -> true);
  }

  public void behebePlausiFehlerByAuffuellenLeererStimmeingabenMitSollWerten(Ergebniseingang ergebniseingang, Predicate<Stimmeingabe> customPredicate) {
    Predicate<Stimmeingabe> stimmeingabeKorrigierbarPredicate = STIMMEINGABE_IS_ZWISCHENSUMME.or(STIMMEINGABE_IS_INITIAL_LEER.and(customPredicate)); // ELECT-13947, ELECT-7530

    behebePlausiFehlerByAuffuellenMitSollWerten(ergebniseingang, stimmeingabeKorrigierbarPredicate);
  }

  private void behebePlausiFehlerByAuffuellenMitSollWerten(Ergebniseingang ergebniseingang, Predicate<Stimmeingabe> stimmeingabeKorrigierbarPredicate) {
    Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet = ergebniseingang.getWahldurchfuehrungsgebiet();
    Ergebniseingangsart ergebniseingangsart = ergebniseingang.getErgebniseingangsart();

    Collection<Stimmeingabe> stimmeingaben = ergebniseingang.getStimmeingaben();
    Collection<Stimmeingabe> stimmeingabenKorrigierbar = stimmeingaben.stream() //
        .filter(stimmeingabeKorrigierbarPredicate) //
        .collect(toSet());

    int anzahlDurchlaeufeMax = ANZAHL_DURCHLAEUFE_MAX_AUFFUELLEN_MIT_SOLL_WERTEN;
    int anzahlDurchlaeufe = 0;

    log("Versuche Stimmeingaben mit Soll-Werten der Plausis aufzufüllen [gebiet = {}, ergebniseingangsart = {}, stimmeingaben = {}, stimmeingaben korrigierbar = {}, durchlaeufe max = {}] - START",
        wahldurchfuehrungsgebiet.getNummer(), ergebniseingangsart, stimmeingaben.size(), stimmeingabenKorrigierbar.size(), anzahlDurchlaeufeMax);

    for (Stimmeingabe stimmeingabe : stimmeingabenKorrigierbar) {
      log("\t  Stimmeingabe korrigierbar [id = {}, gebiet = {}, ergebniseingangsart = {}, gruppe = {}, stimmenart = {}, listenkandidatur = {}, zwischensumme = {}, wert = {}]", stimmeingabe,
          stimmeingabe.getAnzahlEingabe());
    }

    FehlerBeheberResult ergebnisDurchlauf;

    do {
      anzahlDurchlaeufe++;
      ergebnisDurchlauf = plausibilisiereErgebniseingangUndSetzeStimmeingabenAufAnzahlSollWert(anzahlDurchlaeufe, ergebniseingang, stimmeingabenKorrigierbar);

      if (FehlerBeheberResult.NICHTS_GEAENDERT.equals(ergebnisDurchlauf)) {
        // HINT: Wenn durch Auffüllen der Sollwerte nichts erreicht wurde, ergänzen wir die Zahl 0, da so einige Summenplausis erst funktionieren.
        ergebnisDurchlauf = setzeLeereStimmeingabenAufAnzahlNull(anzahlDurchlaeufe, ergebniseingang);
      }
    } while (FehlerBeheberResult.WERT_GEAENDERT.equals(ergebnisDurchlauf) && anzahlDurchlaeufe < anzahlDurchlaeufeMax);

    log("Versuche Stimmeingaben mit Soll-Werten der Plausis aufzufüllen [gebiet = {}, ergebniseingangsart = {}, durchlaeufe = {}/{}] - DONE", wahldurchfuehrungsgebiet.getNummer(), ergebniseingangsart,
        anzahlDurchlaeufe, anzahlDurchlaeufeMax);
  }

  private FehlerBeheberResult setzeLeereStimmeingabenAufAnzahlNull(int nummerDurchlauf, Ergebniseingang ergebniseingang) {
    FehlerBeheberResult result = FehlerBeheberResult.NICHTS_GEAENDERT;

    Collection<Stimmeingabe> stimmeingabenRelevant = ergebniseingang.getStimmeingaben().stream() //
        .filter(stimmeingabe -> !stimmeingabe.hasAnzahlGroesserGleichNull()) //
        .collect(toList());

    if (!stimmeingabenRelevant.isEmpty()) {
      log("Versuche nicht erfasste Stimmeingabe mit 0-Werten aufzufüllen [durchlauf = {}]", nummerDurchlauf);
    }

    for (Stimmeingabe stimmeingabe : stimmeingabenRelevant) {
      long anzahlSoll = 0L;

      log("\t      Stimmeingabe GEÄNDERT [id = {}, gebiet = {}, ergebniseingangsart = {}, gruppe = {}, stimmenart = {}, listenkandidatur = {}, zwischensumme = {}, von = {}, nach = {}]", stimmeingabe,
          stimmeingabe.getAnzahlEingabe(), anzahlSoll);

      stimmeingabe.setAnzahl(anzahlSoll);
      result = FehlerBeheberResult.WERT_GEAENDERT;
    }

    return result;
  }

  // ELECTMUC-2243, ELECTMUC-2225
  private FehlerBeheberResult plausibilisiereErgebniseingangUndSetzeStimmeingabenAufAnzahlSollWert(int nummerDurchlauf, Ergebniseingang ergebniseingang,
      Collection<Stimmeingabe> stimmeingabenKorrigierbar) {
    Set<Stimmeingabe> stimmeingabenKorrigiert = new HashSet<>();

    Map<Stimmeingabe, List<StimmeingabePlausiResult>> plausiResultsPerStimmeingabe = plausibilisiere(ergebniseingang);

    if (!plausiResultsPerStimmeingabe.isEmpty()) {
      log("Versuche Stimmeingaben mit Soll-Werten der Plausis aufzufüllen [durchlauf = {}]", nummerDurchlauf);
    }

    for (Entry<Stimmeingabe, List<StimmeingabePlausiResult>> entry : plausiResultsPerStimmeingabe.entrySet()) {
      Stimmeingabe stimmeingabe = entry.getKey();

      List<StimmeingabePlausiResult> plausiResultsRelevant = entry.getValue().stream() //
          .filter(plausiResult -> plausiResult.getStimmeingabePlausiStatus().isBerechneFiktiveUndSollWerte() && plausiResult.hasAnzahlSollAutomatischUebernehmbar()) //
          .collect(toList());

      for (StimmeingabePlausiResult plausiResult : plausiResultsRelevant) {
        long anzahlIst = stimmeingabe.getAnzahl();
        long anzahlSoll = plausiResult.getAnzahlSoll().longValue();

        log("\t                     Plausi [fehlerart = {}, soll-wert = {}, beschreibung = {}]", plausiResult.getStimmeingabePlausiFehlerart(), plausiResult.getAnzahlSollEingabe(),
            plausiResult.getStimmeingabefehlerBeschreibung());

        // NOTE, dass es ein Kriterium geben MUSS, das vermeidet, dass zwei konkurrierende Plausis abwechselnd, also von einem Durchlauf zum nächsten, den IST-Wert abweichend neu setzen.
        // Ein Beispiel bietet die BY LTW mit den endgültigen Briefwahlbezirk-Plausis StimmeingabePlausiTyp#WAEHLER_LISTENSTIMMEN_ZWISCHENSUMME_GLEICH_WAEHLER_GESAMT_ANZAHL vs.
        // StimmeingabePlausiTyp#WAEHLER_LISTENSTIMMEN_ZWISCHENSUMME_GLEICH_GUELTIGE_LISTENSTIMMEN_ANZAHL_PLUS_UNGUELTIGE_LISTENSTIMMEN_ANZAHL...
        // ... der höchste SOLL-Wert gewinnt also momentan, ein manuell zu korrigierender MUSS-Fehler liegt so oder so, also unabhängig vom verwendeten SOLL-Wert, vor.
        boolean anzahlIstKorrigierbar = anzahlIst < anzahlSoll;

        boolean stimmeingabeKorrigierbar = stimmeingabenKorrigierbar.contains(stimmeingabe);
        boolean stimmeingabeBereitsKorrigiert = stimmeingabenKorrigiert.contains(stimmeingabe);

        if (anzahlIstKorrigierbar && stimmeingabeKorrigierbar && !stimmeingabeBereitsKorrigiert) {
          log("\t      Stimmeingabe GEÄNDERT [id = {}, gebiet = {}, ergebniseingangsart = {}, gruppe = {}, stimmenart = {}, listenkandidatur = {}, zwischensumme = {}, von = {}, nach = {}]",
              stimmeingabe, stimmeingabe.getAnzahlEingabe(), anzahlSoll);

          stimmeingabe.setAnzahl(anzahlSoll);
          stimmeingabenKorrigiert.add(stimmeingabe);
        } else {
          log("\tStimmeingabe NICHT GEÄNDERT [id = {}, gebiet = {}, ergebniseingangsart = {}, gruppe = {}, stimmenart = {}, listenkandidatur = {}, zwischensumme = {}, stimmeingabe korrigierbar = {}, stimmeingabe bereits korrigiert = {}, wert = {}, wert korrigierbar = {}]",
              stimmeingabe, stimmeingabeKorrigierbar, stimmeingabeBereitsKorrigiert, stimmeingabe.getAnzahlEingabe(), anzahlIstKorrigierbar);
        }

        log("");
      }
    }

    if (stimmeingabenKorrigiert.isEmpty()) {
      return FehlerBeheberResult.NICHTS_GEAENDERT;
    }

    return FehlerBeheberResult.WERT_GEAENDERT;
  }

  /**
   * ACHTUNG: ÄNDERT Werte von Stimmeingaben, um Fehler zu beheben!
   *
   * @see https://support.elect-it.com/wiki/display/ELECT/Fiktive+Stimmeingaben
   */
  private void behebePlausiFehlerByStimmeingabePlausiFiktiveWerteFehlerBeheber(Ergebniseingang ergebniseingang) {
    Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet = ergebniseingang.getWahldurchfuehrungsgebiet();
    Ergebniseingangsart ergebniseingangsart = ergebniseingang.getErgebniseingangsart();

    Collection<Stimmeingabe> stimmeingaben = ergebniseingang.getStimmeingaben();
    Collection<Stimmeingabe> stimmeingabenKorrigierbar = new HashSet<>(stimmeingaben);

    int anzahlDurchlaeufeMax = ANZAHL_DURCHLAEUFE_MAX_STIMMEINGABE_PLAUSI_FIKTIVE_WERTE_FEHLER_BEHEBER;
    int anzahlDurchlaeufe = 0;

    log("Versuche fiktive Stimmeingaben zu korrigieren [gebiet = {}, ergebniseingangsart = {}, stimmeingaben = {}, stimmeingaben korrigierbar = {}, durchlaeufe max = {}] - START",
        wahldurchfuehrungsgebiet.getNummer(), ergebniseingangsart, stimmeingaben.size(), stimmeingabenKorrigierbar.size(), anzahlDurchlaeufeMax);

    for (Stimmeingabe stimmeingabe : stimmeingabenKorrigierbar) {
      log("\t                        Stimmeingabe korrigierbar [id = {}, gebiet = {}, ergebniseingangsart = {}, gruppe = {}, stimmenart = {}, listenkandidatur = {}, zwischensumme = {}, wert = {}]",
          stimmeingabe.getId(), stimmeingabe.getAnzahlEingabe());
    }

    FehlerBeheberResult ergebnisDurchlauf;

    do {
      anzahlDurchlaeufe++;
      ergebnisDurchlauf = FehlerBeheberResult.NICHTS_GEAENDERT;

      List<StimmeingabePlausiResult> plausiResults = plausibilisiere(ergebniseingang).entrySet().stream().flatMap(entry -> entry.getValue().stream()).collect(toList());

      if (!plausiResults.isEmpty()) {
        log("Versuche fiktive Stimmeingaben zu korrigieren [durchlauf = {}]", anzahlDurchlaeufe);
      }

      for (StimmeingabePlausiResult plausiResult : plausiResults) {
        boolean plausiResultDefiniert = plausiResult.hasStimmeingabe() && (plausiResult.isMussFehler() || plausiResult.isKannFehler()) || isNotBlank(plausiResult.getStimmeingabefehlerBeschreibung());

        if (plausiResultDefiniert) {
          log("\t                                           Plausi [fehlerart = {}, status = {}, fehlerbeheber vorhanden = {}, beschreibung = {}]", plausiResult.getStimmeingabePlausiFehlerart(),
              plausiResult.getStimmeingabePlausiStatus(), plausiResult.hasFiktiveWerteFehlerBeheber(), plausiResult.getStimmeingabefehlerBeschreibung());
        }

        if (plausiResult.getStimmeingabePlausiStatus().isBerechneFiktiveUndSollWerte()) {
          if (plausiResult.hasFiktiveWerteFehlerBeheber()) {
            ergebnisDurchlauf = tryVerwendeFiktiveWerteFehlerBeheber(plausiResult);
          } else if (isStimmeingabeUngueltig(plausiResult)) {
            ergebnisDurchlauf = tryKorrigiereUngueltigeStimmeingabe(plausiResult);
          } else if (plausiResultDefiniert) {
            Stimmeingabe stimmeingabe = plausiResult.getStimmeingabe();

            log("\t                  Stimmeingabe NICHT KORRIGIERBAR [id = {}, gebiet = {}, ergebniseingangsart = {}, gruppe = {}, stimmenart = {}, listenkandidatur = {}, zwischensumme = {}, wert = {}]",
                stimmeingabe, stimmeingabe.getAnzahlEingabe());
          }
        }

        if (plausiResultDefiniert) {
          log("");
        }
      }
    } while (FehlerBeheberResult.WERT_GEAENDERT.equals(ergebnisDurchlauf) && anzahlDurchlaeufe < anzahlDurchlaeufeMax);

    log("Versuche fiktive Stimmeingaben zu korrigieren [gebiet = {}, ergebniseingangsart = {}, durchlaeufe = {}/{}] - DONE", wahldurchfuehrungsgebiet.getNummer(), ergebniseingangsart,
        anzahlDurchlaeufe, anzahlDurchlaeufeMax);
  }

  private FehlerBeheberResult tryKorrigiereUngueltigeStimmeingabe(StimmeingabePlausiResult plausiResult) {
    Stimmeingabe stimmeingabe = plausiResult.getStimmeingabe();
    long anzahl = 0L; // ungültige Werte auf 0 setzen

    log("\t       Stimmeingabe durch Zurücksetzen KORRIGIERT [id = {}, gebiet = {}, ergebniseingangsart = {}, gruppe = {}, stimmenart = {}, listenkandidatur = {}, zwischensumme = {}, von = {}, nach = {}]",
        stimmeingabe, stimmeingabe.getAnzahlEingabe(), anzahl);

    stimmeingabe.setAnzahl(anzahl);

    return FehlerBeheberResult.WERT_GEAENDERT;
  }

  private FehlerBeheberResult tryVerwendeFiktiveWerteFehlerBeheber(StimmeingabePlausiResult plausiResult) {
    Stimmeingabe stimmeingabe = plausiResult.getStimmeingabe();
    String anzahlEingabeInitial = stimmeingabe.getAnzahlEingabe();

    try {
      plausiResult.behebeFiktiveWerteFehler();

      log("\t      Stimmeingabe durch Fehlerbeheber KORRIGIERT [id = {}, gebiet = {}, ergebniseingangsart = {}, gruppe = {}, stimmenart = {}, listenkandidatur = {}, zwischensumme = {}, von = {}, nach = {}, fehlerbeheber = {}]",
          stimmeingabe, anzahlEingabeInitial, stimmeingabe.getAnzahlEingabe(), plausiResult.getFiktiveWerteFehlerBeheber().getClass().getSimpleName());

      return FehlerBeheberResult.WERT_GEAENDERT;
    } catch (StimmeingabePlausiFiktiveWerteFehlerNichtBehebbarException ex) {
      log("\tStimmeingabe durch Fehlerbeheber NICHT KORRIGIERT [id = {}, gebiet = {}, ergebniseingangsart = {}, gruppe = {}, stimmenart = {}, listenkandidatur = {}, zwischensumme = {}, fehler = {}]",
          stimmeingabe, ThrowableUtil.getMessagesAllerGruendeAlsString(ex, " --- caused by: "));
    }

    return FehlerBeheberResult.NICHTS_GEAENDERT;
  }

  private boolean isStimmeingabeUngueltig(StimmeingabePlausiResult plausiResult) {
    return plausiResult.hasStimmeingabe() && plausiResult.getStimmeingabe().hasAnzahlEingabe() && !plausiResult.getStimmeingabe().hasAnzahlGroesserGleichNull();
  }

  private PlausiBuilder getStimmeingabePlausiBuilder(Ergebniseingang ergebniseingang) {
    return getStimmeingabePlausiBuilderResolver().getStimmeingabePlausiBuilder(ergebniseingang);
  }

  private Map<Stimmeingabe, List<StimmeingabePlausiResult>> plausibilisiere(Ergebniseingang ergebniseingang) {
    Map<Stimmeingabe, List<StimmeingabePlausiResult>> result = new HashMap<>();

    for (Stimmeingabe stimmeingabe : ergebniseingang.getStimmeingaben()) {
      result.put(stimmeingabe, stimmeingabe.plausibilisiere());
    }

    return result;
  }

  protected StimmeingabePlausiBuilderResolver getStimmeingabePlausiBuilderResolver() {
    return stimmeingabePlausiBuilderResolver;
  }

  private static void log(String message, Stimmeingabe stimmeingabe, Object... more) {
    List<Object> params = new ArrayList<>();

    params.add(stimmeingabe.getId());
    params.add(stimmeingabe.getGebiet().getNummer());
    params.add(stimmeingabe.getErgebniseingangsart());
    params.add(stimmeingabe.getGruppe().getName());
    params.add(stimmeingabe.getStimmenart());
    params.add(stimmeingabe.hasListenkandidatur() ? stimmeingabe.getListenkandidatur().getFamilienname() : "false");
    params.add(stimmeingabe.isZwischensummeEingabe()
        ? "[index = " + ((ZwischensummeEingabe) stimmeingabe).getSummenIndex() + ", name = " + ((ZwischensummeEingabe) stimmeingabe).getSummenName() + "]"
        : "false");

    if (more != null) {
      Collections.addAll(params, more);
    }

    log(message, params.toArray(new Object[params.size()]));
  }

  private static void log(String message, Object... params) {
    LOG.debug(message, params);
  }

  private enum FehlerBeheberResult {
    WERT_GEAENDERT,
    NICHTS_GEAENDERT
  }
}
