package com.github.aesteve.vertx.nubes.routing;

import com.github.aesteve.vertx.nubes.Config;
import com.github.aesteve.vertx.nubes.annotations.Blocking;
import com.github.aesteve.vertx.nubes.handlers.Processor;
import com.github.aesteve.vertx.nubes.handlers.impl.DefaultMethodInvocationHandler;
import com.github.aesteve.vertx.nubes.handlers.impl.PayloadTypeProcessor;
import com.github.aesteve.vertx.nubes.reflections.Filter;
import io.vertx.core.Handler;
import io.vertx.core.Vertx;
import io.vertx.core.http.HttpMethod;
import io.vertx.ext.auth.AuthProvider;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.handler.*;
import io.vertx.ext.web.sstore.LocalSessionStore;

import java.lang.reflect.Method;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.BiConsumer;

public class MVCRoute {

  private final String path;
  private final HttpMethod httpMethod;
  private final Object instance;
  private final Set<Filter> beforeFilters;
  private final Set<Filter> afterFilters;
  private Method mainHandler;
  private final Set<Handler<RoutingContext>> handlers;
  private Set<Processor> processors;
  private MVCRoute redirectRoute;
  private final Handler<RoutingContext> authHandler;
  private String loginRedirect;
  private Handler<RoutingContext> preInterceptor;
  private Handler<RoutingContext> postInterceptor;
  private final Config config;
  private final boolean disabled;
  private BiConsumer<RoutingContext, ?> returnHandler;
  private final boolean usesSession;

  public MVCRoute(Object instance, String path, HttpMethod method, Config config, Handler<RoutingContext> authHandler, boolean disabled, final boolean usesSession) {
    this.instance = instance;
    this.config = config;
    this.path = path;
    this.httpMethod = method;
    this.beforeFilters = new TreeSet<>();
    this.afterFilters = new TreeSet<>();
    this.handlers = new LinkedHashSet<>();
    this.processors = new LinkedHashSet<>();
    this.authHandler = authHandler;
    this.disabled = disabled;
    this.usesSession = usesSession;
  }

  public boolean isEnabled() {
    return !disabled;
  }

  public void redirectTo(MVCRoute anotherRoute) {
    redirectRoute = anotherRoute;
  }

  public void setLoginRedirect(String loginRedirect) {
    this.loginRedirect = loginRedirect;
  }

  public void addProcessor(Processor processor) {
    processors.add(processor);
  }

  public void addProcessors(Set<Processor> processors) {
    this.processors.addAll(processors);
  }

  public void addProcessorsFirst(Set<Processor> processors) {
    Set<Processor> oldProcessors = new LinkedHashSet<>(this.processors);
    this.processors = new LinkedHashSet<>(oldProcessors.size() + processors.size());
    this.processors.addAll(processors);
    this.processors.addAll(oldProcessors);
  }

  public void attachInterceptor(Handler<RoutingContext> handler, boolean before) {
    if (before) {
      this.preInterceptor = handler;
    } else {
      this.postInterceptor = handler;
    }
  }

  public void attachReturnHandler(BiConsumer<RoutingContext, ?> handler) {
    returnHandler = handler;
  }

  public void attachHandlers(Set<Handler<RoutingContext>> newHandlers) {
    handlers.addAll(newHandlers);
  }

  public void setMainHandler(Method mainHandler) {
    this.mainHandler = mainHandler;
  }

  public void addBeforeFilters(Set<Filter> beforeFilters) {
    this.beforeFilters.addAll(beforeFilters);
  }

  public void addAfterFilters(Set<Filter> afterFilters) {
    this.afterFilters.addAll(afterFilters);
  }

  public String path() {
    return path;
  }

  public HttpMethod method() {
    return httpMethod;
  }

  public void attachHandlersToRouter(Router router) {
    config.forEachGlobalHandler(handler -> router.route(httpMethod, path).handler(handler));
    final Vertx vertx = config.getVertx();
    if (authHandler != null) {
      attachAuthHandler(router, vertx);
    } else if (usesSession) {
      router.route(httpMethod, path).handler(SessionHandler.create(LocalSessionStore.create(vertx)));
    }
    handlers.forEach(handler ->
      router.route(httpMethod, path).handler(handler)
    );
    attachPreProcessingHandlers(router);
    boolean hasPostProcessors = redirectRoute != null || postInterceptor != null || !afterFilters.isEmpty()|| !processors.isEmpty();
    setHandler(router, mainHandler, hasPostProcessors);
    if (redirectRoute != null) {
      // intercepted -> redirected => do not call post processing handlers
      router.route(httpMethod, path).handler(ctx ->
        ctx.reroute(redirectRoute.method(), redirectRoute.path())
      );
    }
    attachPostProcessingHandlers(router);
  }

  private void attachPreProcessingHandlers(Router router) {
    processors.forEach(processor -> router.route(httpMethod, path).handler(processor::preHandle));
    int i = 0;
    boolean beforeFiltersHaveNext = mainHandler != null;
    for (Filter filter : beforeFilters) {
      boolean hasNext = beforeFiltersHaveNext || i < beforeFilters.size() - 1;
      setHandler(router, filter.method(), hasNext);
      i++;
    }
    if (preInterceptor != null) {
      router.route(httpMethod, path).handler(preInterceptor);
    }
  }

  private void attachPostProcessingHandlers(Router router) {
    if (postInterceptor != null) {
      router.route(httpMethod, path).handler(postInterceptor);
    }
    int i = 0;
    boolean afterFiltersHaveNext = !processors.isEmpty();
    for (Filter filter : afterFilters) {
      boolean hasNext = afterFiltersHaveNext || i < afterFilters.size() - 1;
      setHandler(router, filter.method(), hasNext);
      i++;
    }
    if (!mainHandler.getReturnType().equals(Void.TYPE) && returnHandler == null) { // try to set as payload
      processors.add(new PayloadTypeProcessor(config.getMarshallers()));
    }
    processors.forEach(processor -> router.route(httpMethod, path).handler(processor::postHandle));
    processors.forEach(processor -> router.route(httpMethod, path).handler(processor::afterAll));

  }

  private void attachAuthHandler(Router router, Vertx vertx) {
    final AuthProvider authProvider = config.getAuthProvider();
    router.route(httpMethod, path).handler(CookieHandler.create());
    router.route(httpMethod, path).handler(UserSessionHandler.create(authProvider));
    router.route(httpMethod, path).handler(SessionHandler.create(LocalSessionStore.create(vertx)));
    router.route(httpMethod, path).handler(authHandler);
    if (loginRedirect != null && !"".equals(loginRedirect)) {
      router.post(loginRedirect).handler(CookieHandler.create());
      router.post(loginRedirect).handler(BodyHandler.create());
      router.post(loginRedirect).handler(UserSessionHandler.create(authProvider));
      router.post(loginRedirect).handler(SessionHandler.create(LocalSessionStore.create(vertx)));
      router.post(loginRedirect).handler(FormLoginHandler.create(authProvider));
    }
  }

  private void setHandler(Router router, Method method, boolean hasNext) {
    Handler<RoutingContext> handler = new DefaultMethodInvocationHandler<>(instance, method, config, hasNext, returnHandler);
    if (method.isAnnotationPresent(Blocking.class)) {
      router.route(httpMethod, path).blockingHandler(handler);
    } else {
      router.route(httpMethod, path).handler(handler);
    }
  }

  @Override
  public String toString() {
    return "Route : " + httpMethod.toString() + " " + path();
  }
}
