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

import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

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

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

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.gruppeimgebiet.boundary.GruppeImGebietDAO;
import de.ivu.elect.business.gruppeimgebiet.entity.GruppeImGebiet;
import de.ivu.elect.business.kandidatur.boundary.ListenkandidaturDAO;
import de.ivu.elect.business.kandidatur.entity.Listenkandidatur;
import de.ivu.elect.business.stimmeingabe.boundary.StimmeingabeAufGebietMoeglichChecker;
import de.ivu.elect.business.stimmeingabe.control.plausi.StimmeingabePlausiResult;
import de.ivu.elect.business.stimmeingabe.control.validierung.ErgebniseingangValidator;
import de.ivu.elect.business.stimmeingabe.entity.Ergebniseingang;
import de.ivu.elect.business.stimmeingabe.entity.Stimmeingabe;
import de.ivu.elect.business.stimmeingabe.entity.Stimmeingabefehler;
import de.ivu.elect.business.wahl.entity.Wahl;
import de.ivu.elect.business.wahlvorschlag.boundary.ReferendumOptionDAO;
import de.ivu.elect.jee.entity.tk.EntityTK;
import de.ivu.elect.jee.performance.PerformanceLog;
import de.ivu.elect.jee.performance.PerformanceLogTransactionHandling;
import de.ivu.elect.jee.performance.PerformanceStore;
import de.ivu.elect.jee.performance.PhaseIdEnum;
import de.ivu.elect.jee.performance.ViewAccessTarget;
import de.ivu.elect.jee.performance.ViewAccessType;
import de.ivu.elect.jee.performance.ViewType;

@PerformanceLog(viewType = ViewType.STIMMENEINGABE, transactionHandling = PerformanceLogTransactionHandling.IMMEDIATELY)
public class StimmeingabeCreate extends AbstractElectStimmeingabeCreate {

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

  private static final int MAX_LISTENKANDIDATUREN_PER_FINDER = 800;

  @Inject
  protected transient WahldurchfuehrungsgebietDAO wahldurchfuehrungsgebietDAO;

  @Inject
  protected transient GruppeImGebietDAO gruppeImGebietDAO;

  @Inject
  protected transient ListenkandidaturDAO listenkandidaturDAO;

  @Inject
  protected transient ReferendumOptionDAO referendumOptionDAO;

  @Inject
  protected transient StimmeingabeAufGebietMoeglichChecker stimmeingabeAufGebietMoeglichChecker;

  @Inject
  protected transient StimmeingabefehlerFactory stimmeingabefehlerFactory;

  @Inject
  protected transient StimmeingabeCreateKonfiguration stimmeingabeCreateKonfiguration;

  @Inject
  protected transient WahlaufteilenStimmeingabeCreate wahlaufteilenStimmeingabeCreate;

  public StimmeingabeCreate() {
    super();
  }

  public void saveUndKannFehlerFuerBenutzerBestaetigen(Ergebniseingang ergebniseingang, Ergebniseingang letzterErgebniseingangInGebietErwartet, StimmeingabePlausiBuilder stimmeingabePlausiBuilder) {
    if (!letzterErgebniseingangInGebietErwartet.isUngueltigKannFehler()) {
      throw new IllegalStateException(
          "Bestaetigen von Kann-Fehlern nicht moeglich fuer einen Ergebniseingang [pk=" + letzterErgebniseingangInGebietErwartet.getPk() + "] ohne Kann- oder mit Muss-Fehlern.");
    }

    ergebniseingang.setErgebniseingangsherkunft(letzterErgebniseingangInGebietErwartet.getErgebniseingangsherkunft()); // KANN-Fehler bestätigen soll Herkunft nicht anpassen: ELECT-9200

    saveMitLetzterErgebniseingangInGebietPruefungMitStimmeingabePlausibilisierungKannFehlerBestaetigen(ergebniseingang, letzterErgebniseingangInGebietErwartet, stimmeingabePlausiBuilder);
  }

  public void saveMitLetzterErgebniseingangInGebietPruefungMitStimmeingabePlausibilisierung(Ergebniseingang ergebniseingang, @Nullable Ergebniseingang letzterErgebniseingangInGebietErwartet,
      StimmeingabePlausiBuilder stimmeingabePlausiBuilder) {
    boolean letztenErgebniseingangInGebietPruefen = true;
    boolean stimmeingabenPlausibilisieren = true;

    save(ergebniseingang, letzterErgebniseingangInGebietErwartet, letztenErgebniseingangInGebietPruefen, stimmeingabePlausiBuilder, stimmeingabenPlausibilisieren);
  }

  public void saveMitLetzterErgebniseingangInGebietPruefungMitStimmeingabePlausibilisierungKannFehlerBestaetigen(Ergebniseingang ergebniseingang,
      Ergebniseingang letzterErgebniseingangInGebietErwartet, StimmeingabePlausiBuilder stimmeingabePlausiBuilder) {
    boolean letztenErgebniseingangInGebietPruefen = true;

    saveMitStimmeingabePlausibilisierungKannFehlerBestaetigen(ergebniseingang, letzterErgebniseingangInGebietErwartet, letztenErgebniseingangInGebietPruefen, stimmeingabePlausiBuilder);
  }

  public void saveOhneLetzterErgebniseingangInGebietPruefungMitStimmeingabePlausibilisierung(Ergebniseingang ergebniseingang, @Nullable Ergebniseingang letzterErgebniseingangInGebietErwartet,
      StimmeingabePlausiBuilder stimmeingabePlausiBuilder) {
    boolean letztenErgebniseingangInGebietPruefen = false;
    boolean stimmeingabenPlausibilisieren = true;

    save(ergebniseingang, letzterErgebniseingangInGebietErwartet, letztenErgebniseingangInGebietPruefen, stimmeingabePlausiBuilder, stimmeingabenPlausibilisieren);
  }

  public void saveOhneLetzterErgebniseingangInGebietPruefungMitStimmeingabePlausibilisierungKannFehlerBestaetigen(Ergebniseingang ergebniseingang,
      @Nullable Ergebniseingang letzterErgebniseingangInGebietErwartet, StimmeingabePlausiBuilder stimmeingabePlausiBuilder) {
    boolean letztenErgebniseingangInGebietPruefen = false;

    saveMitStimmeingabePlausibilisierungKannFehlerBestaetigen(ergebniseingang, letzterErgebniseingangInGebietErwartet, letztenErgebniseingangInGebietPruefen, stimmeingabePlausiBuilder);
  }

  public void saveOhneLetzterErgebniseingangInGebietPruefungOhneStimmeingabePlausibilisierung(Ergebniseingang ergebniseingang, @Nullable Ergebniseingang letzterErgebniseingangInGebietErwartet,
      StimmeingabePlausiBuilder stimmeingabePlausiBuilder) {
    boolean letztenErgebniseingangInGebietPruefen = false;
    boolean stimmeingabenPlausibilisieren = false;

    save(ergebniseingang, letzterErgebniseingangInGebietErwartet, letztenErgebniseingangInGebietPruefen, stimmeingabePlausiBuilder, stimmeingabenPlausibilisieren);
  }

  private void saveMitStimmeingabePlausibilisierungKannFehlerBestaetigen(Ergebniseingang ergebniseingang, Ergebniseingang letzterErgebniseingangInGebietErwartet,
      boolean letztenErgebniseingangInGebietPruefen, StimmeingabePlausiBuilder stimmeingabePlausiBuilder) {
    boolean stimmeingabenPlausibilisieren = true;

    saveInternal(ergebniseingang, letzterErgebniseingangInGebietErwartet, letztenErgebniseingangInGebietPruefen, stimmeingabePlausiBuilder, stimmeingabenPlausibilisieren,
        getDefaultErgebniseingangValidatoren());

    if (!ergebniseingang.isUngueltigMussFehler() // ELECT-7530
        && (ergebniseingang.isUngueltigKannFehler() || StimmeingabePlausiBuilder.NO_OP_BUILDER.equals(stimmeingabePlausiBuilder))) {
      ergebniseingang.setGueltigKannFehler();
    }

    postSaveInternal(ergebniseingang, stimmeingabePlausiBuilder);
  }

  private void save(Ergebniseingang ergebniseingang, @Nullable Ergebniseingang letzterErgebniseingangInGebietErwartet, boolean letztenErgebniseingangInGebietPruefen,
      StimmeingabePlausiBuilder stimmeingabePlausiBuilder, boolean stimmeingabenPlausibilisieren) {
    save(ergebniseingang, letzterErgebniseingangInGebietErwartet, letztenErgebniseingangInGebietPruefen, stimmeingabePlausiBuilder, stimmeingabenPlausibilisieren,
        getDefaultErgebniseingangValidatoren());
  }

  protected void save(Ergebniseingang ergebniseingang, @Nullable Ergebniseingang letzterErgebniseingangInGebietErwartet, boolean letztenErgebniseingangInGebietPruefen,
      StimmeingabePlausiBuilder stimmeingabePlausiBuilder, boolean stimmeingabenPlausibilisieren, Iterable<ErgebniseingangValidator> ergebniseingangValidatoren) {
    saveInternal(ergebniseingang, letzterErgebniseingangInGebietErwartet, letztenErgebniseingangInGebietPruefen, stimmeingabePlausiBuilder, stimmeingabenPlausibilisieren, ergebniseingangValidatoren);

    postSaveInternal(ergebniseingang, stimmeingabePlausiBuilder);
  }

  private void saveInternal(Ergebniseingang ergebniseingang, @Nullable Ergebniseingang letzterErgebniseingangInGebietErwartet, boolean letztenErgebniseingangInGebietPruefen,
      StimmeingabePlausiBuilder stimmeingabePlausiBuilder, boolean stimmeingabenPlausibilisieren, Iterable<ErgebniseingangValidator> ergebniseingangValidatoren) {
    long millis = System.currentTimeMillis();

    // ELECT-6428
    ergebniseingang.setWahldurchfuehrungsgebiet(wahldurchfuehrungsgebietDAO.attach(ergebniseingang.getWahldurchfuehrungsgebiet()));

    validiereGebiet(ergebniseingang.getWahldurchfuehrungsgebiet());

    long start = System.currentTimeMillis();
    saveErgebniseingang(ergebniseingang, letzterErgebniseingangInGebietErwartet, letztenErgebniseingangInGebietPruefen, ergebniseingangValidatoren);
    if (PerformanceStore.isStatistikAktiviert()) {
      PerformanceStore.addDuration(
          new ViewAccessTarget(ViewType.STIMMENEINGABE, ViewAccessType.GESAMT, PhaseIdEnum.INVOKE_APPLICATION, ViewAccessTarget.SESSION_ID_OHNE,
              getClass().getSimpleName() + "-saveErgebniseingang"), System.currentTimeMillis() - start);
    }

    start = System.currentTimeMillis();

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

    // ELECT-3302
    attachEntities(ergebniseingang, stimmeingaben);
    if (PerformanceStore.isStatistikAktiviert()) {
      PerformanceStore.addDuration(
          new ViewAccessTarget(ViewType.STIMMENEINGABE, ViewAccessType.GESAMT, PhaseIdEnum.INVOKE_APPLICATION, ViewAccessTarget.SESSION_ID_OHNE,
              getClass().getSimpleName() + "-attachEntities"), System.currentTimeMillis() - start);
    }

    start = System.currentTimeMillis();


    for (Stimmeingabe stimmeingabe : stimmeingaben) {
      save(stimmeingabe);
    }

    if (PerformanceStore.isStatistikAktiviert()) {
      PerformanceStore.addDuration(
          new ViewAccessTarget(ViewType.STIMMENEINGABE, ViewAccessType.GESAMT, PhaseIdEnum.INVOKE_APPLICATION, ViewAccessTarget.SESSION_ID_OHNE,
              getClass().getSimpleName() + "-saveStimmeingaben"), System.currentTimeMillis() - start);
    }

    start = System.currentTimeMillis();

    if (stimmeingabenPlausibilisieren) { // WASDE-984, hiermit kann man die Plausibilisierung kurzzeitig ausschalten (z.B. nur für einen Import) ohne den Systemparameter zu ändern
      plausibilisiere(ergebniseingang, stimmeingabePlausiBuilder);

      bestimmeFehlerStatus(ergebniseingang);
    }

    if (PerformanceStore.isStatistikAktiviert()) {
      PerformanceStore.addDuration(
          new ViewAccessTarget(ViewType.STIMMENEINGABE, ViewAccessType.GESAMT, PhaseIdEnum.INVOKE_APPLICATION, ViewAccessTarget.SESSION_ID_OHNE,
              getClass().getSimpleName() + "-plausibilisiere"), System.currentTimeMillis() - start);
    }

    start = System.currentTimeMillis();
    validiereStimmeingaben(ergebniseingang);
    if (PerformanceStore.isStatistikAktiviert()) {
      PerformanceStore.addDuration(
          new ViewAccessTarget(ViewType.STIMMENEINGABE, ViewAccessType.GESAMT, PhaseIdEnum.INVOKE_APPLICATION, ViewAccessTarget.SESSION_ID_OHNE,
              getClass().getSimpleName() + "-validiereStimmeingaben"), System.currentTimeMillis() - start);
    }

    LOG.debug("saveInternal [runtime={} ms]", System.currentTimeMillis() - millis);
  }

  protected void postSaveInternal(Ergebniseingang ergebniseingang, StimmeingabePlausiBuilder stimmeingabePlausiBuilder) {
    // ELECT-8023 ELECT-8298 Wahl aufteilen => Ergebnis in Zieleingang ebenfalls anlegen
    wahlaufteilenStimmeingabeCreate.saveFuerZielWahl(ergebniseingang);

    postSave(ergebniseingang);
  }

  protected void attachEntities(Ergebniseingang ergebniseingang, Collection<Stimmeingabe> stimmeingaben) {
    Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet = ergebniseingang.getWahldurchfuehrungsgebiet();

    // ELECT-3302 Performanceoptimierungen - alles vorher laden und nicht dao.attach verwenden
    Map<Long, GruppeImGebiet> gruppenImGebietPerId = GruppeImGebiet.mappeNachId(gruppeImGebietDAO.findByGebiet(wahldurchfuehrungsgebiet));

    List<Listenkandidatur> listenkandidaturen;

    Set<Long> listenkandidaturIds = new HashSet<>();

    for (Stimmeingabe stimmeingabe : stimmeingaben) {
      Listenkandidatur listenkandidatur = stimmeingabe.getListenkandidatur();

      if (listenkandidatur != null) {
        listenkandidaturIds.add(listenkandidatur.getId());
      }
    }

    if (listenkandidaturIds.isEmpty()) {
      listenkandidaturen = Collections.emptyList();
    } else if (listenkandidaturIds.size() < MAX_LISTENKANDIDATUREN_PER_FINDER) {
      listenkandidaturen = listenkandidaturDAO.findByIds(listenkandidaturIds);
    } else {
      listenkandidaturen = listenkandidaturDAO.findByWahl(wahldurchfuehrungsgebiet.getWahl());
    }

    Map<Long, Listenkandidatur> listenkandidaturenPerId = Listenkandidatur.mappeNachId(listenkandidaturen);

    for (Stimmeingabe stimmeingabe : stimmeingaben) {
      stimmeingabe.setReferendumOption(referendumOptionDAO.attach(stimmeingabe.getReferendumOption())); // Referendumoptionen bremsen nicht und sind die Ausnahme, daher hier keine Optimierungen
      stimmeingabe.setGruppeImGebiet(gruppenImGebietPerId.get(stimmeingabe.getGruppeImGebiet().getId()));

      Listenkandidatur listenkandidatur = stimmeingabe.getListenkandidatur();

      if (listenkandidatur != null) {
        stimmeingabe.setListenkandidatur(listenkandidaturenPerId.get(listenkandidatur.getId()));
      }
    }
  }

  protected void validiereStimmeingaben(Ergebniseingang ergebniseingang) {
    Set<EntityTK> seenStimmeingabeTks = new HashSet<>();

    for (Stimmeingabe stimmeingabe : ergebniseingang.getStimmeingaben()) {
      EntityTK stimmeingabeTk = stimmeingabe.getTk();

      if (seenStimmeingabeTks.contains(stimmeingabeTk)) {
        throw new IllegalStateException(
            "Versuch, in neuem Ergebniseingang zwei Stimmeingaben mit demselben fachlichen Schlüssel zu persistieren! [tk = " + stimmeingabeTk + ", ergebniseingang = " + ergebniseingang + "]");
      }

      seenStimmeingabeTks.add(stimmeingabeTk);
    }
  }

  private void bestimmeFehlerStatus(Ergebniseingang ergebniseingang) {
    if (isPlausibilisierungAktiv(ergebniseingang.getWahldurchfuehrung())) {
      boolean ergebniseingangUngueltigMussFehler = false;
      boolean ergebniseingangUngueltigKannFehler = false;

      for (Stimmeingabefehler stimmeingabefehler : ergebniseingang.getStimmeingabefehler()) {
        ergebniseingangUngueltigMussFehler |= stimmeingabefehler.isMussFehler();
        ergebniseingangUngueltigKannFehler |= stimmeingabefehler.isKannFehler();
      }

      if (ergebniseingangUngueltigMussFehler) {
        ergebniseingang.setUngueltigMussFehler();
      } else if (ergebniseingangUngueltigKannFehler) {
        ergebniseingang.setUngueltigKannFehler();
      } else {
        ergebniseingang.setGueltigKeinFehler();
      }
    } else {
      ergebniseingang.setGueltigKeinFehler(); // Keine Plausibilisierung => immer fehlerfrei
    }
  }

  private void plausibilisiere(Ergebniseingang ergebniseingang, StimmeingabePlausiBuilder stimmeingabePlausiBuilder) {
    if (stimmeingabePlausiBuilder == null) {
      return; // ELECT-9287 Stimmeingabefehler wurden manuell erzeugt, aber Status des Ergebniseingangs muss daraus ermittelt werden.
    }
    if (isPlausibilisierungAktiv(ergebniseingang.getWahldurchfuehrung())) {

      long start = System.currentTimeMillis();

      stimmeingabePlausiBuilder.fillStimmeingabePlausis(ergebniseingang);

      if (PerformanceStore.isStatistikAktiviert()) {
        PerformanceStore.addDuration(
            new ViewAccessTarget(ViewType.STIMMENEINGABE, ViewAccessType.GESAMT, PhaseIdEnum.INVOKE_APPLICATION, ViewAccessTarget.SESSION_ID_OHNE,
                getClass().getSimpleName() + "-fillStimmeingabePlausis"), System.currentTimeMillis() - start);
      }

      long anzahlGesamt = 0;
      long anzahlFehler = 0;

      for (Stimmeingabe stimmeingabe : ergebniseingang.getStimmeingaben()) {
        for (StimmeingabePlausiResult plausiResult : stimmeingabe.plausibilisiere()) {
          anzahlGesamt++;

          if (plausiResult.getStimmeingabePlausiStatus().isPlausibilisiereStimmeingabe() && plausiResult.isFehler()) {
            anzahlFehler++;
            persistiere(stimmeingabefehlerFactory.fromStimmeingabePlausiResult(plausiResult));
          }
        }
      }

      LOG.debug("plausibilisiere [Fehler in " + anzahlFehler + " von " + anzahlGesamt + " PlausiResults]");
    } else {
      LOG.info("Plausibilisierung deaktiviert");
    }
  }

  protected boolean isPlausibilisierungAktiv(Wahl wahl) {
    return stimmeingabeCreateKonfiguration.isPlausibilisierungAktiv(wahl);
  }

  @Override
  public void sendCreateEvent(Stimmeingabe entity) {
    // ELECT-3302 nichts tun, CreateEvent wird nicht benötigt und kostet Zeit  ;)
  }

  protected void validiereGebiet(Gebiet gebiet) {
    if (!isErfassungsgebiet(gebiet)) {
      throw new IllegalStateException("Versuch, Stimmeingabe für Nicht-Erfassungsgebiet zu speichern! [gebiet=" + gebiet + "]");
    }
  }

  private boolean isErfassungsgebiet(Gebiet gebiet) {
    return stimmeingabeAufGebietMoeglichChecker.isStimmeingabeAufGebietMoeglich(gebiet);
  }
}
