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

import static java.util.Arrays.asList;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;

import javax.ejb.Stateless;
import javax.inject.Inject;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.ivu.elect.business.entschaedigung.control.AuszahlungProtokollierungEntschaedigungStatus;
import de.ivu.elect.business.entschaedigung.control.EntschaedigungAggregator;
import de.ivu.elect.business.entschaedigung.control.EntschaedigungCreate;
import de.ivu.elect.business.entschaedigung.control.EntschaedigungProtokollierung;
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.boundary.PersonendatenDAO;
import de.ivu.elect.business.person.entity.Personendaten;
import de.ivu.elect.business.person.entity.PersonendatenArt;
import de.ivu.elect.business.wahl.entity.Wahl;
import de.ivu.elect.business.wahl.entity.Wahltermin;

@Stateless
public class EntschaedigungBulkEdit extends AbstractEntschaedigungEdit {

  // TODO ELECTMUC-1785, nach erfolgreichem Nachtesten diesen Logger und seine Verwendung entfernen!
  private static final Logger LOG = LoggerFactory.getLogger(EntschaedigungBulkEdit.class);

  @Inject
  private transient PersonendatenDAO personendatenDAO;

  @Inject
  private transient EntschaedigungsartDAO entschaedigungsartDAO;

  @Inject
  private transient EntschaedigungFactory entschaedigungFactory;

  @Inject
  private transient EntschaedigungCreate entschaedigungCreate;

  @Inject
  private transient EntschaedigungCalculator entschaedigungCalculator;

  public EntschaedigungBulkEdit() {
    super();
  }

  /**
   * @deprecated
   * @see EntschaedigungBulkEdit#saveEntschaedigungVorgesehen
   * @see EntschaedigungBulkEdit#saveEntschaedigungAusgezahlt
   * @see EntschaedigungBulkEdit#saveEntschaedigungBearbeitet
   */
  @Deprecated
  @Override
  public void save(Entschaedigung entschaedigung) {
    saveEntschaedigungBearbeitet(entschaedigung);
  }

  private <P extends Personendaten> EntschaedigungAggregator collectEntschaedigungenAuszahlbar(PersonendatenArt personendatenArt, P personendatenHavingPersonendatenArt) {
    return collectEntschaedigungenAuszahlbar(personendatenArt, asList(personendatenHavingPersonendatenArt));
  }

  public <P extends Personendaten> EntschaedigungAggregator collectEntschaedigungenAuszahlbar(PersonendatenArt personendatenArt, Collection<P> personendatenHavingPersonendatenArt) {
    Collection<Entschaedigung> result = new HashSet<>();

    for (Personendaten personendaten : personendatenHavingPersonendatenArt) {
      // NOTE, dass nur manuell, mit abweichenden Werten erfasste Entschädigungen persistiert vorliegen! Daher alle vorgesehenen Entschädigungen immer "live" berechnen...
      result.addAll(entschaedigungCalculator.calculateEntschaedigungenAuszahlbarGesamtbetragZeroExcluded(personendatenArt, attach(personendaten)));
    }

    return new EntschaedigungAggregator(result);
  }

  private <P extends Personendaten> EntschaedigungAggregator collectEntschaedigungenAusgezahlt(PersonendatenArt personendatenArt, P personendaten) {
    return collectEntschaedigungenAusgezahlt(personendatenArt, asList(personendaten));
  }

  public <P extends Personendaten> EntschaedigungAggregator collectEntschaedigungenAusgezahlt(PersonendatenArt personendatenArt, Collection<P> personendatenHavingPersonendatenArt) {
    Collection<Entschaedigung> result = new HashSet<>();

    for (Personendaten personendaten : personendatenHavingPersonendatenArt) {
      List<Entschaedigung> entschaedigungen = new ArrayList<>(dao.findByErstelltFuerKonkretePersondendatenAndEntschaedigungStatus(attach(personendaten), EntschaedigungStatus.valuesAusgezahlt()));
      entschaedigungen.removeIf(entschaedigung -> !entschaedigung.usesKriteriumPersonendatenArt(personendatenArt));

      result.addAll(entschaedigungen);
    }

    return new EntschaedigungAggregator(result);
  }

  public <P extends Personendaten> void saveEntschaedigungenAusgezahltAsVorgesehen(PersonendatenArt personendatenArt, P personendaten) {
    Collection<Entschaedigung> entschaedigungenFuerPersondendatenAusgezahlt = collectEntschaedigungenAusgezahlt(personendatenArt, attach(personendaten)).getEntschaedigungen();

    saveEntschaedigungenAsVorgesehen(entschaedigungenFuerPersondendatenAusgezahlt);
  }

  public void saveEntschaedigungenAsVorgesehen(Auszahlung auszahlung) {
    EntschaedigungAggregator entschaedigungenFuerPersonendatenAggregator = EntschaedigungAggregator.entschaedigungenErstelltFuerBeliebigePersonendaten(dao, auszahlung.getWahl(),
        auszahlung.getWahltermin());

    // NOTE, dass nicht die Entschädigungen in der Auszahlung, sondern die "ausgezahlten" Entschädigungen an den Personendaten als "unbezahlt" markiert werden müssen!
    Collection<Entschaedigung> entschaedigungenFuerPersondendatenAusgezahlt = new ArrayList<>();

    for (Entschaedigung entschaedigungInAuszahlung : auszahlung.getEntschaedigungen(dao)) {
      entschaedigungenFuerPersonendatenAggregator.findEntschaedigungByEntschaedigungsart(entschaedigungInAuszahlung.getPersonendaten(), entschaedigungInAuszahlung.getEntschaedigungsart()) //
          .filter(entschaedigungFuerPersondendaten -> entschaedigungFuerPersondendaten.getStatus().isAusgezahlt()) //
          .ifPresent(entschaedigungenFuerPersondendatenAusgezahlt::add);
    }

    saveEntschaedigungenAsVorgesehen(entschaedigungenFuerPersondendatenAusgezahlt);

    AuszahlungProtokollierungEntschaedigungStatus.protokolliere(auszahlung, EntschaedigungStatus.VORGESEHEN); // ELECTMUC-2909
  }

  private void saveEntschaedigungenAsVorgesehen(Collection<Entschaedigung> entschaedigungen) {
    for (Entschaedigung entschaedigung : entschaedigungen) {
      updateOrDeleteEntschaedigungAsVorgesehen(entschaedigung);
    }
  }

  private void updateOrDeleteEntschaedigungAsVorgesehen(Entschaedigung entschaedigung) {
    entschaedigung.setStatus(EntschaedigungStatus.VORGESEHEN);

    saveEntschaedigungBearbeitet(entschaedigung);
  }

  public <P extends Personendaten> void saveEntschaedigungenAuszahlbarAsAusgezahlt(PersonendatenArt personendatenArt, P personendaten) {
    Collection<Entschaedigung> entschaedigungenFuerPersonendaten = collectEntschaedigungenAuszahlbar(personendatenArt, personendaten).getEntschaedigungen();

    saveEntschaedigungenAsAusgezahlt(personendaten.getWahl(), personendaten.getWahltermin(), entschaedigungenFuerPersonendaten);
  }

  public void saveEntschaedigungenAsAusgezahlt(Auszahlung auszahlung) {
    Collection<Entschaedigung> entschaedigungenInAuszahlung = auszahlung.getEntschaedigungen(dao);

    saveEntschaedigungenAsAusgezahlt(auszahlung.getWahl(), auszahlung.getWahltermin(), entschaedigungenInAuszahlung);

    AuszahlungProtokollierungEntschaedigungStatus.protokolliere(auszahlung, EntschaedigungStatus.AUSGEZAHLT); // ELECTMUC-2909
  }

  private void saveEntschaedigungenAsAusgezahlt(Wahl wahl, Wahltermin wahltermin, Collection<Entschaedigung> entschaedigungen) {
    EntschaedigungAggregator entschaedigungenFuerPersonendatenAggregator = EntschaedigungAggregator.entschaedigungenErstelltFuerBeliebigePersonendaten(dao, wahl, wahltermin);

    for (Entschaedigung entschaedigung : entschaedigungen) {
      updateOrCreateEntschaedigungAsAusgezahlt(entschaedigungenFuerPersonendatenAggregator, entschaedigung);
    }
  }

  // NOTE ELECTMUC-2052, dass eine Kopie der übergebenen, "ausgezahlten" Entschädigung (bspw. aus einer exportierten Auszahlung) an den Personendaten hinterlegt/aktualisert wird,
  // um anschließend feststellen zu können, ob eine Entschädigungsart für eine Person bereits ausgezahlt wurde.
  private void updateOrCreateEntschaedigungAsAusgezahlt(EntschaedigungAggregator entschaedigungenFuerPersonendatenAggregator, Entschaedigung entschaedigung) {
    EntschaedigungStatus entschaedigungStatus = EntschaedigungStatus.AUSGEZAHLT;

    Personendaten personendaten = attach(entschaedigung.getPersonendaten());
    Entschaedigungsart entschaedigungsart = attach(entschaedigung.getEntschaedigungsart());

    Optional<Entschaedigung> entschaedigungFuerPersondendatenIfPresent = entschaedigungenFuerPersonendatenAggregator.findEntschaedigungByEntschaedigungsart(personendaten, entschaedigungsart);

    if (entschaedigungFuerPersondendatenIfPresent.isPresent()) {
      Entschaedigung entschaedigungFuerPersondendatenUpdated = entschaedigungFuerPersondendatenIfPresent.get();
      entschaedigungFuerPersondendatenUpdated.copyEntschaedigungDetailsFrom(entschaedigung);
      entschaedigungFuerPersondendatenUpdated.setStatus(entschaedigungStatus);

      log("Entschaedigung als ausgezahlt markieren - speichere persistente Entschaedigung", entschaedigungFuerPersondendatenUpdated);

      protokolliere(entschaedigungFuerPersondendatenUpdated);
    } else {
      // Dann also eine neue Entität speichern...
      Entschaedigung entschaedigungFuerPersondendatenCreated = entschaedigungFactory.create();
      entschaedigungFuerPersondendatenCreated.copyEntschaedigungDetailsFrom(entschaedigung);
      entschaedigungFuerPersondendatenCreated.setStatus(entschaedigungStatus);

      entschaedigungFuerPersondendatenCreated.setPersonendaten(personendaten);
      entschaedigungFuerPersondendatenCreated.setEntschaedigungsart(entschaedigungsart);

      log("Entschaedigung als ausgezahlt markieren - speichere NICHT persistente Entschaedigung", entschaedigungFuerPersondendatenCreated);

      entschaedigungCreate.save(entschaedigungFuerPersondendatenCreated);

      protokolliere(entschaedigungFuerPersondendatenCreated);
    }
  }

  public void saveEntschaedigungenBearbeitet(Collection<Entschaedigung> entschaedigungen) {
    entschaedigungen.forEach(this::saveEntschaedigungBearbeitet);
  }

  private void saveEntschaedigungBearbeitet(Entschaedigung entschaedigung) {
    if (entschaedigung.isBearbeitbar()) {
      if (entschaedigung.isPersistent()) {
        updateOrDeleteEntschaedigungBearbeitet(entschaedigung);
      } else {
        createEntschaedigungBearbeitet(entschaedigung);
      }
    } else {
      log("Entschaedigung bearbeiten - ignoriere NICHT bearbeitbare Entschaedigung", entschaedigung);
    }
  }

  private void updateOrDeleteEntschaedigungBearbeitet(Entschaedigung entschaedigung) {
    // Die bearbeitbaren Werte vor dem Attachen sichern!
    EntschaedigungStatus entschaedigungStatus = entschaedigung.getStatus();
    Long anzahl = entschaedigung.getAnzahl();
    BigDecimal einzelbetrag = entschaedigung.getEinzelbetrag();

    Entschaedigung entschaedigungPersisted = attach(entschaedigung);

    if (entschaedigungPersisted != null) { // ELECT-12363
      entschaedigungPersisted.setStatus(entschaedigungStatus);
      entschaedigungPersisted.setEinzelbetrag(einzelbetrag);
      entschaedigungPersisted.setAnzahl(anzahl);

      if (entschaedigungPersisted.overridesEntschaedigungDefaults()) {
        log("Entschaedigung bearbeiten - speichere persistente abweichende Entschaedigung", entschaedigungPersisted);

        persistEntschaedigungOverridingDefaults(entschaedigungPersisted);
      } else {
        log("Entschaedigung bearbeiten - loesche persistente NICHT abweichende Entschaedigung", entschaedigungPersisted);

        deleteEntschaedigungHavingDefaults(entschaedigungPersisted);
      }
    }
  }

  private void createEntschaedigungBearbeitet(Entschaedigung entschaedigung) {
    entschaedigung.setEntschaedigungsart(attach(entschaedigung.getEntschaedigungsart()));
    entschaedigung.setPersonendaten(attach(entschaedigung.getPersonendaten()));

    if (entschaedigung.overridesEntschaedigungDefaults()) {
      log("Entschaedigung bearbeiten - speichere NICHT persistente abweichende Entschaedigung", entschaedigung);

      persistEntschaedigungOverridingDefaults(entschaedigung);
    } else {
      log("Entschaedigung bearbeiten - ignoriere NICHT persistente NICHT abweichende Entschaedigung", entschaedigung);
    }
  }

  protected void persistEntschaedigungOverridingDefaults(Entschaedigung entschaedigung) {
    dao.persist(entschaedigung);

    protokolliere(entschaedigung);
  }

  protected void deleteEntschaedigungHavingDefaults(Entschaedigung entschaedigung) {
    dao.remove(entschaedigung);

    protokolliere(entschaedigung);
  }

  protected void protokolliere(Entschaedigung entschaedigung) {
    EntschaedigungProtokollierung.protokolliere(entschaedigung);
  }

  protected Personendaten attach(Personendaten personendaten) {
    return personendatenDAO.attach(personendaten);
  }

  protected Entschaedigungsart attach(Entschaedigungsart entschaedigungsart) {
    return entschaedigungsartDAO.attach(entschaedigungsart);
  }

  protected Entschaedigung attach(Entschaedigung entschaedigung) {
    return getAttachedEntity(entschaedigung);
  }

  protected void log(String message, Entschaedigung entschaedigung) {
    log(message + " [" + (entschaedigung == null ? "null" : entschaedigung.toStringLogged()) + "]");
  }

  protected void log(String message) {
    LOG.info(message);
  }
}
