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

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.inject.Inject;

import de.ivu.elect.business.ergebnis.boundary.ErgebnisDAO;
import de.ivu.elect.business.ergebnis.boundary.StimmenDAO;
import de.ivu.elect.business.ergebnis.entity.Ergebnis;
import de.ivu.elect.business.ergebnis.entity.Ergebnisart;
import de.ivu.elect.business.ergebnis.entity.Stimmen;
import de.ivu.elect.business.ergebnis.entity.Stimmenart;
import de.ivu.elect.business.gebietsbaum.boundary.WahldurchfuehrungsgebietDAO;
import de.ivu.elect.business.gebietsbaum.entity.Gebiet;
import de.ivu.elect.business.gebietsbaum.entity.Wahldurchfuehrungsgebiet;
import de.ivu.elect.business.stimmeingabe.boundary.ErgebniseingangDAO;
import de.ivu.elect.business.stimmeingabe.boundary.StimmeingabeErfassenDescriptionResolver;
import de.ivu.elect.business.stimmeingabe.control.StimmeingabeKopfzahlKonfiguration;
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.StimmeingabeGruppeStimmen;
import de.ivu.elect.business.stimmeingabe.entity.StimmeingabeGruppeStimmenErfassenModel;
import de.ivu.elect.business.stimmeingabe.entity.StimmeingabeKopfzahl;
import de.ivu.elect.business.stimmeingabe.entity.StimmeingabeKopfzahlErfassenModel;
import de.ivu.elect.business.stimmeingabe.entity.StimmeingabeListenkandidaturStimmenErfassenModel;
import de.ivu.elect.business.stimmeingabe.entity.StimmeingabePlausiFehlerart;
import de.ivu.elect.business.stimmeingabe.entity.StimmeingabePlausiTyp;
import de.ivu.elect.business.stimmzettel.boundary.AnzahlStimmenProWaehlerBestimmer;
import de.ivu.elect.business.stimmzettel.boundary.EntityAufStimmzettelDatenDAOFactory;
import de.ivu.elect.business.stimmzettel.entity.EntityAufStimmzettelDaten;
import de.ivu.elect.business.wahl.entity.Wahl;
import de.ivu.elect.business.wahl.entity.WahlartErgebnisart;
import de.ivu.elect.business.wahlvorschlag.boundary.AbstractDirektkandidatDAO;
import de.ivu.elect.business.wahlvorschlag.boundary.AbstractListeDAO;
import de.ivu.elect.business.wahlvorschlag.boundary.DirektkandidatDAO;
import de.ivu.elect.business.wahlvorschlag.boundary.ListeDAO;
import de.ivu.elect.business.wahlvorschlag.boundary.WahlvorschlagDAO;
import de.ivu.elect.business.wahlvorschlag.entity.Liste;
import de.ivu.elect.jee.faces.Messages;

public abstract class AbstractErgebniseingangPlausiBuilder extends AbstractPlausiBuilder implements Serializable {

  private static final long serialVersionUID = 1L;

  private static final String MSG_KEY_PREFIX_ANZAHL_STIMMEN_PRO_WAEHLER_MAXIMAL_NICHT_HINTERLEGT = "stimmeingabe.create.action.anzahl.stimmen.pro.waehler.maximal.nicht.hinterlegt.message";

  private static final String MSG_KEY_ANZAHL_STIMMEN_PRO_WAEHLER_PRO_LISTENKANDIDATUR_MAXIMAL_NICHT_HINTERLEGT = "stimmeingabe.create.action.anzahl.stimmen.pro.waehler.pro.listenkandidatur.maximal.nicht.hinterlegt.message";

  private static final String MSG_KEY_ANZAHL_STIMMEN_PRO_WAEHLER_PRO_DIREKTKANDIDATUR_MAXIMAL_NICHT_HINTERLEGT = "stimmeingabe.create.action.anzahl.stimmen.pro.waehler.pro.direktkandidatur.maximal.nicht.hinterlegt.message";

  @Inject
  protected transient ErgebniseingangDAO ergebniseingangDAO;

  @Inject
  protected transient ErgebnisDAO ergebnisDAO;

  @Inject
  protected transient StimmenDAO stimmenDAO;

  @Inject
  protected transient WahlvorschlagDAO wahlvorschlagDAO;

  @Inject
  protected transient DirektkandidatDAO direktkandidatDAO;

  @Inject
  protected transient ListeDAO listeDAO;

  @Inject
  protected transient EntityAufStimmzettelDatenDAOFactory entityAufStimmzettelDatenDAOFactory;

  @Inject
  protected transient AnzahlStimmenProWaehlerBestimmer anzahlStimmenProWaehlerBestimmer;

  @Inject
  protected transient WahldurchfuehrungsgebietDAO wahldurchfuehrungsgebietDAO;

  protected AbstractErgebniseingangPlausiBuilder() {
    super();
  }

  @Deprecated // nur für Tests
  protected AbstractErgebniseingangPlausiBuilder(Messages messages, StimmeingabeErfassenDescriptionResolver stimmeingabeErfassenDescriptionResolver,
      StimmeingabeKopfzahlKonfiguration stimmeingabeKopfzahlKonfiguration) {
    super(messages, stimmeingabeErfassenDescriptionResolver, stimmeingabeKopfzahlKonfiguration);
  }

  public void checkPreConditions(Gebiet gebiet, WahlartErgebnisart ergebnisart, AbstractDirektkandidatDAO direktkandidatDAO, AbstractListeDAO listeDAO) {
    // ELECT-8960, to be overridden...
  }

  @Override
  public void fillStimmeingabePlausis(Ergebniseingang ergebniseingang) {
    StimmeingabePlausiBuilderContext context = createContext(ergebniseingang);

    fillStimmeingabePlausis(context, ergebniseingang.getStimmeingabeKopfzahlErfassenModel(), ergebniseingang.getStimmeingabeGruppeStimmenErfassenModel(),
        ergebniseingang.getStimmeingabeListenkandidaturStimmenErfassenModels());

    logStimmeingabePlausis(context, ergebniseingang.getStimmeingaben());
  }

  protected abstract void fillStimmeingabePlausis(StimmeingabePlausiBuilderContext context, @Nullable StimmeingabeKopfzahlErfassenModel kopfzahlErfassenModel,
      @Nullable StimmeingabeGruppeStimmenErfassenModel gruppeStimmenErfassenModel, Collection<StimmeingabeListenkandidaturStimmenErfassenModel> listenkandidaturStimmenErfassenModels);

  protected void fillAllgemeinePlausis(StimmeingabePlausiBuilderContext context) {
    for (Stimmeingabe stimmeingabe : context.getStimmeingabenZuPlausibilisieren()) {
      if (stimmeingabe.isAnzahlEingabeZuKonvertieren()) {
        stimmeingabe.addStimmeingabePlausi(new StimmeingabeAnzahlKonvertierbarPlausi(
            findStimmeingabePlausiKonfigurationOrDefaults(context, StimmeingabePlausiTyp.STIMMEINGABE_ANZAHL_KONVERTIERBAR, StimmeingabePlausiFehlerart.ERROR), getMessages()));
      }
    }
  }

  protected void fillGruppeStimmenVergleichVorlaeufigEndgueltigPlausis(StimmeingabeGruppeStimmenErfassenModel model, StimmeingabePlausiBuilderContext context) {
    for (Stimmeingabe stimmeingabeEndgueltig : model.getStimmeingabenPolitischeGruppen()) {
      Stimmeingabe stimmeingabeVorlaeufig = context.getVorlaeufigeStimmeingabePerTk(stimmeingabeEndgueltig.getTk());

      if (stimmeingabeVorlaeufig != null) {
        addPlausi(stimmeingabeEndgueltig,
            new StimmeingabeAnzahlEndgueltigGleichStimmeingabeAnzahlVorlaeufigPlausi(StimmeingabeAnzahlReference.GRUPPE_STIMMEN, stimmeingabeVorlaeufig,
                findStimmeingabePlausiKonfigurationOrDefaults(context, StimmeingabePlausiTyp.STIMMEINGABE_ANZAHL_ENDGUELTIG_GLEICH_STIMMEINGABE_ANZAHL_VORLAEUFIG, StimmeingabePlausiFehlerart.WARNING),
                getMessages()),
            context);
      }
    }
  }

  protected Collection<Stimmeingabe> findGueltigeStimmeingabenAllerWahlvorschlaege(StimmeingabePlausiBuilderContext context) {
    Collection<Stimmeingabe> result = new ArrayList<>();

    for (Stimmeingabe stimmeingabe : context.getErgebniseingang().getStimmeingaben()) {
      if (!stimmeingabe.isZwischensummeEingabe() && stimmeingabe.isWahlvorschlagStimmeingabe()) {
        result.add(stimmeingabe);
      }
    }

    return result;
  }

  protected Collection<Stimmeingabe> findGueltigeDirektstimmenEingabenAllerWahlvorschlaege(StimmeingabePlausiBuilderContext context) {
    return findGueltigeStimmeingabenAllerWahlvorschlaege(Stimmenart.DIREKTSTIMME, context);
  }

  protected Collection<Stimmeingabe> findGueltigeListenstimmenEingabenAllerWahlvorschlaege(StimmeingabePlausiBuilderContext context) {
    return findGueltigeStimmeingabenAllerWahlvorschlaege(Stimmenart.LISTENSTIMME, context);
  }

  private Collection<Stimmeingabe> findGueltigeStimmeingabenAllerWahlvorschlaege(Stimmenart stimmenart, StimmeingabePlausiBuilderContext context) {
    Collection<Stimmeingabe> result = new ArrayList<>();

    for (Stimmeingabe stimmeingabe : context.getErgebniseingang().getStimmeingaben()) {
      if (stimmeingabe.isWahlvorschlagStimmeingabe() && stimmenart.equals(stimmeingabe.getStimmenart())) {
        result.add(stimmeingabe);
      }
    }

    return result;
  }

  protected Collection<Stimmeingabe> findGueltigeStimmeingabenAllerListenkandidaturen(StimmeingabePlausiBuilderContext context) {
    Collection<Stimmeingabe> result = new ArrayList<>();

    for (Stimmeingabe stimmeingabe : context.getErgebniseingang().getStimmeingaben()) {
      if (stimmeingabe.hasListenkandidatur()) {
        result.add(stimmeingabe);
      }
    }

    return result;
  }

  protected void addWarnungenDirektListenstimmen(StimmeingabeGruppeStimmenErfassenModel model, StimmeingabeKopfzahlErfassenModel kopfzahlErfassenModel, StimmeingabePlausiBuilderContext context) {
    if (model.hasStimmenGruppeUngueltige()) {
      StimmeingabeGruppeStimmen stimmeingabeUngueltigeStimmen = model.getStimmenGruppeUngueltige();

      // Ungültige <= 5 % * Wähler
      kopfzahlErfassenModel.getWaehlerGesamtStimmeingabe().ifPresent(stimmeingabeWaehlerGesamt -> {
        addPlausiDirektstimme(stimmeingabeUngueltigeStimmen,
            new UngueltigeStimmenAnzahlKleinerGleichWaehlerGesamtAnzahlAnteilPlausi(stimmeingabeWaehlerGesamt,
                findStimmeingabePlausiKonfigurationOrDefaults(context, StimmeingabePlausiTyp.UNGUELTIGE_DIREKTSTIMMEN_ANZAHL_KLEINER_GLEICH_WAEHLER_GESAMT_ANZAHL_ANTEIL,
                    StimmeingabePlausiFehlerart.WARNING, new StimmeingabeAnzahlComparedToStimmeingabeAnzahlAnteilPlausiCustomData(0.05D)),
                getMessages()),
            context);
        addPlausiListenstimme(stimmeingabeUngueltigeStimmen,
            new UngueltigeStimmenAnzahlKleinerGleichWaehlerGesamtAnzahlAnteilPlausi(stimmeingabeWaehlerGesamt,
                findStimmeingabePlausiKonfigurationOrDefaults(context, StimmeingabePlausiTyp.UNGUELTIGE_LISTENSTIMMEN_ANZAHL_KLEINER_GLEICH_WAEHLER_GESAMT_ANZAHL_ANTEIL,
                    StimmeingabePlausiFehlerart.WARNING, new StimmeingabeAnzahlComparedToStimmeingabeAnzahlAnteilPlausiCustomData(0.05D)),
                getMessages()),
            context);
      });
    }
  }

  protected void fillGruppeStimmenVergleichDirektstimmenListenstimmenPlausi(StimmeingabeGruppeStimmenErfassenModel model, StimmeingabeKopfzahlErfassenModel kopfzahlErfassenModel,
      StimmeingabePlausiBuilderContext context) {
    if (isWaehlerGesamtZuPlausibilisieren(kopfzahlErfassenModel)) {
      StimmeingabeKopfzahl kopfzahlWaehlerGesamt = kopfzahlErfassenModel.getWaehlerGesamt();

      Collection<Stimmeingabe> stimmeingabenGruppeDirektstimmen = model.getStimmeingabenPolitischeGruppenByStimmenart(Stimmenart.DIREKTSTIMME);
      Collection<Stimmeingabe> stimmeingabenGruppeListenstimmen = model.getStimmeingabenPolitischeGruppenByStimmenart(Stimmenart.LISTENSTIMME);

      addPlausiKopfzahl(kopfzahlWaehlerGesamt,
          new GruppeDirektstimmenAnzahlenUngleichGruppeListenstimmenAnzahlenPlausi(stimmeingabenGruppeDirektstimmen, stimmeingabenGruppeListenstimmen,
              findStimmeingabePlausiKonfigurationOrDefaults(context, StimmeingabePlausiTyp.GRUPPE_DIREKTSTIMMEN_ANZAHLEN_UNGLEICH_GRUPPE_LISTENSTIMMEN_ANZAHLEN, StimmeingabePlausiFehlerart.WARNING),
              getMessages()),
          context);
    }
  }

  protected List<Stimmen> getStimmenVorperiode(Ergebniseingang ergebniseingang) {
    List<Ergebnis> ergebnisseVorperiode = ergebnisDAO.findByWahlAndErgebnisart(ergebniseingang.getWahldurchfuehrung().getWahlIfWahl(), Ergebnisart.VORPERIODE);

    if (ergebnisseVorperiode.isEmpty()) {
      return new ArrayList<>();
    }

    return stimmenDAO.findByErgebnisAndGebietNoCache(ergebnisseVorperiode.get(0), ergebniseingang.getGebiet());
  }

  private StimmeingabePlausiBuilderContext createContext(Ergebniseingang ergebniseingang) {
    return new StimmeingabePlausiBuilderContext(ergebniseingang, getGueltigenVorlaeufigenErgebniseingang(ergebniseingang),
        new WahlvorschlaegeSupplier(ergebniseingang.getWahldurchfuehrungsgebiet().getWahl(), wahlvorschlagDAO), getEntityAufStimmzettelDaten(), getWahlParameterLoader(),
        getFindByErfasstFuerGebiete());
  }

  @Nonnull
  private Function<Gebiet, List<Wahldurchfuehrungsgebiet>> getFindByErfasstFuerGebiete() {
    if (wahldurchfuehrungsgebietDAO == null) {
      return x -> Collections.emptyList(); // tests
    }
    return wahldurchfuehrungsgebietDAO::findByErfasstFuerGebiete;
  }

  @Nullable
  protected Ergebniseingang getGueltigenVorlaeufigenErgebniseingang(Ergebniseingang ergebniseingang) {
    return ergebniseingang.getWahldurchfuehrungsgebiet().getLetzterErgebniseingangAktuellInErgebnisUebernehmbar(ergebniseingangDAO, Ergebniseingangsart.VORLAEUFIG);
  }

  protected EntityAufStimmzettelDaten getEntityAufStimmzettelDaten() {
    return entityAufStimmzettelDatenDAOFactory.getEntityAufStimmzettelDaten();
  }

  protected void addMessageAnzahlStimmenProWaehlerNichtHinterlegt(Gebiet gebiet, WahlartErgebnisart ergebnisart) {
    String msgKey = MSG_KEY_PREFIX_ANZAHL_STIMMEN_PRO_WAEHLER_MAXIMAL_NICHT_HINTERLEGT;

    Wahl wahl = gebiet.getWahl();

    if (wahl != null && wahl.getWahlart() != null) {
      msgKey += "." + wahl.getWahlart().getAnzahlStimmenProWaehlerArt().name();
    }

    getMessages().addErrorMessage(msgKey);
  }

  protected Optional<Long> resolveAnzahlStimmenProWaehlerMaximalConfigured(StimmeingabePlausiBuilderContext context) {
    return resolveAnzahlStimmenProWaehlerMaximalConfigured(context.getGebiet(), context.getErgebniseingangsart());
  }

  protected Optional<Long> resolveAnzahlStimmenProWaehlerMaximalConfigured(Gebiet gebiet, WahlartErgebnisart ergebnisart) {
    return fixNonPositiveToEmpty(getAnzahlStimmenProWaehlerBestimmer().getAnzahlStimmenProWaehlerMaximal(gebiet));
  }

  protected void addMessageAnzahlStimmenProWaehlerProDirektkandidaturMaximalNichtHinterlegt(Gebiet gebiet, WahlartErgebnisart ergebnisart) {
    getMessages().addErrorMessage(MSG_KEY_ANZAHL_STIMMEN_PRO_WAEHLER_PRO_DIREKTKANDIDATUR_MAXIMAL_NICHT_HINTERLEGT);
  }

  protected Optional<Long> resolveAnzahlStimmenProWaehlerProDirektkandidaturMaximalConfigured(StimmeingabePlausiBuilderContext context) {
    return resolveAnzahlStimmenProWaehlerProDirektkandidaturMaximalConfigured(context.getGebiet(), context.getErgebniseingangsart());
  }

  protected Optional<Long> resolveAnzahlStimmenProWaehlerProDirektkandidaturMaximalConfigured(Gebiet gebiet, WahlartErgebnisart ergebnisart) {
    return resolveAnzahlStimmenProWaehlerProDirektkandidaturMaximalConfigured(gebiet, ergebnisart, direktkandidatDAO);
  }

  protected Optional<Long> resolveAnzahlStimmenProWaehlerProDirektkandidaturMaximalConfigured(Gebiet gebiet, WahlartErgebnisart ergebnisart, AbstractDirektkandidatDAO direktkandidatDAO) {
    return fixNonPositiveToEmpty(getAnzahlStimmenProWaehlerBestimmer().getAnzahlStimmenProWaehlerProDirektkandidaturMaximal(gebiet, direktkandidatDAO::findByGebiet));
  }

  protected void addMessageAnzahlStimmenProWaehlerProListenkandidaturMaximalNichtHinterlegt(Gebiet gebiet, WahlartErgebnisart ergebnisart) {
    getMessages().addErrorMessage(MSG_KEY_ANZAHL_STIMMEN_PRO_WAEHLER_PRO_LISTENKANDIDATUR_MAXIMAL_NICHT_HINTERLEGT);
  }

  protected Optional<Long> resolveAnzahlStimmenProWaehlerProListenkandidaturMaximalConfigured(StimmeingabePlausiBuilderContext context) {
    return resolveAnzahlStimmenProWaehlerProListenkandidaturMaximalConfigured(context.getGebiet(), context.getErgebniseingangsart());
  }

  protected Optional<Long> resolveAnzahlStimmenProWaehlerProListenkandidaturMaximalConfigured(Gebiet gebiet, WahlartErgebnisart ergebnisart) {
    return resolveAnzahlStimmenProWaehlerProListenkandidaturMaximalConfigured(gebiet, ergebnisart, listeDAO);
  }

  protected Optional<Long> resolveAnzahlStimmenProWaehlerProListenkandidaturMaximalConfigured(Gebiet gebiet, WahlartErgebnisart ergebnisart, AbstractListeDAO listeDAO) {
    return fixNonPositiveToEmpty(getAnzahlStimmenProWaehlerBestimmer().getAnzahlStimmenProWaehlerProListenkandidaturMaximal(gebiet, listeDAO::findByGebiet));
  }

  protected Optional<Long> resolveAnzahlStimmenProWaehlerFuerListeMaximal(StimmeingabePlausiBuilderContext context, Stimmeingabe stimmeingabe) {
    Collection<Liste> alleListen = context.getEntityAufStimmzettelDaten().collectListenInWahl(stimmeingabe.getWahl());

    try {
      return resolveAnzahlStimmenProWaehlerFuerListeMaximal(context, stimmeingabe.getGruppeImGebiet().getListeAufStimmzettelInUebergeordneterGebietshierarchie(alleListen));
    } catch (Exception ignored) {
    }

    return Optional.empty();
  }

  protected Optional<Long> resolveAnzahlStimmenProWaehlerFuerListeMaximal(StimmeingabePlausiBuilderContext context, Liste liste) {
    return fixNonPositiveToEmpty(getAnzahlStimmenProWaehlerBestimmer().getAnzahlStimmenProWaehlerMaximal(liste));
  }

  protected AnzahlStimmenProWaehlerBestimmer getAnzahlStimmenProWaehlerBestimmer() {
    return anzahlStimmenProWaehlerBestimmer;
  }

  protected Optional<Long> fixNonPositiveToEmpty(Long value) {
    if (value == null || value < 1L) {
      return Optional.empty();
    }

    return Optional.of(value);
  }
}
