
package de.ivu.elect.jee.exception;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;

import javax.annotation.Nullable;
import javax.faces.application.ViewExpiredException;
import javax.faces.context.ExceptionHandler;
import javax.faces.context.ExceptionHandlerWrapper;
import javax.faces.context.FacesContext;
import javax.faces.event.AbortProcessingException;
import javax.faces.event.ExceptionQueuedEvent;
import javax.faces.event.ExceptionQueuedEventContext;

import org.apache.commons.lang3.StringUtils;
import org.primefaces.PrimeFaces;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.ivu.elect.jee.cdi.BeanManagerUtil;
import de.ivu.elect.jee.exception.ElectCommonException.RedirectDestination;
import de.ivu.elect.jee.faces.FacesContextUtil;
import de.ivu.elect.jee.faces.FacesMessageUtil;
import de.ivu.elect.jee.faces.Messages;
import de.ivu.elect.jee.presentation.security.StartViewResolver;


import static java.util.Arrays.asList;

public class ElectCommonExceptionHandler extends ExceptionHandlerWrapper {

  static Logger LOG = LoggerFactory.getLogger(ElectCommonExceptionHandler.class);

  public static final AtomicBoolean PREVENT_ERROR_PAGE = new AtomicBoolean(false);

  public static final Set<Class> SPECIAL_EXCEPTION_CLASSES = new HashSet<>(asList(ElectCommonException.class, ViewExpiredException.class));

  private final ExceptionHandler wrapped;

  public ElectCommonExceptionHandler(ExceptionHandler wrapped) {
    super();
    this.wrapped = wrapped;
  }

  @Override
  public ExceptionHandler getWrapped() {
    return this.wrapped;
  }

  @Override
  public void handle() {
    Iterator<ExceptionQueuedEvent> unhandledExceptionQueuedEvents = getUnhandledExceptionQueuedEvents().iterator();
    while (unhandledExceptionQueuedEvents.hasNext()) {
      Throwable t = getThrowable(unhandledExceptionQueuedEvents);
      Throwable rootCauseOfThrowable = getRootCauseOfThrowable(t);

      RedirectDestination redirectTo = RedirectDestination.NONE;
      if (ExceptionUtil.isDatabaseLockException(t)) {
        logUnhandledException(t);
        addErrorMessage("error.page.fehler.prolog.retry");
      } else if (rootCauseOfThrowable instanceof ElectCommonException) {
        logUnhandledException((ElectCommonException) rootCauseOfThrowable);
        ElectCommonException electCommonException = (ElectCommonException) rootCauseOfThrowable;
        handleElectCommonException(unhandledExceptionQueuedEvents, electCommonException);
        redirectTo = electCommonException.getRedirectDestination();
      } else if (t instanceof ViewExpiredException) {
        unhandledExceptionQueuedEvents.remove();
        redirectTo = RedirectDestination.LOGIN_VIEW;
      } else if (FacesContextUtil.isAjaxRequest()) {
        // Faces "verschluckt" AJAX-Request gerne ohne Meldung im Log und Anzeige in der UI. Deshalb hier: Loggen!
        if ("null source".equals(t.getMessage())) {
          // ELECTMUC-540 es sei denn, es ist diese unsägliche "null source" Meldung
          redirectTo = RedirectDestination.LOGIN_VIEW;
        } else {
          logUnhandledException(t);
          handleNotElectCommonException(t, rootCauseOfThrowable);
          // ELECTDD-688 redirectTo = RedirectDestination.ERROR_VIEW;
          try {
            PrimeFaces.current().ajax().update(FacesMessageUtil.GLOBAL_MESSAGES_ID);
          } catch (Exception i) {
            // ignore
            LOG.warn("Cannont update Messages: " + i.getMessage());
          }
        }
        unhandledExceptionQueuedEvents.remove();
      }

      preventErrorPage(redirectTo, PREVENT_ERROR_PAGE.get()).redirect();

      getWrapped().handle();
    }
  }

  private RedirectDestination preventErrorPage(RedirectDestination redirectTo, boolean preventErrorPage) {
    if (preventErrorPage && redirectTo.isErrorPage()) {
      return RedirectDestination.START_VIEW;
    }

    return redirectTo;
  }

  private Throwable getRootCauseOfThrowable(Throwable t) {
    Throwable t2 = t;
    while (t2.getCause() != null) {
      for (Class specialExceptionClass : SPECIAL_EXCEPTION_CLASSES) {
        if (specialExceptionClass.isInstance(t2)) {
          return t2;
        }
      }
      t2 = t2.getCause();
    }
    return t2;
  }

  protected static void logUnhandledException(ElectCommonException e) {
    e.getLogLevel().log(LOG, e);
  }

  protected static void logUnhandledException(Throwable throwable) {
    if (isSecurityException(throwable)) {
      // Sonderwunsch aus MUC ELECTMUC-221 nicht auf ERROR loggen
      LOG.warn("sec - " + throwable.getMessage());
    } else if (throwable != null) {
      // Sonderwunsch aus MUC ELECTMUC-221 nicht auf ERROR loggen
      LOG.warn("nonsec - " + throwable.getMessage(), throwable);
    }
  }

  private static boolean isSecurityException(Throwable throwable) {
    if (throwable == null) {
      return false;
    }
    if (throwable instanceof ElectCommonException) {
      if (((ElectCommonException) throwable).isSecurityException()) {
        return true;
      }
    }
    return isSecurityException(throwable.getCause());
  }

  protected void handleElectCommonException(Iterator<ExceptionQueuedEvent> i, ElectCommonException ecex) {
    try {
      addErrorMessage(ecex.getMessageKey(), ecex.getObjects());
    } finally {
      i.remove();
    }
  }

  protected void handleNotElectCommonException(Throwable t, Throwable root) {
    addErrorMessage("exception.unknown.message", t.getClass().getName(), t.getMessage(), root.getClass().getName(), root.getMessage(),
        getReadableMessage(t.getMessage(), root.getMessage()));
  }

  private String getReadableMessage(String message, String rootMessage) {
    if (StringUtils.isNotBlank(rootMessage) && !"null".equalsIgnoreCase(rootMessage)) {
      return rootMessage;
    }

    return message;
  }

  @Deprecated
  public static void handleException(Throwable e, Messages messages) {
    handleException(e);
  }

  public static void handleException(Throwable e) {

    if (e instanceof ElectCommonException) {
      ElectCommonException electCommonException = (ElectCommonException) e;
      addErrorMessage(electCommonException.getMessageKey(), electCommonException.getObjects());
      RedirectDestination redirectTo = electCommonException.getRedirectDestination();
      if (RedirectDestination.ERROR_VIEW.equals(redirectTo)) {
        FacesContextUtil.redirectToView("/error.xhtml"); // direkt zur ViewId nicht über outcome, da das bei Actions nicht funktioniert
      } else if (RedirectDestination.LOGIN_VIEW.equals(redirectTo)) {
        FacesContextUtil.redirectToLoginView();
      } else if (RedirectDestination.START_VIEW.equals(redirectTo)) {
        try {
          FacesContextUtil.redirectToView(BeanManagerUtil.getBeanByClass(StartViewResolver.class).getStartViewId());
        } catch (Exception ex) {
          // ignore: Wenn etwas beim Bestimmen des Startviews schief geht...
          FacesContextUtil.redirectToView(FacesContextUtil.getCurrentViewId());
        }
      } else {
        FacesContextUtil.redirectToView(FacesContextUtil.getCurrentViewId());
      }
    } else if (e.getCause() != null) {
      handleException(e.getCause());
    } else {
      logUnhandledException(e);
      addErrorMessage("message.unknown.error", e.getLocalizedMessage(), e.getClass().getName(), erzeugeStacktrace(e.getStackTrace()));
      FacesContextUtil.redirectToErrorView();
    }
  }

  private static void addErrorMessage(String messageKey, Object... objects) {
    try {
      LOG.warn("Fehler: messageKey=" + messageKey + ", " + Arrays.toString(objects));
    } catch (Exception ignore) {
      //
    }
    try {
      FacesMessageUtil.addErrorMessage(FacesContext.getCurrentInstance(), messageKey, messageKey, objects);
    } catch (Exception iggnore) {
      try {
        BeanManagerUtil.getBeanByClass(Messages.class).addErrorMessage(messageKey, objects);
      } catch (Exception ignore2) {
        //
      }
    }
  }

  @Nullable
  private static String erzeugeStacktrace(StackTraceElement[] stackTrace) {
    if (stackTrace == null || stackTrace.length == 0) {
      return null;
    }

    try {
      return Arrays.stream(stackTrace).map(stackTraceElement -> '\n' + stackTraceElement.toString()).collect(Collectors.joining());
    } catch (Exception e) {
      // nichts tun, egal, wenn hier was schief läuft
    }
    return "-";
  }

  protected void logException(ElectCommonException ex) {
    if (isSecurityException(ex)) {
      LOG.warn(ex.getMessage());
    } else {
      if (ex != null) {
        LOG.warn(ex.getClass().toString(), ex);
      }
    }
  }

  protected Throwable getThrowable(Iterator<ExceptionQueuedEvent> i) {
    ExceptionQueuedEvent event = i.next();
    ExceptionQueuedEventContext context = (ExceptionQueuedEventContext) event.getSource();

    Throwable t = context.getException();
    if (t instanceof AbortProcessingException) {
      Throwable cause = t.getCause();
      if (cause != null) {
        t = cause;
      }
    }
    return t;
  }

}
