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

import java.text.DateFormat;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
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 javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.persistence.Transient;

import de.ivu.elect.business.gebietsbaum.entity.EntityMitGebiet;
import de.ivu.elect.business.gebietsbaum.entity.Gebiet;
import de.ivu.elect.business.gruppe.entity.PolitischeGruppe;
import de.ivu.elect.business.kandidatur.boundary.WaehlerBenannteKandidaturKonfiguration;
import de.ivu.elect.business.kandidatur.entity.Kandidatur;
import de.ivu.elect.business.mandant.entity.EntityMitMandant;
import de.ivu.elect.business.pruefung.boundary.ZulassungPrueferFactory;
import de.ivu.elect.business.stimmzettel.entity.EntityAufStimmzettel;
import de.ivu.elect.business.stimmzettel.entity.EntityAufStimmzettelDaten;
import de.ivu.elect.business.wahl.boundary.WahlZulassungspruefung;
import de.ivu.elect.business.wahl.entity.CommonWahlart;
import de.ivu.elect.business.wahl.entity.Wahl;
import de.ivu.elect.business.wahlvorschlag.boundary.WahlvorschlagUnterstuetzerNotwendigErmittler;
import de.ivu.elect.business.wahlvorschlag.control.WahlvorschlagByGebietComparator;
import de.ivu.elect.business.wahlvorschlagsperson.boundary.AbstractWahlvorschlagsPersonDAO;


import static java.util.Arrays.asList;
import static org.apache.commons.lang3.StringUtils.isNotBlank;

public abstract class AbstractWahlvorschlag implements WahlvorschlagBase, EntityMitGebiet, EntityMitMandant, EntityAufStimmzettel {

  public static final String TRANSIENT_CONTEXT_KEY_WAHLVORSCHLAGS_PERSONEN = "WAHLVORSCHLAGS_PERSONEN";
  public static final String TRANSIENT_CONTEXT_KEY_WVP_WARNUNGEN = "WVP_WARNUNGEN";

  private static final long serialVersionUID = 1L;

  public AbstractWahlvorschlag() {
    super();
  }

  public abstract boolean hasSameWahlvorschlagArt(AbstractWahlvorschlag wahlvorschlag);

  public boolean isParteiWaehlergruppeOderVereinigung() {
    return getGruppeImGebiet() != null && getGruppeImGebiet().isParteiWaehlergruppeOderVereinigung();
  }

  public boolean isEinzelbewerber() {
    return getGruppeImGebiet() != null && getGruppeImGebiet().isEinzelbewerber();
  }

  public boolean isPartei() {
    return getGruppeImGebiet() != null && getGruppeImGebiet().isPartei();
  }

  public boolean isWaehlergruppe() {
    return getGruppeImGebiet() != null && getGruppeImGebiet().isWaehlergruppe();
  }

  public boolean isVereinigung() {
    return getGruppeImGebiet() != null && getGruppeImGebiet().isVereinigung();
  }

  @Deprecated // teure Operation(!)
  public String getNameAufGebiet() {
    return getGruppe().getNameAufGebiet(getGebiet());
  }

  @Deprecated // teure Operation(!)
  public String getKurznameAufGebiet() {
    return getGruppe().getKurznameAufGebiet(getGebiet());
  }

  public boolean hasKurzname() {
    return isNotBlank(getKurzname());
  }

  public boolean hasKennwort() {
    return isNotBlank(getKennwort());
  }

  public String getKurznameOderKennwort() {
    if (hasKurzname()) {
      return getKurzname();
    }

    return getKennwort();
  }

  public boolean isUnterstuetzerErforderlich(WahlvorschlagUnterstuetzerNotwendigErmittler wahlvorschlagUnterstuetzerNotwendigErmittler) {
    return wahlvorschlagUnterstuetzerNotwendigErmittler.isUnterstuetzerErforderlich((Wahlvorschlag) this);
  }

  public boolean isUnterstuetzerErfassbar() {
    return isUnterstuetzerErfassbar(new Date());
  }

  protected boolean isUnterstuetzerErfassbar(Date zeitpunkt) {
    Date unterstuetzerErfassbarAb = getUnterstuetzerErfassbarAbOrDefault();

    return zeitpunkt != null && unterstuetzerErfassbarAb != null && !zeitpunkt.before(unterstuetzerErfassbarAb);
  }

  public Date getUnterstuetzerErfassbarAbOrDefault() {
    Date result = getUnterstuetzerErfassbarAb();

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

    return result;
  }

  // NOTE ELECTMUC-583, dass das Datum der Einreichung auch das Datum bestimmt, ab dem Unterstützer für einen Wahlvorschlag erfassbar sind!
  private Date getUnterstuetzerErfassbarAbDefault() {
    if (getZeitpunktDerEinreichung() == null) {
      return null;
    }

    Calendar calendar = Calendar.getInstance();
    calendar.setTime(getZeitpunktDerEinreichung());
    calendar.add(Calendar.DAY_OF_MONTH, getUnterstuetzerErfassbarAbDefaultOffsetInDays());

    DateFormat dateFormat = DateFormat.getDateInstance();

    try {
      // Behalte Datumsangaben, entferne Zeitangaben.
      return dateFormat.parse(dateFormat.format(calendar.getTime()));
    } catch (ParseException ex) {
      throw new IllegalArgumentException("Datum kann nicht geparsed werden [datum = " + calendar.getTime() + ", format = " + dateFormat + "]", ex);
    }
  }

  private int getUnterstuetzerErfassbarAbDefaultOffsetInDays() {
    PolitischeGruppe politischeGruppe = getGruppe();

    if (politischeGruppe != null && asList( //
        CommonWahlart.BUNDESTAGSWAHL, // ELECT-4516
        CommonWahlart.KREISTAGSWAHL, CommonWahlart.GEMEINDERATSWAHL // ELECTMUC-2484
    ).stream().anyMatch(wahlartTemplate -> wahlartTemplate.equalsWahlart(politischeGruppe.getWahl()))) {
      return 0;
    }

    return 1;
  }

  @Nonnull
  public Long getAnzahlUnterstuetzerGeeignetGesamt(AbstractWahlvorschlagsPersonDAO wahlvorschlagsPersonDAO) {
    return getAnzahlUnterstuetzerGeeignetGesamt(getAnzahlUnterstuetzerGeeignetErfasstIntern(wahlvorschlagsPersonDAO));
  }

  @Nonnull
  public Long getAnzahlUnterstuetzerGeeignetGesamt(Long anzahlUnterstuetzerGeeignetErfasstIntern) {
    return fixNullToZero(getAnzahlUnterstuetzerGeeignetErfasstExtern()) + fixNullToZero(anzahlUnterstuetzerGeeignetErfasstIntern);
  }

  @Nonnull
  public Long getAnzahlUnterstuetzerNichtGeeignetGesamt(AbstractWahlvorschlagsPersonDAO wahlvorschlagsPersonDAO) {
    return getAnzahlUnterstuetzerNichtGeeignetGesamt(getAnzahlUnterstuetzerNichtGeeignetErfasstIntern(wahlvorschlagsPersonDAO));
  }

  @Nonnull
  public Long getAnzahlUnterstuetzerNichtGeeignetGesamt(Long anzahlUnterstuetzerNichtGeeignetErfasstIntern) {
    return fixNullToZero(getAnzahlUnterstuetzerNichtGeeignetErfasstExtern()) + fixNullToZero(anzahlUnterstuetzerNichtGeeignetErfasstIntern);
  }

  public Long getAnzahlUnterstuetzerGeeignetErfasstIntern(AbstractWahlvorschlagsPersonDAO wahlvorschlagsPersonDAO) {
    return wahlvorschlagsPersonDAO.findByCountUnterstuetzerGeeignetByWahlvorschlag((Wahlvorschlag) this);
  }

  public Long getAnzahlUnterstuetzerNichtGeeignetErfasstIntern(AbstractWahlvorschlagsPersonDAO wahlvorschlagsPersonDAO) {
    return wahlvorschlagsPersonDAO.findByCountUnterstuetzerNichtGeeignetByWahlvorschlag((Wahlvorschlag) this);
  }

  public boolean isInGebietshierarchie(Gebiet gebiet) {
    return getGebiet().isInGebietshierarchieUntergeordnetesGebietVon(gebiet) || getGebiet().isInGebietshierarchieUebergeordnetesGebietVon(gebiet);
  }

  @Override
  public Gebiet getGebiet() {
    return getGruppeImGebiet().getGebiet();
  }

  public PolitischeGruppe getGruppe() {
    if (getGruppeImGebiet() == null || getGruppeImGebiet().getGruppe() == null) {
      return null;
    }
    return getGruppeImGebiet().getGruppe().castTo(PolitischeGruppe.class).getEntity();
  }

  public boolean isUngeprueftOderAufStimmzettel() {
    return isUngeprueft() || isAufStimmzettel();
  }

  public boolean isAufStimmzettel() {
    return getWahlvorschlagStatus() != null && getWahlvorschlagStatus().isWahlvorschlagAufStimmzettel();
  }

  public boolean isUngeprueft() {
    return getWahlvorschlagStatus().isUngeprueft();
  }

  /**
   * siehe auch isAufStimmzettel()
   */
  public boolean isZugelassen() {
    return getWahlvorschlagStatus().isZugelassen();
  }

  /**
   * siehe auch isUngeprueftOderAufStimmzettel()
   */
  public boolean isZugelassenOderUnbestimmt() {
    // NOTE, dass dies früher ein Bool'sches Flag war. Genau kann ich nicht sagen, ob diese Abbildung auf das neue Enum
    // nun fachlich identisch ist...
    return getWahlvorschlagStatus() == null || isUngeprueft() || isZugelassen();
  }

  public abstract boolean isListe();

  /**
   * @deprecated castTo(Liste.class) verwenden
   * @see https://support.elect-it.com/wiki/display/ELECT/Entity+Cast+und+InstanceOf
   */
  @Deprecated
  public abstract Liste getListe();

  public abstract boolean isDirektkandidat();

  /**
   * @deprecated castTo(Direktkanddat.class) verwenden
   * @see https://support.elect-it.com/wiki/display/ELECT/Entity+Cast+und+InstanceOf
   */
  @Deprecated
  public abstract Direktkandidat getDirektkandidat();

  public abstract boolean isReferendumOption();

  public abstract ReferendumOption getReferendumOption();

  public Wahl getWahl() {
    return getGruppe().getWahl();
  }

  protected void preSetWahlvorschlagStatus(WahlvorschlagStatus alterWert, WahlvorschlagStatus neuerWert) {
    // nicht benötigt, muss aber implementiert sein
  }

  protected void postSetWahlvorschlagStatus(WahlvorschlagStatus alterWert, WahlvorschlagStatus neuerWert) {
    // nichts zu tun
  }

  public WahlvorschlagStatus getWahlvorschlagStatusInitial() {
    return getWahlZulassungspruefung().getZulassungWahlvorschlagStatusInitial();
  }

  /**
   * NOTE, dass diese Instanz NUR INTERN verwendet werden darf, da die Instanzen von ZulassungPruefer und WaehlerBenannteKandidaturKonfiguration eigentlich injiziert werden müssen!!!
   *
   * @see CurrentWahlZulassungspruefung
   */
  private WahlZulassungspruefung getWahlZulassungspruefung() {
    return new WahlZulassungspruefung(getWahl(), ZulassungPrueferFactory.DEAKTIVIERT_NUR_ZUGELASSENE_WAHLVORSCHLAEGE, new WaehlerBenannteKandidaturKonfiguration());
  }

  @Transient
  private transient Map<Gebiet, WahlvorschlagStimmzettelPosition> stimmzettelPositionCache = new HashMap<>();

  public WahlvorschlagStimmzettelPosition getStimmzettelPosition(EntityAufStimmzettelDaten entityAufStimmzettelDaten) {
    return getStimmzettelPosition(getGebiet(), entityAufStimmzettelDaten);
  }

  @Override
  public WahlvorschlagStimmzettelPosition getStimmzettelPosition(Gebiet gebiet, EntityAufStimmzettelDaten entityAufStimmzettelDaten) {
    if (gebiet != null && stimmzettelPositionCache.containsKey(gebiet)) {
      return stimmzettelPositionCache.get(gebiet);
    }

    WahlvorschlagStimmzettelPosition result = calculateStimmzettelPosition(gebiet, entityAufStimmzettelDaten);

    if (gebiet != null) {
      stimmzettelPositionCache.put(gebiet, result);
    }

    return result;
  }

  public void setStimmzettelPosition(Gebiet gebiet, WahlvorschlagStimmzettelPosition result) {
    stimmzettelPositionCache.put(gebiet, result);
  }

  protected WahlvorschlagStimmzettelPosition calculateStimmzettelPosition(@Nullable Gebiet gebiet, EntityAufStimmzettelDaten entityAufStimmzettelDaten) {
    return entityAufStimmzettelDaten.calculateStimmzettelPosition(gebiet, (Wahlvorschlag) this);
  }

  public boolean isNichtZugelassenImSinneAbgelehnt() {
    return getWahlvorschlagStatus().isNichtZugelassenImSinneAbgelehnt();
  }

  @Deprecated // isNichtZugelassenImSinneAbgelehnt verwenden
  public boolean isNichtZugelassen() {
    return isNichtZugelassenImSinneAbgelehnt();
  }

  public boolean isZugelassenObwohlNichtZulassungsfaehig() {
    return getWahlvorschlagStatus().isNichtZugelassenNachtraeglichKeineZulassungsfaehigkeitBeimZulassungsausschuss();
  }

  public boolean isZulassungNachtraeglichWeggefallen() {
    return getWahlvorschlagStatus().isNichtZugelassenNachtraeglichKeineZulassungsfaehigkeitNachZulassungsausschuss();
  }

  protected String preProcessKurzname(String neuerWert) {
    return neuerWert;
  }

  protected WahlvorschlagStatus preProcessWahlvorschlagStatus(WahlvorschlagStatus neuerWert) {
    return neuerWert;
  }

  protected String preProcessKennwort(String neuerWert) {
    return neuerWert;
  }

  /**
   * NOTE, dass Dubletten NICHT erkannt werden sollen, da sie nicht zu den übergebenen Wahlvorschlägen gehören!
   */
  public static Set<Kandidatur> extractAllKandidaturen(Iterable<Wahlvorschlag> wahlvorschlaege) {
    Set<Kandidatur> result = new HashSet<>();

    for (Wahlvorschlag wahlvorschlag : wahlvorschlaege) {
      result.addAll(wahlvorschlag.extractAllKandidaturen());
    }

    return result;
  }

  /**
   * NOTE, dass Dubletten NICHT erkannt werden sollen, da sie nicht zum Wahlvorschlag gehören!
   */
  public Set<Kandidatur> extractAllKandidaturen() {
    Set<Kandidatur> result = new HashSet<>();

    if (isListe()) {
      Liste liste = (Liste) this;
      result.addAll(extractKandidaturen(liste.getListenkandidaturen()));

    } else if (isDirektkandidat()) {
      Direktkandidat direktkandidat = (Direktkandidat) this;
      result.addAll(extractKandidaturen(asList(direktkandidat.getDirektkandidatur())));

    } else if (isReferendumOption()) {
      return Collections.emptySet();

    } else {
      throw new IllegalStateException("Wahlvorschlag [" + toString() + "] hat nicht unterstützte Wahlvorschlags-Art!");
    }

    return result;
  }

  /**
   * NOTE, dass Dubletten NICHT erkannt werden sollen!
   */
  private static Collection<Kandidatur> extractKandidaturen(Collection< ? extends Kandidatur> kandidaturen) {
    Collection<Kandidatur> result = new HashSet<>();

    for (Kandidatur kandidatur : kandidaturen) {
      result.add(kandidatur);
      result.addAll(kandidatur.getUntergeordneteErsatzkandidaturen());
    }

    return result;
  }

  private static long fixNullToZero(Long value) {
    if (value == null) {
      return 0;
    }

    return value;
  }

  protected void preSetKennwort(String alterWert, String neuerWert) {
    // nichts zu tun
  }

  protected void postSetKennwort(String alterWert, String neuerWert) {
    // Da Einzelbewerber ihre eigene Gruppe bilden, sollte diese jeweils den gleichen Namen haben
    if (isEinzelbewerber() && neuerWert != null && !neuerWert.equals(alterWert)) {
      getGruppe().setName(neuerWert);
    }
  }

  protected void preSetKurzname(String alterWert, String neuerWert) {
    // nichts zu tun
  }

  protected void postSetKurzname(String alterWert, String neuerWert) {
    // Da Einzelbewerber ihre eigene Gruppe bilden, sollte diese jeweils den gleichen Kurznamen haben
    if (isEinzelbewerber()) {
      if (neuerWert != null && !neuerWert.equals(alterWert)) {
        getGruppe().setKurzname(neuerWert);
        // NOTE: Dieser Code wird mit EinzelbewerberBoundaryCreate.verifyCompleteEinzelbewerber() nicht mehr benötigt!
        //      } else if (neuerWert == null && alterWert != null) {
        //        getGruppe().setKurzname(getKennwort()); // Bei Gruppe ist Kurzname nicht nullable
      }
    }
  }

  public static List<Wahlvorschlag> sortByGebiet(Collection<Wahlvorschlag> wahlvorschlaege) {
    List<Wahlvorschlag> result = new ArrayList<>(wahlvorschlaege);

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

    return result;
  }

  // Destatis Europawahl Sonderbehandlung
  protected boolean isWahlMitMehrerenListenGebietsarten() {
    return getWahl().isWahlMitMehrerenListenGebietsarten();
  }

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

  protected <T> boolean wertHatSichGeaendert(T alterWert, T neuerWert) {
    return !Objects.equals(alterWert, neuerWert);
  }
}
