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

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

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

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.ivu.elect.business.benutzerverwaltung.entity.Rolle;
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.control.Gebietsbaum;
import de.ivu.elect.business.gebietsbaum.control.GebietsbaumExchangeContext;
import de.ivu.elect.business.gebietsbaum.control.GebietsbaumStimmbezirkImportExchange;
import de.ivu.elect.business.gebietsbaum.control.StimmbezirkGebietsarten;
import de.ivu.elect.business.gebietsbaum.control.StimmbezirkGebietsbaumExchangeContext;
import de.ivu.elect.business.gebietsbaum.entity.Gebiet;
import de.ivu.elect.business.gebietsbaum.entity.GebietMeta;
import de.ivu.elect.business.gebietsbaum.entity.GebietTK;
import de.ivu.elect.business.gebietsbaum.entity.Gebietsart;
import de.ivu.elect.business.gebietsbaum.entity.GebietsartTK;
import de.ivu.elect.business.gruppeimgebiet.boundary.GruppeImGebietDAO;
import de.ivu.elect.business.gruppeimgebiet.boundary.GruppeImGebietHierarchyBuilder;
import de.ivu.elect.business.gruppeimgebiet.entity.GruppeImGebiet;
import de.ivu.elect.business.gruppeimgebiet.entity.GruppeImGebietTK;
import de.ivu.elect.business.mandant.entity.Mandant;
import de.ivu.elect.business.wahl.boundary.WahlDAO;
import de.ivu.elect.business.wahl.entity.Wahl;
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 static java.util.Objects.nonNull;

/**
 * Importiert nur die Stimmbezirke / Briefwahlbezirke, die im WOS gepflegt wurden
 */
@Stateless
public class GebietsbaumStimmbezirkImporter extends AbstractImporter<Gebietsbaum, StimmbezirkGebietsbaumExchangeContext, DateiImportBerichtCustomData> {
  private static final Logger LOG = LoggerFactory.getLogger(GebietsbaumStimmbezirkImporter.class);

  @Inject
  private transient GebietsbaumStimmbezirkImportExchange gebietsbaumStimmbezirkImportExchange;

  @Inject
  private transient GebietMeta gebietMeta;

  @Inject
  private transient StimmbezirkGebietsarten stimmbezirkGebietsarten;

  @Inject
  private transient GebietsbaumImporter gebietsbaumImporter;

  @Inject
  private transient GebietDAO gebietDAO;

  @Inject
  private transient GebietsartDAO gebietsartDAO;

  @Inject
  private transient WahlDAO wahlDAO;

  @Inject
  private transient GruppeImGebietHierarchyBuilder gruppeImGebietHierarchyBuilder;

  @Inject
  private transient GruppeImGebietDAO gruppeImGebietDAO;

  @Inject
  private transient GebietNummerFormatter gebietNummerFormatter;

  @Inject
  private transient GebietEdit gebietEdit;

  @Override
  protected void validiere(Violations violations, Gebietsbaum gebietsbaum, StimmbezirkGebietsbaumExchangeContext context, ImportOptions importOptions) {
    gebietsbaumImporter.validiere(violations, gebietsbaum, context, importOptions);

    Map<GebietTK, Gebiet> gebieteNachTk = getGebietTKGebietMap(gebietsbaum);

    for (Gebiet gebiet : gebietsbaum.getGebiete()) {

      Gebietsart gebietsart = gebiet.getGebietsart();
      String gebietsartName = gebietsart.getName();

      if (context.isNurStimmbezirke() && !isGebietsartDerStimmbezirke(context, gebietsartName)) {
        violations.addError("gebiet.stimmbezirk.import.unbekannte.gebietsart", gebietsartName, context.getWahl().getWahlart().getName());
      }

      Gebiet uebergeordnetesGebiet = gebiet.getUebergeordnetesGebiet();
      if (uebergeordnetesGebiet == null) {
        if (context.isNurStimmbezirke()) {
          violations.addError("gebiet.stimmbezirk.import.uebergeordnetes.gebiet.nicht.gesetzt", gebiet.getNummer(), gebiet.getGebietsart().getName());
        }
      } else {
        if (!isUebergeordnetesGebietInHierarchieVorhanden(uebergeordnetesGebiet, gebieteNachTk, context)) {
          String gebietsartNameUebergeordnet = "";
          if (uebergeordnetesGebiet.getGebietsart() != null) {
            gebietsartNameUebergeordnet = uebergeordnetesGebiet.getGebietsart().getName();
          }

          violations.addError("gebiet.stimmbezirk.import.unbekanntes.uebergeordnetes.gebiet", uebergeordnetesGebiet.getNummer(),
            gebietsartNameUebergeordnet);
        }
      }
    }

    Set<Gebiet> gebieteUnterhalbDenenStimmbezirkeGeloeschtWerden = getGebietUnterhalbDenenStimmbezirkeGeloeschtWerden(gebietsbaum, context);

    for (Gebiet loescheUnterhalbVon : gebieteUnterhalbDenenStimmbezirkeGeloeschtWerden) {
      violations.addInfo("gebiet.stimmbezirk.import.stimmbezirke.unterhalb.von.werden.geloescht", gebietNummerFormatter.getDarstellung(loescheUnterhalbVon));
    }

    Map<GebietTK, Gebiet> gebietTKGebietMap = GebietTK.mappeGebietNachTK(getEntitiesInDatabase(context));

    // ELECTKOM-196
    for (Gebiet gebietInDatei : gebietsbaum.getGebiete()) {

      Gebiet uebergeordnetesGebietInDatei = gebietInDatei.getUebergeordnetesGebiet();
      if (uebergeordnetesGebietInDatei != null) {
        Gebiet gebietInDb = gebietTKGebietMap.get(gebietInDatei.getGebietTK());
        if (gebietInDb != null) {
          Gebiet uebergeordnetesGebietInDb = gebietInDb.getUebergeordnetesGebiet();
          if (uebergeordnetesGebietInDb != null && !gebieteUnterhalbDenenStimmbezirkeGeloeschtWerden.contains(uebergeordnetesGebietInDb)) {
            violations.addWarning("gebiet.stimmbezirk.import.stimmbezirke.unter.anderdem.uebergeordneten.gebiet", gebietInDatei.getNummer(),
              gebietInDatei.getGebietsart().getName(), uebergeordnetesGebietInDatei.getNummer(), uebergeordnetesGebietInDatei.getGebietsart().getName(),
              uebergeordnetesGebietInDb.getNummer(), uebergeordnetesGebietInDb.getName());
          }
        }
      }
    }

  }

  private Map<GebietTK, Gebiet> getGebietTKGebietMap(Gebietsbaum gebietsbaum) {
    return gebietsbaum.getGebiete().stream().collect(Collectors.toMap(Gebiet::getGebietTK, gebiet -> gebiet, (a, b) -> b));
  }

  private boolean isUebergeordnetesGebietInHierarchieVorhanden(Gebiet uebergeordnetesGebiet, Map<GebietTK, Gebiet> gebieteNachTk, StimmbezirkGebietsbaumExchangeContext context) {
    return findeUebergeordnetesGebietInHierarchie(uebergeordnetesGebiet, gebieteNachTk, context) != null;
  }

  @Nullable
  protected Gebiet findeUebergeordnetesGebietInHierarchie(Gebiet uebergeordnetesGebiet, Map<GebietTK, Gebiet> gebieteNachTk,
                                                          StimmbezirkGebietsbaumExchangeContext context) {
    if (uebergeordnetesGebiet.getGebietsart() != null && StringUtils.isNotBlank(uebergeordnetesGebiet.getGebietsart().getName())) {
      try {
        Gebiet gebiet = findeGebietMitGebietsart(uebergeordnetesGebiet, context, attachGebietsart(context.getMandant(), uebergeordnetesGebiet.getGebietsart()));
        if (gebiet != null) {
          return gebiet;
        }
      } catch (Exception e) {
        // ignorieren
      }
    }

    if (context.isNurStimmbezirke()) {
      return null; // direktes Übergebiet muss vorhanden sein (Verhalten bisher)
    }

    if (uebergeordnetesGebiet.getUebergeordnetesGebiet() != null) {
      return findeUebergeordnetesGebietInHierarchie(uebergeordnetesGebiet.getUebergeordnetesGebiet(), gebieteNachTk, context);
    }

    Gebiet gebietNachTk = gebieteNachTk.get(uebergeordnetesGebiet.getGebietTK());
    if (gebietNachTk != null && gebietNachTk.getUebergeordnetesGebiet() != null) {
      return findeUebergeordnetesGebietInHierarchie(gebietNachTk.getUebergeordnetesGebiet(), gebieteNachTk, context);
    }

    return null;

  }

  @Nullable
  protected Gebiet findeGebietMitGebietsart(Gebiet uebergeordnetesGebiet, StimmbezirkGebietsbaumExchangeContext context, Gebietsart gebietsart) {
    if (gebietsart == null) {
      return null;
    }
    try {
      return gebietDAO.findByWahlAndNummerAndGebietsart(context.getWahl(), uebergeordnetesGebiet.getNummer(), gebietsart);
    } catch (Exception e) {
      // ignorieren
      return null;
    }
  }



  private Set<Gebiet> getGebietUnterhalbDenenStimmbezirkeGeloeschtWerden(Gebietsbaum gebietsbaum, StimmbezirkGebietsbaumExchangeContext context) {
    return bestimmeZuLoeschendeGebiete(gebietsbaum, context).stream()
        .filter(zuloeschen -> zuloeschen.getUebergeordnetesGebiet() != null).map(Gebiet::getUebergeordnetesGebiet).collect(Collectors.toSet());
  }

  private boolean isGebietsartDerStimmbezirke(StimmbezirkGebietsbaumExchangeContext context, String gebietsartName) {
    GebietsartTemplate gebietsartTemplateForStimmbezirke = stimmbezirkGebietsarten.getGebietsartTemplateForStimmbezirke(context.getMandant());
    GebietsartTemplate gebietsartTemplateForBriefwahlbezirke = stimmbezirkGebietsarten.getGebietsartTemplateForBriefwahlbezirke(context.getMandant());

    return nonNull(gebietsartTemplateForStimmbezirke) && gebietsartTemplateForStimmbezirke.equalsGebietsart(gebietsartName)
        || nonNull(gebietsartTemplateForBriefwahlbezirke) && gebietsartTemplateForBriefwahlbezirke.equalsGebietsart(gebietsartName);
  }

  @Override
  protected void erstelleBericht(Gebietsbaum gebietsbaum, StimmbezirkGebietsbaumExchangeContext context, ImportOptions importOptions, StringBuilder bericht, boolean vorab) {
    // nothing to do
  }

  @Override
  protected ImportUpdateReport createImportUpdateReport(Gebietsbaum gebietsbaum, Violations violations, StimmbezirkGebietsbaumExchangeContext context, ImportOptions importOptions,
                                                        StringBuilder bericht) {

    Set<Gebiet> gebietUnterhalbDenenStimmbezirkeGeloeschtWerden = getGebietUnterhalbDenenStimmbezirkeGeloeschtWerden(gebietsbaum, context);
    List<Gebiet> zuLoeschendeGebiete = bestimmeZuLoeschendeGebiete(gebietsbaum, context);
    List<Gebiet> gebietAnzulegen = filterAnderesObergebiet(gebietsbaum.getGebiete(), gebietUnterhalbDenenStimmbezirkeGeloeschtWerden, context);
    return new ImportUpdateReport(violations, gebietAnzulegen.size(), zuLoeschendeGebiete.size(), gebietsbaum.getGebiete().size() - gebietAnzulegen.size(),
      importOptions, bericht);
  }

  private List<Gebiet> filterAnderesObergebiet(List<Gebiet> gebiete, Set<Gebiet> gebietUnterhalbDenenStimmbezirkeGeloeschtWerden, StimmbezirkGebietsbaumExchangeContext context) {

    // ELECTKOM-196
    List<Gebiet> result = new ArrayList<>(gebiete.size());
    for (Gebiet gebietInDatei : gebiete) {
      Map<GebietTK, Gebiet> gebietTKGebietMap = GebietTK.mappeGebietNachTK(getEntitiesInDatabase(context));

      Gebiet gebietInDb = gebietTKGebietMap.get(gebietInDatei.getGebietTK());
      if (gebietInDb != null) {
        Gebiet uebergeordnetesGebietInDb = gebietInDb.getUebergeordnetesGebiet();
        if (uebergeordnetesGebietInDb != null && !gebietUnterhalbDenenStimmbezirkeGeloeschtWerden.contains(uebergeordnetesGebietInDb)) {
          continue;
        }
      }
      result.add(gebietInDatei);
    }

    return result;
  }

  private List<Gebiet> getEntitiesInDatabase(StimmbezirkGebietsbaumExchangeContext context) {
    if (context.isNurStimmbezirke()) {
      List<Gebiet> inDatenbank = new ArrayList<>();
      inDatenbank.addAll(gebietDAO.findByWahlAndGebietsart(context.getWahl(), getGebietsartStimmbezirk(context)));
      inDatenbank.addAll(gebietDAO.findByWahlAndGebietsart(context.getWahl(), getGebietsartBriefwahlbezirk(context)));
      return inDatenbank;
    } else {
      return gebietDAO.findByWahl(context.getWahl());
    }
  }

  private Gebietsart getGebietsartStimmbezirk(GebietsbaumExchangeContext context) {
    return gebietsartDAO.findByName(context.getMandant(), stimmbezirkGebietsarten.getGebietsartTemplateForStimmbezirke(context.getMandant()).getGebietsartName()).get(0);
  }

  private Gebietsart getGebietsartBriefwahlbezirk(GebietsbaumExchangeContext context) {
    return gebietsartDAO.findByName(context.getMandant(), stimmbezirkGebietsarten.getGebietsartTemplateForBriefwahlbezirke(context.getMandant()).getGebietsartName()).get(0);
  }

  @Override
  protected void anlegenWennNichtVorhanden(Gebietsbaum gebietsbaum, StimmbezirkGebietsbaumExchangeContext context, DateiImportBerichtCustomData customDateiImportBericht, StringBuilder bericht) {

    Wahl wahl = wahlDAO.attach(context.getWahl());

    Map<GebietsartTK, Gebietsart> gebietsartenInDb = GebietsartTK.mappeGebietsartNachTK(gebietsartDAO.findByAll(wahl.getMandant() ));

    Map<String, Rolle> rollenInDb = gebietsbaumImporter.sucheRollenInDb(wahl.getMandant());

    Map<Long, Mandant> mandantenInDb = gebietsbaumImporter.sucheMandanten();

    Map<GebietTK, Gebiet> gebietTKInDbGebietMap = GebietTK.mappeGebietNachTK(getEntitiesInDatabase(context));

    Map<GebietTK, Gebiet> gebieteNachTkInDatei = getGebietTKGebietMap(gebietsbaum);

    List<Gebiet> persistierteGebiete = new ArrayList<>();
    for (Gebiet gebiet : gebietsbaum.getGebiete()) {
      if (gebietTKInDbGebietMap.containsKey(gebiet.getGebietTK())) {
        // ELECTKOM-196
        LOG.warn("Gebiet bereits vorhanden (skip): "+gebiet);
      } else {
        gebietsbaumImporter.setzePersistenteDaten(gebiet, wahl.getWahltermin(), wahl, rollenInDb, gebietsartenInDb, mandantenInDb, null/*irrelevent*/);
        findeUndSetzeUebergeordnetesGebiet(context, gebiet, gebieteNachTkInDatei);
        gebietsbaumImporter.saveGebietWithCreate(gebiet, context);
        postCreateGebiet(gebiet);
        persistierteGebiete.add(gebiet);

        if (!context.isNurStimmbezirke()) {
          gebietDAO.flush();
        }
      }
    }

    postCreateGebiete(persistierteGebiete, wahl);
  }

  protected void postCreateGebiete(List<Gebiet> persistierteGebiete, Wahl wahl) {
    // nichts zu tun
  }

  protected void postCreateGebiet(Gebiet gebiet) {
    // nichts zu tun
  }

  private void findeUndSetzeUebergeordnetesGebiet(StimmbezirkGebietsbaumExchangeContext context, Gebiet gebiet, Map<GebietTK, Gebiet> gebieteNachTkInDatei) {
    Gebiet uebergeordnetesGebiet = gebiet.getUebergeordnetesGebiet();
    Gebiet atttachedUebergeordnetesGebiet = findeUebergeordnetesGebietInHierarchie(uebergeordnetesGebiet,gebieteNachTkInDatei , context);
    gebiet.setUebergeordnetesGebiet(atttachedUebergeordnetesGebiet);
  }

  private Gebietsart attachGebietsart(Mandant mandant, Gebietsart gebietsart) {
    return gebietsartDAO.findByName(mandant, gebietsart.getName()).get(0);
  }

  @Override
  protected void aktualisiereBestehendenDaten(Gebietsbaum gebietsbaum, StimmbezirkGebietsbaumExchangeContext context, DateiImportBerichtCustomData customDateiImportBericht, StringBuilder bericht) {
    // nothing to do
  }

  @Override
  protected void loescheBestehendenDatenWennNichtInImportDatenVorhanden(Gebietsbaum gebietsbaum, StimmbezirkGebietsbaumExchangeContext context, DateiImportBerichtCustomData customDateiImportBericht, StringBuilder bericht) {
    for (Gebiet gebietAusDb : bestimmeZuLoeschendeGebiete(gebietsbaum, context)) {
      gebietEdit.delete(gebietAusDb);
//      gebietAusDb.setUebergeordnetesGebiet(null); // aus Baum komplett entfernen
//      gebietDAO.remove(gebietAusDb);
    }
    gebietDAO.flush();
  }

  public List<Gebiet> bestimmeZuLoeschendeGebiete(Gebietsbaum gebietsbaum, StimmbezirkGebietsbaumExchangeContext context) {
    if (context.isNurStimmbezirke()) {

      // Gelöscht werden genau die Stimmbezirke, die bereits vorhanden sind und deren übergeordnetes Gebiet ein Gebiet in den zu importierenden Daten ist

      Set<GebietTK> uebergeordneteGebieteInZuImportierendenDaten = bestimmeUebergeordneteGebiete(gebietsbaum);

      return getEntitiesInDatabase(context).stream()
        .filter(gebiet -> isUebergeordnetesGebietVorhanden(gebiet.getUebergeordnetesGebiet(), uebergeordneteGebieteInZuImportierendenDaten))
        .collect(Collectors.toList());
    }
    return Collections.emptyList();
  }

  private boolean isUebergeordnetesGebietVorhanden(Gebiet uebergeordnetesGebiet, Set<GebietTK> uebergeordneteGebieteInZuImportierendenDaten) {
    if (uebergeordnetesGebiet == null) {
      return false;
    }

    if (uebergeordneteGebieteInZuImportierendenDaten.contains(uebergeordnetesGebiet.getGebietTK())) {
      return true;
    }

    return isUebergeordnetesGebietVorhanden(uebergeordnetesGebiet.getUebergeordnetesGebiet(), uebergeordneteGebieteInZuImportierendenDaten);
  }

  private Set<GebietTK> bestimmeUebergeordneteGebiete(Gebietsbaum gebietsbaum) {
    return gebietsbaum.getGebiete().stream().map(Gebiet::getUebergeordnetesGebiet).filter(Objects::nonNull).map(Gebiet::getGebietTK).collect(Collectors.toSet());
  }

  @Override
  protected ImportExchange<Gebietsbaum, StimmbezirkGebietsbaumExchangeContext> getExchange() {
    return gebietsbaumStimmbezirkImportExchange;
  }

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

  @Override
  protected void postImportActions(Gebietsbaum gebietsbaum, StimmbezirkGebietsbaumExchangeContext gebietsbaumExchangeContext, ImportOptions importOptions,
                                   DateiImportBerichtCustomData customDateiImportBericht, StringBuilder bericht) {
    super.postImportActions(gebietsbaum, gebietsbaumExchangeContext, importOptions, customDateiImportBericht, bericht);
    // ELECT-1875 Vervollständige Gebietsbaum
    gruppeImGebietDAO.flush(); // erstmal alles speichern, damit die Finder die gerade angelegten Daten in der DB finden
    List<GruppeImGebiet> gruppenImGebietInDB = gruppeImGebietDAO.findByWahl(wahlDAO.attach(gebietsbaumExchangeContext.getWahl()));

    Map<GruppeImGebietTK, GruppeImGebiet> mappedInDB = GruppeImGebietTK.mappeGruppeImGebietNachTK(gruppenImGebietInDB);

    Set<GruppeImGebiet> bereitsBetrachteteGruppeImGebiet = new HashSet<>();

    for (GruppeImGebiet gruppeImGebiet : gruppenImGebietInDB) {
      // unnötige doppelte Aufrufe vermeiden
      if (!bereitsBetrachteteGruppeImGebiet.contains(gruppeImGebiet)) {
        bereitsBetrachteteGruppeImGebiet.add(gruppeImGebiet);
        List<GruppeImGebiet> result = gruppeImGebietHierarchyBuilder.saveAndCompleteHierarchy(gruppeImGebiet, mappedInDB);
        bereitsBetrachteteGruppeImGebiet.addAll(result);
        mappedInDB.putAll(GruppeImGebietTK.mappeGruppeImGebietNachTK(gruppenImGebietInDB));
      }
    }
  }
}
