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

import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;

import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Supplier;

import javax.annotation.Nullable;
import javax.persistence.Transient;

import de.ivu.elect.business.datei.entity.FileType;
import de.ivu.elect.business.gebietsbaum.boundary.WahldurchfuehrungsgebietDAO;
import de.ivu.elect.business.gebietsbaum.entity.EntityMitGebiet;
import de.ivu.elect.business.gebietsbaum.entity.Gebiet;
import de.ivu.elect.business.gebietsbaum.entity.Wahldurchfuehrungsgebiet;
import de.ivu.elect.business.gruppe.entity.Gruppe;
import de.ivu.elect.business.gruppeimgebiet.boundary.GruppeImGebietDAO;
import de.ivu.elect.business.gruppeimgebiet.entity.GruppeImGebiet;
import de.ivu.elect.business.mandant.entity.EntityMitMandant;
import de.ivu.elect.business.stimmeingabe.boundary.AbstractErgebniseingangDAO;
import de.ivu.elect.business.stimmeingabe.boundary.AbstractStimmeingabefehlerDAO;
import de.ivu.elect.business.stimmeingabe.boundary.DefaultStimmeingabeErfassenContext;
import de.ivu.elect.business.stimmeingabe.boundary.ErgebniseingangDAOHelper;
import de.ivu.elect.business.stimmeingabe.boundary.StimmeingabeErfassenDescription;
import de.ivu.elect.business.stimmeingabe.boundary.StimmeingabeErfassenDescriptionResolver;
import de.ivu.elect.business.stimmeingabe.control.StimmeingabeErfassenContext;
import de.ivu.elect.business.stimmeingabe.control.plausi.WahlvorschlaegeSupplier;
import de.ivu.elect.business.stimmzettel.boundary.AbstractStimmzettelDAO;
import de.ivu.elect.business.stimmzettel.control.StimmzettelGebietUndNummerComparator;
import de.ivu.elect.business.stimmzettel.entity.EntityAufStimmzettelDaten;
import de.ivu.elect.business.stimmzettel.entity.Stimmzettel;
import de.ivu.elect.business.wahl.entity.Wahldurchfuehrung;
import de.ivu.elect.business.wahlvorschlag.boundary.WahlvorschlagDAO;
import de.ivu.elect.business.wahlvorschlag.entity.Wahlvorschlag;
import de.ivu.elect.util.CaseAndDiacriticIgnoringComparator;

public abstract class AbstractErgebniseingang implements ErgebniseingangBase, EntityMitGebiet, EntityMitMandant {

  private static final long serialVersionUID = 1L;

  public static final Long ERGEBNISSEQUENZNUMMER_UNGUELTIG = -1L;

  @Transient
  private transient StimmeingabeGruppeStimmenErfassenModel stimmeingabeGruppeStimmenErfassenModel;

  @Transient
  private transient StimmeingabeReferendumVorlageStimmenErfassenModel stimmeingabeReferendumVorlageStimmenErfassenModel;

  @Transient
  private transient StimmeingabeKopfzahlErfassenModel stimmeingabeKopfzahlErfassenModel;

  @Transient
  private transient boolean isOverrideErgebniseingangsart = false; // ELECTKOM-27

  @Transient
  private transient Map<GruppeImGebiet, StimmeingabeListenkandidaturStimmenErfassenModel> stimmeingabeListenkandidaturStimmenErfassenModels = new HashMap<>();

  public AbstractErgebniseingang() {
    super();
  }

  public boolean hasErfasserSystem() {
    return hasErfasser(ErgebniseingangErfasser.system());
  }

  public boolean hasErfasser(ErgebniseingangErfasser erfasser) {
    return erfasser != null && !erfasser.isUnbekannt() && Objects.equals(erfasser, ErgebniseingangErfasser.benutzer(this));
  }

  public boolean isOverrideErgebniseingangsart() {
    return isOverrideErgebniseingangsart;
  }

  public void setOverrideErgebniseingangsart(boolean overrideErgebniseingangsart) {
    isOverrideErgebniseingangsart = overrideErgebniseingangsart;
  }

  public Wahldurchfuehrung getWahldurchfuehrung() {
    return getWahldurchfuehrungsgebiet().getWahldurchfuehrung();
  }

  public List<Stimmzettel> getStimmzettel(AbstractStimmzettelDAO stimmzettelDAO) {
    List<Stimmzettel> result = new ArrayList<>();

    Gebiet gebiet = getGebiet();

    if (gebiet != null && gebiet.isPersistent()) {
      result.addAll(stimmzettelDAO.findByGebiet(gebiet)); // Ergebniseingänge und Stimmzettel werden auf derselben Ebene erfasst...
    }

    result.sort(StimmzettelGebietUndNummerComparator.INSTANCE);

    return result;
  }

  public List<String> getStimmeingabefehlerBeschreibungen(AbstractStimmeingabefehlerDAO stimmeingabefehlerDAO) {
    return getStimmeingabefehlerBeschreibungen(stimmeingabefehlerDAO, CaseAndDiacriticIgnoringComparator.DEFAULT_INSTANCE);
  }

  public List<String> getStimmeingabefehlerBeschreibungen(AbstractStimmeingabefehlerDAO stimmeingabefehlerDAO, Comparator<String> comparator) {
    Set<String> result = new HashSet<>();

    if (isUngueltigMussFehler()) { // Bei Muss-Fehlern sind nur diese interessant
      result.addAll(stimmeingabefehlerDAO.findByErgebniseingangAndFehlerartBeschreibung((Ergebniseingang) this, Stimmeingabefehlerart.ERROR));
    } else {
      result.addAll(stimmeingabefehlerDAO.findByErgebniseingangBeschreibung((Ergebniseingang) this));
    }

    return result.stream().sorted(comparator).collect(toList());
  }

  public Collection<Stimmeingabefehler> getStimmeingabefehler() {
    return getStimmeingaben().stream().flatMap(stimmeingabe -> stimmeingabe.getStimmeingabefehler().stream()).collect(toSet());
  }

  public Collection<Stimmeingabe> getStimmeingaben() {
    return getStimmeingabesByErgebniseingang();
  }

  public void addStimmeingabe(Stimmeingabe stimmeingabe) {
    addStimmeingabeByErgebniseingang(stimmeingabe);
  }

  public void removeAllStimmeingaben() {
    removeAllStimmeingabeByErgebniseingangs();
  }

  public void removeStimmeingabe(Stimmeingabe stimmeingabe) {
    removeStimmeingabeByErgebniseingang(stimmeingabe);
  }

  public boolean isErgebniseingangsartEndgueltig() {
    return getErgebniseingangsart().isEndgueltig();
  }

  public boolean isErgebniseingangsartVorlaeufig() {
    return getErgebniseingangsart().isVorlaeufig();
  }

  public boolean isInErgebnisUebernehmbar() {
    return getErgebniseingabestufe() != null && getErgebniseingabestufe().isInErgebnisUebernehmbar() && getErgebniseingangsfehlerstatus() != null
        && getErgebniseingangsfehlerstatus().isInErgebnisUebernehmbar();
  }

  public boolean isGueltig() {
    return getErgebniseingangsfehlerstatus() != null && getErgebniseingangsfehlerstatus().isGueltig();
  }

  public boolean isGueltigKeinFehler() {
    return getErgebniseingangsfehlerstatus() != null && getErgebniseingangsfehlerstatus().isGueltigKeinFehler();
  }

  public void setGueltigKeinFehler() {
    Ergebniseingabestufe ergebniseingabestufe = getErgebniseingabestufe();

    if (ergebniseingabestufe == null) {
      ergebniseingabestufe = Ergebniseingabestufe.ENDSTUFE;
    }

    setErgebniseingangsfehlerstatus(ergebniseingabestufe.getErgebniseingangsfehlerstatusGueltigKeinFehler());
  }

  public boolean isGueltigKannFehler() {
    return getErgebniseingangsfehlerstatus() != null && getErgebniseingangsfehlerstatus().isGueltigKannFehler();
  }

  public void setGueltigKannFehler() {
    Ergebniseingabestufe ergebniseingabestufe = getErgebniseingabestufe();

    if (ergebniseingabestufe == null) {
      ergebniseingabestufe = Ergebniseingabestufe.ENDSTUFE;
    }

    setErgebniseingangsfehlerstatus(ergebniseingabestufe.getErgebniseingangsfehlerstatusGueltigKannFehler());
  }

  public boolean isUngueltigKannFehler() {
    return getErgebniseingangsfehlerstatus() != null && getErgebniseingangsfehlerstatus().isUngueltigKannFehler();
  }

  public void setUngueltigKannFehler() {
    Ergebniseingabestufe ergebniseingabestufe = getErgebniseingabestufe();

    if (ergebniseingabestufe == null) {
      ergebniseingabestufe = Ergebniseingabestufe.ENDSTUFE;
    }

    setErgebniseingangsfehlerstatus(ergebniseingabestufe.getErgebniseingangsfehlerstatusUngueltigKannFehler());
  }

  public boolean isUngueltigMussFehler() {
    return getErgebniseingangsfehlerstatus() != null && getErgebniseingangsfehlerstatus().isUngueltigMussFehler();
  }

  public void setUngueltigMussFehler() {
    Ergebniseingabestufe ergebniseingabestufe = getErgebniseingabestufe();

    if (ergebniseingabestufe == null) {
      ergebniseingabestufe = Ergebniseingabestufe.ENDSTUFE;
    }

    setErgebniseingangsfehlerstatus(ergebniseingabestufe.getErgebniseingangsfehlerstatusUngueltigMussFehler());
  }

  public boolean isFiktiv() {
    return getErgebniseingangsherkunft().isFiktiv();
  }

  public boolean isVorEingangEingegeben(Ergebniseingang ergebniseingang) {
    return this.getEingangssequenznummer() < ergebniseingang.getEingangssequenznummer();
  }

  public String getErgebniseingangsartPathMitWahlnameFilesystemKompatibel() {
    return getWahldurchfuehrung().getDatumWahlartId() + File.separator + getErgebniseingangsart().name().toLowerCase();
  }

  /**
   * Am Ergebniseingang hängt nur eine Menge von Stimmeingaben. Diese Modelle bieten einen High-Level-Zugriff auf Stimmeingaben, indem sie diese strukturieren.
   */
  @Nullable
  public StimmeingabeGruppeStimmenErfassenModel getStimmeingabeGruppeStimmenErfassenModel() {
    return stimmeingabeGruppeStimmenErfassenModel;
  }

  public void setStimmeingabeGruppeStimmenErfassenModel(StimmeingabeGruppeStimmenErfassenModel stimmeingabeGruppeStimmenErfassenModel) {
    this.stimmeingabeGruppeStimmenErfassenModel = stimmeingabeGruppeStimmenErfassenModel;
  }

  public boolean isStimmeingabeGruppeStimmenErfassenModelSet() {
    return getStimmeingabeGruppeStimmenErfassenModel() != null;
  }

  /**
   * Am Ergebniseingang hängt nur eine Menge von Stimmeingaben. Diese Modelle bieten einen High-Level-Zugriff auf Stimmeingaben, indem sie diese strukturieren.
   */
  @Nullable
  public StimmeingabeReferendumVorlageStimmenErfassenModel getStimmeingabeReferendumVorlageStimmenErfassenModel() {
    return stimmeingabeReferendumVorlageStimmenErfassenModel;
  }

  public void setStimmeingabeReferendumVorlageStimmenErfassenModel(StimmeingabeReferendumVorlageStimmenErfassenModel stimmeingabeReferendumVorlageStimmenErfassenModel) {
    this.stimmeingabeReferendumVorlageStimmenErfassenModel = stimmeingabeReferendumVorlageStimmenErfassenModel;
  }

  public boolean isStimmeingabeReferendumVorlageStimmenErfassenModelSet() {
    return getStimmeingabeReferendumVorlageStimmenErfassenModel() != null;
  }

  /**
   * Am Ergebniseingang hängt nur eine Menge von Stimmeingaben. Diese Modelle bieten einen High-Level-Zugriff auf Stimmeingaben, indem sie diese strukturieren.
   */
  @Nullable
  public StimmeingabeKopfzahlErfassenModel getStimmeingabeKopfzahlErfassenModel() {
    return stimmeingabeKopfzahlErfassenModel;
  }

  public void setStimmeingabeKopfzahlErfassenModel(StimmeingabeKopfzahlErfassenModel stimmeingabeKopfzahlErfassenModel) {
    this.stimmeingabeKopfzahlErfassenModel = stimmeingabeKopfzahlErfassenModel;
  }

  public boolean isStimmeingabeKopfzahlErfassenModelSet() {
    return getStimmeingabeKopfzahlErfassenModel() != null;
  }

  public void addStimmeingabeListenkandidaturStimmenErfassenModel(StimmeingabeListenkandidaturStimmenErfassenModel listenkandidaturStimmenErfassenModel) {
    stimmeingabeListenkandidaturStimmenErfassenModels.put(listenkandidaturStimmenErfassenModel.getBezugGruppeImGebiet(), listenkandidaturStimmenErfassenModel);
  }

  /**
   * Am Ergebniseingang hängt nur eine Menge von Stimmeingaben. Diese Modelle bieten einen High-Level-Zugriff auf Stimmeingaben, indem sie diese strukturieren.
   */
  public Collection<StimmeingabeListenkandidaturStimmenErfassenModel> getStimmeingabeListenkandidaturStimmenErfassenModels() {
    return stimmeingabeListenkandidaturStimmenErfassenModels.values();
  }

  public StimmeingabeListenkandidaturStimmenErfassenModel getStimmeingabeListenkandidaturStimmenErfassenModel(GruppeImGebiet gruppeImGebiet) {
    return stimmeingabeListenkandidaturStimmenErfassenModels.get(gruppeImGebiet);
  }

  public boolean hasStimmeingabeListenkandidaturStimmenErfassenModels() {
    return !stimmeingabeListenkandidaturStimmenErfassenModels.isEmpty();
  }

  public String getDateinameFuerSicherung() {
    return getEingangssequenznummer() + "." + FileType.ZIP.getFileExtension();
  }

  @Transient
  private transient boolean keineReplikation = false;

  public void setKeineReplikation(boolean keineReplikation) {
    this.keineReplikation = keineReplikation;
  }

  public boolean isKeineReplikation() {
    return keineReplikation;
  }

  public static Map<Wahldurchfuehrungsgebiet, Ergebniseingang> findeLetzteEingaengeProGebiet(Wahldurchfuehrung wahldurchfuehrung, Ergebniseingangsart ergebniseingangsart,
      AbstractErgebniseingangDAO ergebniseingangDAO) {
    return findeLetzteEingaengeProGebiet(wahldurchfuehrung, ergebniseingangsart, EnumSet.allOf(Ergebniseingabestufe.class), ergebniseingangDAO);
  }

  public static Map<Wahldurchfuehrungsgebiet, Ergebniseingang> findeLetzteEingaengeProGebiet(Wahldurchfuehrung wahldurchfuehrung, Ergebniseingangsart ergebniseingangsart,
      Collection<Ergebniseingabestufe> ergebniseingabestufen, AbstractErgebniseingangDAO ergebniseingangDAO) {
    List<Ergebniseingang> letzteEingaengeProGebiet = ErgebniseingangDAOHelper.from(ergebniseingangDAO).findByNeuesteEingaengeByWahlAndErgebniseingangsartAndErgebniseingabestufe(wahldurchfuehrung,
        ergebniseingangsart, ergebniseingabestufen);

    Map<Wahldurchfuehrungsgebiet, Ergebniseingang> letzterEingangProGebiet = new HashMap<>();

    for (Ergebniseingang ergebniseingang : letzteEingaengeProGebiet) {
      letzterEingangProGebiet.put(ergebniseingang.getWahldurchfuehrungsgebiet(), ergebniseingang);
    }

    return letzterEingangProGebiet;
  }

  @Override
  public Gebiet getGebiet() {
    return getWahldurchfuehrungsgebiet();
  }

  @Override
  public EntityMitMandant getParent() {
    return getGebiet();
  }

  public void fillTransientModels(StimmeingabeErfassenDescriptionResolver stimmeingabeErfassenDescriptionResolver, @Nullable Ergebniseingang letzterErgebniseingang,
                                  GruppeImGebietDAO gruppeImGebietDAO, WahlvorschlagDAO wahlvorschlagDAO, EntityAufStimmzettelDaten entityAufStimmzettelDaten
      , WahldurchfuehrungsgebietDAO wahldurchfuehrungsgebietDAO) {
    Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet = getWahldurchfuehrungsgebiet();
    Wahldurchfuehrung wahldurchfuehrung = wahldurchfuehrungsgebiet.getWahldurchfuehrung();

    fillTransientModels(stimmeingabeErfassenDescriptionResolver.getStimmeingabeErfassenDescriptionByGebiet(wahldurchfuehrungsgebiet), letzterErgebniseingang,
        () -> gruppeImGebietDAO.findByGebiet(wahldurchfuehrungsgebiet), new WahlvorschlaegeSupplier(wahldurchfuehrung, wahlvorschlagDAO),
        entityAufStimmzettelDaten, wahldurchfuehrungsgebietDAO::findByErfasstFuerGebiete);
  }

  public void fillTransientModels(StimmeingabeErfassenDescription stimmeingabeErfassenDescription, @Nullable Ergebniseingang letzterErgebniseingang,
      Supplier<Collection<GruppeImGebiet>> gruppenImGebietSupplier, Supplier<Map<Gruppe, Collection<Wahlvorschlag>>> wahlvorschlaegeSupplier,
                                  EntityAufStimmzettelDaten entityAufStimmzettelDaten, Function<Gebiet, List<Wahldurchfuehrungsgebiet>> erfasstFuer) {
    StimmeingabeErfassenContext stimmeingabeErfassenContext = new DefaultStimmeingabeErfassenContext((Ergebniseingang) this, letzterErgebniseingang, gruppenImGebietSupplier.get(),
        wahlvorschlaegeSupplier, entityAufStimmzettelDaten, stimmeingabeErfassenDescription, erfasstFuer);

    stimmeingabeErfassenDescription.buildStimmeingabeErfassenModels(stimmeingabeErfassenContext);
  }
}
