/*******************************************************************************
 * Copyright (c) 2015 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.
 ******************************************************************************/
package com.openshift.internal.restclient;

import static java.util.stream.Collectors.joining;

import static com.openshift.internal.restclient.capability.CapabilityInitializer.initializeClientCapabilities;

import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang.ObjectUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.openshift.internal.restclient.authorization.AuthorizationContext;
import com.openshift.internal.restclient.okhttp.WatchClient;
import com.openshift.restclient.IApiTypeMapper;
import com.openshift.restclient.IClient;
import com.openshift.restclient.IOpenShiftWatchListener;
import com.openshift.restclient.IResourceFactory;
import com.openshift.restclient.IWatcher;
import com.openshift.restclient.OpenShiftException;
import com.openshift.restclient.ResourceKind;
import com.openshift.restclient.UnsupportedOperationException;
import com.openshift.restclient.api.ITypeFactory;
import com.openshift.restclient.authorization.IAuthorizationContext;
import com.openshift.restclient.capability.CapabilityVisitor;
import com.openshift.restclient.capability.ICapability;
import com.openshift.restclient.http.IHttpConstants;
import com.openshift.restclient.model.IList;
import com.openshift.restclient.model.IResource;
import com.openshift.restclient.model.JSONSerializeable;

import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;

/**
 * @author Jeff Cantrill
 */
public class DefaultClient implements IClient, IHttpConstants{
	
	public static final String SYSTEM_PROP_K8E_API_VERSION = "osjc.k8e.apiversion"; 
	public static final String SYSTEM_PROP_OPENSHIFT_API_VERSION = "osjc.openshift.apiversion"; 
	
	private static final Logger LOGGER = LoggerFactory.getLogger(DefaultClient.class);
	private URL baseUrl;
	private OkHttpClient client;
	private IResourceFactory factory;
	private Map<Class<? extends ICapability>, ICapability> capabilities = new HashMap<Class<? extends ICapability>, ICapability>();
	private boolean capabilitiesInitialized = false;
	
	private static final String OS_API_ENDPOINT = "oapi";
	
	private String openShiftVersion;
	private String kubernetesVersion;
	private AuthorizationContext authContext;
	private IApiTypeMapper typeMapper;
	
	public DefaultClient(URL baseUrl,  OkHttpClient client, IResourceFactory factory, IApiTypeMapper typeMapper, AuthorizationContext authContext){
		this.baseUrl = baseUrl;
		this.client = client; 
		this.factory = factory;
		if(this.factory != null) {
			this.factory.setClient(this);
		}
		openShiftVersion = System.getProperty(SYSTEM_PROP_OPENSHIFT_API_VERSION, null);
		kubernetesVersion = System.getProperty(SYSTEM_PROP_K8E_API_VERSION, null);
		this.typeMapper = typeMapper != null ? typeMapper :  new ApiTypeMapper(baseUrl.toString(), client);
		this.authContext = authContext;
	}
	
	
	@Override
	public IClient clone() {
		AuthorizationContext context = authContext.clone();
		DefaultClient clone = new DefaultClient(baseUrl, client, factory, typeMapper, context);
		context.setClient(clone);
		return clone;
	}


	@Override
	public IResourceFactory getResourceFactory() {
		return factory;
	};
	
	@Override
	public IWatcher watch(String namespace, IOpenShiftWatchListener listener, String...kinds) {
		WatchClient watcher = new WatchClient(this, this.typeMapper, this.client);
		return watcher.watch(Arrays.asList(kinds), namespace, listener);
	}

	@Override
	public IWatcher watch(IOpenShiftWatchListener listener, String...kinds) {
		return this.watch("", listener, kinds);
	}

	@Override
	public String getResourceURI(IResource resource) {
		return new URLBuilder(getBaseURL(), typeMapper, resource).build().toString();
	}

	@Override
	public <T extends IResource> List<T> list(String kind) {
		return list(kind,"");
	}

	@Override
	public <T extends IResource> List<T> list(String kind,  Map<String, String> labels) {
		return list(kind,"", labels);
	}


	@Override
	public <T extends IResource> List<T> list(String kind, String namespace) {
		return list(kind, namespace, new HashMap<>());
	}
	
	@SuppressWarnings("unchecked")
	@Override
	public <T extends IResource> List<T> list(String kind, String namespace, Map<String, String> labels) {

		String labelQuery="";
		if(labels != null && !labels.isEmpty()){
			 labelQuery = labels.entrySet().stream()
					.map(e -> e.getKey() + "=" + e.getValue())
					.collect(joining(","));
		}
		return list(kind, namespace, labelQuery);
	}

	@SuppressWarnings("unchecked")
	@Override
	public <T extends IResource> List<T> list(String kind, String namespace, String labelQuery) {

		Map<String, String> params = new HashMap<>();
		if(labelQuery != null && !labelQuery.isEmpty()){
			params.put("labelSelector", labelQuery);
		}

		IList resources = execute(HttpMethod.GET.toString(), kind, namespace, null, null, null, params);
		List<T> items = new ArrayList<>();
		items.addAll((Collection<? extends T>) resources.getItems());
		return items;
	}

	
	@Override
	public Collection<IResource> create(IList list, String namespace){
		List<IResource> results = new ArrayList<IResource>(list.getItems().size());
		for (IResource resource : list.getItems()) {
			try{
				results.add(create(resource, namespace));
			}catch(OpenShiftException e){
				if(e.getStatus() != null){
					results.add(e.getStatus());
				}else{
					throw e;
				}
			}
		}
		return results;
	}

	@Override
	public <T extends IResource> T create(T resource) {
		return create(resource, resource.getNamespace());
	}
	
		
	@Override
	public <T extends IResource> T create(T resource, String namespace) {
		return execute(HttpMethod.POST, resource.getKind(), namespace, null, null, resource);
	}

	@Override
	public <T extends IResource> T create(String kind, String namespace, String name, String subresource, IResource payload) {
		return execute(HttpMethod.POST, kind, namespace, name, subresource, payload);
	}
	
	enum HttpMethod{
		GET,
		PUT,
		POST,
		DELETE
	}
	
	private <T extends IResource> T execute(HttpMethod method, String kind, String namespace, String name, String subresource, IResource payload) {
		return execute(method.toString(), kind, namespace, name, subresource, payload);
	}

	@SuppressWarnings("unchecked")
	public <T extends IResource> T execute(String method, String kind, String namespace, String name, String subresource, IResource payload, String subContext) {
		return (T) execute(this.factory, method, kind, namespace, name, subresource, subContext, payload,
				Collections.emptyMap());
	}	
	
	@Override
	@SuppressWarnings("unchecked")
	public <T extends IResource> T execute(String method, String kind, String namespace, String name, String subresource, IResource payload) {
		return (T) execute(this.factory, method, kind, namespace, name, subresource, null, payload,
				Collections.emptyMap());
	}

    @Override
    @SuppressWarnings("unchecked")
    public <T extends IResource> T execute(String method, String kind, String namespace, String name, String subresource, IResource payload, Map<String, String> params) {
		return (T) execute(this.factory, method, kind, namespace, name, subresource, null, payload,params);
    }
	@SuppressWarnings("unchecked")
	public <T> T execute(ITypeFactory factory, String method, String kind, String namespace, String name,
        String subresource, String subContext, JSONSerializeable payload, Map<String, String> params) {
		if(factory == null) {
			throw new OpenShiftException("ITypeFactory is null while trying to call IClient#execute");
		}

		if(params == null){
			params = Collections.emptyMap();
		}

		if(ResourceKind.LIST.equals(kind)) 
			throw new UnsupportedOperationException("Generic create operation not supported for resource type 'List'");
		final URL endpoint = new URLBuilder(this.baseUrl, typeMapper)
				.kind(kind)
				.name(name)
				.namespace(namespace)
				.subresource(subresource)
				.subContext(subContext)
 				.addParameters(params)
				.build();
			
		try {
			Request request = newRequestBuilderTo(endpoint.toString())
					.method(method, getPayload(method, payload))
					.build();
			LOGGER.debug("About to make {} request: {}", request.method(), request);
			try(Response result = client.newCall(request).execute()){
				String response =  result.body().string();
				LOGGER.debug("Response: {}", response);
				return (T) factory.createInstanceFrom(response);
			}
		} catch (IOException e){
			throw new OpenShiftException(e, "Unable to execute request to %s", endpoint);
		}
	}
	
	private RequestBody getPayload(String method, JSONSerializeable payload) {
		switch(method.toUpperCase()){
			case "GET":
			case "DELETE":
				return null;
			default:
				String json = payload == null ? "" : payload.toJson(true);
				LOGGER.debug("About to send payload: {}", json);
				return RequestBody.create(MediaType.parse(MEDIATYPE_APPLICATION_JSON), json);
		}
	}
	
	
	@Override
	public String getServerReadyStatus() {
		try {
			Request request = new Request.Builder()
					.url(new URL(this.baseUrl, "healthz/ready"))
					.header(PROPERTY_ACCEPT, "*/*")
					.build();
			try(Response response = client.newCall(request).execute()){
				return response.body().string();
			}
		} catch (IOException e) {
			throw new OpenShiftException(e, "Exception while trying to determine the health/ready response of the server");
		}
	}

	public Request.Builder newRequestBuilderTo(String endpoint){
		return newRequestBuilderTo(endpoint, MEDIATYPE_APPLICATION_JSON);
	}

	public Request.Builder newRequestBuilderTo(String endpoint,String acceptMediaType){
		Request.Builder builder = new Request.Builder()
				.url(endpoint.toString())
				.header(PROPERTY_ACCEPT, acceptMediaType);

		String token =  null;
		if(this.authContext != null &&  StringUtils.isNotBlank(this.authContext.getToken())){
			token = this.authContext.getToken();
		}
		builder.header(IHttpConstants.PROPERTY_AUTHORIZATION, String.format("%s %s", IHttpConstants.AUTHORIZATION_BEARER, token));
		return builder;
	}


	@Override
	public <T extends IResource> T update(T resource) {
		return execute(HttpMethod.PUT, resource.getKind(), resource.getNamespace(), resource.getName(), null, resource);
	}

	@Override
	public <T extends IResource> void delete(T resource) {
		execute(HttpMethod.DELETE, resource.getKind(), resource.getNamespace(), resource.getName(), null, resource);
	}
	
	
	@Override
	public IList get(String kind, String namespace) {
		return execute(HttpMethod.GET, kind, namespace, null, null, null);
	}


	@Override
	public <T extends IResource> T get(String kind, String name, String namespace) {
		return execute(HttpMethod.GET, kind, namespace, name, null, null);
	}

	public synchronized void initializeCapabilities(){
		if(capabilitiesInitialized) return;
		initializeClientCapabilities(capabilities, this);
		capabilitiesInitialized = true;
	}

	@SuppressWarnings("unchecked")
	@Override
	public <T extends ICapability> T getCapability(Class<T> capability) {
		return  (T) capabilities.get(capability);
	}

	@Override
	public  boolean supports(Class<? extends ICapability> capability) {
		if(!capabilitiesInitialized ){
			initializeCapabilities();
		}
		return capabilities.containsKey(capability);
	}
	
	@SuppressWarnings("unchecked")
	@Override
	public <T extends ICapability, R> R accept(CapabilityVisitor<T, R> visitor, R unsupportedCapabililityValue){
		if(!capabilitiesInitialized) initializeCapabilities();
		if(capabilities.containsKey(visitor.getCapabilityType())){
			T capability = (T) capabilities.get(visitor.getCapabilityType());
			return (R) visitor.visit(capability);
		}
		return unsupportedCapabililityValue;
	}

	@Override
	public String getOpenShiftAPIVersion() {
		return typeMapper.getPreferedVersionFor(OS_API_ENDPOINT);
	}
	
	@Override
	public URL getBaseURL() {
		return this.baseUrl;
	}

	@Override
	public IAuthorizationContext getAuthorizationContext() {
		return this.authContext;
	}
	
	public void setToken(String token) {
		this.authContext.setToken(token);
	}
	
	public String getToken() {
		return getAuthorizationContext().getToken();
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((baseUrl == null) ? 0 : baseUrl.toString().hashCode());
		result = prime * result + ((kubernetesVersion == null) ? 0 : kubernetesVersion.hashCode());
		result = prime * result + ((openShiftVersion == null) ? 0 : openShiftVersion.hashCode());
		result = prime * result + ((authContext == null || authContext.getToken() == null) ? 0 : authContext.getToken().hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (!(obj instanceof DefaultClient))
			return false;
		DefaultClient other = (DefaultClient) obj;
		if (baseUrl == null) {
			if (other.baseUrl != null)
				return false;
		} else if (!baseUrl.toString().equals(other.baseUrl.toString()))
			return false;
		if (kubernetesVersion == null) {
			if (other.kubernetesVersion != null)
				return false;
		} else if (!kubernetesVersion.equals(other.kubernetesVersion))
			return false;
		if (openShiftVersion == null) {
			if (other.openShiftVersion != null)
				return false;
		} else if (!openShiftVersion.equals(other.openShiftVersion)) {
			return false;
		}
		if (authContext == null) {
			return other.authContext == null;
		} else {
			if (other.authContext == null) {
				return false;
			}
			return ObjectUtils.equals(authContext.getUserName(), other.authContext.getUserName());
		}
	}

	@SuppressWarnings("unchecked")
	@Override
	public <T> T adapt(Class<T> klass) {
		if(DefaultClient.class.equals(klass)){
			return (T) this;
		}
		if(OkHttpClient.class.equals(klass)) {
			return (T) this.client;
		}
		if(IApiTypeMapper.class.equals(klass)) {
			return (T)this.typeMapper;
		}
		if(ICapability.class.isAssignableFrom(klass) && this.supports((Class<? extends ICapability>) klass)) {
			return (T) getCapability((Class<? extends ICapability>)klass);
		}
		return null;
	}
	
	
	
}
