package com.github.aesteve.vertx.nubes.reflections.visitors;

import com.github.aesteve.vertx.nubes.Config;
import com.github.aesteve.vertx.nubes.annotations.auth.Auth;
import com.github.aesteve.vertx.nubes.annotations.filters.After;
import com.github.aesteve.vertx.nubes.annotations.filters.Before;
import com.github.aesteve.vertx.nubes.annotations.routing.Disabled;
import com.github.aesteve.vertx.nubes.annotations.routing.Forward;
import com.github.aesteve.vertx.nubes.auth.AuthMethod;
import com.github.aesteve.vertx.nubes.handlers.AnnotationProcessor;
import com.github.aesteve.vertx.nubes.handlers.Processor;
import com.github.aesteve.vertx.nubes.reflections.RouteRegistry;
import com.github.aesteve.vertx.nubes.reflections.factories.AuthenticationFactory;
import com.github.aesteve.vertx.nubes.routing.HttpMethodFactory;
import com.github.aesteve.vertx.nubes.routing.MVCRoute;
import io.vertx.core.Handler;
import io.vertx.core.http.HttpMethod;
import io.vertx.core.logging.Logger;
import io.vertx.core.logging.LoggerFactory;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.Session;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.*;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;

class MethodVisitor<T> {

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

  private final Class<T> controller;
  private final Method method;
  private final Config config;
  private final T instance;
  private final String basePath;
  private final Auth auth;
  private final AuthenticationFactory authFactory;
  private final RouteRegistry routeRegistry;
  private final Map<Class<? extends Annotation>, BiConsumer<RoutingContext, ?>> returnHandlers;
  private final Set<Processor> processors;
  private final Set<Handler<RoutingContext>> paramsHandlers;
  private final List<MVCRoute> routes;
  private boolean usesSession;


  MethodVisitor(ControllerVisitor<T> parent, Method method) {
    this.method = method;
    controller = parent.clazz;
    config = parent.config;
    instance = parent.instance;
    basePath = parent.basePath;
    auth = method.getAnnotation(Auth.class) == null ? controller.getAnnotation(Auth.class) : method.getAnnotation(Auth.class);
    authFactory = parent.authFactory;
    routeRegistry = parent.routeRegistry;
    returnHandlers = parent.returnHandlers;
    processors = parent.processors;
    paramsHandlers = new LinkedHashSet<>();
    routes = new ArrayList<>();
  }

  List<MVCRoute> visit() {
    if (!HttpMethodFactory.isRouteMethod(method)) {
      return routes;
    }
    createParamsHandlers();
    Map<HttpMethod, String> httpMethods = HttpMethodFactory.fromAnnotatedMethod(method);
    routes.addAll(httpMethods.entrySet().stream().map(this::createHandlers).collect(Collectors.toList()));
    return routes;
  }

  private MVCRoute createHandlers(Map.Entry<HttpMethod, String> entry) {
    HttpMethod httpMethod = entry.getKey();
    String path = entry.getValue();
    MVCRoute route = createRoute(httpMethod, path);
    handleMethodAnnotations(route);
    createAopProcessors(route);
    route.addProcessors(processors);
    route.attachHandlers(paramsHandlers);
    route.setMainHandler(method);
    routeRegistry.register(controller, method, route);
    if (method.isAnnotationPresent(Forward.class)) {
      Forward redirect = method.getAnnotation(Forward.class);
      routeRegistry.bindRedirect(route, redirect);
    }
    return route;
  }

  private void createAopProcessors(MVCRoute route) {
    Before before = method.getAnnotation(Before.class);
    After after = method.getAnnotation(After.class);
    if (before != null) {
      Handler<RoutingContext> beforeHandler = config.getAopHandler(before.name());
      if (beforeHandler == null) {
        LOG.warn("The interceptor with name" + (before.name()) + " could not be found");
      } else {
        route.attachInterceptor(beforeHandler, true);
      }
    }
    if (after != null) {
      Handler<RoutingContext> afterHandler = config.getAopHandler(after.name());
      if (afterHandler == null) {
        LOG.warn("The interceptor with name" + (after.name()) + " could not be found");
      } else {
        route.attachInterceptor(afterHandler, false);
      }
    }
  }

  private void handleMethodAnnotations(MVCRoute route) {
    for (Annotation methodAnnotation : method.getDeclaredAnnotations()) {
      Class<? extends Annotation> annotClass = methodAnnotation.annotationType();
      Set<Handler<RoutingContext>> handler = config.getAnnotationHandler(annotClass);
      if (handler != null) {
        route.attachHandlers(handler);
      }
      AnnotationProcessor<?> annProcessor = config.getAnnotationProcessor(methodAnnotation);
      if (annProcessor != null) {
        route.addProcessor(annProcessor);
      }
      BiConsumer<RoutingContext, ?> returnHandler = returnHandlers.get(annotClass);
      if (returnHandler != null) {
        route.attachReturnHandler(returnHandler);
      }
    }
  }

  private MVCRoute createRoute(HttpMethod httpMethod, String path) {
    Handler<RoutingContext> authHandler = null;
    String redirectURL = null;
    if (auth != null) {
      authHandler = authFactory.create(auth);
      if (AuthMethod.REDIRECT.equals(auth.method())) {
        redirectURL = auth.redirectURL();
      }
    }
    boolean disabled = method.isAnnotationPresent(Disabled.class) || controller.isAnnotationPresent(Disabled.class);
    MVCRoute route = new MVCRoute(instance, basePath + path, httpMethod, config, authHandler, disabled, usesSession);
    route.setLoginRedirect(redirectURL);
    return route;
  }

  private void createParamsHandlers() {
    for (Parameter p : method.getParameters()) {
      Class<?> parameterClass = p.getType();
      if (Session.class.isAssignableFrom(parameterClass)) {
        usesSession = true;
      }
      Processor typeProcessor = config.getTypeProcessor(parameterClass);
      if (typeProcessor != null) {
        processors.add(typeProcessor);
      }
      Handler<RoutingContext> handler = config.getParamHandler(parameterClass);
      if (handler != null) {
        paramsHandlers.add(handler);
      }
      createParamAnnotationHandlers(p);
    }
  }

  private void createParamAnnotationHandlers(Parameter p) {
    Annotation[] paramAnnotations = p.getAnnotations();
    if (paramAnnotations != null) {
      for (Annotation annotation : paramAnnotations) {
        Set<Handler<RoutingContext>> paramHandler = config.getAnnotationHandler(annotation.annotationType());
        if (paramHandler != null) {
          paramsHandlers.addAll(paramHandler);
        }
      }
    }
  }

}
