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

import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
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.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

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

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

import de.ivu.elect.business.anschrift.entity.EntityMitAnschrift;
import de.ivu.elect.business.gebietsbaum.boundary.AbstractGebietDAO;
import de.ivu.elect.business.gebietsbaum.boundary.GebietsartTemplate;
import de.ivu.elect.business.gebietsbaum.control.GebietByGebietsartAndPositionComparator;
import de.ivu.elect.business.gebietsbaum.control.GebietByNummerComparator;
import de.ivu.elect.business.mandant.entity.EntityMitMandant;
import de.ivu.elect.business.mandant.entity.Mandant;
import de.ivu.elect.business.wahl.boundary.AbstractWahlDAO;
import de.ivu.elect.business.wahl.entity.Wahl;
import de.ivu.elect.business.wahl.entity.Wahltermin;
import de.ivu.elect.jee.entity.EntityUtil;
import de.ivu.elect.jee.protokollierung.AttributTyp;
import de.ivu.elect.jee.protokollierung.VorherNachherWert;
import de.ivu.elect.presentation.security.AbstractGebietSecurity;
import de.ivu.elect.util.FileUtil;


import static java.util.Objects.nonNull;
import static org.apache.commons.lang3.StringUtils.defaultIfBlank;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.apache.commons.lang3.StringUtils.trimToEmpty;

public abstract class AbstractGebiet implements GebietBase, EntityMitAnschrift, EntityMitGebiet, EntityMitMandant {

  private static final long serialVersionUID = 1L;

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

  public static final GebietByGebietsartAndPositionComparator GEBIET_BY_GEBIETSART_AND_POSITION_COMPARATOR = new GebietByGebietsartAndPositionComparator();

  private static final int DEFAULT_EXPECTED_DEPTH = 5; // mehr als fünf Ebenen gibt es bisher nicht..

  private static final boolean SUCHE_PERFORMANCE = false; // ELECT-12152

  @Transient
  private transient Long[] uebergeordneteGebietIds = null;

  public AbstractGebiet() {
    super();
  }

  @Deprecated // teure Operation
  public Collection<Gebiet> getGebietshierarchie() {
    Set<Gebiet> result = new HashSet<>();

    result.add((Gebiet) this);
    result.addAll(getInGebietshierarchieUebergeordneteGebiete());
    result.addAll(getInGebietshierarchieUntergeordneteGebiete());

    return result;
  }

  /**
   * Bei Massenoperationen(*) wesentlich schneller, als naive Methode, bei einzelnen Aufrufen langsamer...
   * <p>
   * *) Erzeugen von Internetseiten
   */
  public boolean isInGebietshierarchieUntergeordnetesGebietVonSchnelleVersionFuerMassenoperationen(Gebiet gebiet) {
    if (this == gebiet) { // korrekt an dieser Stelle, equals vermeiden...
      return true;
    }
    Long otherId = gebiet.getId();
    if (getId().equals(otherId)) {
      return true;
    }

    if (uebergeordneteGebietIds == null) {
      uebergeordneteGebietIds = ladeUebergeordneteGebiete();
    }

    // Set ist hier zu aufwendig...
    for (Long uebergeordnetesGebietId : uebergeordneteGebietIds) {
      if (uebergeordnetesGebietId.equals(otherId)) {
        return true;
      }
    }
    return false;
  }

  private Long[] ladeUebergeordneteGebiete() {
    //  nicht ändern (Performance, Lambdas und Sets machen die Sache nicht schenller...)
    List<Long> list = new ArrayList<>(DEFAULT_EXPECTED_DEPTH);
    Gebiet g = (Gebiet) this;
    while (g.hasUebergeordnetesGebiet()) {
      g = g.getUebergeordnetesGebiet();
      list.add(g.getId());
    }

    return list.toArray(new Long[list.size()]);
  }

  public boolean isInGebietshierarchieUntergeordnetesGebietVon(Gebiet gebiet) {
    if (this.equals(gebiet)) {
      return true;
    }

    return hasUebergeordnetesGebiet() && getUebergeordnetesGebiet().isInGebietshierarchieUntergeordnetesGebietVon(gebiet);
  }

  public Collection<Gebiet> getInGebietshierarchieUntergeordneteGebiete() {
    Set<Gebiet> result = new HashSet<>();

    for (Gebiet untergeordnetesGebiet : getUntergeordneteGebiete()) {
      result.add(untergeordnetesGebiet);
      result.addAll(untergeordnetesGebiet.getInGebietshierarchieUntergeordneteGebiete());
    }

    return result;
  }

  /**
   * Kann zum iterieren über ein Gebiet mit seinen untergeordneten Gebieten verwendet werden, wobei die Reihenfolge durch eine Tiefensuche definiert ist und die Untergebiete jeweils in der Reihenfolge
   * ihrer Position besucht werden.
   */
  public Stream<Gebiet> getMitUntergeordnetenGebietenSortiertNachPositionStream() {
    Gebiet gebiet = (Gebiet) this;
    return Stream.concat(Stream.of(gebiet), gebiet.getUntergeordneteGebieteSortiertNachPosition().stream().flatMap(g -> g.getMitUntergeordnetenGebietenSortiertNachPositionStream()));
  }

  public boolean isInGebietshierarchieUebergeordnetesGebietVon(Gebiet gebiet) {
    if (gebiet == null) {
      return false;
    }
    return gebiet.isInGebietshierarchieUntergeordnetesGebietVon((Gebiet) this);
  }

  public boolean isInGebietshierarchieVon(Gebiet gebiet) {
    return isInGebietshierarchieUebergeordnetesGebietVon(gebiet) || isInGebietshierarchieUntergeordnetesGebietVon(gebiet);
  }

  @Transient
  private transient Set<Gebiet> inGebietshierarchieUebergeordneteGebiete;

  public Set<Gebiet> getInGebietshierarchieUebergeordneteGebiete() {

    if (inGebietshierarchieUebergeordneteGebiete == null) {
      if (hasUebergeordnetesGebiet()) {
        inGebietshierarchieUebergeordneteGebiete = new HashSet<>();
        inGebietshierarchieUebergeordneteGebiete.add(getUebergeordnetesGebiet());
        inGebietshierarchieUebergeordneteGebiete.addAll(getUebergeordnetesGebiet().getInGebietshierarchieUebergeordneteGebiete());
      } else {
        inGebietshierarchieUebergeordneteGebiete = Collections.emptySet();
      }
    }

    return inGebietshierarchieUebergeordneteGebiete;
  }

  public Set<Gebiet> getInGebietshierarchieUebergeordneteGebieteMitGebietSelbst() {
    HashSet<Gebiet> result = new HashSet<>(getInGebietshierarchieUebergeordneteGebiete());
    result.add((Gebiet) this);
    return result;
  }

  /**
   * Das gleiche wie #getInGebietshierarchieUebergeordneteGebieteMitGebietSelbst(), nur mit Reihenfolge
   *
   * @return Liste mit dem Gebiet selbst und danach das jeweils übergeordnete Gebiet usw.
   * @see AbstractGebiet#getInGebietshierarchieUebergeordneteGebieteMitGebietSelbst()
   */
  public List<Gebiet> getMitInGebietshierarchieUebergeordnetenGebietenAufsteigend() {
    List<Gebiet> result = new ArrayList<>();
    Gebiet g = getGebiet();
    while (g != null) {
      result.add(g);
      g = g.getUebergeordnetesGebiet();
    }
    return result;
  }

  public boolean isUnterstesGebiet() {
    return !hasUntergeordneteGebiete();
  }

  @Deprecated //teure Operation(!)
  public boolean hasUntergeordneteGebiete() {
    return !getUntergeordneteGebiete().isEmpty();
  }

  @Transient
  private transient Boolean listenwahlgebietOderOberhalbVonListenwahlgebiet = null;

  public boolean isListenwahlgebietOderOberhalbVonListenwahlgebiet() {

    if (listenwahlgebietOderOberhalbVonListenwahlgebiet == null) {
      listenwahlgebietOderOberhalbVonListenwahlgebiet = bestimmeListenwahlgebietOderOberhalbVonListenwahlgebiet();
    }

    return listenwahlgebietOderOberhalbVonListenwahlgebiet;
  }

  private boolean bestimmeListenwahlgebietOderOberhalbVonListenwahlgebiet() {
    if (isListenwahlgebiet()) {
      return true;
    }
    return getUntergeordneteGebiete().stream().anyMatch(AbstractGebiet::isListenwahlgebietOderOberhalbVonListenwahlgebiet);
  }

  public boolean isListenwahlgebietOderDrunter() {
    if (isListenwahlgebiet()) {
      return true;
    }
    if (isWurzelgebiet()) {
      return false;
    }
    return getUebergeordnetesGebiet().isListenwahlgebietOderDrunter();
  }

  public boolean isDirektwahlgebietOderDrunter() {
    if (isDirektwahlgebiet()) {
      return true;
    }
    if (isWurzelgebiet()) {
      return false;
    }

    return getUebergeordnetesGebiet().isDirektwahlgebietOderDrunter();
  }

  @Nullable
  public Gebiet getGebietAndereWahl(List<Gebiet> gebieteAndererWahl) {
    GebietTK gebietTK = getGebietTK();
    return gebieteAndererWahl.stream().filter(anderesGebiet -> gebietTK.equals(anderesGebiet.getGebietTK())).findFirst().orElse(null);

  }

  protected abstract GebietTK getGebietTK();

  @Transient
  private transient String darstellung;

  public String getDarstellung() {
    if (darstellung == null) {
      String key = new Gebietsart().getDarstellung(); // fallback

      Gebietsart gebietsart = getGebietsart();

      String gebietsartTitel = "";
      String gebietsartKennzeichen = "";

      if (gebietsart != null) {
        try {
          key = gebietsart.getDarstellung();

          gebietsartTitel = trimToEmpty(gebietsart.getTitel());
          gebietsartKennzeichen = trimToEmpty(gebietsart.getKennzeichen());
        } catch (Exception ex) {
          LOG.warn("FIXME! Fehler beim Aufruf der Gebietsart: " + ex.getMessage(), ex); // nicht werfen ELECT-7690
        }
      }

      if (isNotBlank(key)) {
        // NOTE, dass bei Anpassungen auch GebietsartViewFormBuilder#getDarstellungTooltip anpasst werden muss!!!
        String nummer = trimToEmpty(getNummerBezirkFallsGesetzt());
        try {
          if (key.contains("{0,number,") && isParsableNumber(nummer)) {
            darstellung = MessageFormat.format(key, Long.valueOf(nummer), trimToEmpty(getName()), trimToEmpty(getKuerzel()), gebietsartTitel, gebietsartKennzeichen, trimToEmpty(getNummer()));
          } else {
            darstellung = MessageFormat.format(key, nummer, trimToEmpty(getName()), trimToEmpty(getKuerzel()), gebietsartTitel, gebietsartKennzeichen, trimToEmpty(getNummer()));
          }
        } catch (Exception e) {
          LOG.warn("Darstellung nicht möglich: " + e.getMessage() + "; key: " + key + ", nummer: " + nummer);
          darstellung = "!!!" + nummer + "!!!";
        }
      } else {
        darstellung = "";
      }
    }

    return darstellung;
  }

  private boolean isParsableNumber(String nummer) {
    if (isBlank(nummer)) {
      return false;
    }
    try {
      Long.valueOf(nummer);
    } catch (NumberFormatException e) {
      return false;
    }
    return true;
  }

  public String getNummerBezirkFallsGesetzt() {
    return defaultIfBlank(getNummerBezirk(), getNummer());
  }

  @Deprecated // nicht verwenden, besser über DAO gehen
  public static List<Gebiet> getUntergeordneteGebiete(List<Gebiet> gebiete) {
    return gebiete.stream().map(Gebiet::getUntergeordneteGebiete).flatMap(u -> u.stream()).collect(Collectors.toList());
  }


  // ELECT-12152
  @Transient
  private transient Set<Gebiet> gebietsLoaded;

  public boolean isGebietsLoaded() {
    return gebietsLoaded!=null;
  }

  public void setGebietsLoaded(Set<Gebiet> gebietsLoaded) {
    this.gebietsLoaded = gebietsLoaded;
  }

  @Deprecated
  // handle with care - Aufruf relativ teuer, da kein Hibernate-Cache verwendet wird =>getUntergeordneteGebiete(AbstractGebietDAO)
  public Set<Gebiet> getUntergeordneteGebiete() {
    if (gebietsLoaded==null) {
      if (SUCHE_PERFORMANCE) {
        LOG.warn("*************************************************************************************");
        LOG.warn("************************************* LADE UNTERGEORDNETE GEBIETE *******************" + this);
        LOG.warn("*************************************************************************************");
        // new RuntimeException().printStackTrace();
      }
      return getGebiets();
    }
    return gebietsLoaded; // ELECT-12152
  }


  public Set<Gebiet> getUntergeordneteGebiete(AbstractGebietDAO abstractGebietDAO) {
    return new HashSet<>(abstractGebietDAO.findByUebergeordnetesGebiet((Gebiet) this));
  }

  @Deprecated
  // handle with care - Aufruf relativ teuer, da kein Hibernate-Cache verwendet wird, wenn in GUI häufig verwendet
  public Set<Gebiet> getGebietUndAlleUntergeordnetenGebieteAllerEbenen() {
    return getGebietUndAlleUntergeordnetenGebieteAllerEbenen(new HashSet<>());
  }

  public Set<Gebiet> getGebietUndAlleUntergeordnetenGebieteAllerEbenen(AbstractGebietDAO gebietDAO) {
    return getGebietUndAlleUntergeordnetenGebieteAllerEbenen(new HashSet<>(), gebietDAO);
  }

  @Deprecated
  Set<Gebiet> getGebietUndAlleUntergeordnetenGebieteAllerEbenen(Set<Gebiet> result) {
    result.add((Gebiet) this);
    for (Gebiet gebiet : getUntergeordneteGebiete()) {
      gebiet.getGebietUndAlleUntergeordnetenGebieteAllerEbenen(result);
    }
    return result;
  }

  Set<Gebiet> getGebietUndAlleUntergeordnetenGebieteAllerEbenen(Set<Gebiet> result, AbstractGebietDAO gebietDAO) {
    result.add((Gebiet) this);
    for (Gebiet gebiet : getUntergeordneteGebiete(gebietDAO)) {
      gebiet.getGebietUndAlleUntergeordnetenGebieteAllerEbenen(result, gebietDAO);
    }
    return result;
  }

  @Deprecated
  // handle with care - Aufruf relativ teuer, da kein Hibernate-Cache verwendet wird, wenn in GUI häufig verwendet => getUntergeordneteGebieteSortiertNachPosition(AbstractGebietDAO)
  public List<Gebiet> getUntergeordneteGebieteSortiertNachPosition() {
    List<Gebiet> result = new ArrayList<>(getUntergeordneteGebiete());
    result.sort(Comparator.comparing(Gebiet::getPosition)); // Gebiete auf gleicher Ebene => nur Position relevant
    return result;
  }

  public List<Gebiet> getUntergeordneteGebieteSortiertNachPosition(AbstractGebietDAO abstractGebietDAO) {
    List<Gebiet> result = new ArrayList<>(getUntergeordneteGebiete(abstractGebietDAO));
    result.sort(Comparator.comparing(Gebiet::getPosition)); // Gebiete auf gleicher Ebene => nur Position relevant
    return result;
  }

  @Deprecated
  // handle with care - Aufruf relativ teuer, da kein Hibernate-Cache verwendet wird, wenn in GUI häufig verwendet
  public Set<Gebiet> getBenachbarteGebiete() {
    Gebiet uebergeordnetesGebiet = getUebergeordnetesGebiet();
    if (uebergeordnetesGebiet == null) {
      return new HashSet<>();
    }
    Set<Gebiet> benachbarteGebiete = uebergeordnetesGebiet.getUntergeordneteGebiete();
    Set<Gebiet> result = new HashSet<>();
    for (Gebiet gebiet : benachbarteGebiete) {
      if (!gebiet.equals(this)) {
        result.add(gebiet);
      }
    }
    return result;
  }

  public boolean isWahlgebiet() {
    if (getWahl() == null) { // TODO ELECT-762
      return false;
    }

    Gebietsarteigenschaft gebietsarteigenschaft = getGebietsarteigenschaft();
    if (gebietsarteigenschaft == null) {
      return false;
    }
    return gebietsarteigenschaft.getWahlgebiet();
  }

  public boolean isWahlgebietOderWurzelgebiet() {
    return isWurzelgebiet() || isWahlgebiet();
  }

  public boolean hasUebergeordnetesGebiet() {
    return getUebergeordnetesGebiet() != null;
  }

  /**
   * @see AbstractWahldurchfuehrungsgebiet#getWahldurchfuehrungsgebieteSortiertNachPosition()
   */
  @Deprecated // mit DAO verwenden(!)
  public List<Gebiet> getGebieteSortiertNachPosition() {
    List<Gebiet> result = new ArrayList<>(getUntergeordneteGebiete());

    Collections.sort(result, new GebietByGebietsartAndPositionComparator());

    return result;
  }

  public List<Gebiet> getGebieteSortiertNachPosition(AbstractGebietDAO gebietDAO) {
    List<Gebiet> result = new ArrayList<>(gebietDAO.findByUebergeordnetesGebiet((Gebiet) this));

    result.sort(new GebietByGebietsartAndPositionComparator());

    return result;
  }

  public boolean isWahlvorschlaggebiet() {
    return isListenwahlgebiet() || isDirektwahlgebiet() || isReferendumgebiet();
  }

  public boolean isListenwahlgebiet() {
    Gebietsarteigenschaft gebietsarteigenschaft = getGebietsarteigenschaft();
    return gebietsarteigenschaft != null && gebietsarteigenschaft.getListenwahlgebiet();
  }

  public boolean isDirektwahlgebiet() {
    Gebietsarteigenschaft gebietsarteigenschaft = getGebietsarteigenschaft();
    return gebietsarteigenschaft != null && gebietsarteigenschaft.getDirektwahlgebiet();
  }

  public boolean isReferendumgebiet() {
    Gebietsarteigenschaft gebietsarteigenschaft = getGebietsarteigenschaft();
    return gebietsarteigenschaft != null && gebietsarteigenschaft.getReferendumgebiet();
  }

  @Transient
  private transient Gebietsarteigenschaft gebietsarteigenschaft;

  /**
   * Ist im WAS immer != null.
   */
  @Nullable
  public Gebietsarteigenschaft getGebietsarteigenschaft() {
    if (gebietsarteigenschaft == null) {
      gebietsarteigenschaft = bestimmeGebietsarteigenschaft();
    }
    return gebietsarteigenschaft;
  }

  public void resetCachedGebietsarteigenschaft() {
    gebietsarteigenschaft = null;
  }

  @Nullable
  private Gebietsarteigenschaft bestimmeGebietsarteigenschaft() {
    if (getWahl() == null || getWahl().getWahlart() == null) { // TODO ELECT-762 Gebietsarteigenschaften im WOS?
      return null;
    }

    return getWahl().getWahlart().getGebietsarteigenschaftFuerGebietsart(getGebietsart());
  }

  public boolean hasExterneId() {
    return isNotBlank(getExterneId());
  }

  public String findExterneIdInGebietshierarchie() {
    String externeId = getExterneId();

    if (isBlank(externeId) && hasUebergeordnetesGebiet()) {
      return getUebergeordnetesGebiet().findExterneIdInGebietshierarchie();
    }

    return externeId;
  }

  // NOTE ELECT-534, dass stattdessen GebietNummerFormatter.getNameMitNummer(Gebiet) verwendet werden sollte
  @Deprecated
  public String getNameMitNummer() {
    return getName() + " (" + getNummer() + ")";
  }

  public boolean isWurzelgebiet() {
    return !hasUebergeordnetesGebiet();
  }

  public Gebiet getWurzelgebiet() {
    if (isWurzelgebiet()) {
      return (Gebiet) this;
    }

    return getUebergeordnetesGebiet().getWurzelgebiet();
  }

  @Nullable
  public Gebiet getUebergeordnetesGebietMitRecht(AbstractGebietSecurity gebietSecurity) {
    if (hasUebergeordnetesGebiet()) {
      Gebiet result = getUebergeordnetesGebiet().getUebergeordnetesGebietMitRecht(gebietSecurity);

      if (result != null) {
        return result;
      }
    }

    if (gebietSecurity.isRechtVorhanden((Gebiet) this)) {
      return (Gebiet) this;
    }

    return null;
  }

  public static void sortByNummer(List< ? extends Gebiet> gebiete) {
    gebiete.sort(GebietByNummerComparator.ASCENDING);
  }

  @Deprecated // NICHT BENUTZEN/NICHT LÖSCHEN WIRD NOCH IN elect-interfaces-legacy-exchange verwendet(!)
  public static List<Gebiet> sortiereGebieteBreadthFirstNachPosition(List<Gebiet> gebiete) {
    if (gebiete.isEmpty()) {
      return gebiete;
    }

    List<Gebiet> result = new ArrayList<>(gebiete.size());

    Gebiet wurzel = gebiete.get(0).getWurzelgebiet();

    result.add(wurzel);

    fuegeUntergeordneteGebieteHinzu(wurzel, result);

    return result;
  }

  @Deprecated // NICHT BENUTZEN/NICHT LÖSCHEN WIRD NOCH IN elect-interfaces-legacy-exchange verwendet(!)
  private static void fuegeUntergeordneteGebieteHinzu(Gebiet wurzel, List<Gebiet> result) {
    List<Gebiet> gebieteSortiertNachPosition = wurzel.getGebieteSortiertNachPosition();

    result.addAll(gebieteSortiertNachPosition);

    for (Gebiet gebiet : gebieteSortiertNachPosition) {
      fuegeUntergeordneteGebieteHinzu(gebiet, result);
    }
  }

  /**
   * @return Unterstete Gebiete, wenn vorhanden und exklusive diesem Gebiet selbst.
   */
  @Deprecated // Quatschmethode, überdenken(!)
  public Collection<Gebiet> getNurGebieteAufUntersterEbene() {
    Set<Gebiet> gebiets = new HashSet<>();
    Set<Gebiet> untergeordneteGebiete = getUntergeordneteGebiete();

    if (untergeordneteGebiete.isEmpty()) {
      return gebiets;
    }

    for (Gebiet gebiet : untergeordneteGebiete) {
      Collection<Gebiet> untersteGebiete = gebiet.getNurGebieteAufUntersterEbene();

      if (untersteGebiete.isEmpty()) {
        gebiets.add(gebiet);
      } else {
        gebiets.addAll(untersteGebiete);
      }
    }

    return gebiets;
  }

  /**
   * @return Wahlrechtlich relevante unterstete Gebiete, wenn vorhanden und exklusive diesem Gebiet selbst.
   */
  @Deprecated // Quatschmethode, überdenken(!)
  public Collection<Gebiet> getNurWahlrechtlichRelevanteGebieteAufUntersterEbene() {
    Collection<Gebiet> nurGebieteAufUntersterEbene = getNurGebieteAufUntersterEbene();

    List<Gebiet> toRemove = new ArrayList<>();

    for (Gebiet gebiet : nurGebieteAufUntersterEbene) {
      if (!gebiet.isErgebnisRelevant()) {
        toRemove.add(gebiet);
      }
    }

    for (Gebiet gebiet : toRemove) {
      nurGebieteAufUntersterEbene.remove(gebiet);
    }

    return nurGebieteAufUntersterEbene;
  }

  @Transient
  private transient Boolean wahlOderDirektwahlOderListenwahlgebietOderHatEntprechendesUntergebiet;

  public boolean isWahlOderDirektwahlOderListenwahlgebietOderHatEntprechendesUntergebiet() {
    if (wahlOderDirektwahlOderListenwahlgebietOderHatEntprechendesUntergebiet == null) {
      if (isWahlgebiet() || isListenwahlgebiet() || isDirektwahlgebiet() || isWahlOderDirektwahlOderListenwahlgebietOderHatEntprechendesUntergebietRekursion(getUntergeordneteGebiete())) {
        wahlOderDirektwahlOderListenwahlgebietOderHatEntprechendesUntergebiet = true;
      } else {
        wahlOderDirektwahlOderListenwahlgebietOderHatEntprechendesUntergebiet = false;
      }
    }

    return wahlOderDirektwahlOderListenwahlgebietOderHatEntprechendesUntergebiet;
  }

  protected boolean isWahlOderDirektwahlOderListenwahlgebietOderHatEntprechendesUntergebietRekursion(Collection<Gebiet> gebiete) {
    return gebiete.stream().anyMatch(gebiet -> gebiet.isWahlgebiet() || gebiet.isListenwahlgebiet() || gebiet.isDirektwahlgebiet()
        || isWahlOderDirektwahlOderListenwahlgebietOderHatEntprechendesUntergebietRekursion(gebiet.getUntergeordneteGebiete()));
  }

  @Transient
  private transient Boolean hasUntergeordnetesDirektwahlOderListenwahlgebiet;

  public boolean hasUntergeordnetesDirektwahlOderListenwahlgebiet(AbstractGebietDAO gebietDAO) {
    if (hasUntergeordnetesDirektwahlOderListenwahlgebiet == null) {
      hasUntergeordnetesDirektwahlOderListenwahlgebiet = getUntergeordneteGebiete(gebietDAO).stream()
          .anyMatch(gebiet -> gebiet.isDirektwahlgebiet() || gebiet.isListenwahlgebiet()
              || gebiet.hasUntergeordnetesDirektwahlOderListenwahlgebiet(gebietDAO));
    }
    return hasUntergeordnetesDirektwahlOderListenwahlgebiet;
  }

  public boolean hasUebergeordnetesListenwahlgebiet() {
    Gebiet uebergeordnetesListenwahlgebiet = getUebergeordnetesListenwahlgebiet();
    return uebergeordnetesListenwahlgebiet != null && !this.equals(uebergeordnetesListenwahlgebiet);
  }

  protected void preSetWahl(Wahl alterWert, Wahl neuerWert) {
    if (neuerWert != null) {
      setWahltermin(neuerWert.getWahltermin());
    }
  }

  protected void postSetWahl(Wahl alterWert, Wahl neuerWert) {
    // wird nicht benötigt, muss aber implementier sein
  }

  public boolean isEqualOderUntergebietVon(Gebiet gebiet) {
    if (equals(gebiet)) {
      return true;
    }

    return isUntergebietVon(gebiet);
  }

  public boolean isUntergebietVon(Gebiet gebiet) {
    Gebiet uebergeordnetesGebiet = getUebergeordnetesGebiet();
    if (uebergeordnetesGebiet == null) {
      return false;
    }
    if (uebergeordnetesGebiet.equals(gebiet)) {
      return true;
    }
    return uebergeordnetesGebiet.isUntergebietVon(gebiet);
  }

  public int getAnzahlUebergeordneterGebietsebenen() {
    if (!hasUebergeordnetesGebiet()) {
      return 0;
    }
    return 1 + getUebergeordnetesGebiet().getAnzahlUebergeordneterGebietsebenen();
  }

  @Deprecated // verwendet getUntergeordneteGebiete
  public int getAnzahlUntergeordneterGebietsebenen() {
    if (getUntergeordneteGebiete().isEmpty()) {
      return 0;
    }
    int maxAnzahlEbenenVonUntergebieten = 0;
    for (Gebiet untergebiet : getUntergeordneteGebiete()) {
      int anzahlEbenenUntergebiet = untergebiet.getAnzahlUntergeordneterGebietsebenen();
      if (anzahlEbenenUntergebiet > maxAnzahlEbenenVonUntergebieten) {
        maxAnzahlEbenenVonUntergebieten = anzahlEbenenUntergebiet;
      }
    }
    return maxAnzahlEbenenVonUntergebieten + 1;
  }

  /**
   * ELECT-3919: nicht relevant sind --z.B.-- NUR(!) Sondergebiete Berlin-Ost und Berlin-West. Deren Stimmen dürfen nicht bei der Kumulation der Stimmen des Obergebiets mitgerechnet werden.
   *
   * @return ob die Stimmen dieses Gebiets bei der Berechnung der Stimmen des Obergebiets mitzählen
   */
  public boolean isErgebnisRelevant() {
    return GebietRelevantBestimmer.isRelevant((Gebiet) this); // ELECT-8198
  }

  public boolean isListenkandidatenWaehlbarInUebergeordneterGebietshierarchie() {
    if (isListenkandidatenWaehlbar()) {
      return true;
    }

    if (getUebergeordnetesGebiet() != null) {
      return getUebergeordnetesGebiet().isListenkandidatenWaehlbarInUebergeordneterGebietshierarchie();
    }

    return false;
  }

  private boolean isListenkandidatenWaehlbar() {
    Gebietsarteigenschaft gebietsarteigenschaft = getGebietsarteigenschaft();
    if (gebietsarteigenschaft == null) {
      return false;
    }
    return gebietsarteigenschaft.getListenkandidatenWaehlbar();
  }

  private transient Boolean erfassungsgebiet;

  public boolean isErfassungsgebiet() {
    return isErfassungsgebietLautGebietsart();
  }

  public boolean isErfassungsgebietLautGebietsart() {
    // notwendig wegen de.ivu.elect.business.gebietsbaum.entity.AbstractWahldurchfuehrungsgebiet.isErfassungsgebiet
    if (erfassungsgebiet == null) {
      Gebietsarteigenschaft gebietsarteigenschaft = getGebietsarteigenschaft();
      if (gebietsarteigenschaft == null) {
        // WOS?
        erfassungsgebiet = getGebietsart() != null && getGebietsart().getBezirksart() != null && getGebietsart().getBezirksart().isErfassungsgebiet();

      } else {
        erfassungsgebiet = gebietsarteigenschaft.getErfassungsgebiet();
      }
    }
    return erfassungsgebiet;
  }

  private transient Boolean briefwahlgebiet;

  public boolean isBriefwahlgebiet() {
    if (briefwahlgebiet == null) {
      Gebietsarteigenschaft gebietsarteigenschaft = getGebietsarteigenschaft();
      if (gebietsarteigenschaft == null) {
        // WOS?
        briefwahlgebiet = getGebietsart() != null && getGebietsart().getBezirksart() != null && getGebietsart().getBezirksart().isBriefwahlbezirk();
      } else {
        briefwahlgebiet = gebietsarteigenschaft.getBriefwahlgebiet();
      }
    }
    return briefwahlgebiet;
  }

  public boolean isUrnenwahlgebiet() {
    return isErfassungsgebiet() && !isBriefwahlgebiet();
  }

  public static <G extends AbstractGebiet> Collection<G> filterErfassungsgebiete(Collection<G> gebiete) {
    return gebiete.stream().filter(AbstractGebiet::isErfassungsgebiet).collect(Collectors.toList());
  }

  public static <T extends EntityMitGebiet> Map<Gebiet, List<T>> map(Collection<T> entities) {
    Map<Gebiet, List<T>> result = new HashMap<>();
    for (T entity : entities) {
      if (!result.containsKey(entity.getGebiet())) {
        result.put(entity.getGebiet(), new ArrayList<T>());
      }
      result.get(entity.getGebiet()).add(entity);
    }
    return result;
  }

  public Gebiet getDirektwahlgebiet() {
    if (isDirektwahlgebiet()) {
      return (Gebiet) this;
    }
    if (hasUebergeordnetesGebiet()) {
      return getUebergeordnetesGebiet().getDirektwahlgebiet();
    }
    throw new IllegalStateException("Kein Direktwahlgebiet gefunden!");
  }

  public Gebiet findeGebietMitGebietsartInUebergeordneterHierarchie(GebietsartTemplate gebietsart) {
    Gebiet result = findeGebietMitGebietsartInUebergeordneterHierarchieOrReturnNull(gebietsart);
    if (result != null) {
      return result;
    }

    throw new IllegalStateException("Kein Gebiet mit Gebietsart " + gebietsart);
  }

  @Nullable
  public Gebiet findeGebietMitGebietsartInUebergeordneterHierarchieOrReturnNull(GebietsartTemplate gebietsart) {
    if (gebietsart.equalsGebiet((Gebiet) this)) {
      return (Gebiet) this;
    }
    if (getUebergeordnetesGebiet() == null) {
      return null;
    }

    return getUebergeordnetesGebiet().findeGebietMitGebietsartInUebergeordneterHierarchieOrReturnNull(gebietsart);
  }

  @Nullable
  public Long getAnzahlSitzeImWahlgebiet() {
    if (isWahlgebietOderWurzelgebiet()) { // ELECTKOM-830
      return getAnzahlSitze();
    }

    if (hasUebergeordnetesGebiet()) {
      return getUebergeordnetesGebiet().getAnzahlSitzeImWahlgebiet();
    }

    return null;
  }

  @Nullable // ELECTKOM-649
  public Long getAnzahlStimmenProWaehlerImWahlgebiet() {
    if (isWahlgebietOderWurzelgebiet()) { // ELECTKOM-830
      Long result = getAnzahlStimmenProWaehler();

      if (result == null) {
        result = getAnzahlSitze();
      }

      return result;
    }

    if (hasUebergeordnetesGebiet()) {
      return getUebergeordnetesGebiet().getAnzahlStimmenProWaehlerImWahlgebiet();
    }

    return null;
  }

  @Nullable
  public Gebiet getErstesUebergeordnetesDirektwahlOderListenwahlOderWahlgebiet() {
    if (isListenwahlgebiet() || isDirektwahlgebiet() || /* ELECTMUC-2087 */ isReferendumgebiet() || isWahlgebiet()) {
      return (Gebiet) this;
    }
    if (getUebergeordnetesGebiet() == null) {
      return null;
    }

    return getUebergeordnetesGebiet().getErstesUebergeordnetesDirektwahlOderListenwahlOderWahlgebiet();
  }

  @Nullable
  public Gebiet getUebergeordnetesWahlvorschlaggebiet() {
    if (isWahlvorschlaggebiet()) {
      return (Gebiet) this;
    }
    if (getUebergeordnetesGebiet() == null) {
      return null;
    }

    return getUebergeordnetesGebiet().getUebergeordnetesWahlvorschlaggebiet();
  }

  @Nullable
  public Gebiet getUebergeordnetesListenwahlgebiet() {
    if (isListenwahlgebiet()) {
      return (Gebiet) this;
    }
    if (getUebergeordnetesGebiet() == null) {
      return null;
    }

    return getUebergeordnetesGebiet().getUebergeordnetesListenwahlgebiet();
  }

  @Nullable
  public Gebiet getUebergeordnetesDirektwahlgebiet() {
    if (isDirektwahlgebiet()) {
      return (Gebiet) this;
    }
    if (getUebergeordnetesGebiet() == null) {
      return null;
    }

    return getUebergeordnetesGebiet().getUebergeordnetesDirektwahlgebiet();
  }

  @Nullable
  public Gebiet getUebergeordnetesReferendumgebiet() {
    if (isReferendumgebiet()) {
      return (Gebiet) this;
    }
    if (getUebergeordnetesGebiet() == null) {
      return null;
    }

    return getUebergeordnetesGebiet().getUebergeordnetesReferendumgebiet();
  }

  @Nullable
  public Gebiet getUebergeordnetesWahlgebiet() {
    if (isWahlgebiet()) {
      return (Gebiet) this;
    }
    if (getUebergeordnetesGebiet() == null) {
      return null;
    }

    return getUebergeordnetesGebiet().getUebergeordnetesWahlgebiet();
  }

  @Nullable
  protected Gebiet getErstesUntergeordnetesWahlgebiet() {
    if (isWahlgebiet()) {
      return (Gebiet) this;
    }
    if (getUntergeordneteGebiete().isEmpty()) {
      return null;
    }

    return getUntergeordneteGebiete().iterator().next().getErstesUntergeordnetesWahlgebiet();
  }

  public static Gebiet bestimmeEinUnterstesGebiet(Collection< ? extends Gebiet> gebiete) {
    if (gebiete.isEmpty()) {
      throw new IllegalStateException("Keine Gebiete vorhanden!");
    }

    Set<Gebiet> gebieteOhneUntergeordnetesGebiet = new HashSet<>(gebiete);

    for (Gebiet wahldurchfuehrungsgebiet : gebiete) {
      if (wahldurchfuehrungsgebiet.hasUebergeordnetesGebiet()) {
        gebieteOhneUntergeordnetesGebiet.remove(wahldurchfuehrungsgebiet.getUebergeordnetesGebiet());
      }
    }

    return gebieteOhneUntergeordnetesGebiet.iterator().next();
  }

  @Transient
  private transient Boolean oberhalbVomWahlgebiet = null;

  public boolean isOberhalbVomWahlgebietUndSelbstKeinWahlgebiet() {
    return !isWahlgebiet() && isOberhalbVomWahlgebiet();
  }

  public boolean isOberhalbVomWahlgebiet() {
    if (oberhalbVomWahlgebiet == null) {
      oberhalbVomWahlgebiet = bestimmeOberhalbVomWahlgebiet();
    }
    return oberhalbVomWahlgebiet;
  }

  private boolean bestimmeOberhalbVomWahlgebiet() {
    return getUebergeordnetesWahlgebiet() == null && getErstesUntergeordnetesWahlgebiet() != null;
  }

  public List<Gebiet> getUntergeordneteWahlgebiete(AbstractGebietDAO abstractGebietDAO) {
    List<Gebiet> result = new ArrayList<>();
    for (Gebiet untergeordnetesGebiet : getUntergeordneteGebiete(abstractGebietDAO)) {
      if (untergeordnetesGebiet.isWahlgebiet()) {
        result.add(untergeordnetesGebiet);
      } else {
        result.addAll(untergeordnetesGebiet.getUntergeordneteWahlgebiete(abstractGebietDAO));
      }
    }
    return result;
  }

  @Transient
  private transient String rollenNameFuerEinsehen = null;

  public String getRollenNameFuerEinsehen() {
    if (rollenNameFuerEinsehen == null) {
      rollenNameFuerEinsehen = bestimmeRollenNameFuerEinsehen();
    }
    return rollenNameFuerEinsehen;
  }

  protected String bestimmeRollenNameFuerEinsehen() {
    if (getEinsehenRolle() != null) {
      return getEinsehenRolle().getName();
    }
    if (isWurzelgebiet()) {
      return ""; // keine Rolle notwendig
    }
    return getUebergeordnetesGebiet().getRollenNameFuerEinsehen();
  }

  @Transient
  private transient String rollenNameFuerBearbeiten = null;

  public String getRollenNameFuerBearbeiten() {
    if (rollenNameFuerBearbeiten == null) {
      rollenNameFuerBearbeiten = bestimmeRollenNameFuerBearbeiten();
    }
    return rollenNameFuerBearbeiten;
  }

  @Transient
  private transient Set<Mandant> verantwortlicheMandantenAbgeleitetCache;

  public Set<Mandant> getVerantwortlicheMandantenAbgeleitet() {
    if (verantwortlicheMandantenAbgeleitetCache == null) {
      verantwortlicheMandantenAbgeleitetCache = new HashSet<>();
      if (getVerantwortlicherMandant() != null) {
        verantwortlicheMandantenAbgeleitetCache.add(getVerantwortlicherMandant());
      }
      if (!isWurzelgebiet()) {
        verantwortlicheMandantenAbgeleitetCache.addAll(getUebergeordnetesGebiet().getVerantwortlicheMandantenAbgeleitet());
      }
    }
    return verantwortlicheMandantenAbgeleitetCache;
  }

  public Set<Wahl> getVerantwortlicheMandantenWahlen(AbstractWahlDAO wahlDAO) {
    if (getWahl() == null) {
      return Collections.emptySet();
    }

    return getVerantwortlicheMandatenWahlen(wahlDAO, getVerantwortlicheMandantenAbgeleitet());
  }

  private Set<Wahl> getVerantwortlicheMandatenWahlen(AbstractWahlDAO wahlDAO, Set<Mandant> verantwortlicheMandantenAbgeleitet) {
    if (verantwortlicheMandantenAbgeleitet.isEmpty()) {
      return Collections.emptySet();
    }

    Set<Wahl> result = new HashSet<>();
    for (Mandant verantwortlicherMandant : verantwortlicheMandantenAbgeleitet) {
      result.addAll(wahlDAO.findByBerichtetAnAndMandant(getWahl(), verantwortlicherMandant));
    }

    return result;
  }

  public Set<Wahl> getZielWahlenBeiAufteilung(AbstractWahlDAO wahlDAO, AbstractGebietDAO abstractGebietDAO) {
    if (getWahl() != null && getWahl().getBerichtetAn() != null) {
      return getAlleWahlenVerantwortlich(getWahl().getBerichtetAn(), wahlDAO, abstractGebietDAO);
    }
    return getVerantwortlicheMandantenWahlen(wahlDAO);
  }

  private Set<Wahl> getAlleWahlenVerantwortlich(Wahl berichtetAnWahl, AbstractWahlDAO wahlDAO, AbstractGebietDAO gebietDAO) {
    Set<Wahl> result = new HashSet<>();
    result.add(berichtetAnWahl);
    result.addAll(getAlleWahlenVerantwortlichGebietshierarchie(this, berichtetAnWahl, wahlDAO, gebietDAO));
    result.remove(getWahl()); // eigene Wahl abziehen
    return result;
  }

  private static Set<Wahl> getAlleWahlenVerantwortlichGebietshierarchie(AbstractGebiet gebiet, Wahl berichtetAnWahl, AbstractWahlDAO wahlDAO,
                                                                        AbstractGebietDAO gebietDAO) {
    if (gebiet == null) {
      return Collections.emptySet();
    }
    Set<Wahl> result = new HashSet<>();
    Gebiet gebietInBerichtetAnWahl = findeGebietBeiAufteilungHandleWithCare(gebiet, berichtetAnWahl, gebietDAO);
    if (gebietInBerichtetAnWahl != null) {
      result.addAll(gebietInBerichtetAnWahl.getVerantwortlicheMandantenWahlen(wahlDAO));
    }
    result.addAll(getAlleWahlenVerantwortlichGebietshierarchie(gebiet.getUebergeordnetesGebiet(), berichtetAnWahl, wahlDAO, gebietDAO));

    return result;
  }

  public Set<Gebiet> getZielGebieteBeiAufteilung(AbstractWahlDAO wahlDAO, AbstractGebietDAO gebietDAO) {

    Set<Wahl> zielWahlenBeiAufteilung = getZielWahlenBeiAufteilung(wahlDAO, gebietDAO);

    if (zielWahlenBeiAufteilung.isEmpty()) {
      return Collections.emptySet();
    }

    return zielWahlenBeiAufteilung.stream()
        .map(zielWahlBeiAufteilung -> findeGebietBeiAufteilungHandleWithCare(this, zielWahlBeiAufteilung, gebietDAO))
        .filter(Objects::nonNull)
        .collect(Collectors.toSet());
  }

  @Nullable
  @Deprecated // getZielGebieteBeiAufteilung benutzen
  public static Gebiet findeGebietBeiAufteilungHandleWithCare(AbstractGebiet gebiet, Wahl zielWahl, AbstractGebietDAO gebietDAO) {
    if (zielWahl == null || gebiet == null) {
      return null;
    }
    return findeGebietBeiAufteilungHandleWithCare(createGebietUkFuerWahl(gebiet, zielWahl), gebietDAO, bestimmeBerichtenderMandant(gebiet));
  }

  private static Mandant bestimmeBerichtenderMandant(AbstractGebiet gebiet) {
    if (gebiet.getWahl().getBerichtetAn() == null) {
      return null;
    }
    return gebiet.getMandant();
  }

  @Nullable
  @Deprecated // getZielGebieteBeiAufteilung benutzen
  public static Gebiet findeGebietBeiAufteilungHandleWithCare(GebietUK gebietUK, AbstractGebietDAO gebietDAO, Mandant berichtenderMandant) {
    try {
      Gebiet zielGebiet = gebietDAO.findByUK(gebietUK);
      if (zielGebiet == null || berichtenderMandant == null || zielGebiet.getWahl() == null) {
        return zielGebiet;
      }
      // ELECTKOM-488
      if (zielGebiet.getWahl().getBerichtetAn() != null || zielGebiet.getVerantwortlicheMandantenAbgeleitet().contains(berichtenderMandant)) {
        return zielGebiet;
      }

      LOG.warn(
          "Gebiet in Zielwahl anderen Mandanten zugeordnet: gebietUK=" + gebietUK + ", zielGebiet=" + zielGebiet + ", berichtenderMandant=" + berichtenderMandant
          + ", zielGebiet.getVerantwortlicheMandantenAbgeleitet()=" + zielGebiet.getVerantwortlicheMandantenAbgeleitet());

      return null;
    } catch (NoResultException nre) {
      return null;
    }
  }

  public static GebietUK createGebietUkFuerWahl(AbstractGebiet gebiet, Wahl zielWahl) {
    return new GebietUK(gebiet.getNummer(), new GebietsartUK(gebiet.getGebietsart().getName(), zielWahl.getMandant().getMandantUK()), zielWahl.getWahlUK(),
        zielWahl.getWahltermin().getWahlterminUK());
  }

  protected String bestimmeRollenNameFuerBearbeiten() {
    if (getBearbeitenRolle() != null) {
      return getBearbeitenRolle().getName();
    }
    if (isWurzelgebiet()) {
      return ""; // keine Rolle notwendig
    }
    return getUebergeordnetesGebiet().getRollenNameFuerBearbeiten();
  }

  protected void preSetWahltermin(Wahltermin alterWert, Wahltermin wahltermin) {
    // nichts zu tun
  }

  protected void postSetWahltermin(Wahltermin alterWert, Wahltermin wahltermin) {
    if (getAnsprechpartner() != null) {
      getAnsprechpartner().setWahltermin(wahltermin);
    } else {
      LOG.warn("!!!!!!! Ansprechpartner ist null - sollte außerhalb der Testfälle nicht auftreten!");
    }
  }

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

  public String getGeoId() {
    return getGebietsart().getName() + "_" + getNummer();
  }

  @Override
  public EntityMitMandant getParent() {
    Wahl wahl = getWahl();
    if (wahl != null) {
      return wahl;
    }
    Wahltermin wahltermin = getWahltermin();
    if (wahltermin != null) {
    return wahltermin;
  }
    return null;
  }

  @Override
  public Map<String, VorherNachherWert> createEmptyProtokollierteAttributeMitWertenMap() {
    Map<String, VorherNachherWert> result = new HashMap<>();
    addVerantwortlicherMandantFallsSeitLadenVeraendert(result); // ELECTKOM-487
    return result;
  }

  @Transient
  private transient boolean isSeitLadenVeraendertMandant = false;

  @Transient
  private transient Mandant wertNachLadenVerantwortlicherMandant;

  protected void addVerantwortlicherMandantFallsSeitLadenVeraendert(Map<String, VorherNachherWert> result) {
    // ELECTKOM-487
    if (isSeitLadenVeraendertMandant) {
      result.put(GebietConstants.MESSAGE_KEY_VERANTWORTLICHER_MANDANT_ID_EINZAHL,
          new VorherNachherWert(AttributTyp.LONG, nonNull(wertNachLadenVerantwortlicherMandant) ? wertNachLadenVerantwortlicherMandant.getNummer() : null,
              getVerantwortlicherMandant() != null ? getVerantwortlicherMandant().getNummer() : null));
    }
  }

  protected void postSetVerantwortlicherMandant(Mandant alterWert, Mandant verantwortlicherMandant) {
    // ELECTKOM-487
    if (!isSeitLadenVeraendertMandant) {
      isSeitLadenVeraendertMandant = !EntityUtil.compareObjects(alterWert, verantwortlicherMandant);

      if (isSeitLadenVeraendertMandant) {
        wertNachLadenVerantwortlicherMandant = alterWert;
      }
    }

  }

  protected void preSetVerantwortlicherMandant(Mandant alterWert, Mandant verantwortlicherMandant) {
  }

  @Transient
  private transient Supplier<String> externeIdSupplier; // ELECT-8968

  public Supplier<String> getExterneIdSupplier() {
    if (externeIdSupplier == null) {
      return this::getExterneId;
    }
    return externeIdSupplier;
  }

  public void setExterneIdSupplier(Supplier<String> externeIdSupplier) {
    this.externeIdSupplier = externeIdSupplier;
  }

  public String getGebietsartNummerId() {
    return FileUtil.normalizeZipEntryName(getGebietsart().getName() + "_" + getNummer());
  }

  public Boolean getBriefwahlInUrneErfassen() {
    return false; // ELECT-9380
  }

  public long getTiefeImBaum() {
    if (getUebergeordnetesGebiet() == null) {
      return 0L;
    }

    return 1L + getUebergeordnetesGebiet().getTiefeImBaum();
  }

  public boolean hasAnzahlWahlberechtigteVorlaeufig() {
    return getAnzahlWahlberechtigteVorlaeufig() != null;
  }

  public boolean isBezirk() {
    return false;
  }
}
