/******************************************************************************* 
 * Copyright (c) 2016 Red Hat, Inc. 
 * Distributed under license by Red Hat, Inc. All rights reserved. 
 * This program is made available under the terms of the 
 * Eclipse Public License v1.0 which accompanies this distribution, 
 * and is available at http://www.eclipse.org/legal/epl-v10.html 
 * 
 * Contributors: 
 * Red Hat, Inc. - initial API and implementation 
 ******************************************************************************/
package com.openshift.internal.restclient.okhttp;

import java.io.IOException;
import java.net.ProtocolException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;

import org.apache.commons.lang.StringUtils;
import org.jboss.dmr.ModelNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.openshift.internal.restclient.DefaultClient;
import com.openshift.internal.restclient.URLBuilder;
import com.openshift.internal.restclient.model.properties.ResourcePropertyKeys;
import com.openshift.restclient.IApiTypeMapper;
import com.openshift.restclient.IClient;
import com.openshift.restclient.IOpenShiftWatchListener;
import com.openshift.restclient.IOpenShiftWatchListener.ChangeType;
import com.openshift.restclient.IWatcher;
import com.openshift.restclient.OpenShiftException;
import com.openshift.restclient.http.IHttpConstants;
import com.openshift.restclient.model.IList;
import com.openshift.restclient.model.IResource;

import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okhttp3.ws.WebSocket;
import okhttp3.ws.WebSocketCall;
import okhttp3.ws.WebSocketListener;
import okio.Buffer;

public class WatchClient implements IWatcher, IHttpConstants {

	private static final Logger LOGGER = LoggerFactory.getLogger(WatchClient.class);
	private DefaultClient client;
	private OkHttpClient okClient;
	private AtomicReference<Status> status = new AtomicReference<>(Status.Stopped);
	private IApiTypeMapper typeMappings;
	private Map<String, WatchEndpoint> endpointMap = Collections.synchronizedMap(new HashMap<>());
	
	private enum Status {
		Started,
		Starting,
		Stopped,
		Stopping
	}
	
	public WatchClient(DefaultClient client, IApiTypeMapper typeMapper, OkHttpClient okClient) {
		this.client = client;
		this.typeMappings = typeMapper;
		this.okClient = okClient;
	}
	
	@Override
	public void stop() {
		if(status.compareAndSet(Status.Started, Status.Stopping)) {
			Map<String, WatchEndpoint> endpoints = new HashMap<>(endpointMap);
			endpointMap.clear();
			endpoints.values().forEach(w->w.close());
			status.set(Status.Stopped);
		}
	}
	
	public IWatcher watch(Collection<String> kinds, String namespace, IOpenShiftWatchListener listener) {
		
		if(status.compareAndSet(Status.Stopped, Status.Starting)) {
			try {
				for (String kind : kinds) {
					WatchEndpoint socket = new WatchEndpoint(client, listener, kind);
					final String resourceVersion = getResourceVersion(kind, namespace, socket);
					
					final String endpoint = new URLBuilder(client.getBaseURL(), this.typeMappings)
							.kind(kind)
							.namespace(namespace)
							.watch()
							.addParmeter(ResourcePropertyKeys.RESOURCE_VERSION, resourceVersion)
							.websocket();
					Request request = client.newRequestBuilderTo(endpoint)
							.header(PROPERTY_ORIGIN, client.getBaseURL().toString())
							.header(PROPERTY_USER_AGENT, "openshift-restclient-java")
							.build();	
					WebSocketCall call = WebSocketCall.create(okClient.newBuilder().build(), request);
					socket.setCall(call);
					endpointMap.put(kind, socket);
					call.enqueue(socket);
				}
				status.set(Status.Started);
			} catch (Exception e) {
				endpointMap.clear();
				status.set(Status.Stopped);
				try {
					throw ResponseCodeInterceptor.createOpenShiftException(client, 0, String.format("Could not watch resources in namespace %s: %s", namespace, e.getMessage()), null, e);
				} catch (IOException e1) {
					throw new OpenShiftException(e1, "IOException trying to create an OpenShift specific exception");
				}
			}
		}
		return this;
	}
	
	private String getResourceVersion(String kind, String namespace, WatchEndpoint endpoint) throws Exception{
		IList list = client.get(kind, namespace);
		Collection<IResource> items = list.getItems();
		List<IResource> resources = new ArrayList<>(items.size());
		resources.addAll(items);
		endpoint.setResources(resources);
		return list.getResourceVersion();
	}
	
	static class WatchEndpoint implements WebSocketListener{
		
		private IOpenShiftWatchListener listener;
		private List<IResource> resources;
		private final String kind;
		private final IClient client;
		private WebSocket wsClient;
		private WebSocketCall call;
		
		public WatchEndpoint(IClient client, IOpenShiftWatchListener listener, String kind) {
			this.listener = listener;
			this.kind = kind;
			this.client = client;
		}
		
		public void setCall(WebSocketCall call) {
			this.call = call;
		}

		void close() {
			try {
				if(wsClient != null) {
					wsClient.close(STATUS_NORMAL_STOP, "Client was asked to stop.");
					wsClient = null;
				}
				if(call != null) {
					call.cancel();
				}
				listener.disconnected();
			} catch (Exception e) {
				LOGGER.debug("Unable to stop the watch client",e);
			}finally {
				wsClient = null;
			}
		}
		
		public void setResources(List<IResource> resources) {
			this.resources = resources;
		}
		
		@Override
		public void onClose(int statusCode, String reason) {
			LOGGER.debug("WatchSocket closed for kind: {}, code: {}, reason: {}", new Object[]{kind, statusCode, reason});
			listener.disconnected();
		}

		@Override
		public void onFailure(IOException err, Response response) {
			LOGGER.debug("WatchSocket Error for kind {}: {}", kind, err);
			try {
				if (response == null) {
					listener.error(ResponseCodeInterceptor.createOpenShiftException(client, 0, "", "", err));
				} else if (response.code() == IHttpConstants.STATUS_OK && err instanceof ProtocolException) {
					// Just swallow it. Means the feature isn't supported in this OS server version yet.
					// WebSocket creates error "Expected HTTP 101 response but was '200 OK'"
					// This is described in the web socket specification.
					LOGGER.debug("The feature isn't supported", err);
				} else {
					listener.error(ResponseCodeInterceptor.createOpenShiftException(client, response.code(),
							response.body().string(), response.request().url().toString(), err));
				}
			} catch (IOException e) {
				LOGGER.error("IOException trying to notify listener of specific OpenShiftException", err);
				listener.error(err);
			}
		}

		@Override
		public void onMessage(ResponseBody body) throws IOException {
			String message = body.string();
			LOGGER.debug(message);
			ModelNode node = ModelNode.fromJSONString(message);
			IOpenShiftWatchListener.ChangeType event = new ChangeType(node.get("type").asString());
			IResource resource = client.getResourceFactory().create(node.get("object").toJSONString(true));
			if(StringUtils.isEmpty(resource.getKind())) {
				LOGGER.error("Unable to determine resource kind from: " + node.get("object").toJSONString(false));
			}
			listener.received(resource, event);
		}

		@Override
		public void onOpen(WebSocket socket, Response response) {
			LOGGER.debug("WatchSocket connected for {}", kind);
			wsClient = socket;
			listener.connected(resources);
		}

		@Override
		public void onPong(Buffer buffer) {
		}
		
	}

}
