/**
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package tools.descartes.teastore.registryclient;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.ws.rs.ProcessingException;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.GenericType;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

import org.glassfish.jersey.client.ClientConfig;
import org.glassfish.jersey.client.ClientProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.netflix.loadbalancer.Server;

import tools.descartes.teastore.registryclient.loadbalancers.LoadBalancerUpdaterDaemon;

/**
 * Client with common functionality for registering with the registry.
 * 
 * @author Simon Eismann
 *
 */
public class RegistryClient {

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

  /**
   * The registry client.
   */
  private static RegistryClient client = new RegistryClient();
  private String registryRESTURL;
  private String hostName = null;
  private Integer port = null;

  private Server myServiceInstanceServer = null;
  private Service myService = null;

  private static final int LOAD_BALANCER_REFRESH_INTERVAL_MS = 2500;
  private static final int HEARTBEAT_INTERVAL_MS = 2500;

  private ScheduledExecutorService loadBalancerUpdateScheduler = Executors
      .newSingleThreadScheduledExecutor();
  private ScheduledExecutorService heartbeatScheduler = Executors
      .newSingleThreadScheduledExecutor();
  private ScheduledExecutorService availabilityScheduler = Executors
      .newSingleThreadScheduledExecutor();

  /**
   * Constructor.
   */
  protected RegistryClient() {
    System.setProperty("org.slf4j.simpleLogger.logFile", "System.out");
    String useHostIP = "false";
    try {
      useHostIP = (String) new InitialContext().lookup("java:comp/env/useHostIP");
    } catch (NamingException e) {
      LOG.warn("useHostIP not set. Not using host ip as hostname.");
    }
    try {
      if (useHostIP.equalsIgnoreCase("true")) {
        hostName = InetAddress.getLocalHost().getHostAddress();
      } else {
        hostName = (String) new InitialContext().lookup("java:comp/env/hostName");
      }
    } catch (NamingException e) {
      LOG.warn("hostName not set. Using default OS-provided hostname.");
    } catch (UnknownHostException e) {
      LOG.error("could not resolve host IP. Using default OS-provided hostname: " + e.getMessage());
    }
    try {
      port = Integer.parseInt((String) new InitialContext().lookup("java:comp/env/servicePort"));
    } catch (NamingException | NumberFormatException e) {
      LOG.warn("Could not read servicePort! Using port 8080 as fallback.");
      port = 8080;
    }
    try {
      registryRESTURL = (String) new InitialContext().lookup("java:comp/env/registryURL");
    } catch (NamingException e) {
      LOG.warn("registryURL not set. Falling back to default registry URL (localhost, port " + port
          + ").");
      registryRESTURL = "http://localhost:" + port
          + "/tools.descartes.teastore.registry/rest/services/";
    }
  }

  /**
   * Getter.
   * 
   * @return registry client
   */
  public static RegistryClient getClient() {
    return client;
  }

  /**
   * Handles full registration.
   * 
   * @param contextPath
   *          contextPath private String getContextPath(ServletContextEvent event)
   *          { return event.getServletContext().getContextPath(); }
   */
  public void unregister(String contextPath) {
    Service service = getService(contextPath);
    Server host = getServer();
    LOG.info("Shutting down " + service.getServiceName() + "@" + host);
    heartbeatScheduler.shutdownNow();
    loadBalancerUpdateScheduler.shutdownNow();
    availabilityScheduler.shutdownNow();
    try {
      loadBalancerUpdateScheduler.awaitTermination(20, TimeUnit.SECONDS);
      heartbeatScheduler.awaitTermination(20, TimeUnit.SECONDS);
      availabilityScheduler.awaitTermination(20, TimeUnit.SECONDS);
      RegistryClient.client.unregisterOnce(service, host);
    } catch (ProcessingException e) {
      LOG.warn("Could not unregister " + service.getServiceName() + " when it was shutting "
          + "down, since it could not reach the registry. This can be caused by shutting "
          + "down the registry before other services, but is in it self not a problem.");
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }

  /**
   * Handles full unregistration.
   * 
   * @param contextPath
   *          contextPath private String getContextPath(ServletContextEvent event)
   *          { return event.getServletContext().getContextPath(); }
   */
  public void register(String contextPath) {
    Service service = getService(contextPath);
    Server host = getServer();
    heartbeatScheduler.scheduleAtFixedRate(new RegistryClientHeartbeatDaemon(service, host), 0,
        HEARTBEAT_INTERVAL_MS, TimeUnit.MILLISECONDS);
    loadBalancerUpdateScheduler.scheduleAtFixedRate(new LoadBalancerUpdaterDaemon(), 1000,
        LOAD_BALANCER_REFRESH_INTERVAL_MS, TimeUnit.MILLISECONDS);
  }

  /**
   * Calls the StartupCallback after the service is available.
   * 
   * @param requestedService
   *          service to check for
   * @param myService
   *          The Service enum for the waiting service (the service calling this).
   * @param callback
   *          StartupCallback to call
   */
  public void runAfterServiceIsAvailable(Service requestedService, StartupCallback callback,
      Service myService) {
    availabilityScheduler.schedule(new StartupCallbackTask(requestedService, callback, myService),
        0, TimeUnit.NANOSECONDS);
    availabilityScheduler.shutdown();
  }

  /**
   * Get all servers for a service in the {@link Service} enum from the registry.
   * 
   * @param targetService
   *          The service for which to get the servers.
   * @return List of servers.
   */
  public List<Server> getServersForService(Service targetService) {
    List<String> list = null;
    List<Server> serverList = new ArrayList<Server>();
    try {
      Response response = getRESTClient(5000).target(registryRESTURL)
          .path("/" + targetService.getServiceName() + "/").request(MediaType.APPLICATION_JSON)
          .get();
      list = response.readEntity(new GenericType<List<String>>() {
      });
    } catch (ProcessingException e) {
      return null;
    }

    if (list != null) {
      for (String string : list) {
        serverList.add(new Server(string));
      }
    }

    return serverList;
  }

  /**
   * Get the server for this service. Returns null if the service is not
   * registered yet.
   * 
   * @return The server for this service. Null, if not registered.
   */
  public Server getMyServiceInstanceServer() {
    return myServiceInstanceServer;
  }

  /**
   * Get the service of this application. Returns null if the service is not
   * registered yet.
   * 
   * @return The service for this application. Null, if not registered.
   */
  public Service getMyService() {
    return myService;
  }

  /**
   * Register a new server for a service in the registry.
   * 
   * @param service
   *          The service for which to register.
   * @param server
   *          The server address.
   * @return True, if registration succeeded.
   */
  protected boolean registerOnce(Service service, Server server) {
    myService = service;
    myServiceInstanceServer = server;
    try {
      Response response = getRESTClient(5000).target(registryRESTURL).path(service.getServiceName())
          .path(server.toString()).request(MediaType.APPLICATION_JSON).put(Entity.text(""));
      return (response.getStatus() == Response.Status.OK.getStatusCode());
    } catch (ProcessingException e) {
      return false;
    }
  }

  /**
   * Unregister a server for a service in the registry.
   * 
   * @param service
   *          The service for which to unregister.
   * @param server
   *          The server address to remove.
   * @return True, if unregistration succeeded.
   */
  private boolean unregisterOnce(Service service, Server server) {
    try {
      Response response = getRESTClient(1000).target(registryRESTURL).path(service.getServiceName())
          .path(server.toString()).request(MediaType.APPLICATION_JSON).delete();
      return (response.getStatus() == Response.Status.OK.getStatusCode());
    } catch (ProcessingException e) {
      return false;
    }
  }

  private Client getRESTClient(int timeout) {
    ClientConfig configuration = new ClientConfig();
    configuration.property(ClientProperties.CONNECT_TIMEOUT, timeout);
    configuration.property(ClientProperties.READ_TIMEOUT, timeout);
    return ClientBuilder.newClient(configuration);
  }

  private Service getService(String serviceName) {
    serviceName = cleanupServiceName(serviceName);
    for (Service service : Service.values()) {
      if (service.getServiceName().equals(serviceName)) {
        return service;
      }
    }
    throw new IllegalStateException(
        "The service " + serviceName + " is not registered in the Services enum");
  }

  private Server getServer() {
    return new Server(getHostName(), getPort());
  }

  private String getHostName() {
    if (hostName != null && !hostName.isEmpty()) {
      return hostName;
    }
    try {
      return InetAddress.getLocalHost().getCanonicalHostName();
    } catch (UnknownHostException e) {
      throw new IllegalStateException("could not load hostname from OS.");
    }
  };

  private int getPort() {
    if (port != null) {
      return port;
    } else {
      throw new IllegalStateException("Could not read servicePort!");
    }
  }

  /**
   * Protected for testing.
   * 
   * @param serviceName
   *          name of service
   * @return cleaned service name
   */
  protected String cleanupServiceName(String serviceName) {
    return serviceName.replace("/", "");
  }

  /**
   * Protected for test.
   * 
   * @return scheduler
   */
  protected ScheduledExecutorService getHeartbeatScheduler() {
    return heartbeatScheduler;
  }

  /**
   * Protected for test.
   * 
   * @return scheduler
   */
  protected ScheduledExecutorService getLoadBalancerUpdateScheduler() {
    return loadBalancerUpdateScheduler;
  }
}