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

import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Function;
import java.util.stream.Collectors;

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

import de.ivu.elect.business.datei.entity.DateiImport;
import de.ivu.elect.business.datei.entity.DateiImportBerichtCustomData;
import de.ivu.elect.business.exchange.control.AbstractEntityImporter;
import de.ivu.elect.business.exchange.control.ImportExchange;
import de.ivu.elect.business.gebietsbaum.control.GebietGeometrieImportExchange;
import de.ivu.elect.business.gebietsbaum.entity.Gebiet;
import de.ivu.elect.business.gebietsbaum.entity.GebietGeometrieImportContext;
import de.ivu.elect.business.gebietsbaum.entity.GebietGeometrieNummerMapping;
import de.ivu.elect.business.gebietsbaum.entity.GebietPolygonPoint;
import de.ivu.elect.business.gebietsbaum.entity.GebietPolygonPointMeta;
import de.ivu.elect.business.gebietsbaum.entity.GebietPolygonPointTK;
import de.ivu.elect.business.gebietsbaum.entity.GebietTK;
import de.ivu.elect.business.gebietsbaum.entity.GebietsartTK;
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.jee.presentation.dynamicform.FormBuilder;
import de.ivu.elect.jee.presentation.dynamicform.LambdaEnumProperty;

@Stateless
public class GebietGeometrieImporter
    extends
    AbstractEntityImporter<GebietPolygonPoint, GebietPolygonPointTK, GebietGeometrieImportContext, DateiImportBerichtCustomData> {

  private static final int MAX_NICHT_GEFUDNEN_SIZE = 20; // willkürlich

  private static final int MAX_NICHT_GEFUDNEN_PRO_GA_SIZE = 5; // willkürlich

  public static final String GEBIET_GEOMETRIE_IMPORTER_GEBIET_GEOMETRIE_NUMMER_MAPPING = "GebietGeometrieImporter.GebietGeometrieNummerMapping";

  @Inject
  private transient GebietPolygonPointMeta meta;

  @Inject
  private transient GebietGeometrieImportExchange gebietGeometrieImportExchange;

  @Inject
  private transient GebietPolygonPointDAO gebietPolygonPointDAO;

  @Inject
  private transient GebietDAO gebietDAO;

  public GebietGeometrieImporter() {
    super();
  }

  @Override
  protected void validiere(Violations violations, List<GebietPolygonPoint> importObject, GebietGeometrieImportContext context, ImportOptions importOptions) {
    for (GebietPolygonPoint gebietPolygonPoint : importObject) {
      validateEntity(violations, gebietPolygonPoint, gebietPolygonPoint.getTk(), meta);
    }

    Set<GebietTK> gefunden = new HashSet<>();
    Set<GebietTK> nichtInDb = new HashSet<>();
    Map<GebietTK, Gebiet> gebieteInDbNachTk = getGebieteInDbNachTk(context);

    Map<String, AtomicLong> anzahlGebieteNachGebietsartGefunden = new HashMap<>();
    Map<String, AtomicLong> anzahlGebieteNachGebietsartNichtInDbGefunden = new HashMap<>();
    Map<String, AtomicLong> anzahlGebieteNachGebietsartNichtInDateiGefunden = new HashMap<>();

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

    for (GebietPolygonPoint gebietPolygonPoint : importObject) {
      GebietTK tk = gebietPolygonPoint.getGebiet().getGebietTK();
      // Wenn noch nicht vorhanden
      if (betrachteteTks.add(tk)) {
        if (!gebieteInDbNachTk.containsKey(tk)) {
          anzahlGebieteNachGebietsartNichtInDbGefunden.computeIfAbsent(tk.getGebietsartTK().getName(), x -> new AtomicLong(0L)).addAndGet(1);
          nichtInDb.add(tk);
        } else {
          anzahlGebieteNachGebietsartGefunden.computeIfAbsent(tk.getGebietsartTK().getName(), x -> new AtomicLong(0L)).addAndGet(1);
          gefunden.add(tk);
        }
      }
    }

    for (GebietTK gebietTK : gebieteInDbNachTk.keySet()) {
      if (!gefunden.contains(gebietTK)) {
        anzahlGebieteNachGebietsartNichtInDateiGefunden.computeIfAbsent(gebietTK.getGebietsartTK().getName(), x -> new AtomicLong(0L)).addAndGet(1L);
      }
    }

    anzahlGebieteNachGebietsartGefunden.entrySet().stream().sorted(Map.Entry.comparingByKey()).forEach(e -> {
      violations.addInfo("gebiet.geometrie.import.gebiete.mit.art.gefunden", e.getKey(), e.getValue().get());
    });

    anzahlGebieteNachGebietsartNichtInDbGefunden.entrySet().stream().sorted(Map.Entry.comparingByKey()).forEach(e -> {
      violations.addInfo("gebiet.geometrie.import.gebiete.mit.art.nicht.in.db.gefunden", e.getKey(), e.getValue().get());
    });

    anzahlGebieteNachGebietsartNichtInDateiGefunden.entrySet().stream().sorted(Map.Entry.comparingByKey()).forEach(e -> {
      violations.addInfo("gebiet.geometrie.import.gebiete.mit.art.nicht.in.datei.gefunden", e.getKey(), e.getValue().get());
    });

    for (GebietTK gebietTK : nichtInDb) {
      violations.addInfo("gebiet.geometrie.import.gebiet.nicht.in.db", gebietTK.toStringLokalisiert(getMessages()));
    }

    Set<GebietTK> nichtGefunden = new HashSet<>();
    for (GebietTK gebietTK : gebieteInDbNachTk.keySet()) {
      if (!gefunden.contains(gebietTK)) {
        nichtGefunden.add(gebietTK);
        // violations.addInfo("gebiet.geometrie.import.gebiet.nicht.in.datei", gebietTK.toStringLokalisiert(getMessages()));
      }
    }

    if (nichtGefunden.size() <= MAX_NICHT_GEFUDNEN_SIZE) {
      for (GebietTK gebietTK : nichtGefunden) {
        violations.addInfo("gebiet.geometrie.import.gebiet.nicht.in.datei", gebietTK.toStringLokalisiert(getMessages()));
      }
    } else {
      Map<GebietsartTK, Set<GebietTK>> nachGa = new HashMap<>();
      for (GebietTK gebietTK : nichtGefunden) {
        nachGa.computeIfAbsent(gebietTK.getGebietsartTK(), x -> new HashSet<>()).add(gebietTK);
      }
      for (var entry : nachGa.entrySet()) {
        GebietsartTK key = entry.getKey();
        Set<GebietTK> gebiete = entry.getValue();
        if (gebiete.size() <= MAX_NICHT_GEFUDNEN_PRO_GA_SIZE) {
          for (GebietTK gebietTK : gebiete) {
            violations.addInfo("gebiet.geometrie.import.gebiet.nicht.in.datei", gebietTK.toStringLokalisiert(getMessages()));
          }
        } else {
          String nummern = gebiete.stream().map(GebietTK::getNummer).collect(Collectors.joining(", "));

          violations.addInfo("gebiet.geometrie.import.gebiete.mit.gebietsart.nicht.in.datei", key.toStringLokalisiert(getMessages()), gebiete.size(), nummern);
        }
      }
    }
  }

  @Override
  public void addProperties(FormBuilder formBuilder, DateiImport dateiImport, GebietGeometrieImportContext gebietGeometrieImportContext) {
    super.addProperties(formBuilder, dateiImport, gebietGeometrieImportContext);

    formBuilder.addProperty(new LambdaEnumProperty<>(
        getMessage("gebietgeometrie.gebiet.nummer.mapping"),
        true,
        m -> gebietGeometrieImportContext.put(GEBIET_GEOMETRIE_IMPORTER_GEBIET_GEOMETRIE_NUMMER_MAPPING, m),
        () -> getGeometrieNummerMapping(gebietGeometrieImportContext),
        GebietGeometrieNummerMapping.class,
        Arrays.asList(GebietGeometrieNummerMapping.values()))
    );
  }

  protected GebietGeometrieNummerMapping getGeometrieNummerMapping(GebietGeometrieImportContext gebietGeometrieImportContext) {
    return gebietGeometrieImportContext.getOrDefault(
        GEBIET_GEOMETRIE_IMPORTER_GEBIET_GEOMETRIE_NUMMER_MAPPING, GebietGeometrieNummerMapping.class, GebietGeometrieNummerMapping.OHNE);
  }

  private List<Gebiet> getGebieteInDb(GebietGeometrieImportContext context) {
    if (context.getWahl() != null) {
      return gebietDAO.findByWahl(context.getWahl());
    }

    return gebietDAO.findByWahltermin(context.getWahltermin());

  }

  @Override
  protected void erstelleBericht(List<GebietPolygonPoint> importObject, GebietGeometrieImportContext context, ImportOptions importOptions,
                                 StringBuilder bericht, boolean vorab) {
  }

  @Override
  protected void persistiere(Collection<GebietPolygonPoint> neueEntities, GebietGeometrieImportContext context,
                             DateiImportBerichtCustomData customDateiImportBerichtCustomData, StringBuilder bericht) {

    Map<GebietTK, Gebiet> gebieteInDbNachTk = getGebieteInDbNachTk(context);

    for (GebietPolygonPoint neueEntity : neueEntities) {
      neueEntity.setGebiet(gebieteInDbNachTk.get(neueEntity.getGebiet().getGebietTK()));
      if (neueEntity.getGebiet() != null) {
        gebietPolygonPointDAO.persist(neueEntity);
      }
    }
  }

  protected Map<GebietTK, Gebiet> getGebieteInDbNachTk(GebietGeometrieImportContext context) {
    var nummerMapping = getGeometrieNummerMapping(context);
    return getGebieteInDb(context).stream().collect(Collectors.toMap(nummerMapping::toTk, gebiet -> gebiet, (a, b) -> b));
  }

  @Override
  protected void aktualisiere(GebietPolygonPoint entityInDatei, GebietPolygonPoint entityInDatenbank, GebietGeometrieImportContext context,
                              DateiImportBerichtCustomData customDateiImportBerichtCustomData, StringBuilder bericht) {
    entityInDatenbank.setLatitude(entityInDatei.getLatitude());
    entityInDatenbank.setLongitude(entityInDatei.getLongitude());
    entityInDatenbank.setGebietPolygonPointArt(entityInDatei.getGebietPolygonPointArt());
    entityInDatenbank.setPosition(entityInDatei.getPosition());
  }

  @Override
  protected void loescheAusDatenbank(Collection<GebietPolygonPoint> entitiesInDb, GebietGeometrieImportContext context,
                                     DateiImportBerichtCustomData customDateiImportBerichtCustomData, StringBuilder bericht) {
    for (GebietPolygonPoint gebietPolygonPoint : entitiesInDb) {
      gebietPolygonPointDAO.remove(gebietPolygonPoint);
    }
  }

  @Override
  protected List<GebietPolygonPoint> getAllEntitiesInDB(GebietGeometrieImportContext context) {
    if (context.getWahl() != null) {
      return gebietPolygonPointDAO.findByWahl(context.getWahl());
    }

    return gebietPolygonPointDAO.findByWahltermin(context.getWahltermin());
  }

  @Override
  protected Function<GebietPolygonPoint, GebietPolygonPointTK> getEntityToEntityTKFunction() {
    return GebietPolygonPointTK.getGebietPolygonPointToTKFunction();
  }

  @Override
  protected ImportExchange<List<GebietPolygonPoint>, GebietGeometrieImportContext> getExchange() {
    return gebietGeometrieImportExchange;
  }

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

  @Override
  protected boolean isUnterstuetztAnlegenWennNichtVorhanden() {
    return true;
  }

  @Override
  protected boolean isUnterstuetztAktualisiereBestehendenDaten() {
    return true;
  }

  @Override
  protected boolean isUnterstuetztLoescheBestehendenDatenWennNichtInImportDatenVorhanden() {
    return true;
  }

}
