package com.github.aesteve.vertx.nubes;

import com.github.aesteve.vertx.nubes.context.RateLimit;
import com.github.aesteve.vertx.nubes.handlers.AnnotationProcessor;
import com.github.aesteve.vertx.nubes.handlers.AnnotationProcessorRegistry;
import com.github.aesteve.vertx.nubes.handlers.Processor;
import com.github.aesteve.vertx.nubes.marshallers.PayloadMarshaller;
import com.github.aesteve.vertx.nubes.reflections.adapters.ParameterAdapterRegistry;
import com.github.aesteve.vertx.nubes.reflections.annotations.IAnnotationProvider;
import com.github.aesteve.vertx.nubes.reflections.factories.AnnotationProcessorFactory;
import com.github.aesteve.vertx.nubes.reflections.injectors.annot.AnnotatedParamInjector;
import com.github.aesteve.vertx.nubes.reflections.injectors.annot.AnnotatedParamInjectorRegistry;
import com.github.aesteve.vertx.nubes.reflections.injectors.typed.ParamInjector;
import com.github.aesteve.vertx.nubes.reflections.injectors.typed.TypedParamInjectorRegistry;
import com.github.aesteve.vertx.nubes.services.ServiceRegistry;
import io.vertx.core.Handler;
import io.vertx.core.Vertx;
import io.vertx.core.VertxException;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.core.logging.Logger;
import io.vertx.core.logging.LoggerFactory;
import io.vertx.ext.auth.AuthProvider;
import io.vertx.ext.auth.jdbc.JDBCAuth;
import io.vertx.ext.auth.jwt.JWTAuth;
import io.vertx.ext.auth.shiro.ShiroAuth;
import io.vertx.ext.auth.shiro.ShiroAuthOptions;
import io.vertx.ext.jdbc.JDBCClient;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.handler.BodyHandler;
import io.vertx.ext.web.handler.sockjs.SockJSHandlerOptions;
import io.vertx.ext.web.templ.*;

import java.lang.annotation.Annotation;
import java.util.*;
import java.util.concurrent.TimeUnit;

public class Config {

  private static final Logger LOG = LoggerFactory.getLogger(Config.class);
  private final Map<Locale, ResourceBundle> bundlesByLocale;
  private final List<Handler<RoutingContext>> globalHandlers;
  private final Map<String, TemplateEngine> templateEngines;
  private final SockJSHandlerOptions sockJSOptions;
  private JsonObject json;
  private List<String> controllerPackages;
  private List<String> fixturePackages;
  private String reflectionProvider;
  private String verticlePackage;
  private String domainPackage;
  private RateLimit rateLimit;
  private String webroot;
  private String assetsPath;
  private String tplDir;
  private boolean displayErrors;
  private Vertx vertx;
  private AuthProvider authProvider;
  private String i18nDir;
  private AnnotationProcessorRegistry apRegistry;
  private Map<Class<? extends Annotation>, Set<Handler<RoutingContext>>> annotationHandlers;
  private Map<Class<?>, Processor> typeProcessors;
  private TypedParamInjectorRegistry typeInjectors;
  private AnnotatedParamInjectorRegistry annotInjectors;
  private ServiceRegistry serviceRegistry;
  private Map<Class<?>, Handler<RoutingContext>> paramHandlers;
  private Map<String, Handler<RoutingContext>> aopHandlerRegistry;
  private Map<String, PayloadMarshaller> marshallers;

  private Config() {
    bundlesByLocale = new HashMap<>();
    globalHandlers = new ArrayList<>();
    templateEngines = new HashMap<>();
    sockJSOptions = new SockJSHandlerOptions();
    marshallers = new HashMap<>();
    annotationHandlers = new HashMap<>();
    paramHandlers = new HashMap<>();
    typeProcessors = new HashMap<>();
    apRegistry = new AnnotationProcessorRegistry();
    typeInjectors = new TypedParamInjectorRegistry(this);
    aopHandlerRegistry = new HashMap<>();
  }

  /**
   * TODO : we should be consistent on single/multiple values
   * (controllers is an array, fixtures is a list, domain is a single value, verticle is a single value) : this is wrong
   *
   * @param json JsonObject describing the config
   * @return config a type safe config object
   */
  public static Config fromJsonObject(JsonObject json, Vertx vertx) {
    Config instance = new Config();
    instance.json = json;
    instance.vertx = vertx;

    instance.readPackages();
    // Register services included in config
    instance.createServices();
    // Register templateEngines for extensions added in config
    instance.createTemplateEngines();

    instance.createRateLimit();

    instance.createAuthHandlers();

    instance.webroot = json.getString("webroot", "web/assets");
    instance.assetsPath = json.getString("static-path", "/assets");
    instance.tplDir = json.getString("views-dir", "web/views");
    instance.displayErrors = json.getBoolean("display-errors", Boolean.FALSE);
    // TODO : read sockJSOptions from config

    instance.reflectionProvider = json.getString("relectionprovider", "reflections");
    instance.globalHandlers.add(BodyHandler.create());
    return instance;
  }

  private void createAuthHandlers() {
    String auth = json.getString("auth-type");
    JsonObject authProperties = json.getJsonObject("auth-properties");

    // TODO : discuss it. I'm really not convinced about all the boilerplate needed in config (dbName only for JDBC, etc.)
    if (authProperties != null) {
      // For now, only JWT,Shiro and JDBC supported (same as for Vert.x web)
      switch (auth) {
        case "JWT":// For now only allow properties realm
          this.authProvider = JWTAuth.create(vertx, authProperties);
          break;
        case "Shiro":
          ShiroAuth.create(vertx, new ShiroAuthOptions(authProperties));
          break;
        case "JDBC":
          String dbName = json.getString("db-name");
          Objects.requireNonNull(dbName);
          JDBCClient client = JDBCClient.createShared(vertx, authProperties, dbName);
          this.authProvider = JDBCAuth.create(vertx, client);
          break;
        default:
          LOG.warn("Unknown type of auth : " + auth + " . Ignoring.");
      }
    } else if (auth != null) {
      LOG.warn("You have defined " + auth + " as auth type, but didn't provide any configuration, can't create authProvider");
    }

  }

  private void createRateLimit() {
    JsonObject rateLimitJson = json.getJsonObject("throttling");
    if (rateLimitJson != null) {
      int count = rateLimitJson.getInteger("count");
      int value = rateLimitJson.getInteger("time-frame");
      TimeUnit timeUnit = TimeUnit.valueOf(rateLimitJson.getString("time-unit"));
      this.rateLimit = new RateLimit(count, value, timeUnit);
    }
  }

  private void createTemplateEngines() {
    JsonArray templates = json.getJsonArray("templates", new JsonArray());
    if (templates.contains("hbs")) {
      this.templateEngines.put("hbs", HandlebarsTemplateEngine.create());
    }
    if (templates.contains("jade")) {
      this.templateEngines.put("jade", JadeTemplateEngine.create());
    }
    if (templates.contains("mvel")) {
      this.templateEngines.put("templ", MVELTemplateEngine.create());
    }
    if (templates.contains("thymeleaf")) {
      this.templateEngines.put("html", ThymeleafTemplateEngine.create());
    }

  }

  private void createServices() {
    JsonObject services = json.getJsonObject("services", new JsonObject());
    this.serviceRegistry = new ServiceRegistry(vertx, this);
    services.forEach(entry -> {
      String name = entry.getKey();
      String className = (String) entry.getValue();
      try {
        Class<?> clazz = Class.forName(className);
        this.serviceRegistry.registerService(name, clazz.newInstance());
      } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
        throw new VertxException(e);
      }
    });
  }

  @SuppressWarnings("unchecked")
  private void readPackages() {
    this.i18nDir = json.getString("i18nDir", "web/i18n/");
    if (!this.i18nDir.endsWith("/")) {
      this.i18nDir = this.i18nDir + "/";
    }
    String srcPackage = json.getString("src-package");
    JsonArray controllers = json.getJsonArray("controller-packages");
    if (controllers == null) {
      controllers = new JsonArray();
      if (srcPackage != null) {
        controllers.add(srcPackage + ".controllers");
      }
    }
    this.controllerPackages = controllers.getList();

    this.verticlePackage = json.getString("verticle-package");
    if (this.verticlePackage == null && srcPackage != null) {
      this.verticlePackage = srcPackage + ".verticles";
    }

    this.domainPackage = json.getString("domain-package");
    if (this.domainPackage == null && srcPackage != null) {
      this.domainPackage = srcPackage + ".domains";
    }
    JsonArray fixtures = json.getJsonArray("fixture-packages");
    if (fixtures == null) {
      fixtures = new JsonArray();
      if (srcPackage != null) {
        fixtures.add(srcPackage + ".fixtures");
      }
    }
    this.fixturePackages = fixtures.getList();
  }

  public ResourceBundle getResourceBundle(Locale loc) {
    return bundlesByLocale.get(loc);
  }

  public JsonObject json() {
    return json;
  }

  public List<String> getFixturePackages() {
    return fixturePackages;
  }

  public TypedParamInjectorRegistry getTypeInjectors() {
    return typeInjectors;
  }

  public AnnotatedParamInjectorRegistry getAnnotatedInjectors() {
    return annotInjectors;
  }

  public boolean isDisplayErrors() {
    return displayErrors;
  }

  public Map<String, TemplateEngine> getTemplateEngines() {
    return templateEngines;
  }

  public String getTplDir() {
    return tplDir;
  }

  public RateLimit getRateLimit() {
    return rateLimit;
  }

  void createAnnotInjectors(ParameterAdapterRegistry registry) {
    annotInjectors = new AnnotatedParamInjectorRegistry(marshallers, registry);
  }

  public String getReflectionProvider() {
    return reflectionProvider;
  }

  public String getDomainPackage() {
    return domainPackage;
  }

  public ServiceRegistry getServiceRegistry() {
    return serviceRegistry;
  }

  void registerTemplateEngine(String extension, TemplateEngine engine) {
    templateEngines.put(extension, engine);
  }

  void registerInterceptor(String name, Handler<RoutingContext> handler) {
    aopHandlerRegistry.put(name, handler);
  }

  void registerService(String name, Object service) {
    serviceRegistry.registerService(name, service);
  }

  Object getService(String name) {
    return serviceRegistry.get(name);
  }

  void registerParamHandler(Class<?> parameterClass, Handler<RoutingContext> handler) {
    paramHandlers.put(parameterClass, handler);
  }

  public Set<Handler<RoutingContext>> getAnnotationHandler(Class<? extends Annotation> annotation) {
    return annotationHandlers.get(annotation);
  }

  void registerAnnotationHandler(Class<? extends Annotation> annotation, Set<Handler<RoutingContext>> handlers) {
    annotationHandlers.put(annotation, handlers);
  }

  void registerTypeProcessor(Class<?> type, Processor processor) {
    typeProcessors.put(type, processor);
  }

  <T extends Annotation> void registerAnnotationProcessor(Class<T> annotation, AnnotationProcessorFactory<T> processor) {
    apRegistry.registerProcessor(annotation, processor);
  }

  <T extends Annotation> void registerAnnotationProcessor(Class<T> annotation, AnnotationProcessor<T> processor) {
    apRegistry.registerProcessor(annotation, processor);
  }

  <T> void registerInjector(Class<? extends T> clazz, ParamInjector<T> injector) {
    typeInjectors.registerInjector(clazz, injector);
  }

  <T extends Annotation> void registerInjector(Class<? extends T> clazz, AnnotatedParamInjector<T> injector) {
    annotInjectors.registerInjector(clazz, injector);
  }

  void addHandler(Handler<RoutingContext> handler) {
    globalHandlers.add(handler);
  }

  String getWebroot() {
    return webroot;
  }

  String getAssetsPath() {
    return assetsPath;
  }

  public AuthProvider getAuthProvider() {
    return authProvider;
  }

  void setAuthProvider(AuthProvider authProvider) {
    this.authProvider = authProvider;
  }

  String getI18nDir() {
    return i18nDir;
  }

  void createBundle(Locale loc, ResourceBundle bundle) {
    bundlesByLocale.put(loc, bundle);
  }

  public String getVerticlePackage() {
    return verticlePackage;
  }

  public void forEachControllerPackage(Handler<? super String> consumer) {
    controllerPackages.forEach(consumer::handle);
  }

  public Vertx getVertx() {
    return vertx;
  }

  public SockJSHandlerOptions getSockJSOptions() {
    return sockJSOptions;
  }

  public Processor getTypeProcessor(Class<?> parameterClass) {
    return typeProcessors.get(parameterClass);
  }

  public Handler<RoutingContext> getParamHandler(Class<?> parameterClass) {
    return paramHandlers.get(parameterClass);
  }

  public<T extends Annotation> AnnotationProcessor<T> getAnnotationProcessor(T methodAnnotation) {
    return apRegistry.getProcessor(methodAnnotation);
  }

  public Handler<RoutingContext> getAopHandler(String name) {
    return aopHandlerRegistry.get(name);
  }

  public void forEachGlobalHandler(Handler<Handler<RoutingContext>> handler) {
    globalHandlers.forEach(handler::handle);
  }

  public Map<String, PayloadMarshaller> getMarshallers() {
    return marshallers;
  }

  public void setMarshallers(Map<String, PayloadMarshaller> marshallers) {
    this.marshallers = marshallers;
  }

  public List<String> getControllerPackages() {
    return controllerPackages;
  }

}