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

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

import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import java.util.function.Predicate;

import javax.inject.Inject;

import de.ivu.elect.business.entschaedigung.control.EntschaedigungAggregator;
import de.ivu.elect.business.entschaedigung.control.EntschaedigungDefaultComparator;
import de.ivu.elect.business.entschaedigung.entity.Auszahlung;
import de.ivu.elect.business.entschaedigung.entity.Entschaedigung;
import de.ivu.elect.business.entschaedigung.entity.EntschaedigungStatus;
import de.ivu.elect.business.entschaedigung.entity.Entschaedigungsart;
import de.ivu.elect.business.entschaedigung.entity.QEntschaedigung;
import de.ivu.elect.business.person.entity.Personendaten;
import de.ivu.elect.business.person.entity.PersonendatenArt;
import de.ivu.elect.business.person.entity.QPersonendaten;
import de.ivu.elect.business.wahl.entity.Wahl;
import de.ivu.elect.business.wahl.entity.Wahltermin;
import de.ivu.elect.jee.entity.EagerLoadingCollector;
import de.ivu.elect.jee.entity.QueryDsl;
import de.ivu.elect.jee.faces.Messages;

public class EntschaedigungSearch extends AbstractEntschaedigungSearch {

  private static final Comparator<Entschaedigung> DEFAULT_COMPARATOR = new EntschaedigungDefaultComparator();

  @Inject
  private transient AuszahlungExtendedDAO auszahlungDAO;

  @Inject
  private transient EntschaedigungCalculator entschaedigungCalculator;

  public EntschaedigungSearch() {
    super();
  }

  @Override
  protected List<Entschaedigung> querySearchResult(EntschaedigungSearchParameters searchParameters) {
    return querySearchResultEntschaedigungenInAuszahlung(searchParameters).getEntschaedigungen();
  }

  private EntschaedigungAggregator querySearchResultEntschaedigungenInAuszahlung(EntschaedigungSearchParameters searchParameters) {
    List<Entschaedigung> result = new ArrayList<>();

    if (searchParameters.hasAuszahlung()) {
      QEntschaedigung entschaedigung = QEntschaedigung.entschaedigung;

      QueryDsl queryDsl = dao.getQueryDsl();
      queryDsl.from(entschaedigung);

      queryDsl.eq(entschaedigung.auszahlung, attach(searchParameters.getAuszahlung()));

      QPersonendaten personendaten = entschaedigung.personendaten;

      queryDsl.likeNormalizedIfNotBlank(personendaten.familiennameNormalisiert, searchParameters.getFamilienname());
      queryDsl.likeInAnyNormalizedIfNotBlank(personendaten.rufnameNormalisiert, personendaten.vornamenNormalisiert, searchParameters.getVornameSeparator(), searchParameters.getVorname());

      queryDsl.joinFetch(entschaedigung.personendaten);

      result.addAll(searchParameters.applyLimit(queryDsl).list(entschaedigung));
    }

    result.sort(DEFAULT_COMPARATOR);

    return new EntschaedigungAggregator(result);
  }

  public EntschaedigungenInAuszahlungObsoleteSearchResult getSearchResultEntschaedigungenInAuszahlungObsolete(EntschaedigungSearchParameters searchParameters,
      EagerLoadingCollector<Entschaedigung> eagerLoadingCollector) {
    EntschaedigungenInAuszahlungObsoleteSearchResult result = querySearchResultEntschaedigungenInAuszahlungObsolete(searchParameters.getAuszahlung(),
        querySearchResultEntschaedigungenInAuszahlung(searchParameters));
    eagerLoadingCollector.eagerLoad(result.getEntschaedigungen());

    return result;
  }

  public EntschaedigungenInAuszahlungObsoleteSearchResult getSearchResultEntschaedigungenInAuszahlungObsolete(Auszahlung template) {
    EntschaedigungAggregator entschaedigungenInAuszahlungAggregator = new EntschaedigungAggregator(new ArrayList<>());

    if (template != null) {
      entschaedigungenInAuszahlungAggregator = template.createEntschaedigungenAggregator(dao);
    }

    return querySearchResultEntschaedigungenInAuszahlungObsolete(template, entschaedigungenInAuszahlungAggregator);
  }

  private EntschaedigungenInAuszahlungObsoleteSearchResult querySearchResultEntschaedigungenInAuszahlungObsolete(Auszahlung template, EntschaedigungAggregator entschaedigungenInAuszahlungAggregator) {
    List<Entschaedigung> entschaedigungenVeraendert = new ArrayList<>();
    List<Entschaedigung> entschaedigungenAusgezahlt = new ArrayList<>();

    if (template != null && entschaedigungenInAuszahlungAggregator.isNotEmpty()) {
      Auszahlung auszahlung = attach(template);

      EntschaedigungAggregator entschaedigungenFuerPersonendatenAktuellAggregator = createEntschaedigungenFuerPersonendatenAktuellAggregator(auszahlung.getWahl(), auszahlung.getWahltermin(),
          entschaedigungenInAuszahlungAggregator);

      Map<Personendaten, List<Entschaedigung>> entschaedigungenInAuszahlungPerPersonendaten = entschaedigungenInAuszahlungAggregator.groupByPersonendaten();
      Map<Personendaten, List<Entschaedigung>> entschaedigungenFuerPersonendatenAktuellPerPersonendaten = entschaedigungenFuerPersonendatenAktuellAggregator.groupByPersonendaten();

      for (Entry<Personendaten, List<Entschaedigung>> entryPerPersonendaten : entschaedigungenInAuszahlungPerPersonendaten.entrySet()) {
        Personendaten personendaten = entryPerPersonendaten.getKey();
        Collection<Entschaedigung> entschaedigungenInAuszahlung = entryPerPersonendaten.getValue();

        addEntschaedigungenInAuszahlungVeraendert(entschaedigungenVeraendert, auszahlung, personendaten, entschaedigungenInAuszahlung, entschaedigungenFuerPersonendatenAktuellPerPersonendaten);
        addEntschaedigungenInAuszahlungAusgezahlt(entschaedigungenAusgezahlt, auszahlung, personendaten, entschaedigungenInAuszahlung, entschaedigungenFuerPersonendatenAktuellPerPersonendaten);
      }
    }

    return new EntschaedigungenInAuszahlungObsoleteSearchResult(DEFAULT_COMPARATOR, entschaedigungenVeraendert, entschaedigungenAusgezahlt);
  }

  private void addEntschaedigungenInAuszahlungVeraendert(Collection<Entschaedigung> result, Auszahlung auszahlung, Personendaten personendaten, Collection<Entschaedigung> entschaedigungenInAuszahlung,
      Map<Personendaten, List<Entschaedigung>> entschaedigungenFuerPersonendatenAktuellPerPersonendaten) {
    if (entschaedigungenFuerPersonendatenAktuellPerPersonendaten.containsKey(personendaten)) {
      Collection<Entschaedigung> entschaedigungenFuerPersonendatenAktuell = entschaedigungenFuerPersonendatenAktuellPerPersonendaten.get(personendaten);

      for (Entschaedigung entschaedigungInAuszahlung : entschaedigungenInAuszahlung) {
        if (entschaedigungenFuerPersonendatenAktuell.stream().noneMatch(entschaedigungUsingSameEntschaedigungDetails(entschaedigungInAuszahlung))) {
          result.add(entschaedigungInAuszahlung);
        }
      }
    } else {
      result.addAll(entschaedigungenInAuszahlung);
    }
  }

  private void addEntschaedigungenInAuszahlungAusgezahlt(Collection<Entschaedigung> result, Auszahlung auszahlung, Personendaten personendaten, Collection<Entschaedigung> entschaedigungenInAuszahlung,
      Map<Personendaten, List<Entschaedigung>> entschaedigungenFuerPersonendatenAktuellPerPersonendaten) {
    if (entschaedigungenFuerPersonendatenAktuellPerPersonendaten.containsKey(personendaten)) {
      Collection<Entschaedigung> entschaedigungenFuerPersonendatenAktuell = entschaedigungenFuerPersonendatenAktuellPerPersonendaten.get(personendaten);

      for (Entschaedigung entschaedigungInAuszahlung : entschaedigungenInAuszahlung) {
        if (entschaedigungenFuerPersonendatenAktuell.stream().anyMatch(entschaedigungUsingSameEntschaedigungDetails(entschaedigungInAuszahlung).and(entschaedigungAusgezahlt(auszahlung)))) {
          result.add(entschaedigungInAuszahlung);
        }
      }
    }
  }

  private Predicate<Entschaedigung> entschaedigungUsingSameEntschaedigungDetails(Entschaedigung entschaedigungInAuszahlung) {
    return entschaedigung -> entschaedigung.usesSameEntschaedigungDetails(entschaedigungInAuszahlung);
  }

  private Predicate<Entschaedigung> entschaedigungAusgezahlt(Auszahlung auszahlung) {
    return entschaedigung -> entschaedigung.isPersistent() && entschaedigung.getStatus().isAusgezahlt() && !Objects.equals(auszahlung, entschaedigung.getAuszahlung());
  }

  /**
   * @return EntschaedigungAggregator für ALLE (vorgesehenen, ausgezahlten) aktuellen Entschädigungen der Personendaten in der Auszahlung.
   */
  private EntschaedigungAggregator createEntschaedigungenFuerPersonendatenAktuellAggregator(Wahl wahl, Wahltermin wahltermin, EntschaedigungAggregator entschaedigungenInAuszahlungAggregator) {
    Collection<Entschaedigung> result = new HashSet<>();

    Collection<PersonendatenArt> personendatenArtenSupported = collectPersonendatenArtenSupported();

    Map<PersonendatenArt, List<Entschaedigung>> entschaedigungenInAuszahlungPerPersonendatenArt = new HashMap<>();

    for (Entry<Entschaedigungsart, List<Entschaedigung>> entryPerEntschaedigungsart : entschaedigungenInAuszahlungAggregator.groupByEntschaedigungsart().entrySet()) {
      PersonendatenArt personendatenArt = extractPersonendatenArt(entryPerEntschaedigungsart.getKey(), personendatenArtenSupported);

      entschaedigungenInAuszahlungPerPersonendatenArt.putIfAbsent(personendatenArt, new ArrayList<>());
      entschaedigungenInAuszahlungPerPersonendatenArt.get(personendatenArt).addAll(entryPerEntschaedigungsart.getValue());
    }

    EntschaedigungAggregator entschaedigungenFuerPersonendatenAggregator = EntschaedigungAggregator.entschaedigungenErstelltFuerBeliebigePersonendaten(dao, wahl, wahltermin);

    for (Entry<PersonendatenArt, List<Entschaedigung>> entryPerEntschaedigungsart : entschaedigungenInAuszahlungPerPersonendatenArt.entrySet()) {
      PersonendatenArt personendatenArt = entryPerEntschaedigungsart.getKey();

      Collection<Personendaten> personendatenInAuszahlungFuerPersonendatenArt = entryPerEntschaedigungsart.getValue().stream() //
          .map(Entschaedigung::getPersonendaten) //
          .collect(toSet());

      for (Personendaten personendaten : personendatenInAuszahlungFuerPersonendatenArt) {
        Collection<Entschaedigung> entschaedigungenVorgesehen = entschaedigungCalculator.calculateEntschaedigungenVorgesehenGesamtbetragZeroIncluded(personendatenArt, personendaten);
        Collection<Entschaedigung> entschaedigungenAusgezahlt = entschaedigungenFuerPersonendatenAggregator.filterEntschaedigungenByEntschaedigungStatus(personendaten,
            EntschaedigungStatus.valuesAusgezahlt());

        result.addAll(entschaedigungenVorgesehen);
        result.addAll(entschaedigungenAusgezahlt);
      }
    }

    return new EntschaedigungAggregator(result);
  }

  private Collection<PersonendatenArt> collectPersonendatenArtenSupported() {
    Collection<PersonendatenArt> result = new HashSet<>();
    result.addAll(entschaedigungCalculator.getSupportedPersonendatenArten().stream().map(personendatenArt -> (PersonendatenArt) personendatenArt).collect(toList()));

    return result;
  }

  private PersonendatenArt extractPersonendatenArt(Entschaedigungsart entschaedigungsart, Collection<PersonendatenArt> personendatenArtenSupported) {
    return personendatenArtenSupported.stream() //
        .filter(entschaedigungsart::usesKriteriumPersonendatenArt) //
        .findFirst() //
        .orElseThrow(() -> new IllegalArgumentException("Entschädigungsart mit unbekannter Art der Person gefunden [bezeichnung = " + entschaedigungsart.getBezeichnung() + ", personendatenArt = "
            + entschaedigungsart.getKriteriumPersonendatenArt() + "]"));
  }

  private Auszahlung attach(Auszahlung auszahlung) {
    return auszahlungDAO.attach(auszahlung);
  }

  public static class EntschaedigungenInAuszahlungObsoleteSearchResult {

    private final List<Entschaedigung> entschaedigungen;

    private final List<Entschaedigung> entschaedigungenVeraendert;

    private final List<Entschaedigung> entschaedigungenAusgezahlt;

    public EntschaedigungenInAuszahlungObsoleteSearchResult(Comparator<Entschaedigung> comparator, List<Entschaedigung> entschaedigungenVeraendert, List<Entschaedigung> entschaedigungenAusgezahlt) {
      super();

      this.entschaedigungenVeraendert = new ArrayList<>(entschaedigungenVeraendert);
      this.entschaedigungenVeraendert.sort(comparator);

      this.entschaedigungenAusgezahlt = new ArrayList<>(entschaedigungenAusgezahlt);
      this.entschaedigungenAusgezahlt.sort(comparator);

      Set<Entschaedigung> entschaedigungenObsolete = new HashSet<>();
      entschaedigungenObsolete.addAll(this.entschaedigungenVeraendert);
      entschaedigungenObsolete.addAll(this.entschaedigungenAusgezahlt);

      this.entschaedigungen = new ArrayList<>(entschaedigungenObsolete);
      this.entschaedigungen.sort(comparator);
    }

    public List<Entschaedigung> getEntschaedigungen() {
      return entschaedigungen;
    }

    public List<Entschaedigung> getEntschaedigungenVeraendert() {
      return entschaedigungenVeraendert;
    }

    public List<Entschaedigung> getEntschaedigungenAusgezahlt() {
      return entschaedigungenAusgezahlt;
    }

    public String toString(Messages messages) {
      return new EntschaedigungenInAuszahlungObsoleteSearchResultFormatted(messages, this).toString();
    }
  }

  public static class EntschaedigungenInAuszahlungObsoleteSearchResultFormatted {

    private final Messages messages;

    private final int anzahlEntschaedigungenInAuszahlungObsolete;

    private final int anzahlEntschaedigungenInAuszahlungObsoleteVeraendert;

    private final int anzahlEntschaedigungenInAuszahlungObsoleteAusgezahlt;

    public EntschaedigungenInAuszahlungObsoleteSearchResultFormatted(Messages messages, EntschaedigungenInAuszahlungObsoleteSearchResult entschaedigungenInAuszahlungObsoleteSearchResult) {
      this(messages, entschaedigungenInAuszahlungObsoleteSearchResult.getEntschaedigungen().size(), entschaedigungenInAuszahlungObsoleteSearchResult.getEntschaedigungenVeraendert().size(),
          entschaedigungenInAuszahlungObsoleteSearchResult.getEntschaedigungenAusgezahlt().size());
    }

    public EntschaedigungenInAuszahlungObsoleteSearchResultFormatted(Messages messages, int anzahlEntschaedigungenInAuszahlungObsolete, int anzahlEntschaedigungenInAuszahlungObsoleteVeraendert,
        int anzahlEntschaedigungenInAuszahlungObsoleteAusgezahlt) {
      super();

      this.messages = messages;

      this.anzahlEntschaedigungenInAuszahlungObsolete = anzahlEntschaedigungenInAuszahlungObsolete;
      this.anzahlEntschaedigungenInAuszahlungObsoleteVeraendert = anzahlEntschaedigungenInAuszahlungObsoleteVeraendert;
      this.anzahlEntschaedigungenInAuszahlungObsoleteAusgezahlt = anzahlEntschaedigungenInAuszahlungObsoleteAusgezahlt;
    }

    public String getFormatKey() {
      String result = "entschaedigungen.pruefen.result.success.message";

      if (anzahlEntschaedigungenInAuszahlungObsolete > 0) {
        result = "entschaedigungen.pruefen.result.entschaedigung.obsolete.message";

        if (anzahlEntschaedigungenInAuszahlungObsoleteVeraendert > 0 || anzahlEntschaedigungenInAuszahlungObsoleteAusgezahlt > 0) {
          result = "entschaedigungen.pruefen.result.entschaedigung.obsolete.veraendert.ausgezahlt.message";
        }
      }

      return result;
    }

    public Object[] getFormatParams() {
      return new Object[] {anzahlEntschaedigungenInAuszahlungObsolete, anzahlEntschaedigungenInAuszahlungObsoleteVeraendert, anzahlEntschaedigungenInAuszahlungObsoleteAusgezahlt};
    }

    @Override
    public String toString() {
      return messages.getMessageForMessageKey(getFormatKey(), getFormatParams());
    }
  }
}
