package eu.dnetlib.enabling.aas.service;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.annotation.Resource;
import javax.jws.WebService;
import javax.xml.namespace.QName;
import javax.xml.ws.WebServiceContext;
import javax.xml.ws.handler.MessageContext;

import org.apache.cxf.headers.Header;
import org.apache.cxf.helpers.CastUtils;
import org.apache.cxf.helpers.DOMUtils;
import org.apache.cxf.jaxws.context.WrappedMessageContext;
import org.apache.cxf.message.Message;
import org.apache.log4j.Logger;
import org.bouncycastle.util.encoders.Base64;
import org.w3c.dom.Document;
import org.w3c.dom.Element;


import com.sun.xacml.ctx.ResponseCtx;

import eu.dnetlib.enabling.aas.DriverPDP;
import eu.dnetlib.enabling.aas.ctx.ISecurityContextContainer;
import eu.dnetlib.enabling.aas.ctx.SecCtxInfoDTO;
import eu.dnetlib.enabling.aas.ctx.SecurityContext;
import eu.dnetlib.enabling.aas.ctx.SecurityContextContainerException;
import eu.dnetlib.enabling.aas.ctx.SecurityContextUtils;
import eu.dnetlib.enabling.aas.ctx.tools.DecodingResponse;
import eu.dnetlib.enabling.aas.ctx.tools.ICtxRecoder;
import eu.dnetlib.enabling.aas.nh.rmi.INotificationHandler;
import eu.dnetlib.enabling.aas.rmi.A2Error;
import eu.dnetlib.enabling.aas.rmi.A2Service;
import eu.dnetlib.enabling.aas.rmi.AuthenticateRequest;
import eu.dnetlib.enabling.aas.rmi.AuthenticateResp;
import eu.dnetlib.enabling.aas.rmi.AuthorizeRequest;
import eu.dnetlib.enabling.aas.rmi.AuthorizeResp;
import eu.dnetlib.enabling.aas.rmi.InvalidateRequest;
import eu.dnetlib.enabling.aas.rmi.InvalidateResp;
import eu.dnetlib.enabling.aas.rmi.TypedString;
import eu.dnetlib.enabling.aas.utils.CommonUtils;
import eu.dnetlib.enabling.aas.utils.DriverConverter;
import eu.dnetlib.enabling.aas.utils.DriverThreadLocal;
import eu.dnetlib.enabling.aas.validator.rmi.IProfileValidator;

/**
 * Authorization and authentication service.
 * @author mhorst
 *
 */
@WebService(serviceName="A2Service",
			endpointInterface="eu.dnetlib.enabling.aas.rmi.A2Service")
public class A2ServiceImpl implements A2Service {

	/**
	 * static container for passing secCtxId chains from filter.
	 * Requires attention. After performing every request the value from threadLocal needs to be removed.
	 * Its because of ThreadPool and reusability of threads.
	 */
	public static final DriverThreadLocal threadLocal = new DriverThreadLocal(); 
	
	INotificationHandler notificationHandler;
	IProfileValidator profileValidator;
	DriverPDP driverPDP;
	ISecurityContextContainer securityContextContainer;
	ICtxRecoder ctxRecoder;
	
	/**
	 * Flag indicating if HTTP header should be additionally used for setting secCtxId
	 */
	private boolean useHTTPHeader = false;
	
	protected static final Logger log = Logger.getLogger(A2ServiceImpl.class);
	
	private String aasVersion;
	
	/**
	 * CXF webservice context.
	 */
	@Resource
	private WebServiceContext webServiceContext;
	
	/* (non-Javadoc)
	 * @see eu.dnetlib.enabling.aas.service.A2Service#authenticate(eu.dnetlib.enabling.aas.service.AuthenticateRequest)
	 */
	@SuppressWarnings("unchecked")
	public AuthenticateResp authenticate(AuthenticateRequest request) {
		AuthenticateResp authResponse = null;
		SecCtxInfoDTO secCtxInfoDTO = null;
		String secCtxId = null;
		try {
			Object[] authnResult = DriverConverter.convertAuthenticateResponse(driverPDP.evaluate(request));
			if (authnResult==null || authnResult[0]==null) {
//				no authnResponse created
				log.error("Unsupported A2Request! No authnResponse created!");
				AuthenticateResp response = new AuthenticateResp();
				response.addError(new A2Error(A2Error.EVALUATION_ERROR,"Invalid authentication request!"));
				response.addError(new A2Error(A2Error.DECISION_INDETERMINATE));
				return response;
			}

			authResponse = (AuthenticateResp) authnResult[0];
			if (authnResult[1]!=null)
				secCtxInfoDTO = (SecCtxInfoDTO) authnResult[1];

			if (secCtxInfoDTO!=null && secCtxInfoDTO.getResourceId()!=null) {
				Map<String, Object> properties = new HashMap<String, Object>();
				if (authResponse.getIdentities() != null)
					properties.put(ISecurityContextContainer.IDENTITIES_PROPERTY_KEY, authResponse.getIdentities());
				if (authResponse.getObligations() != null)
					properties.put(ISecurityContextContainer.OBLIGATIONS_PROPERTY_KEY, authResponse.getObligations());
				if (secCtxInfoDTO.getSecCtxType()!=null && secCtxInfoDTO.getSecCtxType().length()>0)
					properties.put(ISecurityContextContainer.SECCTX_TYPE_PROPERTY_KEY, secCtxInfoDTO.getSecCtxType());
				
				SecurityContext secCtx = null;
				try {
					secCtx = securityContextContainer.createContext(secCtxInfoDTO.getResourceId(), properties);
				} catch (SecurityContextContainerException e) {
					log.error("Exception occured when creating new SecurityContext for resource '"+secCtxInfoDTO.getResourceId()+"'!",e);
					authResponse.addError(new A2Error(A2Error.SYSTEM_ERROR,"Error occured when creating new SecurityContext.",e));
				}
				if (secCtx!=null) {
					secCtxId = secCtx.getSecCtxId();
					String privKey = new String(Base64.encode(secCtx.getPrivKey()));
					String publKey = new String(Base64.encode(secCtx.getPubKey()));
					TypedString[] keys = new TypedString[2];
					keys[0] = new TypedString(privKey,A2Constants.IDENTITY_PRIVATE_KEY);
					keys[1] = new TypedString(publKey,A2Constants.IDENTITY_PUBLIC_KEY);
//						adding publ/priv keys to the response.identities
					authResponse.setIdentities(CommonUtils.joinTypedStringTables(authResponse.getIdentities(), keys));
				}
			} else {
				log.info("No obligation with defined resourceId for creating SecurityContext found!");
			}
			return authResponse;
			
		} catch (A2Exception e) {
			log.error("Unsupported A2Request!",e);
			AuthenticateResp response = new AuthenticateResp();
			response.addError(new A2Error(A2Error.EVALUATION_ERROR,"Invalid authentication request!",e));
			response.addError(new A2Error(A2Error.DECISION_INDETERMINATE));
			return response;
		} catch (Exception e) {
			log.error("got unknown exception", e);
			AuthenticateResp response = new AuthenticateResp();
			response.addError(new A2Error(A2Error.UNKNOWN_ERROR,"unknown exception",e));
			return response;
		} finally {
			if (useHTTPHeader)
				threadLocal.set(secCtxId);
			else
				threadLocal.remove();
			setSecCtxIdInSOAPHeader(secCtxId);
		}
	}
	
	/* (non-Javadoc)
	 * @see eu.dnetlib.enabling.aas.service.A2Service#authorize(eu.dnetlib.enabling.aas.service.AuthorizeRequest)
	 */
	public AuthorizeResp authorize(AuthorizeRequest request) {
		ResponseCtx responseCtx = null;
		try {
			String secCtxChain = getSecCtxIdFromHeader();
			log.info("raw SecCtxId value: "+secCtxChain);
			String[] tokenizedSecCtxChain = null;
			try {
				tokenizedSecCtxChain = SecurityContextUtils.tokenizeSecCtxChain(secCtxChain);
			} catch (A2Exception e) {
				log.error("Invalid secCtxChain!",e);
				AuthorizeResp response = new AuthorizeResp();
				response.setAuthorized(false);
				response.addError(new A2Error(A2Error.CONTEXT_ID_INVALID,"Invalid secCtxChain!",e));
				return response;
			}
			String parsedSecCtxChain = null;
			if (tokenizedSecCtxChain!=null && tokenizedSecCtxChain[0]!=null) {
//				checking secCtxChain encoder version
				if (!ctxRecoder.identify().equals(tokenizedSecCtxChain[0])) {
					log.error("Unsupported secCtxChain encoder version! Found: "+
							tokenizedSecCtxChain[0]+", supported: "+ctxRecoder.identify());
					AuthorizeResp response = new AuthorizeResp();
					response.setAuthorized(false);
					response.addError(new A2Error(A2Error.CONTEXT_ID_INVALID,"Unsupported secCtxChain encoder version! Found: "+
							tokenizedSecCtxChain[0]+", supported: "+ctxRecoder.identify()));
					return response;
				} else {
//					parsing secCtxChain with valid decoder
					String[] resolvedSecCtxIds = resolveIds(tokenizedSecCtxChain[1]);
					parsedSecCtxChain = SecurityContextUtils.encodeSecCtxs(resolvedSecCtxIds);
				}
			}
			log.info("processed SecCtxId value: "+parsedSecCtxChain);
			if (request!=null) {
				if (parsedSecCtxChain!=null)
					request.setContextId((String) parsedSecCtxChain);
				else {
//					overwrites secCtxId in request if set by mistake
					request.setContextId(null);
				}
			}
			responseCtx = driverPDP.evaluate(request);
			return DriverConverter.convertAuthorizeResponse(responseCtx);
		} catch (A2Exception e) {
			log.error("Unsupported A2Request!",e);
			AuthorizeResp response = new AuthorizeResp();
			response.setAuthorized(false);
			response.addError(new A2Error(A2Error.EVALUATION_ERROR,"Invalid authorization request!",e));
			return response;
		} catch (Exception e) {
			log.error("got unknown exception", e);
			AuthorizeResp response = new AuthorizeResp();
			response.setAuthorized(false);
			response.addError(new A2Error(A2Error.UNKNOWN_ERROR,"unknown exception",e));
			return response;
		} finally {
//			just in case...
			threadLocal.remove();
		}
		
	}
	
	/**
	 * TODO change method implementation when changing ctxRecoder implementation.
	 * <p>
	 * Parses contextIdChain and returns String[] with secCtx identifiers.
	 * @param contextIdChain
	 * @return String[] ids
	 */
	String[] resolveIds(String contextIdChain) {
		if (contextIdChain==null)
			return null;
		List<String> secCtxIds = new ArrayList<String>();
		String currentChain = contextIdChain;
		while (currentChain!=null) {
			DecodingResponse resp  = ctxRecoder.decode(null, currentChain, null);
			if (resp!=null) {
				currentChain = resp.chain;
				if (resp.secCtxId!=null && resp.secCtxId.length()>0) {
					secCtxIds.add(resp.secCtxId);
				}
			} else
				return secCtxIds.toArray(new String[0]);
		}
		return secCtxIds.toArray(new String[0]);
	}
	
	/* (non-Javadoc)
	 * @see eu.dnetlib.enabling.aas.service.A2Service#invalidate(eu.dnetlib.enabling.aas.service.InvalidateRequest)
	 */
	public InvalidateResp invalidate(InvalidateRequest request) {
		try {
			InvalidateResp response = new InvalidateResp();
			if (request==null || request.getInvalidContextId()==null
					|| request.getInvalidContextId().length()==0) {
				response.addError(new A2Error(A2Error.CONTEXT_ID_NOT_PROVIDED));
				return response;
			}
			SecurityContext deletedSecCtx = null;
			try {
				deletedSecCtx = securityContextContainer.deleteContext(request.getInvalidContextId());
			} catch (SecurityContextContainerException e) {
				log.error("Exception occured when deleting SecurityContext: "+request.getInvalidContextId(),e);
				response.addError(new A2Error(A2Error.UNKNOWN_ERROR, "Exception occured when deleting SecurityContext: "+request.getInvalidContextId(),e));
				return response;
			}
			if (deletedSecCtx==null) {
				response.addError(new A2Error(A2Error.CONTEXT_ID_UNKNOWN));
				return response;
			} else {
	//			TODO set obligations in InvalidateResp (directly from SecCtx?)
				return response;
			}
		} finally {
			threadLocal.remove();
		}
	}

	/**
	 * Returns DriverCtxIdChain header value from SOAP Header (if found) or HTTP Header.
	 * @return
	 */
	private String getSecCtxIdFromHeader() {
//		must be always read from threadLocal!
		Object secCtxIdHTTPHeader = threadLocal.get();
		
//		looking for SOAP Header
		try {
			MessageContext ctx = webServiceContext.getMessageContext();
			if (ctx==null) {
				log.error("Couldn't retrieve MessageContext from web service context " +
				"therefore couldn't get SecCtxId from SOAPHeader!");
				log.error("Returning value from HTTP Header (if any).");
				if (secCtxIdHTTPHeader!=null)
					return (String) secCtxIdHTTPHeader;
				else 
					return null;
			} else {
				List<Header> headers = getHeaders();
				if (headers!=null) {
					for (Header currentHeader : headers) {
						if (A2Constants.HTTP_HEADER_SEC_CTX.equals(
								currentHeader.getName().getLocalPart())) {
//							FIXME probably this will be node
							return extractValueFromElement((Element) currentHeader.getObject());
						}
					}
					
				}
			}
			
//			Element header = currentMessage.getHeader();
//			if (header!=null) {
//				List headerContents = header.getContent();
//				if (headerContents!=null) {
//					Iterator it = headerContents.iterator();
//					while (it.hasNext()) {
//						Object currentContent = it.next();
//						if (currentContent instanceof Element) {
//							Element currentElement = (Element) currentContent;
//							if (currentElement.getName()!=null &&
//									currentElement.getName().equals(A2Constants.HTTP_HEADER_SEC_CTX)) {
//								List secCtxElContents = currentElement.getContent();
//								if (secCtxElContents!=null) {
//									Iterator itSecCtxElContent = secCtxElContents.iterator();
//									while (itSecCtxElContent.hasNext()) {
//										Object currentSecCtxContent = itSecCtxElContent.next();
//										if (currentSecCtxContent instanceof Text) {
//											String value = ((Text)currentSecCtxContent).getValue();
//											if (value!=null)
//												return value.trim();
//										}
//									}
//								}
//							}
//						}
//					}
//				}
//			}
		} catch(Exception e) {
			log.error("Exception occured when getting SOAP DriverCtxIdChain header!",e);
		}
//		if no DriverCtxIdChain SOAP header found
		if (secCtxIdHTTPHeader!=null)
			return (String) secCtxIdHTTPHeader;
		else 
			return null;
	}
	
	String extractValueFromElement(Element element) {
		if (element==null) {
			return null;
		} else {
			return element.getTextContent();
		}
	}
	
	/**
	 * Sets secCtxId in SOAP Header.
	 * @param secCtxId
	 */
	private void setSecCtxIdInSOAPHeader(String secCtxId) {
		if (secCtxId==null)
			return;
		try {
			List<Header> sourceHeaders = getHeaders();
			if (sourceHeaders!=null) {
				sourceHeaders = new ArrayList<Header>();
			}
			sourceHeaders.add(new Header(new QName(A2Constants.HTTP_HEADER_SEC_CTX), 
					buildSimpleHeaderElement(A2Constants.HTTP_HEADER_SEC_CTX, secCtxId)));
			setHeaders(sourceHeaders);
//			MessageContext ctx = AbstractInvoker.getContext();
//			if (ctx!=null && ctx.getOutMessage()!=null) {
//				List<Text> secCtxElementContents = new ArrayList<Text>();
//				secCtxElementContents.add(new Text(secCtxId));
//				Element secCtxElement = new Element(A2Constants.HTTP_HEADER_SEC_CTX);
//				secCtxElement.setContent(secCtxElementContents);
//				List<Element> contents = new ArrayList<Element>();
//				contents.add(secCtxElement);
//				Element header = new Element("Header");
//				header.setContent(contents);
//				ctx.getOutMessage().setHeader(header);
//			} else {
//				log.error("Couldn't retrieve MessageContext from Abstract Invoker " +
//						"therefore couldn't set SecCtxId in SOAPHeader!");
//			}
		} catch(Exception e){
			log.error("Exception occured when setting SOAP DriverCtxIdChain header!",e);
		}
	}
	
	/**
	 * Returns all headers from SOAP message.
	 * @return all headers from SOAP message
	 */
	private List<Header> getHeaders() {
	    MessageContext messageContext = webServiceContext.getMessageContext();
	    if (messageContext == null || !(messageContext instanceof WrappedMessageContext)) {
	        return null;
	    } else {
	    	Message message = ((WrappedMessageContext) messageContext).getWrappedMessage();
		    List<Header> headers = CastUtils.cast((List<?>) message.get(Header.HEADER_LIST));
		    return headers;
	    }
	}
	
	/**
	 * Sets headers in message context.
	 * @param headers
	 */
	private void setHeaders(List<Header> headers) {
		MessageContext messageContext = webServiceContext.getMessageContext();
	    if (messageContext == null || !(messageContext instanceof WrappedMessageContext)) {
	    	log.error("cannot set SOAP headers, proper message context not found!");
	        return;
	    } else {
	    	Message message = ((WrappedMessageContext) messageContext).getWrappedMessage();
	    	message.put(Header.HEADER_LIST, headers);
	    }
	}
	
	/**
	 * Builds simple SOAP header element for given tag name and text content.
	 * @param tagName
	 * @param textContent
	 * @return simple SOAP header element
	 */
	Element buildSimpleHeaderElement(String tagName, String textContent) {
		Document doc = DOMUtils.createDocument();
		Element elem = doc.createElement(tagName);
		elem.setTextContent(textContent);
		return elem;
	}
	
	/* (non-Javadoc)
	 * @see eu.dnetlib.enabling.aas.nh.rmi.INotificationHandler#notify(java.lang.String, java.lang.String, java.lang.String, java.lang.String)
	 */
	public boolean notify(String subscrId, String topic, String isId, String message) {
		try {
			return notificationHandler.notify(subscrId, topic, isId, message);
		} finally {
			threadLocal.remove();
		}
	}
	
	
	/* (non-Javadoc)
	 * @see eu.dnetlib.enabling.aas.validator.IProfileValidator#validate(java.lang.String, java.lang.String)
	 */
	public boolean validate(String profId, String secProfId) {
		try {
			return profileValidator.validate(profId, secProfId);
		} finally {
			threadLocal.remove();
		}
	}

	/* (non-Javadoc)
	 * @see eu.dnetlib.enabling.aas.service.A2Service#identify()
	 */
	public String identify() {
		return getAasVersion();
	}
	
	/**
	 * Returns driverPDP object.
	 * @return driverPDP object
	 */
	public DriverPDP getDriverPDP() {
		return driverPDP;
	}

	/**
	 * Sets driverPDP object.
	 * @param driverPDP
	 */
	public void setDriverPDP(DriverPDP driverPDP) {
		this.driverPDP = driverPDP;
	}

	/**
	 * Returns SecurityContext container.
	 * @return
	 */
	public ISecurityContextContainer getSecurityContextContainer() {
		return securityContextContainer;
	}

	/**
	 * Sets SecurityContext container.
	 * @param securityContextContainer
	 */
	public void setSecurityContextContainer(
			ISecurityContextContainer securityContextContainer) {
		this.securityContextContainer = securityContextContainer;
	}

	/**
	 * Returns AAS notification hander.
	 * @return AAS notification hander
	 */
	public INotificationHandler getNotificationHandler() {
		return notificationHandler;
	}

	/**
	 * Sets AAS notification handler.
	 * @param notificationHandler
	 */
	public void setNotificationHandler(INotificationHandler notificationHandler) {
		this.notificationHandler = notificationHandler;
	}

	/**
	 * Sets http header flag.
	 * @param useHTTPHeader
	 */
	public void setUseHTTPHeader(boolean useHTTPHeader) {
		this.useHTTPHeader = useHTTPHeader;
	}

	/**
	 * Returns profile validator.
	 * @return profile validator
	 */
	public IProfileValidator getProfileValidator() {
		return profileValidator;
	}

	/**
	 * Sets profile validator.
	 * @param profileValidator
	 */
	public void setProfileValidator(IProfileValidator profileValidator) {
		this.profileValidator = profileValidator;
	}

	/**
	 * Returns current aas version.
	 * @return current aas version
	 */
	public String getAasVersion() {
		return aasVersion;
	}

	/**
	 * Sets current aas version.
	 * @param aasVersion
	 */
	public void setAasVersion(String aasVersion) {
		this.aasVersion = aasVersion;
	}

	/**
	 * Returns SecurityContext recoder.
	 * @return SecurityContext recoder
	 */
	public ICtxRecoder getCtxRecoder() {
		return ctxRecoder;
	}

	/**
	 * Sets SecurityContext recoder.
	 * @param ctxRecoder
	 */
	public void setCtxRecoder(ICtxRecoder ctxRecoder) {
		this.ctxRecoder = ctxRecoder;
	}

}
