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

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

import javax.annotation.Nullable;
import javax.inject.Inject;

import de.ivu.elect.business.mandant.entity.Mandant;
import de.ivu.elect.business.person.entity.PersonenVerknuepfung;
import de.ivu.elect.business.person.entity.Personendaten;
import de.ivu.elect.business.person.entity.PersonendatenPairKey;
import de.ivu.elect.business.person.entity.PotentiellePersonenVerknuepfung;
import de.ivu.elect.business.person.entity.VerknuepfungsArt;
import de.ivu.elect.business.person.entity.VerknuepfungsGrundlage;
import de.ivu.elect.business.person.entity.VerknuepfungsStatus;
import de.ivu.elect.business.system.boundary.SystemLogWriter;
import de.ivu.elect.business.system.entity.SystemLogQuelle;
import de.ivu.elect.business.wahl.entity.Wahl;
import de.ivu.elect.business.wahl.entity.Wahltermin;

public class PersonenVerknuepfungBerechner<T extends Personendaten> {

  private static final int MAX_GLEICHE_PERSONEN = 250;

  private static final int MAX_NEUE_PERSONENVERKNUPFUNGEN = MAX_GLEICHE_PERSONEN * MAX_GLEICHE_PERSONEN;

  @Inject
  private transient SystemLogWriter systemLogWriter;

  public PersonenVerknuepfungBerechner() {
    super();
  }

  /**
   * Bestimmt die Verknüpfungen zwischen den übergebenen Personen, für die noch keine Verknüpfungen existieren, persistiert diese noch nicht.
   */
  public List<PersonenVerknuepfung> bestimmeFehlendeVerknuepfungen(Collection<? extends T> personendaten, PersonenVergleichFunktion<T> personenVergleichFunktion,
                                                                   PersonenVerknuepfungFilter verknuepfungsFilter, VerknuepfungsArt verknuepfungsArt) {
    Set<PersonenVerknuepfung> vorhandeneVerknuepfungen = new HashSet<>();

    for (T person : personendaten) {
      vorhandeneVerknuepfungen.addAll(person.getPersonenVerknuepfungen());
    }

    var groupsOfPersonenSharingSameAttributes = personenVergleichFunktion.paketierePersonen(personendaten);

    return bestimmeFehlendeVerknuepfungen(groupsOfPersonenSharingSameAttributes, vorhandeneVerknuepfungen, personenVergleichFunktion, verknuepfungsFilter,
        verknuepfungsArt);
  }

  private List<PersonenVerknuepfung> bestimmeFehlendeVerknuepfungen(List<List<T>> groupsOfPersonenSharingSameAttributes, Collection<PersonenVerknuepfung> vorhandeneVerknuepfungen,
                                                                    PersonenVergleichFunktion<T> personenVergleichFunktion, PersonenVerknuepfungFilter verknuepfungsFilter, VerknuepfungsArt verknuepfungsArt) {
    var personenVerknupefungPerPersonendatenPairKey = PersonenVerknuepfung.mappePersonenVerknuepfungenNachPersonendatenPairKey(
        vorhandeneVerknuepfungen);

    if (personenVerknupefungPerPersonendatenPairKey.size() != vorhandeneVerknuepfungen.size()) {
      throw new IllegalStateException("Offensichtlich sind Personenverknüpfungen zwischen zwei Personen in beiden Richtungen vorhanden!");
    }

    List<PersonenVerknuepfung> result = new ArrayList<>();

    for (var personenSharingSameAttributes : groupsOfPersonenSharingSameAttributes) {
      if (personenSharingSameAttributes.size() > 1) {
        result.addAll(berechnePersonenVerknuepfungen(personenSharingSameAttributes, personenVergleichFunktion, verknuepfungsFilter,
            personenVerknupefungPerPersonendatenPairKey, verknuepfungsArt));
      }
    }

    return result;
  }

  private Collection<PersonenVerknuepfung> berechnePersonenVerknuepfungen(List<T> personenSharingSameAttributes, PersonenVergleichFunktion<T> personenVergleichFunktion,
                                                                          PersonenVerknuepfungFilter verknuepfungsFilter, Map<PersonendatenPairKey, PersonenVerknuepfung> personenVerknupefungPerPersonendatenPairKey, VerknuepfungsArt verknuepfungsArt) {

    Collection<PersonenVerknuepfung> result = new ArrayList<>();

    var potentiellePersonenVerknuepfung = new PotentiellePersonenVerknuepfung();
    potentiellePersonenVerknuepfung.setVerknuepfungsGrundlage(VerknuepfungsGrundlage.MASCHINELL);
    potentiellePersonenVerknuepfung.setVerknuepfungsArt(verknuepfungsArt);

    for (var i = 0; i < personenSharingSameAttributes.size(); i++) {
      for (var j = i + 1; j < personenSharingSameAttributes.size(); j++) {
        var one = personenSharingSameAttributes.get(i);
        var another = personenSharingSameAttributes.get(j);

        var personendatenPairKey = new PersonendatenPairKey(one, another);

        if (personenVerknupefungPerPersonendatenPairKey.containsKey(personendatenPairKey)) {
          continue;
        }

        potentiellePersonenVerknuepfung.setPerson1(one);
        potentiellePersonenVerknuepfung.setPerson2(another);
        potentiellePersonenVerknuepfung.setVerknuepfungsStatus(vergleiche(one, another, personenVergleichFunktion));

        if (verknuepfungsFilter.isAccept(potentiellePersonenVerknuepfung)) {
          var personenVerknuepfung = potentiellePersonenVerknuepfung.createPersonenVerknuepfung();
          personenVerknupefungPerPersonendatenPairKey.put(personendatenPairKey, personenVerknuepfung);
          result.add(personenVerknuepfung);
        }
      }
    }

    if (result.size() > MAX_NEUE_PERSONENVERKNUPFUNGEN) {
      // ELECT-15885 und ELECT-16094
      var mandant = bestimmeMandant(personenSharingSameAttributes);
      var message =
          "Zu viele Personenverknüpfungen, Dublettenprüfung unvollständig: " + result.size() + " - " +
              "betrifft Wahl: " + bestimmeWahl(personenSharingSameAttributes) + ", betrifft Wahltermin: " + bestimmeWahltermin(personenSharingSameAttributes);
      if (mandant == null || !mandant.getArt().isSystemMandant()) {
        systemLogWriter.writeErrorLogMessageFuerMandant(mandant, message, SystemLogQuelle.PERSONENPRUEFUNG);
      }
      systemLogWriter.writeErrorLogMessageFuerSystemMandant(message, SystemLogQuelle.PERSONENPRUEFUNG);
      return result.stream().limit(MAX_NEUE_PERSONENVERKNUPFUNGEN).collect(Collectors.toSet());
    }

    return result;
  }

  @Nullable
  private Mandant bestimmeMandant(List<T> personenSharingSameAttributes) {
    if (personenSharingSameAttributes.isEmpty()) {
      return null;
    }
    return personenSharingSameAttributes.iterator().next().getMandant();
  }

  @Nullable
  private Wahl bestimmeWahl(List<T> personenSharingSameAttributes) {
    if (personenSharingSameAttributes.isEmpty()) {
      return null;
    }
    return personenSharingSameAttributes.iterator().next().getWahl();
  }

  @Nullable
  private Wahltermin bestimmeWahltermin(List<T> personenSharingSameAttributes) {
    if (personenSharingSameAttributes.isEmpty()) {
      return null;
    }
    return personenSharingSameAttributes.iterator().next().getWahltermin();
  }

  private VerknuepfungsStatus vergleiche(T one, T another, PersonenVergleichFunktion<T> personenVergleichFunktion) {
    // NOTE, dass die REIHENFOLGE WICHTIG IST!
    if (personenVergleichFunktion.isPersonenIdentisch(one, another)) {
      return VerknuepfungsStatus.IDENTISCH;
    }

    if (personenVergleichFunktion.isPersonenAehnlich(one, another)) {
      return VerknuepfungsStatus.AEHNLICH;
    }

    return VerknuepfungsStatus.VERSCHIEDEN;
  }
}
