package geniusweb.profilesserver.websocket;

import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.net.InetAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.stream.Collectors;

import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import javax.management.Query;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

import geniusweb.profilesserver.Jackson;
import geniusweb.profilesserver.ProfilesFactory;
import geniusweb.profilesserver.events.ChangeEvent;
import tudelft.utilities.listener.Listener;
import tudelft.utilities.logging.ReportToLogger;
import tudelft.utilities.logging.Reporter;

/**
 * Returns a websocket that communicates the list of currently available domains
 * and profiles. Every time something changes, a new list of domains and
 * profiles is sent. For each new websocket the server will create one of this
 * but they all share one {@link ProfilesFactory}.
 */
@ServerEndpoint("/websocket/liststream")
public class ProfilesListSocket {

	private final Reporter log;
	/** following both final but set in {@link #start(Session)} */
	private Session session;
	private Listener<ChangeEvent> changeListener;
	private static transient String hostport = ""; // cache

	public ProfilesListSocket() {
		this(new ReportToLogger("profilesserver"));
	}

	public ProfilesListSocket(ReportToLogger reportToLogger) {
		this.log = reportToLogger;
	}

	@OnOpen
	public void start(Session session) throws IOException {
		this.session = session;
		log.log(Level.INFO, "New connection " + session.getRequestURI());

		sendupdatedProfiles();
		changeListener = new Listener<ChangeEvent>() {

			@Override
			public void notifyChange(ChangeEvent data) {
				try {
					sendupdatedProfiles();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		};
		Profiles.factory.addListener(changeListener);
	}

	/**
	 * Send the latest profiles to the client.
	 * 
	 * @throws IOException
	 */
	private void sendupdatedProfiles() throws IOException {
		log.log(Level.FINER, "sending updated profiles");
		session.getBasicRemote().sendText(
				Jackson.instance().writeValueAsString(getDomainsProfiles()));
	}

	/**
	 * @return list of domain and profile URIs, as a hashmap.
	 */
	private Map<URI, List<URI>> getDomainsProfiles() {
		Map<URI, List<URI>> allprofiles = new HashMap<>();
		for (String domain : Profiles.factory.getDomains()) {
			List<URI> profiles = Profiles.factory.getProfiles(domain).stream()
					.map(profile -> makeURI(domain, profile.getName()))
					.collect(Collectors.toList());
			allprofiles.put(makeURI(domain, null), profiles);
		}
		return allprofiles;
	}

	/**
	 * @param domain  the domain name.
	 * @param profile the profile name. If null, a ref to the the domain (no
	 *                profile) is needed
	 * @return a URI where to get the given profile/domain.
	 */
	private URI makeURI(String domain, String profile) {
		try {
			return new URI("ws://" + getIpAddressAndPort() + "/"
					+ getServerName() + "/websocket/get/" + domain
					+ (profile != null ? "/" + profile : ""));
		} catch (MalformedObjectNameException | UnknownHostException
				| URISyntaxException e) {
			log.log(Level.SEVERE, "Failed to create profile URI", e);
			return null;
		}
	}

	/**
	 * 
	 * @return name of our service, eg "profilesserver"
	 */
	private String getServerName() {
		// typically something like /profilesserver/websocket/liststream
		// profilesserver is the name we're looking for
		return session.getRequestURI().getPath().split("/")[1]; // drop leading
																// '/'
	}

	private String getIpAddressAndPort()
			throws UnknownHostException, MalformedObjectNameException {
		synchronized (hostport) {
			if (hostport.isEmpty()) {
				MBeanServer beanServer = ManagementFactory
						.getPlatformMBeanServer();

				Set<ObjectName> objectNames = beanServer.queryNames(
						new ObjectName("*:type=Connector,*"),
						Query.match(Query.attr("protocol"),
								Query.value("HTTP/1.1")));

				String host = InetAddress.getLocalHost().getHostAddress();
				String port = objectNames.iterator().next()
						.getKeyProperty("port");

				hostport = host + ":" + port;
			}
			return hostport;
		}

	}

	@OnClose
	public void end() throws IOException {
		Profiles.factory.removeListener(changeListener);
	}

	@OnError
	public void onError(Throwable t) throws Throwable {
		t.printStackTrace();
	}

}
