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

import de.ivu.elect.business.benutzerverwaltung.boundary.RolleDAO;
import de.ivu.elect.business.benutzerverwaltung.control.RolleCreate;
import de.ivu.elect.business.benutzerverwaltung.entity.Rolle;
import de.ivu.elect.business.datei.entity.DateiImport;
import de.ivu.elect.business.datei.entity.DateiImportBerichtCustomData;
import de.ivu.elect.business.exchange.control.AbstractImporter;
import de.ivu.elect.business.exchange.control.FileFormat;
import de.ivu.elect.business.exchange.control.MappingException;
import de.ivu.elect.business.exchange.entity.ImportUpdateReport;
import de.ivu.elect.business.gebietsbaum.control.*;
import de.ivu.elect.business.gebietsbaum.entity.*;
import de.ivu.elect.business.mandant.boundary.MandantDAO;
import de.ivu.elect.business.mandant.entity.Mandant;
import de.ivu.elect.business.wahl.boundary.WahlterminDAO;
import de.ivu.elect.business.wahl.control.WahlAttacher;
import de.ivu.elect.business.wahl.entity.Wahl;
import de.ivu.elect.business.wahl.entity.Wahltermin;
import de.ivu.elect.jee.entity.Meta;
import de.ivu.elect.jee.exception.ElectCommonException;
import de.ivu.elect.jee.exchange.ImportOptions;
import de.ivu.elect.jee.exchange.Violations;

import javax.annotation.Nullable;
import javax.ejb.Stateless;
import javax.enterprise.inject.Instance;
import javax.inject.Inject;
import java.util.*;
import java.util.stream.Collectors;

import static java.util.Arrays.asList;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonList;
import static org.apache.commons.lang3.StringUtils.isBlank;

@Stateless
public class GebietsbaumImporter extends AbstractImporter<Gebietsbaum, GebietsbaumExchangeContext, DateiImportBerichtCustomData> {

  @Inject
  private transient WahlAttacher wahlDAO;

  @Inject
  private transient WahlterminDAO wahlterminDAO;

  @Inject
  private transient GebietDAO gebietDAO;

  @Inject
  private transient GebietMeta gebietMeta;

  @Inject
  private transient GebietCreate gebietCreate;

  @Inject
  private transient GebietsartCreate gebietsartCreate;

  @Inject
  private transient GebietsartDAO gebietsartDAO;

  @Inject
  private transient RolleDAO rolleDAO;

  @Inject
  private transient RolleCreate rolleCreate;

  @Inject
  private transient GebietsbaumImportExchange gebietsbaumImportExchange;

  @Inject
  private transient Instance<GebietsbaumImportAktualisierer> gebietsbaumImportAktualisiererList;

  @Inject
  private transient GebietsbaumImporterReportUtil gebietsbaumImporterReportUtil;

  @Inject
  private transient GebietsgruppierungDAO gebietsgruppierungDAO;

  @Inject
  private transient GebietsbaumImportFilter gebietsbaumImportFilter;

  @Inject
  private transient MandantDAO mandantDAO;

  public GebietsbaumImporter() {
    super();
  }

  public GebietImportAktualisierenArt getSupportedGebietImportAktualisierenArt(FileFormat fileFormat) {
    return getExchange().getSupportedGebietImportAktualisierenArt(fileFormat);
  }

  @Override
  public Collection<ImportOptions> getSupportedImportOptions(GebietsbaumExchangeContext context) {
    if (isGebietsbaumImportAktualisierenSelected(context)) {
      return singletonList(ImportOptions.NUR_BESTEHENDE_AKTUALISIEREN);
    }

    return asList(ImportOptions.IMPORT_VOLLSTAENDIG_UEBERNEHMEN, ImportOptions.NUR_BESTEHENDE_AKTUALISIEREN);
  }

  @Override
  protected GebietsbaumImportExchange getExchange() {
    return gebietsbaumImportExchange;
  }

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

  @Override
  public void validiere(Violations violations, Gebietsbaum importObject, GebietsbaumExchangeContext context, ImportOptions importOptions) {
    if (isGebietsbaumImportAktualisierenSelected(context)) {
      GebietsbaumImportAktualisierer gebietsbaumImportAktualisierer = findGebietsbaumImportAktualisierer(context);

      gebietsbaumImportAktualisierer.validiere(violations, context, importObject.getGebiete());

      if (!isWeitereValidierungErforderlich(gebietsbaumImportAktualisierer)) {
        return;
      }
    }

    validiereGebiete(violations, importObject, context);
    validiereGebietsgruppierungen(violations, importObject, context);
  }

  @Override
  protected Gebietsbaum readImportObject(DateiImport dateiImport, GebietsbaumExchangeContext gebietsbaumExchangeContext) throws MappingException {
    Gebietsbaum gebietsbaum = super.readImportObject(dateiImport, gebietsbaumExchangeContext);
    return gebietsbaumImportFilter.filter(gebietsbaum);
  }

  private boolean isWeitereValidierungErforderlich(GebietsbaumImportAktualisierer gebietsbaumImportAktualisierer) {
    return gebietsbaumImportAktualisierer.isWeitereValidierungErforderlich();
  }

  private void validiereGebiete(Violations violations, Gebietsbaum importObject, GebietsbaumExchangeContext context) {
    int anzahlGebieteOhneUebergeordnetesGebiet = 0;

    Set<String> gebietsartNamen;

    if (context.hasWahl()) {
      gebietsartNamen = gebietsartDAO.findByWahlart(context.getWahl().getWahlart()).stream().map(Gebietsart::getName).collect(Collectors.toSet());
    } else {
      gebietsartNamen = gebietsartDAO.findByAll(context.getMandant()).stream().map(Gebietsart::getName).collect(Collectors.toSet());
    }

    importObject.getGebietsarten().stream().map(Gebietsart::getName).forEach(gebietsartNamen::add);

    Set<GebietTK> gebietTks = new HashSet<>();

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

    for (Gebiet gebiet : importObject.getGebiete()) {
      Gebietsart gebietsart = gebiet.getGebietsart();
      GebietTK gebietTk = gebiet.getGebietTK();

      if (!gebietsartNamen.contains(gebietsart.getName())) {
        if (context.hasWahl()) {
          violations.addError("gebiet.import.unbekannte.gebietsart.fuer.wahlart", gebietsart.getName(), context.getWahl().getWahlart().getName());
        } else {
          violations.addError("gebiet.import.unbekannte.gebietsart", gebietsart.getName());
        }
      }

      if (gebietTks.contains(gebietTk)) {
        violations.addError("gebiet.import.gebiet.mehrfach", gebietTk.getNummer(), gebietTk.getGebietsartTK().getName());
      } else {
        gebietTks.add(gebietTk);
      }

      if (!gebiet.hasUebergeordnetesGebiet()) {
        anzahlGebieteOhneUebergeordnetesGebiet++;
      }

      validateEntity(violations, gebiet, gebietTk);
      validateEntity(violations, gebietsart, gebietTk);

      validateGebietMandant(violations, mandantenInDb, gebiet);
    }

    if (anzahlGebieteOhneUebergeordnetesGebiet > 1) {
      violations.addError("gebiet.import.mehrere.gebiete.ohne.uebergeordnetes.gebiet", anzahlGebieteOhneUebergeordnetesGebiet);
    }
  }

  public void validateGebietMandant(Violations violations, Map<Long, Mandant> mandantenInDb, Gebiet gebiet) {
    if (gebiet.getVerantwortlicherMandant() != null && gebiet.getVerantwortlicherMandant().getNummer() != null) {
      if (!mandantenInDb.containsKey(gebiet.getVerantwortlicherMandant().getNummer())) {
        violations.addWarning("gebiet.import.gebiet.mandant.nicht.vorhanden", gebiet.getVerantwortlicherMandant().getNummer());
      }
    }
  }

  private void validiereGebietsgruppierungen(Violations violations, Gebietsbaum importObject, GebietsbaumExchangeContext context) {
    // TODO validieren:
    // - Namen eindeutig pro Art
    // - Positionen eindeutig pro Art
    // Code existiert schon beim GebietsgruppierungImporter bzw. DefaultGebietsgruppierungCsvImportMapper
  }

  @Override
  public void importiereImportObject(Gebietsbaum importObject, GebietsbaumExchangeContext context, ImportOptions importOptions, DateiImportBerichtCustomData dateiImportBerichtCustomData,
      StringBuilder bericht) {
    if (importObject.getGebiete().isEmpty()) {
      return;
    }

    createGebietsartenIfNeeded(importObject.getGebietsarten(), context);

    if (isAktualisiereBestehendenDaten(importOptions)) {
      aktualisiereBestehendenDaten(importObject, context, dateiImportBerichtCustomData, bericht);
    } else if (!isAktualisiereBestehendenDaten(importOptions) && isGebietsbaumImportAktualisierenSelected(context)) {
      throw new ElectCommonException("gebietsbaum.importer.nur.aktualisieren.erlaubt");
    } else {
      importVollstaendigUebernehmen(importObject, context);
    }
  }

  private void importVollstaendigUebernehmen(Gebietsbaum importObject, GebietsbaumExchangeContext context) {
    Wahl wahl = wahlDAO.attach(context.getWahl());
    Wahltermin wahltermin;

    if (context.getWahltermin() == null && wahl != null) {
      wahltermin = wahl.getWahltermin();
    } else {
      wahltermin = wahlterminDAO.attach(context.getWahltermin());
    }

    preGebietsbaumImport(context, wahl, wahltermin);

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

    Gebiet wurzelgebietInDatei = importObject.getGebiete().iterator().next().getWurzelgebiet();

    aktualisiereWahl(importObject, context, wahl);

    // ELECT-6794
    Map<Gebiet, Set<Gebietsgruppierung>> gebietsgruppierungen = extrahiereUndLoescheGebietsgruppierungen(wurzelgebietInDatei);
    gebietsgruppierungen.values().stream().flatMap(Collection::stream).forEach(Gebietsgruppierung::removeAllGebiets);

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

    persistiereGebiet(wurzelgebietInDatei, wahltermin, wahl, gebietsartenInDb, null, sucheRollenInDb(context.getMandant()), mandantenInDb, context);

    gebietDAO.flush(); // ELECT-6691 WildFly 11 hack und ELECT-6794

    // ELECT-6794 Gebietsgruppierungen wieder zurückschreiben und persistieren
    Set<Gebietsgruppierung> gebietsgruppierungenZuPersistieren = new HashSet<>();
    gebietsgruppierungen.values().stream().flatMap(Collection::stream).forEach(g -> g.setWahl(wahl));
    gebietsgruppierungen.values().stream().flatMap(Collection::stream).forEach(g -> g.setWahltermin(wahltermin));

    for (Map.Entry<Gebiet, Set<Gebietsgruppierung>> entry : gebietsgruppierungen.entrySet()) {
      Gebiet gebiet = entry.getKey();

      for (Gebietsgruppierung gebietsgruppierung : entry.getValue()) {
        gebietsgruppierungenZuPersistieren.add(gebietsgruppierung);
        gebietsgruppierung.addGebiet(gebiet);
      }
    }

    for (Gebietsgruppierung gebietsgruppierung : gebietsgruppierungenZuPersistieren) {
      gebietsgruppierungDAO.persist(gebietsgruppierung);
    }

    postGebietsbaumImport(importObject, wahl, wahltermin);
  }

  public Map<Long, Mandant> sucheMandanten() {
    return mandantDAO.findByAll().stream().filter(m -> m.getArt().isCustomerMandant()).collect(Collectors.toMap(Mandant::getNummer, mandant -> mandant, (a, b) -> b));
  }

  private void createGebietsartenIfNeeded(List<Gebietsart> gebietsartList, GebietsbaumExchangeContext context) {
    gebietsartList.forEach(gebietsart -> createGebietsartIfNeeded(gebietsart, context));

    // HINT: Flush, damit nachfolgende Statements die Gebietsart in der Datenbank finden
    gebietsartDAO.flush();
  }

  private void createGebietsartIfNeeded(Gebietsart gebietsart, GebietsbaumExchangeContext context) {
    gebietsart.setMandant(context.getMandant());

    if (!gebietsartDAO.isEntityWithUKPresent(gebietsart.getGebietsartUK())) {
      gebietsartCreate.save(gebietsart);
    }
  }

  private Map<Gebiet, Set<Gebietsgruppierung>> extrahiereUndLoescheGebietsgruppierungen(Gebiet gebiet) {
    if (gebiet == null) {
      return emptyMap();
    }

    Map<Gebiet, Set<Gebietsgruppierung>> result = new HashMap<>();
    result.put(gebiet, /* Kopie anlegen */ new HashSet<>(gebiet.getGebietsgruppierungs()));

    gebiet.removeAllGebietsgruppierungs(); // löschen, damit ELECT-6794 nicht auftritt

    for (Gebiet untergebiet : gebiet.getUntergeordneteGebiete()) {
      result.putAll(extrahiereUndLoescheGebietsgruppierungen(untergebiet));
    }

    return result;
  }

  private boolean isAktualisiereBestehendenDaten(ImportOptions importOptions) {
    return ImportOptions.NUR_BESTEHENDE_AKTUALISIEREN.equals(importOptions);
  }

  protected void postGebietsbaumImport(Gebietsbaum importObject, Wahl wahl, Wahltermin wahltermin) {
    // nichts zu tun
  }

  protected void preGebietsbaumImport(GebietsbaumExchangeContext context, Wahl wahl, Wahltermin wahltermin) {
    if (context.hasWahl()) {
      gebietsgruppierungDAO.deleteWahlDelete(wahl);
      gebietDAO.deleteWahlDelete(wahl);
    } else {
      gebietsgruppierungDAO.deleteWahlterminDelete(wahltermin);
      gebietDAO.deleteWahlterminDelete(wahltermin);
    }
  }

  public Map<String, Rolle> sucheRollenInDb(Mandant mandant) {
    Map<String, Rolle> result = new HashMap<>();

    for (Rolle rolle : rolleDAO.findByAll(mandant)) {
      result.put(rolle.getName().trim(), rolle);
    }

    return result;
  }

  protected void aktualisiereWahl(Gebietsbaum importObject, GebietsbaumExchangeContext context, Wahl wahl) {
    // nichts zu tun
  }

  @Override
  public void erstelleBericht(Gebietsbaum importObject, GebietsbaumExchangeContext context, ImportOptions importOptions, StringBuilder bericht, boolean vorab) {
    if (isGebietsbaumImportAktualisierenSelected(context)) {
      gebietsbaumImporterReportUtil.erstelleBericht(context.getMandant(), importObject.getGebiete(), bericht);
    } else {
      gebietsbaumImporterReportUtil.erstelleBericht(context.getMandant(), importObject, bericht);
    }
  }

  @Override
  protected ImportUpdateReport createImportUpdateReport(Gebietsbaum importObject, Violations violations, GebietsbaumExchangeContext context, ImportOptions importOptions, StringBuilder bericht) {
    return gebietsbaumImporterReportUtil.createImportUpdateReport(findGebieteInDb(context), importObject, violations, importOptions, bericht);
  }

  /*
   * Methode wird rekursiv aufgerufen, daher alle Entitäten aus der DB als Parameter durchschleifen
   */
  protected void persistiereGebiet(Gebiet gebietInDatei, Wahltermin wahltermin, Wahl wahl, Map<GebietsartTK, Gebietsart> gebietsartenInDb, @Nullable Gebiet uebergeordnetesGebietInDB,
      Map<String, Rolle> rollenInDb, Map<Long, Mandant> mandantenInDb, GebietsbaumExchangeContext context) {
    Set<Gebiet> gebiets = new HashSet<>(gebietInDatei.getGebiets());
    gebietInDatei.removeAllGebiets(); // Wildfly 11 Fix

    setzePersistenteDaten(gebietInDatei, wahltermin, wahl, rollenInDb, gebietsartenInDb, mandantenInDb, uebergeordnetesGebietInDB);

    aktualisiereAnzahlSitze(gebietInDatei, wahl);

    saveGebietWithCreate(gebietInDatei, context);

    gebietInDatei.setUebergeordnetesGebiet(uebergeordnetesGebietInDB); // ELECT-6691 Wildfly 11 Fix, erst nach dem Persistieren obergebiet setzen

    if (!gebiets.isEmpty()) {
      gebietDAO.flush(); // ELECT-6691 WildFly 11 hack...keine Ahnung, ob noch notwendig...
    }

    for (Gebiet untergeordnetesGebietInDatei : gebiets) {
      persistiereGebiet(untergeordnetesGebietInDatei, wahltermin, wahl, gebietsartenInDb, gebietInDatei, rollenInDb, mandantenInDb,
          context);
    }
  }

  public void setzePersistenteDaten(Gebiet gebietInDatei, Wahltermin wahltermin, Wahl wahl, Map<String, Rolle> rollenInDb, Map<GebietsartTK, Gebietsart> gebietsartenInDb,
                                    Map<Long, Mandant> mandantenInDb, Gebiet uebergeordnetesGebietInDB) {
    gebietInDatei.setWahltermin(wahltermin);
    gebietInDatei.setWahl(wahl);

    tauscheRollen(wahltermin.getMandant(), gebietInDatei, rollenInDb);
    aktualisiereOderErstelleGebietsinformation(gebietInDatei, wahltermin, gebietsartenInDb);

    tauscheMandanten(gebietInDatei, mandantenInDb, uebergeordnetesGebietInDB);
  }

  public void tauscheMandanten(Gebiet gebietInDatei, Map<Long, Mandant> mandantenInDb, Gebiet uebergeordnetesGebietInDB) {
    if (gebietInDatei.getVerantwortlicherMandant() != null && gebietInDatei.getVerantwortlicherMandant().getNummer() != null) {
      gebietInDatei.setVerantwortlicherMandant(mandantenInDb.get(gebietInDatei.getVerantwortlicherMandant().getNummer()));
    }
  }

  public void tauscheRollen(Mandant mandant, Gebiet gebietInDatei, Map<String, Rolle> rollenInDb) {
    gebietInDatei.setBearbeitenRolle(tauscheRolleOderLegeAn(mandant, gebietInDatei.getBearbeitenRolle(), rollenInDb));
    gebietInDatei.setEinsehenRolle(tauscheRolleOderLegeAn(mandant, gebietInDatei.getEinsehenRolle(), rollenInDb));
  }

  @Nullable
  private Rolle tauscheRolleOderLegeAn(Mandant mandant, @Nullable Rolle rolleInDatei, Map<String, Rolle> rollenInDb) {
    if (rolleInDatei == null || isBlank(rolleInDatei.getName())) {
      return null;
    }

    String nameDerRolle = rolleInDatei.getName().trim();

    if (!rollenInDb.containsKey(nameDerRolle)) {
      Rolle rolle = new Rolle();
      rolle.setMandant(mandant);
      rolle.setName(nameDerRolle);
      rolleCreate.save(rolle);
      rollenInDb.put(nameDerRolle, rolle);
    }

    return rollenInDb.get(nameDerRolle);
  }

  public void saveGebietWithCreate(Gebiet gebietInDatei, GebietsbaumExchangeContext context) {
    gebietCreate.save(gebietInDatei);
  }

  public void aktualisiereOderErstelleGebietsinformation(Gebiet gebietInDatei, Wahltermin wahltermin, Map<GebietsartTK, Gebietsart> gebietsartenInDb) {
    gebietInDatei.setGebietsart(gebietsartenInDb.get(gebietInDatei.getGebietsart().getGebietsartTK()));
  }

  protected void aktualisiereAnzahlSitze(Gebiet gebietInDatei, Wahl wahl) {
    // ELECT-111
    if (wahl != null && gebietInDatei.isWahlgebiet() && gebietInDatei.getAnzahlSitze() == null) {
      gebietInDatei.setAnzahlSitze(0L);
    }
  }

  /*
   * der normale Lifecycle des AbstractImporter wird nicht unterstützt, da Gebiete nicht teilweise gelöscht und ergänzt werden können (Baumstruktur)
   */

  @Override
  protected void anlegenWennNichtVorhanden(Gebietsbaum importObject, GebietsbaumExchangeContext context, DateiImportBerichtCustomData dateiImportBerichtCustomData, StringBuilder bericht) {
    throw new UnsupportedOperationException();
  }

  @Override
  protected void aktualisiereBestehendenDaten(Gebietsbaum importObject, GebietsbaumExchangeContext context, DateiImportBerichtCustomData dateiImportBerichtCustomData, StringBuilder bericht) {
    if (!isGebietsbaumImportAktualisierenSelected(context)) {
      throw new ElectCommonException("gebietsbaum.importer.aktualisieren.nicht.erlaubt");
    }

    findGebietsbaumImportAktualisierer(context).aktualisiereGebiete(context, findGebieteInDb(context), importObject.getGebiete());
  }

  @Override
  protected void loescheBestehendenDatenWennNichtInImportDatenVorhanden(Gebietsbaum importObject, GebietsbaumExchangeContext context, DateiImportBerichtCustomData dateiImportBerichtCustomData,
      StringBuilder bericht) {
    throw new UnsupportedOperationException();
  }

  private boolean isGebietsbaumImportAktualisierenSelected(GebietsbaumExchangeContext context) {
    return !GebietImportAktualisierenArt.NICHTS_AKTUALISIEREN.equals(context.getGebietImportAktualisierenArt());
  }

  private GebietsbaumImportAktualisierer findGebietsbaumImportAktualisierer(GebietsbaumExchangeContext context) {
    for (GebietsbaumImportAktualisierer gebietsbaumImportAktualisierer : gebietsbaumImportAktualisiererList) {
      if (gebietsbaumImportAktualisierer.getAktualisierungsart().equals(context.getGebietImportAktualisierenArt())) {
        return gebietsbaumImportAktualisierer;
      }
    }

    throw new IllegalStateException("Keinen passenden Gebietaktualisierer gefunden! Soll GebietImportAktualisierenArt: " + context.getGebietImportAktualisierenArt());
  }

  private List<Gebiet> findGebieteInDb(GebietsbaumExchangeContext context) {
    if (context.hasWahl()) {
      return gebietDAO.findByWahl(context.getWahl());
    }

    if (context.hasWahltermin()) {
      return gebietDAO.findByWahltermin(context.getWahltermin());
    }

    throw new IllegalStateException("Weder Wahl noch Wahltermin gesetzt.");
  }
}
