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

import static org.apache.commons.lang3.StringUtils.isNotBlank;

import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Set;

import javax.inject.Inject;
import javax.validation.ConstraintViolation;

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

import de.ivu.elect.business.datei.boundary.DateiImportDAO;
import de.ivu.elect.business.datei.entity.Datei;
import de.ivu.elect.business.datei.entity.DateiImport;
import de.ivu.elect.business.datei.entity.DateiImportBerichtCustomData;
import de.ivu.elect.business.exchange.entity.ImportEvent;
import de.ivu.elect.business.exchange.entity.ImportStatus;
import de.ivu.elect.business.exchange.entity.ImportUpdateReport;
import de.ivu.elect.jee.entity.Entity;
import de.ivu.elect.jee.entity.EntityConstraintViolation;
import de.ivu.elect.jee.entity.EntityValidation;
import de.ivu.elect.jee.entity.Meta;
import de.ivu.elect.jee.entity.tk.EntityTK;
import de.ivu.elect.jee.exception.ElectCommonException;
import de.ivu.elect.jee.exchange.AbstractViolation;
import de.ivu.elect.jee.exchange.ImportOptions;
import de.ivu.elect.jee.exchange.Violations;
import de.ivu.elect.jee.performance.PerformanceLog;
import de.ivu.elect.jee.performance.PerformanceLogTransactionHandling;
import de.ivu.elect.jee.performance.PerformanceNoLog;
import de.ivu.elect.jee.performance.ViewType;
import de.ivu.elect.jee.presentation.dynamicform.FormBuilder;

// Abgeleitete Klasse mit @Stateless annotieren!
@PerformanceLog(viewType = ViewType.IMPORT, transactionHandling = PerformanceLogTransactionHandling.AFTER_TRANSACTION)
public abstract class AbstractImporter<ImportObject, Context, CustomDateiImportBerichtCustomData extends DateiImportBerichtCustomData> extends AbstractImporterExporter<Context> {

  public static final String DATEI_IMPORT_BERICHT_LINE_SEPARATOR = DateiImportBerichtCustomData.DATEI_IMPORT_BERICHT_LINE_SEPARATOR;

  private static final Logger LOG = LoggerFactory.getLogger(AbstractImporter.class);

  private static final boolean IST_VORAB_ERZEUGT = true;

  private static final boolean IST_NICHT_VORAB_ERZEUGT = false;

  private static final String MSG_KEY_FEHLER_VORHANDEN = "importer.importieren.fehlgeschlagen.fehler.vorhanden";

  @Inject
  private transient DateiImportDAO dateiImportDAO;

  @Inject
  private transient ImportExportStreamCreator streamCreator;

  @Inject
  private transient EntityValidation entityValidation;

  @Inject
  private transient ImportEvent importEvent;

  public AbstractImporter() {
    super();
  }

  public void addProperties(FormBuilder formBuilder, DateiImport dateiImport, Context context) {
    Datei datei = attach(dateiImport).getDatei();
    getExchange().addProperties(formBuilder, createFileKonfiguration(datei).getFileFormat(), context, datei);
  }

  public void importieren(DateiImport dateiImportUnattached, Context context, ImportOptions importOptions) {
    DateiImport dateiImport = attach(dateiImportUnattached);

    if (!dateiImport.isImportierenMoeglich()) {
      throw new ElectCommonException("importer.importieren.nicht.moeglich", dateiImport.getImportStatus());
    }

    try {

      // ELECT-10070 Mapper Violations prüfen
      Violations violations = new Violations();
      validiere(violations, dateiImport, context);
      checkViolation(violations);

      preImportActions(dateiImport, context, importOptions);

      ImportObject importObject = readImportObject(dateiImport, context);
      CustomDateiImportBerichtCustomData bericht = (CustomDateiImportBerichtCustomData) dateiImport.getCustomData(DateiImportBerichtCustomData.class);

      importieren(importObject, importOptions, bericht, context);

      dateiImport.setImportStatus(ImportStatus.IMPORTIERT);
      dateiImport.setImportiertAm(new Date());

      postImportActions(dateiImport, context, importOptions, bericht);

    } catch (ElectCommonException e) {
      throw e;
    } catch (Exception ex) {
      warn("Import fehlgeschlagen [" + dateiImport.getPk() + "] wegen Exception [class=" + ex.getClass().getCanonicalName() + ",message=\"" + ex.getMessage() + "\"]", ex);

      throw new ElectCommonException(MSG_KEY_FEHLER_VORHANDEN, ex, ex.getMessage());
    }
  }

  public void importieren(ImportObject importObject, ImportOptions importOptions, CustomDateiImportBerichtCustomData berichtCustomData, Context context) {
    try {
      importiere(context, importOptions, berichtCustomData, importObject);
    } catch (ElectCommonException e) {
      throw e;
    } catch (Exception ex) {
      warn("Import wegen Exception fehlgeschlagen [class=" + ex.getClass().getCanonicalName() + ",message=\"" + ex.getMessage() + "\"]", ex);
      throw new ElectCommonException(MSG_KEY_FEHLER_VORHANDEN, ex, ex.getMessage());
    }
  }

  protected void importiere(Context context, ImportOptions importOptions, CustomDateiImportBerichtCustomData customDateiImportBericht, ImportObject importObject) {
    Violations violations = new Violations();

    ImportUpdateReport importUpdateReport = createImportUpdateReport(importObject, violations, context, importOptions);

    checkViolation(violations);

    customDateiImportBericht.setImportUpdateReport(importUpdateReport);

    StringBuilder bericht = importUpdateReport.getBericht();

    importiereImportObject(importObject, context, importOptions, customDateiImportBericht, bericht);

    postImportActions(importObject, context, importOptions, customDateiImportBericht, bericht);

    if (!violations.isEmpty()) { // ELECT-8894
      bericht.append(DATEI_IMPORT_BERICHT_LINE_SEPARATOR);
      bericht.append(DATEI_IMPORT_BERICHT_LINE_SEPARATOR);
      bericht.append(getMessage("datei.import.bestaetigen.import.report.legend.errors"));

      for (AbstractViolation violation : violations.getViolations()) {
        bericht.append(DATEI_IMPORT_BERICHT_LINE_SEPARATOR);
        bericht.append(getMessage(violation.getViolationLevel())).append(": ").append(violation.getMessage(getMessages()));
      }
    }

    customDateiImportBericht.setBericht(bericht.toString());
  }

  protected void checkViolation(Violations violations) {
    if (violations.containsViolationsWithLevelError()) {
      List<AbstractViolation> errorViolations = violations.getErrorViolations();

      for (AbstractViolation violation : errorViolations) {
        warn(violation.toString());
      }

      AbstractViolation erste = errorViolations.get(0);

      throw new ImportException(getMessages(), MSG_KEY_FEHLER_VORHANDEN, errorViolations, getMessage(erste.getMessageKey(), erste.getMessageParameters(getMessages())));
    }
  }

  protected void preImportActions(DateiImport dateiImport, Context context, ImportOptions importOptions) {
    // nichts zu tun
  }

  protected void postImportActions(DateiImport dateiImport, Context context, ImportOptions importOptions, CustomDateiImportBerichtCustomData customDateiImportBericht) {
    importEvent.sendEvent();
  }

  protected void postImportActions(ImportObject importObject, Context context, ImportOptions importOptions, CustomDateiImportBerichtCustomData customDateiImportBericht, StringBuilder bericht) {
    // nichts zu tun
  }

  @PerformanceNoLog
  public ImportUpdateReport getImportUpdateReport(DateiImport dateiImport, Context context, ImportOptions importOptions) {
    Violations violations = new Violations();

    try {

      validiere(violations, dateiImport, context);
      ImportObject importObject = readImportObject(dateiImport, context);
      return createImportUpdateReport(importObject, violations, context, importOptions);

    } catch (Exception ex) {
      violations.addError("importer.importieren.exception", describeThrowableWithCauses(ex));
      warn("Erstellen des Importberichts fehlgeschlagen [" + dateiImport.getPk() + "]", ex);

      return new ImportUpdateReport(violations, 0, 0, 0, importOptions, new StringBuilder());
    }
  }

  @PerformanceNoLog
  public ImportUpdateReport getImportUpdateReport(ImportObject importObject, Context context, ImportOptions importOptions) {
    Violations violations = new Violations();

    try {

      return createImportUpdateReport(importObject, violations, context, importOptions);

    } catch (Exception ex) {
      violations.addError("importer.importieren.exception", describeThrowableWithCauses(ex));
      warn("Erstellen des Importberichts fehlgeschlagen [" + importObject + "]", ex);

      return new ImportUpdateReport(violations, 0, 0, 0, importOptions, new StringBuilder());
    }
  }

  protected ImportUpdateReport createImportUpdateReport(ImportObject importObject, Violations violations, Context context, ImportOptions importOptions) {
    validiere(violations, importObject, context, importOptions);

    StringBuilder bericht = new StringBuilder();
    erstelleBericht(importObject, context, importOptions, bericht, IST_VORAB_ERZEUGT);

    return createImportUpdateReport(importObject, violations, context, importOptions, bericht);
  }

  private String describeThrowableWithCauses(Throwable e) {
    StringBuilder sb = new StringBuilder();

    if (e.getCause() != null) {
      sb.append(describeThrowableWithCauses(e.getCause()));
    }

    if (sb.length() != 0) {
      sb.append(" - ");
    }

    sb.append(describeThrowable(e));

    return sb.toString();
  }

  private String describeThrowable(Throwable e) {
    if (isNotBlank(e.getLocalizedMessage())) {
      return e.getLocalizedMessage();
    }

    return e.getClass().getSimpleName();
  }

  protected abstract void validiere(Violations violations, ImportObject importObject, Context context, ImportOptions importOptions);

  /**
   * @param importObject
   *          die aus der Importdatei erzeugten Daten (meist eine Liste von Entity's)
   * @param context
   * @param importOptions
   *          Gibt an, ob Änderungen durch neue, geänderte und wegfallende Daten übernommen oder ignoriert werden sollen
   * @param bericht
   *          Hier hinein wird der Bericht geschrieben
   * @param vorab
   *          <code>true</code>, wenn der Bericht vorab erzeugt wird, <code>false</code>, wenn er nach dem Import erzeugt wird. Dieser Parameter kann im Normalfall ignoriert werden. Er ermöglicht z.B.
   *          eine Unterscheidung in der sprachlichen Formulierung im Vorab-Bericht ("... werden gelöscht") und im persistenten Bericht ("... wurden gelöscht") vorzunehmen.
   */
  protected abstract void erstelleBericht(ImportObject importObject, Context context, ImportOptions importOptions, StringBuilder bericht, boolean vorab);

  protected abstract ImportUpdateReport createImportUpdateReport(ImportObject importObject, Violations violations, Context context, ImportOptions importOptions, StringBuilder bericht);

  /**
   * Daten aus Import Objekt nur anlegen, wenn noch kein persistentes Objekt vorhanden
   */
  protected abstract void anlegenWennNichtVorhanden(ImportObject importObject, Context context, CustomDateiImportBerichtCustomData customDateiImportBericht, StringBuilder bericht);

  /**
   * Daten aus Import Objekt nur an bereits persistente Objekte übernehmen
   */
  protected abstract void aktualisiereBestehendenDaten(ImportObject importObject, Context context, CustomDateiImportBerichtCustomData customDateiImportBericht, StringBuilder bericht);

  /**
   * lösche persistente Objekte, wenn diese im Import Objekt nicht enthalten sind
   */
  protected abstract void loescheBestehendenDatenWennNichtInImportDatenVorhanden(ImportObject importObject, Context context, CustomDateiImportBerichtCustomData customDateiImportBericht,
      StringBuilder bericht);

  /**
   * Zur Auswahl sollen die ImportOptions stehen, die Aktionen umfassen, die auch vom Importer unterstützt werden. Diese werden anhand der drei Methoden isUnterstuetzt...() automatisch ermittelt.
   * Falls diese fachlich nicht sinnvoll sind, kann man diese Methode überschreiben.
   */
  @PerformanceNoLog
  public Collection<ImportOptions> getSupportedImportOptions(Context context) {
    return ImportOptions.zuUnterstuetztenAktionen(isUnterstuetztAktualisiereBestehendenDaten(), isUnterstuetztAnlegenWennNichtVorhanden(),
        isUnterstuetztLoescheBestehendenDatenWennNichtInImportDatenVorhanden());
  }

  protected boolean isUnterstuetztAnlegenWennNichtVorhanden() {
    return false;
  }

  protected boolean isUnterstuetztAktualisiereBestehendenDaten() {
    return false;
  }

  protected boolean isUnterstuetztLoescheBestehendenDatenWennNichtInImportDatenVorhanden() {
    return false;
  }

  @Override
  protected abstract ImportExchange<ImportObject, Context> getExchange();

  @PerformanceNoLog
  public Meta getMetaForEntity() {
    return getMetaForEntityInternal();
  }

  protected abstract Meta getMetaForEntityInternal();

  /**
   * Diese Methode nur überschreiben, wenn es aus Performancegründen absolut notwendig ist!
   */
  protected void importiereImportObject(ImportObject importObject, Context context, ImportOptions importOptions, CustomDateiImportBerichtCustomData customDateiImportBericht, StringBuilder bericht) {
    if (importOptions.isLoescheBestehendenDatenWennNichtInImportDatenVorhanden()) {
      loescheBestehendenDatenWennNichtInImportDatenVorhanden(importObject, context, customDateiImportBericht, bericht);
    }

    if (importOptions.isAktualisiereBestehendenDaten()) {
      aktualisiereBestehendenDaten(importObject, context, customDateiImportBericht, bericht);
    }

    if (importOptions.isImportDatenUebernehmenWennNichtBereitsVorhanden()) {
      anlegenWennNichtVorhanden(importObject, context, customDateiImportBericht, bericht);
    }
  }

  protected ImportObject readImportObject(DateiImport dateiImport, Context context) throws MappingException {
    Datei datei = dateiImport.getDatei();

    try (InputStream inputStream = datei.createInputStream(streamCreator)) {
      FileKonfiguration fileKonfiguration = createFileKonfiguration(datei);

      return readImportObject(context, inputStream, fileKonfiguration);
    } catch (IOException e) {
      throw new RuntimeException(e.getMessage(), e);
    }
  }

  protected ImportObject readImportObject(Context context, InputStream inputStream, FileKonfiguration fileKonfiguration) throws MappingException {
    return getExchange().importiere(inputStream, fileKonfiguration, context);
  }

  protected void validiere(Violations violations, DateiImport dateiImport, Context context) {
    Datei datei = dateiImport.getDatei();

    try (InputStream inputStream = datei.createInputStream(streamCreator)) {
      violations.add(getExchange().validiereImport(inputStream, createFileKonfiguration(datei), context));
    } catch (IOException e) {
      throw new RuntimeException(e.getMessage(), e);
    }
  }

  private FileKonfiguration createFileKonfiguration(Datei datei) {
    return createFileKonfiguration(datei.getFileFormat(), datei);
  }

  protected Violations validateEntity(Entity e) {
    Violations result = new Violations();

    validateEntity(result, e, getMetaForEntity());

    return result;
  }

  protected boolean validateEntity(Violations violations, Entity e, Meta meta) {
    return validateEntity(violations, e, e.getTk().toStringLokalisiert(getMessages()), meta);
  }

  /**
   * @return true, wenn valide
   */
  protected boolean validateEntity(Violations violations, Entity e, Object identifikator) {
    return validateEntity(violations, e, identifikator, getMetaForEntity());
  }

  protected boolean validateEntity(Violations violations, Entity e, Object identifikator, Meta meta) {
    // ELECT-10226
    //    Set<ConstraintViolation<Entity>> constraintViolations = getEntityValidation().validate(e);
    //
    //    for (ConstraintViolation<Entity> constraintViolation : constraintViolations) {
    //      addErrorViolation(violations, identifikator, meta, constraintViolation);
    //    }

    Set<EntityConstraintViolation> entityConstraintViolations = getEntityValidation().validateEntity(e);
    for (EntityConstraintViolation entityConstraintViolation : entityConstraintViolations) {
      addErrorViolation(violations, identifikator, meta, entityConstraintViolation);
    }

    // return constraintViolations.isEmpty();
    return entityConstraintViolations.isEmpty();
  }

  protected void addErrorViolation(Violations violations, Object identifikator, Meta meta, EntityConstraintViolation entityConstraintViolation) {
    String identifikatorAsString = "" + identifikator;

    if (identifikator instanceof EntityTK) {
      identifikatorAsString = ((EntityTK) identifikator).toStringLokalisiert(getMessages());
    }

    String entityLokalisiert = getMessage(entityConstraintViolation.getEntity().getEntityTechnicalName());
    String propertyLokalisiert = getMessage(entityConstraintViolation.getPropertyKey());
    String fehlerLokalsiert = getMessage("EntityConstraintViolationType." + entityConstraintViolation.getConstraintViolationType().name(), entityConstraintViolation.getParams());

    violations.addError("invalid.entity.validation.message", entityLokalisiert, propertyLokalisiert, fehlerLokalsiert, identifikatorAsString);
  }

  protected EntityValidation getEntityValidation() {
    return entityValidation;
  }

  protected void addErrorViolation(Violations violations, Object identifikator, Meta meta, ConstraintViolation<Entity> constraintViolation) {
    String identifikatorAsString = "" + identifikator;

    if (identifikator instanceof EntityTK) {
      identifikatorAsString = ((EntityTK) identifikator).toStringLokalisiert(getMessages());
    }

    violations.addError("invalid.validation.message", "", meta.getEinzahlMessage(), identifikatorAsString, constraintViolation.getMessage(), constraintViolation.getPropertyPath(),
        constraintViolation.getInvalidValue());
  }

  @PerformanceNoLog
  public List<String> getCsvColumnHeaders(FileFormat fileFormat, Context context) {
    return getExchange().getCsvColumnHeaders(fileFormat, context);
  }

  protected DateiImport attach(DateiImport dateiImport) {
    return dateiImportDAO.attach(dateiImport);
  }

  protected void warn(String message) {
    LOG.warn(message);
  }

  protected void warn(String message, Throwable cause) {
    LOG.warn(message, cause);
  }

  protected void error(String message) {
    LOG.error(message);
  }
}
