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

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;

import javax.enterprise.inject.Specializes;
import javax.persistence.EntityManagerFactory;
import javax.persistence.PersistenceUnit;

import org.hibernate.SessionFactory;
import org.hibernate.ejb.HibernateEntityManagerFactory;
import org.hibernate.stat.SecondLevelCacheStatistics;
import org.hibernate.stat.Statistics;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.ivu.elect.business.system.entity.ActivatedHibernateCollectionStatistik;
import de.ivu.elect.business.system.entity.ActivatedHibernateEntityStatistik;
import de.ivu.elect.business.system.entity.ActivatedHibernateQueryStatistik;
import de.ivu.elect.business.system.entity.ActivatedHibernateSecondLevelCacheStatistik;
import de.ivu.elect.business.system.entity.HibernateCollectionStatistik;
import de.ivu.elect.business.system.entity.HibernateEntityStatistik;
import de.ivu.elect.business.system.entity.HibernateQueryStatistik;
import de.ivu.elect.business.system.entity.HibernateSecondLevelCacheStatistik;
import de.ivu.elect.business.system.entity.HibernateStatistikEntry;
import de.ivu.elect.util.CaseAndDiacriticIgnoringComparator;

@Specializes
public class ActivatedHibernateStatistik extends HibernateStatistik {
  private static final long serialVersionUID = 1L;

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

  public static final Comparator<String> STRING_COMPARATOR = new CaseAndDiacriticIgnoringComparator(Locale.ENGLISH);

  private static final AtomicBoolean FIRST_STATISTIC_CALL = new AtomicBoolean(true);

  @PersistenceUnit
  transient EntityManagerFactory entityManagerFactory;

  @Override
  public boolean isStatistikVerfuegbar() {
    return getStatistics() != null;
  }

  @Override
  public boolean isStatistikAktiviert() {
    if (isStatistikVerfuegbar()) {
      return getStatistics().isStatisticsEnabled();
    }

    return false;
  }

  @Override
  public void aktiviereStatistik() {
    if (isStatistikVerfuegbar()) {
      if (!isStatistikAktiviert()) {
        getStatistics().setStatisticsEnabled(true);
        LOG.info("Hibernate-Statistiken sind jetzt aktiviert");
      }
    } else {
      throw new IllegalStateException("Hibernate-Statistiken nicht verfügbar!");
    }
  }

  @Override
  public void deaktiviereStatistik() {
    if (isStatistikAktiviert()) {
      getStatistics().setStatisticsEnabled(false);
      LOG.info("Hibernate-Statistiken sind jetzt deaktiviert");
    }
  }

  @Override
  public void resetStatistik() {
    if (isStatistikAktiviert()) {
      getStatistics().clear();
      LOG.info("Hibernate-Statistiken zurückgesetzt");
    }
  }

  @Override
  public List<HibernateEntityStatistik> getEntityStatistiken() {
    if (isStatistikAktiviert()) {
      List<HibernateEntityStatistik> result = new ArrayList<>();

      for (String entityName : getStatistics().getEntityNames()) {
        result.add(new ActivatedHibernateEntityStatistik(filterEntityName(entityName), getStatistics().getEntityStatistics(entityName)));
      }

      result = result.stream().filter(HibernateEntityStatistik::isWertVorhanden).collect(Collectors.toList());

      result.sort((u, v) -> STRING_COMPARATOR.compare(u.getName(), v.getName()));

      return result;
    }

    return Collections.emptyList();
  }

  @Override
  public List<HibernateQueryStatistik> getQueryStatistiken() {
    if (isStatistikAktiviert()) {
      return Arrays.stream(getStatistics().getQueries()).map(queryName -> new ActivatedHibernateQueryStatistik(queryName,
          getStatistics().getQueryStatistics(queryName))).filter(HibernateQueryStatistik::isValid)
          .sorted((u, v) -> STRING_COMPARATOR.compare(u.getName(), v.getName())).collect(Collectors.toList());
    }

    return Collections.emptyList();
  }

  @Override
  public List<HibernateSecondLevelCacheStatistik> getSecondLevelCacheStatistiken() {
    if (isStatistikAktiviert()) {
      List<HibernateSecondLevelCacheStatistik> result = new ArrayList<>();

      Statistics statistics = getStatistics();
      final long hitCount = statistics.getSecondLevelCacheHitCount();
      final long missCount = statistics.getSecondLevelCacheMissCount();
      final long putCount = statistics.getSecondLevelCachePutCount();

      for (String region : statistics.getSecondLevelCacheRegionNames()) {
        try {
          result.add(new ActivatedHibernateSecondLevelCacheStatistik(filterEntityName(region), statistics.getSecondLevelCacheStatistics(region)));
        } catch (Exception ex) {
          LOG.warn("Cannot read region: " + region + ", e: " + ex.getMessage()); // ELECTMUC-2925
        }
      }

      result = result.stream().filter(HibernateSecondLevelCacheStatistik::isWertVorhanden).collect(Collectors.toList());

      result.sort((u, v) -> STRING_COMPARATOR.compare(u.getName(), v.getName()));

      result.add(0, new ActivatedHibernateSecondLevelCacheStatistik("<GLOBAL>", new EmptySecondLevelCacheStatistics(hitCount, missCount, putCount)));

      return result;
    }

    return Collections.emptyList();
  }

  @Override
  public List<HibernateCollectionStatistik> getCollectionStatistiken() {
    if (isStatistikAktiviert()) {
      List<HibernateCollectionStatistik> result = new ArrayList<>();

      for (String collectionRoleName : getStatistics().getCollectionRoleNames()) {
        result.add(new ActivatedHibernateCollectionStatistik(filterEntityCollectionName(collectionRoleName),
                getStatistics().getCollectionStatistics(collectionRoleName)));
      }

      result = result.stream().filter(HibernateCollectionStatistik::isWertVorhanden).collect(Collectors.toList());

      result.sort((u, v) -> STRING_COMPARATOR.compare(u.getName(), v.getName()));

      return result;
    }

    return Collections.emptyList();
  }

  /**
   * Package-Kram abschneiden
   */
  private String filterEntityName(String entityName) {

    if (entityName == null || entityName.indexOf('.') < 0) {
      return entityName;
    }
    return entityName.substring(entityName.lastIndexOf('.') + 1);
  }

  /**
   * Package-Kram abschneiden
   */
  private String filterEntityCollectionName(String collectionRoleName) {
    if (collectionRoleName == null || collectionRoleName.indexOf('.') < 0) {
      return collectionRoleName;
    }

    String[] splitted = collectionRoleName.split("\\.");
    if (splitted.length < 2) {
      return collectionRoleName;
    }
    return splitted[splitted.length - 2] + "." + splitted[splitted.length - 1];
  }

  protected Statistics getStatistics() {
    try {
      try {
        // ELECT-9463 Per Reflection, da sich Hibernate-Klassen je nach Version ständig in anderen Packages wiederfinden
        Method getSessionFactory = entityManagerFactory.getClass().getMethod("getSessionFactory");
        Object sessionFactory = getSessionFactory.invoke(entityManagerFactory);
        Method getStatistics = sessionFactory.getClass().getMethod("getStatistics");
        Statistics statistics = (Statistics) getStatistics.invoke(sessionFactory);
        if (statistics != null) {
          if (FIRST_STATISTIC_CALL.get()) {
            LOG.info("Hibernate-Statistics: Reflection");
          }
          return statistics;
        }
      } catch (Exception | Error e) {
        if (FIRST_STATISTIC_CALL.get()) {
          LOG.warn("Hibernate Reflection Statistiken nicht verfügbar: " + e.getMessage());
        }
      }

      return getStatisticsHibernate5Manuell();
    } finally {
      FIRST_STATISTIC_CALL.set(false);
    }
  }

  protected Statistics getStatisticsHibernate5Manuell() {
    try {
      if (entityManagerFactory instanceof SessionFactory) {
        Statistics statistics = ((SessionFactory) entityManagerFactory).getStatistics();
        if (FIRST_STATISTIC_CALL.get()) {
          LOG.info("Hibernate-Statistics: Hibernate 5 Manuell");
        }
        return statistics;
      }
    } catch (Exception | Error e) { // NoSuchMethodError, weil bei WF10 ein Error geworfen wird  //ELECT-4163 Hibernate Statistiken für WildFly 10 aktivieren
      if (FIRST_STATISTIC_CALL.get()) {
        LOG.warn("Hibernate 5 Statistiken nicht verfügbar: " + e.getMessage());
      }
    }

    return getStatisticsHibernate4();
  }

  // fallback auf hibernate 4
  private Statistics getStatisticsHibernate4() {
    try {

      if (entityManagerFactory instanceof org.hibernate.ejb.HibernateEntityManagerFactory) {
        Statistics statistics = ((HibernateEntityManagerFactory) entityManagerFactory).getSessionFactory().getStatistics();
        if (FIRST_STATISTIC_CALL.get()) {
          LOG.info("Hibernate-Statistics: Hibernate 4");
        }
        return statistics;
      }
    } catch (Exception | Error e) { // NoSuchMethodError, kann bei älteren WildFly-Versionen passieren...
      if (FIRST_STATISTIC_CALL.get()) {
        LOG.warn("Hibernate 4 Statistiken nicht verfügbar: " + e.getMessage());
      }
    }
    if (FIRST_STATISTIC_CALL.get()) {
      LOG.info("Hibernate-Statistics: NOOP");
    }
    return new NoopStatistics();
  }

  @Override
  public String logStatistics(boolean vollstaendig, String lineSeparator) {
    StringBuilder sb = new StringBuilder();
    if (isStatistikVerfuegbar()) {
      if (vollstaendig) {
        getStatistics().logSummary();
      }

      sb.append("QueryStatistiken:").append(lineSeparator);
      sb.append(HibernateQueryStatistik.toStringHeader());
      sb.append(lineSeparator);
      for (HibernateQueryStatistik hibernateQueryStatistik : getQueryStatistiken()) {
        sb.append(hibernateQueryStatistik.toString());
        sb.append(lineSeparator);
      }

      sb.append("EntityStatistiken:").append(lineSeparator);
      sb.append(HibernateEntityStatistik.toStringHeader());
      sb.append(lineSeparator);
      for (HibernateEntityStatistik hibernateEntityStatistik : getEntityStatistiken()) {
        sb.append(hibernateEntityStatistik.toString());
        sb.append(lineSeparator);
      }

      sb.append("SecondLevelCacheStatistiken:").append(lineSeparator);
      sb.append(HibernateSecondLevelCacheStatistik.toStringHeader());
      sb.append(lineSeparator);
      for (HibernateSecondLevelCacheStatistik hibernateSecondLevelCacheStatistik : getSecondLevelCacheStatistiken()) {
        sb.append(hibernateSecondLevelCacheStatistik.toString());
        sb.append(lineSeparator);
      }

      sb.append("CollectionStatistiken:").append(lineSeparator);
      sb.append(HibernateCollectionStatistik.toStringHeader());
      sb.append(lineSeparator);
      for (HibernateCollectionStatistik hibernateCollectionStatistik : getCollectionStatistiken()) {
        sb.append(hibernateCollectionStatistik.toString());
        sb.append(lineSeparator);
      }

      sb.append("HibernateStatistikEntries:").append(lineSeparator);
      sb.append(HibernateStatistikEntry.toStringHeader());
      sb.append(lineSeparator);
      for (HibernateStatistikEntry hibernateStatistikEntry : getStatistikEntries()) {
        sb.append(hibernateStatistikEntry.toString());
        sb.append(lineSeparator);
      }
      LOG.info(lineSeparator + sb.toString());
    } else {
      LOG.info("Hibernate-Statistiken sind deaktiviert");
    }
    return sb.toString();
  }

  @Override
  public List<HibernateStatistikEntry> getStatistikEntries() {

    List<HibernateStatistikEntry> result = new ArrayList<>();
    if (isStatistikVerfuegbar()) {
      Statistics s = getStatistics();

      result.add(new HibernateStatistikEntry("EntityDeleteCount", s.getEntityDeleteCount()));

      result.add(new HibernateStatistikEntry("EntityInsertCount", s.getEntityInsertCount()));

      result.add(new HibernateStatistikEntry("EntityLoadCount", s.getEntityLoadCount()));

      result.add(new HibernateStatistikEntry("EntityFetchCount", s.getEntityFetchCount()));

      result.add(new HibernateStatistikEntry("EntityUpdateCount", s.getEntityUpdateCount()));

      result.add(new HibernateStatistikEntry("QueryExecutionCount", s.getQueryExecutionCount()));

      result.add(new HibernateStatistikEntry("QueryExecutionMaxTime", s.getQueryExecutionMaxTime()));

      result.add(new HibernateStatistikEntry("QueryCacheHitCount", s.getQueryCacheHitCount()));

      result.add(new HibernateStatistikEntry("QueryCacheMissCount", s.getQueryCacheMissCount()));

      result.add(new HibernateStatistikEntry("QueryCachePutCount", s.getQueryCachePutCount()));

      result.add(new HibernateStatistikEntry("NaturalIdQueryExecutionCount", s.getNaturalIdQueryExecutionCount()));

      result.add(new HibernateStatistikEntry("NaturalIdQueryExecutionMaxTime", s.getNaturalIdQueryExecutionMaxTime()));

      result.add(new HibernateStatistikEntry("NaturalIdCacheHitCount", s.getNaturalIdCacheHitCount()));

      result.add(new HibernateStatistikEntry("NaturalIdCacheMissCount", s.getNaturalIdCacheMissCount()));

      result.add(new HibernateStatistikEntry("NaturalIdCachePutCount", s.getNaturalIdCachePutCount()));

      result.add(new HibernateStatistikEntry("UpdateTimestampsCacheHitCount", s.getUpdateTimestampsCacheHitCount()));

      result.add(new HibernateStatistikEntry("UpdateTimestampsCacheMissCount", s.getUpdateTimestampsCacheMissCount()));

      result.add(new HibernateStatistikEntry("UpdateTimestampsCachePutCount", s.getUpdateTimestampsCachePutCount()));

      result.add(new HibernateStatistikEntry("FlushCount", s.getFlushCount()));

      result.add(new HibernateStatistikEntry("ConnectCount", s.getConnectCount()));

      result.add(new HibernateStatistikEntry("SecondLevelCacheHitCount", s.getSecondLevelCacheHitCount()));

      result.add(new HibernateStatistikEntry("SecondLevelCacheMissCount", s.getSecondLevelCacheMissCount()));

      result.add(new HibernateStatistikEntry("SecondLevelCachePutCount", s.getSecondLevelCachePutCount()));

      result.add(new HibernateStatistikEntry("SessionCloseCount", s.getSessionCloseCount()));

      result.add(new HibernateStatistikEntry("SessionOpenCount", s.getSessionOpenCount()));

      result.add(new HibernateStatistikEntry("CollectionLoadCount", s.getCollectionLoadCount()));

      result.add(new HibernateStatistikEntry("CollectionFetchCount", s.getCollectionFetchCount()));

      result.add(new HibernateStatistikEntry("CollectionUpdateCount", s.getCollectionUpdateCount()));

      result.add(new HibernateStatistikEntry("CollectionRemoveCount", s.getCollectionRemoveCount()));

      result.add(new HibernateStatistikEntry("CollectionRecreateCount", s.getCollectionRecreateCount()));

      result.add(new HibernateStatistikEntry("StartTime", s.getStartTime()));

      result.add(new HibernateStatistikEntry("SuccessfulTransactionCount", s.getSuccessfulTransactionCount()));

      result.add(new HibernateStatistikEntry("TransactionCount", s.getTransactionCount()));

      result.add(new HibernateStatistikEntry("PrepareStatementCount", s.getPrepareStatementCount()));

      result.add(new HibernateStatistikEntry("CloseStatementCount", s.getCloseStatementCount()));

      result.add(new HibernateStatistikEntry("OptimisticFailureCount", s.getOptimisticFailureCount()));

    }

    return result;

  }

  private static class EmptySecondLevelCacheStatistics implements SecondLevelCacheStatistics {
    private static final long serialVersionUID = 1L;

    private final long hitCount;

    private final long missCount;

    private final long putCount;

    public EmptySecondLevelCacheStatistics(long hitCount, long missCount, long putCount) {
      this.hitCount = hitCount;
      this.missCount = missCount;
      this.putCount = putCount;
    }

    @Override
    public long getHitCount() {
      return hitCount;
    }

    @Override
    public long getMissCount() {
      return missCount;
    }

    @Override
    public String getRegionName() {
      return "";
    }

    @Override
    public long getPutCount() {
      return putCount;
    }

    @Override
    public long getElementCountInMemory() {
      return 0;
    }

    @Override
    public long getElementCountOnDisk() {
      return 0;
    }

    @Override
    public long getSizeInMemory() {
      return 0;
    }

    @Override
    public Map getEntries() {
      return Collections.emptyMap();
    }
  }
}
