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

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

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;

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

import de.ivu.elect.business.datei.entity.DateiImportBerichtCustomData;
import de.ivu.elect.business.exchange.control.AbstractImporter;
import de.ivu.elect.business.exchange.control.ImportExchange;
import de.ivu.elect.business.exchange.entity.ImportUpdateReport;
import de.ivu.elect.business.gebietsbaum.entity.GebietConstants;
import de.ivu.elect.business.gebietsbaum.entity.GebietUK;
import de.ivu.elect.business.person.entity.Personendaten;
import de.ivu.elect.business.person.entity.PersonendatenPK;
import de.ivu.elect.business.veranstaltung.control.TeilnahmeExchangeContext;
import de.ivu.elect.business.veranstaltung.control.TeilnahmeImportExchange;
import de.ivu.elect.business.veranstaltung.control.TeilnahmePersonendatenFinder;
import de.ivu.elect.business.veranstaltung.control.TeilnahmePersonendatenFinder.Result;
import de.ivu.elect.business.veranstaltung.control.TeilnahmePersonendatenFinderFactory;
import de.ivu.elect.business.veranstaltung.entity.Teilnahme;
import de.ivu.elect.business.veranstaltung.entity.TeilnahmeMeta;
import de.ivu.elect.business.veranstaltung.entity.Veranstaltung;
import de.ivu.elect.business.veranstaltung.entity.VeranstaltungConstants;
import de.ivu.elect.business.veranstaltung.entity.VeranstaltungUK;
import de.ivu.elect.business.wahl.entity.WahlConstants;
import de.ivu.elect.business.wahl.entity.WahlUK;
import de.ivu.elect.business.wahl.entity.WahlterminConstants;
import de.ivu.elect.jee.entity.Meta;
import de.ivu.elect.jee.exchange.ImportOptions;
import de.ivu.elect.jee.exchange.Violations;
import de.ivu.elect.presentation.gebietsbaum.GebietNummerFormatter;
import de.ivu.elect.presentation.person.PersonendatenFormatter;

@Stateless
public class TeilnahmeImporter extends AbstractImporter<List<Teilnahme>, TeilnahmeExchangeContext, DateiImportBerichtCustomData> {

  private static final String VALIDIERUNG_TEILNAHME_IN_DATEI_MEHRFACH_GEFUNDEN = "teilnahme.import.error.teilnahmen.nicht.eindeutig";

  private static final String VALIDIERUNG_VERANSTALTUNG_IN_DB_NICHT_GEFUNDEN = "teilnahme.import.error.veranstaltung.nicht.vorhanden";

  private static final String VALIDIERUNG_VERANSTALTUNG_IN_DB_NICHT_EINDEUTIG_GEFUNDEN = "teilnahme.import.error.veranstaltung.nicht.eindeutig";

  private static final String VALIDIERUNG_TEILNEHMER_IN_DB_NICHT_GEFUNDEN = "teilnahme.import.error.teilnehmer.nicht.vorhanden";

  private static final String VALIDIERUNG_TEILNEHMER_IN_DB_NICHT_EINDEUTIG_GEFUNDEN = "teilnahme.import.error.teilnehmer.nicht.eindeutig";

  @Inject
  private transient VeranstaltungDAO veranstaltungDAO;

  @Inject
  private transient TeilnahmeDAO teilnahmeDAO;

  @Inject
  private transient TeilnahmeMeta teilnahmeMeta;

  @Inject
  private transient TeilnahmeImportExchange teilnahmeImportExchange;

  @Inject
  private transient TeilnahmePersonendatenFinderFactory personendatenFinderFactory;

  @Inject
  private transient PersonendatenFormatter personendatenFormatter;

  @Inject
  private transient GebietNummerFormatter gebietNummerFormatter;

  public TeilnahmeImporter() {
    super();
  }

  @Override
  public Meta getMetaForEntityInternal() {
    return teilnahmeMeta;
  }

  @Override
  protected ImportExchange<List<Teilnahme>, TeilnahmeExchangeContext> getExchange() {
    return teilnahmeImportExchange;
  }

  @Override
  protected void aktualisiereBestehendenDaten(List<Teilnahme> teilnahmen, TeilnahmeExchangeContext context, DateiImportBerichtCustomData customDateiImportBericht, StringBuilder bericht) {
    // NOTE, dass die CSV-Datei nur bereits vorhandene Personen einer bereits vorhandenen Veranstaltung zuweist.
    // Es gibt hier nichts zu aktualisieren.
    throw new UnsupportedOperationException("Momentan nicht unterstützt, cf. TeilnahmeImportDescription#getDefaultImportOptions");
  }

  @Override
  protected void loescheBestehendenDatenWennNichtInImportDatenVorhanden(List<Teilnahme> teilnahmen, TeilnahmeExchangeContext context, DateiImportBerichtCustomData customDateiImportBericht,
      StringBuilder bericht) {
    // HINT: Theoretisch könnte man hier Teilnahmen, die in der Datenbank bestehen, jedoch nicht in der CSV-Datei sind,
    // löschen. Ich gehe jetzt aber von einem inkrementellen Import aus, bei dem man verschiedene Teilnehmerlisten
    // nacheinander importieren kann.
    throw new UnsupportedOperationException("Momentan nicht unterstützt, cf. TeilnahmeImportDescription#getDefaultImportOptions");
  }

  @Override
  protected void anlegenWennNichtVorhanden(List<Teilnahme> teilnahmenInDatei, TeilnahmeExchangeContext context, DateiImportBerichtCustomData teilnahmeImportBericht, StringBuilder bericht) {
    Map<Veranstaltung, Collection<Teilnahme>> teilnahmenInDateiPerVeranstaltungInDb = collectTeilnahmenInDateiPerVeranstaltungInDb(teilnahmenInDatei, context);

    for (Entry<Veranstaltung, Collection<Teilnahme>> entry : teilnahmenInDateiPerVeranstaltungInDb.entrySet()) {
      anlegenWennNichtVorhanden(entry.getKey(), entry.getValue(), context, teilnahmeImportBericht);
    }
  }

  public void anlegenWennNichtVorhanden(Veranstaltung veranstaltungInDb, Collection<Teilnahme> teilnahmenInDatei, TeilnahmeExchangeContext context, DateiImportBerichtCustomData customData) {
    Set<PersonendatenPK> teilnehmerPKsInDb = collectTeilnehmerPKsInDb(veranstaltungInDb);

    TeilnahmePersonendatenFinder teilnahmePersonendatenFinder = personendatenFinderFactory.createPersonendatenFinder(teilnahmenInDatei, context);

    for (Teilnahme teilnahmeInDatei : teilnahmenInDatei) {
      Result teilnahmePersonendatenFinderResult = teilnahmePersonendatenFinder.findPersonendatenInDb(teilnahmeInDatei);

      if (teilnahmePersonendatenFinderResult.isErgebnisEindeutig() && !teilnahmePersonendatenFinderResult.isContainedIn(teilnehmerPKsInDb)) {
        teilnahmeInDatei.setPersonendaten(teilnahmePersonendatenFinderResult.getPersonendatenEindeutig());
        teilnahmeInDatei.setVeranstaltung(veranstaltungInDb);

        teilnahmeDAO.persist(teilnahmeInDatei);
      }
    }
  }

  @Override
  public void validiere(Violations violations, List<Teilnahme> teilnahmenInDatei, TeilnahmeExchangeContext context, ImportOptions importOptions) {
    validiereVeranstaltungen(violations, teilnahmenInDatei, context);

    // HINT: Nur weitermachen, wenn die Veranstaltungen zugeordnet werden können
    if (!violations.containsViolationsWithLevelError()) {
      Map<Veranstaltung, Collection<Teilnahme>> teilnahmenInDateiPerVeranstaltungInDb = collectTeilnahmenInDateiPerVeranstaltungInDb(teilnahmenInDatei, context);

      for (Entry<Veranstaltung, Collection<Teilnahme>> entry : teilnahmenInDateiPerVeranstaltungInDb.entrySet()) {
        validateTeilnahmenFuerVeranstaltung(violations, entry.getKey(), entry.getValue(), context);
      }
    }
  }

  private void validiereVeranstaltungen(Violations violations, List<Teilnahme> teilnahmenInDatei, TeilnahmeExchangeContext context) {
    for (Teilnahme teilnahmeInDatei : teilnahmenInDatei) {
      Veranstaltung veranstaltungInDatei = teilnahmeInDatei.getVeranstaltung();

      Set<Veranstaltung> veranstaltungenInDb = collectVeranstaltungenInDb(veranstaltungInDatei, context);

      if (veranstaltungenInDb.isEmpty()) {
        violations.addError(VALIDIERUNG_VERANSTALTUNG_IN_DB_NICHT_GEFUNDEN, veranstaltungInDatei.getBezeichnung(), veranstaltungInDatei.getExterneId());
      } else if (veranstaltungenInDb.size() > 1) {
        violations.addError(VALIDIERUNG_VERANSTALTUNG_IN_DB_NICHT_EINDEUTIG_GEFUNDEN, veranstaltungInDatei.getBezeichnung(), veranstaltungInDatei.getExterneId(), veranstaltungenInDb.size());
      }
    }
  }

  public void validateTeilnahmenFuerVeranstaltung(Violations violations, Veranstaltung veranstaltungInDb, Collection<Teilnahme> teilnahmenFuerVeranstaltungInDatei, TeilnahmeExchangeContext context) {
    TeilnahmePersonendatenFinder teilnahmePersonendatenFinder = personendatenFinderFactory.createPersonendatenFinder(teilnahmenFuerVeranstaltungInDatei, context);

    Set<Personendaten> personendatenInDb = new HashSet<>();

    for (Teilnahme teilnahmeFuerVeranstaltungInDatei : teilnahmenFuerVeranstaltungInDatei) {
      Personendaten personendatenInDatei = teilnahmeFuerVeranstaltungInDatei.getPersonendaten();
      String personendatenInDateiFormatiert = personendatenFormatter.getFamiliennameVornameGeburtsjahr(personendatenInDatei);

      validateEntity(violations, teilnahmeFuerVeranstaltungInDatei, personendatenInDateiFormatiert);

      Result teilnahmePersonendatenFinderResult = teilnahmePersonendatenFinder.findPersonendatenInDb(teilnahmeFuerVeranstaltungInDatei);

      if (teilnahmePersonendatenFinderResult.isErgebnisLeer()) {
        violations.addWarning(VALIDIERUNG_TEILNEHMER_IN_DB_NICHT_GEFUNDEN, personendatenInDateiFormatiert, personendatenInDatei.getExterneId(), veranstaltungInDb.getBezeichnung(),
            veranstaltungInDb.getExterneId(), teilnahmePersonendatenFinderResult.getAnzahlPersonendaten());
      } else if (teilnahmePersonendatenFinderResult.isErgebnisMehrdeutig()) {
        violations.addWarning(VALIDIERUNG_TEILNEHMER_IN_DB_NICHT_EINDEUTIG_GEFUNDEN, personendatenInDateiFormatiert, personendatenInDatei.getExterneId(), veranstaltungInDb.getBezeichnung(),
            veranstaltungInDb.getExterneId(), teilnahmePersonendatenFinderResult.getAnzahlPersonendaten());
      } else {
        if (!personendatenInDb.add(teilnahmePersonendatenFinderResult.getPersonendatenEindeutig())) {
          // ELECT-8896, mehrfache (identische) Teilnahmen in Datei sind ein Fehler!!!
          violations.addError(VALIDIERUNG_TEILNAHME_IN_DATEI_MEHRFACH_GEFUNDEN, personendatenInDateiFormatiert, personendatenInDatei.getExterneId(), veranstaltungInDb.getBezeichnung(),
              veranstaltungInDb.getExterneId());
        }
      }
    }
  }

  @Override
  protected ImportUpdateReport createImportUpdateReport(List<Teilnahme> teilnahmenInDatei, Violations violations, TeilnahmeExchangeContext context, ImportOptions importOptions,
      StringBuilder bericht) {
    Map<Veranstaltung, Collection<Teilnahme>> teilnahmenInDateiPerVeranstaltungInDb = collectTeilnahmenInDateiPerVeranstaltungInDb(teilnahmenInDatei, context);

    int anzahlTeilnahmenInDb = 0;

    int anzahlTeilnahmenInDateiInDb = 0;
    int anzahlTeilnahmenInDateiInDbNichtVorhanden = 0;

    if (context.hasWahl()) {
      anzahlTeilnahmenInDb = teilnahmeDAO.findByWahlCount(context.getWahl()).intValue();
    } else {
      anzahlTeilnahmenInDb = teilnahmeDAO.findByWahlterminCount(context.getWahltermin()).intValue();
    }

    for (Entry<Veranstaltung, Collection<Teilnahme>> entry : teilnahmenInDateiPerVeranstaltungInDb.entrySet()) {
      TeilnahmePersonendatenFinder teilnahmePersonendatenFinderResult = personendatenFinderFactory.createPersonendatenFinder(teilnahmenInDatei, context);

      for (Teilnahme teilnahmeInDatei : entry.getValue()) {
        Result result = teilnahmePersonendatenFinderResult.findPersonendatenInDb(teilnahmeInDatei);

        if (result.isErgebnisMehrdeutigOderLeer()) {
          // HINT: in Statistik ignorieren; erscheint im Bericht
        } else if (result.isErgebnisEindeutig()) {
          anzahlTeilnahmenInDateiInDb++;
        } else {
          anzahlTeilnahmenInDateiInDbNichtVorhanden++;
        }
      }
    }

    return new ImportUpdateReport(violations, anzahlTeilnahmenInDateiInDbNichtVorhanden, anzahlTeilnahmenInDb - anzahlTeilnahmenInDateiInDb, anzahlTeilnahmenInDateiInDb, importOptions, bericht);
  }

  @Override
  public void erstelleBericht(List<Teilnahme> teilnahmenInDatei, TeilnahmeExchangeContext context, ImportOptions importOptions, StringBuilder bericht, boolean vorab) {
    if (context.hasWahl()) {
      bericht.append(getMessage(WahlConstants.MESSAGE_KEY_ENTITY_EINZAHL)).append(": ").append(context.getWahl().getName());
    } else {
      bericht.append(getMessage(WahlterminConstants.MESSAGE_KEY_ENTITY_EINZAHL)).append(": ").append(context.getWahltermin().getName());
    }

    bericht.append(DATEI_IMPORT_BERICHT_LINE_SEPARATOR);
    bericht.append(getMessage(GebietConstants.MESSAGE_KEY_ENTITY_EINZAHL)).append(": ").append(gebietNummerFormatter.getNameMitNummer(context.getGebiet()));

    Map<Veranstaltung, Collection<Teilnahme>> teilnahmenInDateiPerVeranstaltungInDb = collectTeilnahmenInDateiPerVeranstaltungInDb(teilnahmenInDatei, context);

    for (Entry<Veranstaltung, Collection<Teilnahme>> entry : teilnahmenInDateiPerVeranstaltungInDb.entrySet()) {
      List<Teilnahme> teilnahmeFuerVeranstaltungInDateiInDb = new ArrayList<>();
      List<Teilnahme> teilnahmeFuerVeranstaltungInDateiInDbNichtVorhanden = new ArrayList<>();
      List<Teilnahme> teilnahmeFuerVeranstaltungInDateiInDbNichtEindeutig = new ArrayList<>();

      Collection<Teilnahme> teilnahmenFuerVeranstaltungInDatei = entry.getValue();
      TeilnahmePersonendatenFinder teilnahmePersonendatenFinder = personendatenFinderFactory.createPersonendatenFinder(teilnahmenInDatei, context);

      for (Teilnahme teilnahmeFuerVeranstaltungInDatei : teilnahmenFuerVeranstaltungInDatei) {
        Result teilnahmePersonendatenFinderResult = teilnahmePersonendatenFinder.findPersonendatenInDb(teilnahmeFuerVeranstaltungInDatei);

        if (teilnahmePersonendatenFinderResult.isErgebnisLeer()) {
          teilnahmeFuerVeranstaltungInDateiInDbNichtVorhanden.add(teilnahmeFuerVeranstaltungInDatei);
        } else if (teilnahmePersonendatenFinderResult.isErgebnisEindeutig()) {
          teilnahmeFuerVeranstaltungInDateiInDb.add(teilnahmeFuerVeranstaltungInDatei);
        } else if (teilnahmePersonendatenFinderResult.isErgebnisMehrdeutig()) {
          teilnahmeFuerVeranstaltungInDateiInDbNichtEindeutig.add(teilnahmeFuerVeranstaltungInDatei);
        }
      }

      bericht.append(DATEI_IMPORT_BERICHT_LINE_SEPARATOR);
      bericht.append(getMessage(VeranstaltungConstants.MESSAGE_KEY_ENTITY_EINZAHL) + " " + entry.getKey().getBezeichnung()).append(": ");
      bericht.append(getMessage("teilnahme.import.bericht.veranstaltung", teilnahmenFuerVeranstaltungInDatei.size(), teilnahmeFuerVeranstaltungInDateiInDb.size(),
          teilnahmeFuerVeranstaltungInDateiInDbNichtVorhanden.size(), teilnahmeFuerVeranstaltungInDateiInDbNichtEindeutig.size()));
    }
  }

  private Map<Veranstaltung, Collection<Teilnahme>> collectTeilnahmenInDateiPerVeranstaltungInDb(Collection<Teilnahme> teilnahmenInDatei, TeilnahmeExchangeContext context) {
    Map<Veranstaltung, Collection<Teilnahme>> result = new HashMap<>();

    for (Teilnahme teilnahmeInDatei : teilnahmenInDatei) {
      findVeranstaltungInDb(teilnahmeInDatei.getVeranstaltung(), context).ifPresent(veranstaltungInDb -> {
        result.putIfAbsent(veranstaltungInDb, new ArrayList<>());
        result.get(veranstaltungInDb).add(teilnahmeInDatei);
      });
    }

    return result;
  }

  private Set<PersonendatenPK> collectTeilnehmerPKsInDb(Veranstaltung veranstaltungInDb) {
    return veranstaltungInDb.getTeilnahmesByVeranstaltung().stream() //
        .filter(Teilnahme::isPersistent) //
        .map(Teilnahme::getPersonendaten) //
        .map(Personendaten::getPk) //
        .collect(toSet());
  }

  private Optional<Veranstaltung> findVeranstaltungInDb(Veranstaltung veranstaltungInDatei, TeilnahmeExchangeContext context) {
    Collection<Veranstaltung> result = collectVeranstaltungenInDb(veranstaltungInDatei, context);

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

    return Optional.empty();
  }

  private Set<Veranstaltung> collectVeranstaltungenInDb(Veranstaltung veranstaltungInDatei, TeilnahmeExchangeContext context) {
    VeranstaltungUK veranstaltungUK = veranstaltungInDatei.getVeranstaltungUK();
    String externeId = veranstaltungUK.getExterneId();

    GebietUK gebietUK = veranstaltungUK.getGebietUK();
    String gebietNummer = gebietUK.getNummer();
    String gebietGebietsartName = gebietUK.getGebietsartUK().getName();

    if (context.hasWahl()) {
      WahlUK wahlUK = context.getWahl().getWahlUK();

      return new HashSet<>(veranstaltungDAO.findByUkNurWahl(externeId, gebietNummer, gebietGebietsartName, wahlUK.getWahlartUK().getName(), wahlUK.getWahlterminUK().getWahldatum()));
    }

    return new HashSet<>(veranstaltungDAO.findByUkNurWahltermin(externeId, gebietNummer, gebietGebietsartName, context.getWahltermin().getWahlterminUK().getWahldatum()));
  }
}
