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

import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
import static org.apache.commons.lang3.StringUtils.contains;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.apache.commons.lang3.StringUtils.replace;
import static org.apache.commons.lang3.StringUtils.splitByWholeSeparator;
import static org.apache.commons.lang3.StringUtils.trimToEmpty;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

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

import de.ivu.elect.business.gebietsbaum.entity.NlGebietCustomData;
import de.ivu.elect.business.gebietsbaum.entity.Wahldurchfuehrungsgebiet;
import de.ivu.elect.business.stimmeingabe.boundary.AbstractStimmeingabefehlerDAO;
import de.ivu.elect.business.stimmeingabe.entity.Ergebniseingabestufe;
import de.ivu.elect.business.stimmeingabe.entity.Ergebniseingang;
import de.ivu.elect.business.stimmeingabe.entity.ErgebniseingangErfasser;
import de.ivu.elect.business.stimmeingabe.entity.Ergebniseingangsart;
import de.ivu.elect.business.stimmeingabe.entity.Ergebniseingangsfehlerstatus;
import de.ivu.elect.business.stimmeingabe.entity.Ergebniseingangsherkunft;
import de.ivu.elect.business.stimmeingabe.entity.NlErgebniseingang;
import de.ivu.elect.business.stimmeingabe.entity.Stimmeingabefehler;
import de.ivu.elect.business.stimmeingabe.entity.Stimmeingabefehlerart;
import de.ivu.elect.jee.protokollierung.AttributTyp;
import de.ivu.elect.jee.protokollierung.VorherNachherWert;
import de.ivu.elect.util.CaseAndDiacriticIgnoringComparator;

// ELECTVAPP-387
public class NlWasGebietCustomDataErgebniseingangProtokoll {

  private static final String ERGEBNISEINGANG_ATTRIBUTE_SEPARATOR = "^";

  private static final String STIMMEINGABEFEHLER_ATTRIBUTE_SEPARATOR = ":";

  private static final String STIMMEINGABEFEHLER_PROTOKOLL_ATTRIBUTE_SEPARATOR = "@";

  private static final String DATE_TIME_FORMAT = "dd.MM.yyyy HH:mm:ss.SSS";

  private static final String PROTOKOLL_KEY = "NlWasGebietCustomDataErgebniseingangProtokoll";

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

  public NlWasGebietCustomDataErgebniseingangProtokoll() {
    super();
  }

  public static boolean isErgebniseingangProtokolliert(Ergebniseingang ergebniseingang) {
    return ergebniseingang != null && new NlErgebniseingang(ergebniseingang).isProtokoll();
  }

  public void protokolliereErgebniseingang(Ergebniseingang ergebniseingang, ErgebniseingangErfasser protokollErzeugtDurch, Date protokollErzeugtAm, String protokoll) {
    NlGebietCustomData nlGebietCustomData = ergebniseingang.getWahldurchfuehrungsgebiet().getCustomData(NlGebietCustomData.class);
    nlGebietCustomData.addProtokollEintrag(PROTOKOLL_KEY, asProtokollEintrag(ergebniseingang, protokollErzeugtDurch, protokollErzeugtAm, protokoll));
  }

  public Collection<Ergebniseingang> collectErgebniseingaengeProtokolliert(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet) {
    Collection<Ergebniseingang> result = new ArrayList<>();

    if (wahldurchfuehrungsgebiet != null) {
      for (String protokollEintrag : wahldurchfuehrungsgebiet.getCustomData(NlGebietCustomData.class).getProtokollEintraege(PROTOKOLL_KEY)) {
        Optional<Ergebniseingang> ergebniseingangIfParseable = asErgebniseingang(protokollEintrag);

        if (ergebniseingangIfParseable.isPresent()) {
          result.add(NlErgebniseingang.asErgebniseingangProtokolliert(wahldurchfuehrungsgebiet, ergebniseingangIfParseable.get()));
        } else if (isNotBlank(protokollEintrag)) {
          LOG.warn("Protokolleintrag konnte nicht (mehr) gelesen werden [wahldurchfuehrungsgebiet = {}, protokollEintrag = {}]", wahldurchfuehrungsgebiet, protokollEintrag);
        }
      }
    }

    return result;
  }

  protected Optional<Ergebniseingang> asErgebniseingang(String protokollEintrag) {
    Map<String, VorherNachherWert> protokollAttributePerKey = new HashMap<>();

    List<Stimmeingabefehler> stimmeingabefehler = new ArrayList<>();

    Ergebniseingang ergebniseingang = new Ergebniseingang() {

      private static final long serialVersionUID = 1L;

      @Override
      public Map<String, VorherNachherWert> getProtokollierteAttributeMitWerten() {
        return protokollAttributePerKey;
      }

      @Override
      public Collection<Stimmeingabefehler> getStimmeingabefehler() {
        return stimmeingabefehler;
      }

      @Override
      public List<String> getStimmeingabefehlerBeschreibungen(AbstractStimmeingabefehlerDAO stimmeingabefehlerDAO, Comparator<String> ignored) {
        return getStimmeingabefehler().stream() //
            .map(Stimmeingabefehler::getBeschreibung) //
            .distinct() // ELECT-10823
            .collect(toList());
      }
    };

    String[] attributes = splitByWholeSeparator(protokollEintrag, ERGEBNISEINGANG_ATTRIBUTE_SEPARATOR);

    if (attributes != null) {
      for (int i = 0; i < attributes.length; i++) {
        String value = attributes[i];

        switch (i) {
          case 0 :
            ergebniseingang.setEingangssequenznummer(asLong(value));
            break;
          case 1 :
            ergebniseingang.setErgebnissequenznummer(asLong(value));
            break;
          case 2 :
            ergebniseingang.setErfasser(value);
            break;
          case 3 :
            ergebniseingang.setErgebniseingangsherkunft(asEnumValue(Ergebniseingangsherkunft.class, value));
            break;
          case 4 :
            ergebniseingang.setErgebniseingangsart(asEnumValue(Ergebniseingangsart.class, value));
            break;
          case 5 :
            ergebniseingang.setErgebniseingabestufe(asEnumValue(Ergebniseingabestufe.class, value));
            break;
          case 6 :
            ergebniseingang.setErgebniseingangsfehlerstatus(asEnumValue(Ergebniseingangsfehlerstatus.class, value));
            break;
          case 7 :
            ergebniseingang.setZeitstempel(asDate(value));
            break;
          case 8 :
            ergebniseingang.setEingabeHashCode(value);
            break;

          default :
            break;
        }

        if (i > 8) {
          asStimmeingabefehler(ergebniseingang, value).ifPresent(stimmeingabefehler::add);
        }
      }
    }

    if (!missesAnyAttributeRequired(ergebniseingang)) {
      return Optional.of(ergebniseingang);
    }

    return Optional.empty();
  }

  private boolean missesAnyAttributeRequired(Ergebniseingang ergebniseingang) {
    return ergebniseingang == null //
        || ergebniseingang.getErgebniseingangsherkunft() == null //
        || ergebniseingang.getErgebniseingangsart() == null //
        || ergebniseingang.getErgebniseingabestufe() == null //
        || ergebniseingang.getErgebniseingangsfehlerstatus() == null //
    ;
  }

  private Long asLong(String value) {
    try {
      return Long.parseLong(value);
    } catch (Exception ignored) {
    }

    return null;
  }

  private <E extends Enum<E>> E asEnumValue(Class<E> enumClazz, String value) {
    try {
      for (E enumValue : enumClazz.getEnumConstants()) {
        if (Objects.equals(enumValue.name(), value)) {
          return enumValue;
        }
      }
    } catch (Exception ignored) {
    }

    return null;
  }

  private Date asDate(String value) {
    try {
      return new SimpleDateFormat(DATE_TIME_FORMAT).parse(value);
    } catch (Exception ignored) {
    }

    return null;
  }

  private Optional<Stimmeingabefehler> asStimmeingabefehler(Ergebniseingang ergebniseingang, String value) {
    Map<String, VorherNachherWert> protokollAttributePerKey = new HashMap<>();

    String[] beschreibungPerFehlerart = splitByWholeSeparator(value, STIMMEINGABEFEHLER_ATTRIBUTE_SEPARATOR, 2);

    if (beschreibungPerFehlerart != null && beschreibungPerFehlerart.length == 2) {
      Stimmeingabefehler stimmeingabefehler = new Stimmeingabefehler();
      stimmeingabefehler.setFehlerart(asEnumValue(Stimmeingabefehlerart.class, beschreibungPerFehlerart[0]));

      String beschreibung = beschreibungPerFehlerart[1];

      if (contains(beschreibung, STIMMEINGABEFEHLER_PROTOKOLL_ATTRIBUTE_SEPARATOR)) {
        String[] protokollAttribute = splitByWholeSeparator(beschreibung, STIMMEINGABEFEHLER_PROTOKOLL_ATTRIBUTE_SEPARATOR);

        if (protokollAttribute != null && protokollAttribute.length == 3) {
          String protokollErzeugtDurch = protokollAttribute[1];
          Date protokollErzeugtAm = asDate(protokollAttribute[2]);
          String protokoll = protokollAttribute[0];

          protokollAttributePerKey.put(NlErgebniseingang.PROTOKOLL_ERZEUGT_DURCH, new VorherNachherWert(AttributTyp.STRING, protokollErzeugtDurch, protokollErzeugtDurch));
          protokollAttributePerKey.put(NlErgebniseingang.PROTOKOLL_ERZEUGT_AM, new VorherNachherWert(AttributTyp.DATE, protokollErzeugtAm, protokollErzeugtAm));
          protokollAttributePerKey.put(NlErgebniseingang.PROTOKOLL, new VorherNachherWert(AttributTyp.STRING, protokoll, protokoll));

          beschreibung = protokoll; // ELECT-10946
        }
      }

      stimmeingabefehler.setBeschreibung(beschreibung);

      ergebniseingang.getProtokollierteAttributeMitWerten().putAll(protokollAttributePerKey);

      if (!missesAnyAttributeRequired(stimmeingabefehler)) {
        return Optional.of(stimmeingabefehler);
      }
    }

    return Optional.empty();
  }

  private boolean missesAnyAttributeRequired(Stimmeingabefehler stimmeingabefehler) {
    return stimmeingabefehler == null //
        || stimmeingabefehler.getFehlerart() == null //
        || isBlank(stimmeingabefehler.getBeschreibung());
  }

  protected String asProtokollEintrag(Ergebniseingang ergebniseingang, ErgebniseingangErfasser protokollErzeugtDurch, Date protokollErzeugtAm, String protokoll) {
    List<String> attributes = new ArrayList<>();

    attributes.add(asProtokollEintragAttribute(ergebniseingang.getEingangssequenznummer()));
    attributes.add(asProtokollEintragAttribute(ergebniseingang.getErgebnissequenznummer()));
    attributes.add(asProtokollEintragAttribute(ergebniseingang.getErfasser()));
    attributes.add(asProtokollEintragAttribute(ergebniseingang.getErgebniseingangsherkunft()));
    attributes.add(asProtokollEintragAttribute(ergebniseingang.getErgebniseingangsart()));
    attributes.add(asProtokollEintragAttribute(ergebniseingang.getErgebniseingabestufe()));
    attributes.add(asProtokollEintragAttribute(ergebniseingang.getErgebniseingangsfehlerstatus()));
    attributes.add(asProtokollEintragAttribute(ergebniseingang.getZeitstempel()));
    attributes.add(asProtokollEintragAttribute(ergebniseingang.getEingabeHashCode()));
    attributes.add(asProtokollEintragAttribute(protokollErzeugtDurch, protokollErzeugtAm, protokoll, ergebniseingang.getStimmeingabefehler()));

    return String.join(ERGEBNISEINGANG_ATTRIBUTE_SEPARATOR, attributes);
  }

  private String asProtokollEintragAttribute(Number value) {
    return asProtokollEintragAttribute(value == null ? "" : value.toString());
  }

  private String asProtokollEintragAttribute(Enum< ? > value) {
    return asProtokollEintragAttribute(value == null ? "" : value.name());
  }

  private String asProtokollEintragAttribute(Date value) {
    String result = "";

    try {
      result = new SimpleDateFormat(DATE_TIME_FORMAT).format(value);
    } catch (Exception ignored) {
    }

    return asProtokollEintragAttribute(result);
  }

  private String asProtokollEintragAttribute(String value) {
    return trimToEmpty(replace(value, ERGEBNISEINGANG_ATTRIBUTE_SEPARATOR, ""));
  }

  private String asProtokollEintragAttribute(ErgebniseingangErfasser protokollErzeugtDurch, Date protokollErzeugtAm, String protokoll, Collection<Stimmeingabefehler> stimmeingabefehler) {
    List<Stimmeingabefehler> stimmeingabefehlerSerialized = new ArrayList<>(stimmeingabefehler);
    stimmeingabefehlerSerialized.sort((one, another) -> CaseAndDiacriticIgnoringComparator.DEFAULT_INSTANCE.compare(one.getBeschreibung(), another.getBeschreibung()));

    // ELECT-10946
    if (protokollErzeugtDurch != null && protokollErzeugtAm != null && isNotBlank(protokoll)) {
      String protokollSerialized = protokoll;
      protokollSerialized = replace(protokollSerialized, STIMMEINGABEFEHLER_PROTOKOLL_ATTRIBUTE_SEPARATOR, "");

      Stimmeingabefehler stimmeingabefehlerProtokoll = new Stimmeingabefehler();
      stimmeingabefehlerProtokoll.setFehlerart(Stimmeingabefehlerart.WARNING);
      stimmeingabefehlerProtokoll.setBeschreibung(protokollSerialized + STIMMEINGABEFEHLER_PROTOKOLL_ATTRIBUTE_SEPARATOR + protokollErzeugtDurch.getKennung()
          + STIMMEINGABEFEHLER_PROTOKOLL_ATTRIBUTE_SEPARATOR + asProtokollEintragAttribute(protokollErzeugtAm));

      stimmeingabefehlerSerialized.add(0, stimmeingabefehlerProtokoll);
    }

    return stimmeingabefehlerSerialized.stream() //
        .map(value -> asProtokollEintragAttribute(value.getFehlerart()) + STIMMEINGABEFEHLER_ATTRIBUTE_SEPARATOR + asProtokollEintragAttribute(value.getBeschreibung())) //
        .distinct() // ELECT-10823
        .collect(joining(ERGEBNISEINGANG_ATTRIBUTE_SEPARATOR));
  }
}
