
package de.ivu.elect.business.pruefung.boundary;

import static de.ivu.elect.jee.entity.EntityUtil.castTo;
import static java.util.stream.Collectors.toList;
import static org.apache.commons.lang3.StringUtils.defaultIfBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

import javax.annotation.Nullable;

import org.apache.commons.lang3.StringUtils;

import de.ivu.elect.business.ergebnis.control.StimmenPoolFactory;
import de.ivu.elect.business.gruppeimgebiet.control.GruppeImGebietReihenfolgeVorsortierer;
import de.ivu.elect.business.kandidatur.boundary.AbstractListenkandidaturDAO;
import de.ivu.elect.business.kandidatur.control.ListenkandidaturByStimmzettelPositionComparator;
import de.ivu.elect.business.kandidatur.control.StimmzettelFormatter;
import de.ivu.elect.business.kandidatur.entity.AbstractKandidatur;
import de.ivu.elect.business.kandidatur.entity.Direktkandidatur;
import de.ivu.elect.business.kandidatur.entity.Kandidatur;
import de.ivu.elect.business.kandidatur.entity.KandidaturStatus;
import de.ivu.elect.business.kandidatur.entity.Listenkandidatur;
import de.ivu.elect.business.pruefung.control.WusZulassungPruefProzess;
import de.ivu.elect.business.pruefung.control.WusZulassungPrueferCache;
import de.ivu.elect.business.pruefung.control.WusZulassungPrueferCacheHashMap;
import de.ivu.elect.business.stimmzettel.boundary.EntityAufStimmzettelDatenStimmzettelPositionBerechnung;
import de.ivu.elect.business.stimmzettel.boundary.EntityAufStimmzettelDatenVorgeladenFactory;
import de.ivu.elect.business.stimmzettel.entity.EntityAufStimmzettelDaten;
import de.ivu.elect.business.stimmzettel.entity.EntityAufStimmzettelDatenFactory;
import de.ivu.elect.business.wahl.entity.AbstractWahlParameterLoader;
import de.ivu.elect.business.wahl.entity.Wahl;
import de.ivu.elect.business.wahlrechtspruefung.boundary.AbstractWahlrechtspruefungDAO;
import de.ivu.elect.business.wahlrechtspruefung.control.PruefkriteriumByPositionComparator;
import de.ivu.elect.business.wahlrechtspruefung.entity.AbstractWahlrechtspruefung;
import de.ivu.elect.business.wahlrechtspruefung.entity.Pruefkriterium;
import de.ivu.elect.business.wahlrechtspruefung.entity.Wahlrechtspruefung;
import de.ivu.elect.business.wahlvorschlag.boundary.AbstractWahlvorschlagDAO;
import de.ivu.elect.business.wahlvorschlag.entity.AbstractWahlvorschlag;
import de.ivu.elect.business.wahlvorschlag.entity.Direktkandidat;
import de.ivu.elect.business.wahlvorschlag.entity.Liste;
import de.ivu.elect.business.wahlvorschlag.entity.Wahlvorschlag;
import de.ivu.elect.business.wahlvorschlagsperson.boundary.AbstractWahlvorschlagsPersonDAO;
import de.ivu.elect.jee.faces.Messages;

/**
 * NICHT PER @Inject!!!!
 *
 * A.k.a. KOMPLEXE Zulassungspruefung...
 *
 * @see ZulassungPrueferFactory
 */
public class WusZulassungPruefer extends CoreZulassungPruefer {

  private static final long serialVersionUID = 1L;

  private static final int MAX_ANZAHL_GRUENDE_ANGEZEIGT = 3;

  private final WusZulassungPruefProzess zulassungPruefProzess;

  private final AbstractWahlParameterLoader wahlParameterLoader;

  private final StimmzettelFormatter stimmzettelFormatter;

  private final EntityAufStimmzettelDatenStimmzettelPositionBerechnung entityAufStimmzettelDatenStimmzettelPositionBerechnung;

  private final AbstractWahlrechtspruefungDAO wahlrechtspruefungDAO;

  private final AbstractWahlvorschlagDAO wahlvorschlagDAO;

  @Nullable
  private final AbstractListenkandidaturDAO listenkandidaturDAO;

  private final AbstractWahlvorschlagsPersonDAO wahlvorschlagsPersonDAO;

  private final StimmenPoolFactory stimmenPoolFactory;

  private final GruppeImGebietReihenfolgeVorsortierer gruppeImGebietReihenfolgeVorsortierer;

  private final Messages messages;

  private WusZulassungPrueferCache cache = new WusZulassungPrueferCacheHashMap(); // ELECTMUC-1842 Cache verwenden... siehe aber ZulassungPrueferExtendedDAOFactory#createZulassungPruefer!

  /*
   * ******************************************************************
   *
   * Kandidatur
   */

  /**
   * HINWEIS Zum Erstellen die Klassen "ZulassungPrueferRequestScope", "ZulassungPrueferDAOFactory" oder "ZulassungPrueferExtendedDAOFactory" verwenden!
   */
  public WusZulassungPruefer(WusZulassungPruefProzess zulassungPruefProzess, AbstractWahlParameterLoader wahlParameterLoader, StimmzettelFormatter stimmzettelFormatter,
      EntityAufStimmzettelDatenStimmzettelPositionBerechnung entityAufStimmzettelDatenStimmzettelPositionBerechnung, AbstractWahlrechtspruefungDAO wahlrechtspruefungDAO,
      AbstractWahlvorschlagDAO wahlvorschlagDAO, @Nullable AbstractListenkandidaturDAO listenkandidaturDAO, AbstractWahlvorschlagsPersonDAO wahlvorschlagsPersonDAO,
      StimmenPoolFactory stimmenPoolFactory, GruppeImGebietReihenfolgeVorsortierer gruppeImGebietReihenfolgeVorsortierer, Messages messages) {
    super();

    this.zulassungPruefProzess = zulassungPruefProzess;

    this.wahlParameterLoader = wahlParameterLoader;

    this.stimmzettelFormatter = stimmzettelFormatter;

    this.entityAufStimmzettelDatenStimmzettelPositionBerechnung = entityAufStimmzettelDatenStimmzettelPositionBerechnung;

    this.wahlrechtspruefungDAO = wahlrechtspruefungDAO;
    this.wahlvorschlagDAO = wahlvorschlagDAO;
    this.listenkandidaturDAO = listenkandidaturDAO;
    this.wahlvorschlagsPersonDAO = wahlvorschlagsPersonDAO;

    this.stimmenPoolFactory = stimmenPoolFactory;

    this.gruppeImGebietReihenfolgeVorsortierer = gruppeImGebietReihenfolgeVorsortierer;

    this.messages = messages;
  }

  @Override
  public boolean isZulassenErlaubtUndNichtAbgelehnt(Kandidatur kandidatur) {
    if (kandidatur == null) {
      return false;
    }

    if (kandidatur.isNichtZugelassenImSinneAbgelehnt()) {
      return false;
    }

    return isZulassenErlaubt(kandidatur);
  }

  @Override
  public boolean isZulassenErlaubt(Kandidatur kandidatur) {
    if (kandidatur == null) {
      return false;
    }

    // Bei bereits zugelassenen Kandidaturen ist die Prüfung immer positiv, damit die Stimmzettelposition ausgegeben wird
    if (kandidatur.isZugelassen() /* war vor ELECT-11250 .isAufStimmzettel() */) {
      return true;
    }

    if (cache.isZulassenErlaubtKandidaturCacheContainsKey(kandidatur)) {
      return cache.getIsZulassenErlaubtKandidaturCache(kandidatur);
    }

    List<Wahlrechtspruefung> wahlrechtspruefungen = collectWahlrechtspruefungen(kandidatur);

    boolean result = wahlrechtspruefungen.stream().allMatch(AbstractWahlrechtspruefung::isZulassenErlaubt);

    if (result) {
      if (kandidatur.isDirektkandidatur()) { // Spezialfall Direktkandidatur, diese wird wie ein Wahlvorschlag behandelt
        result = isZulassenErlaubt(castTo(kandidatur, Direktkandidatur.class).getDirektkandidat());
      } else {
        result = isZulassungErlaubtPruefProzessKandidatur(kandidatur);
      }
    }

    cache.putIsZulassenErlaubtKandidaturCache(kandidatur, result);

    return result;
  }

  @Override
  public List<String> getGruendeFuerVerweigerungDerZulassung(Kandidatur kandidatur) {
    if (kandidatur == null) {
      return new ArrayList<>();
    }

    List<String> result = cache.getGruendeFuerVerweigerungDerZulassungKandidaturCache(kandidatur);

    if (result == null) {
      result = new ArrayList<>();

      cache.putGruendeFuerVerweigerungDerZulassungKandidaturCache(kandidatur, result);

      if (kandidatur.isDirektkandidatur()) { // Spezialfall Direktkandidatur, diese wird wie ein Wahlvorschlag behandelt
        result.addAll(getGruendeFuerVerweigerungDerZulassung(castTo(kandidatur, Direktkandidatur.class).getDirektkandidat()));
      } else {
        result.addAll(getPruefkriterienNichtErfuelltGruende(collectWahlrechtspruefungen(kandidatur)));

        addGruendeFuerVerweigerungDerZulassungPruefProzessKandidatur(result, kandidatur);
      }
    }

    return result;
  }

  @Override
  public String getBegruendungDerZulassungsverweigerung(Kandidatur kandidatur) {
    return getBegruendungByGruendeDerZulassungsverweigerung(getGruendeFuerVerweigerungDerZulassung(kandidatur));
  }

  /*
   * ******************************************************************
   *
   * Wahlvorschlag
   */

  @Override
  public boolean isZulassenErlaubt(Wahlvorschlag wahlvorschlag) {
    if (wahlvorschlag == null) {
      return false;
    }

    // Bei bereits zugelassenen Wahlvorschlägen ist die Prüfung immer positiv, damit die Stimmzettelposition ausgegeben wird
    if (wahlvorschlag.isZugelassen() /* war vor ELECT-11250 .isAufStimmzettel() */) {
      return true;
    }

    if (cache.isZulassenErlaubtWahlvorschlagCacheContainsKey(wahlvorschlag)) {
      return cache.getIsZulassenErlaubtWahlvorschlagCache(wahlvorschlag);
    }

    List<Wahlrechtspruefung> wahlrechtspruefungen = collectWahlrechtspruefungen(wahlvorschlag);

    boolean result = wahlrechtspruefungen.stream().allMatch(AbstractWahlrechtspruefung::isZulassenErlaubt);

    if (result) {
      if (wahlvorschlag.isListe()) {
        Liste liste = castTo(wahlvorschlag, Liste.class);
        liste.setListenkandidaturenPreloader(listenkandidaturDAO); // ELECT-11498

        Wahl wahl = liste.getWahl();
        Collection<Listenkandidatur> listenkandidaturen = liste.getListenkandidaturen();

        result = !isListenkandidaturVorhanden(listenkandidaturen) || isZulassungErlaubtPruefProzessListenkandidaturen(listenkandidaturen);

        if (result && !isLeereListeErlaubt(wahl)) {
          result = isListenkandidaturVorhanden(listenkandidaturen);
        }

        if (result && wahl.isWahlMitMehrfachnennungen()) {
          result = isAnzahlNennungenGeordnet(liste);
        }
      } else if (wahlvorschlag.isDirektkandidat()) {
        result = isZulassungErlaubtPruefProzessKandidatur(castTo(wahlvorschlag, Direktkandidat.class).getDirektkandidatur());
      }
    }

    if (result) {
      result = isZulassungErlaubtPruefProzessWahlvorschlag(wahlvorschlag);
    }

    cache.putIsZulassenErlaubtWahlvorschlagCache(wahlvorschlag, result);

    return result;
  }

  private boolean isLeereListeErlaubt(Wahl wahl) {
    return wahl.isWahlMitLeereListenErlaubt();
  }

  private boolean isListenkandidaturVorhanden(Collection<Listenkandidatur> listenkandidaturen) {
    return !listenkandidaturen.isEmpty();
  }

  private boolean isZulassungErlaubtPruefProzessListenkandidaturen(Collection<Listenkandidatur> listenkandidaturen) {
    boolean existsListenkandidaturZugelassen = false;

    for (Listenkandidatur listenkandidatur : listenkandidaturen) {
      if (listenkandidatur.isEinzelbewerber()) {
        return true; // ELECT-12034
      }

      if (listenkandidatur.isUngeprueft()) {
        return false;
      }

      if (listenkandidatur.isZugelassen()) {
        existsListenkandidaturZugelassen = true;
      }
    }

    if (existsListenkandidaturZugelassen) {
      return true;
    }

    return false;
  }

  /*
   * Hinweis: Betrachte Listenkandidaturen, die weder Nachrücker, noch nicht zugelassen sind.
   */
  private boolean isAnzahlNennungenGeordnet(Liste liste) {
    liste.setListenkandidaturenPreloader(listenkandidaturDAO); // ELECT-11498
    List<Listenkandidatur> kandidaturenSortiertNachStimmzettelPosition = new ArrayList<>(liste.getListenkandidaturen());
    kandidaturenSortiertNachStimmzettelPosition.sort(new ListenkandidaturByStimmzettelPositionComparator(getEntityAufStimmzettelDaten(liste)));

    Long anzahlNennungenZuletztBetrachtet = null;

    for (Listenkandidatur listenkandidatur : kandidaturenSortiertNachStimmzettelPosition) {
      if (anzahlNennungenZuletztBetrachtet != null && !listenkandidatur.isNichtZugelassenImSinneAbgelehnt() && listenkandidatur.getListenkandidaturArt().isListenkandidatur()
          && listenkandidatur.getAnzahlNennungen() > anzahlNennungenZuletztBetrachtet) {
        return false;
      }

      anzahlNennungenZuletztBetrachtet = listenkandidatur.getAnzahlNennungen();
    }

    return true;
  }

  @Override
  public List<String> getGruendeFuerVerweigerungDerZulassung(Wahlvorschlag wahlvorschlag) {
    if (wahlvorschlag == null) {
      return new ArrayList<>();
    }

    List<String> result = cache.getGruendeFuerVerweigerungDerZulassungWahlvorschlagCache(wahlvorschlag);

    if (result == null) {
      result = new ArrayList<>();

      cache.putGruendeFuerVerweigerungDerZulassungWahlvorschlagCache(wahlvorschlag, result);

      result.addAll(getPruefkriterienNichtErfuelltGruende(collectWahlrechtspruefungen(wahlvorschlag)));

      if (wahlvorschlag.isListe()) {
        Liste liste = castTo(wahlvorschlag, Liste.class);
        liste.setListenkandidaturenPreloader(listenkandidaturDAO); // ELECT-11498

        Wahl wahl = liste.getWahl();
        Collection<Listenkandidatur> listenkandidaturen = liste.getListenkandidaturen();

        long anzahlListenkandidaturenUngeprueft = getAnzahlKandidatenImStatus(listenkandidaturen, KandidaturStatus.OFFEN);

        if (anzahlListenkandidaturenUngeprueft > 0) {
          result.add(getMessage("WahlvorschlagZulassungPruefer.ungepruefte.listenkandidaturen.vorhanden", anzahlListenkandidaturenUngeprueft));
        }

        if (isListenkandidaturVorhanden(listenkandidaturen) && !isMindEineKandidaturZugelassen(listenkandidaturen)) {
          result.add(getMessage("WahlvorschlagZulassungPruefer.mind.eine.zugelassene.listenkandidatur.benoetigt"));
        }

        if (!isLeereListeErlaubt(wahl) && !isListenkandidaturVorhanden(listenkandidaturen)) {
          result.add(getMessage("WahlvorschlagZulassungPruefer.leere.liste.nicht.erlaubt"));
        }

        if (wahl.isWahlMitMehrfachnennungen() && !isAnzahlNennungenGeordnet(liste)) {
          result.add(getMessage("WahlvorschlagZulassungPruefer.anzahl.nennungen.listenkandidaturen.nicht.geordnet"));
        }
      } else if (wahlvorschlag.isDirektkandidat()) {
        addGruendeFuerVerweigerungDerZulassungPruefProzessKandidatur(result, castTo(wahlvorschlag, Direktkandidat.class).getDirektkandidatur());
      }

      addGruendeFuerVerweigerungDerZulassungPruefProzessWahlvorschlag(wahlvorschlag, result);
    }

    return result;
  }

  // NOTE: Erweiterungen des Prüfprozesses künftig besser hier unterbringen!
  private boolean isZulassungErlaubtPruefProzessKandidatur(Kandidatur kandidatur) {
    return zulassungPruefProzess.isZulassungErlaubt(kandidatur, getEntityAufStimmzettelDaten(kandidatur));
  }

  private void addGruendeFuerVerweigerungDerZulassungPruefProzessKandidatur(List<String> result, Kandidatur kandidatur) {
    zulassungPruefProzess.addGruendeFuerVerweigerungDerZulassung(result, kandidatur, getEntityAufStimmzettelDaten(kandidatur));
  }

  // NOTE: Erweiterungen des Prüfprozesses künftig besser hier unterbringen!
  private boolean isZulassungErlaubtPruefProzessWahlvorschlag(Wahlvorschlag wahlvorschlag) {
    return zulassungPruefProzess.isZulassungErlaubt(wahlvorschlag, wahlvorschlagsPersonDAO, getEntityAufStimmzettelDaten(wahlvorschlag));
  }

  private void addGruendeFuerVerweigerungDerZulassungPruefProzessWahlvorschlag(Wahlvorschlag wahlvorschlag, List<String> result) {
    zulassungPruefProzess.addGruendeFuerVerweigerungDerZulassung(result, wahlvorschlag, wahlvorschlagsPersonDAO, getEntityAufStimmzettelDaten(wahlvorschlag));
  }

  private long getAnzahlKandidatenImStatus(Collection<Listenkandidatur> listenkandidaturen, KandidaturStatus kandidaturStatus) {
    return listenkandidaturen.stream().filter(listenkandidatur -> listenkandidatur.getKandidaturStatus().equals(kandidaturStatus)).count();
  }

  private boolean isMindEineKandidaturZugelassen(Collection<Listenkandidatur> listenkandidaturen) {
    return listenkandidaturen.stream().anyMatch(AbstractKandidatur::isZugelassen);
  }

  @Override
  public String getBegruendungDerZulassungsverweigerung(Wahlvorschlag wahlvorschlag) {
    return getBegruendungByGruendeDerZulassungsverweigerung(getGruendeFuerVerweigerungDerZulassung(wahlvorschlag));
  }

  /*
   * ******************************************************************
   *
   * Hilfsmethoden
   */

  private String getBegruendungByGruendeDerZulassungsverweigerung(List<String> verweigerungsgruende) {
    StringBuilder begruendung = new StringBuilder();

    int count = 0;

    for (String verweigerungsgrund : verweigerungsgruende) {
      begruendung.append(verweigerungsgrund);
      begruendung.append(" ");

      count++;

      int anzahlUebrigerGruende = verweigerungsgruende.size() - count;

      if (count >= MAX_ANZAHL_GRUENDE_ANGEZEIGT && anzahlUebrigerGruende > 0) {
        begruendung.append(getMessage("ZulassungPruefer.weitere.gruende", anzahlUebrigerGruende));

        break;
      }
    }

    return begruendung.toString();
  }

  protected List<String> getPruefkriterienNichtErfuelltGruende(List<Wahlrechtspruefung> wahlrechtspruefungen) {
    List<String> result = new ArrayList<>();

    List<Wahlrechtspruefung> wahlrechtspruefungenRelevant = wahlrechtspruefungen.stream().filter(wahlrechtspruefung -> !wahlrechtspruefung.isZulassenErlaubt()).collect(toList());
    int wahlrechtspruefungenRelevantCount = wahlrechtspruefungenRelevant.size();

    if (wahlrechtspruefungenRelevantCount > 0) {
      String summary = getMessage("ZulassungPruefer.pruefkriterium.nicht.erfuellt", wahlrechtspruefungenRelevantCount);
      String summaryFormatted = getMessage("ZulassungPruefer.pruefkriterium.nicht.erfuellt.format", summary, wahlrechtspruefungenRelevantCount);

      if (Objects.equals(summaryFormatted, summary)) {
        result.add(summary);
      } else {
        List<String> kriteriumtexte = wahlrechtspruefungenRelevant.stream().map(Wahlrechtspruefung::getPruefkriterium).sorted(PruefkriteriumByPositionComparator.ASCENDING)
            .map(Pruefkriterium::getKriteriumtext).filter(StringUtils::isNotBlank).collect(toList());

        if (kriteriumtexte.isEmpty()) {
          result.add(defaultIfBlank(summaryFormatted, summary));
        } else {
          if (isNotBlank(summaryFormatted)) {
            result.add(summaryFormatted);
          }

          result.addAll(kriteriumtexte);
        }
      }
    }

    return result;
  }

  public void setCache(WusZulassungPrueferCache cache) {
    this.cache = cache;
  }

  private String getMessage(String msgKey, Object... params) {
    return messages.getMessageForMessageKey(msgKey, params);
  }

  private EntityAufStimmzettelDaten getEntityAufStimmzettelDaten(Kandidatur kandidatur) {
    return createEntityAufStimmzettelDatenFactory(kandidatur.getWahl()).getEntityAufStimmzettelDaten();
  }

  private EntityAufStimmzettelDaten getEntityAufStimmzettelDaten(Wahlvorschlag wahlvorschlag) {
    return createEntityAufStimmzettelDatenFactory(wahlvorschlag.getWahl()).getEntityAufStimmzettelDaten();
  }

  private EntityAufStimmzettelDatenFactory createEntityAufStimmzettelDatenFactory(Wahl wahl) {
    EntityAufStimmzettelDatenFactory result = cache.getEntityAufStimmzettelDatenFactoryCache(wahl);

    if (result == null) {
      result = new EntityAufStimmzettelDatenVorgeladenFactory(wahlParameterLoader, this, stimmzettelFormatter, entityAufStimmzettelDatenStimmzettelPositionBerechnung, stimmenPoolFactory,
          gruppeImGebietReihenfolgeVorsortierer, loadWahlvorschlaege(wahl));

      cache.putEntityAufStimmzettelDatenFactoryCache(wahl, result);
    }

    return result;
  }

  private List<Wahlvorschlag> loadWahlvorschlaege(Wahl wahl) {
    List<Wahlvorschlag> wahlvorschlaege = wahlvorschlagDAO.findByWahl(wahl);
    wahlvorschlaege.stream().filter(AbstractWahlvorschlag::isListe).map(l -> l.castTo(Liste.class).getEntity()).forEach(l -> l.setListenkandidaturenPreloader(listenkandidaturDAO)); // ELECT-11498
    return wahlvorschlaege;
  }

  @Override
  public List<Wahlrechtspruefung> collectWahlrechtspruefungen(Kandidatur kandidatur) {
    return wahlrechtspruefungDAO.findByKandidatur(kandidatur);
  }

  @Override
  public List<Wahlrechtspruefung> collectWahlrechtspruefungen(Wahlvorschlag wahlvorschlag) {
    List<Wahlrechtspruefung> wahlrechtspruefungen;

    if (wahlvorschlag.isListe()) {
      wahlrechtspruefungen = wahlrechtspruefungDAO.findByWahlvorschlagAndKandidaturIsNull(wahlvorschlag);
    } else {
      wahlrechtspruefungen = wahlrechtspruefungDAO.findByWahlvorschlag(wahlvorschlag).stream()
          .filter(wahlrechtspruefung -> wahlrechtspruefung.getKandidatur() == null || !wahlrechtspruefung.getKandidatur().isErsatzkandidatur()) // ELECTMUC-2754
          .collect(Collectors.toList());
    }

    return wahlrechtspruefungen;
  }
}
