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

import static de.ivu.elect.jee.entity.EntityUtil.normalizeCaseAndDiacritics;
import static java.util.Collections.emptySet;
import static java.util.Collections.singleton;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

import de.ivu.elect.business.person.boundary.AbstractPersonendatenDAO;
import de.ivu.elect.business.person.entity.Personendaten;
import de.ivu.elect.business.veranstaltung.boundary.AbstractTeilnahmeDAO;
import de.ivu.elect.business.veranstaltung.entity.Teilnahme;
import de.ivu.elect.business.veranstaltung.entity.Veranstaltung;
import de.ivu.elect.business.wahl.entity.Wahltermin;

public class TeilnahmePersonendatenByExterneIdFinder implements TeilnahmePersonendatenFinder {

  private static final int ANZAHL_ENTITAETEN_FUER_DB_ANFRAGE = 500;

  private final Map<String, Set<Personendaten>> personendatenInDbProExterneId;

  private final Map<Teilnahme, Personendaten> teilnehmerInDbProTeilnahmeInDatei = new HashMap<>();

  public TeilnahmePersonendatenByExterneIdFinder(Collection<Teilnahme> teilnahmenInDatei, AbstractPersonendatenDAO personendatenDao, AbstractTeilnahmeDAO teilnahmeDao, Wahltermin wahltermin) {
    super();

    Collection<Personendaten> personendatenInDbFuerTeilnahmenInDatei = findPersonendatenInDbByTeilnahmePersonendatenExterneId(personendatenDao, wahltermin, teilnahmenInDatei);
    Map<Personendaten, Set<Veranstaltung>> veranstaltungenInDbProPersonendaten = bestimmeVeranstaltungenInDbProPersonendaten(teilnahmeDao, personendatenInDbFuerTeilnahmenInDatei);

    personendatenInDbProExterneId = bestimmePersonendatenInDbProExterneId(personendatenInDbFuerTeilnahmenInDatei);

    for (Teilnahme teilnahmeInDatei : teilnahmenInDatei) {
      teilnehmerInDbProTeilnahmeInDatei.putAll(klassifiziereTeilnahme(teilnahmeInDatei, veranstaltungenInDbProPersonendaten));
    }
  }

  @Override
  public Result findPersonendatenInDb(Teilnahme teilnahmeInDatei) {
    Personendaten personendatenEindeutig = getEindeutigenTeilnehmerInDbFuerTeilnahmeInDatei(teilnahmeInDatei);

    if (personendatenEindeutig != null) {
      return new FindResult(singleton(personendatenEindeutig));
    }

    return new FindResult(getPotentielleTeilnehmerInDb(teilnahmeInDatei));
  }

  private Map<Teilnahme, Personendaten> klassifiziereTeilnahme(Teilnahme teilnahmeInDatei, Map<Personendaten, Set<Veranstaltung>> veranstaltungenInDbProPersonendaten) {
    Map<Teilnahme, Personendaten> result = new HashMap<>();

    Veranstaltung veranstaltungInDatei = teilnahmeInDatei.getVeranstaltung();
    Personendaten personendatenInDatei = teilnahmeInDatei.getPersonendaten();

    if (!personendatenInDbProExterneId.containsKey(personendatenInDatei.getExterneId())) {
      return result;
    }

    Collection<Personendaten> potentiellePersonendatenInDb = getPotentielleTeilnehmerInDb(personendatenInDatei);
    Collection<Personendaten> matchendePersonendatenInDb = bestimmeMatchendePersonendatenInDb(personendatenInDatei, potentiellePersonendatenInDb);

    if (matchendePersonendatenInDb.size() == 1) {
      Personendaten personendatenInDb = matchendePersonendatenInDb.iterator().next();

      if (veranstaltungenInDbProPersonendaten.containsKey(personendatenInDb)) {
        for (Veranstaltung veranstaltungInDb : veranstaltungenInDbProPersonendaten.get(personendatenInDb)) {
          String veranstaltungInDbExterneId = veranstaltungInDb.getExterneId();

          if (veranstaltungInDbExterneId != null && veranstaltungInDbExterneId.equals(veranstaltungInDatei.getExterneId())) {
            return result;
          }
        }

        result.put(teilnahmeInDatei, personendatenInDb);
      }
    }

    return result;
  }

  private Collection<Personendaten> bestimmeMatchendePersonendatenInDb(Personendaten personendatenInDatei, Collection<Personendaten> potentiellePersonendatenInDb) {
    return potentiellePersonendatenInDb.stream().filter(entity -> matchePersonendaten(personendatenInDatei, entity)).collect(Collectors.toList());
  }

  private boolean matchePersonendaten(Personendaten personendatenA, Personendaten personendatenB) {
    String normalisierterRufnameA = normalizeCaseAndDiacritics(personendatenA.getRufname());
    String normalisierterFamiliennameA = normalizeCaseAndDiacritics(personendatenA.getFamilienname());
    String normalisierterRufnameB = normalizeCaseAndDiacritics(personendatenB.getRufname());
    String normalisierterFamiliennameB = normalizeCaseAndDiacritics(personendatenB.getFamilienname());

    return normalisierterRufnameA.equals(normalisierterRufnameB) //
        && normalisierterFamiliennameA.equals(normalisierterFamiliennameB);
  }

  private Map<Personendaten, Set<Veranstaltung>> bestimmeVeranstaltungenInDbProPersonendaten(AbstractTeilnahmeDAO teilnahmeDao, Collection<Personendaten> personendatenInDbFuerTeilnahmenInDatei) {

    Map<Personendaten, Set<Veranstaltung>> ergebnis = new HashMap<>();

    if (!personendatenInDbFuerTeilnahmenInDatei.isEmpty()) {
      for (Teilnahme teilnahme : findTeilnahmenByPersonendaten(teilnahmeDao, personendatenInDbFuerTeilnahmenInDatei)) {
        ergebnis.computeIfAbsent(teilnahme.getPersonendaten(), x -> new HashSet<>()).add(teilnahme.getVeranstaltung());
      }
    }

    return ergebnis;
  }

  private Map<String, Set<Personendaten>> bestimmePersonendatenInDbProExterneId(Collection<Personendaten> personendatenInDbFuerTeilnahmenInDatei) {
    Map<String, Set<Personendaten>> ergebnis = new HashMap<>();

    for (Personendaten personendaten : personendatenInDbFuerTeilnahmenInDatei) {
      ergebnis.computeIfAbsent(personendaten.getExterneId(), x -> new HashSet<>()).add(personendaten);
    }

    return ergebnis;
  }

  private Collection<Personendaten> findPersonendatenInDbByTeilnahmePersonendatenExterneId(AbstractPersonendatenDAO personendatenDao, Wahltermin wahltermin, Collection<Teilnahme> teilnahmenInDatei) {
    Collection<String> externeIds = teilnahmenInDatei.stream().map(Teilnahme::getPersonendaten).map(Personendaten::getExterneId).filter(Objects::nonNull).collect(Collectors.toList());

    if (externeIds.isEmpty()) {
      return emptySet();
    }

    return findPersonendatenByWahlterminAndExterneId(personendatenDao, wahltermin, externeIds);
  }

  /**
   * Erforderlich, weil Oracle DBs IN-Operaten eine begrenzte Anzahl an Elementen unterstützen -- 1000.
   */
  private Collection<Personendaten> findPersonendatenByWahlterminAndExterneId(AbstractPersonendatenDAO personendatenDao, Wahltermin wahltermin, Collection<String> externeIds) {
    Collection<Personendaten> result = new ArrayList<>();
    Collection<String> buffer = null;

    for (String externeId : externeIds) {
      if (buffer == null) {
        buffer = new ArrayList<>();
      }

      buffer.add(externeId);

      if (buffer.size() == ANZAHL_ENTITAETEN_FUER_DB_ANFRAGE) {
        result.addAll(personendatenDao.findByWahlterminAndExterneId(wahltermin, buffer));

        buffer = null;
      }
    }

    if (buffer != null) {
      result.addAll(personendatenDao.findByWahlterminAndExterneId(wahltermin, buffer));
    }

    return result;
  }

  /**
   * Erforderlich, weil Oracle DBs IN-Operaten eine begrenzte Anzahl an Elementen unterstützen -- 1000.
   */
  private Collection<Teilnahme> findTeilnahmenByPersonendaten(AbstractTeilnahmeDAO teilnahmeDao, Collection<Personendaten> personendaten) {
    Collection<Teilnahme> result = new ArrayList<>();
    Collection<Personendaten> buffer = null;

    for (Personendaten daten : personendaten) {
      if (buffer == null) {
        buffer = new ArrayList<>();
      }

      buffer.add(daten);

      if (buffer.size() == ANZAHL_ENTITAETEN_FUER_DB_ANFRAGE) {
        result.addAll(teilnahmeDao.findByPersonen(buffer));
        buffer = null;
      }
    }

    if (buffer != null) {
      result.addAll(teilnahmeDao.findByPersonen(buffer));
    }

    return result;
  }

  public Personendaten getEindeutigenTeilnehmerInDbFuerTeilnahmeInDatei(Teilnahme teilnahmeInDatei) {
    return teilnehmerInDbProTeilnahmeInDatei.get(teilnahmeInDatei);
  }

  public Collection<Personendaten> getPotentielleTeilnehmerInDb(Teilnahme teilnahmeInDatei) {
    return getPotentielleTeilnehmerInDb(teilnahmeInDatei.getPersonendaten());
  }

  private Collection<Personendaten> getPotentielleTeilnehmerInDb(Personendaten personendaten) {
    if (personendaten != null) {
      return personendatenInDbProExterneId.getOrDefault(personendaten.getExterneId(), emptySet());
    }

    return emptySet();
  }
}
