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

import com.github.aesteve.vertx.nubes.Config;
import com.github.aesteve.vertx.nubes.annotations.services.Consumer;
import com.github.aesteve.vertx.nubes.annotations.services.PeriodicTask;
import com.github.aesteve.vertx.nubes.annotations.services.Proxify;
import com.github.aesteve.vertx.nubes.annotations.services.ServiceProxy;
import com.github.aesteve.vertx.nubes.utils.async.MultipleFutures;
import io.vertx.codegen.annotations.ProxyGen;
import io.vertx.core.Future;
import io.vertx.core.Vertx;
import io.vertx.core.VertxException;
import io.vertx.core.eventbus.Message;
import io.vertx.core.logging.Logger;
import io.vertx.core.logging.LoggerFactory;
import io.vertx.serviceproxy.ProxyHelper;
import io.vertx.serviceproxy.ServiceBinder;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;

public class ServiceRegistry {

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

  private final Map<String, Object> services;
  private final Map<String, Object> serviceProxies;
  private final Set<Long> timerIds;
  private final Config config;

  private final Vertx vertx;

  public ServiceRegistry(Vertx vertx, Config config) {
    this.vertx = vertx;
    this.config = config;
    services = new HashMap<>();
    serviceProxies = new HashMap<>();
    timerIds = new HashSet<>();
  }

  public void registerService(String name, Object service) {
    services.put(name, service);
  }

  public Object get(String name) {
    return services.get(name);
  }

  public Object get(Field field) {
    com.github.aesteve.vertx.nubes.annotations.services.Service annot = field.getAnnotation(com.github.aesteve.vertx.nubes.annotations.services.Service.class);
    if (annot != null) {
      return get(annot.value());
    }
    ServiceProxy proxyAnnot = field.getAnnotation(ServiceProxy.class);
    if (proxyAnnot == null) {
      return null;
    }
    Class<?> serviceInterface = getInterface(field.getType());
    if (serviceInterface == null) {
      LOG.error("Could not inject service for : " + field.getName() + " could not find the matching proxified service using @ProxyGen");
      return null;
    }
    String address = proxyAnnot.value();
    if (serviceProxies.get(address) != null) {
      return serviceProxies.get(address);
    } else {
      Object service = createEbProxyClass(serviceInterface, address);
      serviceProxies.put(address, service);
      return service;
    }
  }

  public Collection<Object> services() {
    return services.values();
  }

  public boolean isEmpty() {
    return services.isEmpty();
  }

  public void startAll(Future<Void> future) {
    if (isEmpty()) {
      future.complete();
      return;
    }
    MultipleFutures<Void> futures = new MultipleFutures<>(future);
    futures.addAll(services(), obj -> {
      try {
        introspectService(obj);
      } catch (Exception e) {
        return res -> res.fail(e);
      }
      if (obj instanceof Service) {
        Service service = (Service) obj;
        service.init(vertx, config.json());
        return service::start;
      }
      return null;
    });
    futures.start();
  }

  public void stopAll(Future<Void> future) {
    if (isEmpty()) {
      future.complete();
      return;
    }
    timerIds.forEach(vertx::cancelTimer);
    MultipleFutures<Void> futures = new MultipleFutures<>(future);
    futures.addAll(services(), obj -> {
      if (obj instanceof Service) {
        Service service = (Service) obj;
        return service::stop;
      }
      return null;
    });
    futures.start();
  }

  private void introspectService(Object service) {
    Class<?> serviceClass = service.getClass();
    Proxify annot = serviceClass.getAnnotation(Proxify.class);
    if (annot != null) {
      createServiceProxy(annot.value(), service);
    }
    for (Method method : service.getClass().getMethods()) {
      PeriodicTask periodicTask = method.getAnnotation(PeriodicTask.class);
      if (periodicTask != null) {
        createPeriodicTask(service, periodicTask, method);
      }
      Consumer consumes = method.getAnnotation(Consumer.class);
      if (consumes != null) {
        createConsumer(service, consumes, method);
      }
    }
  }

  private void createConsumer(Object service, Consumer consumes, Method method) {
    Class<?>[] parameterTypes = method.getParameterTypes();
    if (parameterTypes.length != 1 || !parameterTypes[0].equals(Message.class)) {
      String msg = "Cannot register consumer on method : " + getFullName(service, method);
      msg += " .Method should only declare one parameter of io.vertx.core.eventbus.Message type.";
      throw new VertxException(msg);
    }
    vertx.eventBus().consumer(consumes.value(), message -> {
      try {
        method.invoke(service, message);
      } catch (Exception e) {
        LOG.error("Exception happened during message handling on method : " + getFullName(service, method), e);
      }
    });
  }

  private void createPeriodicTask(Object service, PeriodicTask annotation, Method method) {
    if (method.getParameterTypes().length > 0) {
      throw new VertxException("Periodic tasks should not have parameters");
    }
    vertx.setPeriodic(annotation.value(), timerId -> {
      timerIds.add(timerId);
      try {
        method.invoke(service);
      } catch (Exception e) {
        LOG.error("Error while running periodic task", e);
      }
    });
  }

  private static String getFullName(Object service, Method method) {
    return service.getClass().getName() + "." + method.getName();
  }

  private <T> void createServiceProxy(String address, T service) {
    Class<T> serviceClass = getInterface(service.getClass());
    if (serviceClass == null) {
      LOG.error("Could not find a @ProxyGen super interface for class : " + service.getClass().getName() + " cannot proxy it ver the eventBus");
      return;
    }
    new ServiceBinder(vertx)
            .setAddress(address)
            .register(serviceClass, service);
  }

  @SuppressWarnings("unchecked")
  public static <T> Class<T> getInterface(Class<?> serviceClass) {
    if (serviceClass.isAnnotationPresent(ProxyGen.class)) {
      return (Class<T>) serviceClass;
    }
    Class<?>[] interfaces = serviceClass.getInterfaces();
    for (Class<?> someInterface : interfaces) {
      if (someInterface.isAnnotationPresent(ProxyGen.class)) { // it must be it
        return (Class<T>) someInterface;
      }
    }
    return null;
  }

  public Object createEbProxyClass(Class<?> serviceInterface, String address) {
    String name = serviceInterface.getName() + "VertxEBProxy";
    try {
      return Class.forName(name).getConstructor(Vertx.class, String.class).newInstance(vertx, address);
    } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException | ClassNotFoundException e) {
      throw new VertxException("Could not create your service proxy for class : " + serviceInterface, e);
    }
  }
}
