package restservices.publish;

import static com.google.common.base.Preconditions.checkNotNull;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.servlet.ServletException;

import org.apache.commons.fileupload.FileItemIterator;
import org.apache.commons.fileupload.FileItemStream;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import com.mendix.thirdparty.org.json.JSONObject;

import restservices.RestServices;
import restservices.proxies.HttpMethod;
import restservices.publish.RestServiceHandler.HandlerRegistration;
import restservices.publish.RestServiceRequest.RequestContentType;
import restservices.publish.RestServiceRequest.ResponseType;
import restservices.util.DataWriter;
import restservices.util.ICloseable;
import restservices.util.JSONSchemaBuilder;
import restservices.util.JsonDeserializer;
import restservices.util.JsonSerializer;
import restservices.util.UriTemplate;
import restservices.util.Utils;
import system.proxies.FileDocument;

import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.mendix.core.Core;
import com.mendix.core.CoreException;
import com.mendix.systemwideinterfaces.core.IDataType;
import com.mendix.systemwideinterfaces.core.IMendixObject;
import com.mendix.systemwideinterfaces.core.meta.IMetaObject;
import com.mendix.systemwideinterfaces.core.meta.IMetaPrimitive;

public class MicroflowService implements IRestServiceHandler{

	private String microflowname;
	private boolean hasArgument;
	private String argType;
	private boolean isReturnTypePrimitive;
	private String returnType;
	private String argName;
	private String roleOrMicroflow;
	private String description;
	private HttpMethod httpMethod;
	private boolean isFileSource = false;
	private boolean isFileTarget = false;
	
	private static final ServletFileUpload servletFileUpload = new ServletFileUpload(new DiskFileItemFactory(100000, new File(System.getProperty("java.io.tmpdir"))));
	private String relativeUrl;
	private HandlerRegistration serviceHandler;
	private ICloseable metaserviceHandler;
	private static final List<MicroflowService> microflowServices = Lists.newCopyOnWriteArrayList();

	public MicroflowService(String microflowname, String roleOrMicroflow, HttpMethod httpMethod,
			String pathTemplateString, String description) throws CoreException {
		checkNotNull(microflowname);
		checkNotNull(roleOrMicroflow);
		checkNotNull(httpMethod);
		
		this.microflowname = microflowname;
		this.roleOrMicroflow = roleOrMicroflow;
		this.description = description;
		this.httpMethod = httpMethod;
		
		if (pathTemplateString != null)
			this.relativeUrl = Utils.removeLeadingAndTrailingSlash(pathTemplateString);
		else
			this.relativeUrl = microflowname.split("\\.")[1].toLowerCase();
			
		this.consistencyCheck();

		register();
	}

	private void register() {
		unregister();
		
		microflowServices.add(this);
		serviceHandler = RestServiceHandler.registerServiceHandler(httpMethod, getRelativeUrl(), roleOrMicroflow, this);
		metaserviceHandler = RestServiceHandler.registerServiceHandlerMetaUrl(getRelativeUrl());
	}

	public void unregister() {
		microflowServices.remove(this);
		if (serviceHandler != null) {
			serviceHandler.close();
		}
		if (metaserviceHandler != null) {
			metaserviceHandler.close();
		}
	}

	private String getRelativeUrl() {
		return relativeUrl;
	}

	public MicroflowService(String microflowname, String securityRoleOrMicroflow, HttpMethod httpMethod, String description) throws CoreException {
		this(microflowname, securityRoleOrMicroflow, httpMethod, null, description);
	}

	private void consistencyCheck() throws CoreException {
		String secError = ConsistencyChecker.checkAccessRole(this.roleOrMicroflow);
		if (secError != null)
			throw new IllegalArgumentException("Cannot publish microflow " + microflowname + ": " + secError);

		int argCount = Utils.getArgumentTypes(microflowname).size();

		if (argCount > 1)
			throw new IllegalArgumentException("Cannot publish microflow " + microflowname + ", it should exist and have exactly zero or one argument");

		hasArgument = argCount == 1;
		
		List<String> pathParams = new UriTemplate(relativeUrl).getTemplateVariables();
		if (pathParams.size() > 0 && !hasArgument) {
			throw new IllegalArgumentException("Cannot publish microflow " + microflowname + " with path '" + relativeUrl + ", the microflow should have a single input argument object with at least attributes " + pathParams);
		}
		
		if (hasArgument) {
			IDataType argtype = Utils.getFirstArgumentType(microflowname);
			if (!argtype.isMendixObject())
				throw new IllegalArgumentException("Cannot publish microflow " + microflowname + ", it should have a single object as input argument");
			this.argType = argtype.getObjectType();
			this.argName = Utils.getArgumentTypes(microflowname).keySet().iterator().next();
			isFileSource = Core.isSubClassOf(FileDocument.entityName, argType);

			
			IMetaObject metaObject = Core.getMetaObject(argType);
			if (metaObject.isPersistable() && !isFileSource)
				throw new IllegalArgumentException("Cannot publish microflow " + microflowname + ", it should have a transient object of filedocument as input argument");
			
			Set<String> metaPrimitiveNames = Sets.newHashSet();
			for(IMetaPrimitive prim : metaObject.getMetaPrimitives()) {
				metaPrimitiveNames.add(prim.getName().toLowerCase());
			}
			for(String pathParam : pathParams) {
				if (!metaPrimitiveNames.contains(pathParam.toLowerCase()))
					throw new IllegalArgumentException("Cannot publish microflow " + microflowname + ", its input argument should have an attribute with name '" + pathParam +"', as required by the template path");
			}
			
		}

		if (httpMethod == null) {
			throw new IllegalArgumentException("Cannot publish microflow " + microflowname + ", it has no HTTP method defined.");
		}
			
		IDataType returnTypeFromMF = Core.getReturnType(microflowname);
		
		if (returnTypeFromMF.isMendixObject() || returnTypeFromMF.isList()){
			this.returnType = returnTypeFromMF.getObjectType();
			isFileTarget = Core.isSubClassOf(FileDocument.entityName, this.returnType); 

			if (Core.getMetaObject(this.returnType).isPersistable()  && !isFileTarget)
				throw new IllegalArgumentException("Cannot publish microflow " + microflowname+ ", its return type should be a non-persistable object or a file document");
		}
		else
			isReturnTypePrimitive = true;
	}

	@Override
	public void execute(final RestServiceRequest rsr, Map<String, String> params)	throws Exception {
		if (params.containsKey(RestServices.PARAM_ABOUT)) {
			serveDescription(rsr);
		}
		else {
			Map<String, Object> args = new HashMap<String, Object>();
			IMendixObject inputObject = parseInputData(rsr, params);
			
			if(inputObject != null) 
				args.put(argName, inputObject);
			
			if (isReturnTypePrimitive)
				rsr.setResponseContentType(ResponseType.PLAIN); //default, but might be overriden by the executing mf
			else if (isFileTarget)
				rsr.setResponseContentType(ResponseType.BINARY);
	
			Object result = Core.execute(rsr.getContext(), microflowname, args);
			writeOutputData(rsr, result);
		}
	}

	private void writeOutputData(RestServiceRequest rsr, Object result)
			throws IOException, Exception {
		if (result == null) {
			//write nothing
		}
		else if (this.isFileTarget) {
			if (!Utils.hasDataAccess(Core.getMetaObject(argType), rsr.getContext()))
				throw new IllegalStateException("Cannot serialize filedocument of type '" + argType + "', the object is not accessiable for users with role " + rsr.getContext().getSession().getUserRolesNames() + ". Please check the access rules");

			String filename = ((IMendixObject)result).getValue(rsr.getContext(), FileDocument.MemberNames.Name.toString());
			if (filename != null && !filename.isEmpty())
				rsr.response.setHeader(RestServices.HEADER_CONTENTDISPOSITION, "attachment;filename=\"" + Utils.urlEncode(filename) + "\"");
			InputStream stream  = Core.getFileDocumentContent(rsr.getContext(), (IMendixObject)result);
			IOUtils.copy(stream, rsr.response.getOutputStream());
		}
		else if (this.isReturnTypePrimitive) {
			rsr.write(result == null ? "" : String.valueOf(result));
		}
		else if (result instanceof List<?>) {
			rsr.startDoc();

			rsr.datawriter.array();
			for(Object item : (List<?>)result)
				rsr.datawriter.value(JsonSerializer.writeMendixObjectToJson(rsr.getContext(), (IMendixObject) item, true));
			rsr.datawriter.endArray();
			rsr.endDoc();
		}
		else if (result instanceof IMendixObject) {
			rsr.startDoc();
			rsr.datawriter.value(JsonSerializer.writeMendixObjectToJson(rsr.getContext(), (IMendixObject) result, true));
			rsr.endDoc();
		}
		else throw new IllegalStateException("Unexpected result from microflow " + microflowname + ": " + result.getClass().getName());
	}

	private IMendixObject parseInputData(RestServiceRequest rsr, Map<String, String> params)
			throws IOException, ServletException, Exception {
		if (!hasArgument)
			return null;
		
		if (!Utils.hasDataAccess(Core.getMetaObject(argType), rsr.getContext()))
			throw new IllegalStateException("Cannot instantiate object of type '" + argType + "', the object is not accessiable for users with role " + rsr.getContext().getSession().getUserRolesNames() + ". Please check the access rules");
		
		IMendixObject argObject = Core.instantiate(rsr.getContext(), argType);
		JSONObject data = new JSONObject();

		//multipart data
		if (rsr.getRequestContentType() == RequestContentType.MULTIPART) {
			parseMultipartData(rsr, argObject, data);
		}

		//json data
		else if (rsr.getRequestContentType() == RequestContentType.JSON || (rsr.getRequestContentType() == RequestContentType.OTHER && !isFileSource)) { 
			String body = IOUtils.toString(rsr.request.getInputStream());
			data = new JSONObject(StringUtils.isEmpty(body) ? "{}" : body);
		}

		//not multipart but expecting a file?
		else if (isFileSource) {
			Core.storeFileDocumentContent(rsr.getContext(), argObject, rsr.request.getInputStream());
		}

		RestServiceHandler.paramMapToJsonObject(params, data);
		
		//serialize to Mendix Object
		JsonDeserializer.readJsonDataIntoMendixObject(rsr.getContext(), data, argObject, false);
		
		return argObject;
	}

	private void parseMultipartData(RestServiceRequest rsr, IMendixObject argO,
			JSONObject data) throws IOException, FileUploadException {
		boolean hasFile = false;
		
		for(FileItemIterator iter = servletFileUpload.getItemIterator(rsr.request); iter.hasNext();) {
			FileItemStream item = iter.next();
			if (!item.isFormField()){ //This is the file(?!)
				if (!isFileSource) {
					RestServices.LOGPUBLISH.warn("Received request with binary data but input argument isn't a filedocument. Skipping. At: " + rsr.request.getRequestURL().toString());
					continue;
				}
				if (hasFile)
					RestServices.LOGPUBLISH.warn("Received request with multiple files. Only one is supported. At: " + rsr.request.getRequestURL().toString());
				hasFile = true;
				Core.storeFileDocumentContent(rsr.getContext(), argO, determineFileName(item), item.openStream());
			}
			else
				data.put(item.getFieldName(), IOUtils.toString(item.openStream()));
		}
	}

	private String determineFileName(FileItemStream item) {
		return null; //TODO:
	}

	public String getName() {
		return microflowname.split("\\.")[1].toLowerCase();
	}

	public String getRequiredRoleOrMicroflow() {
		return roleOrMicroflow;
	}

	public void serveDescription(RestServiceRequest rsr) {
		rsr.startDoc();

		if (rsr.getResponseContentType() == ResponseType.HTML)
			rsr.write("<h1>Operation: ").write(getRelativeUrl()).write("</h1>");

		DataWriter objectWriter = rsr.datawriter.object()
			.key("name").value(getRelativeUrl())
			.key("description").value(description)
			.key("url").value(RestServices.getAbsoluteUrl(getRelativeUrl()));
		
		if (getHttpMethod() == "GET") {
			objectWriter.key("arguments").value(
				hasArgument
				? JSONSchemaBuilder.build(Utils.getFirstArgumentType(microflowname))
				: null);
		}
		
		objectWriter.key("accepts_binary_data").value(isFileSource);
		
		IDataType returnType = Core.getReturnType(microflowname);
		if (!returnType.isNothing()) {
			objectWriter.key("result").value(isFileTarget
					? RestServices.CONTENTTYPE_OCTET + " stream"
					: JSONSchemaBuilder.build(returnType));
		}
		
		objectWriter.endObject();

		rsr.endDoc();
	}
	
	public String getHttpMethod() {
		return httpMethod == null ? null : httpMethod.toString();
	}

	public static void clearMicroflowServices() {
		while (!microflowServices.isEmpty())
			microflowServices.get(0).unregister();
	}
}