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

import static java.util.Collections.emptyList;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;

import javax.annotation.Nullable;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.ivu.elect.business.gebietsbaum.control.GebietByBreadthFirstComparator;
import de.ivu.elect.business.gebietsbaum.entity.Gebiet;
import de.ivu.elect.business.gruppe.entity.GruppePK;
import de.ivu.elect.business.gruppe.entity.NlPolitischeGruppe;
import de.ivu.elect.business.gruppe.entity.PolitischeGruppe;
import de.ivu.elect.business.kandidatur.boundary.AbstractListenkandidaturDAO;
import de.ivu.elect.business.kandidatur.entity.AbstractKandidatur;
import de.ivu.elect.business.kandidatur.entity.Kandidatur;
import de.ivu.elect.business.kandidatur.entity.Listenkandidatur;
import de.ivu.elect.business.pruefung.boundary.ZulassungPruefer;
import de.ivu.elect.business.security.Customer;
import de.ivu.elect.business.stimmzettel.entity.EntityAufStimmzettelDaten;
import de.ivu.elect.business.wahlrechtspruefung.boundary.AbstractWahlrechtspruefungDAO;
import de.ivu.elect.business.wahlrechtspruefung.entity.Wahlrechtspruefung;
import de.ivu.elect.business.wahlvorschlagsperson.entity.WahlvorschlagsPersonTyp;

public class NlListe {

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

  public static final String ZULASSUNG_ABGELEHNT_BEGRUENDUNG_SEPARATOR = NlPolitischeGruppe.ZULASSUNG_ABGELEHNT_BEGRUENDUNG_SEPARATOR;

  public static final WahlvorschlagsPersonTyp LISTENBEAUFTRAGTER = WahlvorschlagsPersonTyp.BEAUFTRAGTER;

  public static final String PUBLICATION_LANGUAGE_NIEDERLAENDISCH = "nl";

  public static final String PUBLICATION_LANGUAGE_FRIESISCH = "fy";

  private static final Long PUBLISH_GENDER_VALUE = 1L;

  private static final Long NOT_PUBLISH_GENDER_VALUE = -1L;

  private static final Long FRIESISCH_VALUE = 1L;

  private static final Long NOT_FRIESISCH_VALUE = -1L;

  private static final String LISTE_IDENTISCH_KEY = "LISTE_IDENTISCH_KEY";

  /**
   * ELECT-9853
   */
  public static void setStelNummer(Liste liste, Integer stelNummer, Customer customer) {
    if (Customer.NL_WUS.equals(customer)) {
      liste.setListenverbindungEingereicht(null); // wird ausgerechnet
    } else {
      if (stelNummer == null) {
        liste.setListenverbindungEingereicht(null);
      } else {
        liste.setListenverbindungEingereicht(Integer.toString(stelNummer));
      }
    }
  }

  /**
   * ELECT-9777
   */
  public static boolean isBlancoListe(Liste liste) {
    return new NlPolitischeGruppe(liste).isBlancoListe();
  }

  /**
   * ELECT-9777
   */
  public static void setBlancoListe(Liste liste, String name) {
    setName(liste, name);

    new NlPolitischeGruppe(liste).setBlancoListe(name);
  }

  public static void setPublishGender(Liste liste, boolean publishGender) {
    // ELECT-9881
    if (liste != null) {
      if (publishGender) {
        liste.setAnzahlUnterstuetzerGeeignetErfasstExtern(PUBLISH_GENDER_VALUE);
      } else {
        liste.setAnzahlUnterstuetzerGeeignetErfasstExtern(NOT_PUBLISH_GENDER_VALUE);
      }
    }
  }

  public static boolean isPublishGender(Liste liste) {
    return liste != null && !NOT_PUBLISH_GENDER_VALUE.equals(liste.getAnzahlUnterstuetzerGeeignetErfasstExtern());
  }

  public static void setPublicationLanguage(Liste liste, String publicationLanguage) {
    // ELECT-10274 Erwartete Werte: "nl" oder "fy"
    if (liste != null) {
      if (PUBLICATION_LANGUAGE_FRIESISCH.equalsIgnoreCase(publicationLanguage)) {
        liste.setAnzahlUnterstuetzerNichtGeeignetErfasstExtern(FRIESISCH_VALUE);
      } else {
        liste.setAnzahlUnterstuetzerNichtGeeignetErfasstExtern(NOT_FRIESISCH_VALUE);
      }
    }
  }

  public static boolean isPublicationLanguageFriesisch(Liste liste) {
    return liste != null && FRIESISCH_VALUE.equals(liste.getAnzahlUnterstuetzerNichtGeeignetErfasstExtern());
  }

  public static Long getOrdnungszahl(Liste liste) { // ELECT-15512
    return new NlPolitischeGruppe(liste).getRelativePosition();
  }

  public static String getName(Liste liste) {
    return getName(liste.getGruppe());
  }

  private static String getName(PolitischeGruppe gruppe) {
    return gruppe.getName(); // ELECT-14287
  }

  public static void setName(Liste liste, String name) {
    liste.setKurzname(name);
    liste.setKennwort(name);

    new NlPolitischeGruppe(liste).setName(name);
  }

  public static boolean isListeEinesStelWas(Liste liste) {
    return NlListe.getStelNummerWas(liste).isPresent();
  }

  @Deprecated
  public static boolean isEinzelneListe(Liste liste, List<Liste> alleListen) {
    return alleListen.stream().filter(other -> liste.getGruppe().equals(other.getGruppe())).count() == 1;
  }

  public static String createStelKey(GruppePK gruppePk, Integer stelNummer) {
    return gruppePk + "_" + stelNummer;
  }

  public static boolean isIdentisch(Liste liste1, Liste liste2, EntityAufStimmzettelDaten entityAufStimmzettelDaten) {

    if (liste1.equals(liste2)) {
      return true;
    }

    if (!liste1.isZugelassenOderUnbestimmt() || !liste2.isZugelassenOderUnbestimmt()) {
      return false;
    }

    if (!liste1.getGruppe().equals(liste2.getGruppe())) {
      return false;
    }

    Object cached = liste1.getTransientContext().get(LISTE_IDENTISCH_KEY + liste2.getId());

    if (cached == null) {
      cached = calculateIdentisch(liste1, liste2, entityAufStimmzettelDaten);
      liste1.getTransientContext().put(LISTE_IDENTISCH_KEY + liste2.getId(), cached);
      liste2.getTransientContext().put(LISTE_IDENTISCH_KEY + liste1.getId(), cached);
    }

    return (Boolean) cached;
  }

  private static boolean calculateIdentisch(Liste liste1, Liste liste2, EntityAufStimmzettelDaten entityAufStimmzettelDaten) {
    NlListeIdentischCache nlListeIdentischCache = (NlListeIdentischCache) entityAufStimmzettelDaten.getCache().computeIfAbsent("NlListeIdentischCache", x -> new NlListeIdentischCache());

    Boolean result = nlListeIdentischCache.getCachedResult(liste1, liste2);
    if (result != null) {
      return result;
    }

    for (Listenkandidatur listenkandidatur : getListenkandidaturenAusListe(liste1)) {
      if (listenkandidatur.isUngeprueftOderZugelassen()) {
        if (!isVerknuepfteKandidaturAufListeAnSelberPositionNichtNichtZugelassen(listenkandidatur, liste2, listenkandidatur.getStimmzettelPositionMitCache(entityAufStimmzettelDaten).getPosition(),
            entityAufStimmzettelDaten)) {
          return nlListeIdentischCache.add(liste1, liste2, false);
        }
      }
    }
    for (Listenkandidatur listenkandidatur : getListenkandidaturenAusListe(liste2)) {
      if (listenkandidatur.isUngeprueftOderZugelassen()) {
        if (!isVerknuepfteKandidaturAufListeAnSelberPositionNichtNichtZugelassen(listenkandidatur, liste1, listenkandidatur.getStimmzettelPositionMitCache(entityAufStimmzettelDaten).getPosition(),
            entityAufStimmzettelDaten)) {
          return nlListeIdentischCache.add(liste1, liste2, false);
        }
      }
    }

    return nlListeIdentischCache.add(liste1, liste2, true);
  }

  private static Collection<Listenkandidatur> getListenkandidaturenAusListe(Liste liste) {
    return liste.getListenkandidaturen();
  }

  private static boolean isVerknuepfteKandidaturAufListeAnSelberPositionNichtNichtZugelassen(Listenkandidatur listenkandidatur, Liste andereListe, Long position,
      EntityAufStimmzettelDaten entityAufStimmzettelDaten) {
    return getListenkandidaturenAusListe(andereListe).stream().filter(AbstractKandidatur::isUngeprueftOderZugelassen).filter(other -> isGleichePerson(listenkandidatur, other))
        .map(andereListenkandidatur -> andereListenkandidatur.getStimmzettelPositionMitCache(entityAufStimmzettelDaten).getPosition())
        .anyMatch(anderePosition -> Objects.equals(position, anderePosition));
  }

  private static boolean isGleichePerson(Listenkandidatur listenkandidatur, Listenkandidatur other) {
    // ELECT-11466
    if (StringUtils.isNotBlank(listenkandidatur.getExterneId())) {
      return listenkandidatur.getExterneId().equals(other.getExterneId());
    }

    // vor ELECT-11466
    return listenkandidatur.getIdentischVerknuepfteListenkandidaturen().contains(other);
  }

  public static List<String> getZulassungAbgelehntBegruendung(Wahlvorschlag wahlvorschlag) {
    if (wahlvorschlag != null) {
      return getPolitischeGruppe(wahlvorschlag).getZulassungAbgelehntBegruendung(wahlvorschlag);
    }

    return emptyList();
  }

  public static void setZulassungAbgelehntBegruendung(Wahlvorschlag wahlvorschlag, String begruendung) {
    if (wahlvorschlag != null) {
      getPolitischeGruppe(wahlvorschlag).setZulassungAbgelehntBegruendung(wahlvorschlag, begruendung);
    }
  }

  public static List<String> formatPruefungenZuBehebenLangtexteExcludingZulassungPrueferExtraInformation(ZulassungPruefer zulassungPruefer, Wahlvorschlag wahlvorschlag) {
    if (wahlvorschlag != null) {
      return getPolitischeGruppe(wahlvorschlag).formatPruefungenZuBehebenLangtexteExcludingZulassungPrueferExtraInformation(zulassungPruefer, wahlvorschlag);
    }

    return emptyList();
  }

  public static List<String> formatPruefungenZuBehebenLangtexteIncludingZulassungPrueferExtraInformation(ZulassungPruefer zulassungPruefer, Wahlvorschlag wahlvorschlag) {
    if (wahlvorschlag != null) {
      return getPolitischeGruppe(wahlvorschlag).formatPruefungenZuBehebenLangtexteIncludingZulassungPrueferExtraInformation(zulassungPruefer, wahlvorschlag);
    }

    return emptyList();
  }

  public static List<String> formatPruefungenZuBehebenKurztexteIncludingZulassungPrueferExtraInformation(ZulassungPruefer zulassungPruefer, Wahlvorschlag wahlvorschlag) {
    if (wahlvorschlag != null) {
      return getPolitischeGruppe(wahlvorschlag).formatPruefungenZuBehebenKurztexteIncludingZulassungPrueferExtraInformation(zulassungPruefer, wahlvorschlag);
    }

    return emptyList();
  }

  public static List<String> formatPruefungenBehobenLangtexte(ZulassungPruefer zulassungPruefer, Wahlvorschlag wahlvorschlag) {
    if (wahlvorschlag != null) {
      return getPolitischeGruppe(wahlvorschlag).formatPruefungenBehobenLangtexte(zulassungPruefer, wahlvorschlag);
    }

    return emptyList();
  }

  public static List<String> formatPruefungenBehobenKurztexte(ZulassungPruefer zulassungPruefer, Wahlvorschlag wahlvorschlag) {
    if (wahlvorschlag != null) {
      return getPolitischeGruppe(wahlvorschlag).formatPruefungenBehobenKurztexte(zulassungPruefer, wahlvorschlag);
    }

    return emptyList();
  }

  private static NlPolitischeGruppe getPolitischeGruppe(Wahlvorschlag wahlvorschlag) {
    return new NlPolitischeGruppe(wahlvorschlag);
  }

  public static StelInformationen getStelInformationen(Liste liste, Collection<Liste> alleListen, EntityAufStimmzettelDaten entityAufStimmzettelDaten,
      @Nullable AbstractListenkandidaturDAO listenkandidaturDAO) {
    preloadListenkandidaturen(liste, alleListen, listenkandidaturDAO);
    return getStelInformationen(liste, alleListen, entityAufStimmzettelDaten);
  }

  private static void preloadListenkandidaturen(Liste liste, Collection<Liste> alleListen, @Nullable AbstractListenkandidaturDAO listenkandidaturDAO) {

    if (listenkandidaturDAO != null && isPreloadNotwendig(liste, alleListen)) {
      Map<Liste, List<Listenkandidatur>> listenkandidaturenNachListe = listenkandidaturDAO.findByGruppe(liste.getGruppe()).stream().collect(Collectors.groupingBy(Listenkandidatur::getListe));
      liste.setListenkandidaturenPreloader(x -> listenkandidaturenNachListe.getOrDefault(liste, emptyList()));
      for (Liste andereListe : alleListen) {
        if (liste.getGruppe().equals(andereListe.getGruppe())) {
          andereListe.setListenkandidaturenPreloader(x -> listenkandidaturenNachListe.getOrDefault(andereListe, emptyList()));
        }
      }
    }
  }

  private static boolean isPreloadNotwendig(Liste liste, Collection<Liste> alleListen) {
    return isPreloadNotwendig(liste) || alleListen.stream().filter(l -> liste.getGruppe().equals(l.getGruppe())).anyMatch(NlListe::isPreloadNotwendig);
  }

  private static boolean isPreloadNotwendig(Liste liste) {
    return liste.getTransientContext().get(AbstractListe.LISTENKANDIDATUREN_PRELOADER) == null;
  }

  @Deprecated // mit listenkandidaturDAO nutzen
  public static StelInformationen getStelInformationen(Liste liste, Collection<Liste> alleListen, EntityAufStimmzettelDaten entityAufStimmzettelDaten) {

    List<Liste> listenDerGruppeNichtZugelassen = alleListen.stream().filter(l -> {
      PolitischeGruppe gruppe = liste.getGruppe();
      PolitischeGruppe lGruppe = l.getGruppe();
      return lGruppe.equals(gruppe);
    }).filter(l -> !isBetrachteBeiStelErmittlung(l)).collect(Collectors.toList());

    if (listenDerGruppeNichtZugelassen.contains(liste)) {
      return StelInformationen.createAbgelehnt(listenDerGruppeNichtZugelassen);
    }

    List<Liste> listenDerGruppe = alleListen.stream().filter(l -> l.getGruppe().equals(liste.getGruppe())).filter(NlListe::isBetrachteBeiStelErmittlung).collect(Collectors.toList());
    List<Gebiet> listenGebieteSortiert = listenDerGruppe.stream().map(AbstractWahlvorschlag::getGebiet).sorted(GebietByBreadthFirstComparator.INSTANCE).collect(Collectors.toList());
    List<Set<Liste>> gleicheListenSortiertNachGebietsreihenFolge = sortiereNachGebieten(gruppiereGleicheListen(listenDerGruppe, entityAufStimmzettelDaten), listenGebieteSortiert);

    // Spezialfall Einzelliste
    if (gleicheListenSortiertNachGebietsreihenFolge.size() == 1 && gleicheListenSortiertNachGebietsreihenFolge.get(0).size() == 1) {
      return StelInformationen.createEinzelListe(gleicheListenSortiertNachGebietsreihenFolge.get(0).iterator().next());
    }

    // Spezialfall identische Liste
    if (gleicheListenSortiertNachGebietsreihenFolge.size() == 1 /* && gleicheListenSortiertNachGebietsreihenFolge.get(0).size() == alleGebiete.size()*/) {
      return StelInformationen.createIdentischeListe(gleicheListenSortiertNachGebietsreihenFolge.get(0));
    }

    int stelNummerAnzeige = 0;
    int stelNummerSortierung = 0;
    for (Set<Liste> identischeListen : gleicheListenSortiertNachGebietsreihenFolge) {
      stelNummerSortierung++;
      if (identischeListen.size() > 1) {
        stelNummerAnzeige++;
      }
      if (identischeListen.contains(liste)) {
        if (identischeListen.size() > 1) {
          return StelInformationen.createListengruppe(stelNummerAnzeige, stelNummerSortierung, identischeListen);
        } else {
          // ELECT-11708
          return StelInformationen.createListengruppe(null, stelNummerSortierung, identischeListen);
        }
      }
    }

    throw new IllegalStateException("Liste nicht Teil eines Stels: " + liste + ", " + listenDerGruppe + ", " + gleicheListenSortiertNachGebietsreihenFolge);
  }

  private static List<Set<Liste>> sortiereNachGebieten(List<Set<Liste>> gruppiereGleicheListen, List<Gebiet> listenGebieteSortiert) {
    gruppiereGleicheListen.sort(Comparator.comparingInt(u -> listenGebieteSortiert.indexOf(u.iterator().next().getGebiet())));
    return gruppiereGleicheListen;
  }

  private static List<Set<Liste>> gruppiereGleicheListen(List<Liste> listenDerGruppe, EntityAufStimmzettelDaten entityAufStimmzettelDaten) {
    List<Set<Liste>> result = new ArrayList<>();
    for (Liste liste : listenDerGruppe) {
      if (!inVorhandeneGruppeAufnehmen(entityAufStimmzettelDaten, result, liste)) {
        Set<Liste> neueGruppe = new HashSet<>();
        neueGruppe.add(liste);
        result.add(neueGruppe);
      }
    }
    return result;
  }

  private static boolean inVorhandeneGruppeAufnehmen(EntityAufStimmzettelDaten entityAufStimmzettelDaten, List<Set<Liste>> listenGruppen, Liste liste) {
    for (Set<Liste> listenGruppe : listenGruppen) {
      Liste repraesentantDerGruppe = listenGruppe.iterator().next();
      if (isIdentisch(liste, repraesentantDerGruppe, entityAufStimmzettelDaten)) {
        listenGruppe.add(liste);
        return true;
      }
    }
    return false;
  }

  /**
   * ELECT-9853
   */
  public static Optional<Integer> getStelNummer(Liste liste, Supplier<List<Liste>> alleListen, Supplier<EntityAufStimmzettelDaten> entityAufStimmzettelDaten, Customer customer,
      AbstractListenkandidaturDAO listenkandidaturDAO) {
    if (Customer.NL_WAS.equals(customer)) {
      return getStelNummerWas(liste);
    }
    return Optional.ofNullable(getStelInformationen(liste, alleListen.get(), entityAufStimmzettelDaten.get(), listenkandidaturDAO).getStelNummerAnzeige());
  }

  @Deprecated // handleWithCare funktioniert nur(!) im WAS
  public static Optional<Integer> getStelNummerWas(Liste liste) {
    if (StringUtils.isBlank(liste.getListenverbindungEingereicht())) {
      return Optional.empty();
    }
    try {
      return Optional.of(Integer.parseInt(liste.getListenverbindungEingereicht()));
    } catch (Exception e) {
      LOG.warn("Stel-Nummer ungültig: '" + liste.getListenverbindungEingereicht() + "', " + e.getMessage());
      return Optional.empty();
    }
  }

  public static boolean isBetrachteBeiStelErmittlung(Liste liste) {
    return liste != null && liste.isZugelassenOderUnbestimmt();
  }

  /**
   * @see NlKandidatur#getNlListeStatus
   */
  public static NlListeStatus getNlListeStatus(Liste liste, AbstractWahlrechtspruefungDAO wahlrechtspruefungDAO) {
    return getNlListeStatus(liste, wahlrechtspruefungDAO::findByWahlvorschlag);
  }

  /**
   * @see NlKandidatur#getNlListeStatus
   */
  public static NlListeStatus getNlListeStatus(Liste liste, Function<Wahlvorschlag, Collection<Wahlrechtspruefung>> wahlrechtspruefungFct) {

    /*
     *  Bei Anpassungen de.ivu.elect.presentation.wahlvorschlag.NlListeStellinformationCache.getNlListeStatus beachten
     */

    if (liste.getWahlvorschlagStatus().isZugelassen()) {
      return NlListeStatus.ZUGELASSEN;
    }

    if (liste.getWahlvorschlagStatus().isNichtZugelassenImSinneAbgelehnt()) {
      return NlListeStatus.ABGELEHNT;
    }

    for (Wahlrechtspruefung wahlrechtspruefung : wahlrechtspruefungFct.apply(liste)) {
      if (wahlrechtspruefung.getWahlrechtspruefStatus().isNichtErfuellt()) {
        Kandidatur kandidatur = wahlrechtspruefung.getKandidatur();

        if (kandidatur == null || /* ELECT-11242 */ !kandidatur.getKandidaturStatus().isNichtZugelassenImSinneAbgelehnt()) {
          return NlListeStatus.ABLEHNUNGGRUENDE_VORHANDEN;
        }
      }
    }

    return NlListeStatus.OFFEN;
  }
}
