/**
 * 
 */
package com.fp.alfresco.client;

import java.io.InputStream;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;

import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
import net.sf.json.JSONSerializer;

import org.apache.commons.lang.StringEscapeUtils;
import org.apache.http.Consts;
import org.apache.http.HttpEntity;
import org.apache.http.HttpException;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.entity.mime.content.FileBody;
import org.apache.http.entity.mime.content.StringBody;

import com.fp.alfresco.enums.EnumDataType;
import com.fp.alfresco.enums.EnumRequestType;
import com.fp.alfresco.exception.ExceptionWebscript;
import com.fp.alfresco.util.ApiProperties;
import com.fp.alfresco.util.ApiRequest;
import com.fp.alfresco.util.UriUtils;

/**
 * 
 * Clase que proporciona m&eacute;todos comunes para interactuar con el repositorio de Alfresco
 * @author bpt
 *
 */
public class AlfrescoApi {

	/**
	 * Objeto de tipo ApiRequest que realiza las consultas a alfresco
	 */
	private ApiRequest apiRequest;

	/**
	 * Variable que contiene el namespace base del API de alfresco
	 */
	private String baseQuery;
	
	/**
	 * Variable que contiene el nombre del sitio que se consumira en la llamada a Alfresco
	 */
	private String site;
	
	private static final String MIME_TEXT = ApiProperties.getProperty("api.common.mime.type");
	
	private static final String PREFIX_METADATA = ApiProperties.getProperty("api.name.prefix.metadata");

	/**
	 * Constructor
	 */
	public AlfrescoApi(String site){
		this.apiRequest = new ApiRequest();
		this.site = site;
		this.baseQuery = ApiProperties.getProperty("api.site.defaultquery", site);
	}

	/**
	 * M&eacute;todo principal para la busqueda de documentos
	 * @param query
	 * @param ticket
	 * @return
	 * @throws Exception
	 */
	public List<DocumentoAlfresco> findDocuments(String query) throws Exception{
		String uri = ApiProperties.getProperty("api.uri.search.general")+UriUtils.formatearParametrosUrl("query=" + this.baseQuery+ " AND " + query);

		String response = this.apiRequest.authRequest(EnumRequestType.GET, uri).toString();
		return this.transformeResponse(response);
	}

	/**
	 * Retorna una lista de {@link DocumentoAlfresco} por xpath
	 * @param xPathLocation la ruta xpath de una carpeta
	 * @return
	 * @throws Exception
	 */
	private List<DocumentoAlfresco> findByXPathLocation(String xPathLocation) throws Exception{
		String uri = ApiProperties.getProperty("api.uri.search.general")+UriUtils.formatearParametrosUrl("query=PATH:\"" + xPathLocation);
		String response = this.apiRequest.authRequest(EnumRequestType.GET, uri).toString();
		return this.transformeResponse(response);
	}

	/**
	 * M&eacute;todo que busca por id del documento
	 * @param documentId
	 * @return
	 * @throws Exception
	 */
	public DocumentoAlfresco findById(String documentId) throws Exception
	{
		String query = "ID:\""+ documentId +"\"";
		List<DocumentoAlfresco> l = this.findDocuments(query);
		if(!l.isEmpty()){
			return l.get(0);
		}
		return null;
	}

	/**
	 * M&eacute;todo que busca un documento por contenido
	 * @param documentContent
	 * @return
	 * @throws Exception
	 */
	public List<DocumentoAlfresco> findByContent(String documentContent) throws Exception
	{
		String query = "TEXT:\""+  documentContent+"\"";
		return this.findDocuments(query);
	}

	/**
	 * M&eacute;todo que busca un documento por atributo
	 * @param documentContent
	 * @return
	 * @throws Exception
	 */
	public List<DocumentoAlfresco> findByAttribute(String attributeName, String value) throws Exception
	{
		attributeName = attributeName.replace(":", "\\:");
		String query = "@"+attributeName+":\""+  value+"\"";
		return this.findDocuments(query);
	}

	/**
	 * M&eacute;todo que busca un documento por tipo
	 * @param type
	 * @return
	 * @throws Exception
	 */
	public List<DocumentoAlfresco> findByType(String type) throws Exception
	{
		String query = "TYPE:\""+type+"\"";
		return this.findDocuments(query);
	}

	/**
	 * Devuelve una lista de {@link DocumentoAlfresco} por xpath de la carpeta
	 * @param xPathLocationFolder xpath de la carpeta a buscar
	 * @return
	 * @throws Exception
	 */
	public List<DocumentoAlfresco> findListByXPathLocation(String xPathLocationFolder) throws Exception{
		if((xPathLocationFolder==null) || xPathLocationFolder.isEmpty()){
			throw new ExceptionWebscript("Ruta de la carpeta requerido");
		}
		xPathLocationFolder+="/*\" AND TYPE:\"cm:content\"";
		return this.findByXPathLocation(xPathLocationFolder);
	}

	/**
	 * Retorna un {@link DocumentoAlfresco} por direcci&oacute;n xpath
	 * @param xPathLocationDocument direcci&oacute;n xpath del documento
	 * @return
	 * @throws Exception
	 */
	public DocumentoAlfresco findOneByXPathLocation(String xPathLocationDocument) throws Exception{
		if((xPathLocationDocument==null) || xPathLocationDocument.isEmpty()){
			throw new ExceptionWebscript("Ruta del documento requerido");
		}
		xPathLocationDocument+="\"";
		List<DocumentoAlfresco> l = this.findByXPathLocation(xPathLocationDocument);
		if(!l.isEmpty()){
			return l.get(0);
		}
		return null;
	}

	/**
	 * Recibe la respuesta en JSON y retorna una lista de tipo List<DocumentoAlfresco>
	 * @param response
	 * 
	 */
	private List<DocumentoAlfresco> transformeResponse(String response) {
		List<DocumentoAlfresco> l = new ArrayList<DocumentoAlfresco>();
		JSONObject json = (JSONObject) JSONSerializer.toJSON(response);
		Integer resultados = json.getInt("resultados");
		if(resultados>0){
			JSONArray documents = json.getJSONArray("documentos");
			for (Object object : documents) {
				JSONObject jo = (JSONObject) object;
				DocumentoAlfresco d = this.getSingleDocumentResponse(jo);
				l.add(d);
			}
		}
		return l;
	}

	/**
	 * Retorna un {@link DocumentoAlfresco} en base aun {@link JSONObject} enviado
	 * @param jo Objeto json que formar&aacute; el {@link DocumentoAlfresco}
	 * @return
	 */
	@SuppressWarnings("unchecked")
	private DocumentoAlfresco getSingleDocumentResponse(JSONObject jo) {
		Map<String, Object> mprops = jo.getJSONObject("propiedades");
		DocumentoAlfresco d = new DocumentoAlfresco(jo.getString("nombre"), ((Map<String, Object>)mprops.get("cm:modifier")).get("userName").toString());
		d.setId(jo.getString("referencia"));
		d.setNombre(jo.getString("nombre"));
		d.setModificadoPor(((Map<String, Object>)mprops.get("cm:modifier")).get("userName").toString());
		d.setAccesoDescarga(jo.getString("accesoDescarga"));
		d.setxPathLocationPadre(jo.getString("xPathLocationPadre"));
		d.setxPathLocation(jo.getString("xPathLocation"));
		d.setTipo(jo.getString("tipo"));
		d.setPropiedades(new HashMap<String, Object>());
		this.fillPropertiesSearch(d.getPropiedades(), jo.getJSONObject("propiedades"));
		return d;
	}

	/**
	 * M&eacute;todo recursivo que llena las propiedades de un documento en un mapa
	 * @param mprop Mapa para llenar las propiedades de un documento
	 * @param jobject Propiedad de un documento
	 */
	@SuppressWarnings("unchecked")
	private void fillPropertiesSearch(Map<String, Object> mprop, JSONObject jobject) {
		Set<String> ks = jobject.keySet();
		for (String key : ks) {
			Object item = jobject.get(key);
			if(item instanceof JSONObject){
				Map<String, Object> subProp = new HashMap<String, Object>();
				mprop.put(key, subProp);
				this.fillPropertiesSearch(subProp, (JSONObject)item);
			}else{
				mprop.put(key, item.toString());
			}
		}
	}

	/**
	 * M&eacute;todo que arma los par&aacute;metros en base al documento
	 * @param documento
	 * @return
	 * @throws Exception
	 */
	private String armarParametrosPropiedades(DocumentoAlfresco documento) throws Exception {
		TimeZone tz = TimeZone.getTimeZone("GMT-05:00");
		DateFormat iso8601 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
		iso8601.setTimeZone(tz);
		StringBuffer cadena = new StringBuffer("{");
		Map<String, Map<String, Object>> map = documento.getPropiedadesToSet();
		if(map==null){
			cadena.append("}");
			return cadena.toString();
		}
		boolean existAttrs = false;
		Set<String> keys = map.keySet();
		for (String key : keys) {
			if(key.startsWith(PREFIX_METADATA)){
				existAttrs = true;
				Map<String, Object> prop = map.get(key);
				String propValue = null;
				Object type = prop.get("type");
				Object value = prop.get("value");
				if(!(type instanceof EnumDataType)){
					throw new ExceptionWebscript("El campo '"+key+"' debe tener el atributo 'type' de tipo [EnumDataType]");
				}
				if(value!=null){
					if(type==EnumDataType.STRING){
						if(!(value instanceof String)) {
							throw new ExceptionWebscript("El campo '"+key+"' debe tener el atributo 'value' de tipo [String]");
						}
						propValue = StringEscapeUtils.escapeJavaScript(value.toString());
						
					}else if(type==EnumDataType.DATE){
						if(!(value instanceof Date)) {
							throw new ExceptionWebscript("El campo '"+key+"' debe tener el atributo 'value' de tipo [Date]");
						}
						propValue = iso8601.format(value);
						propValue = propValue.substring(0,22)+":00";
					}else if(type==EnumDataType.NUMBER){
						if(!(value instanceof Number)) {
							throw new ExceptionWebscript("El campo '"+key+"' debe tener el atributo 'value' de tipo [Number]");
						}
						propValue = value.toString();
					}
				}
				if((propValue!=null) && !propValue.isEmpty()) {
					cadena.append("\""+key+"\":")
					.append("{")
					.append("\"value\":").append("\""+propValue+"\"")
					.append(",")
					.append("\"type\":").append("\""+((EnumDataType)type).getValue()+"\"")
					.append("},");
				}
			}
		}
		if(existAttrs) {
			cadena.deleteCharAt(cadena.length()-1);
		}
		cadena.append("}");
		return cadena.toString();
	}

	/**
	 * Se obtiene un documento en base a su id
	 * @param documentId id del documento
	 * @return
	 * @throws Exception
	 */
	public InputStream downloadDocumentById(String documentId) throws Exception{
		String uriDownload = ApiProperties.getProperty("api.uri.download.id");
		uriDownload+=documentId.replace("://", "/");
		return (InputStream) this.apiRequest.authRequest(EnumRequestType.GET, uriDownload, true, true);
	}

	/**
	 * Se obtiene un documento en base a su xpath
	 * @param xPathLocation ruta de la direcci&oacute;n en donde se encuentra el documento
	 * @param includeSite bandera si se debe especificar el sitio
	 * @return
	 * @throws Exception
	 */
	public InputStream downloadDocumentByXPathLocation(String xPathLocation, boolean includeSite) throws Exception{
		if(includeSite){
			xPathLocation = xPathLocation.replace("{0}", this.site);
		}
		DocumentoAlfresco docAlfresco = findOneByXPathLocation(xPathLocation);
		if(docAlfresco == null){
			return null;
		}
//		xPathLocation = xPathLocation.replace("/app:", "").replace("/st:", "/").replace("/cm:", "/");
//		String uriDownload = ApiProperties.getProperty("api.uri.download.path", xPathLocation);
//		(InputStream) this.apiRequest.authRequest(EnumRequestType.GET, uriDownload, true, true)
		return downloadDocumentById(docAlfresco.getId());
	}

	public InputStream downloadDocumentByXPathLocation(String xPathLocation) throws Exception{
		return this.downloadDocumentByXPathLocation(xPathLocation, true);
	}

	/**
	 * M&eacute;todo que carga un documento en una ruta especificada
	 * @param location
	 * @param document
	 * @param isUpdate
	 * @return
	 * @throws Exception
	 */
	private DocumentoAlfresco uploadDocument(String location, DocumentoAlfresco document, boolean isUpdate) throws Exception {
		if(document.getFile()==null){
			throw new Exception("Debe fijar un archivo");
		}
		String ubicacion = null;
		if(isUpdate){
			ubicacion = location;
		}else{
			ubicacion = ApiProperties.getProperty("api.doclocation.base", this.site)+"/"+ location;
		}
		String uri = ApiProperties.getProperty("api.uri.create.general")+
				UriUtils.formatearParametrosUrl(
						"tipo=" +document.getTipo()
						+"&ubicacion=" +ubicacion
						+"&modificadoPor="+document.getModificadoPor()
						+"&propiedades="+this.armarParametrosPropiedades(document)
						);

		FileBody bin = new FileBody(document.getFile());
		HttpEntity entityHttp = null;
		if(isUpdate){
			entityHttp = MultipartEntityBuilder.create()
					.addPart("archivo", bin)
					.addPart("filename", new StringBody(document.getNombre(), ContentType.create(MIME_TEXT, Consts.ISO_8859_1)))
					.addPart("xPathUpdateDocument", new StringBody(document.getxPathLocation(), ContentType.create(MIME_TEXT, Consts.ISO_8859_1)))
					.build();
		}else{
			entityHttp = MultipartEntityBuilder.create()
					.addPart("archivo", bin)
					.addPart("filename", new StringBody(document.getNombre(), ContentType.create(MIME_TEXT, Consts.ISO_8859_1)))
					.build();
		}
		try {
			String response = this.apiRequest.authRequest(EnumRequestType.POST, uri, entityHttp).toString();
			JSONObject jo = (JSONObject) JSONSerializer.toJSON(response);
			return this.getSingleDocumentResponse(jo);
		} catch (HttpException e) {
			throw new ExceptionWebscript("Error al conectar con el servidor de alfresco", e);
		}
	}

	/**
	 * M&eacute;todo que carga un nuevo documento en la ruta ingresada
	 * @param partialxPathLocation ruta xpath en donde se almacenar&aacute; el documento
	 * @param document
	 * @return
	 * @throws Exception
	 */
	public DocumentoAlfresco uploadNewDocument(String partialxPathLocation, DocumentoAlfresco document) throws Exception {
		return this.uploadDocument(partialxPathLocation, document, false);
	}

	/**
	 * Para actualizar el documento primero lo consulto
	 * @param document
	 * @return
	 * @throws Exception
	 */
	public DocumentoAlfresco uploadUpdateDocument(DocumentoAlfresco document) throws Exception {
		return this.uploadDocument(document.getxPathLocation(), document, true);
	}
	/**
	 * Elimina un documento dado un nodeRef.
	 * @param nodeRef Identificador &uacute;nico del documento o carpeta que se desea eliminar
	 * @return <code>Boolean</code> indicando si se realiz&oacute; el proceso de eliminaci&oacute;n de forma correcta.
	 * @throws Exception Se ejecuta una excepci&oacute;n cuando no se encuentra el documento en el repositorio Alfresco
	 */
	public boolean deleteDocument(String nodeRef) throws Exception {
		StringBuilder urlDescarga = new StringBuilder(ApiProperties.getProperty("api.uri.delete.general"));
		urlDescarga.append(nodeRef.replace("://", "/"));
		Object response = this.apiRequest.authRequest(EnumRequestType.DELETE, urlDescarga.toString(), false, true);
		JSONObject jo = (JSONObject) JSONSerializer.toJSON(response);
		return Boolean.parseBoolean(jo.get("overallSuccess").toString());
	}

	public String getSite() {
		return site;
	}

}
