package com.hubspot.dropwizard.guicier;

import com.codahale.metrics.health.HealthCheck;
import com.google.inject.Binder;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.Module;
import com.google.inject.matcher.Matchers;
import com.google.inject.spi.ProvisionListener;
import io.dropwizard.lifecycle.Managed;
import io.dropwizard.lifecycle.ServerLifecycleListener;
import io.dropwizard.servlets.tasks.Task;
import io.dropwizard.setup.Environment;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.model.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.ws.rs.Path;
import javax.ws.rs.ext.Provider;
import java.lang.reflect.Type;

public class DropwizardModule implements Module {
  private static final Logger LOG = LoggerFactory.getLogger(DropwizardModule.class);

  private final Environment environment;

  public DropwizardModule(Environment environment) {
    this.environment = environment;
  }

  @Override
  public void configure(final Binder binder) {
    binder.bindListener(Matchers.any(), new ProvisionListener() {
      @Override
      public <T> void onProvision(ProvisionInvocation<T> provision) {
        Object obj = provision.provision();

        if (obj instanceof Managed) {
          handle((Managed) obj);
        }

        if (obj instanceof Task) {
          handle((Task) obj);
        }

        if (obj instanceof HealthCheck) {
          handle((HealthCheck) obj);
        }

        if (obj instanceof ServerLifecycleListener) {
          handle((ServerLifecycleListener) obj);
        }
      }
    });
  }

  public void register(Injector injector) {
    registerResourcesAndProviders(environment.jersey().getResourceConfig(), injector);
  }

  private void handle(Managed managed) {
    environment.lifecycle().manage(managed);
    LOG.info("Added guice injected managed Object: {}", managed.getClass().getName());
  }

  private void handle(Task task) {
    environment.admin().addTask(task);
    LOG.info("Added guice injected Task: {}", task.getClass().getName());
  }

  private void handle(HealthCheck healthcheck) {
    environment.healthChecks().register(healthcheck.getClass().getSimpleName(), healthcheck);
    LOG.info("Added guice injected health check: {}", healthcheck.getClass().getName());
  }

  private void handle(ServerLifecycleListener serverLifecycleListener) {
    environment.lifecycle().addServerLifecycleListener(serverLifecycleListener);
    LOG.info("Added guice injected server lifecycle listener: {}", serverLifecycleListener.getClass().getName());
  }

  private void registerResourcesAndProviders(ResourceConfig config, Injector injector) {
    while (injector != null) {
      for (Key<?> key : injector.getBindings().keySet()) {
        Type type = key.getTypeLiteral().getType();
        if (type instanceof Class<?>) {
          Class<?> c = (Class<?>) type;
          if (isProviderClass(c)) {
            LOG.info("Registering {} as a provider class", c.getName());
            config.register(c);
          } else if (isResourceClass(c)) {
            // Jersey rejects resources that it doesn't think are acceptable
            // Including abstract classes and interfaces, even if there is a valid Guice binding.
            if(Resource.isAcceptable(c)) {
              LOG.info("Registering {} as a root resource class", c.getName());
              config.register(c);
            } else {
              LOG.warn("Class {} was not registered as a resource; bind a concrete implementation instead", c.getName());
            }
          }

        }
      }
      injector = injector.getParent();
    }
  }

  private static boolean isProviderClass(Class<?> c) {
    return c.isAnnotationPresent(Provider.class);
  }

  private static boolean isResourceClass(Class<?> c) {
    if (c.isAnnotationPresent(Path.class)) {
      return true;
    }

    for (Class<?> i : c.getInterfaces()) {
      if (i.isAnnotationPresent(Path.class)) {
        return true;
      }
    }

    return false;
  }
}