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

import static java.util.Arrays.asList;
import static java.util.Collections.emptySet;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toSet;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Collectors;

import de.ivu.elect.business.entschaedigung.boundary.AbstractEntschaedigungDAO;
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.person.entity.Personendaten;
import de.ivu.elect.business.wahl.entity.Wahl;
import de.ivu.elect.business.wahl.entity.Wahltermin;

public class EntschaedigungAggregator {

  private final List<Entschaedigung> entschaedigungen;

  private BigDecimal gesamtbetrag;

  private String waehrungAlsIsoCode;

  private Map<Personendaten, List<Entschaedigung>> entschaedigungenPerPersonendaten;

  private Map<Personendaten, Entschaedigung> gesamtentschaedigungPerPersonendaten;

  private Map<String, List<Entschaedigung>> entschaedigungenPerPersonendatenArt;

  private Map<String, Entschaedigung> gesamtentschaedigungPerPersonendatenArt;

  private Map<Entschaedigungsart, List<Entschaedigung>> entschaedigungenPerEntschaedigungsart;

  private Map<Entschaedigungsart, Entschaedigung> gesamtentschaedigungPerEntschaedigungsart;

  public EntschaedigungAggregator(Collection<Entschaedigung> entschaedigungen) {
    super();

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

  public boolean isEmpty() {
    return getEntschaedigungen().isEmpty();
  }

  public boolean isNotEmpty() {
    return !isEmpty();
  }

  public Optional<Entschaedigung> findEntschaedigungByEntschaedigungsart(Personendaten personendaten, Entschaedigungsart entschaedigungsart) {
    if (entschaedigungsart != null) {
      Collection<Entschaedigung> entschaedigungenFiltered = filterEntschaedigungenByEntschaedigungsart(personendaten, asList(entschaedigungsart));

      if (entschaedigungenFiltered.size() == 1) {
        return Optional.of(entschaedigungenFiltered.iterator().next());
      }
    }

    return Optional.empty();
  }

  public Collection<Entschaedigung> filterEntschaedigungenByEntschaedigungsart(Personendaten personendaten, Collection<Entschaedigungsart> entschaedigungsarten) {
    if (personendaten != null && !entschaedigungsarten.isEmpty()) {
      return groupByPersonendaten().getOrDefault(personendaten, new ArrayList<>()).stream() //
          .filter(entschaedigung -> entschaedigungsarten.contains(entschaedigung.getEntschaedigungsart())) //
          .collect(toSet());
    }

    return emptySet();
  }

  public Optional<Entschaedigung> findEntschaedigungByEntschaedigungStatus(Personendaten personendaten, EntschaedigungStatus entschaedigungStatus) {
    if (entschaedigungStatus != null) {
      Collection<Entschaedigung> entschaedigungenFiltered = filterEntschaedigungenByEntschaedigungStatus(personendaten, asList(entschaedigungStatus));

      if (entschaedigungenFiltered.size() == 1) {
        return Optional.of(entschaedigungenFiltered.iterator().next());
      }
    }

    return Optional.empty();
  }

  public Collection<Entschaedigung> filterEntschaedigungenByEntschaedigungStatus(Personendaten personendaten, Collection<EntschaedigungStatus> entschaedigungStatus) {
    if (personendaten != null && !entschaedigungStatus.isEmpty()) {
      return groupByPersonendaten().getOrDefault(personendaten, new ArrayList<>()).stream() //
          .filter(entschaedigung -> entschaedigungStatus.contains(entschaedigung.getStatus())) //
          .collect(toSet());
    }

    return emptySet();
  }

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

  public BigDecimal calculateGesamtbetrag() {
    if (gesamtbetrag == null) {
      gesamtbetrag = calculateGesamtbetrag(getEntschaedigungen());
    }

    return gesamtbetrag;
  }

  public String getWaehrungAlsIsoCode() {
    if (waehrungAlsIsoCode == null) {
      waehrungAlsIsoCode = getEntschaedigungen().stream().findFirst().map(Entschaedigung::getWaehrungAlsIsoCode).orElse("");
    }

    return waehrungAlsIsoCode;
  }

  public Map<Personendaten, Entschaedigung> calculateGesamtentschaedigungPerPersonendaten() {
    if (gesamtentschaedigungPerPersonendaten == null) {
      gesamtentschaedigungPerPersonendaten = calculateGesamtentschaedigungPerKey(groupByPersonendaten(), (personendaten, gesamtentschaedigung) -> {
        gesamtentschaedigung.setPersonendaten(personendaten);

        return gesamtentschaedigung;
      });
    }

    return gesamtentschaedigungPerPersonendaten;
  }

  public Map<Personendaten, List<Entschaedigung>> groupByPersonendaten() {
    if (entschaedigungenPerPersonendaten == null) {
      entschaedigungenPerPersonendaten = groupByPersonendaten(getEntschaedigungen());
    }

    return entschaedigungenPerPersonendaten;
  }

  public Map<String, Entschaedigung> calculateGesamtentschaedigungPerPersonendatenArt() {
    if (gesamtentschaedigungPerPersonendatenArt == null) {
      gesamtentschaedigungPerPersonendatenArt = calculateGesamtentschaedigungPerKey(groupByPersonendatenArt());
    }

    return gesamtentschaedigungPerPersonendatenArt;
  }

  public Map<String, List<Entschaedigung>> groupByPersonendatenArt() {
    if (entschaedigungenPerPersonendatenArt == null) {
      entschaedigungenPerPersonendatenArt = groupByPersonendatenArt(getEntschaedigungen());
    }

    return entschaedigungenPerPersonendatenArt;
  }

  public Map<Entschaedigungsart, Entschaedigung> calculateGesamtentschaedigungPerEntschaedigungsart() {
    if (gesamtentschaedigungPerEntschaedigungsart == null) {
      gesamtentschaedigungPerEntschaedigungsart = calculateGesamtentschaedigungPerKey(groupByEntschaedigungsart());
    }

    return gesamtentschaedigungPerEntschaedigungsart;
  }

  public Map<Entschaedigungsart, List<Entschaedigung>> groupByEntschaedigungsart() {
    if (entschaedigungenPerEntschaedigungsart == null) {
      entschaedigungenPerEntschaedigungsart = groupByEntschaedigungsart(getEntschaedigungen());
    }

    return entschaedigungenPerEntschaedigungsart;
  }

  private static <K> Map<K, Entschaedigung> calculateGesamtentschaedigungPerKey(Map<K, List<Entschaedigung>> entschaedigungenPerKey) {
    return calculateGesamtentschaedigungPerKey(entschaedigungenPerKey, (key, gesamtentschaedigung) -> gesamtentschaedigung);
  }

  private static <K> Map<K, Entschaedigung> calculateGesamtentschaedigungPerKey(Map<K, List<Entschaedigung>> entschaedigungenPerKey, BiFunction<K, Entschaedigung, Entschaedigung> valueMapper) {
    return entschaedigungenPerKey.entrySet().stream() //
        .filter(entry -> !entry.getValue().isEmpty()) //
        .collect(Collectors.toMap(Entry<K, List<Entschaedigung>>::getKey, entry -> {
          List<Entschaedigung> entschaedigungen = entry.getValue();
          Entschaedigung template = entschaedigungen.get(0);

          Entschaedigung entschaedigung = new Entschaedigung();
          entschaedigung.setAuszahlung(template.getAuszahlung());
          entschaedigung.setBezeichnung(entschaedigungen.stream().map(Entschaedigung::getBezeichnung).collect(joining(", ")));
          entschaedigung.setStatus(containSameAttributeValue(entschaedigungen, Entschaedigung::getStatus) ? template.getStatus() : EntschaedigungStatus.UNBESTIMMT);
          entschaedigung.setGesamtbetrag(calculateGesamtbetrag(entschaedigungen));
          entschaedigung.setWaehrungAlsIsoCode(template.getWaehrungAlsIsoCode());

          return valueMapper.apply(entry.getKey(), entschaedigung);
        }));
  }

  public static BigDecimal calculateGesamtbetrag(Collection<Entschaedigung> entschaedigungen) {
    return entschaedigungen.stream().filter(Entschaedigung::hasGesamtbetrag).map(Entschaedigung::getGesamtbetrag).reduce(BigDecimal.ZERO, BigDecimal::add);
  }

  private static Map<Personendaten, List<Entschaedigung>> groupByPersonendaten(Collection<Entschaedigung> entschaedigungen) {
    return entschaedigungen.stream().filter(Entschaedigung::hasPersonendaten).collect(Collectors.groupingBy(Entschaedigung::getPersonendaten));
  }

  private static Map<String, List<Entschaedigung>> groupByPersonendatenArt(Collection<Entschaedigung> entschaedigungen) {
    return entschaedigungen.stream().filter(Entschaedigung::hasEntschaedigungsart).collect(Collectors.groupingBy(Entschaedigung::getKriteriumPersonendatenArt));
  }

  private static Map<Entschaedigungsart, List<Entschaedigung>> groupByEntschaedigungsart(Collection<Entschaedigung> entschaedigungen) {
    return entschaedigungen.stream().filter(Entschaedigung::hasEntschaedigungsart).collect(Collectors.groupingBy(Entschaedigung::getEntschaedigungsart));
  }

  private static boolean containSameAttributeValue(List<Entschaedigung> entschaedigungen, Function<Entschaedigung, ? > getter) {
    if (entschaedigungen.size() > 1) {
      Object value = getter.apply(entschaedigungen.get(0));

      for (int i = 1; i < entschaedigungen.size(); i++) {
        if (!Objects.equals(value, getter.apply(entschaedigungen.get(i)))) {
          return false;
        }
      }
    }

    return !entschaedigungen.isEmpty();
  }

  public static EntschaedigungAggregator entschaedigungenErstelltFuerAuszahlung(AbstractEntschaedigungDAO entschaedigungDAO, Auszahlung auszahlung) {
    List<Entschaedigung> entschaedigungen = new ArrayList<>();

    if (auszahlung != null) {
      entschaedigungen.addAll(entschaedigungDAO.findByErstelltFuerKonkreteAuszahlung(auszahlung));
    }

    return new EntschaedigungAggregator(entschaedigungen);
  }

  public static EntschaedigungAggregator entschaedigungenErstelltFuerBeliebigePersonendaten(AbstractEntschaedigungDAO entschaedigungDAO, Wahl wahl, Wahltermin wahltermin) {
    List<Entschaedigung> entschaedigungen = new ArrayList<>();

    if (wahl != null) {
      entschaedigungen.addAll(entschaedigungDAO.findByErstelltFuerBeliebigePersonendatenAndWahl(wahl));
    } else if (wahltermin != null) {
      entschaedigungen.addAll(entschaedigungDAO.findByErstelltFuerBeliebigePersonendatenAndWahltermin(wahltermin));
    }

    return new EntschaedigungAggregator(entschaedigungen);
  }

  public static EntschaedigungAggregator entschaedigungenErstelltFuerKonkretePersonendaten(AbstractEntschaedigungDAO entschaedigungDAO, Personendaten personendaten) {
    List<Entschaedigung> entschaedigungen = new ArrayList<>();

    if (personendaten != null) {
      entschaedigungen.addAll(entschaedigungDAO.findByErstelltFuerKonkretePersonendaten(personendaten));
    }

    return new EntschaedigungAggregator(entschaedigungen);
  }
}
