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

import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.stream.Collectors;

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

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

import de.ivu.elect.business.CurrentCustomer;
import de.ivu.elect.business.lokalisierung.boundary.LokalisierungDAO;
import de.ivu.elect.business.lokalisierung.entity.Lokalisierung;
import de.ivu.elect.business.lokalisierung.entity.LokalisierungLocale;
import de.ivu.elect.business.lokalisierung.entity.LokalisierungModul;
import de.ivu.elect.business.lokalisierung.entity.LokalisierungUK;
import de.ivu.elect.business.security.Customer;


import static java.util.Collections.singletonList;
import static org.apache.commons.lang3.StringUtils.isEmpty;

@Stateless
public class LokalisierungInit implements Serializable {

  private static final long serialVersionUID = 1L;

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

  private static final int PROPERTY_KEY_SIZE = 10_000; // Anzahl erwarteter Properties ungefähr - konkreter Wert nicht wichtig

  private static final int PROPERTY_SPRACHEN = 3; // Anzahl erwarteter Sprachen ungefähr - konkreter Wert nicht wichtig

  public static final int PROPERTY_HASHMAP_INITIAL_CAPACITY = PROPERTY_KEY_SIZE * PROPERTY_SPRACHEN * 2;

  @Inject
  private transient CurrentCustomer currentCustomer;

  @Inject
  private transient LokalisierungDAO lokalisierungDAO;

  public LokalisierungInit() {
    super();
  }

  public void init() {
    LOG.info("Initialisiere Lokalisierungen");

    Customer customer = currentCustomer.getCustomer();
    LOG.info("Customer: " + customer);
    LOG.info("Customer-Sprachen: " + currentCustomer.getAlleUnterstuetztenSprachen());

    Map<LokalisierungUK, Lokalisierung> lokalisierungen = ladeLokalisierungen(customer);
    LOG.info("Anzahl Lokalisierungen geladen: " + lokalisierungen.size());

    Map<String, Lokalisierung> alleMessageKeys = bestimmeAlleMessageKeys(lokalisierungen);
    LOG.info("Anzahl Schluessel in fuehrender Sprache geladen: " + alleMessageKeys.size());

    fuelleFehlendeKeysAufEntferneZuViele(alleMessageKeys, lokalisierungen);
    LOG.info("Anzahl Lokalisierungen aufgefuellt: " + lokalisierungen.size());

    Map<LokalisierungUK, Lokalisierung> lokalisierungenInDb = synchronisiereMitDatenbank(lokalisierungen);
    LOG.info("Anzahl persistenter Lokalisierungen: " + lokalisierungenInDb.size());

    aktualisiereLokalisierungStore(lokalisierungenInDb.values());

    LOG.info("Initialisiere Lokalisierungen - done");
  }

  public void aktualisiereLokalisierungStore() {
    aktualisiereLokalisierungStore(lokalisierungDAO.findByAll());
  }

  protected void aktualisiereLokalisierungStore(Collection<Lokalisierung> lokalisierungenInDb) {
    LOG.info("Aktualisiere LokalisierungStore");

    Map<LokalisierungLocale, Map<String, String>> propertiesNachLocale = new EnumMap<>(LokalisierungLocale.class);

    for (LokalisierungLocale lokalisierungLocale : currentCustomer.getAlleUnterstuetztenSprachen()) {
      propertiesNachLocale.put(lokalisierungLocale, new HashMap<>(PROPERTY_HASHMAP_INITIAL_CAPACITY));
    }

    LokalisierungLocale fuehrendesLocale = LokalisierungLocale.getFuehrendesLocale();
    Map<String, String> fuehrendesLocaleProperties = propertiesNachLocale.get(fuehrendesLocale);

    for (Lokalisierung lokalisierung : lokalisierungenInDb) {
      if (lokalisierung.getLocale().equals(fuehrendesLocale)) {
        fuehrendesLocaleProperties.put(lokalisierung.getSchluessel().intern(), lokalisierung
            .getWertGespeichert()); // Schlüssel in JVM String speichern, selbes Objekt wird wieder verwendet, Vergleiche beim Suchen gehen schneller - so die Hoffnung
      }
    }

    for (Lokalisierung lokalisierung : lokalisierungenInDb) {
      if (!lokalisierung.getLocale().equals(fuehrendesLocale)) {
        String wertVerwendet = lokalisierung.getWertGespeichert();
        String schluessel = lokalisierung.getSchluessel()
            .intern(); // Schlüssel in JVM String speichern, selbes Objekt wird wieder verwendet, Vergleiche beim Suchen gehen schneller - so die Hoffnung

        if (wertVerwendet == null) {
          wertVerwendet = fuehrendesLocaleProperties.get(schluessel); // Fallback auf führendes Locale
        }

        propertiesNachLocale.get(lokalisierung.getLocale()).put(schluessel, wertVerwendet);
      }
    }

    LokalisierungStore.getPropertiesNachLocale().clear();
    LokalisierungStore.getPropertiesNachLocale().putAll(propertiesNachLocale);

    LOG.info("Aktualisiere LokalisierungStore - done");
  }



  private Map<LokalisierungUK, Lokalisierung> synchronisiereMitDatenbank(Map<LokalisierungUK, Lokalisierung> lokalisierungenInProperties) {
    Map<LokalisierungUK, Lokalisierung> result = new HashMap<>(PROPERTY_HASHMAP_INITIAL_CAPACITY);

    LOG.info("Lokalisierungen in DB werden geladen");
    Collection<Lokalisierung> lokalisierungenInDb = lokalisierungDAO.findByAll();
    LOG.info("Lokalisierungen in DB geladen " + lokalisierungenInDb.size());
    Collection<Lokalisierung> lokalisierungenInDbZuLoeschen = new HashSet<>();

    for (Lokalisierung lokalisierung : lokalisierungenInDb) {
      LokalisierungUK lokalisierungUK = lokalisierung.getLokalisierungUK();
      if (lokalisierungenInProperties.containsKey(lokalisierungUK)) {
        result.put(lokalisierungUK, lokalisierung);
      } else {
        lokalisierungenInDbZuLoeschen.add(lokalisierung);
      }
    }

    LOG.info("Lokalisierungen in DB nicht in Properties-Datei: " + lokalisierungenInDbZuLoeschen.size() + " - werden geloescht");

    for (Lokalisierung lokalisierung : lokalisierungenInDbZuLoeschen) {
      lokalisierungDAO.remove(lokalisierung);
    }

    LOG.info("Lokalisierungen in DB und Properties-Datei: " + result.size() + " - werden aktualisiert");

    for (Entry<LokalisierungUK, Lokalisierung> entryInDb : result.entrySet()) {
      Lokalisierung lokalisierungInPropertyDatei = lokalisierungenInProperties.get(entryInDb.getKey());
      Lokalisierung lokalisierungInDb = entryInDb.getValue();

      lokalisierungInDb.setWertPropertydatei(lokalisierungInPropertyDatei.getWertPropertydatei());
      lokalisierungInDb.setModul(lokalisierungInPropertyDatei.getModul());
    }

    LOG.info("Lokalisierungen in Properties-Datei nicht in DB werden angelegt");

    for (Entry<LokalisierungUK, Lokalisierung> entryInProperties : lokalisierungenInProperties.entrySet()) {
      LokalisierungUK lokalisierungUK = entryInProperties.getKey();

      if (!result.containsKey(lokalisierungUK)) {
        Lokalisierung lokalisierung = entryInProperties.getValue();
        lokalisierungDAO.persist(lokalisierung);
        result.put(lokalisierungUK, lokalisierung);
      }
    }

    LOG.info("Lokalisierungen mit Db synchronisiert: " + result.size());
    return result;
  }

  private void fuelleFehlendeKeysAufEntferneZuViele(Map<String, Lokalisierung> alleMessageKeys, Map<LokalisierungUK, Lokalisierung> lokalisierungen) {
    Map<LokalisierungLocale, Set<String>> keysProSprache = new EnumMap<>(LokalisierungLocale.class);

    for (LokalisierungLocale lokalisierungLocale : currentCustomer.getAlleUnterstuetztenSprachen()) {
      keysProSprache.put(lokalisierungLocale, new HashSet<>(PROPERTY_KEY_SIZE));
    }

    Set<LokalisierungUK> zuViele = new HashSet<>();

    for (LokalisierungUK lokalisierungUK : lokalisierungen.keySet()) {
      keysProSprache.get(lokalisierungUK.getLocale()).add(lokalisierungUK.getSchluessel());

      if (!alleMessageKeys.containsKey(lokalisierungUK.getSchluessel())) {
        zuViele.add(lokalisierungUK);
      }
    }

    for (Map.Entry<LokalisierungLocale, Set<String>> entry : keysProSprache.entrySet()) {
      Set<String> missing = new HashSet<>(alleMessageKeys.keySet());
      missing.removeAll(entry.getValue());

      for (String key : missing) {
        Lokalisierung lokalisierung = new Lokalisierung();
        lokalisierung.setLocale(entry.getKey());
        lokalisierung.setModul(alleMessageKeys.get(key).getModul());
        lokalisierung.setSchluessel(key);

        lokalisierungen.put(lokalisierung.getLokalisierungUK(), lokalisierung);
      }
    }

    for (LokalisierungUK lokalisierungUK : zuViele) {
      lokalisierungen.remove(lokalisierungUK);
    }
  }

  private Map<String, Lokalisierung> bestimmeAlleMessageKeys(Map<LokalisierungUK, Lokalisierung> lokalisierungen) {

    LokalisierungLocale fuehrendesLocale = LokalisierungLocale.getFuehrendesLocale();

    return lokalisierungen.values().stream()
        .filter(lokalisierung -> fuehrendesLocale.equals(lokalisierung.getLocale()))
        .collect(Collectors.toMap(Lokalisierung::getSchluessel, lokalisierung -> lokalisierung, (a, b) -> b));
  }

  private Map<LokalisierungUK, Lokalisierung> ladeLokalisierungen(Customer customer) {
    Map<LokalisierungUK, Lokalisierung> result = new HashMap<>(PROPERTY_HASHMAP_INITIAL_CAPACITY);

    List<LokalisierungModul> lokalisierungModule = LokalisierungModul.getModuleForCustomer(customer);

    Locale defaultLocale = Locale.getDefault();
    LOG.info("Default Locale: " + defaultLocale);
    LokalisierungLocale fuehrendesLocale = LokalisierungLocale.getFuehrendesLocale();
    LOG.info("Führendes Locale: " + LokalisierungLocale.getFuehrendesLocale());

    try {
      // ELECT-9612 Zum Einlesen der Properties-Dateien ohne Sprachkennung (messages.properties), ist es wohl
      // notwendig,  dass das default Locale gesetzt ist...
      Locale.setDefault(fuehrendesLocale.getLocale());

      for (LokalisierungModul lokalisierungModul : lokalisierungModule) {
        for (final LokalisierungLocale lokalisierungLocale : currentCustomer.getAlleUnterstuetztenSprachen()) {
          try {
            ResourceBundle resourceBundle;

            if (fuehrendesLocale.equals(lokalisierungLocale)) {
              resourceBundle = ResourceBundle.getBundle(lokalisierungModul.getResourceFile(), lokalisierungLocale.getLocale());
            } else {
              resourceBundle = ResourceBundle.getBundle(lokalisierungModul.getResourceFile(), lokalisierungLocale.getLocale(), new ResourceBundle.Control() {

                @Override
                public List<Locale> getCandidateLocales(String baseName, Locale locale) {
                  if (baseName == null) {
                    throw new NullPointerException();
                  }
                  return singletonList(lokalisierungLocale.getLocale());
                }
              });
            }

            LOG.info("Bundle gefunden für: Modul: " + lokalisierungModul + ", Ressource-File:" + lokalisierungModul
                .getResourceFile() + ", Sprache: " + lokalisierungLocale);

            fillInLokalisierungen(result, resourceBundle, lokalisierungModul, lokalisierungLocale);
          } catch (MissingResourceException mre) {
            LOG.warn("Kein Bundle gefunden für: Modul: " + lokalisierungModul + ", Ressource-File:" + lokalisierungModul
                .getResourceFile() + ", Sprache: " + lokalisierungLocale);
          }
        }
      }
    } finally {
      Locale.setDefault(defaultLocale); // ELECT-9612 wiederherstellen
    }

    return result;
  }

  protected void fillInLokalisierungen(Map<LokalisierungUK, Lokalisierung> result, ResourceBundle resourceBundle, LokalisierungModul lokalisierungModul,
                                       final LokalisierungLocale lokalisierungLocale) {
    for (String key : Collections.list(resourceBundle.getKeys())) {
      Lokalisierung lokalisierung = new Lokalisierung();
      lokalisierung.setLocale(lokalisierungLocale);
      lokalisierung.setModul(lokalisierungModul);
      lokalisierung.setSchluessel(key);

      String wert = resourceBundle.getString(key);

      if (isEmpty(wert)) { // ELECT-6850!
        wert = null;
      }

      lokalisierung.setWertPropertydatei(wert);

      result.put(lokalisierung.getLokalisierungUK(), lokalisierung);
    }
  }
}
