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

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
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.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.boundary.StimmeingabeImporter;
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.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) {
    var letztenErgebniseingangInGebietPruefen = true;
    var stimmeingabenPlausibilisieren = true;

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

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

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

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

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

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

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

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

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

  private void saveMitStimmeingabePlausibilisierungKannFehlerBestaetigen(Ergebniseingang ergebniseingang, Ergebniseingang letzterErgebniseingangInGebietErwartet,
                                                                         boolean letztenErgebniseingangInGebietPruefen, StimmeingabePlausiBuilder stimmeingabePlausiBuilder) {
    var 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) {
    preSaveInternal(ergebniseingang, stimmeingabePlausiBuilder);

    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) {
    var millis = System.currentTimeMillis();

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

    validiereGebiet(ergebniseingang.getWahldurchfuehrungsgebiet());

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

    var 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 (var 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 preSaveInternal(Ergebniseingang ergebniseingang, StimmeingabePlausiBuilder stimmeingabePlausiBuilder) {
    // To be overridden...
  }

  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) {
    var wahldurchfuehrungsgebiet = ergebniseingang.getWahldurchfuehrungsgebiet();

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

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

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

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

    List<Listenkandidatur> listenkandidaturen = new ArrayList<>();
    if (listenkandidaturIds.isEmpty()) {
      listenkandidaturen = Collections.emptyList();
    } else if (listenkandidaturIds.size() < MAX_LISTENKANDIDATUREN_PER_FINDER) {
      listenkandidaturen = listenkandidaturDAO.findByIds(listenkandidaturIds);
    } else {
      // ELECTKOM-2626
      StimmeingabeImporter.addListenkandidaturenFuerGebiet(listenkandidaturen, wahldurchfuehrungsgebiet, listenkandidaturDAO);
    }

    var listenkandidaturenPerId = Listenkandidatur.mappeNachId(listenkandidaturen);

    for (var 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()));

      var listenkandidatur = stimmeingabe.getListenkandidatur();

      if (listenkandidatur != null) {
        var listenkandidaturAttached = listenkandidaturenPerId.get(listenkandidatur.getId());
        if (listenkandidaturAttached == null) {
          throw new RuntimeException("Keine Listenkandidatur gefunden: " + listenkandidatur);
        }
        stimmeingabe.setListenkandidatur(listenkandidaturAttached);
      }
    }
  }

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

    for (var stimmeingabe : ergebniseingang.getStimmeingaben()) {
      var 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())) {
      var ergebniseingangUngueltigMussFehler = false;
      var ergebniseingangUngueltigKannFehler = false;

      for (var 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())) {

      var 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 (var stimmeingabe : ergebniseingang.getStimmeingaben()) {
        for (var 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);
  }
}
