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

import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import javax.inject.Inject;

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

import de.ivu.elect.business.ergebnis.boundary.DHondtQuotientDAO;
import de.ivu.elect.business.ergebnis.boundary.ErgebnisLogEintragDAO;
import de.ivu.elect.business.ergebnis.boundary.GewaehltDAO;
import de.ivu.elect.business.ergebnis.boundary.SitzeDAO;
import de.ivu.elect.business.ergebnis.boundary.SitzeFactory;
import de.ivu.elect.business.ergebnis.boundary.StimmenDAO;
import de.ivu.elect.business.ergebnis.control.StimmenKumulierer;
import de.ivu.elect.business.ergebnis.control.StimmenPool;
import de.ivu.elect.business.ergebnis.control.StimmergebnisDatenCreator;
import de.ivu.elect.business.ergebnis.entity.Ergebnis;
import de.ivu.elect.business.ergebnis.entity.ErgebnisStatus;
import de.ivu.elect.business.ergebnis.entity.Gewaehlt;
import de.ivu.elect.business.ergebnis.entity.Sitze;
import de.ivu.elect.business.ergebniskonflikt.boundary.ErgebnisKonfliktDAO;
import de.ivu.elect.business.ergebniskonflikt.entity.ErgebnisKonfliktCustomDataFormatter;
import de.ivu.elect.business.internet.control.PresentationGenerieren;
import de.ivu.elect.business.konfiguration.entity.ElectKonfigurationWerte;
import de.ivu.elect.business.konfiguration.entity.KonfigurationLoader;
import de.ivu.elect.business.sitzberechnung.entity.GewaehltKey;
import de.ivu.elect.business.sitzberechnung.entity.SitzberechnungsalgorithmusKonfiguration;
import de.ivu.elect.business.sitzberechnung.entity.Sitzberechnungseingabe;
import de.ivu.elect.business.sitzberechnung.entity.Sitzberechnungsergebnis;
import de.ivu.elect.business.sitzberechnung.entity.SitzeKey;
import de.ivu.elect.business.system.boundary.SystemLogWriter;
import de.ivu.elect.business.system.entity.SystemLogQuelle;
import de.ivu.elect.business.wahl.boundary.WahldurchfuehrungDAO;
import de.ivu.elect.business.wahl.entity.WahlParameterLoader;
import de.ivu.elect.business.wahl.entity.WahlParameterWerte;
import de.ivu.elect.jee.faces.Messages;
import de.ivu.elect.presentation.person.PersonendatenFormatter;

public class Sitzberechnung {

  protected static final Logger LOG = LoggerFactory.getLogger(Sitzberechnung.class);

  @Inject
  transient SitzberechnungsalgorithmusFinder sitzberechnungsalgorithmusFinder;

  @Inject
  transient WahldurchfuehrungDAO wahldurchfuehrungDAO;

  @Inject
  transient ErgebnisKonfliktDAO konfliktDAO;

  @Inject
  transient SitzeDAO sitzeDAO;

  @Inject
  transient GewaehltDAO gewaehltDAO;

  @Inject
  transient ErgebnisLogEintragDAO ergebnisLogEintragDAO;

  @Inject
  transient DHondtQuotientDAO dHondtQuotientDAO;

  @Inject
  transient StimmergebnisDatenCreator stimmergebnisDatenCreator;

  @Inject
  transient Messages messages;

  @Inject
  transient SystemLogWriter systemLogWriter;

  @Inject
  transient ErgebnisKonfliktCustomDataFormatter ergebnisKonfliktCustomDataFormatter;

  @Inject
  transient PersonendatenFormatter personendatenFormatter;

  @Inject
  transient SitzeFactory sitzeFactory;

  @Inject
  transient WahlParameterLoader wahlParameterLoader;

  @Inject
  transient KonfigurationLoader konfigurationLoader;

  @Inject
  transient StimmenKumulierer stimmenKumulierer;

  @Inject
  transient ErgebnisKonfliktDAO ergebnisKonfliktDAO;

  @Inject
  transient StimmenDAO stimmenDAO;

  @Inject
  transient PresentationGenerieren presentationGenerieren;

  public Sitzberechnung() {
    super();
  }

  public void berechneSitze(Ergebnis ergebnis, SitzberechnungsalgorithmusKonfiguration konfiguration) {
    var wahldurchfuehrung = wahldurchfuehrungDAO.findByPrimaryKey(ergebnis.getWahl().getPk());

    var sitzberechnungsalgorithmus = sitzberechnungsalgorithmusFinder.getSitzberechnungsalgorithmus(wahldurchfuehrung);

    var wahlartSitzberechnungsalgorithmus = sitzberechnungsalgorithmus.getSitzberechnungsalgorithmusEnum();
    LOG.info("Berechne Ergebnis: Sitzberechnungsalgorithmus \"{}\" ({})", messages.lokalisiere(wahlartSitzberechnungsalgorithmus),
        wahlartSitzberechnungsalgorithmus);

    Sitzberechnungsergebnis sitzberechnungsergebnis;

    var praesentationZuruecksetzen = false;

    if (ergebnis.getErgebnisStatus().isZuBerechnen()) {
      praesentationZuruecksetzen = true; // ELECTKOM-957 ergebnisberechnung wurde angestorßen => Präsentation auf jeden Fall neu erstellen
    }

    ergebnis.setErgebnisStatus(ErgebnisStatus.BERECHNET);

    konfiguration.setMessages(messages);
    konfiguration.setSystemLogWriter(systemLogWriter);
    konfiguration.setErgebnisKonfliktCustomDataFormatter(ergebnisKonfliktCustomDataFormatter);
    konfiguration.setPersonendatenFormatter(personendatenFormatter);
    konfiguration.setSitzeFactory(sitzeFactory);
    boolean variantenberechnungEnabled = konfigurationLoader.resolveBooleanValue(ElectKonfigurationWerte.SITZBERECHNUNG_VARIANTENBERECHNUNG_ENABLED, true);
    konfiguration.setVariantenberechnungEnabled(variantenberechnungEnabled);

    var maximaleAnzahlSitzberechnungenInVariantenberechnung = wahlParameterLoader.resolveLongValue(ergebnis.getWahl(),
        WahlParameterWerte.MAX_ANZAHL_SITZBERECHNUNGEN_IN_VARIANTENBERECHNUNG);
    if (maximaleAnzahlSitzberechnungenInVariantenberechnung == null) {
      konfiguration.setMaximaleAnzahlSitzberechnungenInVariantenberechnung(0L);
    } else {
      konfiguration.setMaximaleAnzahlSitzberechnungenInVariantenberechnung(maximaleAnzahlSitzberechnungenInVariantenberechnung);
    }

    var maxAnzahlLogeintraege = wahlParameterLoader.resolveLongValue(ergebnis.getWahl(), WahlParameterWerte.MAX_ANZAHL_SITZBERECHNUNGEN_LOG_EINTRAEGE);
    if (maxAnzahlLogeintraege == null || maxAnzahlLogeintraege <= 0) {
      konfiguration.setProtokollErstellen(false); // ELECT-16199
    }

    ergebnis.setSitzberechnungsalgorithmus(wahlartSitzberechnungsalgorithmus);

    LOG.info("Berechne Ergebnis: vor wahldurchfuehrungDAO.flush()");
    wahldurchfuehrungDAO.flush(); // Aufruf kann aus Ergebnisübernahme erfolgen => geänderte Stimmen, alles in DB speichern
    LOG.info("Berechne Ergebnis: nach wahldurchfuehrungDAO.flush()");

    var bisherigeKonflikte = ergebnisKonfliktDAO.findByErgebnisOrderById(ergebnis);
    var bisherigeSitze = sitzeDAO.findByErgebnis(ergebnis);
    var bisherigeGewaehlte = gewaehltDAO.findByErgebnis(ergebnis);
    var bisherigeLogEintraege = ergebnisLogEintragDAO.findByErgebnisOrderByPosition(ergebnis);
    var bisherigeDHondtQuotienten = dHondtQuotientDAO.findByErgebnis(ergebnis);

    if (sitzberechnungsalgorithmus.isBerechnetErgebnis()) {
      var stimmergebnisDaten = stimmergebnisDatenCreator.createErgebnisDatenSitzberechnung(ergebnis);
      LOG.info("Berechne Ergebnis: lade Stimmenpool");

      var stimmen = StimmenPool.createStimmenPool(stimmenDAO, ergebnis);

      Sitzberechnungseingabe sitzberechnungseingabe = new Sitzberechnungseingabe<>(stimmergebnisDaten, ergebnis, stimmen, bisherigeKonflikte,
          wahlParameterLoader, konfiguration, stimmenKumulierer);
      LOG.info("Berechne Ergebnis: berechneSitzverteilung");
      sitzberechnungsergebnis = sitzberechnungsalgorithmus.berechneSitzverteilung(sitzberechnungseingabe);
      LOG.info("Berechne Ergebnis: berechneSitzverteilung - done");
    } else {
      sitzberechnungsergebnis = Sitzberechnungsergebnis.KEIN_ERGEBNIS;
    }

    var neueKonflikte = sitzberechnungsergebnis.getKonflikte();

    LOG.info("Bisherige Konflikte: " + bisherigeKonflikte.size());

    // Vorhandene Konflikte nach Berechnung löschen, wenn nicht in den neuen Konflikten enthalten
    for (var konflikt : bisherigeKonflikte) {
      if (!neueKonflikte.contains(konflikt)) {
        konfliktDAO.remove(konflikt);
      }
    }

    LOG.info("Anzahl Konflikte: " + neueKonflikte.size());
    // neue Konflikte anlegen, wenn nicht in den bisherigen vorhanden
    for (var neuerKonflikt : neueKonflikte) {
      if (!neuerKonflikt.isGeloest()) {
        ergebnis.setErgebnisStatus(ErgebnisStatus.UNGELOESTE_KONFLIKTE_VORHANDEN);
      }
      if (!bisherigeKonflikte.contains(neuerKonflikt)) {
        neuerKonflikt.setErgebnis(ergebnis);
        konfliktDAO.persist(neuerKonflikt);
      }
    }

    aktualisiereSitze(ergebnis, sitzberechnungsergebnis, bisherigeSitze);

    aktualisierGewaehlte(ergebnis, sitzberechnungsergebnis, bisherigeGewaehlte);

    LOG.info("Lösche bisherige ErgebnisLogEintraege: " + bisherigeLogEintraege.size());

    // bisherige Log-Einträge löschen
    for (var logEintrag : bisherigeLogEintraege) {
      ergebnisLogEintragDAO.remove(logEintrag);
    }

    LOG.info("Anzahl ErgebnisLogEintraege: " + sitzberechnungsergebnis.getErgebnisLogEintraege().size());

    if (maxAnzahlLogeintraege == null || maxAnzahlLogeintraege <= 0) {
      LOG.info("Schreibe keine Logeintraege");
    } else {
      if (sitzberechnungsergebnis.getErgebnisLogEintraege().size() <= maxAnzahlLogeintraege) {
        var count = 1L;
        for (var logEintrag : sitzberechnungsergebnis.getErgebnisLogEintraege()) {
          logEintrag.setErgebnis(ergebnis);
          logEintrag.setPosition(count++);
          ergebnisLogEintragDAO.persist(logEintrag);
        }
      } else {
        systemLogWriter.writeWarnLogMessageFuerMandant(ergebnis,
            messages.getMessageForMessageKey("sitzberechnung.zu.viele.logeintraege",
                sitzberechnungsergebnis.getErgebnisLogEintraege().size(),
                maxAnzahlLogeintraege,
                ergebnis.getTk().toStringLokalisiert(messages),
                messages.lokalisiere(WahlParameterWerte.MAX_ANZAHL_SITZBERECHNUNGEN_LOG_EINTRAEGE),
                ergebnis.getMandant().getTk().toStringLokalisiert(messages),
                ergebnis.getWahl().getTk().toStringLokalisiert(messages)),
            SystemLogQuelle.ZWISCHENERGEBNISBERECHNUNG);
        if (!ergebnis.getMandant().getArt().isSystemMandant()) {
          systemLogWriter.writeWarnLogMessageFuerSystemMandant(
              messages.getMessageForMessageKey("sitzberechnung.zu.viele.logeintraege",
                  sitzberechnungsergebnis.getErgebnisLogEintraege().size(),
                  maxAnzahlLogeintraege,
                  ergebnis.getTk().toStringLokalisiert(messages),
                  messages.lokalisiere(WahlParameterWerte.MAX_ANZAHL_SITZBERECHNUNGEN_LOG_EINTRAEGE),
                  ergebnis.getMandant().getTk().toStringLokalisiert(messages),
                  ergebnis.getWahl().getTk().toStringLokalisiert(messages)),
              SystemLogQuelle.ZWISCHENERGEBNISBERECHNUNG);
        }
      }
    }

    LOG.info("Lösche bisherige DHondtQuotienten: " + bisherigeDHondtQuotienten.size());

    // bisherige dHondtEinträge löschen
    for (var dHondtQuotient : bisherigeDHondtQuotienten) {
      dHondtQuotientDAO.remove(dHondtQuotient);
    }

    LOG.info("Anzahl DHondtQuotienten: " + sitzberechnungsergebnis.getDHondtQuotienten().size());

    for (var dHondtQuotient : sitzberechnungsergebnis.getDHondtQuotienten()) {
      dHondtQuotient.setErgebnis(ergebnis);
      dHondtQuotientDAO.persist(dHondtQuotient);
    }

    ergebnis.setBemerkung(sitzberechnungsergebnis.getBemerkung());

    LOG.info("Custom Data");
    handleCustomData(ergebnis, sitzberechnungsergebnis);

    if (praesentationZuruecksetzen) {
      presentationGenerieren.preasentationFuerErgebnisStarten(ergebnis);
    }
    LOG.info("Berechne Ergebnis: done");
  }

  private void aktualisierGewaehlte(Ergebnis ergebnis, Sitzberechnungsergebnis sitzberechnungsergebnis, List<Gewaehlt> bisherigeGewaehlte) {
    // ELECT-16209
    LOG.info("Bisherige Gewaehlte: " + bisherigeGewaehlte.size());
    var neueGewaehlte = sitzberechnungsergebnis.getGewaehlte();
    LOG.info("Neue Gewaehlte: " + neueGewaehlte.size());

    var gewaehlteMap = bisherigeGewaehlte.stream().collect(Collectors.toMap(GewaehltKey::new, g -> g, (a, b) -> b));

    Set<GewaehltKey> verwendet = new HashSet<>();
    for (var gewaehlt : neueGewaehlte) {
      var gewaehltKey = new GewaehltKey(gewaehlt);
      var gewaehltBisher = gewaehlteMap.get(gewaehltKey);
      if (gewaehltBisher == null) {
        gewaehlt.setErgebnis(ergebnis);
        gewaehltDAO.persist(gewaehlt);
      } else {
        verwendet.add(gewaehltKey);
        gewaehlt.copyEntityAttributesUsingSetter(gewaehltBisher);
      }
    }

    long angelegt = neueGewaehlte.size() - verwendet.size();
    LOG.info("Gewaehlte angelegt: " + angelegt);
    LOG.info("Gewaehlte wiederverwendet: " + verwendet.size());
    long zuLoeschen = bisherigeGewaehlte.size() - verwendet.size();
    LOG.info("Gewaehlte zu löschen: " + zuLoeschen);
    if (zuLoeschen > 0) {
      for (var entry : gewaehlteMap.entrySet()) {
        if (!verwendet.contains(entry.getKey())) {
          gewaehltDAO.remove(entry.getValue());
        }
      }
    }
  }

  private void aktualisiereSitze(Ergebnis ergebnis, Sitzberechnungsergebnis sitzberechnungsergebnis, List<Sitze> bisherigeSitze) {
    // ELECT-16209
    LOG.info("Bisherige Sitze: " + bisherigeSitze.size());
    var neueSitze = sitzberechnungsergebnis.getSitze();
    LOG.info("Neue Sitze: " + neueSitze.size());

    var sitzeMap = bisherigeSitze.stream().collect(Collectors.toMap(SitzeKey::new, sitze -> sitze, (a, b) -> b));

    Set<SitzeKey> verwendet = new HashSet<>();
    for (var sitze : neueSitze) {
      var sitzeKey = new SitzeKey(sitze);
      var sitzeBisher = sitzeMap.get(sitzeKey);
      if (sitzeBisher == null) {
        sitze.setErgebnis(ergebnis);
        sitzeDAO.persist(sitze);
      } else {
        verwendet.add(sitzeKey);
        sitze.copyEntityAttributesUsingSetter(sitzeBisher);
      }
    }

    long angelegt = neueSitze.size() - verwendet.size();
    LOG.info("Sitze angelegt: " + angelegt);
    LOG.info("Sitze wiederverwendet: " + verwendet.size());
    long zuLoeschen = bisherigeSitze.size() - verwendet.size();
    LOG.info("Sitze zu löschen: " + zuLoeschen);
    if (zuLoeschen > 0) {
      for (var entry : sitzeMap.entrySet()) {
        if (!verwendet.contains(entry.getKey())) {
          sitzeDAO.remove(entry.getValue());
        }
      }
    }
  }

  protected void handleCustomData(Ergebnis ergebnis, Sitzberechnungsergebnis sitzberechnungsergebnis) {
    // nichts zu tun
  }
}
