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

import de.ivu.elect.business.mandant.boundary.MandantDAO;
import de.ivu.elect.business.mandant.entity.EntityPKMitMandant;
import de.ivu.elect.business.mandant.entity.Mandant;
import de.ivu.elect.business.system.boundary.SystemLogWriter;
import de.ivu.elect.business.wahl.boundary.WahlDAO;
import de.ivu.elect.business.wahl.boundary.WahlterminDAO;
import de.ivu.elect.business.wahl.control.WahlParameterGeaendertEvent;
import de.ivu.elect.business.wahl.entity.*;
import de.ivu.elect.jee.cron.AbstractCronTimer;
import de.ivu.elect.jee.cron.Crontab;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;

import javax.annotation.PostConstruct;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import javax.enterprise.event.Observes;
import javax.enterprise.event.Reception;
import javax.enterprise.event.TransactionPhase;
import javax.inject.Inject;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;

import static de.ivu.elect.business.mandant.entity.EntityPKMitMandant.from;
import static java.util.Optional.empty;
import static java.util.Optional.of;

/**
 * Abgeleitete Klasse mit @Startup und @Singleton und @DependsOn("StartupSingleton") annotieren
 */
public abstract class AbstractElectMultipleCronTimer<Payload extends Serializable> extends AbstractCronTimer {

  private static final Map<ElectMultipleCronTimerKey, Boolean> RUNNING = new ConcurrentHashMap<>();

  public static final String NICHT_VORHANDEN_DEFAULT_WERT = "NICHT_VORHANDEN_DEFAULT_WERT";

  @Inject
  protected transient WahlterminDAO wahlterminDAO;

  @Inject
  protected transient WahlDAO wahlDAO;

  @Inject
  protected transient MandantDAO mandantDAO;

  @Inject
  private transient WahlParameterLoader wahlParameterLoader;

  @Inject
  private transient SystemLogWriter systemLogWriter;

  @PostConstruct
  public void init() {
    getLog().info("Init " + getJobName());
    try {
      createTimer();
      getLog().info("Init " + getJobName() + " - done");
    } catch (Exception e) {
      systemLogWriter.writeErrorLogMessageFuerSystemMandant(e.getMessage(), e, getAsync().getSystemLogQuelle());
    }
  }

  protected abstract Logger getLog();

  @Override
  protected void timeout(Serializable payload) {
    getLog().info(getJobName() + " - timeout - " + payload);
    ElectMultipleCronTimerKey key = new ElectMultipleCronTimerKey(getClass(), payload);
    if (RUNNING.containsKey(key)) {
      getLog().info(getJobName() + " läuft bereits für payload: " + payload);
    } else {
      RUNNING.put(key, Boolean.TRUE);
      getAsync().async((Payload) payload, p -> {
        RUNNING.remove(key);
      });
      getLog().info(getJobName() + " - timeout - " + payload + " - done");
    }
  }

  protected abstract AbstractElectMultipleCronTimerAsync<Payload> getAsync();

  @Override
  public String getJobName() {
    return getClass().getSimpleName();
  }

  @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
  public void listenWahlParameterChanged(@Observes(during = TransactionPhase.AFTER_SUCCESS, notifyObserver = Reception.ALWAYS) WahlParameterGeaendertEvent wahlParameterGeaendertEvent) {
    if (wahlParameterGeaendertEvent.getWahlParameterWerte().equals(getWahlParameterWerte())) {
      init();
    }
  }

  protected abstract WahlParameterWerte getWahlParameterWerte();

  @Override
  protected List<Crontab> getCrontabs() {
    List<Crontab> result = new ArrayList<>();
    for (Mandant mandant : mandantDAO.findByAll()) {
      if (mandant.getStatus().isAktiv()) {
        wahlDAO.findByAll(mandant).stream().map(this::erstelleCronTabFuerWahl).filter(Optional::isPresent).map(Optional::get).forEach(result::add);
        wahlterminDAO.findByAll(mandant).stream().map(this::erstelleCronTabFuerWahltermin).filter(Optional::isPresent).map(Optional::get).forEach(result::add);
      }
    }
    return result;
  }

  private Optional<Crontab> erstelleCronTabFuerWahl(Wahl wahl) {
    String crontab = wahlParameterLoader.resolveStringValue(wahl, getWahlParameterWerte(), NICHT_VORHANDEN_DEFAULT_WERT);
    if (NICHT_VORHANDEN_DEFAULT_WERT.equalsIgnoreCase(crontab)) {
      return empty();
    }

    if (wahl.getStatus().isCrontabErstellbar()) {
      return of(Crontab.create(crontab, createPayloadFuerWahl(wahl)));
    }
    return empty();
  }

  protected Serializable createPayloadFuerWahl(Wahl wahl) {
    return wahl.getPk();
  }

  private Optional<Crontab> erstelleCronTabFuerWahltermin(Wahltermin wahltermin) {
    String crontab = wahlParameterLoader.resolveStringValue(wahltermin, getWahlParameterWerte(), NICHT_VORHANDEN_DEFAULT_WERT);
    if (NICHT_VORHANDEN_DEFAULT_WERT.equalsIgnoreCase(crontab)) {
      return empty();
    }

    if (wahltermin.getStatus().isCrontabErstellbar()) {
      return of(Crontab.create(crontab, createPayloadFuerWahltermin(wahltermin)));
    }
    return empty();
  }

  protected Serializable createPayloadFuerWahltermin(Wahltermin wahltermin) {
    return wahltermin.getPk();
  }

  @Override
  protected void logError(String jobName, String messageKey, Serializable payload, Exception e) {
    super.logError(jobName, messageKey, payload, e);
    String messageForMessageKey = messages.getMessageForMessageKey(messageKey, payload, e.getLocalizedMessage());
    if (StringUtils.isBlank(messageForMessageKey)) {
      systemLogWriter.writeErrorLogMessageFuerMandant(bestimmeMandant(payload), "Key: " + messageKey + ", payload: " + payload + ", exception: " + e.getLocalizedMessage(),
          getAsync().getSystemLogQuelle());
    } else {
      systemLogWriter.writeErrorLogMessageFuerMandant(bestimmeMandant(payload), messageForMessageKey, getAsync().getSystemLogQuelle());
    }
  }

  protected EntityPKMitMandant bestimmeMandant(Object payload) {
    if (payload != null) {
      try {
        return bestimmeMandantAusPayload((Payload) payload);
      } catch (Exception e) {
        getLog().warn("Mandant kann nicht aus Payload bestimmt werden: " + payload);
      }
    }
    return null;
  }

  protected abstract EntityPKMitMandant bestimmeMandantAusPayload(Payload payload);

  protected EntityPKMitMandant bestimmeMandant(WahlterminPK wahlterminPK) {
    return from(wahlterminPK);
  }

  protected EntityPKMitMandant bestimmeMandant(WahlOderWahlterminPK wahlOderWahlterminPK) {
    return from(wahlOderWahlterminPK);
  }

  protected EntityPKMitMandant bestimmeMandant(WahlPK wahlPK) {
    return from(wahlPK);
  }

}
